@konce-pt/react 0.18.1 → 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.
@@ -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
- /** Szerokość viewBoxa w jednostkach SVG; wykres skaluje się do kontenera. */
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ść viewBoxa w jednostkach SVG. */
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;
@@ -1,20 +1,18 @@
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, axisLabelIndices, bandPath, bandScale, barLayout, chartScale, chartAccessibleName, chartDataTable, 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';
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
- /** Wysokość linii etykiety kategorii na osi pionowej (słupki poziome) - jednostki viewBoxa. */
11
- const CATEGORY_LINE = 12;
12
10
  /**
13
11
  * Etykiety osi poziomej: wszystkie, jeśli się mieszczą, inaczej równy podział 9/5/3/2
14
12
  * (`axisLabelIndices`). Pełna nazwa każdej kategorii zostaje w pigułce prowadnicy i w dymku.
15
13
  */
16
- function bottomLabels(labels, centers, y, width) {
17
- const sizes = labels.map((l) => estimateLabelWidth(l));
14
+ function bottomLabels(labels, centers, y, width, m) {
15
+ const sizes = labels.map((l) => estimateLabelWidth(l, m.fontSize));
18
16
  const shown = new Set(axisLabelIndices(centers, sizes));
19
17
  return labels.map((text, i) => ({
20
18
  at: centers[i] ?? 0,
@@ -26,12 +24,10 @@ function bottomLabels(labels, centers, y, width) {
26
24
  }));
27
25
  }
28
26
  /** Etykiety osi pionowej (słupki poziome): ten sam podział, rozmiarem jest wysokość linii. */
29
- function sideLabels(labels, centers, x) {
30
- const shown = new Set(axisLabelIndices(centers, centers.map(() => CATEGORY_LINE), 2));
31
- return labels.map((text, i) => ({ at: centers[i] ?? 0, x, y: (centers[i] ?? 0) + 3, text, anchor: 'end', visible: shown.has(i) }));
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) }));
32
30
  }
33
- const VERTICAL_INSETS = { top: 12, right: 16, bottom: 28, left: 46 };
34
- const HORIZONTAL_INSETS = { top: 12, right: 16, bottom: 28, left: 76 };
35
31
  /** Szerokość pasma smugi światła (jednostki viewBoxa) - połowa po każdej stronie czoła fali. */
36
32
  const SHEEN_HALF = 90;
37
33
  /** Zjechanie z wykresu: dymek czeka chwilę, zanim zgaśnie (powrót kursora go zatrzymuje). */
@@ -72,13 +68,46 @@ function styleFor(align) {
72
68
  * @example
73
69
  * <KptChart config={{ type: 'line', labels: ['Sty', 'Lut'], series: [{ name: 'Sprzedaż', data: [12, 19] }] }} />
74
70
  */
75
- 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 }) {
76
72
  const { t, locale: activeLocale } = useKptMessages({ locale, dictionary });
77
73
  const descId = `${useId()}-desc`;
78
74
  // Alternatywa tekstowa (WCAG 1.1.1): nazwa SVG (`ariaLabel` -> tytuł -> i18n) i dane w ukrytej
79
75
  // wizualnie tabeli - dymek na najechanie jest dostępny tylko myszą.
80
76
  const accessibleName = chartAccessibleName(config, t('chart.label'));
81
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);
82
111
  const [hidden, setHidden] = useState(new Set());
83
112
  const [highlighted, setHighlighted] = useState(null);
84
113
  /** Indeks kategorii pod kursorem - prowadnica crosshair w wykresie liniowym. */
@@ -148,7 +177,7 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
148
177
  const sheen = (() => {
149
178
  if (!frame.sheen)
150
179
  return null;
151
- const plot = plotArea(width, height, horizontal ? HORIZONTAL_INSETS : VERTICAL_INSETS);
180
+ const plot = plotArea(width, height, horizontal ? m.horizontalInsets : m.verticalInsets);
152
181
  const start = horizontal ? plot.y : plot.x;
153
182
  const length = horizontal ? plot.height : plot.width;
154
183
  const mid = start - SHEEN_HALF + (length + 2 * SHEEN_HALF) * frame.sheen.progress;
@@ -236,7 +265,7 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
236
265
  : chartScale(rows, { valueAxis: view.valueAxis, stacked: view.stacked });
237
266
  // Stos: punkty serii na narastającej sumie, pasmo od linii serii pod nią.
238
267
  const stack = view.stacked ? stackSeries(rows) : null;
239
- const plot = plotArea(width, height, VERTICAL_INSETS);
268
+ const plot = plotArea(width, height, m.verticalInsets);
240
269
  const yScale = linearScale(scale.min, scale.max, plot.y + plot.height, plot.y);
241
270
  const count = view.labels.length;
242
271
  const xAt = (i) => count > 1 ? plot.x + (i / (count - 1)) * plot.width : plot.x + plot.width / 2;
@@ -252,12 +281,12 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
252
281
  y2: yScale(t),
253
282
  }));
254
283
  const valueLabels = scale.ticks.map((t) => ({
255
- x: plot.x - 8,
256
- y: yScale(t) + 3,
284
+ x: plot.x - m.sideGap,
285
+ y: yScale(t) + m.baselineShift,
257
286
  text: formatNumber(t),
258
287
  anchor: 'end',
259
288
  }));
260
- const catLabels = bottomLabels(view.labels, view.labels.map((_, i) => xAt(i)), plot.y + plot.height + 18, width);
289
+ const catLabels = bottomLabels(view.labels, view.labels.map((_, i) => xAt(i)), plot.y + plot.height + m.labelOffset, width, m);
261
290
  const catTicks = view.labels.map((_, i) => ({ x: xAt(i), y: plot.y + plot.height }));
262
291
  const series = active.map(({ s, i }, k) => ({
263
292
  key: `ser:${s.name}`,
@@ -277,7 +306,7 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
277
306
  const crosshairCat = config.crosshair && activeIndex !== null ? catLabels[activeIndex] : undefined;
278
307
  // Prowadnica i pigułka jadą razem z dymkiem.
279
308
  const crossX = crosshairCat ? (frame.tip?.x ?? crosshairCat.at) : 0;
280
- const pillWidth = crosshairCat ? Math.max(28, crosshairCat.text.length * 7 + 16) : 0;
309
+ const pillWidth = crosshairCat ? Math.max(m.pill.minWidth, estimateLabelWidth(crosshairCat.text, m.fontSize) + 2 * m.pill.padding) : 0;
281
310
  // Pigułka przy skrajnej kategorii przesuwa się do środka zamiast wyjść poza viewBox.
282
311
  const pillX = crosshairCat ? clampLabel(crossX, pillWidth, 0, width) : 0;
283
312
  /** Najbliższa kategoria w całym obszarze wykresu: wiersze wszystkich serii, dymek nad najwyższym punktem. */
@@ -299,7 +328,7 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
299
328
  if (moved && tipMotion())
300
329
  setRipple({ id: ++rippleId.current, x: top.pt.x, y: top.pt.y, color: top.s.color });
301
330
  };
302
- 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 - 13, width: pillWidth, height: 18, rx: 9 }), _jsx("text", { x: pillX, y: crosshairCat.y, textAnchor: "middle", children: crosshairCat.text })] })) : null, ghostState
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
303
332
  ? series.map((s) => {
304
333
  const prev = ghostState.prev[s.i] ?? [];
305
334
  const next = ghostState.next[s.i] ?? [];
@@ -334,7 +363,7 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
334
363
  const orientation = view.orientation ?? 'vertical';
335
364
  const vertical = orientation === 'vertical';
336
365
  const active = view.series.map((s, i) => ({ s, i })).filter(({ s }) => !hidden.has(`ser:${s.name}`));
337
- const plot = plotArea(width, height, vertical ? VERTICAL_INSETS : HORIZONTAL_INSETS);
366
+ const plot = plotArea(width, height, vertical ? m.verticalInsets : m.horizontalInsets);
338
367
  const radius = view.borderRadius ?? 4;
339
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 });
340
369
  const colors = active.map(({ s, i }) => seriesColor(s.color, i));
@@ -346,17 +375,17 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
346
375
  ? layout.ticks.map((t) => ({ x1: plot.x, y1: t.pos, x2: plot.x + plot.width, y2: t.pos }))
347
376
  : layout.ticks.map((t) => ({ x1: t.pos, y1: plot.y, x2: t.pos, y2: plot.y + plot.height }));
348
377
  const valueLabels = vertical
349
- ? layout.ticks.map((t) => ({ x: plot.x - 8, y: t.pos + 3, text: formatNumber(t.value), anchor: 'end' }))
378
+ ? layout.ticks.map((t) => ({ x: plot.x - m.sideGap, y: t.pos + m.baselineShift, text: formatNumber(t.value), anchor: 'end' }))
350
379
  : layout.ticks.map((t) => ({
351
380
  x: t.pos,
352
- y: plot.y + plot.height + 18,
381
+ y: plot.y + plot.height + m.labelOffset,
353
382
  text: formatNumber(t.value),
354
383
  anchor: 'middle',
355
384
  }));
356
385
  const centers = view.labels.map((_, i) => cat.center(i));
357
386
  const catLabels = vertical
358
- ? bottomLabels(view.labels, centers, plot.y + plot.height + 18, width)
359
- : sideLabels(view.labels, centers, plot.x - 8);
387
+ ? bottomLabels(view.labels, centers, plot.y + plot.height + m.labelOffset, width, m)
388
+ : sideLabels(view.labels, centers, plot.x - m.sideGap, m);
360
389
  const bars = layout.bars.map((b, order) => ({
361
390
  b,
362
391
  order,
@@ -416,7 +445,7 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
416
445
  const first = view.series[0];
417
446
  if (!first)
418
447
  return null;
419
- const { cx, cy, radius } = radialArea(width, height, 8);
448
+ const { cx, cy, radius } = radialArea(width, height, m.radialPadding.plain);
420
449
  const entries = config.labels
421
450
  .map((label, i) => ({ label, i, value: first.data[i] ?? 0, color: seriesColor(first.color, i) }))
422
451
  .filter((entry) => !hidden.has(`cat:${entry.i}`));
@@ -438,7 +467,7 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
438
467
  const radarVm = (() => {
439
468
  if (config.type !== 'radar')
440
469
  return null;
441
- const { cx, cy, radius } = radialArea(width, height, 30);
470
+ const { cx, cy, radius } = radialArea(width, height, m.radialPadding.labelled);
442
471
  const grid = radarGrid(config.labels.length, cx, cy, radius, 4);
443
472
  const active = view.series.map((s, i) => ({ s, i })).filter(({ s }) => !hidden.has(`ser:${s.name}`));
444
473
  let max = 1;
@@ -446,9 +475,9 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
446
475
  for (const v of s.data)
447
476
  max = Math.max(max, v);
448
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) => {
449
- const p = polarToCartesian(cx, cy, radius + 14, axis.angle);
478
+ const p = polarToCartesian(cx, cy, radius + m.radarLabelGap, axis.angle);
450
479
  const anchor = Math.abs(p.x - cx) < 1 ? 'middle' : p.x > cx ? 'start' : 'end';
451
- return (_jsx("text", { x: p.x, y: p.y + 3, textAnchor: anchor, children: config.labels[axis.labelIndex] ?? '' }, i));
480
+ return (_jsx("text", { x: p.x, y: p.y + m.baselineShift, textAnchor: anchor, children: config.labels[axis.labelIndex] ?? '' }, i));
452
481
  }) }), active.map(({ s, i }) => {
453
482
  const key = `ser:${s.name}`;
454
483
  const color = seriesColor(s.color, i);
@@ -477,5 +506,5 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
477
506
  fontWeight: config.subtitle.font?.weight,
478
507
  paddingTop: config.subtitle.padding?.top,
479
508
  paddingBottom: config.subtitle.padding?.bottom,
480
- }, 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] })] }) }));
481
510
  }
@@ -1,95 +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 viewBox in SVG units (600 x 360); the chart scales to its container
12
-
13
- The whole drawing lives in that coordinate system, TYPOGRAPHY INCLUDED: the axis labels are set in
14
- `font-size: 11px`, but inside an SVG that means 11 user units, not 11 device pixels. Render a
15
- 600-unit chart in a 300px column and the labels come out at roughly half size. `width`/`height` are
16
- therefore not just an aspect ratio - they are the text scale. In a narrow container shrink them
17
- proportionally (`width={300} height={180}`) instead of letting a 600-unit chart squeeze itself.
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 viewBox units, so it follows `width`, not the on-screen size; the outer labels and the
40
- crosshair pill are shifted inward so they never leave the chart
41
- - A line chart marks every category with a small dot on the axis; the active one grows on hover. The full
42
- name of any category stays in the crosshair pill and the tooltip
43
- - The rules live in `@konce-pt/chart`: `axisLabelIndices`, `estimateLabelWidth`, `clampLabel` (tested)
44
-
45
- ## The geometry lives in a package
46
- Scales, paths, bar layout and arcs come from `@konce-pt/chart` - pure functions with `node --test`
47
- coverage, shared with the Angular port. The component only turns them into SVG elements; there is
48
- no DOM manipulation and no canvas.
49
-
50
- ## Hover covers the whole plot
51
- Line and bar charts listen on an invisible surface over the plot: the pointer anywhere picks the nearest
52
- category, the tooltip lists every visible series there and travels from category to category without
53
- fading out in between. Slices and radar points react to the element itself. The bubble never leaves the
54
- chart - it is clamped by its real width.
55
-
56
- ## Motion: data updates and the tooltip
57
- A new `config` with the same type, categories and series names is a data update: the values travel as a
58
- wave from left to right on springs, with a light sheen at the wave front. Line and bar charts keep the
59
- previous state for 1.7 s - a dashed ghost, up/down segments (`--kpt-chart-delta-up` / `--kpt-chart-delta-down`)
60
- and the difference above points that changed by 2% or more - and the value axis waits for the ghost to
61
- fade before zooming to the new data. The tooltip, the crosshair and the category pill travel between
62
- categories, the digits roll on reels and the category name slides in from the direction of travel.
63
- `animation.update: false`, `animation.delta: false` and `tooltip.motion: false` switch the parts off;
64
- `animation.enabled: false`, `<KptMotionProvider enabled={false}>` and the system "reduce motion" setting
65
- switch off all code-driven motion. The frame loop and its numbers live in `@konce-pt/chart`
66
- (`KptChartMotion`, `springStep`, `reelCells`), shared with the Angular port.
67
-
68
- ## Interaction is local state
69
- Hidden series, the highlighted one, the tooltip, the active category and the motion frame are
70
- `useState` - the config stays a plain object the caller owns, and nothing is written back into it.
71
-
72
- ## Examples
73
- <KptChart config={{ type: 'line', labels: ['Jan', 'Feb'], series: [{ name: 'Sales', data: [12, 19] }] }} />
74
-
75
- <KptChart
76
- config={{
77
- type: 'doughnut',
78
- innerRadius: 0.62,
79
- labels: ['Done', 'In progress'],
80
- series: [{ name: 'Tasks', data: [42, 15] }],
81
- legend: { position: 'right' },
82
- }}
83
- />
84
-
85
- ## Tokens
86
- `--kpt-chart-{duration,stagger,delay}` for the animation; `--kpt-chart-sheen`,
87
- `--kpt-chart-delta-up` and `--kpt-chart-delta-down` for the data update; series colours come from
88
- `KPT_CHART_PALETTE` or `series.color`.
89
-
90
- ## Accessibility
91
- The `<svg>` is `role="img"` with a name: `config.ariaLabel`, else the title text, else `chart.label`
92
- from i18n. `config.description` (one sentence with the takeaway) is tied to it through
93
- `aria-describedby`. The data itself sits in a visually hidden `<table>` (categories in rows, series in
94
- columns, numbers in the active language), so a screen reader can walk the values - the tooltip is
95
- pointer-only. The legend is a list of real buttons, so series can be toggled from the keyboard.
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@konce-pt/react",
3
- "version": "0.18.1",
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.1",
69
- "@konce-pt/icons": "0.18.1",
70
- "@konce-pt/chart": "0.18.1",
71
- "@konce-pt/roadmap": "0.18.1",
72
- "@konce-pt/mention": "0.18.1",
73
- "@konce-pt/speech": "0.18.1",
74
- "@konce-pt/motion": "0.18.1",
75
- "@konce-pt/i18n": "0.18.1",
76
- "@konce-pt/map": "0.18.1",
77
- "@konce-pt/chat": "0.18.1",
78
- "@konce-pt/tokens": "0.18.1",
79
- "@konce-pt/datetime": "0.18.1",
80
- "@konce-pt/table": "0.18.1"
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",