ccharts 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/ext/ccharts/extconf.rb +20 -0
- data/ext/ccharts/vendor/ccharts.h +3088 -0
- data/ext/ccharts/vendor/ccharts_abi.c +620 -0
- data/ext/ccharts/vendor/ccharts_abi.h +483 -0
- data/lib/ccharts/chart.rb +253 -0
- data/lib/ccharts/color.rb +46 -0
- data/lib/ccharts/ffi.rb +217 -0
- data/lib/ccharts/settings.rb +365 -0
- data/lib/ccharts/version.rb +7 -0
- data/lib/ccharts.rb +50 -0
- metadata +73 -0
|
@@ -0,0 +1,3088 @@
|
|
|
1
|
+
/* Generated by scripts/sync_sources.py from ccharts.h — do not edit.
|
|
2
|
+
* Change the original and re-run the script. */
|
|
3
|
+
/*
|
|
4
|
+
* ccharts.h — single-header OHLC charts, rendered to a string.
|
|
5
|
+
*
|
|
6
|
+
* Turns financial OHLC data into text: line and candlestick charts drawn with
|
|
7
|
+
* Unicode block characters. Nothing is printed and no stream is written to —
|
|
8
|
+
* the chart functions return a malloc'd string, so the result can go to a
|
|
9
|
+
* terminal, a log, an HTML block or a file just as easily.
|
|
10
|
+
*
|
|
11
|
+
* Color is optional. A color left NULL takes the library default (green
|
|
12
|
+
* rising, red falling); a color set to the EMPTY STRING makes that element
|
|
13
|
+
* carry no escape sequence at all, so setting all four yields plain text.
|
|
14
|
+
*
|
|
15
|
+
* ---------------------------------------------------------------------------
|
|
16
|
+
* USAGE
|
|
17
|
+
* ---------------------------------------------------------------------------
|
|
18
|
+
* Define CCHARTS_IMPLEMENTATION in exactly ONE translation unit before the
|
|
19
|
+
* include; every other TU includes the header as-is (declarations only):
|
|
20
|
+
*
|
|
21
|
+
* #define CCHARTS_IMPLEMENTATION
|
|
22
|
+
* #include "ccharts.h"
|
|
23
|
+
*
|
|
24
|
+
* Minimal example:
|
|
25
|
+
*
|
|
26
|
+
* const char* json =
|
|
27
|
+
* "[{\"open\":1,\"high\":2,\"low\":0.5,\"close\":1.5}]";
|
|
28
|
+
* cc_ohlc_t* ohlc = NULL;
|
|
29
|
+
* int size = 0;
|
|
30
|
+
* cc_json_to_ohlc(json, &ohlc, &size);
|
|
31
|
+
*
|
|
32
|
+
* cc_settings_t s = { .rise_color = CC_COLOR_BLUE };
|
|
33
|
+
* char* chart = cc_line_create(ohlc, size, 60, 8, &s);
|
|
34
|
+
* printf("%s\n", chart);
|
|
35
|
+
* free(chart);
|
|
36
|
+
* free(ohlc);
|
|
37
|
+
*
|
|
38
|
+
* ---------------------------------------------------------------------------
|
|
39
|
+
* DATA FLOW
|
|
40
|
+
* ---------------------------------------------------------------------------
|
|
41
|
+
* 1. Parse raw data into a heap-allocated cc_ohlc_t array:
|
|
42
|
+
* - cc_str_to_ohlc() CSV text (open,high,low,close[,timestamp])
|
|
43
|
+
* - cc_json_to_ohlc() fixed-schema JSON (see its doc comment)
|
|
44
|
+
* 2. Optionally override rendering via a cc_settings_t. Unspecified (NULL /
|
|
45
|
+
* 0) fields fall back to defaults; pass NULL to use all defaults.
|
|
46
|
+
* 3. Call cc_line_create() / cc_candle_create() to get a malloc'd string.
|
|
47
|
+
* 4. Print the string, then free it AND the cc_ohlc_t array.
|
|
48
|
+
*
|
|
49
|
+
* ---------------------------------------------------------------------------
|
|
50
|
+
* RENDERING MODEL
|
|
51
|
+
* ---------------------------------------------------------------------------
|
|
52
|
+
* Line charts : each cell is 8 pixels tall via the 1/8 blocks ▁▂▃▄▅▆▇█,
|
|
53
|
+
* so curves are smooth. Width keeps the same semantics.
|
|
54
|
+
* Candle charts : cells are 2 pixels tall (▀/▄/█). A candle draws a solid
|
|
55
|
+
* body between open/close and a thin vertical wick (│)
|
|
56
|
+
* between high/low; when there is room (width >= size) a
|
|
57
|
+
* gap column separates neighboring candles.
|
|
58
|
+
* Both charts : optionally prepend a left price margin (min/max) and
|
|
59
|
+
* append a time footer (first/last timestamp). The time
|
|
60
|
+
* format is chosen automatically from the average interval.
|
|
61
|
+
*
|
|
62
|
+
* MEMORY OWNERSHIP
|
|
63
|
+
* cc_str_to_ohlc / cc_json_to_ohlc allocate the cc_ohlc_t array (free it).
|
|
64
|
+
* cc_line_create / cc_candle_create return a malloc'd string (free it).
|
|
65
|
+
*
|
|
66
|
+
* INPUT CONSTRAINTS
|
|
67
|
+
* - size (number of candles) must be positive for the renderers and the
|
|
68
|
+
* CSV parser; the JSON parser requires at least one object.
|
|
69
|
+
* - width/height are clamped to CC_MAX_DIM cells per side and
|
|
70
|
+
* CC_MAX_CELLS total; invalid dimensions make the chart functions fail
|
|
71
|
+
* cleanly (empty string) instead of attempting a giant allocation.
|
|
72
|
+
* - CSV lines may be arbitrarily long (each line is heap-copied), and at
|
|
73
|
+
* most `size` lines are read.
|
|
74
|
+
* - JSON must match the fixed schema (see cc_json_to_ohlc); the scanner is
|
|
75
|
+
* an iterative mini-parser, so stray substrings in string values cannot
|
|
76
|
+
* cause false key matches.
|
|
77
|
+
* - ISO8601 timestamps may carry a UTC offset: "+HH:MM", "+HHMM", "+HH"
|
|
78
|
+
* or "Z" (a missing offset is treated as UTC). The offset is applied, so
|
|
79
|
+
* the stored epoch is the true instant, not the local wall time.
|
|
80
|
+
*
|
|
81
|
+
* PORTABILITY
|
|
82
|
+
* Plain C89-compatible code (no VLAs, no C99-only features), so it builds
|
|
83
|
+
* with GCC, Clang and MSVC out of the box; on Windows gmtime_s replaces
|
|
84
|
+
* gmtime_r and functions use internal (static) linkage via CC_INLINE.
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
#ifndef CCHARTS_H
|
|
88
|
+
#define CCHARTS_H
|
|
89
|
+
|
|
90
|
+
/* ============================ ANSI color codes ============================ */
|
|
91
|
+
|
|
92
|
+
#define CC_COLOR_RESET "\x1b[0m"
|
|
93
|
+
#define CC_COLOR_BLACK "\x1b[30m"
|
|
94
|
+
#define CC_COLOR_RED "\x1b[31m"
|
|
95
|
+
#define CC_COLOR_GREEN "\x1b[32m"
|
|
96
|
+
#define CC_COLOR_YELLOW "\x1b[33m"
|
|
97
|
+
#define CC_COLOR_BLUE "\x1b[34m"
|
|
98
|
+
#define CC_COLOR_MAGENTA "\x1b[35m"
|
|
99
|
+
#define CC_COLOR_CYAN "\x1b[36m"
|
|
100
|
+
#define CC_COLOR_WHITE "\x1b[37m"
|
|
101
|
+
#define CC_COLOR_BRIGHT_BLACK "\x1b[90m"
|
|
102
|
+
#define CC_COLOR_BRIGHT_RED "\x1b[91m"
|
|
103
|
+
#define CC_COLOR_BRIGHT_GREEN "\x1b[92m"
|
|
104
|
+
#define CC_COLOR_BRIGHT_YELLOW "\x1b[93m"
|
|
105
|
+
#define CC_COLOR_BRIGHT_BLUE "\x1b[94m"
|
|
106
|
+
#define CC_COLOR_BRIGHT_MAGENTA "\x1b[95m"
|
|
107
|
+
#define CC_COLOR_BRIGHT_CYAN "\x1b[96m"
|
|
108
|
+
#define CC_COLOR_BRIGHT_WHITE "\x1b[97m"
|
|
109
|
+
|
|
110
|
+
/* ============================ Block element characters ============================
|
|
111
|
+
* The 1/8..7/8 left-growing bars (▏▎▍▌▋▊▉) and the 1/8 lower bars (▁▂▃▅▆▇)
|
|
112
|
+
* give sub-cell horizontal/vertical resolution. ▀/▄/█ and the thin vertical
|
|
113
|
+
* line │ (used for candle wicks) round out the drawing primitives.
|
|
114
|
+
* ============================================================================ */
|
|
115
|
+
|
|
116
|
+
#define CC_BLOCK_FULL "\u2588"
|
|
117
|
+
#define CC_BLOCK_7_8 "\u2589"
|
|
118
|
+
#define CC_BLOCK_3_4 "\u258A"
|
|
119
|
+
#define CC_BLOCK_5_8 "\u258B"
|
|
120
|
+
#define CC_BLOCK_1_2 "\u258C"
|
|
121
|
+
#define CC_BLOCK_3_8 "\u258D"
|
|
122
|
+
#define CC_BLOCK_1_4 "\u258E"
|
|
123
|
+
#define CC_BLOCK_1_8 "\u258F"
|
|
124
|
+
#define CC_BLOCK_UPPER_HALF "\u2580"
|
|
125
|
+
#define CC_BLOCK_LOWER_HALF "\u2584"
|
|
126
|
+
#define CC_BLOCK_LOWER_1_8 "\u2581"
|
|
127
|
+
#define CC_BLOCK_LOWER_2_8 "\u2582"
|
|
128
|
+
#define CC_BLOCK_LOWER_3_8 "\u2583"
|
|
129
|
+
#define CC_BLOCK_LOWER_5_8 "\u2585"
|
|
130
|
+
#define CC_BLOCK_LOWER_6_8 "\u2586"
|
|
131
|
+
#define CC_BLOCK_LOWER_7_8 "\u2587"
|
|
132
|
+
#define CC_LINE_VERTICAL "\u2502"
|
|
133
|
+
|
|
134
|
+
#include <stdio.h>
|
|
135
|
+
#include <stdlib.h>
|
|
136
|
+
#include <string.h>
|
|
137
|
+
#include <math.h>
|
|
138
|
+
#include <time.h>
|
|
139
|
+
|
|
140
|
+
/* ============================ Portability macros ============================
|
|
141
|
+
* CC_INLINE: MSVC's C mode has no C11 inline-linkage semantics, so on
|
|
142
|
+
* Windows all functions get internal linkage (static). Everywhere else they
|
|
143
|
+
* are static inline, which keeps each translation unit self-contained and
|
|
144
|
+
* avoids any duplicate-symbol or emission problems on any compiler.
|
|
145
|
+
* CC_GMTIME_R: POSIX gmtime_r vs MSVC gmtime_s (note the argument order). */
|
|
146
|
+
#if defined(_MSC_VER)
|
|
147
|
+
#define CC_INLINE static
|
|
148
|
+
#else
|
|
149
|
+
#define CC_INLINE static inline
|
|
150
|
+
#endif
|
|
151
|
+
|
|
152
|
+
#ifdef _WIN32
|
|
153
|
+
#define CC_GMTIME_R(t, tm) (gmtime_s((tm), (t)) == 0)
|
|
154
|
+
#else
|
|
155
|
+
#define CC_GMTIME_R(t, tm) (gmtime_r((t), (tm)) != NULL)
|
|
156
|
+
#endif
|
|
157
|
+
|
|
158
|
+
#if defined(NAN)
|
|
159
|
+
#define CC_NAN ((double)NAN)
|
|
160
|
+
#elif defined(_MSC_VER)
|
|
161
|
+
#define CC_NAN ((double)sqrt(-1.0))
|
|
162
|
+
#else
|
|
163
|
+
#define CC_NAN (0.0 / 0.0)
|
|
164
|
+
#endif
|
|
165
|
+
|
|
166
|
+
/* Chart dimensions are bounded to keep allocations sane and to avoid
|
|
167
|
+
* integer-overflow surprises. A terminal chart needs nowhere near these
|
|
168
|
+
* sizes; dimension checks fail cleanly (empty string / NULL) instead of
|
|
169
|
+
* attempting a giant allocation. */
|
|
170
|
+
#define CC_MAX_DIM 100000 /* max width or height in cells */
|
|
171
|
+
#define CC_MAX_CELLS 1000000 /* max width * height (total cells) */
|
|
172
|
+
|
|
173
|
+
#ifdef __cplusplus
|
|
174
|
+
extern "C" {
|
|
175
|
+
#endif
|
|
176
|
+
|
|
177
|
+
/* ============================ Public API ============================ */
|
|
178
|
+
|
|
179
|
+
typedef struct cc_ohlc cc_ohlc_t;
|
|
180
|
+
|
|
181
|
+
/* Rendering options. Unspecified fields fall back to defaults:
|
|
182
|
+
* - rise_color : color for rising values / candles (default green)
|
|
183
|
+
* - fall_color : color for falling values / candles (default red)
|
|
184
|
+
* - bg_color : background of empty cells (default: none)
|
|
185
|
+
* - area_color : line chart fill below the line (default: none)
|
|
186
|
+
* - single_color: 1 = whole line in one color chosen from overall change,
|
|
187
|
+
* 0 = color each segment by its own direction (default)
|
|
188
|
+
* - show_prices : print max/min price labels in a left margin
|
|
189
|
+
* - show_times : print first/last timestamp footer under the chart
|
|
190
|
+
* Build with a designated initializer, e.g.
|
|
191
|
+
* cc_settings_t s = { .rise_color = CC_COLOR_BLUE };
|
|
192
|
+
* and pass NULL to any chart function for full defaults. */
|
|
193
|
+
typedef struct cc_settings {
|
|
194
|
+
const char* rise_color;
|
|
195
|
+
const char* fall_color;
|
|
196
|
+
const char* bg_color;
|
|
197
|
+
const char* area_color;
|
|
198
|
+
int single_color;
|
|
199
|
+
int show_prices;
|
|
200
|
+
int show_times;
|
|
201
|
+
} cc_settings_t;
|
|
202
|
+
|
|
203
|
+
/* Fills missing fields of `settings` with defaults and returns the result.
|
|
204
|
+
* Never NULL — passing NULL yields a struct with all defaults. Used
|
|
205
|
+
* internally by every chart function; safe to call directly. */
|
|
206
|
+
CC_INLINE cc_settings_t cc_settings_resolve(const cc_settings_t* settings);
|
|
207
|
+
|
|
208
|
+
/* Parses CSV text into a heap-allocated cc_ohlc_t array.
|
|
209
|
+
*
|
|
210
|
+
* Format: one candle per line, fields separated by `val_seperator`:
|
|
211
|
+
* open,high,low,close (timestamp = 0)
|
|
212
|
+
* open,high,low,close,timestamp (ISO8601 or epoch seconds)
|
|
213
|
+
*
|
|
214
|
+
* `size` is the expected number of lines; at most `size` lines are read
|
|
215
|
+
* (extra lines are ignored, not overflowed). Lines may be arbitrarily long.
|
|
216
|
+
* On success returns 0 and stores a calloc'd array in *ohlc (caller frees
|
|
217
|
+
* it). Returns non-zero on invalid arguments or allocation failure. */
|
|
218
|
+
CC_INLINE int cc_str_to_ohlc(const char* data, int size, cc_ohlc_t** ohlc,
|
|
219
|
+
char val_seperator, char line_seperator);
|
|
220
|
+
|
|
221
|
+
/* Parses a fixed-schema JSON document into a heap-allocated cc_ohlc_t array.
|
|
222
|
+
*
|
|
223
|
+
* Expected schema — an array of objects with string timestamps:
|
|
224
|
+
* [{"ts":"2026-07-20T00:00:00+00:00","open":328.75,"high":330.0,
|
|
225
|
+
* "low":323.75,"close":328.0,"volume":46622936}, ...]
|
|
226
|
+
* The "volume" field is ignored; unknown keys are skipped by value. Keys are
|
|
227
|
+
* matched exactly, so a substring inside a string value can never be
|
|
228
|
+
* mistaken for a key. On success returns 0 and stores the number of candles
|
|
229
|
+
* in *size and a calloc'd array in *ohlc (caller frees both). Returns
|
|
230
|
+
* non-zero on malformed input or allocation failure. */
|
|
231
|
+
CC_INLINE int cc_json_to_ohlc(const char* json, cc_ohlc_t** ohlc, int* size);
|
|
232
|
+
|
|
233
|
+
/* Renders a smooth line chart of the close prices.
|
|
234
|
+
* `width`/`height` are the chart plot area in cells, bounded by
|
|
235
|
+
* CC_MAX_DIM / CC_MAX_CELLS. Returns a malloc'd string (caller frees) that
|
|
236
|
+
* may additionally contain a left price margin and a time footer depending
|
|
237
|
+
* on the settings flags; invalid dimensions yield an empty string. */
|
|
238
|
+
CC_INLINE char* cc_line_create(const cc_ohlc_t* data, int size, int width, int height,
|
|
239
|
+
const cc_settings_t* settings);
|
|
240
|
+
|
|
241
|
+
/* Renders a candlestick chart from the OHLC data.
|
|
242
|
+
* `width`/`height` are the chart plot area in cells, bounded by
|
|
243
|
+
* CC_MAX_DIM / CC_MAX_CELLS. When width >= size each candle is a few cells
|
|
244
|
+
* wide with a gap between neighbors; when width < size neighboring candles
|
|
245
|
+
* are aggregated into virtual candles (like the line chart's downsampling).
|
|
246
|
+
* Returns a malloc'd string (caller frees); invalid dimensions yield an
|
|
247
|
+
* empty string. */
|
|
248
|
+
CC_INLINE char* cc_candle_create(const cc_ohlc_t* data, int size, int width, int height,
|
|
249
|
+
const cc_settings_t* settings);
|
|
250
|
+
|
|
251
|
+
/* ============================ Pie charts ============================
|
|
252
|
+
* A pie / donut chart turns (label, value) slices into a disk drawn with
|
|
253
|
+
* block characters. Unlike line/candle there is no OHLC data: each slice is
|
|
254
|
+
* an amount, the pie computes the percentages, and slices are drawn from
|
|
255
|
+
* 12 o'clock going counter-clockwise. Colors come from a fixed deterministic
|
|
256
|
+
* palette (never random, so every render is byte-identical — which is what
|
|
257
|
+
* keeps the conformance goldens stable) unless the settings override it. */
|
|
258
|
+
|
|
259
|
+
/* One pie slice: a label (may be NULL or empty) and a positive value. A slice
|
|
260
|
+
* whose value is not > 0 (zero, negative, or NaN) makes the whole render fail
|
|
261
|
+
* cleanly with an empty string; +inf is rejected upstream (the wrapper and
|
|
262
|
+
* ABI), which keeps this header free of C99-only `isfinite`. */
|
|
263
|
+
typedef struct cc_pie_slice {
|
|
264
|
+
const char* label;
|
|
265
|
+
double value;
|
|
266
|
+
} cc_pie_slice_t;
|
|
267
|
+
|
|
268
|
+
/* Pie/donut rendering options. Unspecified fields fall back to defaults:
|
|
269
|
+
* - bg_color : background of cells outside the disk (default: none)
|
|
270
|
+
* - colors : per-slice palette override — a NULL-terminated array of
|
|
271
|
+
* ANSI color strings — or NULL for the fixed default palette
|
|
272
|
+
* - donut : 1 = hollow center (donut), 0 = filled disk (default).
|
|
273
|
+
* Only consulted when inner_radius_ratio is left unspecified.
|
|
274
|
+
* - show_legend: 1 = print one legend line per slice below the disk
|
|
275
|
+
* - show_pct : 1 = append "(NN%)" to the default legend entries; ignored
|
|
276
|
+
* by the legend_format variants that always show the percent
|
|
277
|
+
* - slice_gap : angular gap between slices, in radians. 0 = slices touch
|
|
278
|
+
* (default); >0 leaves a thin blank gap between neighbors
|
|
279
|
+
* - inner_radius_ratio : donut thickness as a fraction of the outer radius,
|
|
280
|
+
* in [0,1]. 0 = filled disk; (0,1] = hollow center of that
|
|
281
|
+
* radius. NEGATIVE (e.g. -1) = unspecified: the `donut` flag
|
|
282
|
+
* then decides — 0.5 for a donut, 0 for a disk. Values > 1
|
|
283
|
+
* are clamped to 1.
|
|
284
|
+
* - legend_format : CC_PIE_LEGEND_* enum controlling each legend entry's
|
|
285
|
+
* composition (see below); 0 reproduces the original output
|
|
286
|
+
* - start_angle : angle (radians) where slice 0 begins. NEGATIVE =
|
|
287
|
+
* unspecified (default CC_PI/2, i.e. 12 o'clock)
|
|
288
|
+
* - counter_clockwise : 0 = library default direction (counter-clockwise,
|
|
289
|
+
* matching the original output); nonzero = mirrored
|
|
290
|
+
* (clockwise) sweep
|
|
291
|
+
* - center_text : text drawn in the hollow center. Only shown when there is
|
|
292
|
+
* a real hollow (inner_radius_ratio > 0); NULL or "" disables
|
|
293
|
+
* it. Truncated to fit the hole.
|
|
294
|
+
* Build with a designated initializer and pass NULL for full defaults. To
|
|
295
|
+
* keep the library defaults for a partial brace initializer, leave the double
|
|
296
|
+
* fields negative (their zero values are meaningful: inner_radius_ratio=0 is
|
|
297
|
+
* a filled disk, start_angle=0 is 3 o'clock) and set inner_radius_ratio =
|
|
298
|
+
* -1.0 when you want the `donut` flag's classic hollow. */
|
|
299
|
+
typedef struct cc_pie_settings {
|
|
300
|
+
const char* bg_color;
|
|
301
|
+
const char* const* colors;
|
|
302
|
+
int donut;
|
|
303
|
+
int show_legend;
|
|
304
|
+
int show_pct;
|
|
305
|
+
/* --- Fas 3 pie/donut settings (all optional) --- */
|
|
306
|
+
double slice_gap; /* radians; 0 = adjacent slices */
|
|
307
|
+
double inner_radius_ratio; /* [0,1]; <0 = donut flag decides */
|
|
308
|
+
int legend_format; /* CC_PIE_LEGEND_* */
|
|
309
|
+
double start_angle; /* radians; <0 = CC_PI/2 */
|
|
310
|
+
int counter_clockwise; /* 0 = default CCW sweep; nonzero = CW */
|
|
311
|
+
const char* center_text; /* NULL/"" = none; hollow-center label */
|
|
312
|
+
} cc_pie_settings_t;
|
|
313
|
+
|
|
314
|
+
/* legend_format values. All other values fall back to CC_PIE_LEGEND_VALUE
|
|
315
|
+
* (the original "label value (+ (NN%) when show_pct)" behavior). */
|
|
316
|
+
#define CC_PIE_LEGEND_VALUE 0 /* "label value" (+ " (NN%)" when show_pct) */
|
|
317
|
+
#define CC_PIE_LEGEND_LABEL_PCT 1 /* "label NN%" */
|
|
318
|
+
#define CC_PIE_LEGEND_VALUE_PCT 2 /* "value (NN%)" */
|
|
319
|
+
#define CC_PIE_LEGEND_LABEL 3 /* "label" only */
|
|
320
|
+
|
|
321
|
+
/* Renders a pie/donut chart of `count` slices into a `width` x `height` grid
|
|
322
|
+
* (cells) and returns a malloc'd string (caller frees). Slices are normalized
|
|
323
|
+
* to angles; each grid cell is colored by the slice whose angular range
|
|
324
|
+
* contains the cell's center, inside an outer radius (a donut leaves the
|
|
325
|
+
* cells inside the inner radius blank). A legend is appended below the disk
|
|
326
|
+
* when settings->show_legend is set. Returns an empty string for NULL slices,
|
|
327
|
+
* count <= 0, invalid dimensions, or any non-positive/NaN slice value. */
|
|
328
|
+
CC_INLINE char* cc_pie_create(const cc_pie_slice_t* slices, int count,
|
|
329
|
+
int width, int height,
|
|
330
|
+
const cc_pie_settings_t* settings);
|
|
331
|
+
|
|
332
|
+
/* ============================ Histogram ============================
|
|
333
|
+
* A histogram is a frequency distribution of a scalar sample sequence.
|
|
334
|
+
* Unlike line/candle there is no OHLC data: the samples are grouped into
|
|
335
|
+
* equal-width bins across a value window and each bin is drawn as a
|
|
336
|
+
* vertical bar sampled at 8 sub-pixels per cell (cc_lower_eighth bar
|
|
337
|
+
* tops, the same smoothness as the line chart). */
|
|
338
|
+
|
|
339
|
+
/* Histogram rendering options. Unspecified fields fall back to defaults:
|
|
340
|
+
* - rise_color : bar fill color (default green)
|
|
341
|
+
* - bg_color : background of empty cells below a bar (default: none)
|
|
342
|
+
* - bin_count : number of equal-width bins; <= 0 auto-selects (20 bins
|
|
343
|
+
* for >= 40 samples, else 10, trimmed to the width)
|
|
344
|
+
* - min_value : window minimum; NaN = the data minimum
|
|
345
|
+
* - max_value : window maximum; NaN = the data maximum
|
|
346
|
+
* - show_bins : append a value-axis footer row under the chart (window
|
|
347
|
+
* min on the left, window max on the right)
|
|
348
|
+
* - show_prices: print the max-count / min-count labels in an 8-column
|
|
349
|
+
* left margin (a vertical count axis)
|
|
350
|
+
* Build with a designated initializer and pass NULL to any chart function
|
|
351
|
+
* for full defaults. min_value/max_value use NaN as their "auto" sentinel,
|
|
352
|
+
* but a partial {0} initializer (min==max==0.0) is ALSO auto, because a
|
|
353
|
+
* valid explicit window always has min < max; any range where that does
|
|
354
|
+
* not hold (NaN, equal, inverted) falls back to the data min/max so the
|
|
355
|
+
* histogram auto-scales instead of collapsing to a single bar. */
|
|
356
|
+
typedef struct cc_hist_settings {
|
|
357
|
+
const char* rise_color;
|
|
358
|
+
const char* bg_color;
|
|
359
|
+
int bin_count;
|
|
360
|
+
double min_value;
|
|
361
|
+
double max_value;
|
|
362
|
+
int show_bins;
|
|
363
|
+
int show_prices;
|
|
364
|
+
} cc_hist_settings_t;
|
|
365
|
+
|
|
366
|
+
/* Renders a histogram of `count` samples into a `width` x `height` grid
|
|
367
|
+
* (cells) and returns a malloc'd string (caller frees). Samples are counted
|
|
368
|
+
* into equal-width bins across [min_value, max_value] — NaN endpoints
|
|
369
|
+
* auto-select from the data range. Outliers are clamped into the edge bins
|
|
370
|
+
* so a single huge value cannot empty every other bin, and a flat window
|
|
371
|
+
* (min == max) is widened by +-1 so the mapping never divides by zero. Each
|
|
372
|
+
* bar is scaled to the tallest bin and drawn with 8 sub-pixel rows
|
|
373
|
+
* (cc_lower_eighth tops). Returns an empty string for NULL samples,
|
|
374
|
+
* count <= 0, or invalid dimensions. */
|
|
375
|
+
CC_INLINE char* cc_hist_create(const double* samples, int count,
|
|
376
|
+
int width, int height,
|
|
377
|
+
const cc_hist_settings_t* settings);
|
|
378
|
+
|
|
379
|
+
/* ============================ Sparkline ============================
|
|
380
|
+
* A sparkline is an ultra-compact, axis-less trend line drawn from a series
|
|
381
|
+
* of close-like values. Unlike line/candle there is no OHLC data, no price
|
|
382
|
+
* margin and no time footer — just a single (or few-row) line reusing the
|
|
383
|
+
* line chart's 8-sub-pixel-per-cell smoothness (cc_lower_eighth). height is
|
|
384
|
+
* expected small (default 1, up to ~3). */
|
|
385
|
+
|
|
386
|
+
/* Sparkline rendering options. Unspecified fields fall back to defaults:
|
|
387
|
+
* - rise_color : the single line / trend color (default green)
|
|
388
|
+
* - area_color : faint fill under the line, or NULL for none (default)
|
|
389
|
+
* - min_above : reserved sub-pixels at the TOP edge so the line does not
|
|
390
|
+
* touch the top (default 0)
|
|
391
|
+
* - min_below : reserved sub-pixels at the BOTTOM edge (default 0)
|
|
392
|
+
* Build with a designated initializer and pass NULL to any chart function
|
|
393
|
+
* for full defaults. min_above/min_below are plain ints, so a partial {0}
|
|
394
|
+
* brace initializer gets the same defaults as NULL (0 sub-pixels reserved)
|
|
395
|
+
* with no sentinel ambiguity; rise_color/area_color fall back per field. */
|
|
396
|
+
typedef struct cc_spark_settings {
|
|
397
|
+
const char* rise_color;
|
|
398
|
+
const char* area_color;
|
|
399
|
+
int min_above;
|
|
400
|
+
int min_below;
|
|
401
|
+
} cc_spark_settings_t;
|
|
402
|
+
|
|
403
|
+
/* Renders a sparkline of `count` samples into a `width` x `height` grid
|
|
404
|
+
* (cells). Each column averages the samples in its span, exactly like the
|
|
405
|
+
* line chart's downsampling, then maps the average to an 8-sub-pixel row;
|
|
406
|
+
* heights are drawn with cc_lower_eighth tops and rows are assembled
|
|
407
|
+
* top-to-bottom with no margin/footer/legend. An optional area_color fills
|
|
408
|
+
* cells below the line. A flat (all-equal) series is centered exactly like
|
|
409
|
+
* the line chart (no divide-by-zero). Returns a malloc'd string (caller
|
|
410
|
+
* frees); invalid dimensions or NULL samples/count <= 0 yield an empty
|
|
411
|
+
* string. Scratch buffers are heap-allocated and freed on every path. */
|
|
412
|
+
CC_INLINE char* cc_spark_create(const double* samples, int count,
|
|
413
|
+
int width, int height,
|
|
414
|
+
const cc_spark_settings_t* settings);
|
|
415
|
+
|
|
416
|
+
/* ============================= Bar chart =============================
|
|
417
|
+
* A categorical bar chart draws an ordered list of (label, value) pairs as
|
|
418
|
+
* vertical bars growing up from a shared zero baseline. Unlike the pie
|
|
419
|
+
* (which normalizes slices to proportions) and the histogram (which bins raw
|
|
420
|
+
* samples), each bar is drawn at its OWN height in proportion to the largest
|
|
421
|
+
* value. Labels are categorical and are printed, when requested, in a footer
|
|
422
|
+
* row below the plot (one label per output column, truncated to the column
|
|
423
|
+
* width). */
|
|
424
|
+
|
|
425
|
+
/* One bar: a categorical label (may be NULL or empty) and a non-negative
|
|
426
|
+
* height. Values are clamped to zero for the render (a negative value draws
|
|
427
|
+
* that bar at zero height rather than below the axis; a future stage could
|
|
428
|
+
* add a diverging below-baseline variant). Non-finite values are rejected
|
|
429
|
+
* upstream (the wrapper and ABI), which keeps this header free of C99-only
|
|
430
|
+
* `isfinite`. */
|
|
431
|
+
typedef struct cc_bar_item {
|
|
432
|
+
const char* label;
|
|
433
|
+
double value;
|
|
434
|
+
} cc_bar_item_t;
|
|
435
|
+
|
|
436
|
+
/* Bar chart rendering options. Unspecified fields fall back to defaults:
|
|
437
|
+
* - rise_color : bar fill color (default green)
|
|
438
|
+
* - bg_color : background of empty cells above a bar (default: none)
|
|
439
|
+
* - show_labels: 1 = append a footer row under the plot with each column's
|
|
440
|
+
* label, truncated to the column width
|
|
441
|
+
* - show_prices: 1 = prepend an 8-column left value axis with the max bar
|
|
442
|
+
* value at the top and 0 (the baseline) at the bottom
|
|
443
|
+
* Build with a designated initializer and pass NULL to any chart function
|
|
444
|
+
* for full defaults. All fields here are pointers or plain ints, so a
|
|
445
|
+
* partial {0} brace initializer gets exactly the same defaults as NULL (no
|
|
446
|
+
* double sentinels, so there is no brace-init ambiguity — unlike the
|
|
447
|
+
* histogram's NaN window fields). */
|
|
448
|
+
typedef struct cc_bar_settings {
|
|
449
|
+
const char* rise_color;
|
|
450
|
+
const char* bg_color;
|
|
451
|
+
int show_labels;
|
|
452
|
+
int show_prices;
|
|
453
|
+
} cc_bar_settings_t;
|
|
454
|
+
|
|
455
|
+
/* Renders a bar chart of `count` (label, value) pairs into a `width` x
|
|
456
|
+
* `height` grid (cells) and returns a malloc'd string (caller frees). Bars
|
|
457
|
+
* grow up from a zero baseline, scaled so the largest value fills the full
|
|
458
|
+
* height, and are drawn with 8 sub-pixel rows (cc_lower_eighth tops) like the
|
|
459
|
+
* line chart. When width >= count each item maps to one or more columns;
|
|
460
|
+
* when width < count the items are folded deterministically into the columns
|
|
461
|
+
* (long-arithmetic index mapping, mirroring the histogram) and each column is
|
|
462
|
+
* drawn at the MAX of the values in its span. An optional label footer
|
|
463
|
+
* (show_labels) and 8-column value axis (show_prices) are assembled around
|
|
464
|
+
* the grid. Returns a malloc'd string (caller frees); NULL items, count <= 0,
|
|
465
|
+
* or invalid dimensions yield an empty string. Scratch buffers are
|
|
466
|
+
* heap-allocated (no VLAs) and freed on every path. */
|
|
467
|
+
CC_INLINE char* cc_bar_create(const cc_bar_item_t* items, int count,
|
|
468
|
+
int width, int height,
|
|
469
|
+
const cc_bar_settings_t* settings);
|
|
470
|
+
|
|
471
|
+
/* ============================ Stacked bar chart ===========================
|
|
472
|
+
* A stacked bar chart divides each category's total bar into vertical
|
|
473
|
+
* segments, one per series: every category has the same set of series, and
|
|
474
|
+
* its bar height is the SUM of the series' values for that category (a
|
|
475
|
+
* part-of-whole view), unlike the plain bar where each item is its own full
|
|
476
|
+
* height. All series share the same category count. Draws with 8 sub-pixel
|
|
477
|
+
* rows (cc_lower_eighth tops) exactly like the bar chart; each cell is
|
|
478
|
+
* colored by the series that sits on the stack's surface at that cell. */
|
|
479
|
+
|
|
480
|
+
/* One stacked-bar series: a name (may be NULL/empty — used only for
|
|
481
|
+
* documentation; the per-series color comes from the palette) and a `values`
|
|
482
|
+
* array with one entry per category. All series must share the same category
|
|
483
|
+
* count (supplied by settings->cats). Values are clamped to zero for the
|
|
484
|
+
* render (negative entries draw at zero height rather than below the axis);
|
|
485
|
+
* non-finite values are rejected upstream (the wrapper and ABI), which keeps
|
|
486
|
+
* this header free of C99-only `isfinite`. */
|
|
487
|
+
typedef struct cc_stack_series {
|
|
488
|
+
const char* name;
|
|
489
|
+
const double* values;
|
|
490
|
+
} cc_stack_series_t;
|
|
491
|
+
|
|
492
|
+
/* Stacked bar rendering options. Unspecified fields fall back to defaults:
|
|
493
|
+
* - colors : a NULL-terminated array of ANSI color strings used as the
|
|
494
|
+
* per-series palette, or NULL for the fixed default palette
|
|
495
|
+
* - bg_color : background of empty cells above the tallest stack (default:
|
|
496
|
+
* none)
|
|
497
|
+
* - cat_labels: optional array of `cats` category labels printed, when
|
|
498
|
+
* show_labels is set, one per output column in the footer
|
|
499
|
+
* (NULL = no category names)
|
|
500
|
+
* - series : number of series (mirrors the `series_count` argument)
|
|
501
|
+
* - cats : number of categories — the length of every series' values
|
|
502
|
+
* array; REQUIRED (a NULL settings or cats <= 0 yields an
|
|
503
|
+
* empty chart, since the values lengths cannot be derived
|
|
504
|
+
* from the pointers alone)
|
|
505
|
+
* - show_labels: 1 = append a footer row under the plot with each column's
|
|
506
|
+
* category label (from cat_labels), truncated to the column
|
|
507
|
+
* width
|
|
508
|
+
* - show_prices: 1 = prepend an 8-column left value axis with the tallest
|
|
509
|
+
* stack total at the top and 0 (the baseline) at the bottom
|
|
510
|
+
* All fields are pointers or plain ints, so a partial {0} brace initializer
|
|
511
|
+
* gets exactly the same defaults as NULL (no double sentinels, so there is
|
|
512
|
+
* no brace-init ambiguity — unlike the histogram's NaN window fields). */
|
|
513
|
+
typedef struct cc_stack_settings {
|
|
514
|
+
const char* const* colors;
|
|
515
|
+
const char* bg_color;
|
|
516
|
+
const char* const* cat_labels;
|
|
517
|
+
int series;
|
|
518
|
+
int cats;
|
|
519
|
+
int show_labels;
|
|
520
|
+
int show_prices;
|
|
521
|
+
} cc_stack_settings_t;
|
|
522
|
+
|
|
523
|
+
/* Renders a stacked bar chart of `series_count` series, each with `cats`
|
|
524
|
+
* values (settings->cats), into a `width` x `height` grid (cells) and returns
|
|
525
|
+
* a malloc'd string (caller frees). Within every category the series' values
|
|
526
|
+
* are summed into that category's total; each bar (category) is a vertical
|
|
527
|
+
* stack of segments drawn bottom-to-top, one per series, proportional to each
|
|
528
|
+
* series' value / the tallest stack total, and colored by the series via a
|
|
529
|
+
* deterministic palette (or the settings->colors override). When width >=
|
|
530
|
+
* cats each category maps to one or more columns; when width < cats the
|
|
531
|
+
* categories are folded into the columns deterministically (long-arithmetic
|
|
532
|
+
* index mapping, mirroring the bar chart) and each column aggregates its
|
|
533
|
+
* categories by SUM (stacked bars are about totals). A footer of drop and
|
|
534
|
+
* value-axis margin are assembled exactly like the bar chart. Returns a
|
|
535
|
+
* malloc'd string (caller frees); NULL series, series_count <= 0, settings
|
|
536
|
+
* NULL or cats <= 0, or invalid dimensions yield an empty string. Scratch
|
|
537
|
+
* buffers are heap-allocated (no VLAs) and freed on every path. */
|
|
538
|
+
CC_INLINE char* cc_stack_create(const cc_stack_series_t* series, int series_count,
|
|
539
|
+
int width, int height,
|
|
540
|
+
const cc_stack_settings_t* settings);
|
|
541
|
+
|
|
542
|
+
/* ============================= Heatmap chart =============================
|
|
543
|
+
* A heatmap draws a 2-D matrix of scalar values as a grid of colored cells:
|
|
544
|
+
* every value maps to one of a fixed, deterministic ladder of ANSI colors by
|
|
545
|
+
* its position between the matrix minimum and maximum. There is no OHLC
|
|
546
|
+
* data — the whole chart is a grid of values. The height / width arguments
|
|
547
|
+
* are the grid size in cells, and the matrix (rows x cols) is mapped onto
|
|
548
|
+
* that grid (see the downsample / padding rules in cc_heat_create). */
|
|
549
|
+
|
|
550
|
+
/* Heatmap rendering options. Unspecified fields fall back to defaults:
|
|
551
|
+
* - low_color : ANSI color for the minimum value (default bright-black,
|
|
552
|
+
* a dim gray; ladder index 0)
|
|
553
|
+
* - high_color : ANSI color for the maximum value (default bright-white,
|
|
554
|
+
* ladder index RAMP_LEN-1)
|
|
555
|
+
* - mid_color : optional ANSI color that replaces the ladder's middle
|
|
556
|
+
* entry (ladder index 5) for a 3-stop ramp; NULL (default)
|
|
557
|
+
* leaves the fixed middle ramp color in place (2-stop)
|
|
558
|
+
* - bg_color : color of the grid cells that the matrix does not cover
|
|
559
|
+
* (matrix smaller than width/height), or NULL for none
|
|
560
|
+
* - row_labels : optional `rows` labels printed in a left margin when
|
|
561
|
+
* show_labels is set, or NULL (no row margin)
|
|
562
|
+
* - col_labels : optional `cols` labels printed in a footer row under the
|
|
563
|
+
* grid when show_labels is set, or NULL (no footer)
|
|
564
|
+
* - show_labels: 1 = print the row/col label frame around the grid
|
|
565
|
+
* All fields are pointers or plain ints, so a partial {0} brace initializer
|
|
566
|
+
* gets exactly the same defaults as NULL (no double sentinels, so there is
|
|
567
|
+
* no brace-init ambiguity — unlike the histogram's NaN window fields). */
|
|
568
|
+
typedef struct cc_heat_settings {
|
|
569
|
+
const char* low_color;
|
|
570
|
+
const char* high_color;
|
|
571
|
+
const char* mid_color;
|
|
572
|
+
const char* bg_color;
|
|
573
|
+
const char* const* row_labels;
|
|
574
|
+
const char* const* col_labels;
|
|
575
|
+
int show_labels;
|
|
576
|
+
} cc_heat_settings_t;
|
|
577
|
+
|
|
578
|
+
/* The colormap ladder. This is the chart's determinism contract: a value's
|
|
579
|
+
* ratio r in [0,1] ((value - data_min) / (data_max - data_min), 0.0 for an
|
|
580
|
+
* all-equal matrix) selects color index (int)(r * (CC_HEAT_RAMP_LEN - 1)),
|
|
581
|
+
* clamped into range. Index 0 is the low end and index CC_HEAT_RAMP_LEN-1
|
|
582
|
+
* the high end; low_color/high_color substitute those two entries and an
|
|
583
|
+
* optional mid_color substitutes index CC_HEAT_MID_INDEX. The ramp's ANSI
|
|
584
|
+
* strings are fixed, so every binding reproduces the same bytes for the same
|
|
585
|
+
* ratio. The full mapping, by ratio bucket (r in [0,1]):
|
|
586
|
+
*
|
|
587
|
+
* r in [0/9, 1/9) -> index 0 (low_color default bright-black)
|
|
588
|
+
* r in [1/9, 2/9) -> index 1 (blue)
|
|
589
|
+
* r in [2/9, 3/9) -> index 2 (cyan)
|
|
590
|
+
* r in [3/9, 4/9) -> index 3 (bright-cyan)
|
|
591
|
+
* r in [4/9, 5/9) -> index 4 (green)
|
|
592
|
+
* r in [5/9, 6/9) -> index 5 (MID_INDEX; mid_color default yellow)
|
|
593
|
+
* r in [6/9, 7/9) -> index 6 (bright-yellow)
|
|
594
|
+
* r in [7/9, 8/9) -> index 7 (red)
|
|
595
|
+
* r in [8/9, 9/9) -> index 8 (bright-red)
|
|
596
|
+
* r == 1 (exactly) -> index 9 (high_color default bright-white)
|
|
597
|
+
*/
|
|
598
|
+
#define CC_HEAT_RAMP_LEN 10
|
|
599
|
+
#define CC_HEAT_MID_INDEX 5
|
|
600
|
+
|
|
601
|
+
/* Renders a heatmap of a `rows` x `cols` row-major `values` matrix into a
|
|
602
|
+
* `width` x `height` grid (cells) and returns a malloc'd string (caller
|
|
603
|
+
* frees). Each grid cell (gx, gy — gy counting from the TOP row) represents
|
|
604
|
+
* a rectangular block of the matrix. A cell is "covered" when its block is
|
|
605
|
+
* non-empty: if the matrix is LARGER than the grid (rows > height or cols >
|
|
606
|
+
* width) the matrix is deterministically downsampled by BLOCK-AVERAGE (each
|
|
607
|
+
* covered cell renders the mean of the matrix values in its span); if the
|
|
608
|
+
* matrix is SMALLER than or equal to the grid, each matrix element occupies
|
|
609
|
+
* its own cell in the top-left (row 0 / column 0) and the remaining cells
|
|
610
|
+
* are background (bg_color or blank). Covered cells are colored by their
|
|
611
|
+
* block-average value on the fixed ladder above; an all-equal matrix maps
|
|
612
|
+
* every covered cell to ladder index 0 (a single color). Returns a malloc'd
|
|
613
|
+
* string (caller frees); NULL values, rows <= 0, cols <= 0, or invalid
|
|
614
|
+
* dimensions yield an empty string. Scratch buffers are heap-allocated (no
|
|
615
|
+
* VLAs) and freed on every path. */
|
|
616
|
+
CC_INLINE char* cc_heat_create(const double* values, int rows, int cols,
|
|
617
|
+
int width, int height,
|
|
618
|
+
const cc_heat_settings_t* settings);
|
|
619
|
+
|
|
620
|
+
/* ================================ Box plot ===============================
|
|
621
|
+
* A box plot draws a distribution per category: for each category its
|
|
622
|
+
* samples' five-number summary [min, Q1, median, Q3, max] is rendered as a
|
|
623
|
+
* vertical box (the Q1..Q3 interquartile region) with whiskers (the
|
|
624
|
+
* min..max span) and the median drawn as a solid line. The statistics are
|
|
625
|
+
* computed deterministically by the renderer from the raw samples (see the
|
|
626
|
+
* nearest-rank convention in cc_box_create), so every binding reproduces
|
|
627
|
+
* the same bytes for the same samples. There is no OHLC data — the input is
|
|
628
|
+
* one array of samples per category. */
|
|
629
|
+
|
|
630
|
+
/* One category: a label (may be NULL/empty; labels are not printed by the
|
|
631
|
+
* core — they exist for bindings' own footers) and its `samples` array of
|
|
632
|
+
* `n` values. A category with n <= 0 or a NULL samples array makes the
|
|
633
|
+
* WHOLE chart empty, because a box has no well-defined five-number summary
|
|
634
|
+
* without any samples. */
|
|
635
|
+
typedef struct cc_box_category {
|
|
636
|
+
const char* name;
|
|
637
|
+
const double* samples;
|
|
638
|
+
int n;
|
|
639
|
+
} cc_box_category_t;
|
|
640
|
+
|
|
641
|
+
/* Box plot rendering options. Unspecified fields fall back to defaults:
|
|
642
|
+
* - rise_color : ANSI color for the box (Q1..Q3) and the median line
|
|
643
|
+
* (default green)
|
|
644
|
+
* - area_color : ANSI color for the whiskers (the vertical min..max line);
|
|
645
|
+
* the empty string or NULL means the whiskers share
|
|
646
|
+
* rise_color (the default)
|
|
647
|
+
* - bg_color : color of the empty cells above/below each box, or NULL
|
|
648
|
+
* for none
|
|
649
|
+
* - show_prices: 1 = prepend an 8-column value axis with the global max
|
|
650
|
+
* on the top row and the global min on the bottom row
|
|
651
|
+
* All fields are pointers or plain ints, so a partial {0} brace initializer
|
|
652
|
+
* gets exactly the same defaults as NULL (no double sentinels, so there is
|
|
653
|
+
* no brace-init ambiguity — unlike the histogram's NaN window fields). */
|
|
654
|
+
typedef struct cc_box_settings {
|
|
655
|
+
const char* rise_color;
|
|
656
|
+
const char* area_color;
|
|
657
|
+
const char* bg_color;
|
|
658
|
+
int show_prices;
|
|
659
|
+
} cc_box_settings_t;
|
|
660
|
+
|
|
661
|
+
/* Renders a box plot of `cat_count` categories into a `width` x `height`
|
|
662
|
+
* grid and returns a malloc'd string (caller frees).
|
|
663
|
+
*
|
|
664
|
+
* QUARTILE CONVENTION (nearest-rank). For each category's n sorted samples
|
|
665
|
+
* s[0..n-1]:
|
|
666
|
+
* min = s[0]
|
|
667
|
+
* Q1 = s[floor((n-1) * 0.25)]
|
|
668
|
+
* median = s[floor((n-1) * 0.50)]
|
|
669
|
+
* Q3 = s[floor((n-1) * 0.75)]
|
|
670
|
+
* max = s[n-1]
|
|
671
|
+
* This is the nearest-rank method: a quartile is the value of the element
|
|
672
|
+
* at the rounded-down rank, with no interpolation between elements. For
|
|
673
|
+
* n == 1 all five collapse to the single sample.
|
|
674
|
+
*
|
|
675
|
+
* VALUE AXIS. The global minimum and maximum across every category's samples
|
|
676
|
+
* define the value span. Each value v maps to a sub-pixel row — there are 8
|
|
677
|
+
* sub-pixels per cell, height * 8 total — by
|
|
678
|
+
* level(v) = (gmax == gmin) ? (pixel_height / 2)
|
|
679
|
+
* : floor((v - gmin) / (gmax - gmin) *
|
|
680
|
+
* (pixel_height - 1))
|
|
681
|
+
* so the global min sits on the bottom sub-pixel (row 0) and the global max
|
|
682
|
+
* on the top sub-pixel (row pixel_height - 1). Cell row r (from the bottom)
|
|
683
|
+
* is level/8.
|
|
684
|
+
*
|
|
685
|
+
* COLUMN MAPPING. Output column w (0..width-1) draws the single category
|
|
686
|
+
* cs = floor(w * cat_count / width)
|
|
687
|
+
* — the FIRST category in that column's long-arithmetic span. When
|
|
688
|
+
* width >= cat_count a category whose span covers several columns is drawn
|
|
689
|
+
* in each of them (repeated); when width < cat_count some categories are
|
|
690
|
+
* skipped so every column still maps to exactly one category, always
|
|
691
|
+
* deterministically.
|
|
692
|
+
*
|
|
693
|
+
* GLYPH MAPPING (per column, bottom row 0 .. top row height-1). Let lo =
|
|
694
|
+
* level(min)/8, q1 = level(Q1)/8, md = level(median)/8, q3 = level(Q3)/8,
|
|
695
|
+
* hi = level(max)/8 and LQ1 = level(Q1), LQ3 = level(Q3). A cell row r is
|
|
696
|
+
* drawn with the first matching rule:
|
|
697
|
+
* 1. Median line (r == md): full block `█` in rise_color
|
|
698
|
+
* — always the full block, even when the median aliases the Q3 top
|
|
699
|
+
* edge, so the median is a solid horizontal line (the emphasis)
|
|
700
|
+
* 2. Box interior (q1 < r < q3): full block `█` in rise_color
|
|
701
|
+
* 3. Box top edge (r == q3): `cc_lower_eighth(h)` in
|
|
702
|
+
* rise_color, h = (LQ3 % 8) + 1 — the Q3 sub-pixel, so the top edge is
|
|
703
|
+
* sub-pixel precise (h == 8 renders a full block)
|
|
704
|
+
* 4. Box bottom edge (r == q1, q1 < q3): full block `█` in rise_color
|
|
705
|
+
* — the Q1 sub-pixel floor rounds down to a full cell; when q1 == q3
|
|
706
|
+
* (box within one row) it is `cc_lower_eighth(LQ3 - LQ1 + 1)`
|
|
707
|
+
* 5. Whisker (lo <= r <= hi, not a box row): vertical `│`
|
|
708
|
+
* (CC_LINE_VERTICAL) in area_color (default rise_color)
|
|
709
|
+
* 6. Background: space, or bg_color
|
|
710
|
+
* Partial top edges reuse cc_lower_eighth; the whiskers reach from the
|
|
711
|
+
* global-min row to the global-max row.
|
|
712
|
+
*
|
|
713
|
+
* Returns a malloc'd string (caller frees); NULL cats, cat_count <= 0, any
|
|
714
|
+
* category with n <= 0 / NULL samples, or invalid dimensions yield an empty
|
|
715
|
+
* string. Scratch buffers are heap-allocated (no VLAs) and freed on every
|
|
716
|
+
* path. NaN/inf samples are rejected upstream (wrapper/ABI), so this header
|
|
717
|
+
* stays free of C99-only isfinite. */
|
|
718
|
+
CC_INLINE char* cc_box_create(const cc_box_category_t* cats, int cat_count,
|
|
719
|
+
int width, int height,
|
|
720
|
+
const cc_box_settings_t* settings);
|
|
721
|
+
|
|
722
|
+
#ifdef __cplusplus
|
|
723
|
+
}
|
|
724
|
+
#endif
|
|
725
|
+
|
|
726
|
+
#ifdef CCHARTS_IMPLEMENTATION
|
|
727
|
+
|
|
728
|
+
/* ============================ Public constants ============================ */
|
|
729
|
+
/* (CC_MAX_DIM / CC_MAX_CELLS are defined above, outside the implementation
|
|
730
|
+
* block, so the Python wrapper and other clients can also use them.) */
|
|
731
|
+
|
|
732
|
+
/* One candlestick. `timestamp` is epoch seconds, 0 when unknown. */
|
|
733
|
+
struct cc_ohlc {
|
|
734
|
+
double open;
|
|
735
|
+
double high;
|
|
736
|
+
double low;
|
|
737
|
+
double close;
|
|
738
|
+
long long timestamp;
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
CC_INLINE cc_settings_t cc_settings_resolve(const cc_settings_t* settings) {
|
|
742
|
+
cc_settings_t resolved = {
|
|
743
|
+
.rise_color = (settings && settings->rise_color) ? settings->rise_color : CC_COLOR_GREEN,
|
|
744
|
+
.fall_color = (settings && settings->fall_color) ? settings->fall_color : CC_COLOR_RED,
|
|
745
|
+
.bg_color = (settings && settings->bg_color) ? settings->bg_color : NULL,
|
|
746
|
+
.area_color = (settings && settings->area_color) ? settings->area_color : NULL,
|
|
747
|
+
.single_color = settings ? settings->single_color : 0,
|
|
748
|
+
.show_prices = settings ? settings->show_prices : 0,
|
|
749
|
+
.show_times = settings ? settings->show_times : 0,
|
|
750
|
+
};
|
|
751
|
+
return resolved;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/* ============================ Internal helpers ============================
|
|
755
|
+
* Everything below CCHARTS_IMPLEMENTATION is private. The pieces build on
|
|
756
|
+
* each other in this order:
|
|
757
|
+
* 1. cc_settings_resolve -> normalize settings to full defaults
|
|
758
|
+
* 2. capacity/dim helpers -> allocation and dimension sanity checks
|
|
759
|
+
* 3. parsing helpers -> whitespace trim + timestamp conversion
|
|
760
|
+
* 4. cc_str_to_ohlc -> CSV -> cc_ohlc_t[]
|
|
761
|
+
* 5. JSON mini-scanner -> JSON -> cc_ohlc_t[]
|
|
762
|
+
* 6. rendering helpers -> value-to-pixel math + cell string builders
|
|
763
|
+
* 7. cc_line_create -> line chart renderer
|
|
764
|
+
* 8. cc_candle_create -> candle chart renderer
|
|
765
|
+
* ========================================================================== */
|
|
766
|
+
|
|
767
|
+
/* Validates chart dimensions against the public limits. */
|
|
768
|
+
CC_INLINE int cc_dim_ok(int width, int height) {
|
|
769
|
+
return width > 0 && height > 0 &&
|
|
770
|
+
width <= CC_MAX_DIM && height <= CC_MAX_DIM &&
|
|
771
|
+
(long long)width * (long long)height <= CC_MAX_CELLS;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
CC_INLINE char* cc_trim_whitespace(char* str) {
|
|
775
|
+
if (str == NULL || *str == '\0') {
|
|
776
|
+
return str;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
while (*str != '\0' && (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r')) {
|
|
780
|
+
str++;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (*str == '\0') {
|
|
784
|
+
return str;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
char* end = str + strlen(str) - 1;
|
|
788
|
+
while (end >= str && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) {
|
|
789
|
+
end--;
|
|
790
|
+
}
|
|
791
|
+
*(end + 1) = '\0';
|
|
792
|
+
|
|
793
|
+
return str;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/* Days in a month, leap-year aware (used to validate ISO8601 input). */
|
|
797
|
+
CC_INLINE int cc_days_in_month(int year, int month) {
|
|
798
|
+
static const int days[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
|
|
799
|
+
if (month == 2) {
|
|
800
|
+
int leap = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
|
|
801
|
+
return leap ? 29 : 28;
|
|
802
|
+
}
|
|
803
|
+
return days[month - 1];
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
CC_INLINE long long cc_iso8601_to_epoch(const char* s) {
|
|
807
|
+
/* Parses "YYYY-MM-DDTHH:MM:SS[+ZZ:ZZ|+ZZZZ|+ZZ|Z]" and converts the
|
|
808
|
+
* wall-clock time to epoch seconds using Howard Hinnant's civil_date
|
|
809
|
+
* algorithm, applying the UTC offset:
|
|
810
|
+
* epoch = civil_days(wall) * 86400 + seconds(wall) - offset
|
|
811
|
+
* A missing offset (or "Z") is UTC. Returns 0 for malformed input. */
|
|
812
|
+
int y = 0, mo = 0, d = 0, h = 0, mi = 0, se = 0, consumed = 0;
|
|
813
|
+
if (sscanf(s, "%d-%d-%dT%d:%d:%d%n", &y, &mo, &d, &h, &mi, &se, &consumed) < 6) {
|
|
814
|
+
return 0;
|
|
815
|
+
}
|
|
816
|
+
if (mo < 1 || mo > 12 || d < 1 || d > cc_days_in_month(y, mo) ||
|
|
817
|
+
h < 0 || h > 23 || mi < 0 || mi > 59 || se < 0 || se > 60) {
|
|
818
|
+
return 0;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
long long offset = 0;
|
|
822
|
+
const char* tz = s + consumed;
|
|
823
|
+
if (*tz == 'Z' || *tz == 'z') {
|
|
824
|
+
tz++;
|
|
825
|
+
} else if (*tz == '+' || *tz == '-') {
|
|
826
|
+
int oh = 0, om = 0, n2 = 0;
|
|
827
|
+
int sign = (*tz == '-') ? -1 : 1;
|
|
828
|
+
if (sscanf(tz + 1, "%d:%d%n", &oh, &om, &n2) == 2) {
|
|
829
|
+
/* "+HH:MM" */
|
|
830
|
+
} else if (sscanf(tz + 1, "%d%n", &oh, &n2) == 1) {
|
|
831
|
+
if (n2 == 4) { om = oh % 100; oh = oh / 100; } /* "+HHMM" */
|
|
832
|
+
else if (n2 == 2) { om = 0; } /* "+HH" */
|
|
833
|
+
else return 0;
|
|
834
|
+
} else {
|
|
835
|
+
return 0;
|
|
836
|
+
}
|
|
837
|
+
if (oh > 23 || om > 59) return 0;
|
|
838
|
+
offset = sign * ((long long)oh * 3600 + (long long)om * 60);
|
|
839
|
+
tz += 1 + n2;
|
|
840
|
+
}
|
|
841
|
+
if (*tz != '\0') return 0; /* trailing garbage after the offset */
|
|
842
|
+
|
|
843
|
+
long long yy = y - (mo <= 2);
|
|
844
|
+
long long era = (yy >= 0 ? yy : yy - 399) / 400;
|
|
845
|
+
long long yoe = yy - era * 400;
|
|
846
|
+
long long doy = (153 * (mo + (mo > 2 ? -3 : 9)) + 2) / 5 + d - 1;
|
|
847
|
+
long long doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
|
848
|
+
long long days = era * 146097 + doe - 719468;
|
|
849
|
+
|
|
850
|
+
return days * 86400 + (long long)h * 3600 + (long long)mi * 60 + se - offset;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
CC_INLINE long long cc_parse_ts(const char* s) {
|
|
854
|
+
/* Accepts either an ISO8601 string ("2026-07-20T21:00:00+03:00") or a
|
|
855
|
+
* plain epoch-seconds number (possibly negative), and returns epoch
|
|
856
|
+
* seconds (0 = none). ISO detection looks for the exact YYYY-MM-DD
|
|
857
|
+
* prefix so a negative number like "-500" cannot be mistaken for one. */
|
|
858
|
+
if (s == NULL || *s == '\0') {
|
|
859
|
+
return 0;
|
|
860
|
+
}
|
|
861
|
+
size_t len = strlen(s);
|
|
862
|
+
if (len >= 10 && s[4] == '-' && s[7] == '-' &&
|
|
863
|
+
s[0] >= '0' && s[0] <= '9' && s[1] >= '0' && s[1] <= '9' &&
|
|
864
|
+
s[2] >= '0' && s[2] <= '9' && s[3] >= '0' && s[3] <= '9') {
|
|
865
|
+
return cc_iso8601_to_epoch(s);
|
|
866
|
+
}
|
|
867
|
+
return (long long)atoll(s);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/* ------------------------------ CSV parser ------------------------------ */
|
|
871
|
+
|
|
872
|
+
CC_INLINE int cc_str_to_ohlc(const char* data, int size, cc_ohlc_t** ohlc,
|
|
873
|
+
char val_seperator, char line_seperator)
|
|
874
|
+
{
|
|
875
|
+
/* Splits `data` on `line_seperator` (at most `size` lines) and parses
|
|
876
|
+
* up to 5 `val_seperator`-separated fields per line:
|
|
877
|
+
* open,high,low,close[,timestamp]. Empty lines are skipped. Each line
|
|
878
|
+
* and field is heap/slice-based, so there is no fixed-size buffer that
|
|
879
|
+
* a long line could overflow. */
|
|
880
|
+
if (data == NULL || ohlc == NULL || size <= 0) {
|
|
881
|
+
return 1;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
*ohlc = (cc_ohlc_t*)calloc((size_t)size, sizeof(cc_ohlc_t));
|
|
885
|
+
if (*ohlc == NULL) {
|
|
886
|
+
fprintf(stderr, "ccharts: memory allocation failed\n");
|
|
887
|
+
return 1;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const char* line_start = data;
|
|
891
|
+
int idx = 0;
|
|
892
|
+
|
|
893
|
+
while (*line_start != '\0' && idx < size) {
|
|
894
|
+
const char* eol = strchr(line_start, line_seperator);
|
|
895
|
+
size_t len = eol ? (size_t)(eol - line_start) : strlen(line_start);
|
|
896
|
+
|
|
897
|
+
if (len > 0) {
|
|
898
|
+
char* line = (char*)malloc(len + 1);
|
|
899
|
+
if (line == NULL) {
|
|
900
|
+
free(*ohlc);
|
|
901
|
+
*ohlc = NULL;
|
|
902
|
+
return 1;
|
|
903
|
+
}
|
|
904
|
+
memcpy(line, line_start, len);
|
|
905
|
+
line[len] = '\0';
|
|
906
|
+
|
|
907
|
+
char* tok = cc_trim_whitespace(line);
|
|
908
|
+
if (*tok != '\0') {
|
|
909
|
+
cc_ohlc_t o = {0};
|
|
910
|
+
int field = 0;
|
|
911
|
+
char* field_start = tok;
|
|
912
|
+
while (field < 5) {
|
|
913
|
+
char* field_end = strchr(field_start, val_seperator);
|
|
914
|
+
if (field_end != NULL) *field_end = '\0';
|
|
915
|
+
|
|
916
|
+
char* f = cc_trim_whitespace(field_start);
|
|
917
|
+
if (field == 4) {
|
|
918
|
+
o.timestamp = cc_parse_ts(f);
|
|
919
|
+
} else {
|
|
920
|
+
double v = atof(f);
|
|
921
|
+
if (field == 0) o.open = v;
|
|
922
|
+
else if (field == 1) o.high = v;
|
|
923
|
+
else if (field == 2) o.low = v;
|
|
924
|
+
else if (field == 3) o.close = v;
|
|
925
|
+
}
|
|
926
|
+
field++;
|
|
927
|
+
if (field_end == NULL) break;
|
|
928
|
+
field_start = field_end + 1;
|
|
929
|
+
}
|
|
930
|
+
(*ohlc)[idx++] = o;
|
|
931
|
+
}
|
|
932
|
+
free(line);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (eol == NULL) break;
|
|
936
|
+
line_start = eol + 1;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return 0;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/* ------------------------------ JSON parser ------------------------------
|
|
943
|
+
* A small iterative scanner for the fixed schema (no recursion, no strstr):
|
|
944
|
+
* string literals are the only things that can hide braces or commas, and
|
|
945
|
+
* every value is skipped by structure instead of by substring search, so
|
|
946
|
+
* content like "open" inside a string value can never be misread as a key.
|
|
947
|
+
* ------------------------------------------------------------------------- */
|
|
948
|
+
|
|
949
|
+
/* Skips JSON whitespace. */
|
|
950
|
+
CC_INLINE void cc_json_skip_ws(const char** p) {
|
|
951
|
+
while (**p == ' ' || **p == '\t' || **p == '\n' || **p == '\r') (*p)++;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/* Parses a JSON string literal at *p into out (max n-1 chars + NUL) and
|
|
955
|
+
* advances *p past the closing quote. Escape sequences are consumed but not
|
|
956
|
+
* decoded (the fixed schema never needs them). Returns 0 on malformed input. */
|
|
957
|
+
CC_INLINE int cc_json_parse_string(const char** p, char* out, size_t n) {
|
|
958
|
+
const char* s = *p;
|
|
959
|
+
if (*s != '"') return 0;
|
|
960
|
+
s++;
|
|
961
|
+
size_t k = 0;
|
|
962
|
+
for (;;) {
|
|
963
|
+
if (*s == '\0') return 0;
|
|
964
|
+
if (*s == '\\') {
|
|
965
|
+
s++;
|
|
966
|
+
if (*s == '\0') return 0;
|
|
967
|
+
if (*s == 'u') { /* consume \uXXXX */
|
|
968
|
+
for (int i = 0; i < 4; i++) { s++; if (*s == '\0') return 0; }
|
|
969
|
+
}
|
|
970
|
+
s++;
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
if (*s == '"') break;
|
|
974
|
+
if (k + 1 < n) out[k++] = *s;
|
|
975
|
+
s++;
|
|
976
|
+
}
|
|
977
|
+
out[k] = '\0';
|
|
978
|
+
*p = s + 1;
|
|
979
|
+
return 1;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/* Skips a complete JSON value (string, number, literal, or nested
|
|
983
|
+
* object/array) at *p and advances past it. Iterative — no recursion. */
|
|
984
|
+
CC_INLINE int cc_json_skip_value(const char** p) {
|
|
985
|
+
const char* s = *p;
|
|
986
|
+
cc_json_skip_ws(&s);
|
|
987
|
+
if (*s == '\0') return 0;
|
|
988
|
+
|
|
989
|
+
if (*s == '"') {
|
|
990
|
+
s++;
|
|
991
|
+
while (*s != '"') {
|
|
992
|
+
if (*s == '\0') return 0;
|
|
993
|
+
if (*s == '\\') { s++; if (*s == '\0') return 0; }
|
|
994
|
+
s++;
|
|
995
|
+
}
|
|
996
|
+
s++;
|
|
997
|
+
*p = s;
|
|
998
|
+
return 1;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (*s == '{' || *s == '[') {
|
|
1002
|
+
int depth = 0;
|
|
1003
|
+
for (;;) {
|
|
1004
|
+
if (*s == '\0') return 0;
|
|
1005
|
+
if (*s == '"') {
|
|
1006
|
+
s++;
|
|
1007
|
+
while (*s != '"') {
|
|
1008
|
+
if (*s == '\0') return 0;
|
|
1009
|
+
if (*s == '\\') { s++; if (*s == '\0') return 0; }
|
|
1010
|
+
s++;
|
|
1011
|
+
}
|
|
1012
|
+
} else if (*s == '{' || *s == '[') {
|
|
1013
|
+
depth++;
|
|
1014
|
+
} else if (*s == '}' || *s == ']') {
|
|
1015
|
+
depth--;
|
|
1016
|
+
if (depth == 0) { s++; break; }
|
|
1017
|
+
}
|
|
1018
|
+
s++;
|
|
1019
|
+
}
|
|
1020
|
+
*p = s;
|
|
1021
|
+
return 1;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/* Bare token: number / true / false / null. */
|
|
1025
|
+
while (*s != '\0' && *s != ',' && *s != '}' && *s != ']' &&
|
|
1026
|
+
*s != ' ' && *s != '\t' && *s != '\n' && *s != '\r') s++;
|
|
1027
|
+
if (s == *p) return 0; /* nothing consumed */
|
|
1028
|
+
*p = s;
|
|
1029
|
+
return 1;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/* Copies a bare JSON number token starting at *p into out (max n-1 chars)
|
|
1033
|
+
* and advances *p past it. Returns 0 when no number is present. */
|
|
1034
|
+
CC_INLINE int cc_json_read_number_token(const char** p, char* out, size_t n) {
|
|
1035
|
+
const char* s = *p;
|
|
1036
|
+
size_t k = 0;
|
|
1037
|
+
while (*s == '-' || *s == '+' || (*s >= '0' && *s <= '9') ||
|
|
1038
|
+
*s == '.' || *s == 'e' || *s == 'E') {
|
|
1039
|
+
if (k + 1 < n) out[k++] = *s;
|
|
1040
|
+
s++;
|
|
1041
|
+
}
|
|
1042
|
+
if (k == 0) return 0;
|
|
1043
|
+
out[k] = '\0';
|
|
1044
|
+
*p = s;
|
|
1045
|
+
return 1;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
CC_INLINE int cc_json_to_ohlc(const char* json, cc_ohlc_t** ohlc, int* size) {
|
|
1049
|
+
*ohlc = NULL;
|
|
1050
|
+
*size = 0;
|
|
1051
|
+
if (json == NULL) {
|
|
1052
|
+
return 1;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const char* p = json;
|
|
1056
|
+
cc_json_skip_ws(&p);
|
|
1057
|
+
if (*p != '[') return 1;
|
|
1058
|
+
p++;
|
|
1059
|
+
cc_json_skip_ws(&p);
|
|
1060
|
+
|
|
1061
|
+
/* Pass 1: count the objects in the top-level array. */
|
|
1062
|
+
int count = 0;
|
|
1063
|
+
if (*p != ']') {
|
|
1064
|
+
for (;;) {
|
|
1065
|
+
cc_json_skip_ws(&p);
|
|
1066
|
+
if (*p != '{') return 1;
|
|
1067
|
+
int depth = 0;
|
|
1068
|
+
for (;;) { /* skim to the matching '}' */
|
|
1069
|
+
if (*p == '\0') return 1;
|
|
1070
|
+
if (*p == '"') {
|
|
1071
|
+
p++;
|
|
1072
|
+
while (*p != '"') {
|
|
1073
|
+
if (*p == '\0') return 1;
|
|
1074
|
+
if (*p == '\\') { p++; if (*p == '\0') return 1; }
|
|
1075
|
+
p++;
|
|
1076
|
+
}
|
|
1077
|
+
} else if (*p == '{') {
|
|
1078
|
+
depth++;
|
|
1079
|
+
} else if (*p == '}') {
|
|
1080
|
+
depth--;
|
|
1081
|
+
if (depth == 0) { p++; break; }
|
|
1082
|
+
}
|
|
1083
|
+
p++;
|
|
1084
|
+
}
|
|
1085
|
+
count++;
|
|
1086
|
+
cc_json_skip_ws(&p);
|
|
1087
|
+
if (*p == ',') { p++; continue; }
|
|
1088
|
+
if (*p == ']') break;
|
|
1089
|
+
return 1;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (count <= 0) return 1;
|
|
1093
|
+
|
|
1094
|
+
*ohlc = (cc_ohlc_t*)calloc((size_t)count, sizeof(cc_ohlc_t));
|
|
1095
|
+
if (*ohlc == NULL) {
|
|
1096
|
+
fprintf(stderr, "ccharts: memory allocation failed\n");
|
|
1097
|
+
return 1;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/* Pass 2: parse each object into a cc_ohlc_t. */
|
|
1101
|
+
p = json;
|
|
1102
|
+
cc_json_skip_ws(&p);
|
|
1103
|
+
p++; /* '[' */
|
|
1104
|
+
cc_json_skip_ws(&p);
|
|
1105
|
+
int idx = 0;
|
|
1106
|
+
int valid = 1;
|
|
1107
|
+
if (*p != ']') {
|
|
1108
|
+
for (;;) {
|
|
1109
|
+
cc_json_skip_ws(&p);
|
|
1110
|
+
if (*p != '{') { valid = 0; break; }
|
|
1111
|
+
p++;
|
|
1112
|
+
cc_ohlc_t o = {0};
|
|
1113
|
+
for (;;) {
|
|
1114
|
+
cc_json_skip_ws(&p);
|
|
1115
|
+
if (*p == '}') { p++; break; }
|
|
1116
|
+
if (*p != '"') { valid = 0; break; }
|
|
1117
|
+
char key[16];
|
|
1118
|
+
if (!cc_json_parse_string(&p, key, sizeof(key))) { valid = 0; break; }
|
|
1119
|
+
cc_json_skip_ws(&p);
|
|
1120
|
+
if (*p != ':') { valid = 0; break; }
|
|
1121
|
+
p++; /* ':' */
|
|
1122
|
+
cc_json_skip_ws(&p);
|
|
1123
|
+
|
|
1124
|
+
if (strcmp(key, "ts") == 0) {
|
|
1125
|
+
char sval[64];
|
|
1126
|
+
if (*p == '"') {
|
|
1127
|
+
if (!cc_json_parse_string(&p, sval, sizeof(sval))) { valid = 0; break; }
|
|
1128
|
+
} else {
|
|
1129
|
+
if (!cc_json_read_number_token(&p, sval, sizeof(sval))) { valid = 0; break; }
|
|
1130
|
+
}
|
|
1131
|
+
o.timestamp = cc_parse_ts(sval);
|
|
1132
|
+
} else if (strcmp(key, "open") == 0 || strcmp(key, "high") == 0 ||
|
|
1133
|
+
strcmp(key, "low") == 0 || strcmp(key, "close") == 0) {
|
|
1134
|
+
char nval[32];
|
|
1135
|
+
if (!cc_json_read_number_token(&p, nval, sizeof(nval))) { valid = 0; break; }
|
|
1136
|
+
double v = atof(nval);
|
|
1137
|
+
if (strcmp(key, "open") == 0) o.open = v;
|
|
1138
|
+
else if (strcmp(key, "high") == 0) o.high = v;
|
|
1139
|
+
else if (strcmp(key, "low") == 0) o.low = v;
|
|
1140
|
+
else o.close = v;
|
|
1141
|
+
} else {
|
|
1142
|
+
/* Unknown key — "volume" and friends are skipped. */
|
|
1143
|
+
if (!cc_json_skip_value(&p)) { valid = 0; break; }
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
cc_json_skip_ws(&p);
|
|
1147
|
+
if (*p == ',') { p++; continue; }
|
|
1148
|
+
if (*p == '}') { p++; break; }
|
|
1149
|
+
valid = 0;
|
|
1150
|
+
break;
|
|
1151
|
+
}
|
|
1152
|
+
if (!valid) break;
|
|
1153
|
+
(*ohlc)[idx++] = o;
|
|
1154
|
+
cc_json_skip_ws(&p);
|
|
1155
|
+
if (*p == ',') { p++; continue; }
|
|
1156
|
+
if (*p == ']') break;
|
|
1157
|
+
valid = 0;
|
|
1158
|
+
break;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (!valid) {
|
|
1162
|
+
free(*ohlc);
|
|
1163
|
+
*ohlc = NULL;
|
|
1164
|
+
return 1;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
*size = idx;
|
|
1168
|
+
return 0;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/* --------------------------- Rendering helpers --------------------------- */
|
|
1172
|
+
|
|
1173
|
+
/* Smallest / largest value in arr[0..n-1]. */
|
|
1174
|
+
CC_INLINE double find_min(double arr[], int n) {
|
|
1175
|
+
double min = arr[0];
|
|
1176
|
+
for (int i = 1; i < n; i++) {
|
|
1177
|
+
if (arr[i] < min) {
|
|
1178
|
+
min = arr[i];
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return min;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
CC_INLINE double find_max(double arr[], int n) {
|
|
1185
|
+
double max = arr[0];
|
|
1186
|
+
for (int i = 1; i < n; i++) {
|
|
1187
|
+
if (arr[i] > max) {
|
|
1188
|
+
max = arr[i];
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return max;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/* Maps a value in [min, max] to a pixel row in [0, pixel_height-1].
|
|
1195
|
+
* `range` is max - min; a flat range is centered to avoid divide-by-zero. */
|
|
1196
|
+
CC_INLINE int cc_pixel(double val, double min, double max, double range, int pixel_height) {
|
|
1197
|
+
/* `max` is kept for call-site symmetry (every caller has min/max/range at
|
|
1198
|
+
* hand); the mapping only needs min and range. Silences -Wextra for the
|
|
1199
|
+
* bindings, which compile this header with their own flags. */
|
|
1200
|
+
(void)max;
|
|
1201
|
+
double t = (range == 0.0) ? 0.5 : (val - min) / range;
|
|
1202
|
+
int p = (int)lround(t * (pixel_height - 1));
|
|
1203
|
+
if (p < 0) p = 0;
|
|
1204
|
+
if (p >= pixel_height) p = pixel_height - 1;
|
|
1205
|
+
return p;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/* NULL as an empty string, for the printf calls below. */
|
|
1209
|
+
#define CC_S(p) ((p) != NULL ? (p) : "")
|
|
1210
|
+
|
|
1211
|
+
/* The reset sequence is only needed when something was actually colored.
|
|
1212
|
+
* A color set to the empty string means "emit no escape", so a chart whose
|
|
1213
|
+
* colors are all empty comes out as plain text with no escapes at all —
|
|
1214
|
+
* which is what lets the output go somewhere that is not a terminal. */
|
|
1215
|
+
CC_INLINE const char* cc_reset_for(const char* a, const char* b) {
|
|
1216
|
+
int colored = (a != NULL && *a != '\0') || (b != NULL && *b != '\0');
|
|
1217
|
+
return colored ? CC_COLOR_RESET : "";
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
CC_INLINE void cc_render_cell(char out[32], unsigned char bodybits, unsigned char wickbits, const char* fg, const char* bg) {
|
|
1221
|
+
/* Turns a candle cell's mask bits into a ready-to-print string.
|
|
1222
|
+
* bodybits: bit0 = lower half filled, bit1 = upper half filled (body).
|
|
1223
|
+
* wickbits: non-zero means a wick passes through this cell. Body wins
|
|
1224
|
+
* over wick so a partial body still reads as a solid block. */
|
|
1225
|
+
const char* block;
|
|
1226
|
+
if (bodybits == 3) block = CC_BLOCK_FULL;
|
|
1227
|
+
else if (bodybits == 2) block = CC_BLOCK_UPPER_HALF;
|
|
1228
|
+
else if (bodybits == 1) block = CC_BLOCK_LOWER_HALF;
|
|
1229
|
+
else if (wickbits != 0) block = CC_LINE_VERTICAL;
|
|
1230
|
+
else block = NULL;
|
|
1231
|
+
|
|
1232
|
+
if (block != NULL) {
|
|
1233
|
+
snprintf(out, 32, "%s%s%s%s", CC_S(bg), CC_S(fg), block,
|
|
1234
|
+
cc_reset_for(bg, fg));
|
|
1235
|
+
} else if (bg != NULL) {
|
|
1236
|
+
snprintf(out, 32, "%s %s", bg, cc_reset_for(bg, NULL));
|
|
1237
|
+
} else {
|
|
1238
|
+
snprintf(out, 32, " ");
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
CC_INLINE cc_ohlc_t cc_agg_ohlc(const cc_ohlc_t* data, int start, int end) {
|
|
1243
|
+
/* Aggregates candles [start, end) into one virtual candle:
|
|
1244
|
+
* open = first open, high = max high, low = min low, close = last close.
|
|
1245
|
+
* Used to compress many candles into a few columns (width < size). */
|
|
1246
|
+
cc_ohlc_t v = { .open = data[start].open, .high = data[start].high,
|
|
1247
|
+
.low = data[start].low, .close = data[end - 1].close };
|
|
1248
|
+
for (int k = start + 1; k < end; k++) {
|
|
1249
|
+
if (data[k].high > v.high) v.high = data[k].high;
|
|
1250
|
+
if (data[k].low < v.low) v.low = data[k].low;
|
|
1251
|
+
}
|
|
1252
|
+
return v;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
CC_INLINE const char* cc_lower_eighth(int n) {
|
|
1256
|
+
/* Returns the lower 1/8 block whose bar height is n/8 (n in 1..8).
|
|
1257
|
+
* These characters give the line chart its 8-level vertical resolution:
|
|
1258
|
+
* ▁▂▃▄▅▆▇█. */
|
|
1259
|
+
static const char* table[] = {
|
|
1260
|
+
CC_BLOCK_LOWER_1_8, CC_BLOCK_LOWER_2_8, CC_BLOCK_LOWER_3_8, CC_BLOCK_LOWER_HALF,
|
|
1261
|
+
CC_BLOCK_LOWER_5_8, CC_BLOCK_LOWER_6_8, CC_BLOCK_LOWER_7_8, CC_BLOCK_FULL
|
|
1262
|
+
};
|
|
1263
|
+
if (n < 1) n = 1;
|
|
1264
|
+
if (n > 8) n = 8;
|
|
1265
|
+
return table[n - 1];
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
CC_INLINE const char* cc_time_format(long long first, long long last, int count) {
|
|
1269
|
+
/* Picks a strftime format from the average interval between candles:
|
|
1270
|
+
* >= 20 hours (daily/weekly/monthly) -> "YYYY-MM-DD"
|
|
1271
|
+
* intraday spanning > 1 day -> "MM-DD HH:MM"
|
|
1272
|
+
* intraday within one day -> "HH:MM" */
|
|
1273
|
+
long long span = last - first;
|
|
1274
|
+
long long interval = (count > 1) ? span / (count - 1) : 0;
|
|
1275
|
+
if (interval >= 72000) {
|
|
1276
|
+
return "%Y-%m-%d";
|
|
1277
|
+
}
|
|
1278
|
+
if (span >= 86400) {
|
|
1279
|
+
return "%m-%d %H:%M";
|
|
1280
|
+
}
|
|
1281
|
+
return "%H:%M";
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
CC_INLINE char* cc_assemble_chart(int width, int height, char* columns,
|
|
1285
|
+
const cc_settings_t* s,
|
|
1286
|
+
long long ts_first, long long ts_last, int count,
|
|
1287
|
+
double max, double min) {
|
|
1288
|
+
/* Joins the per-cell strings (columns[(x*height + y)*32 .. +32), y
|
|
1289
|
+
* counted from the bottom) into the final chart string, printing rows
|
|
1290
|
+
* top-to-bottom. Optionally adds a left price margin (max on the top
|
|
1291
|
+
* row, min on the bottom row) and a footer with the first/last
|
|
1292
|
+
* timestamps. Allocates the result with a pointer cursor (no strcat
|
|
1293
|
+
* re-scans); the caller frees it. */
|
|
1294
|
+
int margin = 0;
|
|
1295
|
+
char max_label[16] = "";
|
|
1296
|
+
char min_label[16] = "";
|
|
1297
|
+
if (s->show_prices) {
|
|
1298
|
+
margin = 8;
|
|
1299
|
+
snprintf(max_label, sizeof(max_label), "%.2f", max);
|
|
1300
|
+
snprintf(min_label, sizeof(min_label), "%.2f", min);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
char t_first[32] = "";
|
|
1304
|
+
char t_last[32] = "";
|
|
1305
|
+
int footer = 0;
|
|
1306
|
+
if (s->show_times && count > 0 && ts_first > 0 && ts_last > 0) {
|
|
1307
|
+
const char* fmt = cc_time_format(ts_first, ts_last, count);
|
|
1308
|
+
time_t a = (time_t)ts_first;
|
|
1309
|
+
time_t b = (time_t)ts_last;
|
|
1310
|
+
struct tm tmv;
|
|
1311
|
+
if (CC_GMTIME_R(&a, &tmv)) strftime(t_first, sizeof(t_first), fmt, &tmv);
|
|
1312
|
+
if (CC_GMTIME_R(&b, &tmv)) strftime(t_last, sizeof(t_last), fmt, &tmv);
|
|
1313
|
+
footer = 1;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const int line_len = width + margin;
|
|
1317
|
+
const int rows = height + footer;
|
|
1318
|
+
/* Bounded by CC_MAX_CELLS: width*height <= 1e6, so this is <= ~32 MB. */
|
|
1319
|
+
size_t total_size = (size_t)line_len * (size_t)rows * 32 + (size_t)rows + 1;
|
|
1320
|
+
char* chart = (char*)calloc(total_size, 1);
|
|
1321
|
+
if (chart == NULL) return NULL;
|
|
1322
|
+
|
|
1323
|
+
char* w = chart;
|
|
1324
|
+
for (int y = height - 1; y >= 0; y--) {
|
|
1325
|
+
if (margin > 0) {
|
|
1326
|
+
char mlabel[16] = "";
|
|
1327
|
+
if (y == height - 1) snprintf(mlabel, sizeof(mlabel), "%8s", max_label);
|
|
1328
|
+
else if (y == 0) snprintf(mlabel, sizeof(mlabel), "%8s", min_label);
|
|
1329
|
+
else { memset(mlabel, ' ', 8); mlabel[8] = '\0'; }
|
|
1330
|
+
size_t ml = strlen(mlabel);
|
|
1331
|
+
memcpy(w, mlabel, ml);
|
|
1332
|
+
w += ml;
|
|
1333
|
+
}
|
|
1334
|
+
for (int x = 0; x < width; x++) {
|
|
1335
|
+
const char* cell = columns + ((size_t)x * height + (size_t)y) * 32;
|
|
1336
|
+
size_t cl = strlen(cell);
|
|
1337
|
+
memcpy(w, cell, cl);
|
|
1338
|
+
w += cl;
|
|
1339
|
+
}
|
|
1340
|
+
*w++ = '\n';
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
if (footer) {
|
|
1344
|
+
for (int i = 0; i < line_len; i++) *w++ = ' ';
|
|
1345
|
+
size_t t1 = strlen(t_first);
|
|
1346
|
+
if (t1 > (size_t)line_len) t1 = (size_t)line_len;
|
|
1347
|
+
size_t t2 = strlen(t_last);
|
|
1348
|
+
if (t2 > (size_t)line_len) t2 = (size_t)line_len;
|
|
1349
|
+
if (t1 > 0) memcpy(w - line_len, t_first, t1);
|
|
1350
|
+
if (t2 > 0) memcpy(w - t2, t_last, t2);
|
|
1351
|
+
*w++ = '\n';
|
|
1352
|
+
}
|
|
1353
|
+
*w = '\0';
|
|
1354
|
+
return chart;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/* ------------------------------ Line renderer ------------------------------
|
|
1358
|
+
* For each output column, average the closes that fall into that column
|
|
1359
|
+
* (like the candle chart, when width < size several candles share a column).
|
|
1360
|
+
* The column's height is snapped to a cell boundary on the bottom and drawn
|
|
1361
|
+
* with an 8-level bar on top (cc_lower_eighth), so the line's upper edge is
|
|
1362
|
+
* smooth. Adjacent columns share pixels to avoid gaps. With area_color set,
|
|
1363
|
+
* cells below the line are filled. Colors come from cc_settings_t: either
|
|
1364
|
+
* one color for the whole chart (single_color) or per segment.
|
|
1365
|
+
* All scratch buffers are heap-allocated (no VLAs) and freed on every path.
|
|
1366
|
+
* ------------------------------------------------------------------------- */
|
|
1367
|
+
|
|
1368
|
+
CC_INLINE char* cc_line_create(const cc_ohlc_t* data, int size, int width, int height,
|
|
1369
|
+
const cc_settings_t* settings) {
|
|
1370
|
+
if (data == NULL || size <= 0 || !cc_dim_ok(width, height)) {
|
|
1371
|
+
return (char*)calloc(1, sizeof(char));
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
cc_settings_t s = cc_settings_resolve(settings);
|
|
1375
|
+
|
|
1376
|
+
double* closes = (double*)malloc((size_t)size * sizeof(double));
|
|
1377
|
+
double* vals = (double*)calloc((size_t)width, sizeof(double));
|
|
1378
|
+
const char** col_color = (const char**)malloc((size_t)width * sizeof(char*));
|
|
1379
|
+
int* py = (int*)malloc((size_t)width * sizeof(int));
|
|
1380
|
+
char* columns = (char*)malloc((size_t)width * (size_t)height * 32);
|
|
1381
|
+
if (closes == NULL || vals == NULL || col_color == NULL ||
|
|
1382
|
+
py == NULL || columns == NULL) {
|
|
1383
|
+
free(closes); free(vals); free(col_color); free(py); free(columns);
|
|
1384
|
+
return NULL;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
for (int i = 0; i < size; i++) {
|
|
1388
|
+
closes[i] = data[i].close;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
for (int w = 0; w < width; w++) {
|
|
1392
|
+
int start_idx = (int)(((long long)w * size) / width);
|
|
1393
|
+
int end_idx = (int)((((long long)w + 1) * size) / width);
|
|
1394
|
+
if (end_idx <= start_idx) end_idx = start_idx + 1;
|
|
1395
|
+
|
|
1396
|
+
double sum = 0.0;
|
|
1397
|
+
for (int k = start_idx; k < end_idx && k < size; k++) {
|
|
1398
|
+
sum += closes[k];
|
|
1399
|
+
}
|
|
1400
|
+
vals[w] = sum / (end_idx - start_idx);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
double min = find_min(vals, width);
|
|
1404
|
+
double max = find_max(vals, width);
|
|
1405
|
+
double diff_range = max - min;
|
|
1406
|
+
|
|
1407
|
+
if (s.single_color) {
|
|
1408
|
+
double change = (size > 1) ? (closes[size-1] - closes[size-2]) : 0.0;
|
|
1409
|
+
const char* color = (change >= 0.0) ? s.rise_color : s.fall_color;
|
|
1410
|
+
for (int w = 0; w < width; w++) {
|
|
1411
|
+
col_color[w] = color;
|
|
1412
|
+
}
|
|
1413
|
+
} else {
|
|
1414
|
+
for (int w = 0; w < width; w++) {
|
|
1415
|
+
double prev = (w > 0) ? vals[w-1] : vals[w];
|
|
1416
|
+
col_color[w] = (vals[w] >= prev) ? s.rise_color : s.fall_color;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
int pixel_height = height * 8;
|
|
1421
|
+
|
|
1422
|
+
for (int i = 0; i < width; i++) {
|
|
1423
|
+
py[i] = cc_pixel(vals[i], min, max, diff_range, pixel_height);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
for (int i = 0; i < width; i++) {
|
|
1427
|
+
int lo = py[i];
|
|
1428
|
+
int hi = py[i];
|
|
1429
|
+
if (i + 1 < width) {
|
|
1430
|
+
if (py[i+1] < lo) lo = py[i+1];
|
|
1431
|
+
if (py[i+1] > hi) hi = py[i+1];
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
int cell_lo = lo / 8;
|
|
1435
|
+
int cell_hi = hi / 8;
|
|
1436
|
+
char* col = columns + (size_t)i * height * 32;
|
|
1437
|
+
|
|
1438
|
+
for (int r = 0; r < cell_lo && r < height; r++) {
|
|
1439
|
+
char* cell = col + (size_t)r * 32;
|
|
1440
|
+
if (s.area_color != NULL) {
|
|
1441
|
+
snprintf(cell, 32, "%s %s", s.area_color,
|
|
1442
|
+
cc_reset_for(s.area_color, NULL));
|
|
1443
|
+
} else if (s.bg_color != NULL) {
|
|
1444
|
+
snprintf(cell, 32, "%s %s", s.bg_color,
|
|
1445
|
+
cc_reset_for(s.bg_color, NULL));
|
|
1446
|
+
} else {
|
|
1447
|
+
snprintf(cell, 32, " ");
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
for (int r = cell_lo; r < cell_hi && r < height; r++) {
|
|
1452
|
+
snprintf(col + (size_t)r * 32, 32, "%s%s%s", CC_S(col_color[i]),
|
|
1453
|
+
CC_BLOCK_FULL, cc_reset_for(col_color[i], NULL));
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
if (cell_hi >= 0 && cell_hi < height) {
|
|
1457
|
+
int count = hi - cell_hi * 8 + 1;
|
|
1458
|
+
if (count > 8) count = 8;
|
|
1459
|
+
snprintf(col + (size_t)cell_hi * 32, 32, "%s%s%s",
|
|
1460
|
+
CC_S(col_color[i]), cc_lower_eighth(count),
|
|
1461
|
+
cc_reset_for(col_color[i], NULL));
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
for (int r = cell_hi + 1; r < height; r++) {
|
|
1465
|
+
char* cell = col + (size_t)r * 32;
|
|
1466
|
+
if (s.bg_color != NULL) {
|
|
1467
|
+
snprintf(cell, 32, "%s %s", s.bg_color,
|
|
1468
|
+
cc_reset_for(s.bg_color, NULL));
|
|
1469
|
+
} else {
|
|
1470
|
+
snprintf(cell, 32, " ");
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
long long ts_first = data[0].timestamp;
|
|
1476
|
+
long long ts_last = data[size-1].timestamp;
|
|
1477
|
+
char* chart = cc_assemble_chart(width, height, columns, &s,
|
|
1478
|
+
ts_first, ts_last, size, max, min);
|
|
1479
|
+
free(closes); free(vals); free(col_color); free(py); free(columns);
|
|
1480
|
+
return chart;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
/* ----------------------------- Candle renderer -----------------------------
|
|
1484
|
+
* Two modes:
|
|
1485
|
+
* - width >= size : each candle owns several columns; the body (open-close)
|
|
1486
|
+
* is drawn as blocks, the wick (high-low) as a thin │ in the center
|
|
1487
|
+
* column, and the last column of each candle is left empty as a gap.
|
|
1488
|
+
* - width < size : neighboring candles are collapsed into virtual candles
|
|
1489
|
+
* (cc_agg_ohlc), one per column.
|
|
1490
|
+
* The vertical price range covers the whole high/low span so wicks never
|
|
1491
|
+
* clip. Rises use rise_color, falls use fall_color (close >= open).
|
|
1492
|
+
* All scratch buffers are heap-allocated (no VLAs) and freed on every path.
|
|
1493
|
+
* ------------------------------------------------------------------------- */
|
|
1494
|
+
|
|
1495
|
+
CC_INLINE char* cc_candle_create(const cc_ohlc_t* data, int size, int width, int height,
|
|
1496
|
+
const cc_settings_t* settings) {
|
|
1497
|
+
if (data == NULL || size <= 0 || !cc_dim_ok(width, height)) {
|
|
1498
|
+
return (char*)calloc(1, sizeof(char));
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
cc_settings_t s = cc_settings_resolve(settings);
|
|
1502
|
+
int pixel_height = height * 2;
|
|
1503
|
+
|
|
1504
|
+
double min = data[0].low;
|
|
1505
|
+
double max = data[0].high;
|
|
1506
|
+
for (int i = 1; i < size; i++) {
|
|
1507
|
+
if (data[i].low < min) min = data[i].low;
|
|
1508
|
+
if (data[i].high > max) max = data[i].high;
|
|
1509
|
+
}
|
|
1510
|
+
double range = max - min;
|
|
1511
|
+
|
|
1512
|
+
size_t cells = (size_t)width * (size_t)height;
|
|
1513
|
+
unsigned char* body_mask = (unsigned char*)calloc(cells, 1);
|
|
1514
|
+
unsigned char* wick_mask = (unsigned char*)calloc(cells, 1);
|
|
1515
|
+
const char** col_color = (const char**)malloc((size_t)width * sizeof(char*));
|
|
1516
|
+
char* columns = (char*)malloc(cells * 32);
|
|
1517
|
+
if (body_mask == NULL || wick_mask == NULL || col_color == NULL || columns == NULL) {
|
|
1518
|
+
free(body_mask); free(wick_mask); free(col_color); free(columns);
|
|
1519
|
+
return NULL;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
if (width >= size) {
|
|
1523
|
+
|
|
1524
|
+
for (int i = 0; i < size; i++) {
|
|
1525
|
+
int cs = (int)(((long long)i * width) / size);
|
|
1526
|
+
int ce = (int)((((long long)i + 1) * width) / size);
|
|
1527
|
+
if (ce <= cs) ce = cs + 1;
|
|
1528
|
+
int gap = (ce - cs >= 2) ? (ce - 1) : ce;
|
|
1529
|
+
int cmid = cs + (gap - cs - 1) / 2;
|
|
1530
|
+
|
|
1531
|
+
const char* color = (data[i].close >= data[i].open) ? s.rise_color : s.fall_color;
|
|
1532
|
+
|
|
1533
|
+
int po = cc_pixel(data[i].open, min, max, range, pixel_height);
|
|
1534
|
+
int pc = cc_pixel(data[i].close, min, max, range, pixel_height);
|
|
1535
|
+
int ph = cc_pixel(data[i].high, min, max, range, pixel_height);
|
|
1536
|
+
int pl = cc_pixel(data[i].low, min, max, range, pixel_height);
|
|
1537
|
+
|
|
1538
|
+
int blo = (po < pc) ? po : pc;
|
|
1539
|
+
int bhi = (po > pc) ? po : pc;
|
|
1540
|
+
|
|
1541
|
+
for (int c = cs; c < ce; c++) {
|
|
1542
|
+
col_color[c] = color;
|
|
1543
|
+
if (c < gap) {
|
|
1544
|
+
unsigned char* bm = body_mask + (size_t)c * height;
|
|
1545
|
+
for (int p = blo; p <= bhi; p++) {
|
|
1546
|
+
bm[p / 2] |= (unsigned char)((p % 2) ? 2 : 1);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
unsigned char* wm = wick_mask + (size_t)cmid * height;
|
|
1552
|
+
for (int p = pl; p <= ph; p++) {
|
|
1553
|
+
wm[p / 2] |= (unsigned char)((p % 2) ? 2 : 1);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
} else {
|
|
1557
|
+
|
|
1558
|
+
for (int w = 0; w < width; w++) {
|
|
1559
|
+
int ss = (int)(((long long)w * size) / width);
|
|
1560
|
+
int se = (int)((((long long)w + 1) * size) / width);
|
|
1561
|
+
if (se <= ss) se = ss + 1;
|
|
1562
|
+
|
|
1563
|
+
cc_ohlc_t v = cc_agg_ohlc(data, ss, se);
|
|
1564
|
+
col_color[w] = (v.close >= v.open) ? s.rise_color : s.fall_color;
|
|
1565
|
+
|
|
1566
|
+
int po = cc_pixel(v.open, min, max, range, pixel_height);
|
|
1567
|
+
int pc = cc_pixel(v.close, min, max, range, pixel_height);
|
|
1568
|
+
int ph = cc_pixel(v.high, min, max, range, pixel_height);
|
|
1569
|
+
int pl = cc_pixel(v.low, min, max, range, pixel_height);
|
|
1570
|
+
|
|
1571
|
+
int blo = (po < pc) ? po : pc;
|
|
1572
|
+
int bhi = (po > pc) ? po : pc;
|
|
1573
|
+
|
|
1574
|
+
unsigned char* bm = body_mask + (size_t)w * height;
|
|
1575
|
+
for (int p = blo; p <= bhi; p++) {
|
|
1576
|
+
bm[p / 2] |= (unsigned char)((p % 2) ? 2 : 1);
|
|
1577
|
+
}
|
|
1578
|
+
unsigned char* wm = wick_mask + (size_t)w * height;
|
|
1579
|
+
for (int p = pl; p <= ph; p++) {
|
|
1580
|
+
wm[p / 2] |= (unsigned char)((p % 2) ? 2 : 1);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
for (int i = 0; i < width; i++) {
|
|
1586
|
+
char* col = columns + (size_t)i * height * 32;
|
|
1587
|
+
for (int j = 0; j < height; j++) {
|
|
1588
|
+
cc_render_cell(col + (size_t)j * 32, body_mask[(size_t)i * height + j],
|
|
1589
|
+
wick_mask[(size_t)i * height + j], col_color[i], s.bg_color);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
long long ts_first = data[0].timestamp;
|
|
1594
|
+
long long ts_last = data[size-1].timestamp;
|
|
1595
|
+
char* chart = cc_assemble_chart(width, height, columns, &s,
|
|
1596
|
+
ts_first, ts_last, size, max, min);
|
|
1597
|
+
free(body_mask); free(wick_mask); free(col_color); free(columns);
|
|
1598
|
+
return chart;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
/* ------------------------------- Pie renderer ------------------------------
|
|
1602
|
+
* A pie normalizes slice values to angles (value/total * 2*pi) and samples
|
|
1603
|
+
* each grid cell once at its center in sub-pixel space (8 sub-pixels per
|
|
1604
|
+
* cell, like the line chart, so the disk is round in pixel space). A cell is
|
|
1605
|
+
* colored by the slice whose angular range holds its center, inside an outer
|
|
1606
|
+
* radius; a donut additionally blanks the cells inside the inner radius.
|
|
1607
|
+
* Colors come from a fixed deterministic palette or a NULL-terminated
|
|
1608
|
+
* per-slice override. The legend, when requested, is appended below the disk,
|
|
1609
|
+
* one "label value (pct%)" line per slice. All scratch buffers are
|
|
1610
|
+
* heap-allocated (no VLAs) and freed on every path.
|
|
1611
|
+
* -------------------------------------------------------------------------- */
|
|
1612
|
+
|
|
1613
|
+
#define CC_PI 3.14159265358979323846 /* M_PI is not portable C89/C99 */
|
|
1614
|
+
#define CC_PIE_DONUT_RADIUS_RATIO 0.5 /* inner radius = this * outer radius */
|
|
1615
|
+
/* Terminal character cells are roughly twice as tall as they are wide
|
|
1616
|
+
* (~2:1). The pie renderer draws the disk in square sub-pixel space (8x8 per
|
|
1617
|
+
* cell), so an unscaled circle would read as a stretched ellipse on screen.
|
|
1618
|
+
* CC_PIE_ASPECT is the terminal cell aspect (width / height, ~0.5): the
|
|
1619
|
+
* renderer compresses the horizontal axis by this factor when testing
|
|
1620
|
+
* distance and slice angles, so the disk renders as a round circle. */
|
|
1621
|
+
#define CC_PIE_ASPECT 0.5 /* terminal cell width / height */
|
|
1622
|
+
|
|
1623
|
+
/* Deterministic fixed palette. Never random — a random palette would break
|
|
1624
|
+
* the byte-for-byte conformance goldens. Indexed per slice (mod length). */
|
|
1625
|
+
static const char* const CC_PIE_DEFAULT_PALETTE[] = {
|
|
1626
|
+
CC_COLOR_BLUE, CC_COLOR_GREEN, CC_COLOR_YELLOW, CC_COLOR_RED,
|
|
1627
|
+
CC_COLOR_MAGENTA, CC_COLOR_CYAN, CC_COLOR_BRIGHT_BLUE,
|
|
1628
|
+
CC_COLOR_BRIGHT_GREEN, CC_COLOR_BRIGHT_YELLOW, CC_COLOR_BRIGHT_RED,
|
|
1629
|
+
CC_COLOR_BRIGHT_MAGENTA, CC_COLOR_BRIGHT_CYAN, CC_COLOR_WHITE,
|
|
1630
|
+
CC_COLOR_BRIGHT_WHITE, CC_COLOR_BLACK, CC_COLOR_BRIGHT_BLACK,
|
|
1631
|
+
};
|
|
1632
|
+
#define CC_PIE_DEFAULT_PALETTE_COUNT \
|
|
1633
|
+
((int)(sizeof(CC_PIE_DEFAULT_PALETTE) / sizeof(CC_PIE_DEFAULT_PALETTE[0])))
|
|
1634
|
+
|
|
1635
|
+
/* Resolves the color for `slice`: the per-slice override palette when set
|
|
1636
|
+
* (a NULL-terminated array), otherwise the fixed default palette. */
|
|
1637
|
+
CC_INLINE const char* cc_pie_palette_color(const cc_pie_settings_t* s, int slice) {
|
|
1638
|
+
if (s->colors != NULL) {
|
|
1639
|
+
int n = 0;
|
|
1640
|
+
while (s->colors[n] != NULL) n++;
|
|
1641
|
+
if (n > 0) return s->colors[slice % n];
|
|
1642
|
+
}
|
|
1643
|
+
return CC_PIE_DEFAULT_PALETTE[slice % CC_PIE_DEFAULT_PALETTE_COUNT];
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
/* Returns the slice index whose angular range contains `angle`, given the
|
|
1647
|
+
* slices' start angles. The angle is normalized into the first slice's
|
|
1648
|
+
* revolution so the last slice can wrap past +2*pi and back to the first. */
|
|
1649
|
+
CC_INLINE int cc_pie_find_slice(const double* starts, int count, double angle) {
|
|
1650
|
+
double base = starts[0];
|
|
1651
|
+
double two_pi = 2.0 * CC_PI;
|
|
1652
|
+
while (angle < base) angle += two_pi;
|
|
1653
|
+
while (angle >= base + two_pi) angle -= two_pi;
|
|
1654
|
+
for (int i = 1; i < count; i++) {
|
|
1655
|
+
if (angle < starts[i]) return i - 1;
|
|
1656
|
+
}
|
|
1657
|
+
return count - 1;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/* Gap-aware variant used when slice_gap > 0: each slice spans [starts[i],
|
|
1661
|
+
* starts[i] + widths[i]); the region between a slice's end and the next
|
|
1662
|
+
* slice's start (and the closing gap before 12 o'clock) is blank, so this
|
|
1663
|
+
* returns -1 there. Accounts for the last slice wrapping past the first. */
|
|
1664
|
+
CC_INLINE int cc_pie_find_slice_gap(const double* starts, const double* widths,
|
|
1665
|
+
int count, double angle) {
|
|
1666
|
+
double base = starts[0];
|
|
1667
|
+
double two_pi = 2.0 * CC_PI;
|
|
1668
|
+
int i;
|
|
1669
|
+
|
|
1670
|
+
while (angle < base) angle += two_pi;
|
|
1671
|
+
while (angle >= base + two_pi) angle -= two_pi;
|
|
1672
|
+
for (i = 0; i < count; i++) {
|
|
1673
|
+
double a, e;
|
|
1674
|
+
if (widths[i] <= 0.0) continue;
|
|
1675
|
+
a = starts[i];
|
|
1676
|
+
e = a + widths[i];
|
|
1677
|
+
if (angle >= a && angle < e) return i;
|
|
1678
|
+
}
|
|
1679
|
+
return -1;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/* Joins the per-cell strings (columns[(x*height + y)*32 .. +32), y counted
|
|
1683
|
+
* from the bottom) into the final pie string, printing rows top-to-bottom
|
|
1684
|
+
* and appending the legend below when requested. Allocates with a pointer
|
|
1685
|
+
* cursor (no strcat re-scans); the caller frees the result. */
|
|
1686
|
+
CC_INLINE char* cc_pie_assemble(int width, int height, char* columns,
|
|
1687
|
+
const cc_pie_slice_t* slices, int count,
|
|
1688
|
+
double total, const cc_pie_settings_t* s) {
|
|
1689
|
+
int legend = s->show_legend ? count : 0;
|
|
1690
|
+
/* Bounded by CC_MAX_CELLS for the cells (width*height <= 1e6, each cell
|
|
1691
|
+
* at most 32 bytes); each legend row is capped at 512 bytes by the
|
|
1692
|
+
* snprintf below, so this allocation is always large enough. */
|
|
1693
|
+
size_t total_size = (size_t)width * (size_t)height * 32 + (size_t)height + 1
|
|
1694
|
+
+ (size_t)legend * 512;
|
|
1695
|
+
char* chart = (char*)calloc(total_size, 1);
|
|
1696
|
+
if (chart == NULL) return NULL;
|
|
1697
|
+
|
|
1698
|
+
char* w = chart;
|
|
1699
|
+
for (int y = height - 1; y >= 0; y--) {
|
|
1700
|
+
for (int x = 0; x < width; x++) {
|
|
1701
|
+
const char* cell = columns + ((size_t)x * height + (size_t)y) * 32;
|
|
1702
|
+
size_t cl = strlen(cell);
|
|
1703
|
+
memcpy(w, cell, cl);
|
|
1704
|
+
w += cl;
|
|
1705
|
+
}
|
|
1706
|
+
*w++ = '\n';
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
if (legend > 0) {
|
|
1710
|
+
char line[512];
|
|
1711
|
+
for (int i = 0; i < count; i++) {
|
|
1712
|
+
const char* label = CC_S(slices[i].label);
|
|
1713
|
+
double pct = slices[i].value / total * 100.0;
|
|
1714
|
+
switch (s->legend_format) {
|
|
1715
|
+
case CC_PIE_LEGEND_LABEL_PCT:
|
|
1716
|
+
snprintf(line, sizeof(line), "%s %.0f%%", label, pct);
|
|
1717
|
+
break;
|
|
1718
|
+
case CC_PIE_LEGEND_VALUE_PCT:
|
|
1719
|
+
snprintf(line, sizeof(line), "%g (%.0f%%)",
|
|
1720
|
+
slices[i].value, pct);
|
|
1721
|
+
break;
|
|
1722
|
+
case CC_PIE_LEGEND_LABEL:
|
|
1723
|
+
snprintf(line, sizeof(line), "%s", label);
|
|
1724
|
+
break;
|
|
1725
|
+
case CC_PIE_LEGEND_VALUE:
|
|
1726
|
+
default:
|
|
1727
|
+
if (s->show_pct) {
|
|
1728
|
+
snprintf(line, sizeof(line), "%s %g (%.0f%%)", label,
|
|
1729
|
+
slices[i].value, pct);
|
|
1730
|
+
} else {
|
|
1731
|
+
snprintf(line, sizeof(line), "%s %g", label,
|
|
1732
|
+
slices[i].value);
|
|
1733
|
+
}
|
|
1734
|
+
break;
|
|
1735
|
+
}
|
|
1736
|
+
size_t ll = strlen(line);
|
|
1737
|
+
memcpy(w, line, ll);
|
|
1738
|
+
w += ll;
|
|
1739
|
+
*w++ = '\n';
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
*w = '\0';
|
|
1743
|
+
return chart;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
CC_INLINE char* cc_pie_create(const cc_pie_slice_t* slices, int count,
|
|
1747
|
+
int width, int height,
|
|
1748
|
+
const cc_pie_settings_t* settings) {
|
|
1749
|
+
if (slices == NULL || count <= 0 || !cc_dim_ok(width, height)) {
|
|
1750
|
+
return (char*)calloc(1, sizeof(char));
|
|
1751
|
+
}
|
|
1752
|
+
int i;
|
|
1753
|
+
double total = 0.0;
|
|
1754
|
+
for (i = 0; i < count; i++) {
|
|
1755
|
+
/* A plain positive-range check: NaN fails `> 0.0`, as do zero and
|
|
1756
|
+
* negative amounts. +inf and non-finite option doubles are rejected
|
|
1757
|
+
* upstream (wrapper/ABI), which keeps this header free of C99-only
|
|
1758
|
+
* `isfinite`. */
|
|
1759
|
+
if (!(slices[i].value > 0.0)) {
|
|
1760
|
+
return (char*)calloc(1, sizeof(char));
|
|
1761
|
+
}
|
|
1762
|
+
total += slices[i].value;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
cc_pie_settings_t s = {0};
|
|
1766
|
+
if (settings != NULL) {
|
|
1767
|
+
s.bg_color = settings->bg_color;
|
|
1768
|
+
s.colors = settings->colors;
|
|
1769
|
+
s.donut = settings->donut;
|
|
1770
|
+
s.show_legend = settings->show_legend;
|
|
1771
|
+
s.show_pct = settings->show_pct;
|
|
1772
|
+
s.slice_gap = settings->slice_gap;
|
|
1773
|
+
s.inner_radius_ratio = settings->inner_radius_ratio;
|
|
1774
|
+
s.legend_format = settings->legend_format;
|
|
1775
|
+
s.start_angle = settings->start_angle;
|
|
1776
|
+
s.counter_clockwise = settings->counter_clockwise;
|
|
1777
|
+
s.center_text = settings->center_text;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
/* Resolve the optional settings to concrete values. Negative doubles are
|
|
1781
|
+
* the "unspecified" sentinel: default to the library defaults. */
|
|
1782
|
+
if (s.slice_gap < 0.0) s.slice_gap = 0.0;
|
|
1783
|
+
if (s.legend_format < CC_PIE_LEGEND_VALUE ||
|
|
1784
|
+
s.legend_format > CC_PIE_LEGEND_LABEL) {
|
|
1785
|
+
s.legend_format = CC_PIE_LEGEND_VALUE;
|
|
1786
|
+
}
|
|
1787
|
+
if (s.start_angle < 0.0) s.start_angle = CC_PI / 2.0;
|
|
1788
|
+
|
|
1789
|
+
double* starts = (double*)malloc((size_t)count * sizeof(double));
|
|
1790
|
+
double* widths = (s.slice_gap > 0.0)
|
|
1791
|
+
? (double*)malloc((size_t)count * sizeof(double)) : NULL;
|
|
1792
|
+
char* columns = (char*)malloc((size_t)width * (size_t)height * 32);
|
|
1793
|
+
if (starts == NULL || columns == NULL ||
|
|
1794
|
+
(s.slice_gap > 0.0 && widths == NULL)) {
|
|
1795
|
+
free(starts);
|
|
1796
|
+
free(widths);
|
|
1797
|
+
free(columns);
|
|
1798
|
+
return NULL;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
/* Sub-pixel space: 8 units per cell. The disk is a perfect circle in this
|
|
1802
|
+
* space, centered in the grid, with the radius taken from the smaller
|
|
1803
|
+
* dimension so it always fits. */
|
|
1804
|
+
double cx = (double)width * 4.0;
|
|
1805
|
+
double cy = (double)height * 4.0;
|
|
1806
|
+
double min_dim = (width < height) ? (double)width : (double)height;
|
|
1807
|
+
double outer_r = min_dim * 4.0;
|
|
1808
|
+
double inner_r;
|
|
1809
|
+
if (s.inner_radius_ratio < 0.0) {
|
|
1810
|
+
/* Unspecified: the donut flag's classic hollow (0.5) or a disk. */
|
|
1811
|
+
inner_r = s.donut ? outer_r * CC_PIE_DONUT_RADIUS_RATIO : 0.0;
|
|
1812
|
+
} else {
|
|
1813
|
+
double r = (s.inner_radius_ratio > 1.0) ? 1.0 : s.inner_radius_ratio;
|
|
1814
|
+
inner_r = outer_r * r;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
double angle = s.start_angle;
|
|
1818
|
+
for (i = 0; i < count; i++) {
|
|
1819
|
+
double frac = slices[i].value / total;
|
|
1820
|
+
starts[i] = angle;
|
|
1821
|
+
if (widths != NULL) {
|
|
1822
|
+
double w = frac * 2.0 * CC_PI - s.slice_gap;
|
|
1823
|
+
widths[i] = (w < 0.0) ? 0.0 : w;
|
|
1824
|
+
}
|
|
1825
|
+
angle += frac * 2.0 * CC_PI;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
for (int x = 0; x < width; x++) {
|
|
1829
|
+
double sx = (double)x * 8.0 + 4.0;
|
|
1830
|
+
for (int y = 0; y < height; y++) {
|
|
1831
|
+
double sy = (double)y * 8.0 + 4.0;
|
|
1832
|
+
double dx = (sx - cx) * CC_PIE_ASPECT; /* 1:1 with dy's terminal aspect */
|
|
1833
|
+
double dy = sy - cy;
|
|
1834
|
+
double dist = sqrt(dx * dx + dy * dy);
|
|
1835
|
+
char* cell = columns + ((size_t)x * height + (size_t)y) * 32;
|
|
1836
|
+
|
|
1837
|
+
if (dist > outer_r || (inner_r > 0.0 && dist < inner_r)) {
|
|
1838
|
+
if (s.bg_color != NULL) {
|
|
1839
|
+
snprintf(cell, 32, "%s %s", s.bg_color,
|
|
1840
|
+
cc_reset_for(s.bg_color, NULL));
|
|
1841
|
+
} else {
|
|
1842
|
+
snprintf(cell, 32, " ");
|
|
1843
|
+
}
|
|
1844
|
+
continue;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
/* counter_clockwise mirrors the horizontal axis, which reverses
|
|
1848
|
+
* the sweep (clockwise) while keeping slice 0 at 12 o'clock. */
|
|
1849
|
+
double probe_x = (s.counter_clockwise ? (cx - sx) : (sx - cx))
|
|
1850
|
+
* CC_PIE_ASPECT;
|
|
1851
|
+
double probe = atan2(dy, probe_x);
|
|
1852
|
+
int slice = (widths != NULL)
|
|
1853
|
+
? cc_pie_find_slice_gap(starts, widths, count, probe)
|
|
1854
|
+
: cc_pie_find_slice(starts, count, probe);
|
|
1855
|
+
if (slice < 0) { /* in a slice gap: leave the cell blank */
|
|
1856
|
+
if (s.bg_color != NULL) {
|
|
1857
|
+
snprintf(cell, 32, "%s %s", s.bg_color,
|
|
1858
|
+
cc_reset_for(s.bg_color, NULL));
|
|
1859
|
+
} else {
|
|
1860
|
+
snprintf(cell, 32, " ");
|
|
1861
|
+
}
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
const char* color = cc_pie_palette_color(&s, slice);
|
|
1865
|
+
snprintf(cell, 32, "%s%s%s", CC_S(color), CC_BLOCK_FULL,
|
|
1866
|
+
cc_reset_for(color, NULL));
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
/* center_text is drawn in the hollow center, centered on the row nearest
|
|
1871
|
+
* the vertical middle and truncated to the hole's width in cells. */
|
|
1872
|
+
if (inner_r > 0.0 && s.center_text != NULL && s.center_text[0] != '\0') {
|
|
1873
|
+
double best = 1.0e18;
|
|
1874
|
+
int y_center = 0;
|
|
1875
|
+
for (int y = 0; y < height; y++) {
|
|
1876
|
+
double sy = (double)y * 8.0 + 4.0;
|
|
1877
|
+
double d = (sy > cy) ? (sy - cy) : (cy - sy);
|
|
1878
|
+
if (d < best) {
|
|
1879
|
+
best = d;
|
|
1880
|
+
y_center = y;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
int x0 = -1;
|
|
1884
|
+
int x1 = -1;
|
|
1885
|
+
for (int x = 0; x < width; x++) {
|
|
1886
|
+
double sx = (double)x * 8.0 + 4.0;
|
|
1887
|
+
double d = (sx > cx) ? (sx - cx) : (cx - sx);
|
|
1888
|
+
/* Same aspect-scaled hollow test as the disk geometry. */
|
|
1889
|
+
if (d * CC_PIE_ASPECT < inner_r) {
|
|
1890
|
+
if (x0 < 0) x0 = x;
|
|
1891
|
+
x1 = x;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
if (x0 >= 0) {
|
|
1895
|
+
size_t tlen = strlen(s.center_text);
|
|
1896
|
+
int span = x1 - x0 + 1;
|
|
1897
|
+
int n = (span < (int)tlen) ? span : (int)tlen;
|
|
1898
|
+
int startcol = x0 + (span - n) / 2;
|
|
1899
|
+
for (int k = 0; k < n; k++) {
|
|
1900
|
+
char* cell = columns +
|
|
1901
|
+
((size_t)(startcol + k) * height +
|
|
1902
|
+
(size_t)y_center) * 32;
|
|
1903
|
+
snprintf(cell, 32, "%c", s.center_text[k]);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
char* chart = cc_pie_assemble(width, height, columns, slices, count,
|
|
1909
|
+
total, &s);
|
|
1910
|
+
free(starts);
|
|
1911
|
+
free(widths);
|
|
1912
|
+
free(columns);
|
|
1913
|
+
return chart;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
/* ---------------------------- Histogram renderer ---------------------------
|
|
1917
|
+
* Samples are counted into equal-width bins across a [min, max] value
|
|
1918
|
+
* window; each bin / column is drawn as a vertical bar scaled to the
|
|
1919
|
+
* tallest bin and sampled at 8 sub-pixels per cell (cc_lower_eighth), so
|
|
1920
|
+
* bar tops are smooth like the line chart. When width >= bin_count a bin
|
|
1921
|
+
* may span several columns; when width < bin_count several bins are
|
|
1922
|
+
* aggregated into each column — a single deterministic long-arithmetic
|
|
1923
|
+
* mapping covers both. Outlier samples are clamped into the edge bins so a
|
|
1924
|
+
* single huge value cannot empty every other bin, and a flat window
|
|
1925
|
+
* (min == max) is widened by +-1 so the value-to-bin division never hits
|
|
1926
|
+
* zero. Non-finite samples are rejected upstream (wrapper/ABI); a NaN
|
|
1927
|
+
* window double is the "auto" sentinel that selects the data range. All
|
|
1928
|
+
* scratch buffers are heap-allocated (no VLAs) and freed on every path.
|
|
1929
|
+
* ------------------------------------------------------------------------- */
|
|
1930
|
+
|
|
1931
|
+
/* Joins the per-cell strings (columns[(x*height + y)*32 .. +32), y counted
|
|
1932
|
+
* from the bottom) into the final histogram string, printing rows
|
|
1933
|
+
* top-to-bottom. With show_prices it prepends an 8-column left margin
|
|
1934
|
+
* holding the max-count (top) / min-count (bottom) labels; with show_bins
|
|
1935
|
+
* it appends a value-axis footer row (window min left, window max right).
|
|
1936
|
+
* Allocates with a pointer cursor (no strcat re-scans); the caller frees
|
|
1937
|
+
* the result. */
|
|
1938
|
+
CC_INLINE char* cc_hist_assemble(int width, int height, char* columns,
|
|
1939
|
+
const cc_hist_settings_t* s,
|
|
1940
|
+
double wmin, double wmax,
|
|
1941
|
+
int max_count, int min_count) {
|
|
1942
|
+
int margin = s->show_prices ? 8 : 0;
|
|
1943
|
+
char max_label[16] = "";
|
|
1944
|
+
char min_label[16] = "";
|
|
1945
|
+
if (margin > 0) {
|
|
1946
|
+
snprintf(max_label, sizeof(max_label), "%8d", max_count);
|
|
1947
|
+
snprintf(min_label, sizeof(min_label), "%8d", min_count);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
char lo[16] = "";
|
|
1951
|
+
char hi[16] = "";
|
|
1952
|
+
int footer = 0;
|
|
1953
|
+
if (s->show_bins) {
|
|
1954
|
+
snprintf(lo, sizeof(lo), "%.2f", wmin);
|
|
1955
|
+
snprintf(hi, sizeof(hi), "%.2f", wmax);
|
|
1956
|
+
footer = 1;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
const int line_len = width + margin;
|
|
1960
|
+
const int rows = height + footer;
|
|
1961
|
+
/* Bounded by CC_MAX_CELLS: width*height <= 1e6, so this is <= ~32 MB. */
|
|
1962
|
+
size_t total_size = (size_t)line_len * (size_t)rows * 32 + (size_t)rows + 1;
|
|
1963
|
+
char* chart = (char*)calloc(total_size, 1);
|
|
1964
|
+
if (chart == NULL) return NULL;
|
|
1965
|
+
|
|
1966
|
+
char* w = chart;
|
|
1967
|
+
for (int y = height - 1; y >= 0; y--) {
|
|
1968
|
+
if (margin > 0) {
|
|
1969
|
+
char mlabel[16] = "";
|
|
1970
|
+
if (y == height - 1) snprintf(mlabel, sizeof(mlabel), "%8s", max_label);
|
|
1971
|
+
else if (y == 0) snprintf(mlabel, sizeof(mlabel), "%8s", min_label);
|
|
1972
|
+
else { memset(mlabel, ' ', 8); mlabel[8] = '\0'; }
|
|
1973
|
+
size_t ml = strlen(mlabel);
|
|
1974
|
+
memcpy(w, mlabel, ml);
|
|
1975
|
+
w += ml;
|
|
1976
|
+
}
|
|
1977
|
+
for (int x = 0; x < width; x++) {
|
|
1978
|
+
const char* cell = columns + ((size_t)x * height + (size_t)y) * 32;
|
|
1979
|
+
size_t cl = strlen(cell);
|
|
1980
|
+
memcpy(w, cell, cl);
|
|
1981
|
+
w += cl;
|
|
1982
|
+
}
|
|
1983
|
+
*w++ = '\n';
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
if (footer) {
|
|
1987
|
+
for (int i = 0; i < line_len; i++) *w++ = ' ';
|
|
1988
|
+
size_t l0 = strlen(lo);
|
|
1989
|
+
if (l0 > (size_t)line_len) l0 = (size_t)line_len;
|
|
1990
|
+
size_t l1 = strlen(hi);
|
|
1991
|
+
if (l1 > (size_t)line_len) l1 = (size_t)line_len;
|
|
1992
|
+
if (l0 > 0) memcpy(w - line_len, lo, l0);
|
|
1993
|
+
if (l1 > 0) memcpy(w - l1, hi, l1);
|
|
1994
|
+
*w++ = '\n';
|
|
1995
|
+
}
|
|
1996
|
+
*w = '\0';
|
|
1997
|
+
return chart;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
CC_INLINE char* cc_hist_create(const double* samples, int count,
|
|
2001
|
+
int width, int height,
|
|
2002
|
+
const cc_hist_settings_t* settings) {
|
|
2003
|
+
if (samples == NULL || count <= 0 || !cc_dim_ok(width, height)) {
|
|
2004
|
+
return (char*)calloc(1, sizeof(char));
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
cc_hist_settings_t s;
|
|
2008
|
+
s.rise_color = (settings && settings->rise_color) ? settings->rise_color : CC_COLOR_GREEN;
|
|
2009
|
+
s.bg_color = settings ? settings->bg_color : NULL;
|
|
2010
|
+
s.bin_count = settings ? settings->bin_count : 0;
|
|
2011
|
+
/* NaN is the "auto" sentinel for the window doubles (0.0 is meaningful). */
|
|
2012
|
+
s.min_value = (settings && settings->min_value == settings->min_value)
|
|
2013
|
+
? settings->min_value : CC_NAN;
|
|
2014
|
+
s.max_value = (settings && settings->max_value == settings->max_value)
|
|
2015
|
+
? settings->max_value : CC_NAN;
|
|
2016
|
+
s.show_bins = settings ? settings->show_bins : 0;
|
|
2017
|
+
s.show_prices = settings ? settings->show_prices : 0;
|
|
2018
|
+
|
|
2019
|
+
double dmin = samples[0];
|
|
2020
|
+
double dmax = samples[0];
|
|
2021
|
+
for (int i = 1; i < count; i++) {
|
|
2022
|
+
if (samples[i] < dmin) dmin = samples[i];
|
|
2023
|
+
if (samples[i] > dmax) dmax = samples[i];
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
/* The value window used for binning. A valid explicit window ALWAYS
|
|
2027
|
+
* has min < max; anything else — the NaN auto-sentinels, a partial
|
|
2028
|
+
* {0} initializer that yields 0.0/0.0, or an inverted range — falls
|
|
2029
|
+
* back to the data min/max so a brace-init {0} histogram auto-scales
|
|
2030
|
+
* and renders multiple bars instead of fixing a single-bar [0,0] window. */
|
|
2031
|
+
double wmin, wmax;
|
|
2032
|
+
if (s.min_value < s.max_value) {
|
|
2033
|
+
wmin = s.min_value;
|
|
2034
|
+
wmax = s.max_value;
|
|
2035
|
+
} else {
|
|
2036
|
+
wmin = dmin;
|
|
2037
|
+
wmax = dmax;
|
|
2038
|
+
if (wmax == wmin) { /* all samples equal: widen the flat window */
|
|
2039
|
+
wmin -= 1.0;
|
|
2040
|
+
wmax += 1.0;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
double range = wmax - wmin;
|
|
2044
|
+
|
|
2045
|
+
/* Auto-select the bin count when <= 0: 20 for busy sequences, 10
|
|
2046
|
+
* otherwise, and never more bins than the chart has columns. */
|
|
2047
|
+
int bins = s.bin_count;
|
|
2048
|
+
if (bins <= 0) {
|
|
2049
|
+
bins = (count >= 40) ? 20 : 10;
|
|
2050
|
+
}
|
|
2051
|
+
if (bins > width) bins = width;
|
|
2052
|
+
if (bins < 1) bins = 1;
|
|
2053
|
+
|
|
2054
|
+
int* counts = (int*)calloc((size_t)bins, sizeof(int));
|
|
2055
|
+
int* colv = (int*)calloc((size_t)width, sizeof(int));
|
|
2056
|
+
char* columns = (char*)malloc((size_t)width * (size_t)height * 32);
|
|
2057
|
+
if (counts == NULL || colv == NULL || columns == NULL) {
|
|
2058
|
+
free(counts); free(colv); free(columns);
|
|
2059
|
+
return NULL;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
for (int i = 0; i < count; i++) {
|
|
2063
|
+
double t = (samples[i] - wmin) / range;
|
|
2064
|
+
int b = (int)(t * bins);
|
|
2065
|
+
if (b < 0) b = 0;
|
|
2066
|
+
if (b >= bins) b = bins - 1;
|
|
2067
|
+
counts[b]++;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
/* Fold the bins into `width` columns. The same long-arithmetic mapping
|
|
2071
|
+
* covers both decompositions: width >= bins gives each bin several
|
|
2072
|
+
* columns (aggregating the single bin's count), width < bins aggregates
|
|
2073
|
+
* several bins per column. */
|
|
2074
|
+
for (int w = 0; w < width; w++) {
|
|
2075
|
+
int bs = (int)(((long long)w * bins) / width);
|
|
2076
|
+
int be = (int)((((long long)w + 1) * bins) / width);
|
|
2077
|
+
if (be <= bs) be = bs + 1;
|
|
2078
|
+
int sum = 0;
|
|
2079
|
+
for (int b = bs; b < be && b < bins; b++) sum += counts[b];
|
|
2080
|
+
colv[w] = sum;
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
int max_count = 0;
|
|
2084
|
+
int min_count = colv[0];
|
|
2085
|
+
for (int w = 0; w < width; w++) {
|
|
2086
|
+
if (colv[w] > max_count) max_count = colv[w];
|
|
2087
|
+
if (colv[w] < min_count) min_count = colv[w];
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
const char* color = s.rise_color;
|
|
2091
|
+
int pixel_height = height * 8;
|
|
2092
|
+
|
|
2093
|
+
for (int w = 0; w < width; w++) {
|
|
2094
|
+
int pix;
|
|
2095
|
+
if (max_count <= 0) pix = 0;
|
|
2096
|
+
else pix = (int)((double)colv[w] * pixel_height / max_count);
|
|
2097
|
+
if (pix < 0) pix = 0;
|
|
2098
|
+
if (pix > pixel_height) pix = pixel_height;
|
|
2099
|
+
|
|
2100
|
+
int nfull = pix / 8;
|
|
2101
|
+
int rem = pix % 8;
|
|
2102
|
+
char* col = columns + (size_t)w * height * 32;
|
|
2103
|
+
|
|
2104
|
+
for (int r = 0; r < nfull && r < height; r++) {
|
|
2105
|
+
snprintf(col + (size_t)r * 32, 32, "%s%s%s", CC_S(color),
|
|
2106
|
+
CC_BLOCK_FULL, cc_reset_for(color, NULL));
|
|
2107
|
+
}
|
|
2108
|
+
if (rem > 0 && nfull < height) {
|
|
2109
|
+
snprintf(col + (size_t)nfull * 32, 32, "%s%s%s", CC_S(color),
|
|
2110
|
+
cc_lower_eighth(rem), cc_reset_for(color, NULL));
|
|
2111
|
+
}
|
|
2112
|
+
for (int r = (rem > 0 ? nfull + 1 : nfull); r < height; r++) {
|
|
2113
|
+
char* cell = col + (size_t)r * 32;
|
|
2114
|
+
if (s.bg_color != NULL) {
|
|
2115
|
+
snprintf(cell, 32, "%s %s", s.bg_color,
|
|
2116
|
+
cc_reset_for(s.bg_color, NULL));
|
|
2117
|
+
} else {
|
|
2118
|
+
snprintf(cell, 32, " ");
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
char* chart = cc_hist_assemble(width, height, columns, &s,
|
|
2124
|
+
wmin, wmax, max_count, min_count);
|
|
2125
|
+
free(counts); free(colv); free(columns);
|
|
2126
|
+
return chart;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
/* ---------------------------- Sparkline renderer ---------------------------
|
|
2130
|
+
* An axis-less trend line. For each output column, average the samples that
|
|
2131
|
+
* fall into that column (exactly the line chart's downsampling mapping, so
|
|
2132
|
+
* width < count aggregates and width >= count spreads), map the column
|
|
2133
|
+
* average to an 8-sub-pixel row, then draw the column exactly like the line
|
|
2134
|
+
* chart: cells below the (current,next) line segment get the optional area
|
|
2135
|
+
* fill (or blank), the top partial cell uses cc_lower_eighth, and rows are
|
|
2136
|
+
* assembled top-to-bottom with no margin, footer or legend. min_above /
|
|
2137
|
+
* min_below reserve sub-pixels at the plot's top/bottom edges so the line
|
|
2138
|
+
* does not clip against the very edge at tiny heights. A flat (all-equal)
|
|
2139
|
+
* series is centered (cc_pixel semantics: range 0 -> middle), so there is
|
|
2140
|
+
* no divide-by-zero. Non-finite samples are rejected upstream (wrapper/ABI).
|
|
2141
|
+
* All scratch buffers are heap-allocated (no VLAs), sizeof(char) calloc on
|
|
2142
|
+
* bad args, and freed on every path. ------------------------------------------------------------------------- */
|
|
2143
|
+
|
|
2144
|
+
/* Joins the per-cell strings (columns[(x*height + y)*32 .. +32), y counted
|
|
2145
|
+
* from the bottom) into the final sparkline string, printing rows
|
|
2146
|
+
* top-to-bottom with a trailing newline per row and no margin/footer/legend.
|
|
2147
|
+
* Allocates with a pointer cursor (no strcat re-scans); caller frees. */
|
|
2148
|
+
CC_INLINE char* cc_spark_assemble(int width, int height, char* columns) {
|
|
2149
|
+
/* Bounded by CC_MAX_CELLS: width*height <= 1e6, so this is <= ~32 MB. */
|
|
2150
|
+
size_t total_size = (size_t)width * (size_t)height * 32 + (size_t)height + 1;
|
|
2151
|
+
char* chart = (char*)calloc(total_size, 1);
|
|
2152
|
+
if (chart == NULL) return NULL;
|
|
2153
|
+
|
|
2154
|
+
char* w = chart;
|
|
2155
|
+
for (int y = height - 1; y >= 0; y--) {
|
|
2156
|
+
for (int x = 0; x < width; x++) {
|
|
2157
|
+
const char* cell = columns + ((size_t)x * height + (size_t)y) * 32;
|
|
2158
|
+
size_t cl = strlen(cell);
|
|
2159
|
+
memcpy(w, cell, cl);
|
|
2160
|
+
w += cl;
|
|
2161
|
+
}
|
|
2162
|
+
*w++ = '\n';
|
|
2163
|
+
}
|
|
2164
|
+
*w = '\0';
|
|
2165
|
+
return chart;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
CC_INLINE char* cc_spark_create(const double* samples, int count,
|
|
2169
|
+
int width, int height,
|
|
2170
|
+
const cc_spark_settings_t* settings) {
|
|
2171
|
+
if (samples == NULL || count <= 0 || !cc_dim_ok(width, height)) {
|
|
2172
|
+
return (char*)calloc(1, sizeof(char));
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
cc_spark_settings_t s;
|
|
2176
|
+
/* min_above/min_below are plain ints: a partial {0} brace initializer or
|
|
2177
|
+
* a NULL settings both yield 0, so no sentinel ambiguity (unlike the
|
|
2178
|
+
* histogram's double window fields). */
|
|
2179
|
+
s.rise_color = (settings && settings->rise_color) ? settings->rise_color : CC_COLOR_GREEN;
|
|
2180
|
+
s.area_color = (settings && settings->area_color) ? settings->area_color : NULL;
|
|
2181
|
+
s.min_above = settings ? settings->min_above : 0;
|
|
2182
|
+
s.min_below = settings ? settings->min_below : 0;
|
|
2183
|
+
if (s.min_above < 0) s.min_above = 0;
|
|
2184
|
+
if (s.min_below < 0) s.min_below = 0;
|
|
2185
|
+
|
|
2186
|
+
double* vals = (double*)calloc((size_t)width, sizeof(double));
|
|
2187
|
+
int* py = (int*)malloc((size_t)width * sizeof(int));
|
|
2188
|
+
char* columns = (char*)malloc((size_t)width * (size_t)height * 32);
|
|
2189
|
+
if (vals == NULL || py == NULL || columns == NULL) {
|
|
2190
|
+
free(vals); free(py); free(columns);
|
|
2191
|
+
return NULL;
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
/* Column averages, exactly the line chart's downsampling mapping. */
|
|
2195
|
+
for (int w = 0; w < width; w++) {
|
|
2196
|
+
int start_idx = (int)(((long long)w * count) / width);
|
|
2197
|
+
int end_idx = (int)((((long long)w + 1) * count) / width);
|
|
2198
|
+
if (end_idx <= start_idx) end_idx = start_idx + 1;
|
|
2199
|
+
|
|
2200
|
+
double sum = 0.0;
|
|
2201
|
+
for (int k = start_idx; k < end_idx && k < count; k++) {
|
|
2202
|
+
sum += samples[k];
|
|
2203
|
+
}
|
|
2204
|
+
vals[w] = sum / (end_idx - start_idx);
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
double min = find_min(vals, width);
|
|
2208
|
+
double max = find_max(vals, width);
|
|
2209
|
+
double range = max - min;
|
|
2210
|
+
|
|
2211
|
+
int pixel_height = height * 8;
|
|
2212
|
+
int span = pixel_height - s.min_above - s.min_below;
|
|
2213
|
+
if (span < 1) span = 1;
|
|
2214
|
+
|
|
2215
|
+
for (int i = 0; i < width; i++) {
|
|
2216
|
+
/* Mirrors cc_pixel but reserves the top/bottom sub-pixel margins.
|
|
2217
|
+
* range == 0 (flat series) centers the line in the usable band. */
|
|
2218
|
+
double t = (range == 0.0) ? 0.5 : (vals[i] - min) / range;
|
|
2219
|
+
int p = (int)lround(s.min_above + t * (span - 1));
|
|
2220
|
+
if (p < 0) p = 0;
|
|
2221
|
+
if (p >= pixel_height) p = pixel_height - 1;
|
|
2222
|
+
py[i] = p;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
for (int i = 0; i < width; i++) {
|
|
2226
|
+
int lo = py[i];
|
|
2227
|
+
int hi = py[i];
|
|
2228
|
+
if (i + 1 < width) {
|
|
2229
|
+
if (py[i+1] < lo) lo = py[i+1];
|
|
2230
|
+
if (py[i+1] > hi) hi = py[i+1];
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
int cell_lo = lo / 8;
|
|
2234
|
+
int cell_hi = hi / 8;
|
|
2235
|
+
char* col = columns + (size_t)i * height * 32;
|
|
2236
|
+
|
|
2237
|
+
/* Below the line segment: area fill or blank. */
|
|
2238
|
+
for (int r = 0; r < cell_lo && r < height; r++) {
|
|
2239
|
+
char* cell = col + (size_t)r * 32;
|
|
2240
|
+
if (s.area_color != NULL) {
|
|
2241
|
+
snprintf(cell, 32, "%s %s", s.area_color,
|
|
2242
|
+
cc_reset_for(s.area_color, NULL));
|
|
2243
|
+
} else {
|
|
2244
|
+
snprintf(cell, 32, " ");
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
/* Line body: full blocks between columns' pixels. */
|
|
2249
|
+
for (int r = cell_lo; r < cell_hi && r < height; r++) {
|
|
2250
|
+
snprintf(col + (size_t)r * 32, 32, "%s%s%s", CC_S(s.rise_color),
|
|
2251
|
+
CC_BLOCK_FULL, cc_reset_for(s.rise_color, NULL));
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
/* Top partial cell: smooth 8-level top. */
|
|
2255
|
+
if (cell_hi >= 0 && cell_hi < height) {
|
|
2256
|
+
int count8 = hi - cell_hi * 8 + 1;
|
|
2257
|
+
if (count8 > 8) count8 = 8;
|
|
2258
|
+
snprintf(col + (size_t)cell_hi * 32, 32, "%s%s%s",
|
|
2259
|
+
CC_S(s.rise_color), cc_lower_eighth(count8),
|
|
2260
|
+
cc_reset_for(s.rise_color, NULL));
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
/* Above the line: blank. */
|
|
2264
|
+
for (int r = cell_hi + 1; r < height; r++) {
|
|
2265
|
+
snprintf(col + (size_t)r * 32, 32, " ");
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
char* chart = cc_spark_assemble(width, height, columns);
|
|
2270
|
+
free(vals); free(py); free(columns);
|
|
2271
|
+
return chart;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
/* ------------------------------ Bar renderer ------------------------------
|
|
2275
|
+
* An ordered list of (label, value) pairs drawn as vertical bars from a
|
|
2276
|
+
* shared zero baseline. Each output column maps to a deterministic span of
|
|
2277
|
+
* items via the same long-arithmetic mapping as the histogram and sparkline
|
|
2278
|
+
* (width >= count spreads an item over several columns; width < count folds
|
|
2279
|
+
* several items into one column, drawn at their max value). The tallest bar
|
|
2280
|
+
* fills the full height; heights use 8 sub-pixel rows (cc_lower_eighth tops)
|
|
2281
|
+
* like the line chart. Values are non-negative for now — a negative value is
|
|
2282
|
+
* clamped to zero — and non-finite values are rejected upstream (wrapper/ABI)
|
|
2283
|
+
* so this header stays free of C99-only `isfinite`. Scratch buffers are
|
|
2284
|
+
* heap-allocated (no VLAs), sizeof(char) calloc on bad args, freed on every
|
|
2285
|
+
* path. ------------------------------------------------------------------- */
|
|
2286
|
+
|
|
2287
|
+
/* Joins the per-cell strings (columns[(x*height + y)*32 .. +32), y counted
|
|
2288
|
+
* from the bottom) into the final bar string, printing rows top-to-bottom.
|
|
2289
|
+
* An optional 8-column value axis (show_prices: max value top, 0 bottom) is
|
|
2290
|
+
* prepended to every row, and an optional label footer (show_labels) is
|
|
2291
|
+
* appended below the plot: for each column, the label of the first item in
|
|
2292
|
+
* that column's span, truncated to the column width. Allocates with a pointer
|
|
2293
|
+
* cursor (no strcat re-scans); caller frees. */
|
|
2294
|
+
CC_INLINE char* cc_bar_assemble(int width, int height, char* columns,
|
|
2295
|
+
const cc_bar_item_t* items, int count,
|
|
2296
|
+
const cc_bar_settings_t* s,
|
|
2297
|
+
double max_value,
|
|
2298
|
+
const int* first_idx, const int* span_len) {
|
|
2299
|
+
int margin = s->show_prices ? 8 : 0;
|
|
2300
|
+
char max_label[16] = "";
|
|
2301
|
+
char min_label[16] = "";
|
|
2302
|
+
if (margin > 0) {
|
|
2303
|
+
snprintf(max_label, sizeof(max_label), "%8.4g", max_value);
|
|
2304
|
+
snprintf(min_label, sizeof(min_label), "%8.0f", 0.0);
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
int footer = s->show_labels ? 1 : 0;
|
|
2308
|
+
const int line_len = width + margin;
|
|
2309
|
+
const int rows = height + footer;
|
|
2310
|
+
/* Bounded by CC_MAX_CELLS: width*height <= 1e6, so this is <= ~32 MB. */
|
|
2311
|
+
size_t total_size = (size_t)line_len * (size_t)rows * 32 + (size_t)rows + 1;
|
|
2312
|
+
char* chart = (char*)calloc(total_size, 1);
|
|
2313
|
+
if (chart == NULL) return NULL;
|
|
2314
|
+
|
|
2315
|
+
char* w = chart;
|
|
2316
|
+
for (int y = height - 1; y >= 0; y--) {
|
|
2317
|
+
if (margin > 0) {
|
|
2318
|
+
char mlabel[16] = "";
|
|
2319
|
+
if (y == height - 1) snprintf(mlabel, sizeof(mlabel), "%8s", max_label);
|
|
2320
|
+
else if (y == 0) snprintf(mlabel, sizeof(mlabel), "%8s", min_label);
|
|
2321
|
+
else { memset(mlabel, ' ', 8); mlabel[8] = '\0'; }
|
|
2322
|
+
size_t ml = strlen(mlabel);
|
|
2323
|
+
memcpy(w, mlabel, ml);
|
|
2324
|
+
w += ml;
|
|
2325
|
+
}
|
|
2326
|
+
for (int x = 0; x < width; x++) {
|
|
2327
|
+
const char* cell = columns + ((size_t)x * height + (size_t)y) * 32;
|
|
2328
|
+
size_t cl = strlen(cell);
|
|
2329
|
+
memcpy(w, cell, cl);
|
|
2330
|
+
w += cl;
|
|
2331
|
+
}
|
|
2332
|
+
*w++ = '\n';
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
if (footer) {
|
|
2336
|
+
if (margin > 0) {
|
|
2337
|
+
memset(w, ' ', (size_t)margin);
|
|
2338
|
+
w += margin;
|
|
2339
|
+
}
|
|
2340
|
+
for (int x = 0; x < width; x++) {
|
|
2341
|
+
int span = span_len[x];
|
|
2342
|
+
if (span < 1) span = 1;
|
|
2343
|
+
const char* label = items[first_idx[x]].label;
|
|
2344
|
+
const char* L = (label != NULL) ? label : "";
|
|
2345
|
+
size_t n = strlen(L);
|
|
2346
|
+
if (n > (size_t)span) n = (size_t)span;
|
|
2347
|
+
if (n > 0) { memcpy(w, L, n); w += n; }
|
|
2348
|
+
if ((size_t)span > n) {
|
|
2349
|
+
memset(w, ' ', (size_t)span - n);
|
|
2350
|
+
w += (size_t)span - n;
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
*w++ = '\n';
|
|
2354
|
+
}
|
|
2355
|
+
*w = '\0';
|
|
2356
|
+
(void)count;
|
|
2357
|
+
return chart;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
CC_INLINE char* cc_bar_create(const cc_bar_item_t* items, int count,
|
|
2361
|
+
int width, int height,
|
|
2362
|
+
const cc_bar_settings_t* settings) {
|
|
2363
|
+
if (items == NULL || count <= 0 || !cc_dim_ok(width, height)) {
|
|
2364
|
+
return (char*)calloc(1, sizeof(char));
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
cc_bar_settings_t s;
|
|
2368
|
+
/* All pointer/int fields: a partial {0} brace initializer matches NULL,
|
|
2369
|
+
* so there is no sentinel ambiguity (unlike the histogram's doubles). */
|
|
2370
|
+
s.rise_color = (settings && settings->rise_color) ? settings->rise_color : CC_COLOR_GREEN;
|
|
2371
|
+
s.bg_color = settings ? settings->bg_color : NULL;
|
|
2372
|
+
s.show_labels = settings ? settings->show_labels : 0;
|
|
2373
|
+
s.show_prices = settings ? settings->show_prices : 0;
|
|
2374
|
+
if (s.show_labels < 0) s.show_labels = 0;
|
|
2375
|
+
if (s.show_prices < 0) s.show_prices = 0;
|
|
2376
|
+
|
|
2377
|
+
double* colv = (double*)calloc((size_t)width, sizeof(double));
|
|
2378
|
+
int* first_idx = (int*)malloc((size_t)width * sizeof(int));
|
|
2379
|
+
int* span_len = (int*)malloc((size_t)width * sizeof(int));
|
|
2380
|
+
char* columns = (char*)malloc((size_t)width * (size_t)height * 32);
|
|
2381
|
+
if (colv == NULL || first_idx == NULL || span_len == NULL || columns == NULL) {
|
|
2382
|
+
free(colv); free(first_idx); free(span_len); free(columns);
|
|
2383
|
+
return NULL;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
/* Fold the items into `width` columns. The same long-arithmetic mapping
|
|
2387
|
+
* covers both decompositions: width >= count spreads each item over
|
|
2388
|
+
* several columns (repeating its value), width < count aggregates several
|
|
2389
|
+
* items per column drawn at their max value (so a peak is never hidden by
|
|
2390
|
+
* a collapsing average). */
|
|
2391
|
+
double max_value = 0.0;
|
|
2392
|
+
for (int w = 0; w < width; w++) {
|
|
2393
|
+
int is = (int)(((long long)w * count) / width);
|
|
2394
|
+
int ie = (int)(((((long long)w + 1) * count) / width));
|
|
2395
|
+
if (ie <= is) ie = is + 1;
|
|
2396
|
+
if (ie > count) ie = count;
|
|
2397
|
+
first_idx[w] = is;
|
|
2398
|
+
span_len[w] = ie - is;
|
|
2399
|
+
|
|
2400
|
+
double v = 0.0; /* zero baseline; negative values clamp to 0 */
|
|
2401
|
+
for (int k = is; k < ie; k++) {
|
|
2402
|
+
double val = items[k].value;
|
|
2403
|
+
if (val < 0.0) val = 0.0;
|
|
2404
|
+
if (val > v) v = val;
|
|
2405
|
+
}
|
|
2406
|
+
colv[w] = v;
|
|
2407
|
+
if (v > max_value) max_value = v;
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
const char* color = s.rise_color;
|
|
2411
|
+
int pixel_height = height * 8;
|
|
2412
|
+
|
|
2413
|
+
for (int w = 0; w < width; w++) {
|
|
2414
|
+
int pix;
|
|
2415
|
+
if (max_value <= 0.0) pix = 0;
|
|
2416
|
+
else pix = (int)(colv[w] * pixel_height / max_value);
|
|
2417
|
+
if (pix < 0) pix = 0;
|
|
2418
|
+
if (pix > pixel_height) pix = pixel_height;
|
|
2419
|
+
|
|
2420
|
+
int nfull = pix / 8;
|
|
2421
|
+
int rem = pix % 8;
|
|
2422
|
+
char* col = columns + (size_t)w * height * 32;
|
|
2423
|
+
|
|
2424
|
+
for (int r = 0; r < nfull && r < height; r++) {
|
|
2425
|
+
snprintf(col + (size_t)r * 32, 32, "%s%s%s", CC_S(color),
|
|
2426
|
+
CC_BLOCK_FULL, cc_reset_for(color, NULL));
|
|
2427
|
+
}
|
|
2428
|
+
if (rem > 0 && nfull < height) {
|
|
2429
|
+
snprintf(col + (size_t)nfull * 32, 32, "%s%s%s", CC_S(color),
|
|
2430
|
+
cc_lower_eighth(rem), cc_reset_for(color, NULL));
|
|
2431
|
+
}
|
|
2432
|
+
for (int r = (rem > 0 ? nfull + 1 : nfull); r < height; r++) {
|
|
2433
|
+
char* cell = col + (size_t)r * 32;
|
|
2434
|
+
if (s.bg_color != NULL) {
|
|
2435
|
+
snprintf(cell, 32, "%s %s", s.bg_color,
|
|
2436
|
+
cc_reset_for(s.bg_color, NULL));
|
|
2437
|
+
} else {
|
|
2438
|
+
snprintf(cell, 32, " ");
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
char* chart = cc_bar_assemble(width, height, columns, items, count, &s,
|
|
2444
|
+
max_value, first_idx, span_len);
|
|
2445
|
+
free(colv); free(first_idx); free(span_len); free(columns);
|
|
2446
|
+
return chart;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
/* --------------------------- Stacked bar renderer -------------------------
|
|
2450
|
+
* A stacked bar is a vertical stack of segments (one per series) per
|
|
2451
|
+
* category; the category's bar height is the SUM of its series' values. All
|
|
2452
|
+
* series share one category count (settings->cats). Within a column the
|
|
2453
|
+
* segments are stacked bottom-to-top, proportional to each series'
|
|
2454
|
+
* value / the tallest stack total, drawn with 8 sub-pixel rows exactly like
|
|
2455
|
+
* the bar chart: every cell is colored by the series that sits on the stack's
|
|
2456
|
+
* surface at that cell, and the stack's top partial cell uses cc_lower_eighth
|
|
2457
|
+
* (so the final, tallest series' cell carries the top edge). Falls back to
|
|
2458
|
+
* the pie default palette or a per-series override. All scratch buffers are
|
|
2459
|
+
* heap-allocated (no VLAs) and freed on every path. */
|
|
2460
|
+
|
|
2461
|
+
CC_INLINE const char* cc_stack_palette_color(const cc_stack_settings_t* s,
|
|
2462
|
+
int series_idx) {
|
|
2463
|
+
if (s->colors != NULL) {
|
|
2464
|
+
int n = 0;
|
|
2465
|
+
while (s->colors[n] != NULL) n++;
|
|
2466
|
+
if (n > 0) return s->colors[series_idx % n];
|
|
2467
|
+
}
|
|
2468
|
+
return CC_PIE_DEFAULT_PALETTE[series_idx % CC_PIE_DEFAULT_PALETTE_COUNT];
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
/* Assembles the plot columns plus optional value-axis margin and category
|
|
2472
|
+
* label footer into the returned malloc'd string (caller frees). `max_total`
|
|
2473
|
+
* is the tallest stack total for the value axis; `cat_labels` (may be NULL)
|
|
2474
|
+
* supplies each column's footer label from the first category in that
|
|
2475
|
+
* column's span, truncated to the column width like the bar footer. Allocates
|
|
2476
|
+
* with a pointer cursor; caller frees. */
|
|
2477
|
+
CC_INLINE char* cc_stack_assemble(int width, int height, char* columns,
|
|
2478
|
+
const cc_stack_settings_t* s,
|
|
2479
|
+
double max_total,
|
|
2480
|
+
const char* const* cat_labels,
|
|
2481
|
+
const int* first_idx, const int* span_len) {
|
|
2482
|
+
int margin = s->show_prices ? 8 : 0;
|
|
2483
|
+
char max_label[16] = "";
|
|
2484
|
+
char min_label[16] = "";
|
|
2485
|
+
if (margin > 0) {
|
|
2486
|
+
snprintf(max_label, sizeof(max_label), "%8.4g", max_total);
|
|
2487
|
+
snprintf(min_label, sizeof(min_label), "%8.0f", 0.0);
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
int footer = s->show_labels ? 1 : 0;
|
|
2491
|
+
const int line_len = width + margin;
|
|
2492
|
+
const int rows = height + footer;
|
|
2493
|
+
/* Bounded by CC_MAX_CELLS: width*height <= 1e6, so this is <= ~32 MB. */
|
|
2494
|
+
size_t total_size = (size_t)line_len * (size_t)rows * 32 + (size_t)rows + 1;
|
|
2495
|
+
char* chart = (char*)calloc(total_size, 1);
|
|
2496
|
+
if (chart == NULL) return NULL;
|
|
2497
|
+
|
|
2498
|
+
char* w = chart;
|
|
2499
|
+
for (int y = height - 1; y >= 0; y--) {
|
|
2500
|
+
if (margin > 0) {
|
|
2501
|
+
char mlabel[16] = "";
|
|
2502
|
+
if (y == height - 1) snprintf(mlabel, sizeof(mlabel), "%8s", max_label);
|
|
2503
|
+
else if (y == 0) snprintf(mlabel, sizeof(mlabel), "%8s", min_label);
|
|
2504
|
+
else { memset(mlabel, ' ', 8); mlabel[8] = '\0'; }
|
|
2505
|
+
size_t ml = strlen(mlabel);
|
|
2506
|
+
memcpy(w, mlabel, ml);
|
|
2507
|
+
w += ml;
|
|
2508
|
+
}
|
|
2509
|
+
for (int x = 0; x < width; x++) {
|
|
2510
|
+
const char* cell = columns + ((size_t)x * height + (size_t)y) * 32;
|
|
2511
|
+
size_t cl = strlen(cell);
|
|
2512
|
+
memcpy(w, cell, cl);
|
|
2513
|
+
w += cl;
|
|
2514
|
+
}
|
|
2515
|
+
*w++ = '\n';
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
if (footer) {
|
|
2519
|
+
if (margin > 0) {
|
|
2520
|
+
memset(w, ' ', (size_t)margin);
|
|
2521
|
+
w += margin;
|
|
2522
|
+
}
|
|
2523
|
+
for (int x = 0; x < width; x++) {
|
|
2524
|
+
int span = span_len[x];
|
|
2525
|
+
if (span < 1) span = 1;
|
|
2526
|
+
const char* label = (cat_labels != NULL) ? cat_labels[first_idx[x]] : NULL;
|
|
2527
|
+
const char* L = (label != NULL) ? label : "";
|
|
2528
|
+
size_t n = strlen(L);
|
|
2529
|
+
if (n > (size_t)span) n = (size_t)span;
|
|
2530
|
+
if (n > 0) { memcpy(w, L, n); w += n; }
|
|
2531
|
+
if ((size_t)span > n) {
|
|
2532
|
+
memset(w, ' ', (size_t)span - n);
|
|
2533
|
+
w += (size_t)span - n;
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
*w++ = '\n';
|
|
2537
|
+
}
|
|
2538
|
+
*w = '\0';
|
|
2539
|
+
return chart;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
CC_INLINE char* cc_stack_create(const cc_stack_series_t* series, int series_count,
|
|
2543
|
+
int width, int height,
|
|
2544
|
+
const cc_stack_settings_t* settings) {
|
|
2545
|
+
/* The values lengths come only from settings->cats, so a NULL settings
|
|
2546
|
+
* (or cats <= 0) is a parsing failure, not a "use defaults" signal. */
|
|
2547
|
+
if (series == NULL || series_count <= 0 || !cc_dim_ok(width, height) ||
|
|
2548
|
+
settings == NULL || settings->cats <= 0) {
|
|
2549
|
+
return (char*)calloc(1, sizeof(char));
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
int cats = settings->cats;
|
|
2553
|
+
|
|
2554
|
+
cc_stack_settings_t s;
|
|
2555
|
+
/* All pointer/int fields: a partial {0} brace initializer matches NULL,
|
|
2556
|
+
* so there is no sentinel ambiguity (unlike the histogram's doubles). */
|
|
2557
|
+
s.colors = settings->colors;
|
|
2558
|
+
s.bg_color = settings->bg_color;
|
|
2559
|
+
s.cat_labels = settings->cat_labels;
|
|
2560
|
+
s.series = settings->series;
|
|
2561
|
+
s.cats = cats;
|
|
2562
|
+
s.show_labels = (settings->show_labels < 0) ? 0 : settings->show_labels;
|
|
2563
|
+
s.show_prices = (settings->show_prices < 0) ? 0 : settings->show_prices;
|
|
2564
|
+
|
|
2565
|
+
double* segs = (double*)malloc((size_t)series_count * sizeof(double));
|
|
2566
|
+
double* cum = (double*)malloc(((size_t)series_count + 1) * sizeof(double));
|
|
2567
|
+
int* pix = (int*)malloc(((size_t)series_count + 1) * sizeof(int));
|
|
2568
|
+
double* coltot = (double*)malloc((size_t)width * sizeof(double));
|
|
2569
|
+
int* first_idx = (int*)malloc((size_t)width * sizeof(int));
|
|
2570
|
+
int* span_len = (int*)malloc((size_t)width * sizeof(int));
|
|
2571
|
+
char* columns = (char*)malloc((size_t)width * (size_t)height * 32);
|
|
2572
|
+
if (segs == NULL || cum == NULL || pix == NULL || coltot == NULL ||
|
|
2573
|
+
first_idx == NULL || span_len == NULL || columns == NULL) {
|
|
2574
|
+
free(segs); free(cum); free(pix); free(coltot);
|
|
2575
|
+
free(first_idx); free(span_len); free(columns);
|
|
2576
|
+
return NULL;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
/* Fold the categories into `width` columns. The same long-arithmetic
|
|
2580
|
+
* mapping covers both decompositions: width >= cats spreads each category
|
|
2581
|
+
* over several columns (repeating its totals), width < cats aggregates
|
|
2582
|
+
* several categories per column whose segment sums are ADDED (stacked
|
|
2583
|
+
* bars are about totals). */
|
|
2584
|
+
double max_total = 0.0;
|
|
2585
|
+
for (int w = 0; w < width; w++) {
|
|
2586
|
+
int is = (int)(((long long)w * cats) / width);
|
|
2587
|
+
int ie = (int)(((((long long)w + 1) * cats) / width));
|
|
2588
|
+
if (ie <= is) ie = is + 1;
|
|
2589
|
+
if (ie > cats) ie = cats;
|
|
2590
|
+
first_idx[w] = is;
|
|
2591
|
+
span_len[w] = ie - is;
|
|
2592
|
+
|
|
2593
|
+
double ct = 0.0; /* zero baseline; negative values clamp to 0 */
|
|
2594
|
+
for (int s_i = 0; s_i < series_count; s_i++) {
|
|
2595
|
+
const double* vv = series[s_i].values;
|
|
2596
|
+
double seg = 0.0;
|
|
2597
|
+
for (int c = is; c < ie; c++) {
|
|
2598
|
+
double val = vv[c];
|
|
2599
|
+
if (val < 0.0) val = 0.0;
|
|
2600
|
+
seg += val;
|
|
2601
|
+
}
|
|
2602
|
+
segs[s_i] = seg;
|
|
2603
|
+
ct += seg;
|
|
2604
|
+
}
|
|
2605
|
+
coltot[w] = ct;
|
|
2606
|
+
if (ct > max_total) max_total = ct;
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
int pixel_height = height * 8;
|
|
2610
|
+
double scale = (max_total > 0.0) ? (double)pixel_height / max_total : 0.0;
|
|
2611
|
+
|
|
2612
|
+
for (int w = 0; w < width; w++) {
|
|
2613
|
+
/* Running top pixel after each series (cum[s+1]) — the boundary
|
|
2614
|
+
* between stacked segment s and s+1 in render pixels. */
|
|
2615
|
+
cum[0] = 0.0;
|
|
2616
|
+
for (int s_i = 0; s_i < series_count; s_i++) cum[s_i + 1] = cum[s_i] + segs[s_i];
|
|
2617
|
+
cum[series_count] = coltot[w]; /* exact, not a rounding drift */
|
|
2618
|
+
for (int s_i = 0; s_i <= series_count; s_i++) {
|
|
2619
|
+
pix[s_i] = (int)(cum[s_i] * scale);
|
|
2620
|
+
if (pix[s_i] < 0) pix[s_i] = 0;
|
|
2621
|
+
if (pix[s_i] > pixel_height) pix[s_i] = pixel_height;
|
|
2622
|
+
}
|
|
2623
|
+
int stack_top = pix[series_count];
|
|
2624
|
+
|
|
2625
|
+
char* col = columns + (size_t)w * height * 32;
|
|
2626
|
+
for (int r = 0; r < height; r++) {
|
|
2627
|
+
char* cell = col + (size_t)r * 32;
|
|
2628
|
+
int cell_lo = r * 8;
|
|
2629
|
+
if (stack_top <= cell_lo) {
|
|
2630
|
+
if (s.bg_color != NULL) {
|
|
2631
|
+
snprintf(cell, 32, "%s %s", s.bg_color,
|
|
2632
|
+
cc_reset_for(s.bg_color, NULL));
|
|
2633
|
+
} else {
|
|
2634
|
+
snprintf(cell, 32, " ");
|
|
2635
|
+
}
|
|
2636
|
+
continue;
|
|
2637
|
+
}
|
|
2638
|
+
/* The surface series: the segment that owns the topmost filled
|
|
2639
|
+
* pixel in this cell. pix is the running top after each series,
|
|
2640
|
+
* so the owning segment s satisfies pix[s] <= top_pixel <
|
|
2641
|
+
* pix[s+1]; zero-height (collapsed) series are skipped by the
|
|
2642
|
+
* loop naturally. */
|
|
2643
|
+
int cell_hi = r * 8 + 8;
|
|
2644
|
+
int top_pixel = (stack_top < cell_hi) ? (stack_top - 1) : (cell_hi - 1);
|
|
2645
|
+
int top_s = 0;
|
|
2646
|
+
while (top_s < series_count && pix[top_s + 1] <= top_pixel) top_s++;
|
|
2647
|
+
const char* color = cc_stack_palette_color(&s, top_s);
|
|
2648
|
+
int filled = stack_top - cell_lo;
|
|
2649
|
+
if (filled >= 8) {
|
|
2650
|
+
snprintf(cell, 32, "%s%s%s", CC_S(color), CC_BLOCK_FULL,
|
|
2651
|
+
cc_reset_for(color, NULL));
|
|
2652
|
+
} else {
|
|
2653
|
+
/* 1..7: the stack's top partial cell, in the surface series. */
|
|
2654
|
+
snprintf(cell, 32, "%s%s%s", CC_S(color), cc_lower_eighth(filled),
|
|
2655
|
+
cc_reset_for(color, NULL));
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
char* chart = cc_stack_assemble(width, height, columns, &s, max_total,
|
|
2661
|
+
s.cat_labels, first_idx, span_len);
|
|
2662
|
+
free(segs); free(cum); free(pix); free(coltot);
|
|
2663
|
+
free(first_idx); free(span_len); free(columns);
|
|
2664
|
+
return chart;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
/* ------------------------------ Heatmap renderer --------------------------
|
|
2668
|
+
* A heatmap draws a 2-D matrix onto a grid, coloring every covered cell by
|
|
2669
|
+
* its value's position between the matrix min and max on the fixed ladder
|
|
2670
|
+
* (CC_HEAT_RAMP above). Grid row 0 is the TOP (matrix row 0); columns run
|
|
2671
|
+
* left to right (matrix column 0). Each covered cell block-averages the
|
|
2672
|
+
* matrix values in its span; cells the matrix does not reach (matrix smaller
|
|
2673
|
+
* than the grid) are background. All scratch buffers are heap-allocated (no
|
|
2674
|
+
* VLAs) and freed on every path. */
|
|
2675
|
+
|
|
2676
|
+
/* Returns the matrix row span [*ys, *ye) that output row `gy` (from the top)
|
|
2677
|
+
* covers, plus a non-zero `*covered` when that span is non-empty. When the
|
|
2678
|
+
* matrix has rows > height it is downsampled (block-average); otherwise each
|
|
2679
|
+
* matrix row occupies its own output row on top and the rest are background. */
|
|
2680
|
+
static void cc_heat_row_span(int rows, int height, int gy,
|
|
2681
|
+
int* ys, int* ye, int* covered) {
|
|
2682
|
+
if (rows <= height) {
|
|
2683
|
+
if (gy < rows) { *ys = gy; *ye = gy + 1; *covered = 1; }
|
|
2684
|
+
else { *ys = rows; *ye = rows; *covered = 0; }
|
|
2685
|
+
} else {
|
|
2686
|
+
*ys = (int)(((long long)gy * rows) / height);
|
|
2687
|
+
*ye = (int)(((long long)(gy + 1) * rows) / height);
|
|
2688
|
+
if (*ye <= *ys) *ye = *ys + 1;
|
|
2689
|
+
if (*ye > rows) *ye = rows;
|
|
2690
|
+
*covered = (*ys < rows) ? 1 : 0;
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
static void cc_heat_col_span(int cols, int width, int gx,
|
|
2695
|
+
int* xs, int* xe, int* covered) {
|
|
2696
|
+
if (cols <= width) {
|
|
2697
|
+
if (gx < cols) { *xs = gx; *xe = gx + 1; *covered = 1; }
|
|
2698
|
+
else { *xs = cols; *xe = cols; *covered = 0; }
|
|
2699
|
+
} else {
|
|
2700
|
+
*xs = (int)(((long long)gx * cols) / width);
|
|
2701
|
+
*xe = (int)(((long long)(gx + 1) * cols) / width);
|
|
2702
|
+
if (*xe <= *xs) *xe = *xs + 1;
|
|
2703
|
+
if (*xe > cols) *xe = cols;
|
|
2704
|
+
*covered = (*xs < cols) ? 1 : 0;
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
CC_INLINE char* cc_heat_create(const double* values, int rows, int cols,
|
|
2709
|
+
int width, int height,
|
|
2710
|
+
const cc_heat_settings_t* settings) {
|
|
2711
|
+
int gy, gx, i;
|
|
2712
|
+
if (values == NULL || rows <= 0 || cols <= 0 ||
|
|
2713
|
+
!cc_dim_ok(width, height)) {
|
|
2714
|
+
return (char*)calloc(1, sizeof(char));
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
cc_heat_settings_t s;
|
|
2718
|
+
/* All pointer/int fields: a partial {0} brace initializer matches NULL,
|
|
2719
|
+
* so there is no sentinel ambiguity (unlike the histogram's doubles). */
|
|
2720
|
+
s.low_color = (settings && settings->low_color) ? settings->low_color : CC_COLOR_BRIGHT_BLACK;
|
|
2721
|
+
s.high_color = (settings && settings->high_color) ? settings->high_color : CC_COLOR_BRIGHT_WHITE;
|
|
2722
|
+
s.mid_color = settings ? settings->mid_color : NULL;
|
|
2723
|
+
s.bg_color = settings ? settings->bg_color : NULL;
|
|
2724
|
+
s.row_labels = settings ? settings->row_labels : NULL;
|
|
2725
|
+
s.col_labels = settings ? settings->col_labels : NULL;
|
|
2726
|
+
s.show_labels = (settings && settings->show_labels < 0) ? 0
|
|
2727
|
+
: (settings ? settings->show_labels : 0);
|
|
2728
|
+
|
|
2729
|
+
double dmin = values[0];
|
|
2730
|
+
double dmax = values[0];
|
|
2731
|
+
for (i = 1; i < rows * cols; i++) {
|
|
2732
|
+
if (values[i] < dmin) dmin = values[i];
|
|
2733
|
+
if (values[i] > dmax) dmax = values[i];
|
|
2734
|
+
}
|
|
2735
|
+
double range = dmax - dmin;
|
|
2736
|
+
|
|
2737
|
+
/* Resolve the ladder: fixed ramp, with the settings color endpoints
|
|
2738
|
+
* substituted and an optional mid color replacing the middle entry. */
|
|
2739
|
+
const char* ladder[CC_HEAT_RAMP_LEN];
|
|
2740
|
+
ladder[0] = CC_COLOR_BRIGHT_BLACK;
|
|
2741
|
+
ladder[1] = CC_COLOR_BLUE;
|
|
2742
|
+
ladder[2] = CC_COLOR_CYAN;
|
|
2743
|
+
ladder[3] = CC_COLOR_BRIGHT_CYAN;
|
|
2744
|
+
ladder[4] = CC_COLOR_GREEN;
|
|
2745
|
+
ladder[5] = CC_COLOR_YELLOW;
|
|
2746
|
+
ladder[6] = CC_COLOR_BRIGHT_YELLOW;
|
|
2747
|
+
ladder[7] = CC_COLOR_RED;
|
|
2748
|
+
ladder[8] = CC_COLOR_BRIGHT_RED;
|
|
2749
|
+
ladder[9] = CC_COLOR_BRIGHT_WHITE;
|
|
2750
|
+
ladder[0] = s.low_color;
|
|
2751
|
+
ladder[CC_HEAT_RAMP_LEN - 1] = s.high_color;
|
|
2752
|
+
if (s.mid_color != NULL) ladder[CC_HEAT_MID_INDEX] = s.mid_color;
|
|
2753
|
+
/* Plain mode mirrors the other charts' empty-color convention: an empty
|
|
2754
|
+
* low_color (what the Python plain=True path hands in) blanks the WHOLE
|
|
2755
|
+
* ladder so no ANSI escape from the fixed interior ramp leaks out. */
|
|
2756
|
+
if (s.low_color != NULL && s.low_color[0] == '\0') {
|
|
2757
|
+
for (i = 0; i < CC_HEAT_RAMP_LEN; i++) ladder[i] = "";
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
char* columns = (char*)malloc((size_t)width * (size_t)height * 32);
|
|
2761
|
+
if (columns == NULL) return NULL;
|
|
2762
|
+
|
|
2763
|
+
/* Cells are indexed [(gy * width + gx) * 32], gy from the TOP, so the
|
|
2764
|
+
* assembly loop reads rows top-to-bottom in matrix order. */
|
|
2765
|
+
for (gy = 0; gy < height; gy++) {
|
|
2766
|
+
int ys, ye, ycov;
|
|
2767
|
+
cc_heat_row_span(rows, height, gy, &ys, &ye, &ycov);
|
|
2768
|
+
for (gx = 0; gx < width; gx++) {
|
|
2769
|
+
int xs, xe, xcov;
|
|
2770
|
+
cc_heat_col_span(cols, width, gx, &xs, &xe, &xcov);
|
|
2771
|
+
char* cell = columns + ((size_t)gy * (size_t)width + (size_t)gx) * 32;
|
|
2772
|
+
|
|
2773
|
+
if (!ycov || !xcov) {
|
|
2774
|
+
if (s.bg_color != NULL) {
|
|
2775
|
+
snprintf(cell, 32, "%s %s", s.bg_color,
|
|
2776
|
+
cc_reset_for(s.bg_color, NULL));
|
|
2777
|
+
} else {
|
|
2778
|
+
snprintf(cell, 32, " ");
|
|
2779
|
+
}
|
|
2780
|
+
continue;
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
/* Block-average the covered matrix cells. */
|
|
2784
|
+
double sum = 0.0;
|
|
2785
|
+
int r, c, n = 0;
|
|
2786
|
+
for (r = ys; r < ye; r++) {
|
|
2787
|
+
for (c = xs; c < xe; c++) {
|
|
2788
|
+
sum += values[(size_t)r * (size_t)cols + (size_t)c];
|
|
2789
|
+
n++;
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
if (n < 1) continue;
|
|
2793
|
+
double avg = sum / (double)n;
|
|
2794
|
+
|
|
2795
|
+
double rr = (range > 0.0) ? (avg - dmin) / range : 0.0;
|
|
2796
|
+
if (rr < 0.0) rr = 0.0;
|
|
2797
|
+
if (rr > 1.0) rr = 1.0;
|
|
2798
|
+
int idx = (int)(rr * (CC_HEAT_RAMP_LEN - 1));
|
|
2799
|
+
if (idx >= CC_HEAT_RAMP_LEN) idx = CC_HEAT_RAMP_LEN - 1;
|
|
2800
|
+
const char* color = ladder[idx];
|
|
2801
|
+
snprintf(cell, 32, "%s%s%s", CC_S(color), CC_BLOCK_FULL,
|
|
2802
|
+
cc_reset_for(color, NULL));
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
/* Label frame: an optional left row-label margin and an optional footer
|
|
2807
|
+
* row of column labels, both gated by show_labels, mirroring the bar/
|
|
2808
|
+
* stack footer (each label truncated to its column span). */
|
|
2809
|
+
int rmargin = 0;
|
|
2810
|
+
if (s.show_labels && s.row_labels != NULL) {
|
|
2811
|
+
for (i = 0; i < rows; i++) {
|
|
2812
|
+
if (s.row_labels[i] != NULL) {
|
|
2813
|
+
size_t n = strlen(s.row_labels[i]);
|
|
2814
|
+
if (n > (size_t)rmargin) rmargin = (int)n;
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
if (rmargin > 255) rmargin = 255; /* fixed label-margin cap */
|
|
2818
|
+
}
|
|
2819
|
+
int footer = (s.show_labels && s.col_labels != NULL) ? 1 : 0;
|
|
2820
|
+
const int line_len = width + rmargin;
|
|
2821
|
+
const int rows_out = height + footer;
|
|
2822
|
+
/* Bounded by CC_MAX_CELLS: width*height <= 1e6. */
|
|
2823
|
+
size_t total = (size_t)line_len * (size_t)rows_out * 32 + (size_t)rows_out + 1;
|
|
2824
|
+
char* chart = (char*)calloc(total, 1);
|
|
2825
|
+
if (chart == NULL) {
|
|
2826
|
+
free(columns);
|
|
2827
|
+
return NULL;
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
char* w = chart;
|
|
2831
|
+
for (gy = 0; gy < height; gy++) {
|
|
2832
|
+
if (rmargin > 0) {
|
|
2833
|
+
int ys, ye, ycov;
|
|
2834
|
+
cc_heat_row_span(rows, height, gy, &ys, &ye, &ycov);
|
|
2835
|
+
char lbl[256] = "";
|
|
2836
|
+
if (ycov && s.row_labels != NULL && s.row_labels[ys] != NULL) {
|
|
2837
|
+
snprintf(lbl, sizeof(lbl), "%*s", rmargin, s.row_labels[ys]);
|
|
2838
|
+
} else {
|
|
2839
|
+
memset(lbl, ' ', (size_t)rmargin);
|
|
2840
|
+
lbl[rmargin] = '\0';
|
|
2841
|
+
}
|
|
2842
|
+
memcpy(w, lbl, (size_t)rmargin);
|
|
2843
|
+
w += (size_t)rmargin;
|
|
2844
|
+
}
|
|
2845
|
+
for (gx = 0; gx < width; gx++) {
|
|
2846
|
+
const char* cell = columns + ((size_t)gy * (size_t)width + (size_t)gx) * 32;
|
|
2847
|
+
size_t cl = strlen(cell);
|
|
2848
|
+
memcpy(w, cell, cl);
|
|
2849
|
+
w += cl;
|
|
2850
|
+
}
|
|
2851
|
+
*w++ = '\n';
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
if (footer) {
|
|
2855
|
+
if (rmargin > 0) {
|
|
2856
|
+
memset(w, ' ', (size_t)rmargin);
|
|
2857
|
+
w += (size_t)rmargin;
|
|
2858
|
+
}
|
|
2859
|
+
for (gx = 0; gx < width; gx++) {
|
|
2860
|
+
int xs, xe, xcov;
|
|
2861
|
+
cc_heat_col_span(cols, width, gx, &xs, &xe, &xcov);
|
|
2862
|
+
if (!xcov) { *w++ = ' '; continue; }
|
|
2863
|
+
int span = xe - xs;
|
|
2864
|
+
if (span < 1) span = 1;
|
|
2865
|
+
const char* T = (s.col_labels != NULL && s.col_labels[xs] != NULL)
|
|
2866
|
+
? s.col_labels[xs] : "";
|
|
2867
|
+
size_t n = strlen(T);
|
|
2868
|
+
if (n > (size_t)span) n = (size_t)span;
|
|
2869
|
+
if (n > 0) { memcpy(w, T, n); w += n; }
|
|
2870
|
+
if ((size_t)span > n) {
|
|
2871
|
+
memset(w, ' ', (size_t)span - n);
|
|
2872
|
+
w += (size_t)span - n;
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
*w++ = '\n';
|
|
2876
|
+
}
|
|
2877
|
+
*w = '\0';
|
|
2878
|
+
free(columns);
|
|
2879
|
+
return chart;
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
/* ------------------------------- Box renderer ----------------------------
|
|
2883
|
+
* A box plot draws one five-number summary per category. The five values are
|
|
2884
|
+
* computed by nearest-rank (see cc_box_create's documented convention) from
|
|
2885
|
+
* a sorted copy of each category's samples; the global min/max across every
|
|
2886
|
+
* category span the value axis, and each box, its whiskers and its median
|
|
2887
|
+
* are placed at 8-sub-pixel rows. The horizontal placement and exact glyph
|
|
2888
|
+
* mapping are fully specified in cc_box_create. All scratch buffers are
|
|
2889
|
+
* heap-allocated (no VLAs) and freed on every path. */
|
|
2890
|
+
|
|
2891
|
+
/* A category's five-number summary (computed by cc_box_summary). */
|
|
2892
|
+
typedef struct cc_box_five {
|
|
2893
|
+
double lo; /* min */
|
|
2894
|
+
double q1; /* Q1 */
|
|
2895
|
+
double md; /* median */
|
|
2896
|
+
double q3; /* Q3 */
|
|
2897
|
+
double hi; /* max */
|
|
2898
|
+
} cc_box_five;
|
|
2899
|
+
|
|
2900
|
+
CC_INLINE int cc_box_cmp(const void* a, const void* b) {
|
|
2901
|
+
double x = *(const double*)a;
|
|
2902
|
+
double y = *(const double*)b;
|
|
2903
|
+
return (x > y) - (x < y);
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
/* Sorts s[0..n-1] in place (the caller owns a copy) and returns the
|
|
2907
|
+
* nearest-rank five-number summary. The rank indices use integer arithmetic
|
|
2908
|
+
* so floor((n-1)*0.25) is exact: idx/4, idx/2, idx*3/4 for idx = n-1. */
|
|
2909
|
+
CC_INLINE cc_box_five cc_box_summary(double* s, int n) {
|
|
2910
|
+
cc_box_five f;
|
|
2911
|
+
long long idx = (long long)(n - 1);
|
|
2912
|
+
int q1i = (int)(idx / 4);
|
|
2913
|
+
int mdi = (int)(idx / 2);
|
|
2914
|
+
int q3i = (int)((idx * 3) / 4);
|
|
2915
|
+
qsort(s, (size_t)n, sizeof(double), cc_box_cmp);
|
|
2916
|
+
f.lo = s[0];
|
|
2917
|
+
f.q1 = s[q1i];
|
|
2918
|
+
f.md = s[mdi];
|
|
2919
|
+
f.q3 = s[q3i];
|
|
2920
|
+
f.hi = s[n - 1];
|
|
2921
|
+
return f;
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
/* Maps a value to a 0-based sub-pixel row (bottom 0 .. pixel_height - 1),
|
|
2925
|
+
* scaling the full value span across the chart height. */
|
|
2926
|
+
CC_INLINE int cc_box_level(double v, double gmin, double gmax,
|
|
2927
|
+
int pixel_height) {
|
|
2928
|
+
if (gmax == gmin) return pixel_height / 2;
|
|
2929
|
+
double r = (v - gmin) / (gmax - gmin);
|
|
2930
|
+
int l = (int)(r * (pixel_height - 1));
|
|
2931
|
+
if (l < 0) l = 0;
|
|
2932
|
+
if (l >= pixel_height) l = pixel_height - 1;
|
|
2933
|
+
return l;
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
/* Joins the per-cell strings (columns[(x*height + y)*32 .. +32), y counted
|
|
2937
|
+
* from the bottom) into the final box-string, printing rows top-to-bottom.
|
|
2938
|
+
* An optional 8-column value axis (show_prices: global max top, global min
|
|
2939
|
+
* bottom) is prepended to every row. Allocates with a pointer cursor (no
|
|
2940
|
+
* strcat re-scans); caller frees. */
|
|
2941
|
+
CC_INLINE char* cc_box_assemble(int width, int height, char* columns,
|
|
2942
|
+
const cc_box_settings_t* s,
|
|
2943
|
+
double gmin, double gmax) {
|
|
2944
|
+
int margin = s->show_prices ? 8 : 0;
|
|
2945
|
+
char max_label[16] = "";
|
|
2946
|
+
char min_label[16] = "";
|
|
2947
|
+
if (margin > 0) {
|
|
2948
|
+
snprintf(max_label, sizeof(max_label), "%8.4g", gmax);
|
|
2949
|
+
snprintf(min_label, sizeof(min_label), "%8.4g", gmin);
|
|
2950
|
+
}
|
|
2951
|
+
const int line_len = width + margin;
|
|
2952
|
+
/* Bounded by CC_MAX_CELLS: width*height <= 1e6, so this is <= ~32 MB. */
|
|
2953
|
+
size_t total_size = (size_t)line_len * (size_t)height * 32 + (size_t)height + 1;
|
|
2954
|
+
char* chart = (char*)calloc(total_size, 1);
|
|
2955
|
+
if (chart == NULL) return NULL;
|
|
2956
|
+
|
|
2957
|
+
char* w = chart;
|
|
2958
|
+
for (int y = height - 1; y >= 0; y--) {
|
|
2959
|
+
if (margin > 0) {
|
|
2960
|
+
char mlabel[16] = "";
|
|
2961
|
+
if (y == height - 1) snprintf(mlabel, sizeof(mlabel), "%8s", max_label);
|
|
2962
|
+
else if (y == 0) snprintf(mlabel, sizeof(mlabel), "%8s", min_label);
|
|
2963
|
+
else { memset(mlabel, ' ', 8); mlabel[8] = '\0'; }
|
|
2964
|
+
size_t ml = strlen(mlabel);
|
|
2965
|
+
memcpy(w, mlabel, ml);
|
|
2966
|
+
w += ml;
|
|
2967
|
+
}
|
|
2968
|
+
for (int x = 0; x < width; x++) {
|
|
2969
|
+
const char* cell = columns + ((size_t)x * height + (size_t)y) * 32;
|
|
2970
|
+
size_t cl = strlen(cell);
|
|
2971
|
+
memcpy(w, cell, cl);
|
|
2972
|
+
w += cl;
|
|
2973
|
+
}
|
|
2974
|
+
*w++ = '\n';
|
|
2975
|
+
}
|
|
2976
|
+
*w = '\0';
|
|
2977
|
+
return chart;
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
CC_INLINE char* cc_box_create(const cc_box_category_t* cats, int cat_count,
|
|
2981
|
+
int width, int height,
|
|
2982
|
+
const cc_box_settings_t* settings) {
|
|
2983
|
+
int i, w, r;
|
|
2984
|
+
long long off = 0;
|
|
2985
|
+
long long total = 0;
|
|
2986
|
+
if (cats == NULL || cat_count <= 0 || !cc_dim_ok(width, height)) {
|
|
2987
|
+
return (char*)calloc(1, sizeof(char));
|
|
2988
|
+
}
|
|
2989
|
+
/* A category with no samples has no well-defined five-number summary, so
|
|
2990
|
+
* one empty category empties the whole chart. */
|
|
2991
|
+
for (i = 0; i < cat_count; i++) {
|
|
2992
|
+
if (cats[i].samples == NULL || cats[i].n <= 0) {
|
|
2993
|
+
return (char*)calloc(1, sizeof(char));
|
|
2994
|
+
}
|
|
2995
|
+
total += cats[i].n;
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
cc_box_settings_t s;
|
|
2999
|
+
/* All pointer/int fields: a partial {0} brace initializer matches NULL,
|
|
3000
|
+
* so there is no sentinel ambiguity (unlike the histogram's doubles). */
|
|
3001
|
+
s.rise_color = (settings && settings->rise_color) ? settings->rise_color
|
|
3002
|
+
: CC_COLOR_GREEN;
|
|
3003
|
+
s.area_color = (settings && settings->area_color &&
|
|
3004
|
+
settings->area_color[0] != '\0')
|
|
3005
|
+
? settings->area_color : s.rise_color;
|
|
3006
|
+
s.bg_color = settings ? settings->bg_color : NULL;
|
|
3007
|
+
s.show_prices = settings ? settings->show_prices : 0;
|
|
3008
|
+
if (s.show_prices < 0) s.show_prices = 0;
|
|
3009
|
+
|
|
3010
|
+
double* scratch = (double*)malloc((size_t)total * sizeof(double));
|
|
3011
|
+
cc_box_five* fives = (cc_box_five*)malloc((size_t)cat_count * sizeof(cc_box_five));
|
|
3012
|
+
char* columns = (char*)malloc((size_t)width * (size_t)height * 32);
|
|
3013
|
+
if (scratch == NULL || fives == NULL || columns == NULL) {
|
|
3014
|
+
free(scratch); free(fives); free(columns);
|
|
3015
|
+
return NULL;
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
double gmin = 0.0, gmax = 0.0;
|
|
3019
|
+
int have_minmax = 0;
|
|
3020
|
+
for (i = 0; i < cat_count; i++) {
|
|
3021
|
+
memcpy(scratch + off, cats[i].samples,
|
|
3022
|
+
(size_t)cats[i].n * sizeof(double));
|
|
3023
|
+
fives[i] = cc_box_summary(scratch + off, cats[i].n);
|
|
3024
|
+
if (!have_minmax || fives[i].lo < gmin) gmin = fives[i].lo;
|
|
3025
|
+
if (!have_minmax || fives[i].hi > gmax) gmax = fives[i].hi;
|
|
3026
|
+
have_minmax = 1;
|
|
3027
|
+
off += cats[i].n;
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
int pixel_height = height * 8;
|
|
3031
|
+
|
|
3032
|
+
for (w = 0; w < width; w++) {
|
|
3033
|
+
int cs = (int)(((long long)w * cat_count) / width);
|
|
3034
|
+
cc_box_five f = fives[cs];
|
|
3035
|
+
int lo = cc_box_level(f.lo, gmin, gmax, pixel_height) / 8;
|
|
3036
|
+
int q1 = cc_box_level(f.q1, gmin, gmax, pixel_height) / 8;
|
|
3037
|
+
int md = cc_box_level(f.md, gmin, gmax, pixel_height) / 8;
|
|
3038
|
+
int q3 = cc_box_level(f.q3, gmin, gmax, pixel_height) / 8;
|
|
3039
|
+
int hi = cc_box_level(f.hi, gmin, gmax, pixel_height) / 8;
|
|
3040
|
+
int lq1 = cc_box_level(f.q1, gmin, gmax, pixel_height);
|
|
3041
|
+
int lq3 = cc_box_level(f.q3, gmin, gmax, pixel_height);
|
|
3042
|
+
const char* wc = s.area_color;
|
|
3043
|
+
const char* bc = s.rise_color;
|
|
3044
|
+
char* col = columns + (size_t)w * (size_t)height * 32;
|
|
3045
|
+
const char* reset_b = cc_reset_for(bc, NULL);
|
|
3046
|
+
const char* reset_w = cc_reset_for(wc, NULL);
|
|
3047
|
+
|
|
3048
|
+
for (r = 0; r < height; r++) {
|
|
3049
|
+
char* cell = col + (size_t)r * 32;
|
|
3050
|
+
int in_box = (q1 <= r && r <= q3);
|
|
3051
|
+
int in_whisker = (lo <= r && r <= hi);
|
|
3052
|
+
const char* boxglyph = CC_BLOCK_FULL;
|
|
3053
|
+
|
|
3054
|
+
if (in_box) {
|
|
3055
|
+
/* Median (rule 1) is a solid full line even on a partial
|
|
3056
|
+
* top edge; interior (rule 2) is a full block. */
|
|
3057
|
+
if (r == md) {
|
|
3058
|
+
boxglyph = CC_BLOCK_FULL;
|
|
3059
|
+
} else if (r == q3 && q1 < q3) {
|
|
3060
|
+
/* top edge (rule 3): the Q3 sub-pixel, precise. */
|
|
3061
|
+
boxglyph = cc_lower_eighth((lq3 % 8) + 1);
|
|
3062
|
+
} else if (r == q1 && q1 == q3) {
|
|
3063
|
+
/* single-row box (rule 4): Q1..Q3 height. */
|
|
3064
|
+
boxglyph = cc_lower_eighth(lq3 - lq1 + 1);
|
|
3065
|
+
} else {
|
|
3066
|
+
boxglyph = CC_BLOCK_FULL; /* bottom edge / interior */
|
|
3067
|
+
}
|
|
3068
|
+
snprintf(cell, 32, "%s%s%s", CC_S(bc), boxglyph, reset_b);
|
|
3069
|
+
} else if (in_whisker) {
|
|
3070
|
+
snprintf(cell, 32, "%s%s%s", CC_S(wc), CC_LINE_VERTICAL, reset_w);
|
|
3071
|
+
} else if (s.bg_color != NULL) {
|
|
3072
|
+
snprintf(cell, 32, "%s %s", s.bg_color,
|
|
3073
|
+
cc_reset_for(s.bg_color, NULL));
|
|
3074
|
+
} else {
|
|
3075
|
+
snprintf(cell, 32, " ");
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
char* chart = cc_box_assemble(width, height, columns, &s, gmin, gmax);
|
|
3081
|
+
free(scratch);
|
|
3082
|
+
free(fives);
|
|
3083
|
+
free(columns);
|
|
3084
|
+
return chart;
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
#endif /* CCHARTS_IMPLEMENTATION */
|
|
3088
|
+
#endif /* CCHARTS_H */
|