/* * chart.c - render simple bar / line charts to GIF, using gifenc. * * chart bars out.gif "title" "ylabel" < data * chart lines out.gif "title" "ylabel" < data * * Input: the first line is the series names (space separated); each * remaining line is "label v1 v2 ...". Bar values are plotted on a * logarithmic axis, line values on a linear axis. * * DeepSeek on Obsidian, portable C89. * Author: DeepSeek (deepseek-v4.1-flash), October 2026. */ #include #include #include #include #include "gifenc.h" #include "font.h" #define CW 640 #define CH 420 #define ML 62 /* margins */ #define MR 22 #define MT 58 #define MB 56 #define MAXCAT 32 #define MAXSER 4 enum { C_BG = 0, C_GRID, C_AXIS, C_TEXT, C_S0, C_S1, C_S2, C_S3, C_S4 }; static unsigned char canvas[CW * CH]; static void setpal(unsigned char *pal) { int i; static const unsigned char base[16][3] = { { 16, 19, 31 }, { 42, 50, 78 }, { 120, 136, 170 }, { 224, 230, 240 }, { 255, 179, 71 }, { 79, 163, 255 }, { 111, 220, 111 }, { 255, 111, 159 }, { 210, 210, 70 }, { 180, 120, 255 } }; for (i = 0; i < 256; i++) { if (i < 10) { pal[i*3]=base[i][0]; pal[i*3+1]=base[i][1]; pal[i*3+2]=base[i][2]; } else { pal[i*3]=0; pal[i*3+1]=0; pal[i*3+2]=0; } } } static void px(int x, int y, int c) { if (x < 0 || y < 0 || x >= CW || y >= CH) return; canvas[y * CW + x] = (unsigned char)c; } static void fillrect(int x, int y, int w, int h, int c) { int i, j; for (j = 0; j < h; j++) for (i = 0; i < w; i++) px(x + i, y + j, c); } static void hline(int x0, int x1, int y, int c) { int x; if (x0 > x1) { int t = x0; x0 = x1; x1 = t; } for (x = x0; x <= x1; x++) px(x, y, c); } static void vline(int x, int y0, int y1, int c) { int y; if (y0 > y1) { int t = y0; y0 = y1; y1 = t; } for (y = y0; y <= y1; y++) px(x, y, c); } static void line(int x0, int y0, int x1, int y1, int c) { int dx = abs(x1 - x0), dy = abs(y1 - y0); int sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1; int err = dx - dy; for (;;) { px(x0, y0, c); if (x0 == x1 && y0 == y1) break; { int e2 = 2 * err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } } } static int textw(const char *s) { return (int)strlen(s) * (FONT_W + 1); } static void text(int x, int y, const char *s, int c) { int i, r; for (i = 0; s[i]; i++) { int ch = (unsigned char)s[i]; const unsigned char *g; if (ch < 32 || ch > 126) ch = ' '; g = FONT[ch - 32]; for (r = 0; r < FONT_H; r++) { int bits = g[r], col; for (col = 0; col < FONT_W; col++) if (bits & (1 << col)) px(x + i * (FONT_W + 1) + col, y + r, c); } } } static void textc(int cx, int y, const char *s, int c) { text(cx - textw(s) / 2, y, s, c); } /* pretty linear step for ~5 divisions */ static double nice_step(double raw) { double p = pow(10.0, floor(log10(raw))); double f = raw / p; double m = (f <= 1.0) ? 1.0 : (f <= 2.0) ? 2.0 : (f <= 5.0) ? 5.0 : 10.0; return m * p; } static void draw_axes(void) { hline(ML, CW - MR, CH - MB, C_AXIS); vline(ML, MT, CH - MB, C_AXIS); } static void title_and_axis(const char *title, const char *ylabel, int x, int y) { textc(CW / 2, 14, title, C_TEXT); text(x, y, ylabel, C_GRID); } static void legend(const char *const *names, int nser) { int total = 0, i, x; for (i = 0; i < nser; i++) total += textw(names[i]) + 24; x = CW / 2 - total / 2; for (i = 0; i < nser; i++) { fillrect(x, 40, 12, 12, C_S0 + i); text(x + 17, 40, names[i], C_TEXT); x += textw(names[i]) + 24; } } static void render_bars(const char *title, const char *ylabel, const char *const *names, int nser, char labels[][16], double vals[][MAXSER], int ncat) { double vmax = 1.0, logmax; int i, j, gw, bw, gap, basey = CH - MB; double lmin = log10(1.0); for (i = 0; i < ncat; i++) for (j = 0; j < nser; j++) if (vals[i][j] > vmax) vmax = vals[i][j]; logmax = ceil(log10(vmax)); if (logmax < 1.0) logmax = 1.0; /* gridlines at decades */ for (i = 0; (double)i <= logmax + 0.001; i++) { int y = basey - (int)((i - lmin) / (logmax - lmin) * (double)(basey - MT)); char b[16]; if (i > 0) hline(ML + 1, CW - MR, y, C_GRID); sprintf(b, "%d", (int)pow(10.0, i)); text(ML - textw(b) - 4, y - FONT_H / 2, b, C_AXIS); } gw = (CW - MR - ML) / ncat; gap = gw / 8; bw = (gw - gap) / nser; if (bw < 2) bw = 2; for (i = 0; i < ncat; i++) { int gx = ML + i * gw; for (j = 0; j < nser; j++) { double v = vals[i][j]; int yv, bh; if (v < 1.0) v = 1.0; yv = basey - (int)((log10(v) - lmin) / (logmax - lmin) * (double)(basey - MT)); bh = basey - yv; fillrect(gx + gap / 2 + j * bw, yv, bw - 1, bh, C_S0 + j); } textc(gx + gw / 2, basey + 8, labels[i], C_AXIS); } draw_axes(); title_and_axis(title, ylabel, ML - 2, 2); legend(names, nser); } static void render_lines(const char *title, const char *ylabel, const char *const *names, int nser, char labels[][16], double vals[][MAXSER], int ncat) { double vmax = 1.0, step, ytop; int i, j, basey = CH - MB, ploth = basey - MT; int px_[MAXCAT]; for (i = 0; i < ncat; i++) for (j = 0; j < nser; j++) if (vals[i][j] > vmax) vmax = vals[i][j]; step = nice_step(vmax / 5.0); ytop = step * ceil(vmax / step); if (ytop <= 0.0) ytop = 1.0; for (i = 0; (double)i * step <= ytop + 0.001; i++) { double v = i * step; int y = basey - (int)(v / ytop * (double)ploth); char b[16]; if (i > 0) hline(ML + 1, CW - MR, y, C_GRID); if (step < 1.0) sprintf(b, "%.1f", v); else sprintf(b, "%.0f", v); text(ML - textw(b) - 4, y - FONT_H / 2, b, C_AXIS); } for (i = 0; i < ncat; i++) px_[i] = (ncat == 1) ? (ML + (CW - MR - ML) / 2) : ML + i * (CW - MR - ML) / (ncat - 1); for (j = 0; j < nser; j++) { for (i = 0; i < ncat; i++) { double v = vals[i][j]; int y = basey - (int)(v / ytop * (double)ploth); if (i > 0) { int py = basey - (int)(vals[i-1][j] / ytop * (double)ploth); line(px_[i-1], py, px_[i], y, C_S0 + j); } } for (i = 0; i < ncat; i++) { int y = basey - (int)(vals[i][j] / ytop * (double)ploth); fillrect(px_[i] - 2, y - 2, 5, 5, C_S0 + j); } } for (i = 0; i < ncat; i++) textc(px_[i], basey + 8, labels[i], C_AXIS); draw_axes(); title_and_axis(title, ylabel, ML - 2, 2); legend(names, nser); } int main(int argc, char **argv) { const char *type = (argc > 1) ? argv[1] : "bars"; const char *out = (argc > 2) ? argv[2] : "chart.gif"; const char *title = (argc > 4) ? argv[3] : ""; const char *ylabel = (argc > 4) ? argv[4] : ""; char linebuf[512]; char labels[MAXCAT][16]; double vals[MAXCAT][MAXSER]; char namebuf[256]; char *names[MAXSER]; int nser = 0, ncat = 0; FILE *f; unsigned char pal[256 * 3]; int y; if (fgets(namebuf, sizeof namebuf, stdin) == (char *)0) return 2; { char *p = namebuf; while (*p) { while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; if (!*p) break; names[nser++] = p; while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++; if (*p) *p++ = '\0'; if (nser >= MAXSER) break; } } while (fgets(linebuf, sizeof linebuf, stdin) && ncat < MAXCAT) { char *p = linebuf, *s, *e; int len, k = 0; while (*p == ' ' || *p == '\t') p++; s = p; while (*p && *p != ' ' && *p != '\t') p++; len = (int)(p - s); if (len > 15) len = 15; memcpy(labels[ncat], s, (size_t)len); labels[ncat][len] = '\0'; while (*p) { while (*p == ' ' || *p == '\t') p++; if (*p == '\0' || *p == '\n' || *p == '\r') break; vals[ncat][k++] = strtod(p, &e); if (e == p) break; p = e; if (k >= MAXSER) break; } ncat++; } memset(canvas, C_BG, sizeof canvas); if (strcmp(type, "lines") == 0) render_lines(title, ylabel, (const char **)names, nser, labels, vals, ncat); else render_bars(title, ylabel, (const char **)names, nser, labels, vals, ncat); setpal(pal); f = fopen(out, "wb"); if (!f) return 1; { GifWriter g; if (!gif_begin(&g, f, CW, CH, pal)) { fclose(f); return 1; } for (y = 0; y < CH; y++) if (!gif_write_row(&g, &canvas[y * CW])) { fclose(f); return 1; } gif_end(&g); } fclose(f); return 0; }