@funnest/charts-cli 0.3.0 → 0.5.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/dist/cli.js +263 -2
- package/dist/index.js +263 -2
- package/dist/spec.d.ts +16 -2
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -65,6 +65,15 @@ function suggestCharts(rows) {
|
|
|
65
65
|
const out = [];
|
|
66
66
|
const allPositive = (c) => rows.every((r) => typeof r[c] !== "number" || r[c] >= 0);
|
|
67
67
|
const hasNegative = (c) => rows.some((r) => typeof r[c] === "number" && r[c] < 0);
|
|
68
|
+
const find = (...names) => numeric.find((c) => names.includes(c.toLowerCase()));
|
|
69
|
+
const ohlc = { open: find("open", "o", "\uC2DC\uAC00"), high: find("high", "h", "\uACE0\uAC00"), low: find("low", "l", "\uC800\uAC00"), close: find("close", "c", "\uC885\uAC00") };
|
|
70
|
+
if (ohlc.open && ohlc.high && ohlc.low && ohlc.close) {
|
|
71
|
+
const volume = find("volume", "vol", "v", "\uAC70\uB798\uB7C9");
|
|
72
|
+
out.push({
|
|
73
|
+
reason: { ko: "\uC2DC\uAC00\xB7\uACE0\uAC00\xB7\uC800\uAC00\xB7\uC885\uAC00\uAC00 \uC788\uC5B4 \uCE94\uB4E4 \uCC28\uD2B8\uAC00 \uC798 \uB9DE\uC544\uC694", en: "Open/high/low/close columns: candles" },
|
|
74
|
+
spec: { type: "candlestick", data: rows, ...time ? { x: time } : {}, open: ohlc.open, high: ohlc.high, low: ohlc.low, close: ohlc.close, ...volume ? { volume } : {} }
|
|
75
|
+
});
|
|
76
|
+
}
|
|
68
77
|
if (time && numeric.length) {
|
|
69
78
|
const magnitude = (c) => Math.max(...rows.map((r) => Math.abs(Number(r[c]) || 0)));
|
|
70
79
|
const base = numeric.find(allPositive) ?? numeric[0];
|
|
@@ -96,11 +105,23 @@ function suggestCharts(rows) {
|
|
|
96
105
|
}
|
|
97
106
|
if (category && numeric.length) {
|
|
98
107
|
const v = numeric[0];
|
|
108
|
+
const size = numeric.find(allPositive);
|
|
109
|
+
const change = numeric.find(hasNegative);
|
|
110
|
+
if (size && change && rows.length >= 4) {
|
|
111
|
+
out.push({
|
|
112
|
+
reason: { ko: `${size}\uB85C \uD06C\uAE30, ${change}\uB85C \uC0C9\uC744 \uC815\uD55C \uB9C8\uCF13\uB9F5`, en: `Market map: size by ${size}, color by ${change}` },
|
|
113
|
+
spec: { type: "treemap", data: rows, label: category, value: size, change }
|
|
114
|
+
});
|
|
115
|
+
}
|
|
99
116
|
if (rows.length <= 8 && allPositive(v)) {
|
|
100
117
|
out.push({
|
|
101
118
|
reason: { ko: `${category}\uBCC4 ${v} \uBE44\uC911`, en: `Share of ${v} by ${category}` },
|
|
102
119
|
spec: { type: "percentage-bar", data: rows, label: category, value: v }
|
|
103
120
|
});
|
|
121
|
+
out.push({
|
|
122
|
+
reason: { ko: `${category}\uBCC4 ${v} \uBE44\uC911\uC744 \uB3C4\uB11B\uC73C\uB85C`, en: `Share of ${v} by ${category} as a donut` },
|
|
123
|
+
spec: { type: "donut", data: rows, label: category, value: v }
|
|
124
|
+
});
|
|
104
125
|
}
|
|
105
126
|
out.push({
|
|
106
127
|
reason: { ko: `${category}\uBCC4 ${v} \uBE44\uAD50`, en: `Compare ${v} by ${category}` },
|
|
@@ -185,7 +206,7 @@ Every command accepts --locale ko|en.`,
|
|
|
185
206
|
var messages = (locale = "ko") => MESSAGES[locale];
|
|
186
207
|
|
|
187
208
|
// src/render.tsx
|
|
188
|
-
import { AreaChart, BarChart, LineChart, Point, ReferenceLine } from "@funnest/charts-react";
|
|
209
|
+
import { AreaChart, BarChart, CandlestickChart, DonutChart, LineChart, Point, ReferenceLine, TreemapChart } from "@funnest/charts-react";
|
|
189
210
|
import { percentages, seriesColor } from "@funnest/charts-viz";
|
|
190
211
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
191
212
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -265,6 +286,59 @@ function element(spec, width, height) {
|
|
|
265
286
|
))
|
|
266
287
|
];
|
|
267
288
|
switch (spec.type) {
|
|
289
|
+
case "candlestick": {
|
|
290
|
+
const candles = spec.data.map((r) => {
|
|
291
|
+
const o = num(r[spec.open]);
|
|
292
|
+
const h = num(r[spec.high]);
|
|
293
|
+
const l = num(r[spec.low]);
|
|
294
|
+
const c = num(r[spec.close]);
|
|
295
|
+
if (o === null || h === null || l === null || c === null) return null;
|
|
296
|
+
const v = spec.volume ? num(r[spec.volume]) : null;
|
|
297
|
+
return { open: o, high: h, low: l, close: c, ...v !== null ? { volume: v } : {} };
|
|
298
|
+
});
|
|
299
|
+
return /* @__PURE__ */ jsx(
|
|
300
|
+
CandlestickChart,
|
|
301
|
+
{
|
|
302
|
+
data: candles,
|
|
303
|
+
width,
|
|
304
|
+
height,
|
|
305
|
+
animate: false,
|
|
306
|
+
tooltip: false,
|
|
307
|
+
valueFormatter: fmt,
|
|
308
|
+
...xAxis ? { xAxis } : {},
|
|
309
|
+
showXAxis: spec.showXAxis ?? !!spec.x,
|
|
310
|
+
showYAxis: spec.showYAxis === false ? false : { tickLabelFormatter: fmt, width: 64 },
|
|
311
|
+
children: (spec.referenceLines ?? []).map((r, i) => /* @__PURE__ */ jsx(
|
|
312
|
+
ReferenceLine,
|
|
313
|
+
{
|
|
314
|
+
yAxisId: "price",
|
|
315
|
+
...r.y !== void 0 ? { dataY: r.y } : {},
|
|
316
|
+
...r.x !== void 0 ? { dataX: indexOf(spec, r.x) } : {},
|
|
317
|
+
...r.label !== void 0 ? { label: r.label } : {},
|
|
318
|
+
...r.color ? { color: r.color } : {}
|
|
319
|
+
},
|
|
320
|
+
`r${i}`
|
|
321
|
+
))
|
|
322
|
+
}
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
case "treemap":
|
|
326
|
+
return /* @__PURE__ */ jsx(
|
|
327
|
+
TreemapChart,
|
|
328
|
+
{
|
|
329
|
+
width,
|
|
330
|
+
height,
|
|
331
|
+
enableTooltip: false,
|
|
332
|
+
valueFormatter: fmt,
|
|
333
|
+
...spec.maxChange !== void 0 ? { maxChange: spec.maxChange } : {},
|
|
334
|
+
data: spec.data.map((r, i) => {
|
|
335
|
+
const label = String(r[spec.label] ?? i);
|
|
336
|
+
const change = spec.change ? num(r[spec.change]) : null;
|
|
337
|
+
const color = spec.colors?.[label];
|
|
338
|
+
return { id: `${i}`, label, value: num(r[spec.value]) ?? 0, ...change !== null ? { change } : {}, ...color ? { color } : {} };
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
);
|
|
268
342
|
case "area":
|
|
269
343
|
return /* @__PURE__ */ jsx(AreaChart, { ...common, stacked: spec.stacked ?? false, ...spec.curve ? { curve: spec.curve } : {}, children: extras });
|
|
270
344
|
case "bar":
|
|
@@ -338,11 +412,43 @@ var percentBarSvg = (spec, width, top, p) => {
|
|
|
338
412
|
return bars + labels;
|
|
339
413
|
};
|
|
340
414
|
var textWidth = (t, size) => [...t].reduce((w, ch) => w + (ch.charCodeAt(0) > 11904 ? size : size * 0.6), 0);
|
|
415
|
+
var donutSvg = (spec, width, height, p) => {
|
|
416
|
+
const fmt = valueFormatter(spec);
|
|
417
|
+
const items = spec.data.map((r, i) => {
|
|
418
|
+
const label = String(r[spec.label] ?? i);
|
|
419
|
+
return { id: `${i}`, label, value: num(r[spec.value]) ?? 0, color: spec.colors?.[label] ?? seriesColor(i) };
|
|
420
|
+
});
|
|
421
|
+
const size = Math.max(60, Math.min(height, width * 0.5));
|
|
422
|
+
const markup = renderToStaticMarkup(
|
|
423
|
+
/* @__PURE__ */ jsx(
|
|
424
|
+
DonutChart,
|
|
425
|
+
{
|
|
426
|
+
data: items,
|
|
427
|
+
size,
|
|
428
|
+
showLegend: false,
|
|
429
|
+
valueFormatter: fmt,
|
|
430
|
+
...spec.totalLabel ? { totalLabel: spec.totalLabel } : {},
|
|
431
|
+
...spec.innerRadius !== void 0 ? { innerRadius: spec.innerRadius } : {}
|
|
432
|
+
}
|
|
433
|
+
)
|
|
434
|
+
);
|
|
435
|
+
const svg = markup.slice(markup.indexOf("<svg"), markup.indexOf("</svg>") + 6).replace("<svg", `<svg x="0" y="${(height - size) / 2}"`);
|
|
436
|
+
const segs = percentages(items);
|
|
437
|
+
const lx = size + 28;
|
|
438
|
+
const rowH = 24;
|
|
439
|
+
const top = height / 2 - items.length * rowH / 2 + rowH / 2;
|
|
440
|
+
const labels = items.map((it, i) => {
|
|
441
|
+
const y = top + i * rowH;
|
|
442
|
+
const pct = `${(Math.round(segs[i].fraction * 1e3) / 10).toString()}%`;
|
|
443
|
+
return `<circle cx="${lx + 4}" cy="${y}" r="4" fill="${it.color}"/><text x="${lx + 14}" y="${y}" dominant-baseline="central" font-size="13" fill="${p.muted}">${esc(it.label)} <tspan fill="${p.text}" font-weight="700">${pct}</tspan> <tspan fill="${p.muted}">${esc(fmt(it.value))}</tspan></text>`;
|
|
444
|
+
}).join("");
|
|
445
|
+
return svg + labels;
|
|
446
|
+
};
|
|
341
447
|
function renderSvg(spec, options = {}) {
|
|
342
448
|
const theme = THEMES[options.theme ?? spec.theme ?? "light"];
|
|
343
449
|
const isSpark = spec.type === "sparkline";
|
|
344
450
|
const width = spec.width ?? (isSpark ? 120 : 640);
|
|
345
|
-
const height = spec.height ?? (isSpark ? 36 : spec.type === "percentage-bar" ? 96 : 320);
|
|
451
|
+
const height = spec.height ?? (isSpark ? 36 : spec.type === "percentage-bar" ? 96 : spec.type === "donut" ? 220 : 320);
|
|
346
452
|
const pad = isSpark ? 0 : 16;
|
|
347
453
|
const innerW = width - pad * 2;
|
|
348
454
|
let y = pad;
|
|
@@ -355,6 +461,8 @@ function renderSvg(spec, options = {}) {
|
|
|
355
461
|
if (spec.type === "percentage-bar") {
|
|
356
462
|
body = percentBarSvg(spec, innerW, 0, theme);
|
|
357
463
|
body = `<g transform="translate(${pad},${y})">${body}</g>`;
|
|
464
|
+
} else if (spec.type === "donut") {
|
|
465
|
+
body = `<g transform="translate(${pad},${y})">${donutSvg(spec, innerW, Math.max(60, height - y - pad), theme)}</g>`;
|
|
358
466
|
} else {
|
|
359
467
|
const series = seriesOf(spec);
|
|
360
468
|
const legend = !isSpark && (spec.legend ?? series.length > 1);
|
|
@@ -434,6 +542,57 @@ export function Chart() {
|
|
|
434
542
|
/>
|
|
435
543
|
);
|
|
436
544
|
}
|
|
545
|
+
`;
|
|
546
|
+
}
|
|
547
|
+
if (spec.type === "candlestick") {
|
|
548
|
+
const vol = spec.volume ? `, volume: d[${lit(spec.volume)}]` : "";
|
|
549
|
+
return `import { CandlestickChart${spec.referenceLines?.length ? ", ReferenceLine" : ""} } from '@funnest/charts-react';
|
|
550
|
+
|
|
551
|
+
${dataLine}
|
|
552
|
+
${fmt}
|
|
553
|
+
export function Chart() {
|
|
554
|
+
return (
|
|
555
|
+
<CandlestickChart
|
|
556
|
+
data={data.map((d) => ({ open: d[${lit(spec.open)}], high: d[${lit(spec.high)}], low: d[${lit(spec.low)}], close: d[${lit(spec.close)}]${vol} }))}
|
|
557
|
+
height={${spec.height ?? 320}}${spec.x ? `
|
|
558
|
+
xAxis={{ data: data.map((d) => d[${lit(spec.x)}]) }}` : ""}${fmt ? "\n valueFormatter={fmt}" : ""}
|
|
559
|
+
>${(spec.referenceLines ?? []).map((r) => `
|
|
560
|
+
<ReferenceLine yAxisId="price"${r.y !== void 0 ? ` dataY={${r.y}}` : ""}${r.label ? ` label=${lit(r.label)}` : ""} />`).join("")}
|
|
561
|
+
</CandlestickChart>
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
`;
|
|
565
|
+
}
|
|
566
|
+
if (spec.type === "donut") {
|
|
567
|
+
return `import { DonutChart } from '@funnest/charts-react';
|
|
568
|
+
|
|
569
|
+
${dataLine}
|
|
570
|
+
${fmt}
|
|
571
|
+
export function Chart() {
|
|
572
|
+
return (
|
|
573
|
+
<DonutChart
|
|
574
|
+
data={data.map((d) => ({ id: String(d[${lit(spec.label)}]), label: String(d[${lit(spec.label)}]), value: d[${lit(spec.value)}]${spec.colors ? `, color: (${lit(spec.colors)} as Record<string, string>)[String(d[${lit(spec.label)}])]` : ""} }))}${spec.totalLabel ? `
|
|
575
|
+
totalLabel=${lit(spec.totalLabel)}` : ""}${spec.innerRadius !== void 0 ? `
|
|
576
|
+
innerRadius={${spec.innerRadius}}` : ""}${fmt ? "\n valueFormatter={fmt}" : ""}
|
|
577
|
+
/>
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
`;
|
|
581
|
+
}
|
|
582
|
+
if (spec.type === "treemap") {
|
|
583
|
+
return `import { TreemapChart } from '@funnest/charts-react';
|
|
584
|
+
|
|
585
|
+
${dataLine}
|
|
586
|
+
${fmt}
|
|
587
|
+
export function Chart() {
|
|
588
|
+
return (
|
|
589
|
+
<TreemapChart
|
|
590
|
+
height={${spec.height ?? 320}}
|
|
591
|
+
data={data.map((d, i) => ({ id: String(i), label: String(d[${lit(spec.label)}]), value: d[${lit(spec.value)}]${spec.change ? `, change: d[${lit(spec.change)}]` : ""} }))}${spec.maxChange !== void 0 ? `
|
|
592
|
+
maxChange={${spec.maxChange}}` : ""}${fmt ? "\n valueFormatter={fmt}" : ""}
|
|
593
|
+
/>
|
|
594
|
+
);
|
|
595
|
+
}
|
|
437
596
|
`;
|
|
438
597
|
}
|
|
439
598
|
const component = spec.type === "area" ? "AreaChart" : spec.type === "bar" ? "BarChart" : "LineChart";
|
|
@@ -586,6 +745,108 @@ var CHARTS = {
|
|
|
586
745
|
referenceLines: [{ y: 0 }]
|
|
587
746
|
}
|
|
588
747
|
},
|
|
748
|
+
candlestick: {
|
|
749
|
+
type: "candlestick",
|
|
750
|
+
ko: "\uCE94\uB4E4 \uCC28\uD2B8. \uC2DC\uAC00\xB7\uACE0\uAC00\xB7\uC800\uAC00\xB7\uC885\uAC00\uC640 \uAC70\uB798\uB7C9",
|
|
751
|
+
en: "Candlestick chart: open/high/low/close with volume",
|
|
752
|
+
component: "CandlestickChart",
|
|
753
|
+
fields: {
|
|
754
|
+
...COMMON,
|
|
755
|
+
x: SERIES.x,
|
|
756
|
+
open: { kind: "string", required: true, column: "numeric", ko: "\uC2DC\uAC00 \uC5F4", en: "Open column" },
|
|
757
|
+
high: { kind: "string", required: true, column: "numeric", ko: "\uACE0\uAC00 \uC5F4", en: "High column" },
|
|
758
|
+
low: { kind: "string", required: true, column: "numeric", ko: "\uC800\uAC00 \uC5F4", en: "Low column" },
|
|
759
|
+
close: { kind: "string", required: true, column: "numeric", ko: "\uC885\uAC00 \uC5F4", en: "Close column" },
|
|
760
|
+
volume: { kind: "string", column: "numeric", ko: "\uAC70\uB798\uB7C9 \uC5F4 (\uC788\uC73C\uBA74 \uC544\uB798\uC5D0 \uB9C9\uB300)", en: "Volume column (bars along the bottom)" },
|
|
761
|
+
showXAxis: SERIES.showXAxis,
|
|
762
|
+
showYAxis: SERIES.showYAxis,
|
|
763
|
+
format: FORMAT,
|
|
764
|
+
referenceLines: SERIES.referenceLines
|
|
765
|
+
},
|
|
766
|
+
example: {
|
|
767
|
+
title: "\uC0BC\uC131\uC804\uC790 \uC77C\uBD09",
|
|
768
|
+
data: (() => {
|
|
769
|
+
let p = 71e3;
|
|
770
|
+
return days(24).map((date, i) => {
|
|
771
|
+
const open = p;
|
|
772
|
+
const close = Math.round(open * (1 + Math.sin(i * 1.3) * 0.018 + 2e-3));
|
|
773
|
+
p = close;
|
|
774
|
+
return { date, open, high: Math.round(Math.max(open, close) * 1.008), low: Math.round(Math.min(open, close) * 0.992), close, volume: 8e6 + i * 137e4 % 6e6 };
|
|
775
|
+
});
|
|
776
|
+
})(),
|
|
777
|
+
x: "date",
|
|
778
|
+
open: "open",
|
|
779
|
+
high: "high",
|
|
780
|
+
low: "low",
|
|
781
|
+
close: "close",
|
|
782
|
+
volume: "volume",
|
|
783
|
+
format: { suffix: "\uC6D0" }
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
donut: {
|
|
787
|
+
type: "donut",
|
|
788
|
+
ko: "\uB3C4\uB11B \uCC28\uD2B8. \uC790\uC0B0 \uBE44\uC911, \uAC00\uC6B4\uB370\uC5D0 \uD569\uACC4",
|
|
789
|
+
en: "Donut chart: portfolio shares with the total in the middle",
|
|
790
|
+
component: "DonutChart",
|
|
791
|
+
fields: {
|
|
792
|
+
...COMMON,
|
|
793
|
+
height: { kind: "number", default: 220, ko: "\uB192\uC774(px)", en: "Height (px)" },
|
|
794
|
+
label: { kind: "string", required: true, column: "any", ko: "\uC774\uB984 \uC5F4", en: "Label column" },
|
|
795
|
+
value: { kind: "string", required: true, column: "numeric", ko: "\uAC12 \uC5F4", en: "Value column" },
|
|
796
|
+
colors: { kind: "record", ko: "\uC774\uB984 \u2192 \uC0C9", en: "Label \u2192 color" },
|
|
797
|
+
totalLabel: { kind: "string", ko: "\uAC00\uC6B4\uB370 \uD569\uACC4 \uC544\uB798 \uAE00\uC528", en: "Text under the total" },
|
|
798
|
+
innerRadius: { kind: "number", default: 0.68, ko: "\uAD6C\uBA4D \uD06C\uAE30 (0\uC774\uBA74 \uD30C\uC774)", en: "Hole size (0 = pie)" },
|
|
799
|
+
format: FORMAT
|
|
800
|
+
},
|
|
801
|
+
example: {
|
|
802
|
+
title: "\uC790\uC0B0 \uAD6C\uC131",
|
|
803
|
+
data: [
|
|
804
|
+
{ asset: "\uAD6D\uB0B4\uC8FC\uC2DD", amount: 4200 },
|
|
805
|
+
{ asset: "\uD574\uC678\uC8FC\uC2DD", amount: 3100 },
|
|
806
|
+
{ asset: "\uCF54\uC778", amount: 1700 },
|
|
807
|
+
{ asset: "\uD604\uAE08", amount: 1e3 }
|
|
808
|
+
],
|
|
809
|
+
label: "asset",
|
|
810
|
+
value: "amount",
|
|
811
|
+
colors: { \uD604\uAE08: "#B0B8C1" },
|
|
812
|
+
totalLabel: "\uCD1D \uC790\uC0B0",
|
|
813
|
+
format: { suffix: "\uB9CC" }
|
|
814
|
+
}
|
|
815
|
+
},
|
|
816
|
+
treemap: {
|
|
817
|
+
type: "treemap",
|
|
818
|
+
ko: "\uD2B8\uB9AC\uB9F5. \uC2DC\uAC00\uCD1D\uC561\uC73C\uB85C \uD06C\uAE30, \uB4F1\uB77D\uB960\uB85C \uC0C9\uC744 \uC815\uD558\uB294 \uB9C8\uCF13\uB9F5",
|
|
819
|
+
en: "Treemap: market map sized by value, colored by change",
|
|
820
|
+
component: "TreemapChart",
|
|
821
|
+
fields: {
|
|
822
|
+
...COMMON,
|
|
823
|
+
label: { kind: "string", required: true, column: "any", ko: "\uC774\uB984 \uC5F4", en: "Label column" },
|
|
824
|
+
value: { kind: "string", required: true, column: "numeric", ko: "\uD06C\uAE30 \uC5F4 (\uC2DC\uAC00\uCD1D\uC561 \uB4F1)", en: "Size column (market cap, \u2026)" },
|
|
825
|
+
change: { kind: "string", column: "numeric", ko: "\uB4F1\uB77D\uB960 \uC5F4 (%). \uC624\uB974\uBA74 \uBE68\uAC15, \uB0B4\uB9AC\uBA74 \uD30C\uB791", en: "Change column (%): red up, blue down" },
|
|
826
|
+
maxChange: { kind: "number", default: 5, ko: "\uAC00\uC7A5 \uC9C4\uD55C \uC0C9\uC774 \uB418\uB294 \uB4F1\uB77D\uB960", en: "Change for the full color" },
|
|
827
|
+
colors: { kind: "record", ko: "\uC774\uB984 \u2192 \uC0C9 (change \uB300\uC2E0)", en: "Label \u2192 color (instead of change)" },
|
|
828
|
+
format: FORMAT
|
|
829
|
+
},
|
|
830
|
+
example: {
|
|
831
|
+
title: "\uC624\uB298\uC758 \uC2DC\uC7A5",
|
|
832
|
+
data: [
|
|
833
|
+
{ name: "\uC0BC\uC131\uC804\uC790", cap: 420, pct: 1.84 },
|
|
834
|
+
{ name: "SK\uD558\uC774\uB2C9\uC2A4", cap: 130, pct: 4.12 },
|
|
835
|
+
{ name: "LG\uC5D0\uB108\uC9C0\uC194\uB8E8\uC158", cap: 85, pct: -2.31 },
|
|
836
|
+
{ name: "\uC0BC\uC131\uBC14\uC774\uC624", cap: 55, pct: -0.4 },
|
|
837
|
+
{ name: "\uD604\uB300\uCC28", cap: 48, pct: 0.62 },
|
|
838
|
+
{ name: "\uC140\uD2B8\uB9AC\uC628", cap: 40, pct: 2.95 },
|
|
839
|
+
{ name: "\uAE30\uC544", cap: 38, pct: 1.05 },
|
|
840
|
+
{ name: "NAVER", cap: 32, pct: -0.88 },
|
|
841
|
+
{ name: "KB\uAE08\uC735", cap: 30, pct: 0.21 },
|
|
842
|
+
{ name: "\uCE74\uCE74\uC624", cap: 18, pct: -3.4 }
|
|
843
|
+
],
|
|
844
|
+
label: "name",
|
|
845
|
+
value: "cap",
|
|
846
|
+
change: "pct",
|
|
847
|
+
format: { suffix: "\uC870" }
|
|
848
|
+
}
|
|
849
|
+
},
|
|
589
850
|
"percentage-bar": {
|
|
590
851
|
type: "percentage-bar",
|
|
591
852
|
ko: "\uBE44\uC911 \uB9C9\uB300. \uD3EC\uD2B8\uD3F4\uB9AC\uC624 \uBE44\uC911, \uB9E4\uC218\xB7\uB9E4\uB3C4 \uCCB4\uACB0 \uAC15\uB3C4",
|
package/dist/index.js
CHANGED
|
@@ -171,6 +171,108 @@ var CHARTS = {
|
|
|
171
171
|
referenceLines: [{ y: 0 }]
|
|
172
172
|
}
|
|
173
173
|
},
|
|
174
|
+
candlestick: {
|
|
175
|
+
type: "candlestick",
|
|
176
|
+
ko: "\uCE94\uB4E4 \uCC28\uD2B8. \uC2DC\uAC00\xB7\uACE0\uAC00\xB7\uC800\uAC00\xB7\uC885\uAC00\uC640 \uAC70\uB798\uB7C9",
|
|
177
|
+
en: "Candlestick chart: open/high/low/close with volume",
|
|
178
|
+
component: "CandlestickChart",
|
|
179
|
+
fields: {
|
|
180
|
+
...COMMON,
|
|
181
|
+
x: SERIES.x,
|
|
182
|
+
open: { kind: "string", required: true, column: "numeric", ko: "\uC2DC\uAC00 \uC5F4", en: "Open column" },
|
|
183
|
+
high: { kind: "string", required: true, column: "numeric", ko: "\uACE0\uAC00 \uC5F4", en: "High column" },
|
|
184
|
+
low: { kind: "string", required: true, column: "numeric", ko: "\uC800\uAC00 \uC5F4", en: "Low column" },
|
|
185
|
+
close: { kind: "string", required: true, column: "numeric", ko: "\uC885\uAC00 \uC5F4", en: "Close column" },
|
|
186
|
+
volume: { kind: "string", column: "numeric", ko: "\uAC70\uB798\uB7C9 \uC5F4 (\uC788\uC73C\uBA74 \uC544\uB798\uC5D0 \uB9C9\uB300)", en: "Volume column (bars along the bottom)" },
|
|
187
|
+
showXAxis: SERIES.showXAxis,
|
|
188
|
+
showYAxis: SERIES.showYAxis,
|
|
189
|
+
format: FORMAT,
|
|
190
|
+
referenceLines: SERIES.referenceLines
|
|
191
|
+
},
|
|
192
|
+
example: {
|
|
193
|
+
title: "\uC0BC\uC131\uC804\uC790 \uC77C\uBD09",
|
|
194
|
+
data: (() => {
|
|
195
|
+
let p = 71e3;
|
|
196
|
+
return days(24).map((date, i) => {
|
|
197
|
+
const open = p;
|
|
198
|
+
const close = Math.round(open * (1 + Math.sin(i * 1.3) * 0.018 + 2e-3));
|
|
199
|
+
p = close;
|
|
200
|
+
return { date, open, high: Math.round(Math.max(open, close) * 1.008), low: Math.round(Math.min(open, close) * 0.992), close, volume: 8e6 + i * 137e4 % 6e6 };
|
|
201
|
+
});
|
|
202
|
+
})(),
|
|
203
|
+
x: "date",
|
|
204
|
+
open: "open",
|
|
205
|
+
high: "high",
|
|
206
|
+
low: "low",
|
|
207
|
+
close: "close",
|
|
208
|
+
volume: "volume",
|
|
209
|
+
format: { suffix: "\uC6D0" }
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
donut: {
|
|
213
|
+
type: "donut",
|
|
214
|
+
ko: "\uB3C4\uB11B \uCC28\uD2B8. \uC790\uC0B0 \uBE44\uC911, \uAC00\uC6B4\uB370\uC5D0 \uD569\uACC4",
|
|
215
|
+
en: "Donut chart: portfolio shares with the total in the middle",
|
|
216
|
+
component: "DonutChart",
|
|
217
|
+
fields: {
|
|
218
|
+
...COMMON,
|
|
219
|
+
height: { kind: "number", default: 220, ko: "\uB192\uC774(px)", en: "Height (px)" },
|
|
220
|
+
label: { kind: "string", required: true, column: "any", ko: "\uC774\uB984 \uC5F4", en: "Label column" },
|
|
221
|
+
value: { kind: "string", required: true, column: "numeric", ko: "\uAC12 \uC5F4", en: "Value column" },
|
|
222
|
+
colors: { kind: "record", ko: "\uC774\uB984 \u2192 \uC0C9", en: "Label \u2192 color" },
|
|
223
|
+
totalLabel: { kind: "string", ko: "\uAC00\uC6B4\uB370 \uD569\uACC4 \uC544\uB798 \uAE00\uC528", en: "Text under the total" },
|
|
224
|
+
innerRadius: { kind: "number", default: 0.68, ko: "\uAD6C\uBA4D \uD06C\uAE30 (0\uC774\uBA74 \uD30C\uC774)", en: "Hole size (0 = pie)" },
|
|
225
|
+
format: FORMAT
|
|
226
|
+
},
|
|
227
|
+
example: {
|
|
228
|
+
title: "\uC790\uC0B0 \uAD6C\uC131",
|
|
229
|
+
data: [
|
|
230
|
+
{ asset: "\uAD6D\uB0B4\uC8FC\uC2DD", amount: 4200 },
|
|
231
|
+
{ asset: "\uD574\uC678\uC8FC\uC2DD", amount: 3100 },
|
|
232
|
+
{ asset: "\uCF54\uC778", amount: 1700 },
|
|
233
|
+
{ asset: "\uD604\uAE08", amount: 1e3 }
|
|
234
|
+
],
|
|
235
|
+
label: "asset",
|
|
236
|
+
value: "amount",
|
|
237
|
+
colors: { \uD604\uAE08: "#B0B8C1" },
|
|
238
|
+
totalLabel: "\uCD1D \uC790\uC0B0",
|
|
239
|
+
format: { suffix: "\uB9CC" }
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
treemap: {
|
|
243
|
+
type: "treemap",
|
|
244
|
+
ko: "\uD2B8\uB9AC\uB9F5. \uC2DC\uAC00\uCD1D\uC561\uC73C\uB85C \uD06C\uAE30, \uB4F1\uB77D\uB960\uB85C \uC0C9\uC744 \uC815\uD558\uB294 \uB9C8\uCF13\uB9F5",
|
|
245
|
+
en: "Treemap: market map sized by value, colored by change",
|
|
246
|
+
component: "TreemapChart",
|
|
247
|
+
fields: {
|
|
248
|
+
...COMMON,
|
|
249
|
+
label: { kind: "string", required: true, column: "any", ko: "\uC774\uB984 \uC5F4", en: "Label column" },
|
|
250
|
+
value: { kind: "string", required: true, column: "numeric", ko: "\uD06C\uAE30 \uC5F4 (\uC2DC\uAC00\uCD1D\uC561 \uB4F1)", en: "Size column (market cap, \u2026)" },
|
|
251
|
+
change: { kind: "string", column: "numeric", ko: "\uB4F1\uB77D\uB960 \uC5F4 (%). \uC624\uB974\uBA74 \uBE68\uAC15, \uB0B4\uB9AC\uBA74 \uD30C\uB791", en: "Change column (%): red up, blue down" },
|
|
252
|
+
maxChange: { kind: "number", default: 5, ko: "\uAC00\uC7A5 \uC9C4\uD55C \uC0C9\uC774 \uB418\uB294 \uB4F1\uB77D\uB960", en: "Change for the full color" },
|
|
253
|
+
colors: { kind: "record", ko: "\uC774\uB984 \u2192 \uC0C9 (change \uB300\uC2E0)", en: "Label \u2192 color (instead of change)" },
|
|
254
|
+
format: FORMAT
|
|
255
|
+
},
|
|
256
|
+
example: {
|
|
257
|
+
title: "\uC624\uB298\uC758 \uC2DC\uC7A5",
|
|
258
|
+
data: [
|
|
259
|
+
{ name: "\uC0BC\uC131\uC804\uC790", cap: 420, pct: 1.84 },
|
|
260
|
+
{ name: "SK\uD558\uC774\uB2C9\uC2A4", cap: 130, pct: 4.12 },
|
|
261
|
+
{ name: "LG\uC5D0\uB108\uC9C0\uC194\uB8E8\uC158", cap: 85, pct: -2.31 },
|
|
262
|
+
{ name: "\uC0BC\uC131\uBC14\uC774\uC624", cap: 55, pct: -0.4 },
|
|
263
|
+
{ name: "\uD604\uB300\uCC28", cap: 48, pct: 0.62 },
|
|
264
|
+
{ name: "\uC140\uD2B8\uB9AC\uC628", cap: 40, pct: 2.95 },
|
|
265
|
+
{ name: "\uAE30\uC544", cap: 38, pct: 1.05 },
|
|
266
|
+
{ name: "NAVER", cap: 32, pct: -0.88 },
|
|
267
|
+
{ name: "KB\uAE08\uC735", cap: 30, pct: 0.21 },
|
|
268
|
+
{ name: "\uCE74\uCE74\uC624", cap: 18, pct: -3.4 }
|
|
269
|
+
],
|
|
270
|
+
label: "name",
|
|
271
|
+
value: "cap",
|
|
272
|
+
change: "pct",
|
|
273
|
+
format: { suffix: "\uC870" }
|
|
274
|
+
}
|
|
275
|
+
},
|
|
174
276
|
"percentage-bar": {
|
|
175
277
|
type: "percentage-bar",
|
|
176
278
|
ko: "\uBE44\uC911 \uB9C9\uB300. \uD3EC\uD2B8\uD3F4\uB9AC\uC624 \uBE44\uC911, \uB9E4\uC218\xB7\uB9E4\uB3C4 \uCCB4\uACB0 \uAC15\uB3C4",
|
|
@@ -359,7 +461,7 @@ function chartSchema(type, locale = "ko") {
|
|
|
359
461
|
var exampleSpec = (type) => ({ type, ...structuredClone(CHARTS[type].example) });
|
|
360
462
|
|
|
361
463
|
// src/render.tsx
|
|
362
|
-
import { AreaChart, BarChart, LineChart, Point, ReferenceLine } from "@funnest/charts-react";
|
|
464
|
+
import { AreaChart, BarChart, CandlestickChart, DonutChart, LineChart, Point, ReferenceLine, TreemapChart } from "@funnest/charts-react";
|
|
363
465
|
import { percentages, seriesColor } from "@funnest/charts-viz";
|
|
364
466
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
365
467
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -439,6 +541,59 @@ function element(spec, width, height) {
|
|
|
439
541
|
))
|
|
440
542
|
];
|
|
441
543
|
switch (spec.type) {
|
|
544
|
+
case "candlestick": {
|
|
545
|
+
const candles = spec.data.map((r) => {
|
|
546
|
+
const o = num(r[spec.open]);
|
|
547
|
+
const h = num(r[spec.high]);
|
|
548
|
+
const l = num(r[spec.low]);
|
|
549
|
+
const c = num(r[spec.close]);
|
|
550
|
+
if (o === null || h === null || l === null || c === null) return null;
|
|
551
|
+
const v = spec.volume ? num(r[spec.volume]) : null;
|
|
552
|
+
return { open: o, high: h, low: l, close: c, ...v !== null ? { volume: v } : {} };
|
|
553
|
+
});
|
|
554
|
+
return /* @__PURE__ */ jsx(
|
|
555
|
+
CandlestickChart,
|
|
556
|
+
{
|
|
557
|
+
data: candles,
|
|
558
|
+
width,
|
|
559
|
+
height,
|
|
560
|
+
animate: false,
|
|
561
|
+
tooltip: false,
|
|
562
|
+
valueFormatter: fmt,
|
|
563
|
+
...xAxis ? { xAxis } : {},
|
|
564
|
+
showXAxis: spec.showXAxis ?? !!spec.x,
|
|
565
|
+
showYAxis: spec.showYAxis === false ? false : { tickLabelFormatter: fmt, width: 64 },
|
|
566
|
+
children: (spec.referenceLines ?? []).map((r, i) => /* @__PURE__ */ jsx(
|
|
567
|
+
ReferenceLine,
|
|
568
|
+
{
|
|
569
|
+
yAxisId: "price",
|
|
570
|
+
...r.y !== void 0 ? { dataY: r.y } : {},
|
|
571
|
+
...r.x !== void 0 ? { dataX: indexOf(spec, r.x) } : {},
|
|
572
|
+
...r.label !== void 0 ? { label: r.label } : {},
|
|
573
|
+
...r.color ? { color: r.color } : {}
|
|
574
|
+
},
|
|
575
|
+
`r${i}`
|
|
576
|
+
))
|
|
577
|
+
}
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
case "treemap":
|
|
581
|
+
return /* @__PURE__ */ jsx(
|
|
582
|
+
TreemapChart,
|
|
583
|
+
{
|
|
584
|
+
width,
|
|
585
|
+
height,
|
|
586
|
+
enableTooltip: false,
|
|
587
|
+
valueFormatter: fmt,
|
|
588
|
+
...spec.maxChange !== void 0 ? { maxChange: spec.maxChange } : {},
|
|
589
|
+
data: spec.data.map((r, i) => {
|
|
590
|
+
const label = String(r[spec.label] ?? i);
|
|
591
|
+
const change = spec.change ? num(r[spec.change]) : null;
|
|
592
|
+
const color = spec.colors?.[label];
|
|
593
|
+
return { id: `${i}`, label, value: num(r[spec.value]) ?? 0, ...change !== null ? { change } : {}, ...color ? { color } : {} };
|
|
594
|
+
})
|
|
595
|
+
}
|
|
596
|
+
);
|
|
442
597
|
case "area":
|
|
443
598
|
return /* @__PURE__ */ jsx(AreaChart, { ...common, stacked: spec.stacked ?? false, ...spec.curve ? { curve: spec.curve } : {}, children: extras });
|
|
444
599
|
case "bar":
|
|
@@ -512,11 +667,43 @@ var percentBarSvg = (spec, width, top, p) => {
|
|
|
512
667
|
return bars + labels;
|
|
513
668
|
};
|
|
514
669
|
var textWidth = (t, size) => [...t].reduce((w, ch) => w + (ch.charCodeAt(0) > 11904 ? size : size * 0.6), 0);
|
|
670
|
+
var donutSvg = (spec, width, height, p) => {
|
|
671
|
+
const fmt = valueFormatter(spec);
|
|
672
|
+
const items = spec.data.map((r, i) => {
|
|
673
|
+
const label = String(r[spec.label] ?? i);
|
|
674
|
+
return { id: `${i}`, label, value: num(r[spec.value]) ?? 0, color: spec.colors?.[label] ?? seriesColor(i) };
|
|
675
|
+
});
|
|
676
|
+
const size = Math.max(60, Math.min(height, width * 0.5));
|
|
677
|
+
const markup = renderToStaticMarkup(
|
|
678
|
+
/* @__PURE__ */ jsx(
|
|
679
|
+
DonutChart,
|
|
680
|
+
{
|
|
681
|
+
data: items,
|
|
682
|
+
size,
|
|
683
|
+
showLegend: false,
|
|
684
|
+
valueFormatter: fmt,
|
|
685
|
+
...spec.totalLabel ? { totalLabel: spec.totalLabel } : {},
|
|
686
|
+
...spec.innerRadius !== void 0 ? { innerRadius: spec.innerRadius } : {}
|
|
687
|
+
}
|
|
688
|
+
)
|
|
689
|
+
);
|
|
690
|
+
const svg = markup.slice(markup.indexOf("<svg"), markup.indexOf("</svg>") + 6).replace("<svg", `<svg x="0" y="${(height - size) / 2}"`);
|
|
691
|
+
const segs = percentages(items);
|
|
692
|
+
const lx = size + 28;
|
|
693
|
+
const rowH = 24;
|
|
694
|
+
const top = height / 2 - items.length * rowH / 2 + rowH / 2;
|
|
695
|
+
const labels = items.map((it, i) => {
|
|
696
|
+
const y = top + i * rowH;
|
|
697
|
+
const pct = `${(Math.round(segs[i].fraction * 1e3) / 10).toString()}%`;
|
|
698
|
+
return `<circle cx="${lx + 4}" cy="${y}" r="4" fill="${it.color}"/><text x="${lx + 14}" y="${y}" dominant-baseline="central" font-size="13" fill="${p.muted}">${esc(it.label)} <tspan fill="${p.text}" font-weight="700">${pct}</tspan> <tspan fill="${p.muted}">${esc(fmt(it.value))}</tspan></text>`;
|
|
699
|
+
}).join("");
|
|
700
|
+
return svg + labels;
|
|
701
|
+
};
|
|
515
702
|
function renderSvg(spec, options = {}) {
|
|
516
703
|
const theme = THEMES[options.theme ?? spec.theme ?? "light"];
|
|
517
704
|
const isSpark = spec.type === "sparkline";
|
|
518
705
|
const width = spec.width ?? (isSpark ? 120 : 640);
|
|
519
|
-
const height = spec.height ?? (isSpark ? 36 : spec.type === "percentage-bar" ? 96 : 320);
|
|
706
|
+
const height = spec.height ?? (isSpark ? 36 : spec.type === "percentage-bar" ? 96 : spec.type === "donut" ? 220 : 320);
|
|
520
707
|
const pad = isSpark ? 0 : 16;
|
|
521
708
|
const innerW = width - pad * 2;
|
|
522
709
|
let y = pad;
|
|
@@ -529,6 +716,8 @@ function renderSvg(spec, options = {}) {
|
|
|
529
716
|
if (spec.type === "percentage-bar") {
|
|
530
717
|
body = percentBarSvg(spec, innerW, 0, theme);
|
|
531
718
|
body = `<g transform="translate(${pad},${y})">${body}</g>`;
|
|
719
|
+
} else if (spec.type === "donut") {
|
|
720
|
+
body = `<g transform="translate(${pad},${y})">${donutSvg(spec, innerW, Math.max(60, height - y - pad), theme)}</g>`;
|
|
532
721
|
} else {
|
|
533
722
|
const series = seriesOf(spec);
|
|
534
723
|
const legend = !isSpark && (spec.legend ?? series.length > 1);
|
|
@@ -608,6 +797,57 @@ export function Chart() {
|
|
|
608
797
|
/>
|
|
609
798
|
);
|
|
610
799
|
}
|
|
800
|
+
`;
|
|
801
|
+
}
|
|
802
|
+
if (spec.type === "candlestick") {
|
|
803
|
+
const vol = spec.volume ? `, volume: d[${lit(spec.volume)}]` : "";
|
|
804
|
+
return `import { CandlestickChart${spec.referenceLines?.length ? ", ReferenceLine" : ""} } from '@funnest/charts-react';
|
|
805
|
+
|
|
806
|
+
${dataLine}
|
|
807
|
+
${fmt}
|
|
808
|
+
export function Chart() {
|
|
809
|
+
return (
|
|
810
|
+
<CandlestickChart
|
|
811
|
+
data={data.map((d) => ({ open: d[${lit(spec.open)}], high: d[${lit(spec.high)}], low: d[${lit(spec.low)}], close: d[${lit(spec.close)}]${vol} }))}
|
|
812
|
+
height={${spec.height ?? 320}}${spec.x ? `
|
|
813
|
+
xAxis={{ data: data.map((d) => d[${lit(spec.x)}]) }}` : ""}${fmt ? "\n valueFormatter={fmt}" : ""}
|
|
814
|
+
>${(spec.referenceLines ?? []).map((r) => `
|
|
815
|
+
<ReferenceLine yAxisId="price"${r.y !== void 0 ? ` dataY={${r.y}}` : ""}${r.label ? ` label=${lit(r.label)}` : ""} />`).join("")}
|
|
816
|
+
</CandlestickChart>
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
`;
|
|
820
|
+
}
|
|
821
|
+
if (spec.type === "donut") {
|
|
822
|
+
return `import { DonutChart } from '@funnest/charts-react';
|
|
823
|
+
|
|
824
|
+
${dataLine}
|
|
825
|
+
${fmt}
|
|
826
|
+
export function Chart() {
|
|
827
|
+
return (
|
|
828
|
+
<DonutChart
|
|
829
|
+
data={data.map((d) => ({ id: String(d[${lit(spec.label)}]), label: String(d[${lit(spec.label)}]), value: d[${lit(spec.value)}]${spec.colors ? `, color: (${lit(spec.colors)} as Record<string, string>)[String(d[${lit(spec.label)}])]` : ""} }))}${spec.totalLabel ? `
|
|
830
|
+
totalLabel=${lit(spec.totalLabel)}` : ""}${spec.innerRadius !== void 0 ? `
|
|
831
|
+
innerRadius={${spec.innerRadius}}` : ""}${fmt ? "\n valueFormatter={fmt}" : ""}
|
|
832
|
+
/>
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
`;
|
|
836
|
+
}
|
|
837
|
+
if (spec.type === "treemap") {
|
|
838
|
+
return `import { TreemapChart } from '@funnest/charts-react';
|
|
839
|
+
|
|
840
|
+
${dataLine}
|
|
841
|
+
${fmt}
|
|
842
|
+
export function Chart() {
|
|
843
|
+
return (
|
|
844
|
+
<TreemapChart
|
|
845
|
+
height={${spec.height ?? 320}}
|
|
846
|
+
data={data.map((d, i) => ({ id: String(i), label: String(d[${lit(spec.label)}]), value: d[${lit(spec.value)}]${spec.change ? `, change: d[${lit(spec.change)}]` : ""} }))}${spec.maxChange !== void 0 ? `
|
|
847
|
+
maxChange={${spec.maxChange}}` : ""}${fmt ? "\n valueFormatter={fmt}" : ""}
|
|
848
|
+
/>
|
|
849
|
+
);
|
|
850
|
+
}
|
|
611
851
|
`;
|
|
612
852
|
}
|
|
613
853
|
const component = spec.type === "area" ? "AreaChart" : spec.type === "bar" ? "BarChart" : "LineChart";
|
|
@@ -719,6 +959,15 @@ function suggestCharts(rows) {
|
|
|
719
959
|
const out = [];
|
|
720
960
|
const allPositive = (c) => rows.every((r) => typeof r[c] !== "number" || r[c] >= 0);
|
|
721
961
|
const hasNegative = (c) => rows.some((r) => typeof r[c] === "number" && r[c] < 0);
|
|
962
|
+
const find = (...names) => numeric.find((c) => names.includes(c.toLowerCase()));
|
|
963
|
+
const ohlc = { open: find("open", "o", "\uC2DC\uAC00"), high: find("high", "h", "\uACE0\uAC00"), low: find("low", "l", "\uC800\uAC00"), close: find("close", "c", "\uC885\uAC00") };
|
|
964
|
+
if (ohlc.open && ohlc.high && ohlc.low && ohlc.close) {
|
|
965
|
+
const volume = find("volume", "vol", "v", "\uAC70\uB798\uB7C9");
|
|
966
|
+
out.push({
|
|
967
|
+
reason: { ko: "\uC2DC\uAC00\xB7\uACE0\uAC00\xB7\uC800\uAC00\xB7\uC885\uAC00\uAC00 \uC788\uC5B4 \uCE94\uB4E4 \uCC28\uD2B8\uAC00 \uC798 \uB9DE\uC544\uC694", en: "Open/high/low/close columns: candles" },
|
|
968
|
+
spec: { type: "candlestick", data: rows, ...time ? { x: time } : {}, open: ohlc.open, high: ohlc.high, low: ohlc.low, close: ohlc.close, ...volume ? { volume } : {} }
|
|
969
|
+
});
|
|
970
|
+
}
|
|
722
971
|
if (time && numeric.length) {
|
|
723
972
|
const magnitude = (c) => Math.max(...rows.map((r) => Math.abs(Number(r[c]) || 0)));
|
|
724
973
|
const base = numeric.find(allPositive) ?? numeric[0];
|
|
@@ -750,11 +999,23 @@ function suggestCharts(rows) {
|
|
|
750
999
|
}
|
|
751
1000
|
if (category && numeric.length) {
|
|
752
1001
|
const v = numeric[0];
|
|
1002
|
+
const size = numeric.find(allPositive);
|
|
1003
|
+
const change = numeric.find(hasNegative);
|
|
1004
|
+
if (size && change && rows.length >= 4) {
|
|
1005
|
+
out.push({
|
|
1006
|
+
reason: { ko: `${size}\uB85C \uD06C\uAE30, ${change}\uB85C \uC0C9\uC744 \uC815\uD55C \uB9C8\uCF13\uB9F5`, en: `Market map: size by ${size}, color by ${change}` },
|
|
1007
|
+
spec: { type: "treemap", data: rows, label: category, value: size, change }
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
753
1010
|
if (rows.length <= 8 && allPositive(v)) {
|
|
754
1011
|
out.push({
|
|
755
1012
|
reason: { ko: `${category}\uBCC4 ${v} \uBE44\uC911`, en: `Share of ${v} by ${category}` },
|
|
756
1013
|
spec: { type: "percentage-bar", data: rows, label: category, value: v }
|
|
757
1014
|
});
|
|
1015
|
+
out.push({
|
|
1016
|
+
reason: { ko: `${category}\uBCC4 ${v} \uBE44\uC911\uC744 \uB3C4\uB11B\uC73C\uB85C`, en: `Share of ${v} by ${category} as a donut` },
|
|
1017
|
+
spec: { type: "donut", data: rows, label: category, value: v }
|
|
1018
|
+
});
|
|
758
1019
|
}
|
|
759
1020
|
out.push({
|
|
760
1021
|
reason: { ko: `${category}\uBCC4 ${v} \uBE44\uAD50`, en: `Compare ${v} by ${category}` },
|
package/dist/spec.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type CliLocale } from './i18n';
|
|
2
2
|
/** A row of tabular data: column name → value. */
|
|
3
3
|
export type Row = Record<string, unknown>;
|
|
4
|
-
export type ChartType = 'line' | 'area' | 'bar' | 'percentage-bar' | 'sparkline';
|
|
4
|
+
export type ChartType = 'line' | 'area' | 'bar' | 'candlestick' | 'donut' | 'treemap' | 'percentage-bar' | 'sparkline';
|
|
5
5
|
export interface ReferenceLineSpec {
|
|
6
6
|
y?: number;
|
|
7
7
|
/** Row index, or a value of the x column. */
|
|
@@ -36,9 +36,23 @@ export interface ChartSpec {
|
|
|
36
36
|
labels?: Record<string, string>;
|
|
37
37
|
/** Colors per y column (or per row label for percentage-bar). */
|
|
38
38
|
colors?: Record<string, string>;
|
|
39
|
-
/** percentage-bar: the label and value columns. */
|
|
39
|
+
/** percentage-bar / donut / treemap: the label and value columns. */
|
|
40
40
|
label?: string;
|
|
41
41
|
value?: string;
|
|
42
|
+
/** candlestick: OHLC (+ volume) columns. */
|
|
43
|
+
open?: string;
|
|
44
|
+
high?: string;
|
|
45
|
+
low?: string;
|
|
46
|
+
close?: string;
|
|
47
|
+
volume?: string;
|
|
48
|
+
/** treemap: change column (%) for the tile color. */
|
|
49
|
+
change?: string;
|
|
50
|
+
/** treemap: |change| that gets the full color. Default 5. */
|
|
51
|
+
maxChange?: number;
|
|
52
|
+
/** donut: text under the total. */
|
|
53
|
+
totalLabel?: string;
|
|
54
|
+
/** donut: hole size 0–0.95 (0 = pie). Default 0.68. */
|
|
55
|
+
innerRadius?: number;
|
|
42
56
|
curve?: 'monotone' | 'linear' | 'step';
|
|
43
57
|
area?: 'none' | 'gradient' | 'solid';
|
|
44
58
|
stacked?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@funnest/charts-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Funnest Charts command line: list charts, validate JSON specs, render SVG/PNG/HTML, suggest charts for your data, generate React code",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chart",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"react": "^19.3.1",
|
|
50
50
|
"react-dom": "^19.3.1",
|
|
51
|
-
"@funnest/charts-react": "0.
|
|
52
|
-
"@funnest/charts-viz": "0.
|
|
51
|
+
"@funnest/charts-react": "0.5.0",
|
|
52
|
+
"@funnest/charts-viz": "0.5.0"
|
|
53
53
|
},
|
|
54
54
|
"optionalDependencies": {
|
|
55
55
|
"@resvg/resvg-js": "^2.6.2"
|