@tuicomponents/chart 0.2.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.
- package/README.md +294 -0
- package/dist/index.cjs +3750 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2165 -0
- package/dist/index.d.ts +2165 -0
- package/dist/index.js +3656 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3656 @@
|
|
|
1
|
+
// src/chart.ts
|
|
2
|
+
import {
|
|
3
|
+
BaseTuiComponent,
|
|
4
|
+
measureLines,
|
|
5
|
+
registry
|
|
6
|
+
} from "@tuicomponents/core";
|
|
7
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
8
|
+
|
|
9
|
+
// src/schema.ts
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
var chartTypeSchema = z.enum([
|
|
12
|
+
"bar",
|
|
13
|
+
// Horizontal bars
|
|
14
|
+
"bar-vertical",
|
|
15
|
+
// Vertical bars (column chart)
|
|
16
|
+
"bar-stacked",
|
|
17
|
+
// Stacked horizontal bars
|
|
18
|
+
"bar-stacked-vertical",
|
|
19
|
+
// Stacked vertical bars
|
|
20
|
+
"line",
|
|
21
|
+
// Line chart (height blocks)
|
|
22
|
+
"area",
|
|
23
|
+
// Filled area
|
|
24
|
+
"area-stacked",
|
|
25
|
+
// Stacked area
|
|
26
|
+
"scatter",
|
|
27
|
+
// 2D scatter plot with X/Y axes
|
|
28
|
+
"pie",
|
|
29
|
+
// Pie chart (circular)
|
|
30
|
+
"donut",
|
|
31
|
+
// Donut chart (pie with inner radius)
|
|
32
|
+
"heatmap"
|
|
33
|
+
// 2D grid with intensity shading
|
|
34
|
+
]);
|
|
35
|
+
var barStyleSchema = z.enum([
|
|
36
|
+
"block",
|
|
37
|
+
// ████████
|
|
38
|
+
"shaded",
|
|
39
|
+
// ▓▓▓▓▓▓▓▓
|
|
40
|
+
"light",
|
|
41
|
+
// ░░░░░░░░
|
|
42
|
+
"hash",
|
|
43
|
+
// ########
|
|
44
|
+
"equals",
|
|
45
|
+
// ========
|
|
46
|
+
"arrow"
|
|
47
|
+
// >>>>>>>>
|
|
48
|
+
]);
|
|
49
|
+
var lineStyleSchema = z.enum([
|
|
50
|
+
"blocks",
|
|
51
|
+
// Height blocks: ▁▂▃▄▅▆▇█ (default)
|
|
52
|
+
"braille",
|
|
53
|
+
// Braille dots (high resolution)
|
|
54
|
+
"dots"
|
|
55
|
+
// Point markers: · • ●
|
|
56
|
+
]);
|
|
57
|
+
var scatterStyleSchema = z.enum([
|
|
58
|
+
"dots",
|
|
59
|
+
// Simple character dots (●, ■, ▲)
|
|
60
|
+
"braille"
|
|
61
|
+
// High-resolution braille positioning
|
|
62
|
+
]);
|
|
63
|
+
var scatterMarkerSchema = z.enum([
|
|
64
|
+
"circle",
|
|
65
|
+
// ●
|
|
66
|
+
"square",
|
|
67
|
+
// ■
|
|
68
|
+
"triangle",
|
|
69
|
+
// ▲
|
|
70
|
+
"diamond",
|
|
71
|
+
// ◆
|
|
72
|
+
"plus"
|
|
73
|
+
// +
|
|
74
|
+
]);
|
|
75
|
+
var heatmapStyleSchema = z.enum([
|
|
76
|
+
"blocks",
|
|
77
|
+
// ░▒▓█ (4 intensity levels)
|
|
78
|
+
"ascii",
|
|
79
|
+
// . : * # (4 levels, markdown-friendly)
|
|
80
|
+
"numeric"
|
|
81
|
+
// Show actual values in cells
|
|
82
|
+
]);
|
|
83
|
+
var valueFormatSchema = z.enum([
|
|
84
|
+
"number",
|
|
85
|
+
// Regular number
|
|
86
|
+
"percent",
|
|
87
|
+
// Percentage
|
|
88
|
+
"compact",
|
|
89
|
+
// K/M/B suffixes
|
|
90
|
+
"currency"
|
|
91
|
+
// Dollar prefix
|
|
92
|
+
]);
|
|
93
|
+
var legendPositionSchema = z.enum([
|
|
94
|
+
"none",
|
|
95
|
+
// No legend
|
|
96
|
+
"top",
|
|
97
|
+
// Above chart
|
|
98
|
+
"bottom",
|
|
99
|
+
// Below chart
|
|
100
|
+
"right",
|
|
101
|
+
// To the right
|
|
102
|
+
"inline"
|
|
103
|
+
// Inline with data
|
|
104
|
+
]);
|
|
105
|
+
var dataPointSchema = z.object({
|
|
106
|
+
/**
|
|
107
|
+
* Category or x-value.
|
|
108
|
+
* Can be a string (categorical) or number (continuous).
|
|
109
|
+
*/
|
|
110
|
+
x: z.union([z.string(), z.number()]),
|
|
111
|
+
/**
|
|
112
|
+
* Numeric y-value.
|
|
113
|
+
*/
|
|
114
|
+
y: z.number(),
|
|
115
|
+
/**
|
|
116
|
+
* Optional custom label for this data point.
|
|
117
|
+
*/
|
|
118
|
+
label: z.string().optional()
|
|
119
|
+
});
|
|
120
|
+
var dataSeriesSchema = z.object({
|
|
121
|
+
/**
|
|
122
|
+
* Series name (used in legend).
|
|
123
|
+
*/
|
|
124
|
+
name: z.string(),
|
|
125
|
+
/**
|
|
126
|
+
* Data points in this series.
|
|
127
|
+
*/
|
|
128
|
+
data: z.array(dataPointSchema),
|
|
129
|
+
/**
|
|
130
|
+
* Optional style for this series.
|
|
131
|
+
*/
|
|
132
|
+
style: barStyleSchema.optional()
|
|
133
|
+
});
|
|
134
|
+
var axisConfigSchema = z.object({
|
|
135
|
+
/**
|
|
136
|
+
* Axis title/label.
|
|
137
|
+
*/
|
|
138
|
+
label: z.string().optional(),
|
|
139
|
+
/**
|
|
140
|
+
* Explicit minimum value (auto-computed if not set).
|
|
141
|
+
*/
|
|
142
|
+
min: z.number().optional(),
|
|
143
|
+
/**
|
|
144
|
+
* Explicit maximum value (auto-computed if not set).
|
|
145
|
+
*/
|
|
146
|
+
max: z.number().optional(),
|
|
147
|
+
/**
|
|
148
|
+
* Number of tick marks.
|
|
149
|
+
* @default 5
|
|
150
|
+
*/
|
|
151
|
+
tickCount: z.number().int().positive().default(5),
|
|
152
|
+
/**
|
|
153
|
+
* Whether to show tick marks.
|
|
154
|
+
* @default true
|
|
155
|
+
*/
|
|
156
|
+
showTicks: z.boolean().default(true),
|
|
157
|
+
/**
|
|
158
|
+
* Value format for tick labels.
|
|
159
|
+
* @default "number"
|
|
160
|
+
*/
|
|
161
|
+
format: valueFormatSchema.default("number"),
|
|
162
|
+
/**
|
|
163
|
+
* Number of decimal places.
|
|
164
|
+
*/
|
|
165
|
+
decimals: z.number().int().nonnegative().optional()
|
|
166
|
+
});
|
|
167
|
+
var legendConfigSchema = z.object({
|
|
168
|
+
/**
|
|
169
|
+
* Legend position.
|
|
170
|
+
* @default "bottom"
|
|
171
|
+
*/
|
|
172
|
+
position: legendPositionSchema.default("bottom"),
|
|
173
|
+
/**
|
|
174
|
+
* Whether to use boxed style.
|
|
175
|
+
* @default false
|
|
176
|
+
*/
|
|
177
|
+
boxed: z.boolean().default(false)
|
|
178
|
+
});
|
|
179
|
+
var gridConfigSchema = z.object({
|
|
180
|
+
/**
|
|
181
|
+
* Show horizontal grid lines.
|
|
182
|
+
* @default false
|
|
183
|
+
*/
|
|
184
|
+
horizontal: z.boolean().default(false),
|
|
185
|
+
/**
|
|
186
|
+
* Show vertical grid lines.
|
|
187
|
+
* @default false
|
|
188
|
+
*/
|
|
189
|
+
vertical: z.boolean().default(false),
|
|
190
|
+
/**
|
|
191
|
+
* Grid character.
|
|
192
|
+
* @default "·"
|
|
193
|
+
*/
|
|
194
|
+
char: z.string().default("\xB7")
|
|
195
|
+
});
|
|
196
|
+
var chartInputSchema = z.object({
|
|
197
|
+
/**
|
|
198
|
+
* Chart type.
|
|
199
|
+
*/
|
|
200
|
+
type: chartTypeSchema,
|
|
201
|
+
/**
|
|
202
|
+
* Data series to display.
|
|
203
|
+
*/
|
|
204
|
+
series: z.array(dataSeriesSchema).min(1),
|
|
205
|
+
/**
|
|
206
|
+
* Chart title.
|
|
207
|
+
*/
|
|
208
|
+
title: z.string().optional(),
|
|
209
|
+
/**
|
|
210
|
+
* X-axis configuration.
|
|
211
|
+
*/
|
|
212
|
+
xAxis: axisConfigSchema.optional(),
|
|
213
|
+
/**
|
|
214
|
+
* Y-axis configuration.
|
|
215
|
+
*/
|
|
216
|
+
yAxis: axisConfigSchema.optional(),
|
|
217
|
+
/**
|
|
218
|
+
* Legend configuration.
|
|
219
|
+
*/
|
|
220
|
+
legend: legendConfigSchema.optional(),
|
|
221
|
+
/**
|
|
222
|
+
* Grid configuration.
|
|
223
|
+
*/
|
|
224
|
+
grid: gridConfigSchema.optional(),
|
|
225
|
+
/**
|
|
226
|
+
* Chart width in characters.
|
|
227
|
+
* @default 40
|
|
228
|
+
*/
|
|
229
|
+
width: z.number().int().positive().default(40),
|
|
230
|
+
/**
|
|
231
|
+
* Chart height in lines.
|
|
232
|
+
* @default 10
|
|
233
|
+
*/
|
|
234
|
+
height: z.number().int().positive().default(10),
|
|
235
|
+
/**
|
|
236
|
+
* Show values on data points.
|
|
237
|
+
* @default false
|
|
238
|
+
*/
|
|
239
|
+
showValues: z.boolean().default(false),
|
|
240
|
+
/**
|
|
241
|
+
* Show axes.
|
|
242
|
+
* @default true
|
|
243
|
+
*/
|
|
244
|
+
showAxes: z.boolean().default(true),
|
|
245
|
+
/**
|
|
246
|
+
* Line/area rendering style.
|
|
247
|
+
* @default "blocks"
|
|
248
|
+
*/
|
|
249
|
+
lineStyle: lineStyleSchema.default("blocks"),
|
|
250
|
+
/**
|
|
251
|
+
* Default bar style for series without explicit style.
|
|
252
|
+
* @default "block"
|
|
253
|
+
*/
|
|
254
|
+
barStyle: barStyleSchema.default("block"),
|
|
255
|
+
/**
|
|
256
|
+
* Scatter plot rendering style.
|
|
257
|
+
* @default "dots"
|
|
258
|
+
*/
|
|
259
|
+
scatterStyle: scatterStyleSchema.default("dots"),
|
|
260
|
+
/**
|
|
261
|
+
* Heatmap rendering style.
|
|
262
|
+
* @default "blocks"
|
|
263
|
+
*/
|
|
264
|
+
heatmapStyle: heatmapStyleSchema.default("blocks"),
|
|
265
|
+
/**
|
|
266
|
+
* Center label for donut charts.
|
|
267
|
+
* Displayed in the center of the donut.
|
|
268
|
+
*/
|
|
269
|
+
centerLabel: z.string().optional(),
|
|
270
|
+
/**
|
|
271
|
+
* Inner radius ratio for donut charts (0-0.9).
|
|
272
|
+
* 0 = pie chart, 0.5 = typical donut.
|
|
273
|
+
* @default 0.5
|
|
274
|
+
*/
|
|
275
|
+
innerRadius: z.number().min(0).max(0.9).default(0.5)
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// src/layout/bar.ts
|
|
279
|
+
import { getStringWidth } from "@tuicomponents/core";
|
|
280
|
+
|
|
281
|
+
// src/core/scaling.ts
|
|
282
|
+
var NICE_INTERVALS = [1, 2, 2.5, 5, 10];
|
|
283
|
+
function findNiceStep(rawStep) {
|
|
284
|
+
if (rawStep === 0) return 1;
|
|
285
|
+
const magnitude = Math.pow(10, Math.floor(Math.log10(Math.abs(rawStep))));
|
|
286
|
+
const normalized = rawStep / magnitude;
|
|
287
|
+
for (const interval of NICE_INTERVALS) {
|
|
288
|
+
if (interval >= normalized) {
|
|
289
|
+
return interval * magnitude;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return 10 * magnitude;
|
|
293
|
+
}
|
|
294
|
+
function floorToStep(value, step) {
|
|
295
|
+
return Math.floor(value / step) * step;
|
|
296
|
+
}
|
|
297
|
+
function ceilToStep(value, step) {
|
|
298
|
+
return Math.ceil(value / step) * step;
|
|
299
|
+
}
|
|
300
|
+
function computeNiceTicks(options) {
|
|
301
|
+
const {
|
|
302
|
+
dataMin,
|
|
303
|
+
dataMax,
|
|
304
|
+
tickCount = 5,
|
|
305
|
+
includeZero = true,
|
|
306
|
+
forceMin,
|
|
307
|
+
forceMax
|
|
308
|
+
} = options;
|
|
309
|
+
if (dataMin === dataMax && forceMin === void 0 && forceMax === void 0) {
|
|
310
|
+
const center = dataMin;
|
|
311
|
+
const half = center === 0 ? 5 : Math.abs(center) * 0.5;
|
|
312
|
+
const ticks2 = [center - half, center, center + half];
|
|
313
|
+
return {
|
|
314
|
+
min: ticks2[0] ?? center - half,
|
|
315
|
+
max: ticks2[2] ?? center + half,
|
|
316
|
+
step: half,
|
|
317
|
+
ticks: ticks2
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
let effectiveMin = dataMin;
|
|
321
|
+
let effectiveMax = dataMax;
|
|
322
|
+
if (includeZero) {
|
|
323
|
+
if (effectiveMin > 0) effectiveMin = 0;
|
|
324
|
+
if (effectiveMax < 0) effectiveMax = 0;
|
|
325
|
+
}
|
|
326
|
+
if (forceMin !== void 0) effectiveMin = forceMin;
|
|
327
|
+
if (forceMax !== void 0) effectiveMax = forceMax;
|
|
328
|
+
const range = effectiveMax - effectiveMin;
|
|
329
|
+
const rawStep = range / Math.max(1, tickCount - 1);
|
|
330
|
+
const niceStep = findNiceStep(rawStep);
|
|
331
|
+
let niceMin = forceMin ?? floorToStep(effectiveMin, niceStep);
|
|
332
|
+
let niceMax = forceMax ?? ceilToStep(effectiveMax, niceStep);
|
|
333
|
+
if (niceMin > dataMin) niceMin -= niceStep;
|
|
334
|
+
if (niceMax < dataMax) niceMax += niceStep;
|
|
335
|
+
const ticks = [];
|
|
336
|
+
const epsilon = niceStep * 1e-10;
|
|
337
|
+
for (let value = niceMin; value <= niceMax + epsilon; value += niceStep) {
|
|
338
|
+
const roundedValue = Math.round(value * 1e12) / 1e12;
|
|
339
|
+
ticks.push(roundedValue);
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
min: niceMin,
|
|
343
|
+
max: niceMax,
|
|
344
|
+
step: niceStep,
|
|
345
|
+
ticks
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function formatTickValue(value, format = "number", decimals) {
|
|
349
|
+
switch (format) {
|
|
350
|
+
case "percent":
|
|
351
|
+
return `${(value * 100).toFixed(decimals ?? 0)}%`;
|
|
352
|
+
case "compact": {
|
|
353
|
+
const abs = Math.abs(value);
|
|
354
|
+
if (abs >= 1e9) {
|
|
355
|
+
return `${(value / 1e9).toFixed(decimals ?? 1)}B`;
|
|
356
|
+
}
|
|
357
|
+
if (abs >= 1e6) {
|
|
358
|
+
return `${(value / 1e6).toFixed(decimals ?? 1)}M`;
|
|
359
|
+
}
|
|
360
|
+
if (abs >= 1e3) {
|
|
361
|
+
return `${(value / 1e3).toFixed(decimals ?? 1)}K`;
|
|
362
|
+
}
|
|
363
|
+
return value.toFixed(decimals ?? 0);
|
|
364
|
+
}
|
|
365
|
+
case "currency":
|
|
366
|
+
return `$${value.toLocaleString("en-US", {
|
|
367
|
+
minimumFractionDigits: decimals ?? 0,
|
|
368
|
+
maximumFractionDigits: decimals ?? 0
|
|
369
|
+
})}`;
|
|
370
|
+
case "number":
|
|
371
|
+
default:
|
|
372
|
+
if (decimals !== void 0) {
|
|
373
|
+
return value.toFixed(decimals);
|
|
374
|
+
}
|
|
375
|
+
if (Number.isInteger(value)) {
|
|
376
|
+
return value.toLocaleString("en-US");
|
|
377
|
+
}
|
|
378
|
+
return value.toLocaleString("en-US", {
|
|
379
|
+
minimumFractionDigits: 0,
|
|
380
|
+
maximumFractionDigits: 2
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function scaleValue(value, min, max, size) {
|
|
385
|
+
if (max === min) return size / 2;
|
|
386
|
+
return (value - min) / (max - min) * size;
|
|
387
|
+
}
|
|
388
|
+
function unscaleValue(position, min, max, size) {
|
|
389
|
+
if (size === 0) return min;
|
|
390
|
+
return min + position / size * (max - min);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/core/chars.ts
|
|
394
|
+
var AXIS_CHARS = {
|
|
395
|
+
/** Y-axis line (│) */
|
|
396
|
+
vertical: "\u2502",
|
|
397
|
+
/** X-axis line (─) */
|
|
398
|
+
horizontal: "\u2500",
|
|
399
|
+
/** Origin corner (└) */
|
|
400
|
+
origin: "\u2514",
|
|
401
|
+
/** Y-axis tick (├) - points right */
|
|
402
|
+
yTick: "\u251C",
|
|
403
|
+
/** X-axis tick (┬) - points up */
|
|
404
|
+
xTick: "\u252C",
|
|
405
|
+
/** Grid intersection (┼) */
|
|
406
|
+
cross: "\u253C",
|
|
407
|
+
/** Y-axis with grid (┤) - points left */
|
|
408
|
+
yTickLeft: "\u2524",
|
|
409
|
+
/** X-axis tick (┴) - points down */
|
|
410
|
+
xTickDown: "\u2534"
|
|
411
|
+
};
|
|
412
|
+
var HEIGHT_BLOCKS = [
|
|
413
|
+
"\u2581",
|
|
414
|
+
// 1/8
|
|
415
|
+
"\u2582",
|
|
416
|
+
// 2/8
|
|
417
|
+
"\u2583",
|
|
418
|
+
// 3/8
|
|
419
|
+
"\u2584",
|
|
420
|
+
// 4/8
|
|
421
|
+
"\u2585",
|
|
422
|
+
// 5/8
|
|
423
|
+
"\u2586",
|
|
424
|
+
// 6/8
|
|
425
|
+
"\u2587",
|
|
426
|
+
// 7/8
|
|
427
|
+
"\u2588"
|
|
428
|
+
// 8/8 (full)
|
|
429
|
+
];
|
|
430
|
+
var BAR_CHARS = {
|
|
431
|
+
/** Solid block █ */
|
|
432
|
+
block: "\u2588",
|
|
433
|
+
/** Dark shade ▓ */
|
|
434
|
+
shaded: "\u2593",
|
|
435
|
+
/** Light shade ░ */
|
|
436
|
+
light: "\u2591",
|
|
437
|
+
/** Hash # */
|
|
438
|
+
hash: "#",
|
|
439
|
+
/** Equals = */
|
|
440
|
+
equals: "=",
|
|
441
|
+
/** Arrow > */
|
|
442
|
+
arrow: ">"
|
|
443
|
+
};
|
|
444
|
+
var BRAILLE_BASE = 10240;
|
|
445
|
+
var BRAILLE_DOTS = {
|
|
446
|
+
topLeft: 1,
|
|
447
|
+
upperLeft: 2,
|
|
448
|
+
lowerLeft: 3,
|
|
449
|
+
bottomLeft: 7,
|
|
450
|
+
topRight: 4,
|
|
451
|
+
upperRight: 5,
|
|
452
|
+
lowerRight: 6,
|
|
453
|
+
bottomRight: 8
|
|
454
|
+
};
|
|
455
|
+
function valueToBlock(normalized) {
|
|
456
|
+
if (normalized <= 0) return " ";
|
|
457
|
+
const index = Math.min(7, Math.floor(normalized * 8));
|
|
458
|
+
return HEIGHT_BLOCKS[index] ?? "\u2588";
|
|
459
|
+
}
|
|
460
|
+
function getBarChar(style) {
|
|
461
|
+
return BAR_CHARS[style];
|
|
462
|
+
}
|
|
463
|
+
function toBrailleChar(dots) {
|
|
464
|
+
let pattern = 0;
|
|
465
|
+
for (const dot of dots) {
|
|
466
|
+
if (dot >= 1 && dot <= 8) {
|
|
467
|
+
pattern |= 1 << dot - 1;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return String.fromCharCode(BRAILLE_BASE + pattern);
|
|
471
|
+
}
|
|
472
|
+
var SERIES_STYLES = [
|
|
473
|
+
{ char: "\u2588", useBackticks: false },
|
|
474
|
+
// Solid, plain
|
|
475
|
+
{ char: "\u2588", useBackticks: true },
|
|
476
|
+
// Solid, highlighted
|
|
477
|
+
{ char: "\u2593", useBackticks: false },
|
|
478
|
+
// Shaded, plain
|
|
479
|
+
{ char: "\u2591", useBackticks: true }
|
|
480
|
+
// Light, highlighted
|
|
481
|
+
];
|
|
482
|
+
var SCATTER_MARKERS = {
|
|
483
|
+
circle: "\u25CF",
|
|
484
|
+
square: "\u25A0",
|
|
485
|
+
triangle: "\u25B2",
|
|
486
|
+
diamond: "\u25C6",
|
|
487
|
+
plus: "+"
|
|
488
|
+
};
|
|
489
|
+
var SCATTER_MARKER_SEQUENCE = ["\u25CF", "\u25A0", "\u25B2", "\u25C6", "+"];
|
|
490
|
+
var HEATMAP_BLOCKS = [" ", "\u2591", "\u2592", "\u2593", "\u2588"];
|
|
491
|
+
var HEATMAP_ASCII = [" ", ".", ":", "*", "#"];
|
|
492
|
+
function valueToHeatmapChar(normalized, style) {
|
|
493
|
+
const chars = style === "blocks" ? HEATMAP_BLOCKS : HEATMAP_ASCII;
|
|
494
|
+
if (normalized <= 0) return chars[0];
|
|
495
|
+
if (normalized >= 1) return chars[chars.length - 1] ?? " ";
|
|
496
|
+
const index = Math.min(
|
|
497
|
+
chars.length - 1,
|
|
498
|
+
Math.floor(normalized * chars.length)
|
|
499
|
+
);
|
|
500
|
+
return chars[index] ?? " ";
|
|
501
|
+
}
|
|
502
|
+
var LINE_CHARS = {
|
|
503
|
+
/** Point marker */
|
|
504
|
+
point: "\u25CF",
|
|
505
|
+
/** Horizontal line */
|
|
506
|
+
horizontal: "\u2500",
|
|
507
|
+
/** Vertical line */
|
|
508
|
+
vertical: "\u2502",
|
|
509
|
+
/** Rising diagonal (approximation) */
|
|
510
|
+
rising: "\u2571",
|
|
511
|
+
/** Falling diagonal (approximation) */
|
|
512
|
+
falling: "\u2572"
|
|
513
|
+
};
|
|
514
|
+
var BRAILLE_DOT_MAP = {
|
|
515
|
+
"0,0": 1,
|
|
516
|
+
"0,1": 2,
|
|
517
|
+
"0,2": 3,
|
|
518
|
+
"0,3": 7,
|
|
519
|
+
"1,0": 4,
|
|
520
|
+
"1,1": 5,
|
|
521
|
+
"1,2": 6,
|
|
522
|
+
"1,3": 8
|
|
523
|
+
};
|
|
524
|
+
var BrailleCanvas = class {
|
|
525
|
+
/** Width in character cells */
|
|
526
|
+
width;
|
|
527
|
+
/** Height in character cells */
|
|
528
|
+
height;
|
|
529
|
+
/** Dot grid (width*2 x height*4) */
|
|
530
|
+
dots;
|
|
531
|
+
/** Series index for each dot (for coloring) */
|
|
532
|
+
seriesIndices;
|
|
533
|
+
constructor(width, height) {
|
|
534
|
+
this.width = width;
|
|
535
|
+
this.height = height;
|
|
536
|
+
const dotWidth = width * 2;
|
|
537
|
+
const dotHeight = height * 4;
|
|
538
|
+
this.dots = Array.from(
|
|
539
|
+
{ length: dotHeight },
|
|
540
|
+
() => Array.from({ length: dotWidth }, () => false)
|
|
541
|
+
);
|
|
542
|
+
this.seriesIndices = Array.from(
|
|
543
|
+
{ length: dotHeight },
|
|
544
|
+
() => Array.from({ length: dotWidth }, () => null)
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Set a dot at the given dot coordinates.
|
|
549
|
+
*/
|
|
550
|
+
setDot(dotX, dotY, seriesIndex = 0) {
|
|
551
|
+
if (dotX >= 0 && dotX < this.width * 2 && dotY >= 0 && dotY < this.height * 4) {
|
|
552
|
+
const dotRow = this.dots[dotY];
|
|
553
|
+
const seriesRow = this.seriesIndices[dotY];
|
|
554
|
+
if (dotRow && seriesRow) {
|
|
555
|
+
dotRow[dotX] = true;
|
|
556
|
+
seriesRow[dotX] = seriesIndex;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Draw a line between two points using Bresenham's algorithm.
|
|
562
|
+
* Coordinates are in dot space (width*2 x height*4).
|
|
563
|
+
*/
|
|
564
|
+
drawLine(x0, y0, x1, y1, seriesIndex = 0) {
|
|
565
|
+
const dx = Math.abs(x1 - x0);
|
|
566
|
+
const dy = Math.abs(y1 - y0);
|
|
567
|
+
const sx = x0 < x1 ? 1 : -1;
|
|
568
|
+
const sy = y0 < y1 ? 1 : -1;
|
|
569
|
+
let err = dx - dy;
|
|
570
|
+
let x = x0;
|
|
571
|
+
let y = y0;
|
|
572
|
+
while (true) {
|
|
573
|
+
this.setDot(x, y, seriesIndex);
|
|
574
|
+
if (x === x1 && y === y1) break;
|
|
575
|
+
const e2 = 2 * err;
|
|
576
|
+
if (e2 > -dy) {
|
|
577
|
+
err -= dy;
|
|
578
|
+
x += sx;
|
|
579
|
+
}
|
|
580
|
+
if (e2 < dx) {
|
|
581
|
+
err += dx;
|
|
582
|
+
y += sy;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Draw a point marker (fills more dots for visibility).
|
|
588
|
+
*/
|
|
589
|
+
drawPoint(dotX, dotY, seriesIndex = 0) {
|
|
590
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
591
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
592
|
+
this.setDot(dotX + dx, dotY + dy, seriesIndex);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Draw a circle outline using the midpoint circle algorithm.
|
|
598
|
+
* Coordinates are in dot space (width*2 x height*4).
|
|
599
|
+
*
|
|
600
|
+
* @param centerX - Center X in dot coordinates
|
|
601
|
+
* @param centerY - Center Y in dot coordinates
|
|
602
|
+
* @param radius - Radius in dot units
|
|
603
|
+
* @param seriesIndex - Series index for coloring
|
|
604
|
+
*/
|
|
605
|
+
drawCircle(centerX, centerY, radius, seriesIndex = 0) {
|
|
606
|
+
if (radius <= 0) return;
|
|
607
|
+
let x = radius;
|
|
608
|
+
let y = 0;
|
|
609
|
+
let err = 1 - radius;
|
|
610
|
+
while (x >= y) {
|
|
611
|
+
this.setDot(centerX + x, centerY + y, seriesIndex);
|
|
612
|
+
this.setDot(centerX - x, centerY + y, seriesIndex);
|
|
613
|
+
this.setDot(centerX + x, centerY - y, seriesIndex);
|
|
614
|
+
this.setDot(centerX - x, centerY - y, seriesIndex);
|
|
615
|
+
this.setDot(centerX + y, centerY + x, seriesIndex);
|
|
616
|
+
this.setDot(centerX - y, centerY + x, seriesIndex);
|
|
617
|
+
this.setDot(centerX + y, centerY - x, seriesIndex);
|
|
618
|
+
this.setDot(centerX - y, centerY - x, seriesIndex);
|
|
619
|
+
y++;
|
|
620
|
+
if (err < 0) {
|
|
621
|
+
err += 2 * y + 1;
|
|
622
|
+
} else {
|
|
623
|
+
x--;
|
|
624
|
+
err += 2 * (y - x) + 1;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Draw an arc (portion of a circle).
|
|
630
|
+
* Angles are in radians, 0 = top (12 o'clock), increasing clockwise.
|
|
631
|
+
*
|
|
632
|
+
* @param centerX - Center X in dot coordinates
|
|
633
|
+
* @param centerY - Center Y in dot coordinates
|
|
634
|
+
* @param radius - Radius in dot units
|
|
635
|
+
* @param startAngle - Start angle in radians (0 = top)
|
|
636
|
+
* @param endAngle - End angle in radians
|
|
637
|
+
* @param seriesIndex - Series index for coloring
|
|
638
|
+
*/
|
|
639
|
+
drawArc(centerX, centerY, radius, startAngle, endAngle, seriesIndex = 0) {
|
|
640
|
+
if (radius <= 0) return;
|
|
641
|
+
const twoPi = Math.PI * 2;
|
|
642
|
+
startAngle = (startAngle % twoPi + twoPi) % twoPi;
|
|
643
|
+
endAngle = (endAngle % twoPi + twoPi) % twoPi;
|
|
644
|
+
const arcLength = endAngle > startAngle ? endAngle - startAngle : twoPi - startAngle + endAngle;
|
|
645
|
+
const steps = Math.max(8, Math.ceil(radius * arcLength * 0.5));
|
|
646
|
+
for (let i = 0; i <= steps; i++) {
|
|
647
|
+
const t = i / steps;
|
|
648
|
+
let angle;
|
|
649
|
+
if (endAngle > startAngle) {
|
|
650
|
+
angle = startAngle + t * (endAngle - startAngle);
|
|
651
|
+
} else {
|
|
652
|
+
angle = startAngle + t * (twoPi - startAngle + endAngle);
|
|
653
|
+
if (angle >= twoPi) angle -= twoPi;
|
|
654
|
+
}
|
|
655
|
+
const mathAngle = angle - Math.PI / 2;
|
|
656
|
+
const x = Math.round(centerX + radius * Math.cos(mathAngle));
|
|
657
|
+
const y = Math.round(centerY + radius * Math.sin(mathAngle));
|
|
658
|
+
this.setDot(x, y, seriesIndex);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Fill a wedge (pie slice) from center to edge.
|
|
663
|
+
* Angles are in radians, 0 = top (12 o'clock), increasing clockwise.
|
|
664
|
+
*
|
|
665
|
+
* @param centerX - Center X in dot coordinates
|
|
666
|
+
* @param centerY - Center Y in dot coordinates
|
|
667
|
+
* @param radius - Outer radius in dot units
|
|
668
|
+
* @param startAngle - Start angle in radians (0 = top)
|
|
669
|
+
* @param endAngle - End angle in radians
|
|
670
|
+
* @param seriesIndex - Series index for coloring
|
|
671
|
+
* @param innerRadius - Inner radius for donut (0 for pie)
|
|
672
|
+
*/
|
|
673
|
+
fillWedge(centerX, centerY, radius, startAngle, endAngle, seriesIndex = 0, innerRadius = 0) {
|
|
674
|
+
if (radius <= 0) return;
|
|
675
|
+
const twoPi = Math.PI * 2;
|
|
676
|
+
startAngle = (startAngle % twoPi + twoPi) % twoPi;
|
|
677
|
+
endAngle = (endAngle % twoPi + twoPi) % twoPi;
|
|
678
|
+
const arcLength = endAngle > startAngle ? endAngle - startAngle : twoPi - startAngle + endAngle;
|
|
679
|
+
const angleSteps = Math.max(8, Math.ceil(radius * arcLength * 0.3));
|
|
680
|
+
for (let i = 0; i <= angleSteps; i++) {
|
|
681
|
+
const t = i / angleSteps;
|
|
682
|
+
let angle;
|
|
683
|
+
if (endAngle > startAngle) {
|
|
684
|
+
angle = startAngle + t * (endAngle - startAngle);
|
|
685
|
+
} else {
|
|
686
|
+
angle = startAngle + t * (twoPi - startAngle + endAngle);
|
|
687
|
+
if (angle >= twoPi) angle -= twoPi;
|
|
688
|
+
}
|
|
689
|
+
const mathAngle = angle - Math.PI / 2;
|
|
690
|
+
const cos = Math.cos(mathAngle);
|
|
691
|
+
const sin = Math.sin(mathAngle);
|
|
692
|
+
const startR = Math.ceil(innerRadius);
|
|
693
|
+
for (let r = startR; r <= radius; r++) {
|
|
694
|
+
const x = Math.round(centerX + r * cos);
|
|
695
|
+
const y = Math.round(centerY + r * sin);
|
|
696
|
+
this.setDot(x, y, seriesIndex);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Get the braille character at the given character cell.
|
|
702
|
+
*/
|
|
703
|
+
getChar(charX, charY) {
|
|
704
|
+
const dots = [];
|
|
705
|
+
const dotBaseX = charX * 2;
|
|
706
|
+
const dotBaseY = charY * 4;
|
|
707
|
+
for (let dy = 0; dy < 4; dy++) {
|
|
708
|
+
for (let dx = 0; dx < 2; dx++) {
|
|
709
|
+
const dotX = dotBaseX + dx;
|
|
710
|
+
const dotY = dotBaseY + dy;
|
|
711
|
+
const dotRow = this.dots[dotY];
|
|
712
|
+
if (dotY >= 0 && dotY < this.height * 4 && dotX >= 0 && dotX < this.width * 2 && dotRow?.[dotX]) {
|
|
713
|
+
const dotNum = BRAILLE_DOT_MAP[`${String(dx)},${String(dy)}`];
|
|
714
|
+
if (dotNum) {
|
|
715
|
+
dots.push(dotNum);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (dots.length === 0) {
|
|
721
|
+
return " ";
|
|
722
|
+
}
|
|
723
|
+
return toBrailleChar(dots);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Get the dominant series index for a character cell.
|
|
727
|
+
*/
|
|
728
|
+
getSeriesIndex(charX, charY) {
|
|
729
|
+
const counts = /* @__PURE__ */ new Map();
|
|
730
|
+
const dotBaseX = charX * 2;
|
|
731
|
+
const dotBaseY = charY * 4;
|
|
732
|
+
for (let dy = 0; dy < 4; dy++) {
|
|
733
|
+
for (let dx = 0; dx < 2; dx++) {
|
|
734
|
+
const dotX = dotBaseX + dx;
|
|
735
|
+
const dotY = dotBaseY + dy;
|
|
736
|
+
if (dotY >= 0 && dotY < this.height * 4 && dotX >= 0 && dotX < this.width * 2) {
|
|
737
|
+
const seriesRow = this.seriesIndices[dotY];
|
|
738
|
+
const idx = seriesRow?.[dotX];
|
|
739
|
+
if (idx !== null && idx !== void 0) {
|
|
740
|
+
counts.set(idx, (counts.get(idx) ?? 0) + 1);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
let maxCount = 0;
|
|
746
|
+
let maxIndex = null;
|
|
747
|
+
for (const [idx, count] of counts) {
|
|
748
|
+
if (count > maxCount) {
|
|
749
|
+
maxCount = count;
|
|
750
|
+
maxIndex = idx;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return maxIndex;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Render the entire canvas to a 2D array of characters.
|
|
757
|
+
*/
|
|
758
|
+
render() {
|
|
759
|
+
const chars = [];
|
|
760
|
+
const indices = [];
|
|
761
|
+
for (let y = 0; y < this.height; y++) {
|
|
762
|
+
const row = [];
|
|
763
|
+
const indexRow = [];
|
|
764
|
+
for (let x = 0; x < this.width; x++) {
|
|
765
|
+
row.push(this.getChar(x, y));
|
|
766
|
+
indexRow.push(this.getSeriesIndex(x, y));
|
|
767
|
+
}
|
|
768
|
+
chars.push(row);
|
|
769
|
+
indices.push(indexRow);
|
|
770
|
+
}
|
|
771
|
+
return { chars, seriesIndices: indices };
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
// src/layout/bar.ts
|
|
776
|
+
function computeBarLayout(input) {
|
|
777
|
+
const isVertical = input.type === "bar-vertical";
|
|
778
|
+
const series = input.series;
|
|
779
|
+
const allValues = [];
|
|
780
|
+
const categorySet = /* @__PURE__ */ new Set();
|
|
781
|
+
for (const s of series) {
|
|
782
|
+
for (const point of s.data) {
|
|
783
|
+
allValues.push(point.y);
|
|
784
|
+
categorySet.add(String(point.x));
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
const categories = Array.from(categorySet);
|
|
788
|
+
const dataMin = Math.min(0, ...allValues);
|
|
789
|
+
const dataMax = Math.max(...allValues);
|
|
790
|
+
const yScale = computeNiceTicks({
|
|
791
|
+
dataMin,
|
|
792
|
+
dataMax,
|
|
793
|
+
tickCount: input.yAxis?.tickCount ?? 5,
|
|
794
|
+
includeZero: true,
|
|
795
|
+
forceMin: input.yAxis?.min,
|
|
796
|
+
forceMax: input.yAxis?.max
|
|
797
|
+
});
|
|
798
|
+
let maxLabelWidth = 0;
|
|
799
|
+
for (const category of categories) {
|
|
800
|
+
const width = getStringWidth(category);
|
|
801
|
+
if (width > maxLabelWidth) {
|
|
802
|
+
maxLabelWidth = width;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
const barAreaSize = isVertical ? input.height - 2 : input.width - maxLabelWidth - 3;
|
|
806
|
+
const bars = [];
|
|
807
|
+
const formattedValues = [];
|
|
808
|
+
for (let seriesIndex = 0; seriesIndex < series.length; seriesIndex++) {
|
|
809
|
+
const s = series[seriesIndex];
|
|
810
|
+
if (!s) continue;
|
|
811
|
+
const styleIndex = seriesIndex % SERIES_STYLES.length;
|
|
812
|
+
const styleInfo = SERIES_STYLES[styleIndex];
|
|
813
|
+
if (!styleInfo) continue;
|
|
814
|
+
const barChar = s.style ? getBarChar(s.style) : styleInfo.char;
|
|
815
|
+
const useBackticks = s.style ? false : styleInfo.useBackticks;
|
|
816
|
+
for (let pointIndex = 0; pointIndex < s.data.length; pointIndex++) {
|
|
817
|
+
const point = s.data[pointIndex];
|
|
818
|
+
if (!point) continue;
|
|
819
|
+
const value = point.y;
|
|
820
|
+
const label = point.label ?? String(point.x);
|
|
821
|
+
const normalizedValue = scaleValue(
|
|
822
|
+
value,
|
|
823
|
+
yScale.min,
|
|
824
|
+
yScale.max,
|
|
825
|
+
barAreaSize
|
|
826
|
+
);
|
|
827
|
+
const length = Math.max(0, Math.round(normalizedValue));
|
|
828
|
+
const format = input.yAxis?.format ?? "number";
|
|
829
|
+
const decimals = input.yAxis?.decimals;
|
|
830
|
+
const formattedValue = formatTickValue(value, format, decimals);
|
|
831
|
+
formattedValues.push(formattedValue);
|
|
832
|
+
const percentage = yScale.max !== yScale.min ? (value - yScale.min) / (yScale.max - yScale.min) * 100 : 0;
|
|
833
|
+
bars.push({
|
|
834
|
+
seriesIndex,
|
|
835
|
+
pointIndex,
|
|
836
|
+
label,
|
|
837
|
+
value,
|
|
838
|
+
length,
|
|
839
|
+
formattedValue,
|
|
840
|
+
barChar,
|
|
841
|
+
useBackticks,
|
|
842
|
+
percentage
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
let maxValueWidth = 0;
|
|
847
|
+
for (const formatted of formattedValues) {
|
|
848
|
+
const width = getStringWidth(formatted);
|
|
849
|
+
if (width > maxValueWidth) {
|
|
850
|
+
maxValueWidth = width;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return {
|
|
854
|
+
type: isVertical ? "bar-vertical" : "bar",
|
|
855
|
+
bars,
|
|
856
|
+
categories,
|
|
857
|
+
maxLabelWidth,
|
|
858
|
+
maxValueWidth,
|
|
859
|
+
barAreaSize,
|
|
860
|
+
yScale,
|
|
861
|
+
showValues: input.showValues,
|
|
862
|
+
width: input.width,
|
|
863
|
+
height: input.height
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
function groupBarsByCategory(layout) {
|
|
867
|
+
const groups = /* @__PURE__ */ new Map();
|
|
868
|
+
for (const bar of layout.bars) {
|
|
869
|
+
const existing = groups.get(bar.label);
|
|
870
|
+
if (existing) {
|
|
871
|
+
existing.push(bar);
|
|
872
|
+
} else {
|
|
873
|
+
groups.set(bar.label, [bar]);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return groups;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// src/layout/stacked-bar.ts
|
|
880
|
+
import { getStringWidth as getStringWidth2 } from "@tuicomponents/core";
|
|
881
|
+
function computeStackedBarLayout(input) {
|
|
882
|
+
const isVertical = input.type === "bar-stacked-vertical";
|
|
883
|
+
const series = input.series;
|
|
884
|
+
const categoryTotals = /* @__PURE__ */ new Map();
|
|
885
|
+
const categoryData = /* @__PURE__ */ new Map();
|
|
886
|
+
for (let seriesIndex = 0; seriesIndex < series.length; seriesIndex++) {
|
|
887
|
+
const s = series[seriesIndex];
|
|
888
|
+
if (!s) continue;
|
|
889
|
+
for (const point of s.data) {
|
|
890
|
+
const category = String(point.x);
|
|
891
|
+
const value = point.y;
|
|
892
|
+
const currentTotal = categoryTotals.get(category) ?? 0;
|
|
893
|
+
categoryTotals.set(category, currentTotal + value);
|
|
894
|
+
let seriesMap = categoryData.get(category);
|
|
895
|
+
if (!seriesMap) {
|
|
896
|
+
seriesMap = /* @__PURE__ */ new Map();
|
|
897
|
+
categoryData.set(category, seriesMap);
|
|
898
|
+
}
|
|
899
|
+
seriesMap.set(seriesIndex, value);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const categories = Array.from(categoryTotals.keys());
|
|
903
|
+
const maxTotal = Math.max(...categoryTotals.values());
|
|
904
|
+
const yScale = computeNiceTicks({
|
|
905
|
+
dataMin: 0,
|
|
906
|
+
dataMax: maxTotal,
|
|
907
|
+
tickCount: input.yAxis?.tickCount ?? 5,
|
|
908
|
+
includeZero: true,
|
|
909
|
+
forceMin: input.yAxis?.min,
|
|
910
|
+
forceMax: input.yAxis?.max
|
|
911
|
+
});
|
|
912
|
+
let maxLabelWidth = 0;
|
|
913
|
+
for (const category of categories) {
|
|
914
|
+
const width = getStringWidth2(category);
|
|
915
|
+
if (width > maxLabelWidth) {
|
|
916
|
+
maxLabelWidth = width;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
const barAreaSize = isVertical ? input.height - 2 : input.width - maxLabelWidth - 3;
|
|
920
|
+
const stacks = [];
|
|
921
|
+
let maxValueWidth = 0;
|
|
922
|
+
for (const category of categories) {
|
|
923
|
+
const seriesMap = categoryData.get(category);
|
|
924
|
+
const total = categoryTotals.get(category);
|
|
925
|
+
if (!seriesMap || total === void 0) continue;
|
|
926
|
+
const segments = [];
|
|
927
|
+
let cumulativeValue = 0;
|
|
928
|
+
for (let seriesIndex = 0; seriesIndex < series.length; seriesIndex++) {
|
|
929
|
+
const value = seriesMap.get(seriesIndex) ?? 0;
|
|
930
|
+
if (value <= 0) continue;
|
|
931
|
+
const styleIndex = seriesIndex % SERIES_STYLES.length;
|
|
932
|
+
const styleInfo = SERIES_STYLES[styleIndex];
|
|
933
|
+
if (!styleInfo) continue;
|
|
934
|
+
const s = series[seriesIndex];
|
|
935
|
+
if (!s) continue;
|
|
936
|
+
const barChar = s.style ? getBarChar(s.style) : styleInfo.char;
|
|
937
|
+
const useBackticks = s.style ? false : styleInfo.useBackticks;
|
|
938
|
+
const startPos = scaleValue(
|
|
939
|
+
cumulativeValue,
|
|
940
|
+
yScale.min,
|
|
941
|
+
yScale.max,
|
|
942
|
+
barAreaSize
|
|
943
|
+
);
|
|
944
|
+
cumulativeValue += value;
|
|
945
|
+
const endPos = scaleValue(
|
|
946
|
+
cumulativeValue,
|
|
947
|
+
yScale.min,
|
|
948
|
+
yScale.max,
|
|
949
|
+
barAreaSize
|
|
950
|
+
);
|
|
951
|
+
const length = Math.max(1, Math.round(endPos - startPos));
|
|
952
|
+
const format = input.yAxis?.format ?? "number";
|
|
953
|
+
const decimals = input.yAxis?.decimals;
|
|
954
|
+
const formattedValue = formatTickValue(value, format, decimals);
|
|
955
|
+
const valueWidth = getStringWidth2(formattedValue);
|
|
956
|
+
if (valueWidth > maxValueWidth) {
|
|
957
|
+
maxValueWidth = valueWidth;
|
|
958
|
+
}
|
|
959
|
+
segments.push({
|
|
960
|
+
seriesIndex,
|
|
961
|
+
pointIndex: categories.indexOf(category),
|
|
962
|
+
label: category,
|
|
963
|
+
value,
|
|
964
|
+
length,
|
|
965
|
+
formattedValue,
|
|
966
|
+
barChar,
|
|
967
|
+
useBackticks,
|
|
968
|
+
percentage: total > 0 ? value / total * 100 : 0
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
stacks.push({
|
|
972
|
+
label: category,
|
|
973
|
+
segments,
|
|
974
|
+
total
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
const seriesNames = series.map((s) => s.name);
|
|
978
|
+
const seriesStyles = series.map((s, i) => {
|
|
979
|
+
const styleIndex = i % SERIES_STYLES.length;
|
|
980
|
+
const styleInfo = SERIES_STYLES[styleIndex];
|
|
981
|
+
if (!styleInfo) return null;
|
|
982
|
+
return {
|
|
983
|
+
char: s.style ? getBarChar(s.style) : styleInfo.char,
|
|
984
|
+
useBackticks: s.style ? false : styleInfo.useBackticks
|
|
985
|
+
};
|
|
986
|
+
}).filter(
|
|
987
|
+
(style) => style !== null
|
|
988
|
+
);
|
|
989
|
+
return {
|
|
990
|
+
type: isVertical ? "bar-stacked-vertical" : "bar-stacked",
|
|
991
|
+
stacks,
|
|
992
|
+
seriesNames,
|
|
993
|
+
seriesStyles,
|
|
994
|
+
maxLabelWidth,
|
|
995
|
+
maxValueWidth,
|
|
996
|
+
barAreaSize,
|
|
997
|
+
yScale,
|
|
998
|
+
showValues: input.showValues,
|
|
999
|
+
width: input.width,
|
|
1000
|
+
height: input.height
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// src/layout/line.ts
|
|
1005
|
+
import { getStringWidth as getStringWidth3 } from "@tuicomponents/core";
|
|
1006
|
+
var LINE_DRAW = {
|
|
1007
|
+
horizontal: "\u2500",
|
|
1008
|
+
point: "\u25CF",
|
|
1009
|
+
rise: "\u2571",
|
|
1010
|
+
fall: "\u2572"
|
|
1011
|
+
};
|
|
1012
|
+
function computeLineLayout(input) {
|
|
1013
|
+
const series = input.series;
|
|
1014
|
+
const lineStyle = input.lineStyle;
|
|
1015
|
+
const allValues = [];
|
|
1016
|
+
const categorySet = /* @__PURE__ */ new Set();
|
|
1017
|
+
for (const s of series) {
|
|
1018
|
+
for (const point of s.data) {
|
|
1019
|
+
allValues.push(point.y);
|
|
1020
|
+
categorySet.add(String(point.x));
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
const categories = Array.from(categorySet);
|
|
1024
|
+
const seriesNames = series.map((s) => s.name);
|
|
1025
|
+
const dataMin = Math.min(...allValues);
|
|
1026
|
+
const dataMax = Math.max(...allValues);
|
|
1027
|
+
const yScale = computeNiceTicks({
|
|
1028
|
+
dataMin,
|
|
1029
|
+
dataMax,
|
|
1030
|
+
tickCount: input.yAxis?.tickCount ?? 5,
|
|
1031
|
+
includeZero: input.yAxis?.min === void 0,
|
|
1032
|
+
forceMin: input.yAxis?.min,
|
|
1033
|
+
forceMax: input.yAxis?.max
|
|
1034
|
+
});
|
|
1035
|
+
let maxXLabelWidth = 0;
|
|
1036
|
+
for (const category of categories) {
|
|
1037
|
+
const width = getStringWidth3(category);
|
|
1038
|
+
if (width > maxXLabelWidth) {
|
|
1039
|
+
maxXLabelWidth = width;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
let yAxisWidth = 0;
|
|
1043
|
+
const format = input.yAxis?.format ?? "number";
|
|
1044
|
+
const decimals = input.yAxis?.decimals;
|
|
1045
|
+
for (const tick of yScale.ticks) {
|
|
1046
|
+
const label = formatTickValue(tick, format, decimals);
|
|
1047
|
+
const width = getStringWidth3(label);
|
|
1048
|
+
if (width > yAxisWidth) {
|
|
1049
|
+
yAxisWidth = width;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
yAxisWidth += 2;
|
|
1053
|
+
const chartHeight = input.height - 2;
|
|
1054
|
+
if (lineStyle === "braille") {
|
|
1055
|
+
return computeBrailleLayout(
|
|
1056
|
+
input,
|
|
1057
|
+
categories,
|
|
1058
|
+
seriesNames,
|
|
1059
|
+
series,
|
|
1060
|
+
yScale,
|
|
1061
|
+
yAxisWidth,
|
|
1062
|
+
chartHeight,
|
|
1063
|
+
format,
|
|
1064
|
+
decimals
|
|
1065
|
+
);
|
|
1066
|
+
} else {
|
|
1067
|
+
return computeBlocksLayout(
|
|
1068
|
+
input,
|
|
1069
|
+
categories,
|
|
1070
|
+
seriesNames,
|
|
1071
|
+
series,
|
|
1072
|
+
yScale,
|
|
1073
|
+
yAxisWidth,
|
|
1074
|
+
chartHeight,
|
|
1075
|
+
format,
|
|
1076
|
+
decimals,
|
|
1077
|
+
lineStyle
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
function computeBlocksLayout(input, categories, seriesNames, series, yScale, yAxisWidth, chartHeight, format, decimals, lineStyle) {
|
|
1082
|
+
const chartWidth = categories.length * 2 - 1;
|
|
1083
|
+
const points = [];
|
|
1084
|
+
for (let seriesIndex = 0; seriesIndex < series.length; seriesIndex++) {
|
|
1085
|
+
const s = series[seriesIndex];
|
|
1086
|
+
if (!s) continue;
|
|
1087
|
+
const seriesPoints = [];
|
|
1088
|
+
for (const point of s.data) {
|
|
1089
|
+
const categoryIndex = categories.indexOf(String(point.x));
|
|
1090
|
+
const colPos = categoryIndex * 2;
|
|
1091
|
+
const normalizedY = scaleValue(point.y, yScale.min, yScale.max, 1);
|
|
1092
|
+
const clampedY = Math.max(0, Math.min(1, normalizedY));
|
|
1093
|
+
seriesPoints.push({
|
|
1094
|
+
x: colPos,
|
|
1095
|
+
value: point.y,
|
|
1096
|
+
normalizedY: clampedY,
|
|
1097
|
+
seriesIndex
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
seriesPoints.sort((a, b) => a.x - b.x);
|
|
1101
|
+
points.push(seriesPoints);
|
|
1102
|
+
}
|
|
1103
|
+
const grid = [];
|
|
1104
|
+
const seriesGrid = [];
|
|
1105
|
+
for (let row = 0; row < chartHeight; row++) {
|
|
1106
|
+
grid.push(Array(chartWidth).fill(" "));
|
|
1107
|
+
seriesGrid.push(Array(chartWidth).fill(null));
|
|
1108
|
+
}
|
|
1109
|
+
for (let seriesIndex = 0; seriesIndex < points.length; seriesIndex++) {
|
|
1110
|
+
const seriesPoints = points[seriesIndex];
|
|
1111
|
+
if (!seriesPoints) continue;
|
|
1112
|
+
for (let i = 0; i < seriesPoints.length; i++) {
|
|
1113
|
+
const p = seriesPoints[i];
|
|
1114
|
+
if (!p) continue;
|
|
1115
|
+
const row = Math.floor((1 - p.normalizedY) * (chartHeight - 1e-3));
|
|
1116
|
+
const col = p.x;
|
|
1117
|
+
if (row >= 0 && row < chartHeight && col >= 0 && col < chartWidth) {
|
|
1118
|
+
const gridRow = grid[row];
|
|
1119
|
+
const seriesGridRow = seriesGrid[row];
|
|
1120
|
+
if (!gridRow || !seriesGridRow) continue;
|
|
1121
|
+
gridRow[col] = lineStyle === "dots" ? "\u25CF" : LINE_DRAW.point;
|
|
1122
|
+
seriesGridRow[col] = seriesIndex;
|
|
1123
|
+
if (lineStyle !== "dots" && i < seriesPoints.length - 1) {
|
|
1124
|
+
const nextP = seriesPoints[i + 1];
|
|
1125
|
+
if (!nextP) continue;
|
|
1126
|
+
const nextRow = Math.floor(
|
|
1127
|
+
(1 - nextP.normalizedY) * (chartHeight - 1e-3)
|
|
1128
|
+
);
|
|
1129
|
+
const nextCol = nextP.x;
|
|
1130
|
+
if (nextCol > col + 1) {
|
|
1131
|
+
const rowDiff = nextRow - row;
|
|
1132
|
+
const colDiff = nextCol - col;
|
|
1133
|
+
for (let c = col + 1; c < nextCol; c++) {
|
|
1134
|
+
const t = (c - col) / colDiff;
|
|
1135
|
+
const interpRow = Math.round(row + rowDiff * t);
|
|
1136
|
+
const interpGridRow = grid[interpRow];
|
|
1137
|
+
const interpSeriesGridRow = seriesGrid[interpRow];
|
|
1138
|
+
if (interpRow >= 0 && interpRow < chartHeight && interpGridRow?.[c] === " ") {
|
|
1139
|
+
if (rowDiff < 0) {
|
|
1140
|
+
interpGridRow[c] = LINE_DRAW.rise;
|
|
1141
|
+
} else if (rowDiff > 0) {
|
|
1142
|
+
interpGridRow[c] = LINE_DRAW.fall;
|
|
1143
|
+
} else {
|
|
1144
|
+
interpGridRow[c] = LINE_DRAW.horizontal;
|
|
1145
|
+
}
|
|
1146
|
+
if (interpSeriesGridRow) {
|
|
1147
|
+
interpSeriesGridRow[c] = seriesIndex;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
const rows = [];
|
|
1157
|
+
for (let rowIndex = 0; rowIndex < chartHeight; rowIndex++) {
|
|
1158
|
+
const rowBottom = 1 - (rowIndex + 1) / chartHeight;
|
|
1159
|
+
const rowTop = 1 - rowIndex / chartHeight;
|
|
1160
|
+
let yLabel;
|
|
1161
|
+
let yValue = yScale.min + rowTop * (yScale.max - yScale.min);
|
|
1162
|
+
for (const tick of yScale.ticks) {
|
|
1163
|
+
const tickNormalized = (tick - yScale.min) / (yScale.max - yScale.min);
|
|
1164
|
+
if (tickNormalized > rowBottom && tickNormalized <= rowTop) {
|
|
1165
|
+
yLabel = formatTickValue(tick, format, decimals);
|
|
1166
|
+
yValue = tick;
|
|
1167
|
+
break;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
const chars = grid[rowIndex];
|
|
1171
|
+
const seriesIndices = seriesGrid[rowIndex];
|
|
1172
|
+
if (!chars || !seriesIndices) continue;
|
|
1173
|
+
const useBackticks = seriesIndices.map((idx) => {
|
|
1174
|
+
if (idx === null) return false;
|
|
1175
|
+
const styleIndex = idx % SERIES_STYLES.length;
|
|
1176
|
+
return SERIES_STYLES[styleIndex]?.useBackticks ?? false;
|
|
1177
|
+
});
|
|
1178
|
+
rows.push({ yLabel, yValue, chars, useBackticks, seriesIndices });
|
|
1179
|
+
}
|
|
1180
|
+
return {
|
|
1181
|
+
type: "line",
|
|
1182
|
+
lineStyle: input.lineStyle,
|
|
1183
|
+
categories,
|
|
1184
|
+
seriesNames,
|
|
1185
|
+
points,
|
|
1186
|
+
rows,
|
|
1187
|
+
yScale,
|
|
1188
|
+
maxXLabelWidth: Math.max(...categories.map((c) => getStringWidth3(c))),
|
|
1189
|
+
yAxisWidth,
|
|
1190
|
+
width: input.width,
|
|
1191
|
+
height: input.height,
|
|
1192
|
+
showValues: input.showValues
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
function computeBrailleLayout(input, categories, seriesNames, series, yScale, yAxisWidth, chartHeight, format, decimals) {
|
|
1196
|
+
const charsPerCategory = 3;
|
|
1197
|
+
const chartWidth = categories.length * charsPerCategory;
|
|
1198
|
+
const points = [];
|
|
1199
|
+
for (let seriesIndex = 0; seriesIndex < series.length; seriesIndex++) {
|
|
1200
|
+
const s = series[seriesIndex];
|
|
1201
|
+
if (!s) continue;
|
|
1202
|
+
const seriesPoints = [];
|
|
1203
|
+
for (const point of s.data) {
|
|
1204
|
+
const categoryIndex = categories.indexOf(String(point.x));
|
|
1205
|
+
const colPos = categoryIndex * charsPerCategory + Math.floor(charsPerCategory / 2);
|
|
1206
|
+
const normalizedY = scaleValue(point.y, yScale.min, yScale.max, 1);
|
|
1207
|
+
const clampedY = Math.max(0, Math.min(1, normalizedY));
|
|
1208
|
+
seriesPoints.push({
|
|
1209
|
+
x: colPos,
|
|
1210
|
+
value: point.y,
|
|
1211
|
+
normalizedY: clampedY,
|
|
1212
|
+
seriesIndex
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
seriesPoints.sort((a, b) => a.x - b.x);
|
|
1216
|
+
points.push(seriesPoints);
|
|
1217
|
+
}
|
|
1218
|
+
const canvas = new BrailleCanvas(chartWidth, chartHeight);
|
|
1219
|
+
for (let seriesIndex = 0; seriesIndex < points.length; seriesIndex++) {
|
|
1220
|
+
const seriesPoints = points[seriesIndex];
|
|
1221
|
+
if (!seriesPoints) continue;
|
|
1222
|
+
for (let i = 0; i < seriesPoints.length; i++) {
|
|
1223
|
+
const p = seriesPoints[i];
|
|
1224
|
+
if (!p) continue;
|
|
1225
|
+
const dotX = p.x * 2 + 1;
|
|
1226
|
+
const dotY = Math.round((1 - p.normalizedY) * (chartHeight * 4 - 1));
|
|
1227
|
+
if (i < seriesPoints.length - 1) {
|
|
1228
|
+
const nextP = seriesPoints[i + 1];
|
|
1229
|
+
if (nextP) {
|
|
1230
|
+
const nextDotX = nextP.x * 2 + 1;
|
|
1231
|
+
const nextDotY = Math.round(
|
|
1232
|
+
(1 - nextP.normalizedY) * (chartHeight * 4 - 1)
|
|
1233
|
+
);
|
|
1234
|
+
canvas.drawLine(dotX, dotY, nextDotX, nextDotY, seriesIndex);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
canvas.drawPoint(dotX, dotY, seriesIndex);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
const rendered = canvas.render();
|
|
1241
|
+
const rows = [];
|
|
1242
|
+
for (let rowIndex = 0; rowIndex < chartHeight; rowIndex++) {
|
|
1243
|
+
const rowBottom = 1 - (rowIndex + 1) / chartHeight;
|
|
1244
|
+
const rowTop = 1 - rowIndex / chartHeight;
|
|
1245
|
+
let yLabel;
|
|
1246
|
+
let yValue = yScale.min + rowTop * (yScale.max - yScale.min);
|
|
1247
|
+
for (const tick of yScale.ticks) {
|
|
1248
|
+
const tickNormalized = (tick - yScale.min) / (yScale.max - yScale.min);
|
|
1249
|
+
if (tickNormalized > rowBottom && tickNormalized <= rowTop) {
|
|
1250
|
+
yLabel = formatTickValue(tick, format, decimals);
|
|
1251
|
+
yValue = tick;
|
|
1252
|
+
break;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
const chars = rendered.chars[rowIndex] ?? [];
|
|
1256
|
+
const seriesIndices = rendered.seriesIndices[rowIndex] ?? [];
|
|
1257
|
+
const useBackticks = seriesIndices.map((idx) => {
|
|
1258
|
+
if (idx === null) return false;
|
|
1259
|
+
const styleIndex = idx % SERIES_STYLES.length;
|
|
1260
|
+
return SERIES_STYLES[styleIndex]?.useBackticks ?? false;
|
|
1261
|
+
});
|
|
1262
|
+
rows.push({ yLabel, yValue, chars, useBackticks, seriesIndices });
|
|
1263
|
+
}
|
|
1264
|
+
return {
|
|
1265
|
+
type: "line",
|
|
1266
|
+
lineStyle: input.lineStyle,
|
|
1267
|
+
categories,
|
|
1268
|
+
seriesNames,
|
|
1269
|
+
points,
|
|
1270
|
+
rows,
|
|
1271
|
+
yScale,
|
|
1272
|
+
maxXLabelWidth: Math.max(...categories.map((c) => getStringWidth3(c))),
|
|
1273
|
+
yAxisWidth,
|
|
1274
|
+
width: input.width,
|
|
1275
|
+
height: input.height,
|
|
1276
|
+
showValues: input.showValues
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// src/layout/area.ts
|
|
1281
|
+
import { getStringWidth as getStringWidth4 } from "@tuicomponents/core";
|
|
1282
|
+
function computeAreaLayout(input) {
|
|
1283
|
+
const isStacked = input.type === "area-stacked";
|
|
1284
|
+
const series = input.series;
|
|
1285
|
+
const lineStyle = input.lineStyle;
|
|
1286
|
+
const categoryData = /* @__PURE__ */ new Map();
|
|
1287
|
+
const categories = [];
|
|
1288
|
+
for (const s of series) {
|
|
1289
|
+
for (const point of s.data) {
|
|
1290
|
+
const category = String(point.x);
|
|
1291
|
+
if (!categoryData.has(category)) {
|
|
1292
|
+
categoryData.set(
|
|
1293
|
+
category,
|
|
1294
|
+
new Array(series.length).fill(0)
|
|
1295
|
+
);
|
|
1296
|
+
categories.push(category);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
for (let seriesIndex = 0; seriesIndex < series.length; seriesIndex++) {
|
|
1301
|
+
const s = series[seriesIndex];
|
|
1302
|
+
if (!s) continue;
|
|
1303
|
+
for (const point of s.data) {
|
|
1304
|
+
const category = String(point.x);
|
|
1305
|
+
const values = categoryData.get(category);
|
|
1306
|
+
if (!values) continue;
|
|
1307
|
+
values[seriesIndex] = point.y;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
let maxValue = 0;
|
|
1311
|
+
const columns = [];
|
|
1312
|
+
for (let i = 0; i < categories.length; i++) {
|
|
1313
|
+
const category = categories[i];
|
|
1314
|
+
if (!category) continue;
|
|
1315
|
+
const values = categoryData.get(category);
|
|
1316
|
+
if (!values) continue;
|
|
1317
|
+
const cumulativeValues = [];
|
|
1318
|
+
let cumulative = 0;
|
|
1319
|
+
for (const value of values) {
|
|
1320
|
+
if (isStacked) {
|
|
1321
|
+
cumulative += value;
|
|
1322
|
+
cumulativeValues.push(cumulative);
|
|
1323
|
+
} else {
|
|
1324
|
+
cumulativeValues.push(value);
|
|
1325
|
+
if (value > cumulative) cumulative = value;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
if (cumulative > maxValue) maxValue = cumulative;
|
|
1329
|
+
columns.push({
|
|
1330
|
+
x: i,
|
|
1331
|
+
label: category,
|
|
1332
|
+
cumulativeValues,
|
|
1333
|
+
normalizedHeights: []
|
|
1334
|
+
// Fill in after we have scale
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
const yScale = computeNiceTicks({
|
|
1338
|
+
dataMin: 0,
|
|
1339
|
+
dataMax: maxValue,
|
|
1340
|
+
tickCount: input.yAxis?.tickCount ?? 5,
|
|
1341
|
+
includeZero: true,
|
|
1342
|
+
forceMin: input.yAxis?.min,
|
|
1343
|
+
forceMax: input.yAxis?.max
|
|
1344
|
+
});
|
|
1345
|
+
for (const column of columns) {
|
|
1346
|
+
column.normalizedHeights = column.cumulativeValues.map(
|
|
1347
|
+
(v) => scaleValue(v, yScale.min, yScale.max, 1)
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
let yAxisWidth = 0;
|
|
1351
|
+
const format = input.yAxis?.format ?? "number";
|
|
1352
|
+
const decimals = input.yAxis?.decimals;
|
|
1353
|
+
for (const tick of yScale.ticks) {
|
|
1354
|
+
const label = formatTickValue(tick, format, decimals);
|
|
1355
|
+
const width = getStringWidth4(label);
|
|
1356
|
+
if (width > yAxisWidth) {
|
|
1357
|
+
yAxisWidth = width;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
yAxisWidth += 2;
|
|
1361
|
+
const seriesNames = series.map((s) => s.name);
|
|
1362
|
+
const seriesStyles = series.map((_, i) => {
|
|
1363
|
+
const styleIndex = i % SERIES_STYLES.length;
|
|
1364
|
+
const style = SERIES_STYLES[styleIndex];
|
|
1365
|
+
if (!style) throw new Error(`Invalid style index: ${String(styleIndex)}`);
|
|
1366
|
+
return style;
|
|
1367
|
+
});
|
|
1368
|
+
const chartHeight = input.height - 2;
|
|
1369
|
+
const rows = [];
|
|
1370
|
+
for (let rowIndex = 0; rowIndex < chartHeight; rowIndex++) {
|
|
1371
|
+
const rowBottom = 1 - (rowIndex + 1) / chartHeight;
|
|
1372
|
+
const rowTop = 1 - rowIndex / chartHeight;
|
|
1373
|
+
let yLabel;
|
|
1374
|
+
for (const tick of yScale.ticks) {
|
|
1375
|
+
const tickNormalized = (tick - yScale.min) / (yScale.max - yScale.min);
|
|
1376
|
+
if (tickNormalized > rowBottom && tickNormalized <= rowTop) {
|
|
1377
|
+
yLabel = formatTickValue(tick, format, decimals);
|
|
1378
|
+
break;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
const chars = [];
|
|
1382
|
+
const useBackticks = [];
|
|
1383
|
+
const fillChars = [];
|
|
1384
|
+
for (const column of columns) {
|
|
1385
|
+
let activeSeriesIndex = -1;
|
|
1386
|
+
let fillLevel = 0;
|
|
1387
|
+
for (let seriesIndex = column.normalizedHeights.length - 1; seriesIndex >= 0; seriesIndex--) {
|
|
1388
|
+
const height = column.normalizedHeights[seriesIndex];
|
|
1389
|
+
if (height === void 0) continue;
|
|
1390
|
+
const prevHeight = seriesIndex > 0 ? column.normalizedHeights[seriesIndex - 1] ?? 0 : 0;
|
|
1391
|
+
if (height > rowBottom && prevHeight < rowTop) {
|
|
1392
|
+
activeSeriesIndex = seriesIndex;
|
|
1393
|
+
const effectiveTop = Math.min(height, rowTop);
|
|
1394
|
+
const effectiveBottom = Math.max(prevHeight, rowBottom);
|
|
1395
|
+
fillLevel = (effectiveTop - effectiveBottom) / (rowTop - rowBottom);
|
|
1396
|
+
break;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
if (activeSeriesIndex >= 0) {
|
|
1400
|
+
const char = valueToBlock(fillLevel);
|
|
1401
|
+
const style = seriesStyles[activeSeriesIndex];
|
|
1402
|
+
if (!style) {
|
|
1403
|
+
chars.push(" ");
|
|
1404
|
+
useBackticks.push(false);
|
|
1405
|
+
fillChars.push(" ");
|
|
1406
|
+
} else {
|
|
1407
|
+
chars.push(char);
|
|
1408
|
+
useBackticks.push(style.useBackticks);
|
|
1409
|
+
fillChars.push(style.char);
|
|
1410
|
+
}
|
|
1411
|
+
} else {
|
|
1412
|
+
chars.push(" ");
|
|
1413
|
+
useBackticks.push(false);
|
|
1414
|
+
fillChars.push(" ");
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
rows.push({
|
|
1418
|
+
yLabel,
|
|
1419
|
+
chars,
|
|
1420
|
+
useBackticks,
|
|
1421
|
+
fillChars
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
return {
|
|
1425
|
+
type: isStacked ? "area-stacked" : "area",
|
|
1426
|
+
lineStyle,
|
|
1427
|
+
categories,
|
|
1428
|
+
seriesNames,
|
|
1429
|
+
seriesStyles,
|
|
1430
|
+
columns,
|
|
1431
|
+
rows,
|
|
1432
|
+
yScale,
|
|
1433
|
+
yAxisWidth,
|
|
1434
|
+
width: input.width,
|
|
1435
|
+
height: input.height,
|
|
1436
|
+
showValues: input.showValues
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// src/layout/scatter.ts
|
|
1441
|
+
import { getStringWidth as getStringWidth5 } from "@tuicomponents/core";
|
|
1442
|
+
function computeScatterLayout(input) {
|
|
1443
|
+
const series = input.series;
|
|
1444
|
+
const scatterStyle = input.scatterStyle;
|
|
1445
|
+
const allXValues = [];
|
|
1446
|
+
const allYValues = [];
|
|
1447
|
+
for (const s of series) {
|
|
1448
|
+
for (const point of s.data) {
|
|
1449
|
+
const xVal = typeof point.x === "number" ? point.x : parseFloat(point.x);
|
|
1450
|
+
if (!isNaN(xVal)) {
|
|
1451
|
+
allXValues.push(xVal);
|
|
1452
|
+
}
|
|
1453
|
+
allYValues.push(point.y);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
if (allXValues.length === 0 || allYValues.length === 0) {
|
|
1457
|
+
return {
|
|
1458
|
+
type: "scatter",
|
|
1459
|
+
scatterStyle,
|
|
1460
|
+
seriesNames: series.map((s) => s.name),
|
|
1461
|
+
points: [],
|
|
1462
|
+
xScale: { min: 0, max: 100, step: 20, ticks: [0, 20, 40, 60, 80, 100] },
|
|
1463
|
+
yScale: { min: 0, max: 100, step: 20, ticks: [0, 20, 40, 60, 80, 100] },
|
|
1464
|
+
yAxisWidth: 6,
|
|
1465
|
+
chartWidth: input.width - 6,
|
|
1466
|
+
chartHeight: input.height - 2,
|
|
1467
|
+
width: input.width,
|
|
1468
|
+
height: input.height
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
const seriesNames = series.map((s) => s.name);
|
|
1472
|
+
const xDataMin = Math.min(...allXValues);
|
|
1473
|
+
const xDataMax = Math.max(...allXValues);
|
|
1474
|
+
const yDataMin = Math.min(...allYValues);
|
|
1475
|
+
const yDataMax = Math.max(...allYValues);
|
|
1476
|
+
const xScale = computeNiceTicks({
|
|
1477
|
+
dataMin: xDataMin,
|
|
1478
|
+
dataMax: xDataMax,
|
|
1479
|
+
tickCount: input.xAxis?.tickCount ?? 5,
|
|
1480
|
+
includeZero: input.xAxis?.min === void 0,
|
|
1481
|
+
forceMin: input.xAxis?.min,
|
|
1482
|
+
forceMax: input.xAxis?.max
|
|
1483
|
+
});
|
|
1484
|
+
const yScale = computeNiceTicks({
|
|
1485
|
+
dataMin: yDataMin,
|
|
1486
|
+
dataMax: yDataMax,
|
|
1487
|
+
tickCount: input.yAxis?.tickCount ?? 5,
|
|
1488
|
+
includeZero: input.yAxis?.min === void 0,
|
|
1489
|
+
forceMin: input.yAxis?.min,
|
|
1490
|
+
forceMax: input.yAxis?.max
|
|
1491
|
+
});
|
|
1492
|
+
const yFormat = input.yAxis?.format ?? "number";
|
|
1493
|
+
const yDecimals = input.yAxis?.decimals;
|
|
1494
|
+
let yAxisWidth = 0;
|
|
1495
|
+
for (const tick of yScale.ticks) {
|
|
1496
|
+
const label = formatTickValue(tick, yFormat, yDecimals);
|
|
1497
|
+
const width = getStringWidth5(label);
|
|
1498
|
+
if (width > yAxisWidth) {
|
|
1499
|
+
yAxisWidth = width;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
yAxisWidth += 2;
|
|
1503
|
+
const chartHeight = input.height - 2;
|
|
1504
|
+
const chartWidth = input.width - yAxisWidth - 1;
|
|
1505
|
+
const points = [];
|
|
1506
|
+
for (let seriesIndex = 0; seriesIndex < series.length; seriesIndex++) {
|
|
1507
|
+
const s = series[seriesIndex];
|
|
1508
|
+
if (!s) continue;
|
|
1509
|
+
for (const point of s.data) {
|
|
1510
|
+
const xVal = typeof point.x === "number" ? point.x : parseFloat(point.x);
|
|
1511
|
+
if (isNaN(xVal)) continue;
|
|
1512
|
+
const normalizedX = scaleValue(xVal, xScale.min, xScale.max, 1);
|
|
1513
|
+
const normalizedY = scaleValue(point.y, yScale.min, yScale.max, 1);
|
|
1514
|
+
const charX = Math.round(
|
|
1515
|
+
Math.max(0, Math.min(1, normalizedX)) * (chartWidth - 1)
|
|
1516
|
+
);
|
|
1517
|
+
const charY = Math.round(
|
|
1518
|
+
(1 - Math.max(0, Math.min(1, normalizedY))) * (chartHeight - 1)
|
|
1519
|
+
);
|
|
1520
|
+
points.push({
|
|
1521
|
+
dataX: xVal,
|
|
1522
|
+
dataY: point.y,
|
|
1523
|
+
charX,
|
|
1524
|
+
charY,
|
|
1525
|
+
seriesIndex
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
if (scatterStyle === "braille") {
|
|
1530
|
+
return computeBrailleScatterLayout(
|
|
1531
|
+
input,
|
|
1532
|
+
seriesNames,
|
|
1533
|
+
points,
|
|
1534
|
+
xScale,
|
|
1535
|
+
yScale,
|
|
1536
|
+
yAxisWidth,
|
|
1537
|
+
chartWidth,
|
|
1538
|
+
chartHeight
|
|
1539
|
+
);
|
|
1540
|
+
} else {
|
|
1541
|
+
return computeDotsScatterLayout(
|
|
1542
|
+
input,
|
|
1543
|
+
seriesNames,
|
|
1544
|
+
points,
|
|
1545
|
+
xScale,
|
|
1546
|
+
yScale,
|
|
1547
|
+
yAxisWidth,
|
|
1548
|
+
chartWidth,
|
|
1549
|
+
chartHeight
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
function computeDotsScatterLayout(input, seriesNames, points, xScale, yScale, yAxisWidth, chartWidth, chartHeight) {
|
|
1554
|
+
const grid = [];
|
|
1555
|
+
const seriesIndices = [];
|
|
1556
|
+
for (let row = 0; row < chartHeight; row++) {
|
|
1557
|
+
grid.push(Array(chartWidth).fill(" "));
|
|
1558
|
+
seriesIndices.push(Array(chartWidth).fill(null));
|
|
1559
|
+
}
|
|
1560
|
+
for (const point of points) {
|
|
1561
|
+
if (point.charY >= 0 && point.charY < chartHeight && point.charX >= 0 && point.charX < chartWidth) {
|
|
1562
|
+
const marker = SCATTER_MARKER_SEQUENCE[point.seriesIndex % SCATTER_MARKER_SEQUENCE.length];
|
|
1563
|
+
if (!marker) continue;
|
|
1564
|
+
const gridRow = grid[point.charY];
|
|
1565
|
+
const seriesRow = seriesIndices[point.charY];
|
|
1566
|
+
if (!gridRow || !seriesRow) continue;
|
|
1567
|
+
gridRow[point.charX] = marker;
|
|
1568
|
+
seriesRow[point.charX] = point.seriesIndex;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
return {
|
|
1572
|
+
type: "scatter",
|
|
1573
|
+
scatterStyle: "dots",
|
|
1574
|
+
seriesNames,
|
|
1575
|
+
points,
|
|
1576
|
+
xScale,
|
|
1577
|
+
yScale,
|
|
1578
|
+
yAxisWidth,
|
|
1579
|
+
chartWidth,
|
|
1580
|
+
chartHeight,
|
|
1581
|
+
width: input.width,
|
|
1582
|
+
height: input.height,
|
|
1583
|
+
grid,
|
|
1584
|
+
seriesIndices
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
function computeBrailleScatterLayout(input, seriesNames, points, xScale, yScale, yAxisWidth, chartWidth, chartHeight) {
|
|
1588
|
+
const canvas = new BrailleCanvas(chartWidth, chartHeight);
|
|
1589
|
+
for (const point of points) {
|
|
1590
|
+
const dotX = Math.round(
|
|
1591
|
+
scaleValue(point.dataX, xScale.min, xScale.max, chartWidth * 2 - 1)
|
|
1592
|
+
);
|
|
1593
|
+
const normalizedY = scaleValue(point.dataY, yScale.min, yScale.max, 1);
|
|
1594
|
+
const dotY = Math.round((1 - normalizedY) * (chartHeight * 4 - 1));
|
|
1595
|
+
canvas.drawPoint(dotX, dotY, point.seriesIndex);
|
|
1596
|
+
}
|
|
1597
|
+
const rendered = canvas.render();
|
|
1598
|
+
return {
|
|
1599
|
+
type: "scatter",
|
|
1600
|
+
scatterStyle: "braille",
|
|
1601
|
+
seriesNames,
|
|
1602
|
+
points,
|
|
1603
|
+
xScale,
|
|
1604
|
+
yScale,
|
|
1605
|
+
yAxisWidth,
|
|
1606
|
+
chartWidth,
|
|
1607
|
+
chartHeight,
|
|
1608
|
+
width: input.width,
|
|
1609
|
+
height: input.height,
|
|
1610
|
+
brailleChars: rendered.chars,
|
|
1611
|
+
brailleSeriesIndices: rendered.seriesIndices
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// src/layout/pie.ts
|
|
1616
|
+
function computePieLayout(input) {
|
|
1617
|
+
const type = input.type;
|
|
1618
|
+
const series = input.series;
|
|
1619
|
+
const slicesData = [];
|
|
1620
|
+
for (const s of series) {
|
|
1621
|
+
for (const point of s.data) {
|
|
1622
|
+
const label = point.label ?? String(point.x);
|
|
1623
|
+
if (point.y > 0) {
|
|
1624
|
+
slicesData.push({ label, value: point.y });
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
const total = slicesData.reduce((sum, s) => sum + s.value, 0);
|
|
1629
|
+
if (total === 0 || slicesData.length === 0) {
|
|
1630
|
+
return {
|
|
1631
|
+
type,
|
|
1632
|
+
slices: [],
|
|
1633
|
+
total: 0,
|
|
1634
|
+
radius: 0,
|
|
1635
|
+
centerX: Math.floor(input.width / 2),
|
|
1636
|
+
centerY: Math.floor(input.height / 2),
|
|
1637
|
+
innerRadius: 0,
|
|
1638
|
+
...input.centerLabel !== void 0 && {
|
|
1639
|
+
centerLabel: input.centerLabel
|
|
1640
|
+
},
|
|
1641
|
+
width: input.width,
|
|
1642
|
+
height: input.height,
|
|
1643
|
+
brailleChars: [],
|
|
1644
|
+
brailleSeriesIndices: [],
|
|
1645
|
+
seriesStyles: SERIES_STYLES
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
const chartHeight = input.height - 2;
|
|
1649
|
+
const chartWidth = input.width;
|
|
1650
|
+
const maxRadiusY = Math.floor(chartHeight / 2);
|
|
1651
|
+
const maxRadiusX = Math.floor(chartWidth / 2);
|
|
1652
|
+
const radius = Math.min(maxRadiusX, maxRadiusY);
|
|
1653
|
+
const centerX = Math.floor(chartWidth / 2);
|
|
1654
|
+
const centerY = Math.floor(chartHeight / 2);
|
|
1655
|
+
const innerRadiusRatio = type === "donut" ? input.innerRadius : 0;
|
|
1656
|
+
const slices = [];
|
|
1657
|
+
let currentAngle = 0;
|
|
1658
|
+
for (let i = 0; i < slicesData.length; i++) {
|
|
1659
|
+
const data = slicesData[i];
|
|
1660
|
+
if (!data) continue;
|
|
1661
|
+
const percentage = data.value / total * 100;
|
|
1662
|
+
const arcAngle = data.value / total * Math.PI * 2;
|
|
1663
|
+
const style = SERIES_STYLES[i % SERIES_STYLES.length];
|
|
1664
|
+
if (!style) continue;
|
|
1665
|
+
slices.push({
|
|
1666
|
+
label: data.label,
|
|
1667
|
+
value: data.value,
|
|
1668
|
+
percentage,
|
|
1669
|
+
startAngle: currentAngle,
|
|
1670
|
+
endAngle: currentAngle + arcAngle,
|
|
1671
|
+
barChar: style.char,
|
|
1672
|
+
useBackticks: style.useBackticks,
|
|
1673
|
+
seriesIndex: i
|
|
1674
|
+
});
|
|
1675
|
+
currentAngle += arcAngle;
|
|
1676
|
+
}
|
|
1677
|
+
const canvas = new BrailleCanvas(chartWidth, chartHeight);
|
|
1678
|
+
const dotCenterX = centerX * 2;
|
|
1679
|
+
const dotCenterY = centerY * 4;
|
|
1680
|
+
const dotRadiusX = radius * 2;
|
|
1681
|
+
const dotRadiusY = radius * 4;
|
|
1682
|
+
const dotRadius = Math.min(dotRadiusX, dotRadiusY);
|
|
1683
|
+
const _dotInnerRadius = dotRadius * innerRadiusRatio;
|
|
1684
|
+
for (const slice of slices) {
|
|
1685
|
+
drawEllipticalWedge(
|
|
1686
|
+
canvas,
|
|
1687
|
+
dotCenterX,
|
|
1688
|
+
dotCenterY,
|
|
1689
|
+
dotRadiusX,
|
|
1690
|
+
dotRadiusY,
|
|
1691
|
+
slice.startAngle,
|
|
1692
|
+
slice.endAngle,
|
|
1693
|
+
slice.seriesIndex,
|
|
1694
|
+
dotRadiusX * innerRadiusRatio,
|
|
1695
|
+
dotRadiusY * innerRadiusRatio
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
const rendered = canvas.render();
|
|
1699
|
+
return {
|
|
1700
|
+
type,
|
|
1701
|
+
slices,
|
|
1702
|
+
total,
|
|
1703
|
+
radius,
|
|
1704
|
+
centerX,
|
|
1705
|
+
centerY,
|
|
1706
|
+
innerRadius: radius * innerRadiusRatio,
|
|
1707
|
+
...input.centerLabel !== void 0 && { centerLabel: input.centerLabel },
|
|
1708
|
+
width: input.width,
|
|
1709
|
+
height: input.height,
|
|
1710
|
+
brailleChars: rendered.chars,
|
|
1711
|
+
brailleSeriesIndices: rendered.seriesIndices,
|
|
1712
|
+
seriesStyles: SERIES_STYLES
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
function drawEllipticalWedge(canvas, centerX, centerY, radiusX, radiusY, startAngle, endAngle, seriesIndex, innerRadiusX, innerRadiusY) {
|
|
1716
|
+
if (radiusX <= 0 || radiusY <= 0) return;
|
|
1717
|
+
const twoPi = Math.PI * 2;
|
|
1718
|
+
startAngle = (startAngle % twoPi + twoPi) % twoPi;
|
|
1719
|
+
endAngle = (endAngle % twoPi + twoPi) % twoPi;
|
|
1720
|
+
const arcLength = endAngle > startAngle ? endAngle - startAngle : twoPi - startAngle + endAngle;
|
|
1721
|
+
const avgRadius = (radiusX + radiusY) / 2;
|
|
1722
|
+
const angleSteps = Math.max(16, Math.ceil(avgRadius * arcLength * 0.4));
|
|
1723
|
+
for (let i = 0; i <= angleSteps; i++) {
|
|
1724
|
+
const t = i / angleSteps;
|
|
1725
|
+
let angle;
|
|
1726
|
+
if (endAngle > startAngle) {
|
|
1727
|
+
angle = startAngle + t * (endAngle - startAngle);
|
|
1728
|
+
} else {
|
|
1729
|
+
angle = startAngle + t * (twoPi - startAngle + endAngle);
|
|
1730
|
+
if (angle >= twoPi) angle -= twoPi;
|
|
1731
|
+
}
|
|
1732
|
+
const mathAngle = angle - Math.PI / 2;
|
|
1733
|
+
const cos = Math.cos(mathAngle);
|
|
1734
|
+
const sin = Math.sin(mathAngle);
|
|
1735
|
+
const maxR = Math.max(radiusX, radiusY);
|
|
1736
|
+
const innerR = Math.max(innerRadiusX, innerRadiusY);
|
|
1737
|
+
for (let r = Math.ceil(innerR); r <= maxR; r++) {
|
|
1738
|
+
const scaleX = radiusX / maxR;
|
|
1739
|
+
const scaleY = radiusY / maxR;
|
|
1740
|
+
const x = Math.round(centerX + r * cos * scaleX);
|
|
1741
|
+
const y = Math.round(centerY + r * sin * scaleY);
|
|
1742
|
+
canvas.setDot(x, y, seriesIndex);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// src/layout/heatmap.ts
|
|
1748
|
+
import { getStringWidth as getStringWidth6 } from "@tuicomponents/core";
|
|
1749
|
+
function computeHeatmapLayout(input) {
|
|
1750
|
+
const series = input.series;
|
|
1751
|
+
const heatmapStyle = input.heatmapStyle;
|
|
1752
|
+
const rowLabelSet = /* @__PURE__ */ new Set();
|
|
1753
|
+
const colLabelSet = /* @__PURE__ */ new Set();
|
|
1754
|
+
const valueMap = /* @__PURE__ */ new Map();
|
|
1755
|
+
for (const s of series) {
|
|
1756
|
+
for (const point of s.data) {
|
|
1757
|
+
const colLabel = String(point.x);
|
|
1758
|
+
const rowLabel = point.label ?? s.name;
|
|
1759
|
+
const value = point.y;
|
|
1760
|
+
rowLabelSet.add(rowLabel);
|
|
1761
|
+
colLabelSet.add(colLabel);
|
|
1762
|
+
valueMap.set(JSON.stringify([rowLabel, colLabel]), value);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
const rowLabels = Array.from(rowLabelSet);
|
|
1766
|
+
const colLabels = Array.from(colLabelSet);
|
|
1767
|
+
const values = Array.from(valueMap.values());
|
|
1768
|
+
const minValue = values.length > 0 ? Math.min(...values) : 0;
|
|
1769
|
+
const maxValue = values.length > 0 ? Math.max(...values) : 1;
|
|
1770
|
+
const valueRange = { min: minValue, max: maxValue };
|
|
1771
|
+
let rowLabelWidth = 0;
|
|
1772
|
+
for (const label of rowLabels) {
|
|
1773
|
+
const width = getStringWidth6(label);
|
|
1774
|
+
if (width > rowLabelWidth) {
|
|
1775
|
+
rowLabelWidth = width;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
rowLabelWidth += 2;
|
|
1779
|
+
let cellWidth;
|
|
1780
|
+
if (heatmapStyle === "numeric") {
|
|
1781
|
+
const maxValueWidth = Math.max(
|
|
1782
|
+
getStringWidth6(formatValue(minValue)),
|
|
1783
|
+
getStringWidth6(formatValue(maxValue))
|
|
1784
|
+
);
|
|
1785
|
+
cellWidth = Math.max(3, maxValueWidth + 1);
|
|
1786
|
+
} else {
|
|
1787
|
+
cellWidth = 2;
|
|
1788
|
+
}
|
|
1789
|
+
let colLabelWidth = cellWidth;
|
|
1790
|
+
for (const label of colLabels) {
|
|
1791
|
+
const width = getStringWidth6(label);
|
|
1792
|
+
if (width > colLabelWidth) {
|
|
1793
|
+
colLabelWidth = width;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
const cells = [];
|
|
1797
|
+
for (let rowIdx = 0; rowIdx < rowLabels.length; rowIdx++) {
|
|
1798
|
+
const rowLabel = rowLabels[rowIdx];
|
|
1799
|
+
if (!rowLabel) continue;
|
|
1800
|
+
const rowCells = [];
|
|
1801
|
+
for (let colIdx = 0; colIdx < colLabels.length; colIdx++) {
|
|
1802
|
+
const colLabel = colLabels[colIdx];
|
|
1803
|
+
if (!colLabel) continue;
|
|
1804
|
+
const key = JSON.stringify([rowLabel, colLabel]);
|
|
1805
|
+
const value = valueMap.get(key) ?? 0;
|
|
1806
|
+
const range = maxValue - minValue;
|
|
1807
|
+
const normalizedValue = range > 0 ? (value - minValue) / range : 0.5;
|
|
1808
|
+
let displayChar;
|
|
1809
|
+
if (heatmapStyle === "numeric") {
|
|
1810
|
+
displayChar = formatValue(value);
|
|
1811
|
+
} else {
|
|
1812
|
+
const baseChar = valueToHeatmapChar(
|
|
1813
|
+
normalizedValue,
|
|
1814
|
+
heatmapStyle === "blocks" ? "blocks" : "ascii"
|
|
1815
|
+
);
|
|
1816
|
+
displayChar = baseChar.repeat(cellWidth);
|
|
1817
|
+
}
|
|
1818
|
+
rowCells.push({
|
|
1819
|
+
row: rowIdx,
|
|
1820
|
+
col: colIdx,
|
|
1821
|
+
value,
|
|
1822
|
+
normalizedValue,
|
|
1823
|
+
displayChar
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
cells.push(rowCells);
|
|
1827
|
+
}
|
|
1828
|
+
return {
|
|
1829
|
+
type: "heatmap",
|
|
1830
|
+
heatmapStyle,
|
|
1831
|
+
rowLabels,
|
|
1832
|
+
colLabels,
|
|
1833
|
+
cells,
|
|
1834
|
+
valueRange,
|
|
1835
|
+
cellWidth,
|
|
1836
|
+
colLabelWidth,
|
|
1837
|
+
rowLabelWidth,
|
|
1838
|
+
width: input.width,
|
|
1839
|
+
height: input.height
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
function formatValue(value) {
|
|
1843
|
+
if (Number.isInteger(value)) {
|
|
1844
|
+
return String(value);
|
|
1845
|
+
}
|
|
1846
|
+
if (Math.abs(value) >= 100) {
|
|
1847
|
+
return value.toFixed(0);
|
|
1848
|
+
}
|
|
1849
|
+
if (Math.abs(value) >= 10) {
|
|
1850
|
+
return value.toFixed(1);
|
|
1851
|
+
}
|
|
1852
|
+
return value.toFixed(2);
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
// src/renderers/ansi.ts
|
|
1856
|
+
import { padToWidth as padToWidth2 } from "@tuicomponents/core";
|
|
1857
|
+
|
|
1858
|
+
// src/core/legend.ts
|
|
1859
|
+
import { getStringWidth as getStringWidth7 } from "@tuicomponents/core";
|
|
1860
|
+
function getLegendItemWidth(item) {
|
|
1861
|
+
const baseWidth = getStringWidth7(item.symbol) + 1 + getStringWidth7(item.name);
|
|
1862
|
+
if (item.useBackticks) {
|
|
1863
|
+
return baseWidth + 1;
|
|
1864
|
+
}
|
|
1865
|
+
return baseWidth;
|
|
1866
|
+
}
|
|
1867
|
+
function formatLegendItem(item, forMarkdown) {
|
|
1868
|
+
if (forMarkdown && item.useBackticks) {
|
|
1869
|
+
return ` \`${item.symbol}\` ${item.name}`;
|
|
1870
|
+
}
|
|
1871
|
+
return `${item.symbol} ${item.name}`;
|
|
1872
|
+
}
|
|
1873
|
+
function computeLegendLayout(options) {
|
|
1874
|
+
const { items, position, boxed = false, maxWidth = 80 } = options;
|
|
1875
|
+
if (position === "none" || items.length === 0) {
|
|
1876
|
+
return {
|
|
1877
|
+
position,
|
|
1878
|
+
boxed,
|
|
1879
|
+
rows: [],
|
|
1880
|
+
totalWidth: 0,
|
|
1881
|
+
totalHeight: 0
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
const itemWidths = items.map((item) => getLegendItemWidth(item));
|
|
1885
|
+
const _separator = " ";
|
|
1886
|
+
const separatorWidth = 2;
|
|
1887
|
+
const rows = [];
|
|
1888
|
+
let currentRow = [];
|
|
1889
|
+
let currentWidth = 0;
|
|
1890
|
+
for (let i = 0; i < items.length; i++) {
|
|
1891
|
+
const item = items[i];
|
|
1892
|
+
const itemWidth = itemWidths[i];
|
|
1893
|
+
if (!item || itemWidth === void 0) continue;
|
|
1894
|
+
const widthWithSeparator = currentRow.length > 0 ? itemWidth + separatorWidth : itemWidth;
|
|
1895
|
+
if (currentWidth + widthWithSeparator > maxWidth && currentRow.length > 0) {
|
|
1896
|
+
rows.push({ items: currentRow, width: currentWidth });
|
|
1897
|
+
currentRow = [item];
|
|
1898
|
+
currentWidth = itemWidth;
|
|
1899
|
+
} else {
|
|
1900
|
+
currentRow.push(item);
|
|
1901
|
+
currentWidth += widthWithSeparator;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
if (currentRow.length > 0) {
|
|
1905
|
+
rows.push({ items: currentRow, width: currentWidth });
|
|
1906
|
+
}
|
|
1907
|
+
const totalWidth = Math.max(...rows.map((r) => r.width));
|
|
1908
|
+
const totalHeight = rows.length + (boxed ? 2 : 0);
|
|
1909
|
+
return {
|
|
1910
|
+
position,
|
|
1911
|
+
boxed,
|
|
1912
|
+
rows,
|
|
1913
|
+
totalWidth,
|
|
1914
|
+
totalHeight
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
function renderLegendRow(row, forMarkdown) {
|
|
1918
|
+
return row.items.map((item) => formatLegendItem(item, forMarkdown)).join(" ");
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// src/core/axis-renderer.ts
|
|
1922
|
+
import { padToWidth } from "@tuicomponents/core";
|
|
1923
|
+
function computeYAxisRow(row, config) {
|
|
1924
|
+
const {
|
|
1925
|
+
scale,
|
|
1926
|
+
chartHeight,
|
|
1927
|
+
labelWidth,
|
|
1928
|
+
format = "number",
|
|
1929
|
+
decimals
|
|
1930
|
+
} = config;
|
|
1931
|
+
const rowBottom = row / chartHeight;
|
|
1932
|
+
const rowTop = (row + 1) / chartHeight;
|
|
1933
|
+
let label = " ".repeat(labelWidth);
|
|
1934
|
+
let hasTick = false;
|
|
1935
|
+
for (const tick of scale.ticks) {
|
|
1936
|
+
const tickNorm = (tick - scale.min) / (scale.max - scale.min);
|
|
1937
|
+
if (tickNorm < 1e-3) continue;
|
|
1938
|
+
const epsilon = 1e-3;
|
|
1939
|
+
if (tickNorm > rowBottom - epsilon && tickNorm <= rowTop) {
|
|
1940
|
+
label = padToWidth(formatTickValue(tick, format, decimals), labelWidth - 1) + " ";
|
|
1941
|
+
hasTick = true;
|
|
1942
|
+
break;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
const axisChar = hasTick ? AXIS_CHARS.yTickLeft : AXIS_CHARS.vertical;
|
|
1946
|
+
return { label, hasTick, axisChar };
|
|
1947
|
+
}
|
|
1948
|
+
function renderXAxis(config) {
|
|
1949
|
+
const {
|
|
1950
|
+
categories,
|
|
1951
|
+
barWidth,
|
|
1952
|
+
yAxisWidth,
|
|
1953
|
+
chartWidth,
|
|
1954
|
+
minValue,
|
|
1955
|
+
format = "number",
|
|
1956
|
+
decimals
|
|
1957
|
+
} = config;
|
|
1958
|
+
const zeroLabel = padToWidth(formatTickValue(minValue, format, decimals), yAxisWidth - 1) + " ";
|
|
1959
|
+
let xAxisLine = "";
|
|
1960
|
+
for (let i = 0; i < categories.length; i++) {
|
|
1961
|
+
const tickPos = Math.floor(barWidth / 2);
|
|
1962
|
+
const beforeTick = AXIS_CHARS.horizontal.repeat(tickPos);
|
|
1963
|
+
const afterTick = AXIS_CHARS.horizontal.repeat(barWidth - tickPos - 1);
|
|
1964
|
+
xAxisLine += beforeTick + AXIS_CHARS.xTick + afterTick;
|
|
1965
|
+
if (i < categories.length - 1) {
|
|
1966
|
+
xAxisLine += AXIS_CHARS.horizontal;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
const remaining = chartWidth - yAxisWidth - xAxisLine.length - 1;
|
|
1970
|
+
if (remaining > 0) {
|
|
1971
|
+
xAxisLine += AXIS_CHARS.horizontal.repeat(remaining);
|
|
1972
|
+
}
|
|
1973
|
+
const fullAxisLine = zeroLabel + AXIS_CHARS.origin + xAxisLine;
|
|
1974
|
+
let labelLine = "";
|
|
1975
|
+
for (let i = 0; i < categories.length; i++) {
|
|
1976
|
+
const label = categories[i];
|
|
1977
|
+
if (!label) continue;
|
|
1978
|
+
const tickPos = Math.floor(barWidth / 2);
|
|
1979
|
+
const labelStart = Math.max(0, tickPos - Math.floor(label.length / 2));
|
|
1980
|
+
const labelEnd = labelStart + label.length;
|
|
1981
|
+
const paddingBefore = " ".repeat(labelStart);
|
|
1982
|
+
const paddingAfter = " ".repeat(Math.max(0, barWidth - labelEnd));
|
|
1983
|
+
labelLine += paddingBefore + label + paddingAfter;
|
|
1984
|
+
if (i < categories.length - 1) {
|
|
1985
|
+
labelLine += " ";
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
const fullLabelLine = " ".repeat(yAxisWidth + 1) + labelLine;
|
|
1989
|
+
return {
|
|
1990
|
+
axisLine: fullAxisLine,
|
|
1991
|
+
labelLine: fullLabelLine
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
function buildChartRow(row, content, config) {
|
|
1995
|
+
const { label, axisChar } = computeYAxisRow(row, config);
|
|
1996
|
+
return `${label}${axisChar}${content}`;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// src/renderers/ansi.ts
|
|
2000
|
+
function renderLegendRowAnsi(row, theme) {
|
|
2001
|
+
return row.items.map((item) => {
|
|
2002
|
+
const symbol = item.useBackticks && theme ? theme.semantic.secondary(item.symbol) : theme ? theme.semantic.primary(item.symbol) : item.symbol;
|
|
2003
|
+
return `${symbol} ${item.name}`;
|
|
2004
|
+
}).join(" ");
|
|
2005
|
+
}
|
|
2006
|
+
function renderBarChartAnsi(layout, options) {
|
|
2007
|
+
const { theme } = options;
|
|
2008
|
+
const lines = [];
|
|
2009
|
+
for (const bar of layout.bars) {
|
|
2010
|
+
const label = padToWidth2(bar.label, layout.maxLabelWidth);
|
|
2011
|
+
const coloredLabel = theme ? theme.semantic.header(label) : label;
|
|
2012
|
+
const barStr = bar.barChar.repeat(bar.length);
|
|
2013
|
+
const coloredBar = theme ? theme.semantic.primary(barStr) : barStr;
|
|
2014
|
+
const coloredValue = theme ? theme.semantic.secondary(bar.formattedValue) : bar.formattedValue;
|
|
2015
|
+
if (layout.showValues) {
|
|
2016
|
+
lines.push(`${coloredLabel} ${coloredBar} ${coloredValue}`);
|
|
2017
|
+
} else {
|
|
2018
|
+
lines.push(`${coloredLabel} ${coloredBar}`);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
return lines.join("\n");
|
|
2022
|
+
}
|
|
2023
|
+
function renderVerticalBarChartAnsi(layout, options) {
|
|
2024
|
+
const { theme, input } = options;
|
|
2025
|
+
const lines = [];
|
|
2026
|
+
const chartHeight = layout.barAreaSize;
|
|
2027
|
+
const numBars = layout.bars.length;
|
|
2028
|
+
const barWidth = Math.max(1, Math.floor((layout.width - 2) / numBars) - 1);
|
|
2029
|
+
const yAxisWidth = layout.maxValueWidth + 2;
|
|
2030
|
+
const format = input.yAxis?.format ?? "number";
|
|
2031
|
+
const decimals = input.yAxis?.decimals;
|
|
2032
|
+
for (let row = chartHeight - 1; row >= 0; row--) {
|
|
2033
|
+
const rowThreshold = (row + 1) / chartHeight;
|
|
2034
|
+
let yLabel = " ".repeat(yAxisWidth);
|
|
2035
|
+
for (const tick of layout.yScale.ticks) {
|
|
2036
|
+
const tickNorm = (tick - layout.yScale.min) / (layout.yScale.max - layout.yScale.min);
|
|
2037
|
+
if (Math.abs(tickNorm - rowThreshold) < 0.5 / chartHeight) {
|
|
2038
|
+
yLabel = padToWidth2(formatTickValue(tick, format, decimals), yAxisWidth - 1) + " ";
|
|
2039
|
+
break;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
const axisChar = row === 0 ? AXIS_CHARS.origin : AXIS_CHARS.vertical;
|
|
2043
|
+
const segments = [];
|
|
2044
|
+
for (const bar of layout.bars) {
|
|
2045
|
+
const barNorm = bar.length / layout.barAreaSize;
|
|
2046
|
+
if (barNorm >= rowThreshold) {
|
|
2047
|
+
const barSegment = bar.barChar.repeat(barWidth);
|
|
2048
|
+
segments.push(theme ? theme.semantic.primary(barSegment) : barSegment);
|
|
2049
|
+
} else {
|
|
2050
|
+
segments.push(" ".repeat(barWidth));
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
lines.push(`${yLabel}${axisChar}${segments.join(" ")}`);
|
|
2054
|
+
}
|
|
2055
|
+
const xAxisLine = AXIS_CHARS.horizontal.repeat(layout.width - yAxisWidth - 1);
|
|
2056
|
+
lines.push(" ".repeat(yAxisWidth) + AXIS_CHARS.origin + xAxisLine);
|
|
2057
|
+
const xLabels = [];
|
|
2058
|
+
for (const bar of layout.bars) {
|
|
2059
|
+
xLabels.push(padToWidth2(bar.label, barWidth));
|
|
2060
|
+
}
|
|
2061
|
+
lines.push(" ".repeat(yAxisWidth + 1) + xLabels.join(" "));
|
|
2062
|
+
return lines.join("\n");
|
|
2063
|
+
}
|
|
2064
|
+
function renderStackedBarChartAnsi(layout, options) {
|
|
2065
|
+
const { theme, input } = options;
|
|
2066
|
+
const lines = [];
|
|
2067
|
+
const isVertical = layout.type === "bar-stacked-vertical";
|
|
2068
|
+
if (isVertical) {
|
|
2069
|
+
const chartHeight = layout.barAreaSize;
|
|
2070
|
+
const numStacks = layout.stacks.length;
|
|
2071
|
+
const barWidth = Math.max(
|
|
2072
|
+
1,
|
|
2073
|
+
Math.floor((layout.width - 2) / numStacks) - 1
|
|
2074
|
+
);
|
|
2075
|
+
const yAxisWidth = layout.maxValueWidth + 2;
|
|
2076
|
+
const format = input.yAxis?.format ?? "number";
|
|
2077
|
+
const decimals = input.yAxis?.decimals;
|
|
2078
|
+
const yAxisConfig = {
|
|
2079
|
+
scale: layout.yScale,
|
|
2080
|
+
chartHeight,
|
|
2081
|
+
labelWidth: yAxisWidth,
|
|
2082
|
+
format,
|
|
2083
|
+
decimals
|
|
2084
|
+
};
|
|
2085
|
+
for (let row = chartHeight - 1; row >= 0; row--) {
|
|
2086
|
+
const rowBottom = row / chartHeight;
|
|
2087
|
+
const rowTop = (row + 1) / chartHeight;
|
|
2088
|
+
const { label: yLabel, axisChar } = computeYAxisRow(row, yAxisConfig);
|
|
2089
|
+
const segments = [];
|
|
2090
|
+
for (const stack of layout.stacks) {
|
|
2091
|
+
let activeSegment = null;
|
|
2092
|
+
let cumHeight = 0;
|
|
2093
|
+
for (const segment of stack.segments) {
|
|
2094
|
+
const segmentHeight = segment.length / layout.barAreaSize;
|
|
2095
|
+
if (rowBottom < cumHeight + segmentHeight && rowTop > cumHeight) {
|
|
2096
|
+
activeSegment = segment;
|
|
2097
|
+
break;
|
|
2098
|
+
}
|
|
2099
|
+
cumHeight += segmentHeight;
|
|
2100
|
+
}
|
|
2101
|
+
if (activeSegment) {
|
|
2102
|
+
const barSegment = activeSegment.barChar.repeat(barWidth);
|
|
2103
|
+
segments.push(
|
|
2104
|
+
theme ? theme.semantic.primary(barSegment) : barSegment
|
|
2105
|
+
);
|
|
2106
|
+
} else {
|
|
2107
|
+
segments.push(" ".repeat(barWidth));
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
lines.push(`${yLabel}${axisChar}${segments.join(" ")}`);
|
|
2111
|
+
}
|
|
2112
|
+
const xAxis = renderXAxis({
|
|
2113
|
+
categories: layout.stacks.map((s) => s.label),
|
|
2114
|
+
barWidth,
|
|
2115
|
+
yAxisWidth,
|
|
2116
|
+
chartWidth: layout.width,
|
|
2117
|
+
minValue: layout.yScale.min,
|
|
2118
|
+
format,
|
|
2119
|
+
decimals
|
|
2120
|
+
});
|
|
2121
|
+
lines.push(xAxis.axisLine);
|
|
2122
|
+
lines.push(xAxis.labelLine);
|
|
2123
|
+
} else {
|
|
2124
|
+
for (const stack of layout.stacks) {
|
|
2125
|
+
const label = padToWidth2(stack.label, layout.maxLabelWidth);
|
|
2126
|
+
const coloredLabel = theme ? theme.semantic.header(label) : label;
|
|
2127
|
+
let barStr = "";
|
|
2128
|
+
for (const segment of stack.segments) {
|
|
2129
|
+
const segmentStr = segment.barChar.repeat(segment.length);
|
|
2130
|
+
barStr += theme ? theme.semantic.primary(segmentStr) : segmentStr;
|
|
2131
|
+
}
|
|
2132
|
+
lines.push(`${coloredLabel} ${barStr}`);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
const legendItems = layout.seriesNames.map((name, i) => {
|
|
2136
|
+
const style = layout.seriesStyles[i];
|
|
2137
|
+
if (!style) {
|
|
2138
|
+
throw new Error(`Missing series style at index ${String(i)}`);
|
|
2139
|
+
}
|
|
2140
|
+
return {
|
|
2141
|
+
name,
|
|
2142
|
+
symbol: style.char,
|
|
2143
|
+
useBackticks: style.useBackticks
|
|
2144
|
+
};
|
|
2145
|
+
});
|
|
2146
|
+
const legendLayout = computeLegendLayout({
|
|
2147
|
+
items: legendItems,
|
|
2148
|
+
position: input.legend?.position ?? "bottom",
|
|
2149
|
+
maxWidth: layout.width
|
|
2150
|
+
});
|
|
2151
|
+
if (legendLayout.rows.length > 0) {
|
|
2152
|
+
lines.push("");
|
|
2153
|
+
for (const row of legendLayout.rows) {
|
|
2154
|
+
lines.push(renderLegendRowAnsi(row, theme));
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
return lines.join("\n");
|
|
2158
|
+
}
|
|
2159
|
+
function renderLineChartAnsi(layout, options) {
|
|
2160
|
+
const { theme, input } = options;
|
|
2161
|
+
const lines = [];
|
|
2162
|
+
const chartWidth = layout.rows[0]?.chars.length ?? layout.categories.length;
|
|
2163
|
+
for (const row of layout.rows) {
|
|
2164
|
+
const yLabel = row.yLabel ? padToWidth2(row.yLabel, layout.yAxisWidth - 1) + " " : " ".repeat(layout.yAxisWidth);
|
|
2165
|
+
let content = "";
|
|
2166
|
+
for (let i = 0; i < row.chars.length; i++) {
|
|
2167
|
+
const char = row.chars[i];
|
|
2168
|
+
if (char === void 0) {
|
|
2169
|
+
continue;
|
|
2170
|
+
}
|
|
2171
|
+
const seriesIdx = row.seriesIndices[i];
|
|
2172
|
+
if (char !== " " && theme) {
|
|
2173
|
+
if (seriesIdx !== null && seriesIdx !== void 0 && seriesIdx % 2 === 1) {
|
|
2174
|
+
content += theme.semantic.secondary(char);
|
|
2175
|
+
} else {
|
|
2176
|
+
content += theme.semantic.primary(char);
|
|
2177
|
+
}
|
|
2178
|
+
} else {
|
|
2179
|
+
content += char;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
lines.push(`${yLabel}${AXIS_CHARS.vertical}${content}`);
|
|
2183
|
+
}
|
|
2184
|
+
const xAxisLine = AXIS_CHARS.horizontal.repeat(chartWidth);
|
|
2185
|
+
lines.push(" ".repeat(layout.yAxisWidth) + AXIS_CHARS.origin + xAxisLine);
|
|
2186
|
+
const labelLine = Array(chartWidth).fill(" ");
|
|
2187
|
+
const isBraille = layout.lineStyle === "braille";
|
|
2188
|
+
const spacing = isBraille ? 3 : 2;
|
|
2189
|
+
for (let i = 0; i < layout.categories.length; i++) {
|
|
2190
|
+
const label = layout.categories[i];
|
|
2191
|
+
if (label === void 0) {
|
|
2192
|
+
continue;
|
|
2193
|
+
}
|
|
2194
|
+
const pos = isBraille ? i * spacing + Math.floor(spacing / 2) : i * spacing;
|
|
2195
|
+
if (pos < chartWidth) {
|
|
2196
|
+
labelLine[pos] = label.charAt(0) || " ";
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
lines.push(" ".repeat(layout.yAxisWidth + 1) + labelLine.join(""));
|
|
2200
|
+
if (layout.seriesNames.length > 1) {
|
|
2201
|
+
const legendItems = layout.seriesNames.map((name, i) => {
|
|
2202
|
+
return {
|
|
2203
|
+
name,
|
|
2204
|
+
symbol: "\u28FF",
|
|
2205
|
+
// Use braille full block for line chart legend
|
|
2206
|
+
useBackticks: i % 2 === 1
|
|
2207
|
+
};
|
|
2208
|
+
});
|
|
2209
|
+
const legendLayout = computeLegendLayout({
|
|
2210
|
+
items: legendItems,
|
|
2211
|
+
position: input.legend?.position ?? "bottom",
|
|
2212
|
+
maxWidth: layout.width
|
|
2213
|
+
});
|
|
2214
|
+
if (legendLayout.rows.length > 0) {
|
|
2215
|
+
lines.push("");
|
|
2216
|
+
for (const row of legendLayout.rows) {
|
|
2217
|
+
lines.push(renderLegendRowAnsi(row, theme));
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
return lines.join("\n");
|
|
2222
|
+
}
|
|
2223
|
+
function renderAreaChartAnsi(layout, options) {
|
|
2224
|
+
const { theme, input } = options;
|
|
2225
|
+
const lines = [];
|
|
2226
|
+
for (const row of layout.rows) {
|
|
2227
|
+
const yLabel = row.yLabel ? padToWidth2(row.yLabel, layout.yAxisWidth - 1) + " " : " ".repeat(layout.yAxisWidth);
|
|
2228
|
+
let content = "";
|
|
2229
|
+
for (const char of row.chars) {
|
|
2230
|
+
if (char !== " " && theme) {
|
|
2231
|
+
content += theme.semantic.primary(char);
|
|
2232
|
+
} else {
|
|
2233
|
+
content += char;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
lines.push(`${yLabel}${AXIS_CHARS.vertical}${content}`);
|
|
2237
|
+
}
|
|
2238
|
+
const xAxisLine = AXIS_CHARS.horizontal.repeat(layout.categories.length);
|
|
2239
|
+
lines.push(" ".repeat(layout.yAxisWidth) + AXIS_CHARS.origin + xAxisLine);
|
|
2240
|
+
const xLabels = layout.categories.map((c) => c.charAt(0) || " ").join("");
|
|
2241
|
+
lines.push(" ".repeat(layout.yAxisWidth + 1) + xLabels);
|
|
2242
|
+
const legendItems = layout.seriesNames.map((name, i) => {
|
|
2243
|
+
const style = layout.seriesStyles[i];
|
|
2244
|
+
if (!style) {
|
|
2245
|
+
throw new Error(`Missing series style at index ${String(i)}`);
|
|
2246
|
+
}
|
|
2247
|
+
return {
|
|
2248
|
+
name,
|
|
2249
|
+
symbol: style.char,
|
|
2250
|
+
useBackticks: style.useBackticks
|
|
2251
|
+
};
|
|
2252
|
+
});
|
|
2253
|
+
const legendLayout = computeLegendLayout({
|
|
2254
|
+
items: legendItems,
|
|
2255
|
+
position: input.legend?.position ?? "bottom",
|
|
2256
|
+
maxWidth: layout.width
|
|
2257
|
+
});
|
|
2258
|
+
if (legendLayout.rows.length > 0) {
|
|
2259
|
+
lines.push("");
|
|
2260
|
+
for (const row of legendLayout.rows) {
|
|
2261
|
+
lines.push(renderLegendRowAnsi(row, theme));
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
return lines.join("\n");
|
|
2265
|
+
}
|
|
2266
|
+
function renderScatterChartAnsi(layout, options) {
|
|
2267
|
+
const { theme, input } = options;
|
|
2268
|
+
const lines = [];
|
|
2269
|
+
const format = input.yAxis?.format ?? "number";
|
|
2270
|
+
const decimals = input.yAxis?.decimals;
|
|
2271
|
+
for (let rowIndex = 0; rowIndex < layout.chartHeight; rowIndex++) {
|
|
2272
|
+
let yLabel = " ".repeat(layout.yAxisWidth);
|
|
2273
|
+
const rowTop = 1 - rowIndex / layout.chartHeight;
|
|
2274
|
+
const rowBottom = 1 - (rowIndex + 1) / layout.chartHeight;
|
|
2275
|
+
for (const tick of layout.yScale.ticks) {
|
|
2276
|
+
const tickNorm = (tick - layout.yScale.min) / (layout.yScale.max - layout.yScale.min);
|
|
2277
|
+
if (tickNorm > rowBottom && tickNorm <= rowTop) {
|
|
2278
|
+
yLabel = padToWidth2(
|
|
2279
|
+
formatTickValue(tick, format, decimals),
|
|
2280
|
+
layout.yAxisWidth - 1
|
|
2281
|
+
) + " ";
|
|
2282
|
+
break;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
let content = "";
|
|
2286
|
+
if (layout.scatterStyle === "braille" && layout.brailleChars) {
|
|
2287
|
+
const rowChars = layout.brailleChars[rowIndex] ?? [];
|
|
2288
|
+
const rowIndices = layout.brailleSeriesIndices?.[rowIndex] ?? [];
|
|
2289
|
+
for (let i = 0; i < rowChars.length; i++) {
|
|
2290
|
+
const char = rowChars[i];
|
|
2291
|
+
if (char === void 0) {
|
|
2292
|
+
continue;
|
|
2293
|
+
}
|
|
2294
|
+
const seriesIdx = rowIndices[i];
|
|
2295
|
+
if (char !== " " && theme) {
|
|
2296
|
+
if (seriesIdx !== null && seriesIdx !== void 0 && seriesIdx % 2 === 1) {
|
|
2297
|
+
content += theme.semantic.secondary(char);
|
|
2298
|
+
} else {
|
|
2299
|
+
content += theme.semantic.primary(char);
|
|
2300
|
+
}
|
|
2301
|
+
} else {
|
|
2302
|
+
content += char;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
} else if (layout.grid) {
|
|
2306
|
+
const rowChars = layout.grid[rowIndex] ?? [];
|
|
2307
|
+
const rowIndices = layout.seriesIndices?.[rowIndex] ?? [];
|
|
2308
|
+
for (let i = 0; i < rowChars.length; i++) {
|
|
2309
|
+
const char = rowChars[i];
|
|
2310
|
+
if (char === void 0) {
|
|
2311
|
+
continue;
|
|
2312
|
+
}
|
|
2313
|
+
const seriesIdx = rowIndices[i];
|
|
2314
|
+
if (char !== " " && theme) {
|
|
2315
|
+
if (seriesIdx !== null && seriesIdx !== void 0 && seriesIdx % 2 === 1) {
|
|
2316
|
+
content += theme.semantic.secondary(char);
|
|
2317
|
+
} else {
|
|
2318
|
+
content += theme.semantic.primary(char);
|
|
2319
|
+
}
|
|
2320
|
+
} else {
|
|
2321
|
+
content += char;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
lines.push(`${yLabel}${AXIS_CHARS.vertical}${content}`);
|
|
2326
|
+
}
|
|
2327
|
+
const xAxisLine = AXIS_CHARS.horizontal.repeat(layout.chartWidth);
|
|
2328
|
+
lines.push(" ".repeat(layout.yAxisWidth) + AXIS_CHARS.origin + xAxisLine);
|
|
2329
|
+
const xFormat = input.xAxis?.format ?? "number";
|
|
2330
|
+
const xDecimals = input.xAxis?.decimals;
|
|
2331
|
+
const labelPositions = [];
|
|
2332
|
+
for (const tick of layout.xScale.ticks) {
|
|
2333
|
+
const norm = (tick - layout.xScale.min) / (layout.xScale.max - layout.xScale.min);
|
|
2334
|
+
const pos = Math.round(norm * (layout.chartWidth - 1));
|
|
2335
|
+
const label = formatTickValue(tick, xFormat, xDecimals);
|
|
2336
|
+
labelPositions.push({ pos, label });
|
|
2337
|
+
}
|
|
2338
|
+
const labelLine = Array(layout.chartWidth).fill(" ");
|
|
2339
|
+
for (const { pos, label } of labelPositions) {
|
|
2340
|
+
const halfLen = Math.floor(label.length / 2);
|
|
2341
|
+
const startPos = Math.max(
|
|
2342
|
+
0,
|
|
2343
|
+
Math.min(layout.chartWidth - label.length, pos - halfLen)
|
|
2344
|
+
);
|
|
2345
|
+
for (let i = 0; i < label.length && startPos + i < layout.chartWidth; i++) {
|
|
2346
|
+
const char = label[i];
|
|
2347
|
+
if (char !== void 0) {
|
|
2348
|
+
labelLine[startPos + i] = char;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
lines.push(" ".repeat(layout.yAxisWidth + 1) + labelLine.join(""));
|
|
2353
|
+
if (layout.seriesNames.length > 1) {
|
|
2354
|
+
const symbolArray = ["\u25CF", "\u25A0", "\u25B2", "\u25C6", "+"];
|
|
2355
|
+
const legendItems = layout.seriesNames.map((name, i) => {
|
|
2356
|
+
const symbol = layout.scatterStyle === "braille" ? "\u28FF" : symbolArray[i % 5];
|
|
2357
|
+
if (!symbol) {
|
|
2358
|
+
throw new Error(`Missing symbol at index ${String(i % 5)}`);
|
|
2359
|
+
}
|
|
2360
|
+
return {
|
|
2361
|
+
name,
|
|
2362
|
+
symbol,
|
|
2363
|
+
useBackticks: i % 2 === 1
|
|
2364
|
+
};
|
|
2365
|
+
});
|
|
2366
|
+
const legendLayout = computeLegendLayout({
|
|
2367
|
+
items: legendItems,
|
|
2368
|
+
position: input.legend?.position ?? "bottom",
|
|
2369
|
+
maxWidth: layout.width
|
|
2370
|
+
});
|
|
2371
|
+
if (legendLayout.rows.length > 0) {
|
|
2372
|
+
lines.push("");
|
|
2373
|
+
for (const row of legendLayout.rows) {
|
|
2374
|
+
lines.push(renderLegendRowAnsi(row, theme));
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
return lines.join("\n");
|
|
2379
|
+
}
|
|
2380
|
+
function renderPieChartAnsi(layout, options) {
|
|
2381
|
+
const { theme, input } = options;
|
|
2382
|
+
const lines = [];
|
|
2383
|
+
if (layout.slices.length === 0) {
|
|
2384
|
+
lines.push("No data");
|
|
2385
|
+
return lines.join("\n");
|
|
2386
|
+
}
|
|
2387
|
+
for (let rowIndex = 0; rowIndex < layout.brailleChars.length; rowIndex++) {
|
|
2388
|
+
const rowChars = layout.brailleChars[rowIndex] ?? [];
|
|
2389
|
+
const rowIndices = layout.brailleSeriesIndices[rowIndex] ?? [];
|
|
2390
|
+
let content = "";
|
|
2391
|
+
for (let i = 0; i < rowChars.length; i++) {
|
|
2392
|
+
const char = rowChars[i];
|
|
2393
|
+
if (char === void 0) {
|
|
2394
|
+
continue;
|
|
2395
|
+
}
|
|
2396
|
+
const seriesIdx = rowIndices[i];
|
|
2397
|
+
if (char !== " " && theme) {
|
|
2398
|
+
if (seriesIdx !== null && seriesIdx !== void 0 && seriesIdx % 2 === 1) {
|
|
2399
|
+
content += theme.semantic.secondary(char);
|
|
2400
|
+
} else {
|
|
2401
|
+
content += theme.semantic.primary(char);
|
|
2402
|
+
}
|
|
2403
|
+
} else {
|
|
2404
|
+
content += char;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
if (layout.type === "donut" && layout.centerLabel && rowIndex === Math.floor(layout.brailleChars.length / 2)) {
|
|
2408
|
+
const labelLen = layout.centerLabel.length;
|
|
2409
|
+
const startPos = Math.floor((layout.width - labelLen) / 2);
|
|
2410
|
+
if (startPos >= 0) {
|
|
2411
|
+
const contentArray = [...content];
|
|
2412
|
+
for (let i = 0; i < labelLen && startPos + i < contentArray.length; i++) {
|
|
2413
|
+
const char = layout.centerLabel[i];
|
|
2414
|
+
if (char !== void 0) {
|
|
2415
|
+
contentArray[startPos + i] = char;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
content = contentArray.join("");
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
lines.push(content);
|
|
2422
|
+
}
|
|
2423
|
+
const legendItems = layout.slices.map((slice) => ({
|
|
2424
|
+
name: `${slice.label} ${slice.percentage.toFixed(0)}%`,
|
|
2425
|
+
symbol: slice.barChar,
|
|
2426
|
+
useBackticks: slice.useBackticks
|
|
2427
|
+
}));
|
|
2428
|
+
const legendLayout = computeLegendLayout({
|
|
2429
|
+
items: legendItems,
|
|
2430
|
+
position: input.legend?.position ?? "bottom",
|
|
2431
|
+
maxWidth: layout.width
|
|
2432
|
+
});
|
|
2433
|
+
if (legendLayout.rows.length > 0) {
|
|
2434
|
+
lines.push("");
|
|
2435
|
+
for (const row of legendLayout.rows) {
|
|
2436
|
+
lines.push(renderLegendRowAnsi(row, theme));
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
return lines.join("\n");
|
|
2440
|
+
}
|
|
2441
|
+
function renderHeatmapAnsi(layout, options) {
|
|
2442
|
+
const { theme } = options;
|
|
2443
|
+
const lines = [];
|
|
2444
|
+
const colLabelRow = " ".repeat(layout.rowLabelWidth) + layout.colLabels.map((label) => padToWidth2(label, layout.colLabelWidth)).join(" ");
|
|
2445
|
+
lines.push(colLabelRow);
|
|
2446
|
+
for (let rowIdx = 0; rowIdx < layout.rowLabels.length; rowIdx++) {
|
|
2447
|
+
const rowLabel = layout.rowLabels[rowIdx];
|
|
2448
|
+
if (rowLabel === void 0) {
|
|
2449
|
+
continue;
|
|
2450
|
+
}
|
|
2451
|
+
const rowCells = layout.cells[rowIdx] ?? [];
|
|
2452
|
+
let row = padToWidth2(rowLabel, layout.rowLabelWidth);
|
|
2453
|
+
for (const cell of rowCells) {
|
|
2454
|
+
let cellStr;
|
|
2455
|
+
if (layout.heatmapStyle === "numeric") {
|
|
2456
|
+
cellStr = padToWidth2(cell.displayChar, layout.colLabelWidth);
|
|
2457
|
+
} else {
|
|
2458
|
+
cellStr = padToWidth2(cell.displayChar, layout.colLabelWidth);
|
|
2459
|
+
}
|
|
2460
|
+
if (theme) {
|
|
2461
|
+
if (cell.normalizedValue > 0.5) {
|
|
2462
|
+
cellStr = theme.semantic.primary(cellStr);
|
|
2463
|
+
} else if (cell.normalizedValue > 0.25) {
|
|
2464
|
+
cellStr = theme.semantic.secondary(cellStr);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
row += cellStr + " ";
|
|
2468
|
+
}
|
|
2469
|
+
lines.push(row.trimEnd());
|
|
2470
|
+
}
|
|
2471
|
+
if (layout.heatmapStyle !== "numeric") {
|
|
2472
|
+
lines.push("");
|
|
2473
|
+
const scaleLabel = layout.heatmapStyle === "blocks" ? `Scale: \u2591 ${String(layout.valueRange.min)} \u2592 \u2593 \u2588 ${String(layout.valueRange.max)}` : `Scale: . ${String(layout.valueRange.min)} : * # ${String(layout.valueRange.max)}`;
|
|
2474
|
+
lines.push(theme ? theme.semantic.secondary(scaleLabel) : scaleLabel);
|
|
2475
|
+
}
|
|
2476
|
+
return lines.join("\n");
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
// src/renderers/markdown.ts
|
|
2480
|
+
import { padToWidth as padToWidth3, anchorLine, DEFAULT_ANCHOR } from "@tuicomponents/core";
|
|
2481
|
+
function wrapInlineCode(text, useBackticks) {
|
|
2482
|
+
if (useBackticks) {
|
|
2483
|
+
return ` \`${text}\``;
|
|
2484
|
+
}
|
|
2485
|
+
return text;
|
|
2486
|
+
}
|
|
2487
|
+
function renderBarChartMarkdown(layout, _options) {
|
|
2488
|
+
const lines = [];
|
|
2489
|
+
for (const bar of layout.bars) {
|
|
2490
|
+
const label = padToWidth3(bar.label, layout.maxLabelWidth);
|
|
2491
|
+
const barStr = bar.barChar.repeat(bar.length);
|
|
2492
|
+
const styledBar = wrapInlineCode(barStr, bar.useBackticks);
|
|
2493
|
+
let content;
|
|
2494
|
+
if (layout.showValues) {
|
|
2495
|
+
content = `${label} ${styledBar} ${bar.formattedValue}`;
|
|
2496
|
+
} else {
|
|
2497
|
+
content = `${label} ${styledBar}`;
|
|
2498
|
+
}
|
|
2499
|
+
if (bar.useBackticks) {
|
|
2500
|
+
content += " ";
|
|
2501
|
+
}
|
|
2502
|
+
lines.push(anchorLine(content, DEFAULT_ANCHOR));
|
|
2503
|
+
}
|
|
2504
|
+
return lines.join("\n");
|
|
2505
|
+
}
|
|
2506
|
+
function renderVerticalBarChartMarkdown(layout, options) {
|
|
2507
|
+
const { input } = options;
|
|
2508
|
+
const lines = [];
|
|
2509
|
+
const chartHeight = layout.barAreaSize;
|
|
2510
|
+
const numBars = layout.bars.length;
|
|
2511
|
+
const barWidth = Math.max(1, Math.floor((layout.width - 2) / numBars) - 1);
|
|
2512
|
+
const yAxisWidth = layout.maxValueWidth + 2;
|
|
2513
|
+
const format = input.yAxis?.format ?? "number";
|
|
2514
|
+
const decimals = input.yAxis?.decimals;
|
|
2515
|
+
for (let row = chartHeight - 1; row >= 0; row--) {
|
|
2516
|
+
const rowThreshold = (row + 1) / chartHeight;
|
|
2517
|
+
let yLabel = " ".repeat(yAxisWidth);
|
|
2518
|
+
for (const tick of layout.yScale.ticks) {
|
|
2519
|
+
const tickNorm = (tick - layout.yScale.min) / (layout.yScale.max - layout.yScale.min);
|
|
2520
|
+
if (Math.abs(tickNorm - rowThreshold) < 0.5 / chartHeight) {
|
|
2521
|
+
yLabel = padToWidth3(formatTickValue(tick, format, decimals), yAxisWidth - 1) + " ";
|
|
2522
|
+
break;
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
const axisChar = row === 0 ? AXIS_CHARS.origin : AXIS_CHARS.vertical;
|
|
2526
|
+
const segments = [];
|
|
2527
|
+
for (const bar of layout.bars) {
|
|
2528
|
+
const barNorm = bar.length / layout.barAreaSize;
|
|
2529
|
+
if (barNorm >= rowThreshold) {
|
|
2530
|
+
const barSegment = bar.barChar.repeat(barWidth);
|
|
2531
|
+
segments.push(wrapInlineCode(barSegment, bar.useBackticks));
|
|
2532
|
+
} else {
|
|
2533
|
+
segments.push(" ".repeat(barWidth));
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
lines.push(
|
|
2537
|
+
anchorLine(`${yLabel}${axisChar}${segments.join(" ")}`, DEFAULT_ANCHOR)
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2540
|
+
const xAxisLine = AXIS_CHARS.horizontal.repeat(layout.width - yAxisWidth - 1);
|
|
2541
|
+
lines.push(
|
|
2542
|
+
anchorLine(
|
|
2543
|
+
" ".repeat(yAxisWidth) + AXIS_CHARS.origin + xAxisLine,
|
|
2544
|
+
DEFAULT_ANCHOR
|
|
2545
|
+
)
|
|
2546
|
+
);
|
|
2547
|
+
const xLabels = [];
|
|
2548
|
+
for (const bar of layout.bars) {
|
|
2549
|
+
xLabels.push(padToWidth3(bar.label, barWidth));
|
|
2550
|
+
}
|
|
2551
|
+
lines.push(
|
|
2552
|
+
anchorLine(" ".repeat(yAxisWidth + 1) + xLabels.join(" "), DEFAULT_ANCHOR)
|
|
2553
|
+
);
|
|
2554
|
+
return lines.join("\n");
|
|
2555
|
+
}
|
|
2556
|
+
function renderStackedBarChartMarkdown(layout, options) {
|
|
2557
|
+
const { input } = options;
|
|
2558
|
+
const lines = [];
|
|
2559
|
+
const isVertical = layout.type === "bar-stacked-vertical";
|
|
2560
|
+
if (isVertical) {
|
|
2561
|
+
const chartHeight = layout.barAreaSize;
|
|
2562
|
+
const numStacks = layout.stacks.length;
|
|
2563
|
+
const barWidth = Math.max(
|
|
2564
|
+
1,
|
|
2565
|
+
Math.floor((layout.width - 2) / numStacks) - 1
|
|
2566
|
+
);
|
|
2567
|
+
const yAxisWidth = layout.maxValueWidth + 2;
|
|
2568
|
+
const format = input.yAxis?.format ?? "number";
|
|
2569
|
+
const decimals = input.yAxis?.decimals;
|
|
2570
|
+
const yAxisConfig = {
|
|
2571
|
+
scale: layout.yScale,
|
|
2572
|
+
chartHeight,
|
|
2573
|
+
labelWidth: yAxisWidth,
|
|
2574
|
+
format,
|
|
2575
|
+
decimals
|
|
2576
|
+
};
|
|
2577
|
+
for (let row = chartHeight - 1; row >= 0; row--) {
|
|
2578
|
+
const rowBottom = row / chartHeight;
|
|
2579
|
+
const rowTop = (row + 1) / chartHeight;
|
|
2580
|
+
const { label: yLabel, axisChar } = computeYAxisRow(row, yAxisConfig);
|
|
2581
|
+
const segments = [];
|
|
2582
|
+
for (const stack of layout.stacks) {
|
|
2583
|
+
let activeSegment = null;
|
|
2584
|
+
let cumHeight = 0;
|
|
2585
|
+
for (const segment of stack.segments) {
|
|
2586
|
+
const segmentHeight = segment.length / layout.barAreaSize;
|
|
2587
|
+
if (rowBottom < cumHeight + segmentHeight && rowTop > cumHeight) {
|
|
2588
|
+
activeSegment = segment;
|
|
2589
|
+
break;
|
|
2590
|
+
}
|
|
2591
|
+
cumHeight += segmentHeight;
|
|
2592
|
+
}
|
|
2593
|
+
if (activeSegment) {
|
|
2594
|
+
const barSegment = activeSegment.barChar.repeat(barWidth);
|
|
2595
|
+
if (activeSegment.useBackticks) {
|
|
2596
|
+
segments.push(`\`${barSegment}\``);
|
|
2597
|
+
} else {
|
|
2598
|
+
segments.push(barSegment);
|
|
2599
|
+
}
|
|
2600
|
+
} else {
|
|
2601
|
+
segments.push(" ".repeat(barWidth));
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
lines.push(
|
|
2605
|
+
anchorLine(`${yLabel}${axisChar}${segments.join(" ")}`, DEFAULT_ANCHOR)
|
|
2606
|
+
);
|
|
2607
|
+
}
|
|
2608
|
+
const xAxis = renderXAxis({
|
|
2609
|
+
categories: layout.stacks.map((s) => s.label),
|
|
2610
|
+
barWidth,
|
|
2611
|
+
yAxisWidth,
|
|
2612
|
+
chartWidth: layout.width,
|
|
2613
|
+
minValue: layout.yScale.min,
|
|
2614
|
+
format,
|
|
2615
|
+
decimals
|
|
2616
|
+
});
|
|
2617
|
+
lines.push(anchorLine(xAxis.axisLine, DEFAULT_ANCHOR));
|
|
2618
|
+
lines.push(anchorLine(xAxis.labelLine, DEFAULT_ANCHOR));
|
|
2619
|
+
} else {
|
|
2620
|
+
for (const stack of layout.stacks) {
|
|
2621
|
+
const label = padToWidth3(stack.label, layout.maxLabelWidth);
|
|
2622
|
+
let barStr = "";
|
|
2623
|
+
for (const segment of stack.segments) {
|
|
2624
|
+
const segmentStr = segment.barChar.repeat(segment.length);
|
|
2625
|
+
if (segment.useBackticks) {
|
|
2626
|
+
barStr += `\`${segmentStr}\``;
|
|
2627
|
+
} else {
|
|
2628
|
+
barStr += segmentStr;
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
lines.push(anchorLine(`${label} ${barStr}`, DEFAULT_ANCHOR));
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
const legendItems = layout.seriesNames.map((name, i) => {
|
|
2635
|
+
const style = layout.seriesStyles[i];
|
|
2636
|
+
if (!style) {
|
|
2637
|
+
throw new Error(`Missing style for series ${String(i)}`);
|
|
2638
|
+
}
|
|
2639
|
+
return {
|
|
2640
|
+
name,
|
|
2641
|
+
symbol: style.char,
|
|
2642
|
+
useBackticks: style.useBackticks
|
|
2643
|
+
};
|
|
2644
|
+
});
|
|
2645
|
+
const legendLayout = computeLegendLayout({
|
|
2646
|
+
items: legendItems,
|
|
2647
|
+
position: input.legend?.position ?? "bottom",
|
|
2648
|
+
maxWidth: layout.width
|
|
2649
|
+
});
|
|
2650
|
+
if (legendLayout.rows.length > 0) {
|
|
2651
|
+
lines.push(anchorLine("", DEFAULT_ANCHOR));
|
|
2652
|
+
for (const row of legendLayout.rows) {
|
|
2653
|
+
lines.push(anchorLine(renderLegendRow(row, true), DEFAULT_ANCHOR));
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
return lines.join("\n");
|
|
2657
|
+
}
|
|
2658
|
+
function renderLineChartMarkdown(layout, options) {
|
|
2659
|
+
const { input } = options;
|
|
2660
|
+
const lines = [];
|
|
2661
|
+
const chartWidth = layout.rows[0]?.chars.length ?? layout.categories.length;
|
|
2662
|
+
for (const row of layout.rows) {
|
|
2663
|
+
const yLabel = row.yLabel ? padToWidth3(row.yLabel, layout.yAxisWidth - 1) + " " : " ".repeat(layout.yAxisWidth);
|
|
2664
|
+
let content = "";
|
|
2665
|
+
for (let i = 0; i < row.chars.length; i++) {
|
|
2666
|
+
const char = row.chars[i];
|
|
2667
|
+
const useBackticks = row.useBackticks[i];
|
|
2668
|
+
if (char === void 0 || useBackticks === void 0) {
|
|
2669
|
+
throw new Error(`Missing char or useBackticks at index ${String(i)}`);
|
|
2670
|
+
}
|
|
2671
|
+
content += wrapInlineCode(char, useBackticks && char !== " ");
|
|
2672
|
+
}
|
|
2673
|
+
lines.push(
|
|
2674
|
+
anchorLine(`${yLabel}${AXIS_CHARS.vertical}${content}`, DEFAULT_ANCHOR)
|
|
2675
|
+
);
|
|
2676
|
+
}
|
|
2677
|
+
const xAxisLine = AXIS_CHARS.horizontal.repeat(chartWidth);
|
|
2678
|
+
lines.push(
|
|
2679
|
+
anchorLine(
|
|
2680
|
+
" ".repeat(layout.yAxisWidth) + AXIS_CHARS.origin + xAxisLine,
|
|
2681
|
+
DEFAULT_ANCHOR
|
|
2682
|
+
)
|
|
2683
|
+
);
|
|
2684
|
+
const labelLine = Array(chartWidth).fill(" ");
|
|
2685
|
+
const isBraille = layout.lineStyle === "braille";
|
|
2686
|
+
const spacing = isBraille ? 3 : 2;
|
|
2687
|
+
for (let i = 0; i < layout.categories.length; i++) {
|
|
2688
|
+
const label = layout.categories[i];
|
|
2689
|
+
if (label === void 0) {
|
|
2690
|
+
throw new Error(`Missing category at index ${String(i)}`);
|
|
2691
|
+
}
|
|
2692
|
+
const pos = isBraille ? i * spacing + Math.floor(spacing / 2) : i * spacing;
|
|
2693
|
+
if (pos < chartWidth) {
|
|
2694
|
+
labelLine[pos] = label.charAt(0) || " ";
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
lines.push(
|
|
2698
|
+
anchorLine(
|
|
2699
|
+
" ".repeat(layout.yAxisWidth + 1) + labelLine.join(""),
|
|
2700
|
+
DEFAULT_ANCHOR
|
|
2701
|
+
)
|
|
2702
|
+
);
|
|
2703
|
+
if (layout.seriesNames.length > 1) {
|
|
2704
|
+
const legendItems = layout.seriesNames.map((name, i) => {
|
|
2705
|
+
return {
|
|
2706
|
+
name,
|
|
2707
|
+
symbol: "\u28FF",
|
|
2708
|
+
// Use braille full block for line chart legend
|
|
2709
|
+
useBackticks: i % 2 === 1
|
|
2710
|
+
};
|
|
2711
|
+
});
|
|
2712
|
+
const legendLayout = computeLegendLayout({
|
|
2713
|
+
items: legendItems,
|
|
2714
|
+
position: input.legend?.position ?? "bottom",
|
|
2715
|
+
maxWidth: layout.width
|
|
2716
|
+
});
|
|
2717
|
+
if (legendLayout.rows.length > 0) {
|
|
2718
|
+
lines.push(anchorLine("", DEFAULT_ANCHOR));
|
|
2719
|
+
for (const row of legendLayout.rows) {
|
|
2720
|
+
lines.push(anchorLine(renderLegendRow(row, true), DEFAULT_ANCHOR));
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
return lines.join("\n");
|
|
2725
|
+
}
|
|
2726
|
+
function renderAreaChartMarkdown(layout, options) {
|
|
2727
|
+
const { input } = options;
|
|
2728
|
+
const lines = [];
|
|
2729
|
+
for (const row of layout.rows) {
|
|
2730
|
+
const yLabel = row.yLabel ? padToWidth3(row.yLabel, layout.yAxisWidth - 1) + " " : " ".repeat(layout.yAxisWidth);
|
|
2731
|
+
let content = "";
|
|
2732
|
+
for (let i = 0; i < row.chars.length; i++) {
|
|
2733
|
+
const char = row.chars[i];
|
|
2734
|
+
const useBackticks = row.useBackticks[i];
|
|
2735
|
+
if (char === void 0 || useBackticks === void 0) {
|
|
2736
|
+
throw new Error(`Missing char or useBackticks at index ${String(i)}`);
|
|
2737
|
+
}
|
|
2738
|
+
content += wrapInlineCode(char, useBackticks && char !== " ");
|
|
2739
|
+
}
|
|
2740
|
+
lines.push(
|
|
2741
|
+
anchorLine(`${yLabel}${AXIS_CHARS.vertical}${content}`, DEFAULT_ANCHOR)
|
|
2742
|
+
);
|
|
2743
|
+
}
|
|
2744
|
+
const xAxisLine = AXIS_CHARS.horizontal.repeat(layout.categories.length);
|
|
2745
|
+
lines.push(
|
|
2746
|
+
anchorLine(
|
|
2747
|
+
" ".repeat(layout.yAxisWidth) + AXIS_CHARS.origin + xAxisLine,
|
|
2748
|
+
DEFAULT_ANCHOR
|
|
2749
|
+
)
|
|
2750
|
+
);
|
|
2751
|
+
const xLabels = layout.categories.map((c) => c.charAt(0) || " ").join("");
|
|
2752
|
+
lines.push(
|
|
2753
|
+
anchorLine(" ".repeat(layout.yAxisWidth + 1) + xLabels, DEFAULT_ANCHOR)
|
|
2754
|
+
);
|
|
2755
|
+
const legendItems = layout.seriesNames.map((name, i) => {
|
|
2756
|
+
const style = layout.seriesStyles[i];
|
|
2757
|
+
if (!style) {
|
|
2758
|
+
throw new Error(`Missing style for series ${String(i)}`);
|
|
2759
|
+
}
|
|
2760
|
+
return {
|
|
2761
|
+
name,
|
|
2762
|
+
symbol: style.char,
|
|
2763
|
+
useBackticks: style.useBackticks
|
|
2764
|
+
};
|
|
2765
|
+
});
|
|
2766
|
+
const legendLayout = computeLegendLayout({
|
|
2767
|
+
items: legendItems,
|
|
2768
|
+
position: input.legend?.position ?? "bottom",
|
|
2769
|
+
maxWidth: layout.width
|
|
2770
|
+
});
|
|
2771
|
+
if (legendLayout.rows.length > 0) {
|
|
2772
|
+
lines.push(anchorLine("", DEFAULT_ANCHOR));
|
|
2773
|
+
for (const row of legendLayout.rows) {
|
|
2774
|
+
lines.push(anchorLine(renderLegendRow(row, true), DEFAULT_ANCHOR));
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
return lines.join("\n");
|
|
2778
|
+
}
|
|
2779
|
+
function renderScatterChartMarkdown(layout, options) {
|
|
2780
|
+
const { input } = options;
|
|
2781
|
+
const lines = [];
|
|
2782
|
+
const format = input.yAxis?.format ?? "number";
|
|
2783
|
+
const decimals = input.yAxis?.decimals;
|
|
2784
|
+
for (let rowIndex = 0; rowIndex < layout.chartHeight; rowIndex++) {
|
|
2785
|
+
let yLabel = " ".repeat(layout.yAxisWidth);
|
|
2786
|
+
const rowTop = 1 - rowIndex / layout.chartHeight;
|
|
2787
|
+
const rowBottom = 1 - (rowIndex + 1) / layout.chartHeight;
|
|
2788
|
+
for (const tick of layout.yScale.ticks) {
|
|
2789
|
+
const tickNorm = (tick - layout.yScale.min) / (layout.yScale.max - layout.yScale.min);
|
|
2790
|
+
if (tickNorm > rowBottom && tickNorm <= rowTop) {
|
|
2791
|
+
yLabel = padToWidth3(
|
|
2792
|
+
formatTickValue(tick, format, decimals),
|
|
2793
|
+
layout.yAxisWidth - 1
|
|
2794
|
+
) + " ";
|
|
2795
|
+
break;
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
let content = "";
|
|
2799
|
+
if (layout.scatterStyle === "braille" && layout.brailleChars) {
|
|
2800
|
+
const rowChars = layout.brailleChars[rowIndex] ?? [];
|
|
2801
|
+
const rowIndices = layout.brailleSeriesIndices?.[rowIndex] ?? [];
|
|
2802
|
+
for (let i = 0; i < rowChars.length; i++) {
|
|
2803
|
+
const char = rowChars[i];
|
|
2804
|
+
if (char === void 0) {
|
|
2805
|
+
throw new Error(`Missing char at index ${String(i)}`);
|
|
2806
|
+
}
|
|
2807
|
+
const seriesIdx = rowIndices[i];
|
|
2808
|
+
const useBackticks = seriesIdx !== null && seriesIdx !== void 0 && seriesIdx % 2 === 1;
|
|
2809
|
+
content += wrapInlineCode(char, useBackticks && char !== " ");
|
|
2810
|
+
}
|
|
2811
|
+
} else if (layout.grid) {
|
|
2812
|
+
const rowChars = layout.grid[rowIndex] ?? [];
|
|
2813
|
+
const rowIndices = layout.seriesIndices?.[rowIndex] ?? [];
|
|
2814
|
+
for (let i = 0; i < rowChars.length; i++) {
|
|
2815
|
+
const char = rowChars[i];
|
|
2816
|
+
if (char === void 0) {
|
|
2817
|
+
throw new Error(`Missing char at index ${String(i)}`);
|
|
2818
|
+
}
|
|
2819
|
+
const seriesIdx = rowIndices[i];
|
|
2820
|
+
const useBackticks = seriesIdx !== null && seriesIdx !== void 0 && seriesIdx % 2 === 1;
|
|
2821
|
+
content += wrapInlineCode(char, useBackticks && char !== " ");
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
lines.push(
|
|
2825
|
+
anchorLine(`${yLabel}${AXIS_CHARS.vertical}${content}`, DEFAULT_ANCHOR)
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
const xAxisLine = AXIS_CHARS.horizontal.repeat(layout.chartWidth);
|
|
2829
|
+
lines.push(
|
|
2830
|
+
anchorLine(
|
|
2831
|
+
" ".repeat(layout.yAxisWidth) + AXIS_CHARS.origin + xAxisLine,
|
|
2832
|
+
DEFAULT_ANCHOR
|
|
2833
|
+
)
|
|
2834
|
+
);
|
|
2835
|
+
const xFormat = input.xAxis?.format ?? "number";
|
|
2836
|
+
const xDecimals = input.xAxis?.decimals;
|
|
2837
|
+
const labelPositions = [];
|
|
2838
|
+
for (const tick of layout.xScale.ticks) {
|
|
2839
|
+
const norm = (tick - layout.xScale.min) / (layout.xScale.max - layout.xScale.min);
|
|
2840
|
+
const pos = Math.round(norm * (layout.chartWidth - 1));
|
|
2841
|
+
const label = formatTickValue(tick, xFormat, xDecimals);
|
|
2842
|
+
labelPositions.push({ pos, label });
|
|
2843
|
+
}
|
|
2844
|
+
const labelLine = Array(layout.chartWidth).fill(" ");
|
|
2845
|
+
for (const { pos, label } of labelPositions) {
|
|
2846
|
+
const halfLen = Math.floor(label.length / 2);
|
|
2847
|
+
const startPos = Math.max(
|
|
2848
|
+
0,
|
|
2849
|
+
Math.min(layout.chartWidth - label.length, pos - halfLen)
|
|
2850
|
+
);
|
|
2851
|
+
for (let i = 0; i < label.length && startPos + i < layout.chartWidth; i++) {
|
|
2852
|
+
const char = label[i];
|
|
2853
|
+
if (char === void 0) {
|
|
2854
|
+
throw new Error(`Missing char at index ${String(i)}`);
|
|
2855
|
+
}
|
|
2856
|
+
labelLine[startPos + i] = char;
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
lines.push(
|
|
2860
|
+
anchorLine(
|
|
2861
|
+
" ".repeat(layout.yAxisWidth + 1) + labelLine.join(""),
|
|
2862
|
+
DEFAULT_ANCHOR
|
|
2863
|
+
)
|
|
2864
|
+
);
|
|
2865
|
+
if (layout.seriesNames.length > 1) {
|
|
2866
|
+
const legendItems = layout.seriesNames.map((name, i) => {
|
|
2867
|
+
const symbols = ["\u25CF", "\u25A0", "\u25B2", "\u25C6", "+"];
|
|
2868
|
+
const symbol = layout.scatterStyle === "braille" ? "\u28FF" : symbols[i % 5];
|
|
2869
|
+
if (!symbol) {
|
|
2870
|
+
throw new Error(`Missing symbol for series ${String(i)}`);
|
|
2871
|
+
}
|
|
2872
|
+
return {
|
|
2873
|
+
name,
|
|
2874
|
+
symbol,
|
|
2875
|
+
useBackticks: i % 2 === 1
|
|
2876
|
+
};
|
|
2877
|
+
});
|
|
2878
|
+
const legendLayout = computeLegendLayout({
|
|
2879
|
+
items: legendItems,
|
|
2880
|
+
position: input.legend?.position ?? "bottom",
|
|
2881
|
+
maxWidth: layout.width
|
|
2882
|
+
});
|
|
2883
|
+
if (legendLayout.rows.length > 0) {
|
|
2884
|
+
lines.push(anchorLine("", DEFAULT_ANCHOR));
|
|
2885
|
+
for (const row of legendLayout.rows) {
|
|
2886
|
+
lines.push(anchorLine(renderLegendRow(row, true), DEFAULT_ANCHOR));
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
return lines.join("\n");
|
|
2891
|
+
}
|
|
2892
|
+
function renderPieChartMarkdown(layout, options) {
|
|
2893
|
+
const { input } = options;
|
|
2894
|
+
const lines = [];
|
|
2895
|
+
if (layout.slices.length === 0) {
|
|
2896
|
+
lines.push(anchorLine("No data", DEFAULT_ANCHOR));
|
|
2897
|
+
return lines.join("\n");
|
|
2898
|
+
}
|
|
2899
|
+
for (let rowIndex = 0; rowIndex < layout.brailleChars.length; rowIndex++) {
|
|
2900
|
+
const rowChars = layout.brailleChars[rowIndex] ?? [];
|
|
2901
|
+
const rowIndices = layout.brailleSeriesIndices[rowIndex] ?? [];
|
|
2902
|
+
let content = "";
|
|
2903
|
+
for (let i = 0; i < rowChars.length; i++) {
|
|
2904
|
+
const char = rowChars[i];
|
|
2905
|
+
if (char === void 0) {
|
|
2906
|
+
throw new Error(`Missing char at index ${String(i)}`);
|
|
2907
|
+
}
|
|
2908
|
+
const seriesIdx = rowIndices[i];
|
|
2909
|
+
const useBackticks = seriesIdx !== null && seriesIdx !== void 0 && seriesIdx % 2 === 1;
|
|
2910
|
+
content += wrapInlineCode(char, useBackticks && char !== " ");
|
|
2911
|
+
}
|
|
2912
|
+
if (layout.type === "donut" && layout.centerLabel && rowIndex === Math.floor(layout.brailleChars.length / 2)) {
|
|
2913
|
+
const labelLen = layout.centerLabel.length;
|
|
2914
|
+
const startPos = Math.floor((layout.width - labelLen) / 2);
|
|
2915
|
+
if (startPos >= 0) {
|
|
2916
|
+
const contentArray = [...content];
|
|
2917
|
+
for (let i = 0; i < labelLen && startPos + i < contentArray.length; i++) {
|
|
2918
|
+
const char = layout.centerLabel[i];
|
|
2919
|
+
if (char === void 0) {
|
|
2920
|
+
throw new Error(`Missing centerLabel char at index ${String(i)}`);
|
|
2921
|
+
}
|
|
2922
|
+
contentArray[startPos + i] = char;
|
|
2923
|
+
}
|
|
2924
|
+
content = contentArray.join("");
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
lines.push(anchorLine(content, DEFAULT_ANCHOR));
|
|
2928
|
+
}
|
|
2929
|
+
const legendItems = layout.slices.map((slice) => ({
|
|
2930
|
+
name: `${slice.label} ${slice.percentage.toFixed(0)}%`,
|
|
2931
|
+
symbol: slice.barChar,
|
|
2932
|
+
useBackticks: slice.useBackticks
|
|
2933
|
+
}));
|
|
2934
|
+
const legendLayout = computeLegendLayout({
|
|
2935
|
+
items: legendItems,
|
|
2936
|
+
position: input.legend?.position ?? "bottom",
|
|
2937
|
+
maxWidth: layout.width
|
|
2938
|
+
});
|
|
2939
|
+
if (legendLayout.rows.length > 0) {
|
|
2940
|
+
lines.push(anchorLine("", DEFAULT_ANCHOR));
|
|
2941
|
+
for (const row of legendLayout.rows) {
|
|
2942
|
+
lines.push(anchorLine(renderLegendRow(row, true), DEFAULT_ANCHOR));
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
return lines.join("\n");
|
|
2946
|
+
}
|
|
2947
|
+
function renderHeatmapMarkdown(layout, options) {
|
|
2948
|
+
const { input: _input } = options;
|
|
2949
|
+
const lines = [];
|
|
2950
|
+
if (layout.cells.length === 0 || (layout.cells[0]?.length ?? 0) === 0) {
|
|
2951
|
+
lines.push(anchorLine("No data", DEFAULT_ANCHOR));
|
|
2952
|
+
return lines.join("\n");
|
|
2953
|
+
}
|
|
2954
|
+
const colHeaderPadding = " ".repeat(layout.rowLabelWidth);
|
|
2955
|
+
const colHeaders = layout.colLabels.map((label) => padToWidth3(label, layout.colLabelWidth)).join("");
|
|
2956
|
+
lines.push(anchorLine(`${colHeaderPadding}${colHeaders}`, DEFAULT_ANCHOR));
|
|
2957
|
+
for (let rowIdx = 0; rowIdx < layout.rowLabels.length; rowIdx++) {
|
|
2958
|
+
const rowLabelText = layout.rowLabels[rowIdx];
|
|
2959
|
+
if (!rowLabelText) continue;
|
|
2960
|
+
const rowLabel = padToWidth3(rowLabelText, layout.rowLabelWidth);
|
|
2961
|
+
const rowCells = layout.cells[rowIdx];
|
|
2962
|
+
if (!rowCells) continue;
|
|
2963
|
+
let rowContent = "";
|
|
2964
|
+
for (const cell of rowCells) {
|
|
2965
|
+
if (layout.heatmapStyle === "numeric") {
|
|
2966
|
+
rowContent += padToWidth3(cell.displayChar, layout.colLabelWidth);
|
|
2967
|
+
} else {
|
|
2968
|
+
const useBackticks = cell.normalizedValue > 0.5;
|
|
2969
|
+
const paddedChar = padToWidth3(cell.displayChar, layout.colLabelWidth);
|
|
2970
|
+
const styled = wrapInlineCode(paddedChar, useBackticks);
|
|
2971
|
+
rowContent += styled;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
lines.push(anchorLine(`${rowLabel}${rowContent}`, DEFAULT_ANCHOR));
|
|
2975
|
+
}
|
|
2976
|
+
if (layout.heatmapStyle !== "numeric") {
|
|
2977
|
+
lines.push(anchorLine("", DEFAULT_ANCHOR));
|
|
2978
|
+
const scaleChars = layout.heatmapStyle === "blocks" ? ["\u2591", "\u2592", "\u2593", "\u2588"] : [".", ":", "*", "#"];
|
|
2979
|
+
const scaleLabels = ["Low", "", "", "High"];
|
|
2980
|
+
let scaleLine = "Scale: ";
|
|
2981
|
+
for (let i = 0; i < scaleChars.length; i++) {
|
|
2982
|
+
const char = scaleChars[i] ?? "";
|
|
2983
|
+
const label = scaleLabels[i] ?? "";
|
|
2984
|
+
scaleLine += `${char} ${label}`;
|
|
2985
|
+
if (i < scaleChars.length - 1) scaleLine += " ";
|
|
2986
|
+
}
|
|
2987
|
+
lines.push(anchorLine(scaleLine, DEFAULT_ANCHOR));
|
|
2988
|
+
}
|
|
2989
|
+
return lines.join("\n");
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
// src/chart.ts
|
|
2993
|
+
var ChartComponent = class extends BaseTuiComponent {
|
|
2994
|
+
metadata = {
|
|
2995
|
+
name: "chart",
|
|
2996
|
+
description: "Renders various chart types including bar, line, and area charts",
|
|
2997
|
+
version: "0.1.0",
|
|
2998
|
+
supportedModes: ["ansi", "markdown"],
|
|
2999
|
+
examples: [
|
|
3000
|
+
{
|
|
3001
|
+
name: "horizontal-bar",
|
|
3002
|
+
description: "Simple horizontal bar chart",
|
|
3003
|
+
input: {
|
|
3004
|
+
type: "bar",
|
|
3005
|
+
series: [
|
|
3006
|
+
{
|
|
3007
|
+
name: "Sales",
|
|
3008
|
+
data: [
|
|
3009
|
+
{ x: "Q1", y: 120 },
|
|
3010
|
+
{ x: "Q2", y: 150 },
|
|
3011
|
+
{ x: "Q3", y: 180 },
|
|
3012
|
+
{ x: "Q4", y: 200 }
|
|
3013
|
+
]
|
|
3014
|
+
}
|
|
3015
|
+
],
|
|
3016
|
+
showValues: true
|
|
3017
|
+
}
|
|
3018
|
+
},
|
|
3019
|
+
{
|
|
3020
|
+
name: "vertical-bar",
|
|
3021
|
+
description: "Vertical bar chart (column chart)",
|
|
3022
|
+
input: {
|
|
3023
|
+
type: "bar-vertical",
|
|
3024
|
+
series: [
|
|
3025
|
+
{
|
|
3026
|
+
name: "Revenue",
|
|
3027
|
+
data: [
|
|
3028
|
+
{ x: "Jan", y: 65 },
|
|
3029
|
+
{ x: "Feb", y: 80 },
|
|
3030
|
+
{ x: "Mar", y: 95 },
|
|
3031
|
+
{ x: "Apr", y: 70 }
|
|
3032
|
+
]
|
|
3033
|
+
}
|
|
3034
|
+
],
|
|
3035
|
+
height: 8,
|
|
3036
|
+
width: 30
|
|
3037
|
+
}
|
|
3038
|
+
},
|
|
3039
|
+
{
|
|
3040
|
+
name: "stacked-bar",
|
|
3041
|
+
description: "Stacked horizontal bar chart",
|
|
3042
|
+
input: {
|
|
3043
|
+
type: "bar-stacked",
|
|
3044
|
+
series: [
|
|
3045
|
+
{
|
|
3046
|
+
name: "Product A",
|
|
3047
|
+
data: [
|
|
3048
|
+
{ x: "Q1", y: 50 },
|
|
3049
|
+
{ x: "Q2", y: 60 },
|
|
3050
|
+
{ x: "Q3", y: 70 }
|
|
3051
|
+
]
|
|
3052
|
+
},
|
|
3053
|
+
{
|
|
3054
|
+
name: "Product B",
|
|
3055
|
+
data: [
|
|
3056
|
+
{ x: "Q1", y: 30 },
|
|
3057
|
+
{ x: "Q2", y: 40 },
|
|
3058
|
+
{ x: "Q3", y: 35 }
|
|
3059
|
+
]
|
|
3060
|
+
}
|
|
3061
|
+
],
|
|
3062
|
+
width: 40
|
|
3063
|
+
}
|
|
3064
|
+
},
|
|
3065
|
+
{
|
|
3066
|
+
name: "stacked-vertical",
|
|
3067
|
+
description: "Stacked vertical bar chart",
|
|
3068
|
+
input: {
|
|
3069
|
+
type: "bar-stacked-vertical",
|
|
3070
|
+
series: [
|
|
3071
|
+
{
|
|
3072
|
+
name: "Revenue",
|
|
3073
|
+
data: [
|
|
3074
|
+
{ x: "Q1", y: 100 },
|
|
3075
|
+
{ x: "Q2", y: 120 },
|
|
3076
|
+
{ x: "Q3", y: 90 },
|
|
3077
|
+
{ x: "Q4", y: 150 }
|
|
3078
|
+
]
|
|
3079
|
+
},
|
|
3080
|
+
{
|
|
3081
|
+
name: "Costs",
|
|
3082
|
+
data: [
|
|
3083
|
+
{ x: "Q1", y: 60 },
|
|
3084
|
+
{ x: "Q2", y: 70 },
|
|
3085
|
+
{ x: "Q3", y: 55 },
|
|
3086
|
+
{ x: "Q4", y: 80 }
|
|
3087
|
+
]
|
|
3088
|
+
},
|
|
3089
|
+
{
|
|
3090
|
+
name: "Profit",
|
|
3091
|
+
data: [
|
|
3092
|
+
{ x: "Q1", y: 40 },
|
|
3093
|
+
{ x: "Q2", y: 50 },
|
|
3094
|
+
{ x: "Q3", y: 35 },
|
|
3095
|
+
{ x: "Q4", y: 70 }
|
|
3096
|
+
]
|
|
3097
|
+
}
|
|
3098
|
+
],
|
|
3099
|
+
height: 10,
|
|
3100
|
+
width: 35
|
|
3101
|
+
}
|
|
3102
|
+
},
|
|
3103
|
+
{
|
|
3104
|
+
name: "line",
|
|
3105
|
+
description: "Line chart with height blocks",
|
|
3106
|
+
input: {
|
|
3107
|
+
type: "line",
|
|
3108
|
+
series: [
|
|
3109
|
+
{
|
|
3110
|
+
name: "Temperature",
|
|
3111
|
+
data: [
|
|
3112
|
+
{ x: "J", y: 30 },
|
|
3113
|
+
{ x: "F", y: 35 },
|
|
3114
|
+
{ x: "M", y: 50 },
|
|
3115
|
+
{ x: "A", y: 65 },
|
|
3116
|
+
{ x: "M", y: 75 },
|
|
3117
|
+
{ x: "J", y: 85 },
|
|
3118
|
+
{ x: "J", y: 90 },
|
|
3119
|
+
{ x: "A", y: 88 },
|
|
3120
|
+
{ x: "S", y: 78 },
|
|
3121
|
+
{ x: "O", y: 62 },
|
|
3122
|
+
{ x: "N", y: 45 },
|
|
3123
|
+
{ x: "D", y: 32 }
|
|
3124
|
+
]
|
|
3125
|
+
}
|
|
3126
|
+
],
|
|
3127
|
+
height: 8,
|
|
3128
|
+
width: 20
|
|
3129
|
+
}
|
|
3130
|
+
},
|
|
3131
|
+
{
|
|
3132
|
+
name: "multi-line",
|
|
3133
|
+
description: "Multiple series line chart",
|
|
3134
|
+
input: {
|
|
3135
|
+
type: "line",
|
|
3136
|
+
series: [
|
|
3137
|
+
{
|
|
3138
|
+
name: "2023",
|
|
3139
|
+
data: [
|
|
3140
|
+
{ x: "Q1", y: 100 },
|
|
3141
|
+
{ x: "Q2", y: 120 },
|
|
3142
|
+
{ x: "Q3", y: 110 },
|
|
3143
|
+
{ x: "Q4", y: 140 }
|
|
3144
|
+
]
|
|
3145
|
+
},
|
|
3146
|
+
{
|
|
3147
|
+
name: "2024",
|
|
3148
|
+
data: [
|
|
3149
|
+
{ x: "Q1", y: 130 },
|
|
3150
|
+
{ x: "Q2", y: 145 },
|
|
3151
|
+
{ x: "Q3", y: 135 },
|
|
3152
|
+
{ x: "Q4", y: 160 }
|
|
3153
|
+
]
|
|
3154
|
+
}
|
|
3155
|
+
],
|
|
3156
|
+
height: 6,
|
|
3157
|
+
width: 20
|
|
3158
|
+
}
|
|
3159
|
+
},
|
|
3160
|
+
{
|
|
3161
|
+
name: "area",
|
|
3162
|
+
description: "Area chart",
|
|
3163
|
+
input: {
|
|
3164
|
+
type: "area",
|
|
3165
|
+
series: [
|
|
3166
|
+
{
|
|
3167
|
+
name: "Users",
|
|
3168
|
+
data: [
|
|
3169
|
+
{ x: "W1", y: 100 },
|
|
3170
|
+
{ x: "W2", y: 150 },
|
|
3171
|
+
{ x: "W3", y: 180 },
|
|
3172
|
+
{ x: "W4", y: 160 },
|
|
3173
|
+
{ x: "W5", y: 200 }
|
|
3174
|
+
]
|
|
3175
|
+
}
|
|
3176
|
+
],
|
|
3177
|
+
height: 6,
|
|
3178
|
+
width: 15
|
|
3179
|
+
}
|
|
3180
|
+
},
|
|
3181
|
+
{
|
|
3182
|
+
name: "stacked-area",
|
|
3183
|
+
description: "Stacked area chart",
|
|
3184
|
+
input: {
|
|
3185
|
+
type: "area-stacked",
|
|
3186
|
+
series: [
|
|
3187
|
+
{
|
|
3188
|
+
name: "Mobile",
|
|
3189
|
+
data: [
|
|
3190
|
+
{ x: "J", y: 50 },
|
|
3191
|
+
{ x: "F", y: 60 },
|
|
3192
|
+
{ x: "M", y: 70 },
|
|
3193
|
+
{ x: "A", y: 65 }
|
|
3194
|
+
]
|
|
3195
|
+
},
|
|
3196
|
+
{
|
|
3197
|
+
name: "Desktop",
|
|
3198
|
+
data: [
|
|
3199
|
+
{ x: "J", y: 100 },
|
|
3200
|
+
{ x: "F", y: 90 },
|
|
3201
|
+
{ x: "M", y: 85 },
|
|
3202
|
+
{ x: "A", y: 95 }
|
|
3203
|
+
]
|
|
3204
|
+
}
|
|
3205
|
+
],
|
|
3206
|
+
height: 8,
|
|
3207
|
+
width: 15
|
|
3208
|
+
}
|
|
3209
|
+
},
|
|
3210
|
+
{
|
|
3211
|
+
name: "scatter",
|
|
3212
|
+
description: "Scatter plot with numeric axes",
|
|
3213
|
+
input: {
|
|
3214
|
+
type: "scatter",
|
|
3215
|
+
series: [
|
|
3216
|
+
{
|
|
3217
|
+
name: "Data",
|
|
3218
|
+
data: [
|
|
3219
|
+
{ x: 10, y: 20 },
|
|
3220
|
+
{ x: 30, y: 50 },
|
|
3221
|
+
{ x: 50, y: 30 },
|
|
3222
|
+
{ x: 70, y: 80 },
|
|
3223
|
+
{ x: 90, y: 60 }
|
|
3224
|
+
]
|
|
3225
|
+
}
|
|
3226
|
+
],
|
|
3227
|
+
height: 8,
|
|
3228
|
+
width: 30
|
|
3229
|
+
}
|
|
3230
|
+
},
|
|
3231
|
+
{
|
|
3232
|
+
name: "pie",
|
|
3233
|
+
description: "Pie chart with percentage breakdown",
|
|
3234
|
+
input: {
|
|
3235
|
+
type: "pie",
|
|
3236
|
+
series: [
|
|
3237
|
+
{
|
|
3238
|
+
name: "Revenue",
|
|
3239
|
+
data: [
|
|
3240
|
+
{ label: "Sales", x: "Sales", y: 45 },
|
|
3241
|
+
{ label: "Support", x: "Support", y: 30 },
|
|
3242
|
+
{ label: "Other", x: "Other", y: 25 }
|
|
3243
|
+
]
|
|
3244
|
+
}
|
|
3245
|
+
],
|
|
3246
|
+
height: 10,
|
|
3247
|
+
width: 30
|
|
3248
|
+
}
|
|
3249
|
+
},
|
|
3250
|
+
{
|
|
3251
|
+
name: "donut",
|
|
3252
|
+
description: "Donut chart with center label",
|
|
3253
|
+
input: {
|
|
3254
|
+
type: "donut",
|
|
3255
|
+
series: [
|
|
3256
|
+
{
|
|
3257
|
+
name: "Market Share",
|
|
3258
|
+
data: [
|
|
3259
|
+
{ label: "Chrome", x: "Chrome", y: 65 },
|
|
3260
|
+
{ label: "Firefox", x: "Firefox", y: 20 },
|
|
3261
|
+
{ label: "Safari", x: "Safari", y: 15 }
|
|
3262
|
+
]
|
|
3263
|
+
}
|
|
3264
|
+
],
|
|
3265
|
+
height: 10,
|
|
3266
|
+
width: 30,
|
|
3267
|
+
centerLabel: "100%",
|
|
3268
|
+
innerRadius: 0.5
|
|
3269
|
+
}
|
|
3270
|
+
},
|
|
3271
|
+
{
|
|
3272
|
+
name: "heatmap",
|
|
3273
|
+
description: "Heatmap showing intensity values",
|
|
3274
|
+
input: {
|
|
3275
|
+
type: "heatmap",
|
|
3276
|
+
series: [
|
|
3277
|
+
{
|
|
3278
|
+
name: "Activity",
|
|
3279
|
+
data: [
|
|
3280
|
+
{ x: "Mon", y: 10, label: "9am" },
|
|
3281
|
+
{ x: "Tue", y: 50, label: "9am" },
|
|
3282
|
+
{ x: "Wed", y: 80, label: "9am" },
|
|
3283
|
+
{ x: "Mon", y: 30, label: "10am" },
|
|
3284
|
+
{ x: "Tue", y: 70, label: "10am" },
|
|
3285
|
+
{ x: "Wed", y: 90, label: "10am" }
|
|
3286
|
+
]
|
|
3287
|
+
}
|
|
3288
|
+
],
|
|
3289
|
+
height: 6,
|
|
3290
|
+
width: 25,
|
|
3291
|
+
heatmapStyle: "blocks"
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
]
|
|
3295
|
+
};
|
|
3296
|
+
schema = chartInputSchema;
|
|
3297
|
+
getJsonSchema() {
|
|
3298
|
+
return zodToJsonSchema(this.schema, {
|
|
3299
|
+
name: this.metadata.name,
|
|
3300
|
+
$refStrategy: "none"
|
|
3301
|
+
});
|
|
3302
|
+
}
|
|
3303
|
+
render(input, context) {
|
|
3304
|
+
const parsed = this.schema.parse(input);
|
|
3305
|
+
if (parsed.series.length === 0 || parsed.series.every((s) => s.data.length === 0)) {
|
|
3306
|
+
return { output: "", actualWidth: 0, lineCount: 0 };
|
|
3307
|
+
}
|
|
3308
|
+
let output;
|
|
3309
|
+
switch (parsed.type) {
|
|
3310
|
+
case "bar": {
|
|
3311
|
+
const layout = computeBarLayout(parsed);
|
|
3312
|
+
output = context.renderMode === "markdown" ? renderBarChartMarkdown(layout, { input: parsed }) : renderBarChartAnsi(layout, {
|
|
3313
|
+
theme: context.theme,
|
|
3314
|
+
input: parsed
|
|
3315
|
+
});
|
|
3316
|
+
break;
|
|
3317
|
+
}
|
|
3318
|
+
case "bar-vertical": {
|
|
3319
|
+
const layout = computeBarLayout(parsed);
|
|
3320
|
+
output = context.renderMode === "markdown" ? renderVerticalBarChartMarkdown(layout, { input: parsed }) : renderVerticalBarChartAnsi(layout, {
|
|
3321
|
+
theme: context.theme,
|
|
3322
|
+
input: parsed
|
|
3323
|
+
});
|
|
3324
|
+
break;
|
|
3325
|
+
}
|
|
3326
|
+
case "bar-stacked":
|
|
3327
|
+
case "bar-stacked-vertical": {
|
|
3328
|
+
const layout = computeStackedBarLayout(parsed);
|
|
3329
|
+
output = context.renderMode === "markdown" ? renderStackedBarChartMarkdown(layout, { input: parsed }) : renderStackedBarChartAnsi(layout, {
|
|
3330
|
+
theme: context.theme,
|
|
3331
|
+
input: parsed
|
|
3332
|
+
});
|
|
3333
|
+
break;
|
|
3334
|
+
}
|
|
3335
|
+
case "line": {
|
|
3336
|
+
const layout = computeLineLayout(parsed);
|
|
3337
|
+
output = context.renderMode === "markdown" ? renderLineChartMarkdown(layout, { input: parsed }) : renderLineChartAnsi(layout, {
|
|
3338
|
+
theme: context.theme,
|
|
3339
|
+
input: parsed
|
|
3340
|
+
});
|
|
3341
|
+
break;
|
|
3342
|
+
}
|
|
3343
|
+
case "area":
|
|
3344
|
+
case "area-stacked": {
|
|
3345
|
+
const layout = computeAreaLayout(parsed);
|
|
3346
|
+
output = context.renderMode === "markdown" ? renderAreaChartMarkdown(layout, { input: parsed }) : renderAreaChartAnsi(layout, {
|
|
3347
|
+
theme: context.theme,
|
|
3348
|
+
input: parsed
|
|
3349
|
+
});
|
|
3350
|
+
break;
|
|
3351
|
+
}
|
|
3352
|
+
case "scatter": {
|
|
3353
|
+
const layout = computeScatterLayout(parsed);
|
|
3354
|
+
output = context.renderMode === "markdown" ? renderScatterChartMarkdown(layout, { input: parsed }) : renderScatterChartAnsi(layout, {
|
|
3355
|
+
theme: context.theme,
|
|
3356
|
+
input: parsed
|
|
3357
|
+
});
|
|
3358
|
+
break;
|
|
3359
|
+
}
|
|
3360
|
+
case "pie":
|
|
3361
|
+
case "donut": {
|
|
3362
|
+
const layout = computePieLayout(parsed);
|
|
3363
|
+
output = context.renderMode === "markdown" ? renderPieChartMarkdown(layout, { input: parsed }) : renderPieChartAnsi(layout, {
|
|
3364
|
+
theme: context.theme,
|
|
3365
|
+
input: parsed
|
|
3366
|
+
});
|
|
3367
|
+
break;
|
|
3368
|
+
}
|
|
3369
|
+
case "heatmap": {
|
|
3370
|
+
const layout = computeHeatmapLayout(parsed);
|
|
3371
|
+
output = context.renderMode === "markdown" ? renderHeatmapMarkdown(layout, { input: parsed }) : renderHeatmapAnsi(layout, {
|
|
3372
|
+
theme: context.theme,
|
|
3373
|
+
input: parsed
|
|
3374
|
+
});
|
|
3375
|
+
break;
|
|
3376
|
+
}
|
|
3377
|
+
default: {
|
|
3378
|
+
const _exhaustiveCheck = parsed.type;
|
|
3379
|
+
throw new Error(`Unknown chart type: ${String(_exhaustiveCheck)}`);
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
if (parsed.title) {
|
|
3383
|
+
const titleLine = context.theme ? context.theme.semantic.header(parsed.title) : parsed.title;
|
|
3384
|
+
output = titleLine + "\n" + output;
|
|
3385
|
+
}
|
|
3386
|
+
const measured = measureLines(output);
|
|
3387
|
+
return {
|
|
3388
|
+
output,
|
|
3389
|
+
actualWidth: measured.maxWidth,
|
|
3390
|
+
lineCount: measured.lineCount
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
};
|
|
3394
|
+
function createChart() {
|
|
3395
|
+
return new ChartComponent();
|
|
3396
|
+
}
|
|
3397
|
+
registry.register(createChart);
|
|
3398
|
+
|
|
3399
|
+
// src/core/axis.ts
|
|
3400
|
+
import { getStringWidth as getStringWidth8 } from "@tuicomponents/core";
|
|
3401
|
+
function computeAxisLayout(options) {
|
|
3402
|
+
const {
|
|
3403
|
+
dataMin,
|
|
3404
|
+
dataMax,
|
|
3405
|
+
size,
|
|
3406
|
+
orientation,
|
|
3407
|
+
position,
|
|
3408
|
+
tickCount = 5,
|
|
3409
|
+
showTicks = true,
|
|
3410
|
+
format = "number",
|
|
3411
|
+
decimals,
|
|
3412
|
+
label,
|
|
3413
|
+
forceMin,
|
|
3414
|
+
forceMax,
|
|
3415
|
+
includeZero = true
|
|
3416
|
+
} = options;
|
|
3417
|
+
const scale = computeNiceTicks({
|
|
3418
|
+
dataMin,
|
|
3419
|
+
dataMax,
|
|
3420
|
+
tickCount,
|
|
3421
|
+
includeZero,
|
|
3422
|
+
forceMin,
|
|
3423
|
+
forceMax
|
|
3424
|
+
});
|
|
3425
|
+
const ticks = scale.ticks.map((value) => {
|
|
3426
|
+
const label2 = formatTickValue(value, format, decimals);
|
|
3427
|
+
const normalizedPosition = (value - scale.min) / (scale.max - scale.min);
|
|
3428
|
+
const position2 = orientation === "vertical" ? size * (1 - normalizedPosition) : size * normalizedPosition;
|
|
3429
|
+
return { value, label: label2, position: position2 };
|
|
3430
|
+
});
|
|
3431
|
+
let maxLabelWidth = 0;
|
|
3432
|
+
for (const tick of ticks) {
|
|
3433
|
+
const width = getStringWidth8(tick.label);
|
|
3434
|
+
if (width > maxLabelWidth) {
|
|
3435
|
+
maxLabelWidth = width;
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
let totalWidth;
|
|
3439
|
+
let totalHeight;
|
|
3440
|
+
if (orientation === "vertical") {
|
|
3441
|
+
totalWidth = maxLabelWidth + (showTicks ? 2 : 1);
|
|
3442
|
+
totalHeight = size;
|
|
3443
|
+
} else {
|
|
3444
|
+
totalWidth = size;
|
|
3445
|
+
totalHeight = 1 + (label ? 1 : 0);
|
|
3446
|
+
}
|
|
3447
|
+
return {
|
|
3448
|
+
orientation,
|
|
3449
|
+
position,
|
|
3450
|
+
scale,
|
|
3451
|
+
ticks,
|
|
3452
|
+
maxLabelWidth,
|
|
3453
|
+
title: label,
|
|
3454
|
+
totalWidth,
|
|
3455
|
+
totalHeight
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3458
|
+
function computeDualAxisLayout(options) {
|
|
3459
|
+
const {
|
|
3460
|
+
xMin,
|
|
3461
|
+
xMax,
|
|
3462
|
+
yMin,
|
|
3463
|
+
yMax,
|
|
3464
|
+
chartWidth,
|
|
3465
|
+
chartHeight,
|
|
3466
|
+
xAxis: xAxisOpts = {},
|
|
3467
|
+
yAxis: yAxisOpts = {}
|
|
3468
|
+
} = options;
|
|
3469
|
+
const yAxisInitial = computeAxisLayout({
|
|
3470
|
+
dataMin: yMin,
|
|
3471
|
+
dataMax: yMax,
|
|
3472
|
+
size: chartHeight,
|
|
3473
|
+
orientation: "vertical",
|
|
3474
|
+
position: "left",
|
|
3475
|
+
...yAxisOpts
|
|
3476
|
+
});
|
|
3477
|
+
const chartAreaX = yAxisInitial.totalWidth;
|
|
3478
|
+
const chartAreaY = 0;
|
|
3479
|
+
const chartAreaWidth = Math.max(1, chartWidth - yAxisInitial.totalWidth);
|
|
3480
|
+
const chartAreaHeight = Math.max(1, chartHeight - 2);
|
|
3481
|
+
const yAxis = computeAxisLayout({
|
|
3482
|
+
dataMin: yMin,
|
|
3483
|
+
dataMax: yMax,
|
|
3484
|
+
size: chartAreaHeight,
|
|
3485
|
+
orientation: "vertical",
|
|
3486
|
+
position: "left",
|
|
3487
|
+
...yAxisOpts
|
|
3488
|
+
});
|
|
3489
|
+
const xAxis = computeAxisLayout({
|
|
3490
|
+
dataMin: xMin,
|
|
3491
|
+
dataMax: xMax,
|
|
3492
|
+
size: chartAreaWidth,
|
|
3493
|
+
orientation: "horizontal",
|
|
3494
|
+
position: "bottom",
|
|
3495
|
+
...xAxisOpts
|
|
3496
|
+
});
|
|
3497
|
+
return {
|
|
3498
|
+
xAxis,
|
|
3499
|
+
yAxis,
|
|
3500
|
+
chartAreaWidth,
|
|
3501
|
+
chartAreaHeight,
|
|
3502
|
+
chartAreaX,
|
|
3503
|
+
chartAreaY
|
|
3504
|
+
};
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
// src/core/grid.ts
|
|
3508
|
+
var DEFAULT_GRID_CHAR = "\xB7";
|
|
3509
|
+
function computeGridLayout(options) {
|
|
3510
|
+
const {
|
|
3511
|
+
width,
|
|
3512
|
+
height,
|
|
3513
|
+
xAxis,
|
|
3514
|
+
yAxis,
|
|
3515
|
+
showHorizontal = true,
|
|
3516
|
+
showVertical = false,
|
|
3517
|
+
char = DEFAULT_GRID_CHAR
|
|
3518
|
+
} = options;
|
|
3519
|
+
const horizontalLines = [];
|
|
3520
|
+
const verticalLines = [];
|
|
3521
|
+
if (showHorizontal && yAxis) {
|
|
3522
|
+
for (const tick of yAxis.ticks) {
|
|
3523
|
+
if (tick.position === 0 || tick.position === height) continue;
|
|
3524
|
+
horizontalLines.push({
|
|
3525
|
+
orientation: "horizontal",
|
|
3526
|
+
position: Math.round(tick.position),
|
|
3527
|
+
start: 0,
|
|
3528
|
+
end: width
|
|
3529
|
+
});
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
if (showVertical && xAxis) {
|
|
3533
|
+
for (const tick of xAxis.ticks) {
|
|
3534
|
+
if (tick.position === 0 || tick.position === width) continue;
|
|
3535
|
+
verticalLines.push({
|
|
3536
|
+
orientation: "vertical",
|
|
3537
|
+
position: Math.round(tick.position),
|
|
3538
|
+
start: 0,
|
|
3539
|
+
end: height
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
return {
|
|
3544
|
+
horizontalLines,
|
|
3545
|
+
verticalLines,
|
|
3546
|
+
char
|
|
3547
|
+
};
|
|
3548
|
+
}
|
|
3549
|
+
function createGridBuffer(width, height, grid) {
|
|
3550
|
+
const buffer = [];
|
|
3551
|
+
for (let y = 0; y < height; y++) {
|
|
3552
|
+
const row = [];
|
|
3553
|
+
for (let x = 0; x < width; x++) {
|
|
3554
|
+
row.push(" ");
|
|
3555
|
+
}
|
|
3556
|
+
buffer.push(row);
|
|
3557
|
+
}
|
|
3558
|
+
for (const line of grid.horizontalLines) {
|
|
3559
|
+
const y = line.position;
|
|
3560
|
+
if (y >= 0 && y < height) {
|
|
3561
|
+
const row = buffer[y];
|
|
3562
|
+
if (row) {
|
|
3563
|
+
for (let x = line.start; x < line.end && x < width; x++) {
|
|
3564
|
+
row[x] = grid.char;
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
for (const line of grid.verticalLines) {
|
|
3570
|
+
const x = line.position;
|
|
3571
|
+
if (x >= 0 && x < width) {
|
|
3572
|
+
for (let y = line.start; y < line.end && y < height; y++) {
|
|
3573
|
+
const row = buffer[y];
|
|
3574
|
+
if (row) {
|
|
3575
|
+
row[x] = grid.char;
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
return buffer;
|
|
3581
|
+
}
|
|
3582
|
+
export {
|
|
3583
|
+
AXIS_CHARS,
|
|
3584
|
+
BAR_CHARS,
|
|
3585
|
+
BRAILLE_BASE,
|
|
3586
|
+
BRAILLE_DOTS,
|
|
3587
|
+
BrailleCanvas,
|
|
3588
|
+
ChartComponent,
|
|
3589
|
+
DEFAULT_GRID_CHAR,
|
|
3590
|
+
HEATMAP_ASCII,
|
|
3591
|
+
HEATMAP_BLOCKS,
|
|
3592
|
+
HEIGHT_BLOCKS,
|
|
3593
|
+
LINE_CHARS,
|
|
3594
|
+
SCATTER_MARKERS,
|
|
3595
|
+
SCATTER_MARKER_SEQUENCE,
|
|
3596
|
+
SERIES_STYLES,
|
|
3597
|
+
axisConfigSchema,
|
|
3598
|
+
barStyleSchema,
|
|
3599
|
+
buildChartRow,
|
|
3600
|
+
chartInputSchema,
|
|
3601
|
+
chartTypeSchema,
|
|
3602
|
+
computeAreaLayout,
|
|
3603
|
+
computeAxisLayout,
|
|
3604
|
+
computeBarLayout,
|
|
3605
|
+
computeDualAxisLayout,
|
|
3606
|
+
computeGridLayout,
|
|
3607
|
+
computeHeatmapLayout,
|
|
3608
|
+
computeLegendLayout,
|
|
3609
|
+
computeLineLayout,
|
|
3610
|
+
computeNiceTicks,
|
|
3611
|
+
computePieLayout,
|
|
3612
|
+
computeScatterLayout,
|
|
3613
|
+
computeStackedBarLayout,
|
|
3614
|
+
computeYAxisRow,
|
|
3615
|
+
createChart,
|
|
3616
|
+
createGridBuffer,
|
|
3617
|
+
dataPointSchema,
|
|
3618
|
+
dataSeriesSchema,
|
|
3619
|
+
formatLegendItem,
|
|
3620
|
+
formatTickValue,
|
|
3621
|
+
getBarChar,
|
|
3622
|
+
getLegendItemWidth,
|
|
3623
|
+
gridConfigSchema,
|
|
3624
|
+
groupBarsByCategory,
|
|
3625
|
+
heatmapStyleSchema,
|
|
3626
|
+
legendConfigSchema,
|
|
3627
|
+
legendPositionSchema,
|
|
3628
|
+
lineStyleSchema,
|
|
3629
|
+
renderAreaChartAnsi,
|
|
3630
|
+
renderAreaChartMarkdown,
|
|
3631
|
+
renderBarChartAnsi,
|
|
3632
|
+
renderBarChartMarkdown,
|
|
3633
|
+
renderHeatmapAnsi,
|
|
3634
|
+
renderHeatmapMarkdown,
|
|
3635
|
+
renderLegendRow,
|
|
3636
|
+
renderLineChartAnsi,
|
|
3637
|
+
renderLineChartMarkdown,
|
|
3638
|
+
renderPieChartAnsi,
|
|
3639
|
+
renderPieChartMarkdown,
|
|
3640
|
+
renderScatterChartAnsi,
|
|
3641
|
+
renderScatterChartMarkdown,
|
|
3642
|
+
renderStackedBarChartAnsi,
|
|
3643
|
+
renderStackedBarChartMarkdown,
|
|
3644
|
+
renderVerticalBarChartAnsi,
|
|
3645
|
+
renderVerticalBarChartMarkdown,
|
|
3646
|
+
renderXAxis,
|
|
3647
|
+
scaleValue,
|
|
3648
|
+
scatterMarkerSchema,
|
|
3649
|
+
scatterStyleSchema,
|
|
3650
|
+
toBrailleChar,
|
|
3651
|
+
unscaleValue,
|
|
3652
|
+
valueFormatSchema,
|
|
3653
|
+
valueToBlock,
|
|
3654
|
+
valueToHeatmapChar
|
|
3655
|
+
};
|
|
3656
|
+
//# sourceMappingURL=index.js.map
|