@funnest/charts-cli 0.2.1 → 0.4.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 +267 -4
- package/dist/index.js +267 -4
- package/dist/spec.d.ts +18 -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,10 +286,63 @@ 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":
|
|
271
|
-
return /* @__PURE__ */ jsx(BarChart, { ...common, stacked: spec.stacked ?? false, ...spec.colorBySign ? { getBarColor: (v) => v >= 0 ? UP : DOWN } : {}, children: extras });
|
|
345
|
+
return /* @__PURE__ */ jsx(BarChart, { ...common, stacked: spec.stacked ?? false, ...spec.layout === "horizontal" ? { layout: "horizontal" } : {}, ...spec.colorBySign ? { getBarColor: (v) => v >= 0 ? UP : DOWN } : {}, children: extras });
|
|
272
346
|
case "sparkline": {
|
|
273
347
|
const s = series[0];
|
|
274
348
|
const vals = (s?.data ?? []).filter((v) => v !== null);
|
|
@@ -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";
|
|
@@ -455,6 +614,7 @@ export function Chart() {
|
|
|
455
614
|
if (spec.stacked) props.push("stacked");
|
|
456
615
|
if (spec.area && spec.area !== "none") props.push(`area=${lit(spec.area)}`);
|
|
457
616
|
if (spec.curve) props.push(`curve=${lit(spec.curve)}`);
|
|
617
|
+
if (spec.type === "bar" && spec.layout === "horizontal") props.push('layout="horizontal"');
|
|
458
618
|
if (spec.type === "bar" && spec.colorBySign) props.push("getBarColor={(v) => (v >= 0 ? '#F04452' : '#3182F6')}");
|
|
459
619
|
if (ys.length > 1 || spec.legend) props.push("legend");
|
|
460
620
|
const children = [
|
|
@@ -572,7 +732,8 @@ var CHARTS = {
|
|
|
572
732
|
...COMMON,
|
|
573
733
|
...SERIES,
|
|
574
734
|
stacked: { kind: "boolean", default: false, ko: "\uC704\uB85C \uC313\uAE30", en: "Stack series" },
|
|
575
|
-
colorBySign: { kind: "boolean", default: false, ko: "0 \uC774\uC0C1 \uBE68\uAC15, \uBBF8\uB9CC \uD30C\uB791", en: "Red for \u2265 0, blue for < 0" }
|
|
735
|
+
colorBySign: { kind: "boolean", default: false, ko: "0 \uC774\uC0C1 \uBE68\uAC15, \uBBF8\uB9CC \uD30C\uB791", en: "Red for \u2265 0, blue for < 0" },
|
|
736
|
+
layout: { kind: "string", enum: ["vertical", "horizontal"], default: "vertical", ko: "'horizontal'\uC774\uBA74 \uAC00\uB85C \uB9C9\uB300 (\uC21C\uC704, \uC5C5\uC885\uBCC4 \uC218\uC775\uB960)", en: "'horizontal' for left-to-right bars (rankings, sector returns)" }
|
|
576
737
|
},
|
|
577
738
|
example: {
|
|
578
739
|
title: "\uC77C\uBCC4 \uC190\uC775",
|
|
@@ -584,6 +745,108 @@ var CHARTS = {
|
|
|
584
745
|
referenceLines: [{ y: 0 }]
|
|
585
746
|
}
|
|
586
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
|
+
},
|
|
587
850
|
"percentage-bar": {
|
|
588
851
|
type: "percentage-bar",
|
|
589
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
|
@@ -158,7 +158,8 @@ var CHARTS = {
|
|
|
158
158
|
...COMMON,
|
|
159
159
|
...SERIES,
|
|
160
160
|
stacked: { kind: "boolean", default: false, ko: "\uC704\uB85C \uC313\uAE30", en: "Stack series" },
|
|
161
|
-
colorBySign: { kind: "boolean", default: false, ko: "0 \uC774\uC0C1 \uBE68\uAC15, \uBBF8\uB9CC \uD30C\uB791", en: "Red for \u2265 0, blue for < 0" }
|
|
161
|
+
colorBySign: { kind: "boolean", default: false, ko: "0 \uC774\uC0C1 \uBE68\uAC15, \uBBF8\uB9CC \uD30C\uB791", en: "Red for \u2265 0, blue for < 0" },
|
|
162
|
+
layout: { kind: "string", enum: ["vertical", "horizontal"], default: "vertical", ko: "'horizontal'\uC774\uBA74 \uAC00\uB85C \uB9C9\uB300 (\uC21C\uC704, \uC5C5\uC885\uBCC4 \uC218\uC775\uB960)", en: "'horizontal' for left-to-right bars (rankings, sector returns)" }
|
|
162
163
|
},
|
|
163
164
|
example: {
|
|
164
165
|
title: "\uC77C\uBCC4 \uC190\uC775",
|
|
@@ -170,6 +171,108 @@ var CHARTS = {
|
|
|
170
171
|
referenceLines: [{ y: 0 }]
|
|
171
172
|
}
|
|
172
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
|
+
},
|
|
173
276
|
"percentage-bar": {
|
|
174
277
|
type: "percentage-bar",
|
|
175
278
|
ko: "\uBE44\uC911 \uB9C9\uB300. \uD3EC\uD2B8\uD3F4\uB9AC\uC624 \uBE44\uC911, \uB9E4\uC218\xB7\uB9E4\uB3C4 \uCCB4\uACB0 \uAC15\uB3C4",
|
|
@@ -358,7 +461,7 @@ function chartSchema(type, locale = "ko") {
|
|
|
358
461
|
var exampleSpec = (type) => ({ type, ...structuredClone(CHARTS[type].example) });
|
|
359
462
|
|
|
360
463
|
// src/render.tsx
|
|
361
|
-
import { AreaChart, BarChart, LineChart, Point, ReferenceLine } from "@funnest/charts-react";
|
|
464
|
+
import { AreaChart, BarChart, CandlestickChart, DonutChart, LineChart, Point, ReferenceLine, TreemapChart } from "@funnest/charts-react";
|
|
362
465
|
import { percentages, seriesColor } from "@funnest/charts-viz";
|
|
363
466
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
364
467
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -438,10 +541,63 @@ function element(spec, width, height) {
|
|
|
438
541
|
))
|
|
439
542
|
];
|
|
440
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
|
+
);
|
|
441
597
|
case "area":
|
|
442
598
|
return /* @__PURE__ */ jsx(AreaChart, { ...common, stacked: spec.stacked ?? false, ...spec.curve ? { curve: spec.curve } : {}, children: extras });
|
|
443
599
|
case "bar":
|
|
444
|
-
return /* @__PURE__ */ jsx(BarChart, { ...common, stacked: spec.stacked ?? false, ...spec.colorBySign ? { getBarColor: (v) => v >= 0 ? UP : DOWN } : {}, children: extras });
|
|
600
|
+
return /* @__PURE__ */ jsx(BarChart, { ...common, stacked: spec.stacked ?? false, ...spec.layout === "horizontal" ? { layout: "horizontal" } : {}, ...spec.colorBySign ? { getBarColor: (v) => v >= 0 ? UP : DOWN } : {}, children: extras });
|
|
445
601
|
case "sparkline": {
|
|
446
602
|
const s = series[0];
|
|
447
603
|
const vals = (s?.data ?? []).filter((v) => v !== null);
|
|
@@ -511,11 +667,43 @@ var percentBarSvg = (spec, width, top, p) => {
|
|
|
511
667
|
return bars + labels;
|
|
512
668
|
};
|
|
513
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
|
+
};
|
|
514
702
|
function renderSvg(spec, options = {}) {
|
|
515
703
|
const theme = THEMES[options.theme ?? spec.theme ?? "light"];
|
|
516
704
|
const isSpark = spec.type === "sparkline";
|
|
517
705
|
const width = spec.width ?? (isSpark ? 120 : 640);
|
|
518
|
-
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);
|
|
519
707
|
const pad = isSpark ? 0 : 16;
|
|
520
708
|
const innerW = width - pad * 2;
|
|
521
709
|
let y = pad;
|
|
@@ -528,6 +716,8 @@ function renderSvg(spec, options = {}) {
|
|
|
528
716
|
if (spec.type === "percentage-bar") {
|
|
529
717
|
body = percentBarSvg(spec, innerW, 0, theme);
|
|
530
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>`;
|
|
531
721
|
} else {
|
|
532
722
|
const series = seriesOf(spec);
|
|
533
723
|
const legend = !isSpark && (spec.legend ?? series.length > 1);
|
|
@@ -607,6 +797,57 @@ export function Chart() {
|
|
|
607
797
|
/>
|
|
608
798
|
);
|
|
609
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
|
+
}
|
|
610
851
|
`;
|
|
611
852
|
}
|
|
612
853
|
const component = spec.type === "area" ? "AreaChart" : spec.type === "bar" ? "BarChart" : "LineChart";
|
|
@@ -628,6 +869,7 @@ export function Chart() {
|
|
|
628
869
|
if (spec.stacked) props.push("stacked");
|
|
629
870
|
if (spec.area && spec.area !== "none") props.push(`area=${lit(spec.area)}`);
|
|
630
871
|
if (spec.curve) props.push(`curve=${lit(spec.curve)}`);
|
|
872
|
+
if (spec.type === "bar" && spec.layout === "horizontal") props.push('layout="horizontal"');
|
|
631
873
|
if (spec.type === "bar" && spec.colorBySign) props.push("getBarColor={(v) => (v >= 0 ? '#F04452' : '#3182F6')}");
|
|
632
874
|
if (ys.length > 1 || spec.legend) props.push("legend");
|
|
633
875
|
const children = [
|
|
@@ -717,6 +959,15 @@ function suggestCharts(rows) {
|
|
|
717
959
|
const out = [];
|
|
718
960
|
const allPositive = (c) => rows.every((r) => typeof r[c] !== "number" || r[c] >= 0);
|
|
719
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
|
+
}
|
|
720
971
|
if (time && numeric.length) {
|
|
721
972
|
const magnitude = (c) => Math.max(...rows.map((r) => Math.abs(Number(r[c]) || 0)));
|
|
722
973
|
const base = numeric.find(allPositive) ?? numeric[0];
|
|
@@ -748,11 +999,23 @@ function suggestCharts(rows) {
|
|
|
748
999
|
}
|
|
749
1000
|
if (category && numeric.length) {
|
|
750
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
|
+
}
|
|
751
1010
|
if (rows.length <= 8 && allPositive(v)) {
|
|
752
1011
|
out.push({
|
|
753
1012
|
reason: { ko: `${category}\uBCC4 ${v} \uBE44\uC911`, en: `Share of ${v} by ${category}` },
|
|
754
1013
|
spec: { type: "percentage-bar", data: rows, label: category, value: v }
|
|
755
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
|
+
});
|
|
756
1019
|
}
|
|
757
1020
|
out.push({
|
|
758
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,14 +36,30 @@ 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;
|
|
45
59
|
/** bar: red for ≥ 0, blue for < 0 (Korean convention). */
|
|
46
60
|
colorBySign?: boolean;
|
|
61
|
+
/** bar: 'horizontal' draws one row per category (rankings, sector returns). */
|
|
62
|
+
layout?: 'vertical' | 'horizontal';
|
|
47
63
|
showXAxis?: boolean;
|
|
48
64
|
showYAxis?: boolean;
|
|
49
65
|
legend?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@funnest/charts-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.4.0",
|
|
52
|
+
"@funnest/charts-viz": "0.4.0"
|
|
53
53
|
},
|
|
54
54
|
"optionalDependencies": {
|
|
55
55
|
"@resvg/resvg-js": "^2.6.2"
|