@konce-pt/react 0.18.0 → 0.18.2
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/charts/chart.d.ts +7 -3
- package/dist/charts/chart.js +83 -32
- package/dist/charts/llms.txt +102 -85
- package/dist/charts-entry/public-api.d.ts +1 -1
- package/package.json +14 -14
package/dist/charts/chart.d.ts
CHANGED
|
@@ -3,9 +3,13 @@ import { type KptChartConfig } from '@konce-pt/chart';
|
|
|
3
3
|
import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
|
|
4
4
|
export interface KptChartProps extends Omit<ComponentPropsWithRef<'div'>, 'children'>, KptI18nOverrides {
|
|
5
5
|
config: KptChartConfig;
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Szerokość projektowa. Wykres rysuje się w pikselach swojego kontenera (mierzy go
|
|
8
|
+
* `ResizeObserver`), więc `width`/`height` wyznaczają proporcję (wysokość = szerokość kontenera
|
|
9
|
+
* x height / width) i rozmiar pierwszej klatki, zanim pomiar dotrze (SSR).
|
|
10
|
+
*/
|
|
7
11
|
width?: number;
|
|
8
|
-
/** Wysokość
|
|
12
|
+
/** Wysokość projektowa - patrz `width`. */
|
|
9
13
|
height?: number;
|
|
10
14
|
}
|
|
11
15
|
/**
|
|
@@ -16,4 +20,4 @@ export interface KptChartProps extends Omit<ComponentPropsWithRef<'div'>, 'child
|
|
|
16
20
|
* @example
|
|
17
21
|
* <KptChart config={{ type: 'line', labels: ['Sty', 'Lut'], series: [{ name: 'Sprzedaż', data: [12, 19] }] }} />
|
|
18
22
|
*/
|
|
19
|
-
export declare function KptChart({ config, width, height, locale, dictionary, className, style, ...rest }: Readonly<KptChartProps>): import("react").JSX.Element;
|
|
23
|
+
export declare function KptChart({ config, width: designWidth, height: designHeight, locale, dictionary, className, style, ...rest }: Readonly<KptChartProps>): import("react").JSX.Element;
|
package/dist/charts/chart.js
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useId, useRef, useState } from 'react';
|
|
4
|
-
import { KptChartMotion, areaPath, axisBase, bandPath, bandScale, barLayout, chartScale, chartAccessibleName, chartDataTable, dataScale, deltaKind, domainScale, formatNumber, linePath, linearScale, pieSlices, plotArea, polarSlices, polarToCartesian, polygonPath, radarGrid, radarPoints, radialArea, sameChartShape, scaleRows, seriesColor, significantDelta, stackSeries, } from '@konce-pt/chart';
|
|
4
|
+
import { KptChartMotion, areaPath, axisBase, axisLabelIndices, bandPath, bandScale, barLayout, chartScale, chartAccessibleName, chartDataTable, chartMetrics, clampLabel, dataScale, deltaKind, domainScale, estimateLabelWidth, formatNumber, linePath, linearScale, pieSlices, plotArea, polarSlices, polarToCartesian, polygonPath, radarGrid, radarPoints, radialArea, sameChartShape, scaleRows, seriesColor, significantDelta, stackSeries, } from '@konce-pt/chart';
|
|
5
5
|
import { cn } from "../internal/cn.js";
|
|
6
6
|
import { useKptMotion } from "../motion/motion-context.js";
|
|
7
7
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
8
8
|
import { KptChartLegend } from "./chart-legend.js";
|
|
9
9
|
import { KptChartTooltip } from "./chart-tooltip.js";
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Etykiety osi poziomej: wszystkie, jeśli się mieszczą, inaczej równy podział 9/5/3/2
|
|
12
|
+
* (`axisLabelIndices`). Pełna nazwa każdej kategorii zostaje w pigułce prowadnicy i w dymku.
|
|
13
|
+
*/
|
|
14
|
+
function bottomLabels(labels, centers, y, width, m) {
|
|
15
|
+
const sizes = labels.map((l) => estimateLabelWidth(l, m.fontSize));
|
|
16
|
+
const shown = new Set(axisLabelIndices(centers, sizes));
|
|
17
|
+
return labels.map((text, i) => ({
|
|
18
|
+
at: centers[i] ?? 0,
|
|
19
|
+
x: clampLabel(centers[i] ?? 0, sizes[i] ?? 0, 0, width),
|
|
20
|
+
y,
|
|
21
|
+
text,
|
|
22
|
+
anchor: 'middle',
|
|
23
|
+
visible: shown.has(i),
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
/** Etykiety osi pionowej (słupki poziome): ten sam podział, rozmiarem jest wysokość linii. */
|
|
27
|
+
function sideLabels(labels, centers, x, m) {
|
|
28
|
+
const shown = new Set(axisLabelIndices(centers, centers.map(() => m.categoryLine), 2));
|
|
29
|
+
return labels.map((text, i) => ({ at: centers[i] ?? 0, x, y: (centers[i] ?? 0) + m.baselineShift, text, anchor: 'end', visible: shown.has(i) }));
|
|
30
|
+
}
|
|
12
31
|
/** Szerokość pasma smugi światła (jednostki viewBoxa) - połowa po każdej stronie czoła fali. */
|
|
13
32
|
const SHEEN_HALF = 90;
|
|
14
33
|
/** Zjechanie z wykresu: dymek czeka chwilę, zanim zgaśnie (powrót kursora go zatrzymuje). */
|
|
@@ -49,13 +68,46 @@ function styleFor(align) {
|
|
|
49
68
|
* @example
|
|
50
69
|
* <KptChart config={{ type: 'line', labels: ['Sty', 'Lut'], series: [{ name: 'Sprzedaż', data: [12, 19] }] }} />
|
|
51
70
|
*/
|
|
52
|
-
export function KptChart({ config, width = 600, height = 360, locale, dictionary, className, style, ...rest }) {
|
|
71
|
+
export function KptChart({ config, width: designWidth = 600, height: designHeight = 360, locale, dictionary, className, style, ...rest }) {
|
|
53
72
|
const { t, locale: activeLocale } = useKptMessages({ locale, dictionary });
|
|
54
73
|
const descId = `${useId()}-desc`;
|
|
55
74
|
// Alternatywa tekstowa (WCAG 1.1.1): nazwa SVG (`ariaLabel` -> tytuł -> i18n) i dane w ukrytej
|
|
56
75
|
// wizualnie tabeli - dymek na najechanie jest dostępny tylko myszą.
|
|
57
76
|
const accessibleName = chartAccessibleName(config, t('chart.label'));
|
|
58
77
|
const dataTable = chartDataTable(config, activeLocale);
|
|
78
|
+
// viewBox równy pikselom ekranu: długości rosną z kontenerem, a czcionka (11px) i grubość kresek
|
|
79
|
+
// zostają prawdziwymi pikselami. Stały viewBox skalował się w całości - na szerokim ekranie
|
|
80
|
+
// etykiety puchły i rozmywały się, w wąskiej kolumnie malały do nieczytelności.
|
|
81
|
+
const canvasRef = useRef(null);
|
|
82
|
+
const [measured, setMeasured] = useState(null);
|
|
83
|
+
// Rozmiar tekstu osi z CSS płótna (`--kpt-chart-font-size`, domyślnie `--kpt-font-size-sm`).
|
|
84
|
+
// Tekst w SVG go dziedziczy, a marginesy, odstęp etykiet od osi, pigułka prowadnicy
|
|
85
|
+
// i przerzedzanie etykiet liczą się z niego. 14 to wartość domyślnego tokenu.
|
|
86
|
+
const [fontPx, setFontPx] = useState(14);
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const el = canvasRef.current;
|
|
89
|
+
if (!el || typeof ResizeObserver === 'undefined')
|
|
90
|
+
return;
|
|
91
|
+
const readFont = () => {
|
|
92
|
+
const px = parseFloat(getComputedStyle(el).fontSize);
|
|
93
|
+
if (px > 0)
|
|
94
|
+
setFontPx(px);
|
|
95
|
+
};
|
|
96
|
+
readFont();
|
|
97
|
+
// Wysokość wynika z szerokości, a szerokości kontenera nie zmienia wysokość wykresu - bez pętli.
|
|
98
|
+
// Rozmiar tekstu czytany przy każdej zmianie szerokości - zmiana tokenu zwykle idzie w parze ze zmianą układu.
|
|
99
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
100
|
+
const w = Math.round(entry?.contentRect.width ?? 0);
|
|
101
|
+
if (w > 0)
|
|
102
|
+
setMeasured(w);
|
|
103
|
+
readFont();
|
|
104
|
+
});
|
|
105
|
+
observer.observe(el);
|
|
106
|
+
return () => observer.disconnect();
|
|
107
|
+
}, []);
|
|
108
|
+
const width = measured ?? designWidth;
|
|
109
|
+
const height = Math.round((width * designHeight) / designWidth);
|
|
110
|
+
const m = chartMetrics(fontPx);
|
|
59
111
|
const [hidden, setHidden] = useState(new Set());
|
|
60
112
|
const [highlighted, setHighlighted] = useState(null);
|
|
61
113
|
/** Indeks kategorii pod kursorem - prowadnica crosshair w wykresie liniowym. */
|
|
@@ -125,7 +177,7 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
125
177
|
const sheen = (() => {
|
|
126
178
|
if (!frame.sheen)
|
|
127
179
|
return null;
|
|
128
|
-
const plot = plotArea(width, height, horizontal ?
|
|
180
|
+
const plot = plotArea(width, height, horizontal ? m.horizontalInsets : m.verticalInsets);
|
|
129
181
|
const start = horizontal ? plot.y : plot.x;
|
|
130
182
|
const length = horizontal ? plot.height : plot.width;
|
|
131
183
|
const mid = start - SHEEN_HALF + (length + 2 * SHEEN_HALF) * frame.sheen.progress;
|
|
@@ -213,7 +265,7 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
213
265
|
: chartScale(rows, { valueAxis: view.valueAxis, stacked: view.stacked });
|
|
214
266
|
// Stos: punkty serii na narastającej sumie, pasmo od linii serii pod nią.
|
|
215
267
|
const stack = view.stacked ? stackSeries(rows) : null;
|
|
216
|
-
const plot = plotArea(width, height,
|
|
268
|
+
const plot = plotArea(width, height, m.verticalInsets);
|
|
217
269
|
const yScale = linearScale(scale.min, scale.max, plot.y + plot.height, plot.y);
|
|
218
270
|
const count = view.labels.length;
|
|
219
271
|
const xAt = (i) => count > 1 ? plot.x + (i / (count - 1)) * plot.width : plot.x + plot.width / 2;
|
|
@@ -229,17 +281,13 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
229
281
|
y2: yScale(t),
|
|
230
282
|
}));
|
|
231
283
|
const valueLabels = scale.ticks.map((t) => ({
|
|
232
|
-
x: plot.x -
|
|
233
|
-
y: yScale(t) +
|
|
284
|
+
x: plot.x - m.sideGap,
|
|
285
|
+
y: yScale(t) + m.baselineShift,
|
|
234
286
|
text: formatNumber(t),
|
|
235
287
|
anchor: 'end',
|
|
236
288
|
}));
|
|
237
|
-
const catLabels = view.labels.map((
|
|
238
|
-
|
|
239
|
-
y: plot.y + plot.height + 18,
|
|
240
|
-
text: label,
|
|
241
|
-
anchor: 'middle',
|
|
242
|
-
}));
|
|
289
|
+
const catLabels = bottomLabels(view.labels, view.labels.map((_, i) => xAt(i)), plot.y + plot.height + m.labelOffset, width, m);
|
|
290
|
+
const catTicks = view.labels.map((_, i) => ({ x: xAt(i), y: plot.y + plot.height }));
|
|
243
291
|
const series = active.map(({ s, i }, k) => ({
|
|
244
292
|
key: `ser:${s.name}`,
|
|
245
293
|
name: s.name,
|
|
@@ -257,14 +305,16 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
257
305
|
: [];
|
|
258
306
|
const crosshairCat = config.crosshair && activeIndex !== null ? catLabels[activeIndex] : undefined;
|
|
259
307
|
// Prowadnica i pigułka jadą razem z dymkiem.
|
|
260
|
-
const crossX = crosshairCat ? (frame.tip?.x ?? crosshairCat.
|
|
261
|
-
const pillWidth = crosshairCat ? Math.max(
|
|
308
|
+
const crossX = crosshairCat ? (frame.tip?.x ?? crosshairCat.at) : 0;
|
|
309
|
+
const pillWidth = crosshairCat ? Math.max(m.pill.minWidth, estimateLabelWidth(crosshairCat.text, m.fontSize) + 2 * m.pill.padding) : 0;
|
|
310
|
+
// Pigułka przy skrajnej kategorii przesuwa się do środka zamiast wyjść poza viewBox.
|
|
311
|
+
const pillX = crosshairCat ? clampLabel(crossX, pillWidth, 0, width) : 0;
|
|
262
312
|
/** Najbliższa kategoria w całym obszarze wykresu: wiersze wszystkich serii, dymek nad najwyższym punktem. */
|
|
263
313
|
const onMove = (event) => {
|
|
264
314
|
const p = svgPoint(event);
|
|
265
315
|
if (!p || !series.length)
|
|
266
316
|
return;
|
|
267
|
-
const idx = nearestIndex(catLabels.map((l) => l.
|
|
317
|
+
const idx = nearestIndex(catLabels.map((l) => l.at), p.x);
|
|
268
318
|
if (keepTip(idx))
|
|
269
319
|
return;
|
|
270
320
|
const hits = series.map((s) => ({ s, pt: s.points[idx] })).filter((e) => !!e.pt);
|
|
@@ -278,7 +328,7 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
278
328
|
if (moved && tipMotion())
|
|
279
329
|
setRipple({ id: ++rippleId.current, x: top.pt.x, y: top.pt.y, color: top.s.color });
|
|
280
330
|
};
|
|
281
|
-
return (_jsxs(_Fragment, { children: [_jsx("g", { className: "kpt-chart__grid", children: grid.map((g, i) => (_jsx("line", { className: "kpt-chart__gridline", x1: g.x1, y1: g.y1, x2: g.x2, y2: g.y2 }, i))) }), crosshairCat ? (_jsx("line", { className: "kpt-chart__crosshair", x1: crossX, y1: plot.y, x2: crossX, y2: plot.y + plot.height })) : null, _jsx("g", { className: "kpt-chart__axis", children: [...valueLabels, ...catLabels].map((l, i) => (_jsx("text", { x: l.x, y: l.y, textAnchor: l.anchor, children: l.text }, i))) }), crosshairCat ? (_jsxs("g", { className: "kpt-chart__cat-active", children: [_jsx("rect", { x:
|
|
331
|
+
return (_jsxs(_Fragment, { children: [_jsx("g", { className: "kpt-chart__grid", children: grid.map((g, i) => (_jsx("line", { className: "kpt-chart__gridline", x1: g.x1, y1: g.y1, x2: g.x2, y2: g.y2 }, i))) }), crosshairCat ? (_jsx("line", { className: "kpt-chart__crosshair", x1: crossX, y1: plot.y, x2: crossX, y2: plot.y + plot.height })) : null, _jsx("g", { className: "kpt-chart__axis", children: [...valueLabels, ...catLabels.filter((l) => l.visible)].map((l, i) => (_jsx("text", { x: l.x, y: l.y, textAnchor: l.anchor, children: l.text }, i))) }), _jsx("g", { className: "kpt-chart__cat-ticks", "aria-hidden": "true", children: catTicks.map((tk, i) => (_jsx("circle", { className: cn('kpt-chart__cat-tick', activeIndex === i && 'kpt-chart__cat-tick--active'), r: 2, cx: tk.x, cy: tk.y }, i))) }), crosshairCat ? (_jsxs("g", { className: "kpt-chart__cat-active", children: [_jsx("rect", { x: pillX - pillWidth / 2, y: crosshairCat.y - m.pill.rise, width: pillWidth, height: m.pill.height, rx: m.pill.height / 2 }), _jsx("text", { x: pillX, y: crosshairCat.y, textAnchor: "middle", children: crosshairCat.text })] })) : null, ghostState
|
|
282
332
|
? series.map((s) => {
|
|
283
333
|
const prev = ghostState.prev[s.i] ?? [];
|
|
284
334
|
const next = ghostState.next[s.i] ?? [];
|
|
@@ -313,7 +363,7 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
313
363
|
const orientation = view.orientation ?? 'vertical';
|
|
314
364
|
const vertical = orientation === 'vertical';
|
|
315
365
|
const active = view.series.map((s, i) => ({ s, i })).filter(({ s }) => !hidden.has(`ser:${s.name}`));
|
|
316
|
-
const plot = plotArea(width, height, vertical ?
|
|
366
|
+
const plot = plotArea(width, height, vertical ? m.verticalInsets : m.horizontalInsets);
|
|
317
367
|
const radius = view.borderRadius ?? 4;
|
|
318
368
|
const layout = barLayout(active.map(({ s }) => s.data), view.labels.length, plot, orientation, radius, frame.domain ? domainScale(frame.domain.target, frame.domain.min, frame.domain.max) : undefined, { stacked: view.stacked, valueAxis: view.valueAxis });
|
|
319
369
|
const colors = active.map(({ s, i }) => seriesColor(s.color, i));
|
|
@@ -325,16 +375,17 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
325
375
|
? layout.ticks.map((t) => ({ x1: plot.x, y1: t.pos, x2: plot.x + plot.width, y2: t.pos }))
|
|
326
376
|
: layout.ticks.map((t) => ({ x1: t.pos, y1: plot.y, x2: t.pos, y2: plot.y + plot.height }));
|
|
327
377
|
const valueLabels = vertical
|
|
328
|
-
? layout.ticks.map((t) => ({ x: plot.x -
|
|
378
|
+
? layout.ticks.map((t) => ({ x: plot.x - m.sideGap, y: t.pos + m.baselineShift, text: formatNumber(t.value), anchor: 'end' }))
|
|
329
379
|
: layout.ticks.map((t) => ({
|
|
330
380
|
x: t.pos,
|
|
331
|
-
y: plot.y + plot.height +
|
|
381
|
+
y: plot.y + plot.height + m.labelOffset,
|
|
332
382
|
text: formatNumber(t.value),
|
|
333
383
|
anchor: 'middle',
|
|
334
384
|
}));
|
|
385
|
+
const centers = view.labels.map((_, i) => cat.center(i));
|
|
335
386
|
const catLabels = vertical
|
|
336
|
-
? view.labels
|
|
337
|
-
: view.labels
|
|
387
|
+
? bottomLabels(view.labels, centers, plot.y + plot.height + m.labelOffset, width, m)
|
|
388
|
+
: sideLabels(view.labels, centers, plot.x - m.sideGap, m);
|
|
338
389
|
const bars = layout.bars.map((b, order) => ({
|
|
339
390
|
b,
|
|
340
391
|
order,
|
|
@@ -371,7 +422,7 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
371
422
|
const p = svgPoint(event);
|
|
372
423
|
if (!p)
|
|
373
424
|
return;
|
|
374
|
-
const idx = nearestIndex(catLabels.map((l) =>
|
|
425
|
+
const idx = nearestIndex(catLabels.map((l) => l.at), vertical ? p.x : p.y);
|
|
375
426
|
if (keepTip(idx))
|
|
376
427
|
return;
|
|
377
428
|
const group = bars.filter((e) => e.b.categoryIndex === idx);
|
|
@@ -379,11 +430,11 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
379
430
|
if (!group.length || !center)
|
|
380
431
|
return;
|
|
381
432
|
const label = config.labels[idx] ?? '';
|
|
382
|
-
const x = vertical ? center.
|
|
383
|
-
const y = vertical ? Math.min(...group.map((e) => e.tipY)) : center.
|
|
433
|
+
const x = vertical ? center.at : Math.max(...group.map((e) => e.tipX));
|
|
434
|
+
const y = vertical ? Math.min(...group.map((e) => e.tipY)) : center.at;
|
|
384
435
|
openTip(x, y, label, group.map((e) => row(e.name, label, e.b.value, e.color)), idx);
|
|
385
436
|
};
|
|
386
|
-
return (_jsxs(_Fragment, { children: [_jsx("g", { className: "kpt-chart__grid", children: grid.map((g, i) => (_jsx("line", { className: "kpt-chart__gridline", x1: g.x1, y1: g.y1, x2: g.x2, y2: g.y2 }, i))) }), _jsx("g", { className: "kpt-chart__axis", children: [...valueLabels, ...catLabels].map((l, i) => (_jsx("text", { x: l.x, y: l.y, textAnchor: l.anchor, children: l.text }, i))) }), ghostBars.length ? (_jsx("g", { className: "kpt-chart__ghost", "aria-hidden": "true", children: ghostBars.map((g) => (_jsx("path", { className: "kpt-chart__ghost-bar", d: g.path }, g.key))) })) : null, _jsx("g", { className: "kpt-chart__bars", "data-orientation": orientation, children: bars.map((e) => (_jsx("path", { className: cn('kpt-chart__bar', dim(e.key) && 'kpt-chart__series--dim'), d: e.b.path, style: { fill: e.color, ['--kpt-chart-i']: e.order } }, `${e.key}#${e.order}`))) }), ghostBars.length ? (_jsx("g", { className: "kpt-chart__ghost", "aria-hidden": "true", children: ghostBars.map((g) => (_jsx("rect", { className: "kpt-chart__delta-bar", "data-kind": g.kind, rx: 2, ...g.delta }, g.key))) })) : null, sheen
|
|
437
|
+
return (_jsxs(_Fragment, { children: [_jsx("g", { className: "kpt-chart__grid", children: grid.map((g, i) => (_jsx("line", { className: "kpt-chart__gridline", x1: g.x1, y1: g.y1, x2: g.x2, y2: g.y2 }, i))) }), _jsx("g", { className: "kpt-chart__axis", children: [...valueLabels, ...catLabels.filter((l) => l.visible)].map((l, i) => (_jsx("text", { x: l.x, y: l.y, textAnchor: l.anchor, children: l.text }, i))) }), ghostBars.length ? (_jsx("g", { className: "kpt-chart__ghost", "aria-hidden": "true", children: ghostBars.map((g) => (_jsx("path", { className: "kpt-chart__ghost-bar", d: g.path }, g.key))) })) : null, _jsx("g", { className: "kpt-chart__bars", "data-orientation": orientation, children: bars.map((e) => (_jsx("path", { className: cn('kpt-chart__bar', dim(e.key) && 'kpt-chart__series--dim'), d: e.b.path, style: { fill: e.color, ['--kpt-chart-i']: e.order } }, `${e.key}#${e.order}`))) }), ghostBars.length ? (_jsx("g", { className: "kpt-chart__ghost", "aria-hidden": "true", children: ghostBars.map((g) => (_jsx("rect", { className: "kpt-chart__delta-bar", "data-kind": g.kind, rx: 2, ...g.delta }, g.key))) })) : null, sheen
|
|
387
438
|
? bars.map((e) => (_jsx("path", { className: "kpt-chart__sheen-bar", d: e.b.path, fill: `url(#${sheenId})`, style: { opacity: sheen.opacity * 0.7 }, "aria-hidden": "true" }, `${e.key}#${e.order}`)))
|
|
388
439
|
: null, _jsx("rect", { className: "kpt-chart__hover", x: plot.x, y: plot.y, width: plot.width, height: plot.height, onPointerMove: onMove, onPointerLeave: hideTip })] }));
|
|
389
440
|
})();
|
|
@@ -394,7 +445,7 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
394
445
|
const first = view.series[0];
|
|
395
446
|
if (!first)
|
|
396
447
|
return null;
|
|
397
|
-
const { cx, cy, radius } = radialArea(width, height,
|
|
448
|
+
const { cx, cy, radius } = radialArea(width, height, m.radialPadding.plain);
|
|
398
449
|
const entries = config.labels
|
|
399
450
|
.map((label, i) => ({ label, i, value: first.data[i] ?? 0, color: seriesColor(first.color, i) }))
|
|
400
451
|
.filter((entry) => !hidden.has(`cat:${entry.i}`));
|
|
@@ -416,7 +467,7 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
416
467
|
const radarVm = (() => {
|
|
417
468
|
if (config.type !== 'radar')
|
|
418
469
|
return null;
|
|
419
|
-
const { cx, cy, radius } = radialArea(width, height,
|
|
470
|
+
const { cx, cy, radius } = radialArea(width, height, m.radialPadding.labelled);
|
|
420
471
|
const grid = radarGrid(config.labels.length, cx, cy, radius, 4);
|
|
421
472
|
const active = view.series.map((s, i) => ({ s, i })).filter(({ s }) => !hidden.has(`ser:${s.name}`));
|
|
422
473
|
let max = 1;
|
|
@@ -424,9 +475,9 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
424
475
|
for (const v of s.data)
|
|
425
476
|
max = Math.max(max, v);
|
|
426
477
|
return (_jsxs(_Fragment, { children: [_jsxs("g", { className: "kpt-chart__grid", children: [grid.rings.map((ring, i) => (_jsx("path", { className: "kpt-chart__ring", d: ring }, i))), grid.axes.map((axis, i) => (_jsx("line", { className: "kpt-chart__gridline", x1: cx, y1: cy, x2: axis.end.x, y2: axis.end.y }, i)))] }), _jsx("g", { className: "kpt-chart__axis", children: grid.axes.map((axis, i) => {
|
|
427
|
-
const p = polarToCartesian(cx, cy, radius +
|
|
478
|
+
const p = polarToCartesian(cx, cy, radius + m.radarLabelGap, axis.angle);
|
|
428
479
|
const anchor = Math.abs(p.x - cx) < 1 ? 'middle' : p.x > cx ? 'start' : 'end';
|
|
429
|
-
return (_jsx("text", { x: p.x, y: p.y +
|
|
480
|
+
return (_jsx("text", { x: p.x, y: p.y + m.baselineShift, textAnchor: anchor, children: config.labels[axis.labelIndex] ?? '' }, i));
|
|
430
481
|
}) }), active.map(({ s, i }) => {
|
|
431
482
|
const key = `ser:${s.name}`;
|
|
432
483
|
const color = seriesColor(s.color, i);
|
|
@@ -455,5 +506,5 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
455
506
|
fontWeight: config.subtitle.font?.weight,
|
|
456
507
|
paddingTop: config.subtitle.padding?.top,
|
|
457
508
|
paddingBottom: config.subtitle.padding?.bottom,
|
|
458
|
-
}, children: config.subtitle.text })) : null, _jsxs("div", { className: "kpt-chart__body", children: [_jsxs("div", { className: "kpt-chart__canvas", children: [_jsxs("svg", { className: "kpt-chart__svg", viewBox: `0 0 ${width} ${height}`, role: "img", "aria-label": accessibleName, "aria-describedby": config.description ? descId : undefined, preserveAspectRatio: "xMidYMid meet", children: [sheen ? (_jsx("defs", { children: _jsxs("linearGradient", { id: sheenId, gradientUnits: "userSpaceOnUse", x1: sheen.x1, x2: sheen.x2, y1: sheen.y1, y2: sheen.y2, children: [_jsx("stop", { offset: "0", className: "kpt-chart__sheen-stop", stopOpacity: 0 }), _jsx("stop", { offset: "0.5", className: "kpt-chart__sheen-stop", stopOpacity: 0.95 }), _jsx("stop", { offset: "1", className: "kpt-chart__sheen-stop", stopOpacity: 0 })] }) })) : null, lineVm, barVm, radarVm, pieVm] }), config.description ? (_jsx("p", { className: "kpt-chart__sr", id: descId, children: config.description })) : null, _jsxs("table", { className: "kpt-chart__sr", children: [_jsxs("caption", { children: [t('chart.dataTable'), ": ", accessibleName] }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: t('chart.category') }), dataTable.columns.map((col, i) => (_jsx("th", { scope: "col", children: col }, i)))] }) }), _jsx("tbody", { children: dataTable.rows.map((row, i) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: row.label }), row.values.map((value, j) => (_jsx("td", { children: value }, j)))] }, i))) })] }), _jsx(KptChartTooltip, { state: tooltip })] }), legendOpts.display ? (_jsx(KptChartLegend, { items: legendItems, position: legendOpts.position, align: legendOpts.align, pointStyle: legendOpts.pointStyle, title: legendOpts.title, onToggle: toggle, onHighlight: setHighlighted })) : null] })] }) }));
|
|
509
|
+
}, children: config.subtitle.text })) : null, _jsxs("div", { className: "kpt-chart__body", children: [_jsxs("div", { className: "kpt-chart__canvas", ref: canvasRef, children: [_jsxs("svg", { className: "kpt-chart__svg", viewBox: `0 0 ${width} ${height}`, role: "img", "aria-label": accessibleName, "aria-describedby": config.description ? descId : undefined, preserveAspectRatio: "xMidYMid meet", children: [sheen ? (_jsx("defs", { children: _jsxs("linearGradient", { id: sheenId, gradientUnits: "userSpaceOnUse", x1: sheen.x1, x2: sheen.x2, y1: sheen.y1, y2: sheen.y2, children: [_jsx("stop", { offset: "0", className: "kpt-chart__sheen-stop", stopOpacity: 0 }), _jsx("stop", { offset: "0.5", className: "kpt-chart__sheen-stop", stopOpacity: 0.95 }), _jsx("stop", { offset: "1", className: "kpt-chart__sheen-stop", stopOpacity: 0 })] }) })) : null, lineVm, barVm, radarVm, pieVm] }), config.description ? (_jsx("p", { className: "kpt-chart__sr", id: descId, children: config.description })) : null, _jsxs("table", { className: "kpt-chart__sr", children: [_jsxs("caption", { children: [t('chart.dataTable'), ": ", accessibleName] }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: t('chart.category') }), dataTable.columns.map((col, i) => (_jsx("th", { scope: "col", children: col }, i)))] }) }), _jsx("tbody", { children: dataTable.rows.map((row, i) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: row.label }), row.values.map((value, j) => (_jsx("td", { children: value }, j)))] }, i))) })] }), _jsx(KptChartTooltip, { state: tooltip })] }), legendOpts.display ? (_jsx(KptChartLegend, { items: legendItems, position: legendOpts.position, align: legendOpts.align, pointStyle: legendOpts.pointStyle, title: legendOpts.title, onToggle: toggle, onHighlight: setHighlighted })) : null] })] }) }));
|
|
459
510
|
}
|
package/dist/charts/llms.txt
CHANGED
|
@@ -1,85 +1,102 @@
|
|
|
1
|
-
# KptChart (@konce-pt/react/charts)
|
|
2
|
-
|
|
3
|
-
An SVG chart driven by a single `config` object: line, bar, pie, doughnut, polarArea, radar.
|
|
4
|
-
Import: `import { KptChart, type KptChartConfig } from '@konce-pt/react/charts';`
|
|
5
|
-
|
|
6
|
-
A separate subpath, because the geometry and the chart styles are heavy next to the library core
|
|
7
|
-
and not every app needs them.
|
|
8
|
-
|
|
9
|
-
## Props
|
|
10
|
-
- `config`: `KptChartConfig` (required)
|
|
11
|
-
- `width`, `height` - the
|
|
12
|
-
|
|
13
|
-
The
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- `className`, `ref` and every other `<div>` prop go to the host
|
|
19
|
-
|
|
20
|
-
## The config is the API
|
|
21
|
-
`{ type, labels, series, points?, area?, interpolation?, orientation?, borderRadius?, innerRadius?,
|
|
22
|
-
valueAxis?, stacked?, crosshair?, tooltip?, legend?, title?, subtitle?, animation?, ariaLabel?, description? }` - the same shape the Angular port
|
|
23
|
-
takes, because both read it from `@konce-pt/chart`.
|
|
24
|
-
|
|
25
|
-
## Value axis and stacking (line, bar)
|
|
26
|
-
- `valueAxis?: { zero?, min?, max? }` - the value axis starts at zero by default, so a bar and the area under a line
|
|
27
|
-
show the size directly. `zero: false` fits the axis to the data (a weight of 79-90 kg is no longer a flat line under
|
|
28
|
-
the ceiling); `min` / `max` pin an end of the axis exactly and win over `zero` - a value beyond a pinned end is
|
|
29
|
-
cut at the edge. Bars and areas grow from zero, or from the end of the axis nearer to it
|
|
30
|
-
- `stacked?: boolean` - stacked series: bars on top of each other in one band per category (positive values up,
|
|
31
|
-
negative down; only the outer segment is rounded), lines as running totals with a band under each one (with
|
|
32
|
-
`area`). The axis covers the totals; the tooltip shows the values of the series, not the totals
|
|
33
|
-
- The rules live in `@konce-pt/chart`: `axisScale`, `chartScale`, `stackSeries`, `bandPath` (tested)
|
|
34
|
-
|
|
35
|
-
##
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
##
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
1
|
+
# KptChart (@konce-pt/react/charts)
|
|
2
|
+
|
|
3
|
+
An SVG chart driven by a single `config` object: line, bar, pie, doughnut, polarArea, radar.
|
|
4
|
+
Import: `import { KptChart, type KptChartConfig } from '@konce-pt/react/charts';`
|
|
5
|
+
|
|
6
|
+
A separate subpath, because the geometry and the chart styles are heavy next to the library core
|
|
7
|
+
and not every app needs them.
|
|
8
|
+
|
|
9
|
+
## Props
|
|
10
|
+
- `config`: `KptChartConfig` (required)
|
|
11
|
+
- `width`, `height` - the aspect ratio (default 600 x 360); the chart fills the width of its container
|
|
12
|
+
|
|
13
|
+
The chart draws in the pixels of its container: a `ResizeObserver` measures the canvas width and the
|
|
14
|
+
viewBox follows it, the height comes from the `width`/`height` ratio. Lengths grow and shrink with the
|
|
15
|
+
container, while the text (axis labels, 14px by default) and the line weights stay real pixels - the
|
|
16
|
+
same in a 300px column and on a 3000px screen. `width`/`height` are therefore only the aspect ratio and the
|
|
17
|
+
size of the first frame before the measurement arrives (SSR); there is no need to scale them by hand.
|
|
18
|
+
- `className`, `ref` and every other `<div>` prop go to the host
|
|
19
|
+
|
|
20
|
+
## The config is the API
|
|
21
|
+
`{ type, labels, series, points?, area?, interpolation?, orientation?, borderRadius?, innerRadius?,
|
|
22
|
+
valueAxis?, stacked?, crosshair?, tooltip?, legend?, title?, subtitle?, animation?, ariaLabel?, description? }` - the same shape the Angular port
|
|
23
|
+
takes, because both read it from `@konce-pt/chart`.
|
|
24
|
+
|
|
25
|
+
## Value axis and stacking (line, bar)
|
|
26
|
+
- `valueAxis?: { zero?, min?, max? }` - the value axis starts at zero by default, so a bar and the area under a line
|
|
27
|
+
show the size directly. `zero: false` fits the axis to the data (a weight of 79-90 kg is no longer a flat line under
|
|
28
|
+
the ceiling); `min` / `max` pin an end of the axis exactly and win over `zero` - a value beyond a pinned end is
|
|
29
|
+
cut at the edge. Bars and areas grow from zero, or from the end of the axis nearer to it
|
|
30
|
+
- `stacked?: boolean` - stacked series: bars on top of each other in one band per category (positive values up,
|
|
31
|
+
negative down; only the outer segment is rounded), lines as running totals with a band under each one (with
|
|
32
|
+
`area`). The axis covers the totals; the tooltip shows the values of the series, not the totals
|
|
33
|
+
- The rules live in `@konce-pt/chart`: `axisScale`, `chartScale`, `stackSeries`, `bandPath` (tested)
|
|
34
|
+
|
|
35
|
+
## Category axis (line, bar)
|
|
36
|
+
- When the category labels do not fit, the axis shows an even subset instead of overlapping text: all of
|
|
37
|
+
them if they fit, otherwise the densest split that fits - 9 labels (eighths of the axis), 5 (first, 1/4,
|
|
38
|
+
middle, 3/4, last), 3, and finally the two ends only. Horizontal bars apply the same rule to the rows
|
|
39
|
+
- The choice is made in on-screen pixels (the chart measures its container), so a narrow column shows fewer
|
|
40
|
+
labels and a wide one more; the outer labels and the
|
|
41
|
+
crosshair pill are shifted inward so they never leave the chart
|
|
42
|
+
- A line chart marks every category with a small dot on the axis; the active one grows on hover. The full
|
|
43
|
+
name of any category stays in the crosshair pill and the tooltip
|
|
44
|
+
- The rules live in `@konce-pt/chart`: `axisLabelIndices`, `estimateLabelWidth`, `clampLabel` (tested)
|
|
45
|
+
|
|
46
|
+
## The geometry lives in a package
|
|
47
|
+
Scales, paths, bar layout and arcs come from `@konce-pt/chart` - pure functions with `node --test`
|
|
48
|
+
coverage, shared with the Angular port. The component only turns them into SVG elements; there is
|
|
49
|
+
no DOM manipulation and no canvas.
|
|
50
|
+
|
|
51
|
+
## Hover covers the whole plot
|
|
52
|
+
Line and bar charts listen on an invisible surface over the plot: the pointer anywhere picks the nearest
|
|
53
|
+
category, the tooltip lists every visible series there and travels from category to category without
|
|
54
|
+
fading out in between. Slices and radar points react to the element itself. The bubble never leaves the
|
|
55
|
+
chart - it is clamped by its real width.
|
|
56
|
+
|
|
57
|
+
## Motion: data updates and the tooltip
|
|
58
|
+
A new `config` with the same type, categories and series names is a data update: the values travel as a
|
|
59
|
+
wave from left to right on springs, with a light sheen at the wave front. Line and bar charts keep the
|
|
60
|
+
previous state for 1.7 s - a dashed ghost, up/down segments (`--kpt-chart-delta-up` / `--kpt-chart-delta-down`)
|
|
61
|
+
and the difference above points that changed by 2% or more - and the value axis waits for the ghost to
|
|
62
|
+
fade before zooming to the new data. The tooltip, the crosshair and the category pill travel between
|
|
63
|
+
categories, the digits roll on reels and the category name slides in from the direction of travel.
|
|
64
|
+
`animation.update: false`, `animation.delta: false` and `tooltip.motion: false` switch the parts off;
|
|
65
|
+
`animation.enabled: false`, `<KptMotionProvider enabled={false}>` and the system "reduce motion" setting
|
|
66
|
+
switch off all code-driven motion. The frame loop and its numbers live in `@konce-pt/chart`
|
|
67
|
+
(`KptChartMotion`, `springStep`, `reelCells`), shared with the Angular port.
|
|
68
|
+
|
|
69
|
+
## Interaction is local state
|
|
70
|
+
Hidden series, the highlighted one, the tooltip, the active category and the motion frame are
|
|
71
|
+
`useState` - the config stays a plain object the caller owns, and nothing is written back into it.
|
|
72
|
+
|
|
73
|
+
## Examples
|
|
74
|
+
<KptChart config={{ type: 'line', labels: ['Jan', 'Feb'], series: [{ name: 'Sales', data: [12, 19] }] }} />
|
|
75
|
+
|
|
76
|
+
<KptChart
|
|
77
|
+
config={{
|
|
78
|
+
type: 'doughnut',
|
|
79
|
+
innerRadius: 0.62,
|
|
80
|
+
labels: ['Done', 'In progress'],
|
|
81
|
+
series: [{ name: 'Tasks', data: [42, 15] }],
|
|
82
|
+
legend: { position: 'right' },
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
## Tokens
|
|
87
|
+
`--kpt-chart-{duration,stagger,delay}` for the animation; `--kpt-chart-sheen`,
|
|
88
|
+
`--kpt-chart-delta-up` and `--kpt-chart-delta-down` for the data update; series colours come from
|
|
89
|
+
`KPT_CHART_PALETTE` or `series.color`.
|
|
90
|
+
|
|
91
|
+
Axis text size: `--kpt-chart-font-size` on the chart (default `--kpt-font-size-sm`, 14px - the size of the
|
|
92
|
+
tooltip and the legend). The SVG text inherits it, and the component reads the computed value to derive
|
|
93
|
+
everything that sits next to the text - the margins for the labels, the gap between a label and its axis,
|
|
94
|
+
the crosshair pill, the label thinning (`chartMetrics` in `@konce-pt/chart`). Override it in CSS, the
|
|
95
|
+
layout follows: `.revenue .kpt-chart-host { --kpt-chart-font-size: 12px; }`.
|
|
96
|
+
|
|
97
|
+
## Accessibility
|
|
98
|
+
The `<svg>` is `role="img"` with a name: `config.ariaLabel`, else the title text, else `chart.label`
|
|
99
|
+
from i18n. `config.description` (one sentence with the takeaway) is tied to it through
|
|
100
|
+
`aria-describedby`. The data itself sits in a visually hidden `<table>` (categories in rows, series in
|
|
101
|
+
columns, numbers in the active language), so a screen reader can walk the values - the tooltip is
|
|
102
|
+
pointer-only. The legend is a list of real buttons, so series can be toggled from the keyboard.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { KptChart } from '../charts/chart.tsx';
|
|
2
2
|
export type { KptChartProps } from '../charts/chart.tsx';
|
|
3
|
-
export type { KptChartConfig, KptChartSeries, KptChartType, KptLineInterpolation, KptBarOrientation, KptLegendOptions, KptLegendPosition, KptLegendTitle, KptAlign, KptPointStyle, KptTitleOptions, KptTooltipOptions, KptTooltipContext, KptAnimationOptions, KptFont, KptPadding, } from '@konce-pt/chart';
|
|
3
|
+
export type { KptChartConfig, KptChartSeries, KptChartType, KptLineInterpolation, KptBarOrientation, KptLegendOptions, KptLegendPosition, KptLegendTitle, KptAlign, KptPointStyle, KptTitleOptions, KptTooltipOptions, KptTooltipContext, KptAnimationOptions, KptValueAxisOptions, KptFont, KptPadding, } from '@konce-pt/chart';
|
|
4
4
|
export { KPT_CHART_PALETTE, seriesColor, linePath, areaPath, pieSlices, polarSlices, barLayout, radarGrid, radarPoints, niceScale, } from '@konce-pt/chart';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@konce-pt/react",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"description": "Open-source React 19 component library on the Koncept UI design system - the same kpt-* CSS layer and --kpt-* tokens as the Angular port. Zero runtime dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "konce.pt",
|
|
@@ -65,19 +65,19 @@
|
|
|
65
65
|
"leaflet": "^1.9.0"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@konce-pt/backdrop": "0.18.
|
|
69
|
-
"@konce-pt/
|
|
70
|
-
"@konce-pt/
|
|
71
|
-
"@konce-pt/
|
|
72
|
-
"@konce-pt/
|
|
73
|
-
"@konce-pt/
|
|
74
|
-
"@konce-pt/
|
|
75
|
-
"@konce-pt/
|
|
76
|
-
"@konce-pt/
|
|
77
|
-
"@konce-pt/
|
|
78
|
-
"@konce-pt/table": "0.18.
|
|
79
|
-
"@konce-pt/
|
|
80
|
-
"@konce-pt/
|
|
68
|
+
"@konce-pt/backdrop": "0.18.2",
|
|
69
|
+
"@konce-pt/chat": "0.18.2",
|
|
70
|
+
"@konce-pt/datetime": "0.18.2",
|
|
71
|
+
"@konce-pt/mention": "0.18.2",
|
|
72
|
+
"@konce-pt/chart": "0.18.2",
|
|
73
|
+
"@konce-pt/motion": "0.18.2",
|
|
74
|
+
"@konce-pt/i18n": "0.18.2",
|
|
75
|
+
"@konce-pt/roadmap": "0.18.2",
|
|
76
|
+
"@konce-pt/icons": "0.18.2",
|
|
77
|
+
"@konce-pt/speech": "0.18.2",
|
|
78
|
+
"@konce-pt/table": "0.18.2",
|
|
79
|
+
"@konce-pt/map": "0.18.2",
|
|
80
|
+
"@konce-pt/tokens": "0.18.2"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@types/react": "^19.2.0",
|