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,620 @@
|
|
|
1
|
+
/* Generated by scripts/sync_sources.py from abi/ccharts_abi.c — do not edit.
|
|
2
|
+
* Change the original and re-run the script. */
|
|
3
|
+
/*
|
|
4
|
+
* ccharts_abi.c — the one translation unit that instantiates ccharts.h.
|
|
5
|
+
*
|
|
6
|
+
* Everything here is marshalling and validation; no chart logic lives in this
|
|
7
|
+
* file. The internals reused from the header (cc_dim_ok, cc_trim_whitespace,
|
|
8
|
+
* cc_parse_ts, ...) are visible because CCHARTS_IMPLEMENTATION is defined
|
|
9
|
+
* below, which is precisely why this layer has to exist at all.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/* ccharts.h calls gmtime_r, which is POSIX rather than ISO C: under a strict
|
|
13
|
+
* dialect (-std=c99 -pedantic, which is what Rust's cc crate and several
|
|
14
|
+
* package builds default to) glibc would hide the declaration. Requesting the
|
|
15
|
+
* POSIX feature set here keeps every consumer's flags free of it. MSVC uses
|
|
16
|
+
* gmtime_s and needs nothing. */
|
|
17
|
+
#if !defined(_WIN32) && !defined(_POSIX_C_SOURCE)
|
|
18
|
+
#define _POSIX_C_SOURCE 200809L
|
|
19
|
+
#endif
|
|
20
|
+
|
|
21
|
+
#define CCHARTS_IMPLEMENTATION
|
|
22
|
+
#include "ccharts.h"
|
|
23
|
+
|
|
24
|
+
#include <math.h>
|
|
25
|
+
#include <limits.h>
|
|
26
|
+
|
|
27
|
+
#include "ccharts_abi.h"
|
|
28
|
+
|
|
29
|
+
struct ccharts_data {
|
|
30
|
+
cc_ohlc_t* rows;
|
|
31
|
+
int32_t n;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/* Largest candle count the C library can carry (it uses int for the size). */
|
|
35
|
+
#define CCHARTS_MAX_ROWS ((int32_t)(INT_MAX / (int)sizeof(cc_ohlc_t)))
|
|
36
|
+
|
|
37
|
+
/* ---------------------------- Building data ---------------------------- */
|
|
38
|
+
|
|
39
|
+
/* Wraps an already-parsed cc_ohlc_t array, taking ownership of it. */
|
|
40
|
+
static ccharts_status wrap_rows(cc_ohlc_t* rows, int32_t n, ccharts_data** out) {
|
|
41
|
+
ccharts_data* data = (ccharts_data*)malloc(sizeof(ccharts_data));
|
|
42
|
+
if (data == NULL) {
|
|
43
|
+
free(rows);
|
|
44
|
+
return CCHARTS_ERR_NOMEM;
|
|
45
|
+
}
|
|
46
|
+
data->rows = rows;
|
|
47
|
+
data->n = n;
|
|
48
|
+
*out = data;
|
|
49
|
+
return CCHARTS_OK;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
int32_t ccharts_from_arrays(const double* open, const double* high,
|
|
53
|
+
const double* low, const double* close,
|
|
54
|
+
const int64_t* ts, int32_t n,
|
|
55
|
+
ccharts_data** out) {
|
|
56
|
+
cc_ohlc_t* rows;
|
|
57
|
+
int32_t i;
|
|
58
|
+
|
|
59
|
+
if (out == NULL) return CCHARTS_ERR_INVALID_ARG;
|
|
60
|
+
*out = NULL;
|
|
61
|
+
if (open == NULL || high == NULL || low == NULL || close == NULL) {
|
|
62
|
+
return CCHARTS_ERR_INVALID_ARG;
|
|
63
|
+
}
|
|
64
|
+
if (n <= 0 || n > CCHARTS_MAX_ROWS) return CCHARTS_ERR_INVALID_ARG;
|
|
65
|
+
|
|
66
|
+
for (i = 0; i < n; i++) {
|
|
67
|
+
if (!isfinite(open[i]) || !isfinite(high[i]) ||
|
|
68
|
+
!isfinite(low[i]) || !isfinite(close[i])) {
|
|
69
|
+
return CCHARTS_ERR_NON_FINITE;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
rows = (cc_ohlc_t*)calloc((size_t)n, sizeof(cc_ohlc_t));
|
|
74
|
+
if (rows == NULL) return CCHARTS_ERR_NOMEM;
|
|
75
|
+
|
|
76
|
+
for (i = 0; i < n; i++) {
|
|
77
|
+
rows[i].open = open[i];
|
|
78
|
+
rows[i].high = high[i];
|
|
79
|
+
rows[i].low = low[i];
|
|
80
|
+
rows[i].close = close[i];
|
|
81
|
+
rows[i].timestamp = (ts != NULL) ? (long long)ts[i] : 0;
|
|
82
|
+
}
|
|
83
|
+
return wrap_rows(rows, n, out);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
int32_t ccharts_parse_json(const char* json, ccharts_data** out) {
|
|
87
|
+
cc_ohlc_t* rows = NULL;
|
|
88
|
+
int n = 0;
|
|
89
|
+
|
|
90
|
+
if (out == NULL) return CCHARTS_ERR_INVALID_ARG;
|
|
91
|
+
*out = NULL;
|
|
92
|
+
if (json == NULL) return CCHARTS_ERR_INVALID_ARG;
|
|
93
|
+
|
|
94
|
+
if (cc_json_to_ohlc(json, &rows, &n) != 0 || n <= 0) {
|
|
95
|
+
free(rows);
|
|
96
|
+
return CCHARTS_ERR_PARSE;
|
|
97
|
+
}
|
|
98
|
+
return wrap_rows(rows, (int32_t)n, out);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Counts the lines cc_str_to_ohlc would actually store: a line contributes a
|
|
102
|
+
* candle when it holds at least one non-whitespace character (it trims with
|
|
103
|
+
* cc_trim_whitespace and skips what is left empty). */
|
|
104
|
+
static int32_t count_csv_rows(const char* csv, char line_separator) {
|
|
105
|
+
int32_t rows = 0;
|
|
106
|
+
int has_content = 0;
|
|
107
|
+
const char* p;
|
|
108
|
+
|
|
109
|
+
for (p = csv; ; p++) {
|
|
110
|
+
if (*p == '\0' || *p == line_separator) {
|
|
111
|
+
if (has_content) {
|
|
112
|
+
if (rows == CCHARTS_MAX_ROWS) return rows;
|
|
113
|
+
rows++;
|
|
114
|
+
}
|
|
115
|
+
has_content = 0;
|
|
116
|
+
if (*p == '\0') break;
|
|
117
|
+
} else if (*p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') {
|
|
118
|
+
has_content = 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return rows;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
int32_t ccharts_parse_csv(const char* csv, char value_separator,
|
|
125
|
+
char line_separator, ccharts_data** out) {
|
|
126
|
+
cc_ohlc_t* rows = NULL;
|
|
127
|
+
int32_t n;
|
|
128
|
+
|
|
129
|
+
if (out == NULL) return CCHARTS_ERR_INVALID_ARG;
|
|
130
|
+
*out = NULL;
|
|
131
|
+
if (csv == NULL || value_separator == '\0' || line_separator == '\0') {
|
|
132
|
+
return CCHARTS_ERR_INVALID_ARG;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
n = count_csv_rows(csv, line_separator);
|
|
136
|
+
if (n <= 0) return CCHARTS_ERR_PARSE;
|
|
137
|
+
|
|
138
|
+
if (cc_str_to_ohlc(csv, (int)n, &rows, value_separator, line_separator) != 0) {
|
|
139
|
+
free(rows);
|
|
140
|
+
return CCHARTS_ERR_NOMEM;
|
|
141
|
+
}
|
|
142
|
+
return wrap_rows(rows, n, out);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
int32_t ccharts_data_len(const ccharts_data* data) {
|
|
146
|
+
return (data != NULL) ? data->n : 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
void ccharts_data_free(ccharts_data* data) {
|
|
150
|
+
if (data != NULL) {
|
|
151
|
+
free(data->rows);
|
|
152
|
+
free(data);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* ------------------------------ Rendering ------------------------------ */
|
|
157
|
+
|
|
158
|
+
typedef char* (*cc_renderer)(const cc_ohlc_t*, int, int, int, const cc_settings_t*);
|
|
159
|
+
|
|
160
|
+
static ccharts_status render(cc_renderer draw, const ccharts_data* data,
|
|
161
|
+
int32_t width, int32_t height,
|
|
162
|
+
const ccharts_settings* settings,
|
|
163
|
+
char** out, size_t* out_len) {
|
|
164
|
+
cc_settings_t resolved = {0};
|
|
165
|
+
char* chart;
|
|
166
|
+
|
|
167
|
+
if (out == NULL) return CCHARTS_ERR_INVALID_ARG;
|
|
168
|
+
*out = NULL;
|
|
169
|
+
if (out_len != NULL) *out_len = 0;
|
|
170
|
+
if (data == NULL || data->rows == NULL || data->n <= 0) {
|
|
171
|
+
return CCHARTS_ERR_INVALID_ARG;
|
|
172
|
+
}
|
|
173
|
+
/* ccharts.h answers invalid dimensions with an empty string; the ABI
|
|
174
|
+
* reports them instead, using the library's own bounds check. */
|
|
175
|
+
if (!cc_dim_ok((int)width, (int)height)) return CCHARTS_ERR_DIMENSIONS;
|
|
176
|
+
|
|
177
|
+
if (settings != NULL) {
|
|
178
|
+
resolved.rise_color = settings->rise_color;
|
|
179
|
+
resolved.fall_color = settings->fall_color;
|
|
180
|
+
resolved.bg_color = settings->bg_color;
|
|
181
|
+
resolved.area_color = settings->area_color;
|
|
182
|
+
resolved.single_color = (int)settings->single_color;
|
|
183
|
+
resolved.show_prices = (int)settings->show_prices;
|
|
184
|
+
resolved.show_times = (int)settings->show_times;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
chart = draw(data->rows, (int)data->n, (int)width, (int)height, &resolved);
|
|
188
|
+
if (chart == NULL) return CCHARTS_ERR_NOMEM;
|
|
189
|
+
|
|
190
|
+
*out = chart;
|
|
191
|
+
if (out_len != NULL) *out_len = strlen(chart);
|
|
192
|
+
return CCHARTS_OK;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
int32_t ccharts_line(const ccharts_data* data, int32_t width,
|
|
196
|
+
int32_t height, const ccharts_settings* settings,
|
|
197
|
+
char** out, size_t* out_len) {
|
|
198
|
+
return render(cc_line_create, data, width, height, settings, out, out_len);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
int32_t ccharts_candle(const ccharts_data* data, int32_t width,
|
|
202
|
+
int32_t height, const ccharts_settings* settings,
|
|
203
|
+
char** out, size_t* out_len) {
|
|
204
|
+
return render(cc_candle_create, data, width, height, settings, out, out_len);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
void ccharts_string_free(char* s) {
|
|
208
|
+
free(s);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* -------------------------------- Pie -------------------------------- */
|
|
212
|
+
|
|
213
|
+
/* Copies the caller's per-slice palette into a NULL-terminated array, which
|
|
214
|
+
* is what cc_pie_settings_t expects; returns CCHARTS_OK and leaves *out
|
|
215
|
+
* pointing at the copy (free with free()) or NULL for the default palette. */
|
|
216
|
+
static ccharts_status resolve_pie_colors(const char* const* colors,
|
|
217
|
+
int32_t color_count,
|
|
218
|
+
const char*** out) {
|
|
219
|
+
const char** copy;
|
|
220
|
+
int32_t i;
|
|
221
|
+
|
|
222
|
+
*out = NULL;
|
|
223
|
+
if (colors == NULL || color_count <= 0) return CCHARTS_OK;
|
|
224
|
+
|
|
225
|
+
copy = (const char**)calloc((size_t)color_count + 1, sizeof(const char*));
|
|
226
|
+
if (copy == NULL) return CCHARTS_ERR_NOMEM;
|
|
227
|
+
for (i = 0; i < color_count; i++) copy[i] = colors[i];
|
|
228
|
+
*out = copy;
|
|
229
|
+
return CCHARTS_OK;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
int32_t ccharts_pie_from_slices(const ccharts_pie_slice* slices, int32_t count,
|
|
233
|
+
int32_t width, int32_t height,
|
|
234
|
+
int32_t donut,
|
|
235
|
+
const char* const* colors, int32_t color_count,
|
|
236
|
+
int32_t show_legend, int32_t show_pct,
|
|
237
|
+
double slice_gap, double inner_radius_ratio,
|
|
238
|
+
int32_t legend_format, double start_angle,
|
|
239
|
+
int32_t counter_clockwise,
|
|
240
|
+
const char* center_text,
|
|
241
|
+
char** out, size_t* out_len) {
|
|
242
|
+
cc_pie_settings_t ps;
|
|
243
|
+
cc_pie_slice_t* slice_rows = NULL;
|
|
244
|
+
const char** resolved_colors = NULL;
|
|
245
|
+
ccharts_status status;
|
|
246
|
+
char* chart;
|
|
247
|
+
int32_t i;
|
|
248
|
+
|
|
249
|
+
if (out == NULL) return CCHARTS_ERR_INVALID_ARG;
|
|
250
|
+
*out = NULL;
|
|
251
|
+
if (out_len != NULL) *out_len = 0;
|
|
252
|
+
if (slices == NULL || count <= 0) return CCHARTS_ERR_INVALID_ARG;
|
|
253
|
+
if (!cc_dim_ok((int)width, (int)height)) return CCHARTS_ERR_DIMENSIONS;
|
|
254
|
+
|
|
255
|
+
/* NaN/inf must not reach the renderer: cc_pie_create guards values with a
|
|
256
|
+
* positive-range check, but +inf would slip through (inf > 0 is true) and
|
|
257
|
+
* corrupt the angle math, and non-finite option doubles (slice_gap,
|
|
258
|
+
* inner_radius_ratio, start_angle) would poison the geometry. Mirror the
|
|
259
|
+
* ABI's finite guard for prices. */
|
|
260
|
+
for (i = 0; i < count; i++) {
|
|
261
|
+
if (!isfinite(slices[i].value)) return CCHARTS_ERR_NON_FINITE;
|
|
262
|
+
}
|
|
263
|
+
if (!isfinite(slice_gap) || !isfinite(inner_radius_ratio) ||
|
|
264
|
+
!isfinite(start_angle)) {
|
|
265
|
+
return CCHARTS_ERR_NON_FINITE;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
status = resolve_pie_colors(colors, color_count, &resolved_colors);
|
|
269
|
+
if (status != CCHARTS_OK) return status;
|
|
270
|
+
|
|
271
|
+
slice_rows = (cc_pie_slice_t*)calloc((size_t)count, sizeof(cc_pie_slice_t));
|
|
272
|
+
if (slice_rows == NULL) {
|
|
273
|
+
free(resolved_colors);
|
|
274
|
+
return CCHARTS_ERR_NOMEM;
|
|
275
|
+
}
|
|
276
|
+
for (i = 0; i < count; i++) {
|
|
277
|
+
slice_rows[i].label = slices[i].label;
|
|
278
|
+
slice_rows[i].value = slices[i].value;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
memset(&ps, 0, sizeof(ps));
|
|
282
|
+
ps.colors = resolved_colors;
|
|
283
|
+
ps.donut = (int)donut;
|
|
284
|
+
ps.show_legend = (int)show_legend;
|
|
285
|
+
ps.show_pct = (int)show_pct;
|
|
286
|
+
ps.slice_gap = slice_gap;
|
|
287
|
+
ps.inner_radius_ratio = inner_radius_ratio;
|
|
288
|
+
ps.legend_format = (int)legend_format;
|
|
289
|
+
ps.start_angle = start_angle;
|
|
290
|
+
ps.counter_clockwise = (int)counter_clockwise;
|
|
291
|
+
ps.center_text = center_text;
|
|
292
|
+
|
|
293
|
+
chart = cc_pie_create(slice_rows, (int)count, (int)width, (int)height, &ps);
|
|
294
|
+
free(slice_rows);
|
|
295
|
+
free(resolved_colors);
|
|
296
|
+
if (chart == NULL) return CCHARTS_ERR_NOMEM;
|
|
297
|
+
|
|
298
|
+
*out = chart;
|
|
299
|
+
if (out_len != NULL) *out_len = strlen(chart);
|
|
300
|
+
return CCHARTS_OK;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* ------------------------------ Histogram ------------------------------ */
|
|
304
|
+
|
|
305
|
+
int32_t ccharts_hist(const double* samples, int32_t count,
|
|
306
|
+
int32_t width, int32_t height,
|
|
307
|
+
const ccharts_hist_settings* settings,
|
|
308
|
+
char** out, size_t* out_len) {
|
|
309
|
+
cc_hist_settings_t hs;
|
|
310
|
+
char* chart;
|
|
311
|
+
int32_t i;
|
|
312
|
+
|
|
313
|
+
if (out == NULL) return CCHARTS_ERR_INVALID_ARG;
|
|
314
|
+
*out = NULL;
|
|
315
|
+
if (out_len != NULL) *out_len = 0;
|
|
316
|
+
if (samples == NULL || count <= 0) return CCHARTS_ERR_INVALID_ARG;
|
|
317
|
+
if (!cc_dim_ok((int)width, (int)height)) return CCHARTS_ERR_DIMENSIONS;
|
|
318
|
+
|
|
319
|
+
for (i = 0; i < count; i++) {
|
|
320
|
+
if (!isfinite(samples[i])) return CCHARTS_ERR_NON_FINITE;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
memset(&hs, 0, sizeof(hs));
|
|
324
|
+
if (settings != NULL) {
|
|
325
|
+
hs.rise_color = settings->rise_color;
|
|
326
|
+
hs.bg_color = settings->bg_color;
|
|
327
|
+
hs.bin_count = (int)settings->bin_count;
|
|
328
|
+
hs.min_value = settings->min_value;
|
|
329
|
+
hs.max_value = settings->max_value;
|
|
330
|
+
hs.show_bins = (int)settings->show_bins;
|
|
331
|
+
hs.show_prices = (int)settings->show_prices;
|
|
332
|
+
/* NaN min/max is the "auto" sentinel (allowed); inf must not reach
|
|
333
|
+
* the binned geometry, so reject any other non-finite value. */
|
|
334
|
+
if (settings->min_value == settings->min_value &&
|
|
335
|
+
!isfinite(settings->min_value)) return CCHARTS_ERR_NON_FINITE;
|
|
336
|
+
if (settings->max_value == settings->max_value &&
|
|
337
|
+
!isfinite(settings->max_value)) return CCHARTS_ERR_NON_FINITE;
|
|
338
|
+
} else {
|
|
339
|
+
hs.bin_count = 0; /* auto */
|
|
340
|
+
hs.min_value = 0.0 / 0.0; /* auto sentinel (NaN) */
|
|
341
|
+
hs.max_value = 0.0 / 0.0; /* auto sentinel (NaN) */
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
chart = cc_hist_create(samples, (int)count, (int)width, (int)height, &hs);
|
|
345
|
+
if (chart == NULL) return CCHARTS_ERR_NOMEM;
|
|
346
|
+
|
|
347
|
+
*out = chart;
|
|
348
|
+
if (out_len != NULL) *out_len = strlen(chart);
|
|
349
|
+
return CCHARTS_OK;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/* ------------------------------ Sparkline ------------------------------ */
|
|
353
|
+
|
|
354
|
+
int32_t ccharts_spark(const double* samples, int32_t count,
|
|
355
|
+
int32_t width, int32_t height,
|
|
356
|
+
const ccharts_spark_settings* settings,
|
|
357
|
+
char** out, size_t* out_len) {
|
|
358
|
+
cc_spark_settings_t ss;
|
|
359
|
+
char* chart;
|
|
360
|
+
int32_t i;
|
|
361
|
+
|
|
362
|
+
if (out == NULL) return CCHARTS_ERR_INVALID_ARG;
|
|
363
|
+
*out = NULL;
|
|
364
|
+
if (out_len != NULL) *out_len = 0;
|
|
365
|
+
if (samples == NULL || count <= 0) return CCHARTS_ERR_INVALID_ARG;
|
|
366
|
+
if (!cc_dim_ok((int)width, (int)height)) return CCHARTS_ERR_DIMENSIONS;
|
|
367
|
+
|
|
368
|
+
/* NaN/inf samples would poison the column-average min/max math (and
|
|
369
|
+
* lround in the mapping), so reject them at the boundary like every
|
|
370
|
+
* other price path. */
|
|
371
|
+
for (i = 0; i < count; i++) {
|
|
372
|
+
if (!isfinite(samples[i])) return CCHARTS_ERR_NON_FINITE;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
memset(&ss, 0, sizeof(ss));
|
|
376
|
+
if (settings != NULL) {
|
|
377
|
+
ss.rise_color = settings->rise_color;
|
|
378
|
+
ss.area_color = settings->area_color;
|
|
379
|
+
ss.min_above = (int)settings->min_above;
|
|
380
|
+
ss.min_below = (int)settings->min_below;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
chart = cc_spark_create(samples, (int)count, (int)width, (int)height, &ss);
|
|
384
|
+
if (chart == NULL) return CCHARTS_ERR_NOMEM;
|
|
385
|
+
|
|
386
|
+
*out = chart;
|
|
387
|
+
if (out_len != NULL) *out_len = strlen(chart);
|
|
388
|
+
return CCHARTS_OK;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/* ------------------------------ Bar chart ------------------------------ */
|
|
392
|
+
|
|
393
|
+
int32_t ccharts_bar(const ccharts_bar_slice* items, int32_t count,
|
|
394
|
+
int32_t width, int32_t height,
|
|
395
|
+
const ccharts_bar_settings* settings,
|
|
396
|
+
char** out, size_t* out_len) {
|
|
397
|
+
cc_bar_settings_t bs;
|
|
398
|
+
char* chart;
|
|
399
|
+
int32_t i;
|
|
400
|
+
|
|
401
|
+
if (out == NULL) return CCHARTS_ERR_INVALID_ARG;
|
|
402
|
+
*out = NULL;
|
|
403
|
+
if (out_len != NULL) *out_len = 0;
|
|
404
|
+
if (items == NULL || count <= 0) return CCHARTS_ERR_INVALID_ARG;
|
|
405
|
+
if (!cc_dim_ok((int)width, (int)height)) return CCHARTS_ERR_DIMENSIONS;
|
|
406
|
+
|
|
407
|
+
/* NaN/inf bar values would poison the column max/min math (and lround in
|
|
408
|
+
* the pixel mapping), so reject them at the boundary like every other
|
|
409
|
+
* price path. Negative values are clamped to zero by the renderer, not
|
|
410
|
+
* rejected. */
|
|
411
|
+
for (i = 0; i < count; i++) {
|
|
412
|
+
if (!isfinite(items[i].value)) return CCHARTS_ERR_NON_FINITE;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
memset(&bs, 0, sizeof(bs));
|
|
416
|
+
if (settings != NULL) {
|
|
417
|
+
bs.rise_color = settings->rise_color;
|
|
418
|
+
bs.bg_color = settings->bg_color;
|
|
419
|
+
bs.show_labels = (int)settings->show_labels;
|
|
420
|
+
bs.show_prices = (int)settings->show_prices;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/* ccharts_bar_slice and cc_bar_item_t have identical {label, value}
|
|
424
|
+
* layout; a cast (rather than a copy) hand is all the FFI needs. */
|
|
425
|
+
chart = cc_bar_create((const cc_bar_item_t*)items, (int)count,
|
|
426
|
+
(int)width, (int)height, &bs);
|
|
427
|
+
if (chart == NULL) return CCHARTS_ERR_NOMEM;
|
|
428
|
+
|
|
429
|
+
*out = chart;
|
|
430
|
+
if (out_len != NULL) *out_len = strlen(chart);
|
|
431
|
+
return CCHARTS_OK;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/* ---------------------------- Stacked bar chart -------------------------- */
|
|
435
|
+
|
|
436
|
+
int32_t ccharts_stack(const ccharts_stack_series* series, int32_t series_count,
|
|
437
|
+
int32_t width, int32_t height,
|
|
438
|
+
const ccharts_stack_settings* settings,
|
|
439
|
+
char** out, size_t* out_len) {
|
|
440
|
+
cc_stack_settings_t ss;
|
|
441
|
+
char* chart;
|
|
442
|
+
int32_t cats, s, c;
|
|
443
|
+
|
|
444
|
+
if (out == NULL) return CCHARTS_ERR_INVALID_ARG;
|
|
445
|
+
*out = NULL;
|
|
446
|
+
if (out_len != NULL) *out_len = 0;
|
|
447
|
+
/* settings is required here: the values lengths are carried only by
|
|
448
|
+
* settings->cats, so a NULL settings is a marshalling error, not a
|
|
449
|
+
* "use defaults" signal. */
|
|
450
|
+
if (series == NULL || series_count <= 0 || settings == NULL ||
|
|
451
|
+
settings->cats <= 0) {
|
|
452
|
+
return CCHARTS_ERR_INVALID_ARG;
|
|
453
|
+
}
|
|
454
|
+
if (!cc_dim_ok((int)width, (int)height)) return CCHARTS_ERR_DIMENSIONS;
|
|
455
|
+
|
|
456
|
+
cats = (int32_t)settings->cats;
|
|
457
|
+
/* The settings->series count, when supplied, must agree with the
|
|
458
|
+
* argument (a 2-D marshalling sanity check for the bindings). */
|
|
459
|
+
if (settings->series > 0 && settings->series != series_count) {
|
|
460
|
+
return CCHARTS_ERR_INVALID_ARG;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/* NaN/inf stack values would poison the column-total / pixel math, so
|
|
464
|
+
* reject them at the boundary like every other price path. Negative
|
|
465
|
+
* values are clamped to zero by the renderer, not rejected. */
|
|
466
|
+
for (s = 0; s < series_count; s++) {
|
|
467
|
+
if (series[s].values == NULL) return CCHARTS_ERR_INVALID_ARG;
|
|
468
|
+
for (c = 0; c < cats; c++) {
|
|
469
|
+
if (!isfinite(series[s].values[c])) return CCHARTS_ERR_NON_FINITE;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
memset(&ss, 0, sizeof(ss));
|
|
474
|
+
ss.colors = settings->colors;
|
|
475
|
+
ss.bg_color = settings->bg_color;
|
|
476
|
+
ss.cat_labels = settings->cat_labels;
|
|
477
|
+
ss.series = (int)settings->series;
|
|
478
|
+
ss.cats = (int)cats;
|
|
479
|
+
ss.show_labels = (int)settings->show_labels;
|
|
480
|
+
ss.show_prices = (int)settings->show_prices;
|
|
481
|
+
|
|
482
|
+
/* ccharts_stack_series and cc_stack_series_t have identical {name,
|
|
483
|
+
* values} layout; a cast (rather than a copy hand) is all the FFI needs. */
|
|
484
|
+
chart = cc_stack_create((const cc_stack_series_t*)series, (int)series_count,
|
|
485
|
+
(int)width, (int)height, &ss);
|
|
486
|
+
if (chart == NULL) return CCHARTS_ERR_NOMEM;
|
|
487
|
+
|
|
488
|
+
*out = chart;
|
|
489
|
+
if (out_len != NULL) *out_len = strlen(chart);
|
|
490
|
+
return CCHARTS_OK;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/* ------------------------------ Heatmap chart ---------------------------- */
|
|
494
|
+
|
|
495
|
+
int32_t ccharts_heat(const double* values, int32_t rows, int32_t cols,
|
|
496
|
+
int32_t width, int32_t height,
|
|
497
|
+
const ccharts_heat_settings* settings,
|
|
498
|
+
char** out, size_t* out_len) {
|
|
499
|
+
cc_heat_settings_t hs;
|
|
500
|
+
char* chart;
|
|
501
|
+
int64_t n, i;
|
|
502
|
+
|
|
503
|
+
if (out == NULL) return CCHARTS_ERR_INVALID_ARG;
|
|
504
|
+
*out = NULL;
|
|
505
|
+
if (out_len != NULL) *out_len = 0;
|
|
506
|
+
if (values == NULL || rows <= 0 || cols <= 0) return CCHARTS_ERR_INVALID_ARG;
|
|
507
|
+
if (!cc_dim_ok((int)width, (int)height)) return CCHARTS_ERR_DIMENSIONS;
|
|
508
|
+
|
|
509
|
+
/* NaN/inf values would poison the min/max and block-average math (and
|
|
510
|
+
* lround in the colour mapping), so reject them at the boundary like
|
|
511
|
+
* every other price path. Counted in 64-bit: rows*cols is not bounded
|
|
512
|
+
* by cc_dim_ok (only width/height are). */
|
|
513
|
+
n = (int64_t)rows * (int64_t)cols;
|
|
514
|
+
for (i = 0; i < n; i++) {
|
|
515
|
+
if (!isfinite(values[i])) return CCHARTS_ERR_NON_FINITE;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
memset(&hs, 0, sizeof(hs));
|
|
519
|
+
if (settings != NULL) {
|
|
520
|
+
hs.low_color = settings->low_color;
|
|
521
|
+
hs.high_color = settings->high_color;
|
|
522
|
+
hs.mid_color = settings->mid_color;
|
|
523
|
+
hs.bg_color = settings->bg_color;
|
|
524
|
+
hs.row_labels = settings->row_labels;
|
|
525
|
+
hs.col_labels = settings->col_labels;
|
|
526
|
+
hs.show_labels = (int)settings->show_labels;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/* The caller's row-major values array matches cc_heat_create's layout
|
|
530
|
+
* exactly, so no copy is needed as long as it stays valid for the call. */
|
|
531
|
+
chart = cc_heat_create(values, (int)rows, (int)cols,
|
|
532
|
+
(int)width, (int)height, &hs);
|
|
533
|
+
if (chart == NULL) return CCHARTS_ERR_NOMEM;
|
|
534
|
+
|
|
535
|
+
*out = chart;
|
|
536
|
+
if (out_len != NULL) *out_len = strlen(chart);
|
|
537
|
+
return CCHARTS_OK;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/* ------------------------------ Box plot ------------------------------ */
|
|
541
|
+
|
|
542
|
+
int32_t ccharts_box(const ccharts_box_category* cats, int32_t cat_count,
|
|
543
|
+
int32_t width, int32_t height,
|
|
544
|
+
const ccharts_box_settings* settings,
|
|
545
|
+
char** out, size_t* out_len) {
|
|
546
|
+
cc_box_settings_t bs;
|
|
547
|
+
char* chart;
|
|
548
|
+
int32_t c;
|
|
549
|
+
|
|
550
|
+
if (out == NULL) return CCHARTS_ERR_INVALID_ARG;
|
|
551
|
+
*out = NULL;
|
|
552
|
+
if (out_len != NULL) *out_len = 0;
|
|
553
|
+
if (cats == NULL || cat_count <= 0) return CCHARTS_ERR_INVALID_ARG;
|
|
554
|
+
if (!cc_dim_ok((int)width, (int)height)) return CCHARTS_ERR_DIMENSIONS;
|
|
555
|
+
|
|
556
|
+
/* A NULL samples array or n <= 0 is a marshalling error (no five-number
|
|
557
|
+
* summary without samples), and NaN/inf samples would poison the sort /
|
|
558
|
+
* nearest-rank / pixel math, so both are rejected at the boundary.
|
|
559
|
+
* Negative values are fine — a box has no zero baseline. */
|
|
560
|
+
for (c = 0; c < cat_count; c++) {
|
|
561
|
+
int32_t k;
|
|
562
|
+
if (cats[c].samples == NULL || cats[c].n <= 0) {
|
|
563
|
+
return CCHARTS_ERR_INVALID_ARG;
|
|
564
|
+
}
|
|
565
|
+
for (k = 0; k < cats[c].n; k++) {
|
|
566
|
+
if (!isfinite(cats[c].samples[k])) return CCHARTS_ERR_NON_FINITE;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
memset(&bs, 0, sizeof(bs));
|
|
571
|
+
if (settings != NULL) {
|
|
572
|
+
bs.rise_color = settings->rise_color;
|
|
573
|
+
bs.area_color = settings->area_color;
|
|
574
|
+
bs.bg_color = settings->bg_color;
|
|
575
|
+
bs.show_prices = (int)settings->show_prices;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/* ccharts_box_category and cc_box_category_t have identical {name,
|
|
579
|
+
* samples, n} layout; a cast (rather than a copy hand) is all the FFI
|
|
580
|
+
* needs. */
|
|
581
|
+
chart = cc_box_create((const cc_box_category_t*)cats, (int)cat_count,
|
|
582
|
+
(int)width, (int)height, &bs);
|
|
583
|
+
if (chart == NULL) return CCHARTS_ERR_NOMEM;
|
|
584
|
+
|
|
585
|
+
*out = chart;
|
|
586
|
+
if (out_len != NULL) *out_len = strlen(chart);
|
|
587
|
+
return CCHARTS_OK;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/* ---------------------------- Introspection ---------------------------- */
|
|
591
|
+
|
|
592
|
+
const char* ccharts_color(int32_t index) {
|
|
593
|
+
static const char* const table[CCHARTS_COLOR_COUNT] = {
|
|
594
|
+
CC_COLOR_BLACK, CC_COLOR_RED, CC_COLOR_GREEN, CC_COLOR_YELLOW,
|
|
595
|
+
CC_COLOR_BLUE, CC_COLOR_MAGENTA, CC_COLOR_CYAN, CC_COLOR_WHITE,
|
|
596
|
+
CC_COLOR_BRIGHT_BLACK, CC_COLOR_BRIGHT_RED, CC_COLOR_BRIGHT_GREEN,
|
|
597
|
+
CC_COLOR_BRIGHT_YELLOW, CC_COLOR_BRIGHT_BLUE, CC_COLOR_BRIGHT_MAGENTA,
|
|
598
|
+
CC_COLOR_BRIGHT_CYAN, CC_COLOR_BRIGHT_WHITE, CC_COLOR_RESET
|
|
599
|
+
};
|
|
600
|
+
if (index < 0 || index >= CCHARTS_COLOR_COUNT) return NULL;
|
|
601
|
+
return table[index];
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const char* ccharts_error_message(int32_t status) {
|
|
605
|
+
switch (status) {
|
|
606
|
+
case CCHARTS_OK: return "ok";
|
|
607
|
+
case CCHARTS_ERR_INVALID_ARG: return "invalid argument";
|
|
608
|
+
case CCHARTS_ERR_PARSE: return "could not parse the input data";
|
|
609
|
+
case CCHARTS_ERR_NOMEM: return "out of memory";
|
|
610
|
+
case CCHARTS_ERR_NON_FINITE: return "OHLC values must be finite (no NaN or inf)";
|
|
611
|
+
case CCHARTS_ERR_DIMENSIONS: return "width and height must be positive and within the limits";
|
|
612
|
+
default: return "unknown error";
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const char* ccharts_version(void) { return CCHARTS_VERSION; }
|
|
617
|
+
|
|
618
|
+
int32_t ccharts_max_dim(void) { return (int32_t)CC_MAX_DIM; }
|
|
619
|
+
|
|
620
|
+
int32_t ccharts_max_cells(void) { return (int32_t)CC_MAX_CELLS; }
|