@pushwoosh/kit-charts 0.0.1

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.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/ai-documentation.md +378 -0
  3. package/core/Chart/Axes.d.ts +19 -0
  4. package/core/Chart/Axes.js +204 -0
  5. package/core/Chart/Chart.d.ts +33 -0
  6. package/core/Chart/Chart.js +616 -0
  7. package/core/Chart/Chart.styles.d.ts +109 -0
  8. package/core/Chart/Chart.styles.js +227 -0
  9. package/core/Chart/ChartErrorBoundary.d.ts +15 -0
  10. package/core/Chart/ChartErrorBoundary.js +27 -0
  11. package/core/Chart/Hinted.d.ts +11 -0
  12. package/core/Chart/Hinted.js +63 -0
  13. package/core/Chart/Legend.d.ts +15 -0
  14. package/core/Chart/Legend.js +92 -0
  15. package/core/Chart/PlotDecorations.d.ts +17 -0
  16. package/core/Chart/PlotDecorations.js +172 -0
  17. package/core/Chart/StackLabels.d.ts +6 -0
  18. package/core/Chart/StackLabels.js +85 -0
  19. package/core/Chart/Titles.d.ts +6 -0
  20. package/core/Chart/Titles.js +67 -0
  21. package/core/Chart/Tooltip.d.ts +13 -0
  22. package/core/Chart/Tooltip.js +87 -0
  23. package/core/Chart/useSize.d.ts +5 -0
  24. package/core/Chart/useSize.js +45 -0
  25. package/core/animation/motion.d.ts +20 -0
  26. package/core/animation/motion.js +20 -0
  27. package/core/animation/useTweenedNumbers.d.ts +1 -0
  28. package/core/animation/useTweenedNumbers.js +51 -0
  29. package/core/data/points.d.ts +14 -0
  30. package/core/data/points.js +73 -0
  31. package/core/format/time.d.ts +2 -0
  32. package/core/format/time.js +38 -0
  33. package/core/geometry/path.d.ts +19 -0
  34. package/core/geometry/path.js +76 -0
  35. package/core/layout/legend-layout.d.ts +21 -0
  36. package/core/layout/legend-layout.js +103 -0
  37. package/core/layout/measure-text.d.ts +9 -0
  38. package/core/layout/measure-text.js +57 -0
  39. package/core/layout/metrics.d.ts +63 -0
  40. package/core/layout/metrics.js +66 -0
  41. package/core/layout/point-width.d.ts +11 -0
  42. package/core/layout/point-width.js +86 -0
  43. package/core/layout/title-layout.d.ts +30 -0
  44. package/core/layout/title-layout.js +99 -0
  45. package/core/model/build-model.d.ts +69 -0
  46. package/core/model/build-model.js +556 -0
  47. package/core/model/palette.d.ts +15 -0
  48. package/core/model/palette.js +17 -0
  49. package/core/options/normalize.d.ts +8 -0
  50. package/core/options/normalize.js +44 -0
  51. package/core/registry/registry.d.ts +5 -0
  52. package/core/registry/registry.js +32 -0
  53. package/core/scale/band.d.ts +10 -0
  54. package/core/scale/band.js +33 -0
  55. package/core/scale/color.d.ts +9 -0
  56. package/core/scale/color.js +69 -0
  57. package/core/scale/linear.d.ts +17 -0
  58. package/core/scale/linear.js +64 -0
  59. package/core/scale/time.d.ts +19 -0
  60. package/core/scale/time.js +187 -0
  61. package/core/stack/stack.d.ts +9 -0
  62. package/core/stack/stack.js +76 -0
  63. package/core/types/formatter.d.ts +59 -0
  64. package/core/types/formatter.js +1 -0
  65. package/core/types/geometry.d.ts +16 -0
  66. package/core/types/geometry.js +1 -0
  67. package/core/types/options.d.ts +331 -0
  68. package/core/types/options.js +2 -0
  69. package/core/types/renderer.d.ts +101 -0
  70. package/core/types/renderer.js +1 -0
  71. package/core/types/scale.d.ts +34 -0
  72. package/core/types/scale.js +1 -0
  73. package/index.d.ts +16 -0
  74. package/index.js +10 -0
  75. package/package.json +64 -0
  76. package/presets/AreaChart.d.ts +3 -0
  77. package/presets/AreaChart.js +13 -0
  78. package/presets/ColumnChart.d.ts +3 -0
  79. package/presets/ColumnChart.js +13 -0
  80. package/presets/LineChart.d.ts +3 -0
  81. package/presets/LineChart.js +13 -0
  82. package/presets/PieChart.d.ts +3 -0
  83. package/presets/PieChart.js +13 -0
  84. package/presets/StackedColumnChart.d.ts +7 -0
  85. package/presets/StackedColumnChart.js +31 -0
  86. package/presets/index.d.ts +5 -0
  87. package/presets/index.js +5 -0
  88. package/series/area/area.d.ts +2 -0
  89. package/series/area/area.js +57 -0
  90. package/series/area/index.d.ts +1 -0
  91. package/series/area/index.js +1 -0
  92. package/series/column/column.d.ts +2 -0
  93. package/series/column/column.js +163 -0
  94. package/series/column/index.d.ts +1 -0
  95. package/series/column/index.js +1 -0
  96. package/series/heatmap/heatmap.d.ts +2 -0
  97. package/series/heatmap/heatmap.js +172 -0
  98. package/series/heatmap/index.d.ts +1 -0
  99. package/series/heatmap/index.js +1 -0
  100. package/series/line/index.d.ts +1 -0
  101. package/series/line/index.js +1 -0
  102. package/series/line/line.d.ts +2 -0
  103. package/series/line/line.js +49 -0
  104. package/series/pie/index.d.ts +1 -0
  105. package/series/pie/index.js +1 -0
  106. package/series/pie/pie.d.ts +2 -0
  107. package/series/pie/pie.js +224 -0
  108. package/series/shared/DataLabels.d.ts +14 -0
  109. package/series/shared/DataLabels.js +124 -0
  110. package/series/shared/Markers.d.ts +9 -0
  111. package/series/shared/Markers.js +34 -0
  112. package/series/shared/color.d.ts +1 -0
  113. package/series/shared/color.js +19 -0
  114. package/series/shared/domain.d.ts +3 -0
  115. package/series/shared/domain.js +14 -0
  116. package/series/shared/hit.d.ts +3 -0
  117. package/series/shared/hit.js +33 -0
  118. package/series/shared/legend.d.ts +3 -0
  119. package/series/shared/legend.js +12 -0
@@ -0,0 +1,556 @@
1
+ import { colorAt, DEFAULT_COLORS, DEFAULT_SURFACE_COLOR } from './palette';
2
+ import { normalizePoints } from '../data/points';
3
+ import { applyValueFormat, formatTime } from '../format/time';
4
+ import { measureLegend } from '../layout/legend-layout';
5
+ import { measureText, textHeight } from '../layout/measure-text';
6
+ import { AXIS_GUTTER_PADDING, AXIS_LABEL_GAP, AXIS_TITLE_GAP, MARKER_REACH, VALUE_LABEL_HEADROOM, X_AXIS_LABEL_DROP } from '../layout/metrics';
7
+ import { computeSlots } from '../layout/point-width';
8
+ import { layoutTitles } from '../layout/title-layout';
9
+ import { resolveAxes, resolveSeries, isSeriesVisible } from '../options/normalize';
10
+ import { createBandScale } from '../scale/band';
11
+ import { createColorScale } from '../scale/color';
12
+ import { createLinearScale } from '../scale/linear';
13
+ import { createTimeScale, defaultTimeFormat } from '../scale/time';
14
+ import { computeStacks } from '../stack/stack';
15
+ /** Key for a switched-off series, or for one of its points. */
16
+ export const hiddenKey = (seriesIndex, pointIndex) => pointIndex === undefined ? `${seriesIndex}` : `${seriesIndex}:${pointIndex}`;
17
+ const DEFAULT_SPACING = [10, 10, 15, 10];
18
+ const isBand = scale => scale.kind === 'band';
19
+ const isContinuous = scale => scale.kind === 'linear' || scale.kind === 'time';
20
+ function tickLabel(value, axis, timePattern, index, count, kind, numberFormat) {
21
+ var _axis$labels, _axis$labels2;
22
+ const formatter = (_axis$labels = axis.labels) === null || _axis$labels === void 0 ? void 0 : _axis$labels.formatter;
23
+ if (formatter) {
24
+ const node = formatter({
25
+ value,
26
+ index,
27
+ isFirst: index === 0,
28
+ isLast: index === count - 1,
29
+ axis: {
30
+ kind: kind,
31
+ min: undefined,
32
+ max: undefined
33
+ }
34
+ });
35
+ if (typeof node === 'string' || typeof node === 'number') {
36
+ return String(node);
37
+ }
38
+ return '';
39
+ }
40
+ if ((_axis$labels2 = axis.labels) !== null && _axis$labels2 !== void 0 && _axis$labels2.format) {
41
+ return applyValueFormat(axis.labels.format, value);
42
+ }
43
+ if (typeof value === 'number') {
44
+ return timePattern ? formatTime(timePattern, value) : numberFormat(value);
45
+ }
46
+ return String(value);
47
+ }
48
+ function collectCategories(axis, series, numeric = false) {
49
+ var _axis$categories;
50
+ if ((_axis$categories = axis.categories) !== null && _axis$categories !== void 0 && _axis$categories.length) {
51
+ return [...axis.categories];
52
+ }
53
+ const seen = [];
54
+ series.forEach(({
55
+ points
56
+ }) => {
57
+ points.forEach(point => {
58
+ if (typeof point.x !== 'string' && !numeric) {
59
+ return;
60
+ }
61
+ const label = String(point.x);
62
+ if (!seen.includes(label)) {
63
+ seen.push(label);
64
+ }
65
+ });
66
+ });
67
+ return seen;
68
+ }
69
+ const emptyExtent = () => ({
70
+ min: Infinity,
71
+ max: -Infinity
72
+ });
73
+ const grow = (extent, value) => {
74
+ if (value === null || !Number.isFinite(value)) {
75
+ return;
76
+ }
77
+ extent.min = Math.min(extent.min, value);
78
+ extent.max = Math.max(extent.max, value);
79
+ };
80
+ const settled = (extent, fallback) => Number.isFinite(extent.min) && Number.isFinite(extent.max) ? [extent.min, extent.max] : fallback;
81
+ function buildXScale(axis, kind, extent, categories, range, hint) {
82
+ if (kind === 'band') {
83
+ return createBandScale({
84
+ domain: categories,
85
+ range,
86
+ reversed: axis.reversed === true
87
+ });
88
+ }
89
+ const [min, max] = settled(extent, kind === 'time' ? [0, 1] : [0, 1]);
90
+ const domain = [axis.min ?? min, axis.max ?? max];
91
+ if (kind === 'time') {
92
+ return createTimeScale({
93
+ domain,
94
+ range,
95
+ tickHint: hint,
96
+ tickUnit: typeof axis.tickInterval === 'string' ? axis.tickInterval : undefined
97
+ });
98
+ }
99
+ return createLinearScale({
100
+ domain,
101
+ range,
102
+ tickHint: hint,
103
+ allowDecimals: axis.allowDecimals
104
+ });
105
+ }
106
+ function buildYScale(axis, extent, range, hint, linked) {
107
+ var _axis$categories2;
108
+ if ((_axis$categories2 = axis.categories) !== null && _axis$categories2 !== void 0 && _axis$categories2.length) {
109
+ return createBandScale({
110
+ domain: [...axis.categories],
111
+ range: (linked === null || linked === void 0 ? void 0 : linked.kind) === 'band' ? linked.range() : range,
112
+ paddingInner: 0,
113
+ paddingOuter: 0,
114
+ // A y axis counts up from the bottom; `reversed` puts the first row on top.
115
+ reversed: axis.reversed !== true
116
+ });
117
+ }
118
+ if (linked) {
119
+ return linked;
120
+ }
121
+ const [min, max] = settled(extent, [0, 1]);
122
+ return createLinearScale({
123
+ domain: [axis.min ?? min, axis.max ?? max],
124
+ range,
125
+ // Only the end the caller left open may be rounded outwards; an explicit
126
+ // `min: 0` must not cost the other end its headroom for data labels.
127
+ nice: {
128
+ min: axis.min === undefined,
129
+ max: axis.max === undefined
130
+ },
131
+ tickHint: hint,
132
+ allowDecimals: axis.allowDecimals
133
+ });
134
+ }
135
+ function buildColorScale(options, extent, colors) {
136
+ var _axis$stops;
137
+ const axis = options.colorAxis;
138
+ if (!axis) {
139
+ return undefined;
140
+ }
141
+ const [min, max] = settled(extent, [0, 1]);
142
+ const stops = (_axis$stops = axis.stops) !== null && _axis$stops !== void 0 && _axis$stops.length ? axis.stops : [[0, DEFAULT_SURFACE_COLOR], [1, colorAt(colors, 0)]];
143
+ return createColorScale({
144
+ domain: [axis.min ?? min, axis.max ?? max],
145
+ stops
146
+ });
147
+ }
148
+ const EXPLICIT_ANCHOR = {
149
+ left: 'start',
150
+ center: 'middle',
151
+ right: 'end'
152
+ };
153
+ function tickAnchor(label, position, axis, bounds) {
154
+ var _axis$labels3, _axis$labels4;
155
+ const explicit = ((_axis$labels3 = axis.labels) === null || _axis$labels3 === void 0 ? void 0 : _axis$labels3.align) && EXPLICIT_ANCHOR[axis.labels.align];
156
+ if (explicit) {
157
+ return explicit;
158
+ }
159
+ if (!bounds || !label) {
160
+ return 'middle';
161
+ }
162
+ const half = measureText(label, (_axis$labels4 = axis.labels) === null || _axis$labels4 === void 0 ? void 0 : _axis$labels4.style) / 2;
163
+ if (position - half < bounds[0]) {
164
+ return 'start';
165
+ }
166
+ if (position + half > bounds[1]) {
167
+ return 'end';
168
+ }
169
+ return 'middle';
170
+ }
171
+ /**
172
+ * Blanks the label of any tick whose text would run into the last one kept.
173
+ * Highcharts hides the text and leaves the tick, which is what keeps a dense
174
+ * axis readable instead of turning it into a smear.
175
+ */
176
+ function dropOverlapping(ticks, axis, along) {
177
+ var _axis$labels5, _axis$labels6;
178
+ if (((_axis$labels5 = axis.labels) === null || _axis$labels5 === void 0 ? void 0 : _axis$labels5.allowOverlap) === true || ticks.length < 2) {
179
+ return ticks;
180
+ }
181
+ const style = (_axis$labels6 = axis.labels) === null || _axis$labels6 === void 0 ? void 0 : _axis$labels6.style;
182
+ // A y axis counts upwards, so its tick positions descend. Comparing distance
183
+ // rather than order keeps one rule for both directions.
184
+ let kept = null;
185
+ return ticks.map(tick => {
186
+ if (!tick.label) {
187
+ return tick;
188
+ }
189
+ const half = (along === 'x' ? measureText(tick.label, style) : textHeight(style)) / 2;
190
+ if (kept && Math.abs(tick.position - kept.position) < half + kept.half + AXIS_LABEL_GAP) {
191
+ return {
192
+ ...tick,
193
+ label: ''
194
+ };
195
+ }
196
+ kept = {
197
+ position: tick.position,
198
+ half
199
+ };
200
+ return tick;
201
+ });
202
+ }
203
+ function axisTicks(axis, scale, hint, timePattern, along, numberFormat, bounds) {
204
+ var _axis$tickPositions;
205
+ let values;
206
+ if ((_axis$tickPositions = axis.tickPositions) !== null && _axis$tickPositions !== void 0 && _axis$tickPositions.length) {
207
+ values = [...axis.tickPositions];
208
+ } else if (isBand(scale)) {
209
+ values = scale.ticks();
210
+ } else if (typeof axis.tickInterval === 'number' && isContinuous(scale)) {
211
+ const [min, max] = scale.domain();
212
+ values = [];
213
+ for (let v = Math.ceil(min / axis.tickInterval) * axis.tickInterval; v <= max && values.length < 1000; v += axis.tickInterval) {
214
+ values.push(v);
215
+ }
216
+ } else {
217
+ values = isContinuous(scale) ? scale.ticks(hint) : [];
218
+ }
219
+ const offset = isBand(scale) ? scale.bandwidth() / 2 : 0;
220
+ const built = values.map((value, index) => {
221
+ var _axis$labels7, _axis$labels7$hint;
222
+ const position = (isBand(scale) ? scale.map(String(value)) : scale.map(Number(value))) + offset;
223
+ const label = tickLabel(value, axis, timePattern, index, values.length, scale.kind, numberFormat);
224
+ return {
225
+ value,
226
+ position,
227
+ label,
228
+ anchor: tickAnchor(label, position, axis, bounds),
229
+ hint: (_axis$labels7 = axis.labels) === null || _axis$labels7 === void 0 || (_axis$labels7$hint = _axis$labels7.hint) === null || _axis$labels7$hint === void 0 ? void 0 : _axis$labels7$hint.call(_axis$labels7, {
230
+ value,
231
+ index,
232
+ isFirst: index === 0,
233
+ isLast: index === values.length - 1,
234
+ axis: {
235
+ kind: scale.kind,
236
+ min: undefined,
237
+ max: undefined
238
+ }
239
+ })
240
+ };
241
+ });
242
+ return dropOverlapping(built, axis, along);
243
+ }
244
+ function halfStepPadding(series) {
245
+ const xs = new Set();
246
+ series.forEach(({
247
+ renderer,
248
+ points
249
+ }) => {
250
+ if (renderer.scales.x !== 'band') {
251
+ return;
252
+ }
253
+ points.forEach(point => {
254
+ if (typeof point.x === 'number' && Number.isFinite(point.x)) {
255
+ xs.add(point.x);
256
+ }
257
+ });
258
+ });
259
+ if (xs.size < 2) {
260
+ return 0;
261
+ }
262
+ const sorted = [...xs].sort((left, right) => left - right);
263
+ let step = Infinity;
264
+ for (let i = 1; i < sorted.length; i += 1) {
265
+ step = Math.min(step, sorted[i] - sorted[i - 1]);
266
+ }
267
+ return Number.isFinite(step) ? step / 2 : 0;
268
+ }
269
+ export function buildChartModel({
270
+ options,
271
+ size,
272
+ renderers,
273
+ hidden
274
+ }) {
275
+ var _options$colors, _xAxisOptions$categor, _resolved$, _options$chart, _xAxisOptions$labels, _xAxisOptions$labels2, _xAxisOptions$labels3, _xAxisOptions$labels4, _options$legend, _options$legend2, _yAxes$, _yAxes$2, _yAxes$3, _yAxes$4, _options$legend3, _options$chart2, _options$chart3, _options$chart4, _options$chart5;
276
+ const colors = (_options$colors = options.colors) !== null && _options$colors !== void 0 && _options$colors.length ? options.colors : DEFAULT_COLORS;
277
+ const numberFormat = options.numberFormat ?? String;
278
+ const {
279
+ xAxes,
280
+ yAxes
281
+ } = resolveAxes(options);
282
+ const xAxisOptions = xAxes[0] ?? {};
283
+ const all = [];
284
+ resolveSeries(options).forEach((series, index) => {
285
+ const renderer = renderers.get(series.type);
286
+ if (!renderer) {
287
+ return;
288
+ }
289
+ all.push({
290
+ options: series,
291
+ renderer,
292
+ index,
293
+ color: series.color ?? colorAt(colors, index),
294
+ points: normalizePoints(series.data, series.name ?? `series ${index}`),
295
+ hiddenPoints: new Set(normalizePoints(series.data, series.name ?? `series ${index}`).filter(point => hidden === null || hidden === void 0 ? void 0 : hidden.has(hiddenKey(index, point.index))).map(point => point.index)),
296
+ hidden: !isSeriesVisible(series) || ((hidden === null || hidden === void 0 ? void 0 : hidden.has(hiddenKey(index))) ?? false)
297
+ });
298
+ });
299
+ // Everything that shapes the chart — domains, stacks, column slots — comes from
300
+ // the series on show; a hidden one only keeps its place in the legend.
301
+ const shown = all.filter(series => !series.hidden);
302
+ // With nothing left on show the axes would collapse to their fallback range and
303
+ // the series being switched off would jump there before it finished fading, so
304
+ // the chart holds the shape it had until something is switched back on.
305
+ const resolved = shown.length ? shown : all;
306
+ const explicitCategories = Boolean((_xAxisOptions$categor = xAxisOptions.categories) === null || _xAxisOptions$categor === void 0 ? void 0 : _xAxisOptions$categor.length);
307
+ const axisless = resolved.length > 0 && resolved.every(({
308
+ renderer
309
+ }) => renderer.scales.x === 'none');
310
+ const labelled = collectCategories(xAxisOptions, resolved);
311
+ const rendererXKind = (_resolved$ = resolved[0]) === null || _resolved$ === void 0 ? void 0 : _resolved$.renderer.scales.x;
312
+ const xKind = (() => {
313
+ if (xAxisOptions.type === 'datetime') {
314
+ return 'time';
315
+ }
316
+ if (xAxisOptions.type === 'category' || labelled.length > 0) {
317
+ return 'band';
318
+ }
319
+ return rendererXKind === 'band' ? 'band' : 'linear';
320
+ })();
321
+ const categories = xKind === 'band' && !labelled.length ? collectCategories(xAxisOptions, resolved, true) : labelled;
322
+ const stacks = computeStacks(resolved);
323
+ if (xKind === 'band') {
324
+ resolved.forEach(({
325
+ points
326
+ }) => {
327
+ points.forEach(point => {
328
+ if (typeof point.x === 'string') {
329
+ point.band = point.x;
330
+ return;
331
+ }
332
+ if (!explicitCategories) {
333
+ point.band = String(point.x);
334
+ return;
335
+ }
336
+ point.band = categories[Number(point.x)] ?? categories[point.index] ?? String(point.x);
337
+ });
338
+ });
339
+ resolved.forEach(({
340
+ points
341
+ }) => {
342
+ points.forEach(point => {
343
+ if (point.band !== undefined && !categories.includes(point.band)) {
344
+ categories.push(point.band);
345
+ }
346
+ });
347
+ });
348
+ }
349
+ const xExtent = emptyExtent();
350
+ const colorExtent = emptyExtent();
351
+ const yExtents = yAxes.map(() => emptyExtent());
352
+ resolved.forEach(({
353
+ points,
354
+ options: series,
355
+ index,
356
+ hiddenPoints
357
+ }) => {
358
+ const yIndex = Math.min(series.yAxis ?? 0, yExtents.length - 1);
359
+ const stacked = stacks.get(index);
360
+ points.forEach((point, pointIndex) => {
361
+ if (hiddenPoints.has(point.index)) {
362
+ return;
363
+ }
364
+ if (typeof point.x === 'number') {
365
+ grow(xExtent, point.x);
366
+ }
367
+ grow(colorExtent, point.value);
368
+ const cumulative = stacked === null || stacked === void 0 ? void 0 : stacked[pointIndex];
369
+ if (cumulative) {
370
+ grow(yExtents[yIndex], cumulative.from);
371
+ grow(yExtents[yIndex], cumulative.to);
372
+ } else {
373
+ grow(yExtents[yIndex], point.y);
374
+ }
375
+ });
376
+ });
377
+ const seeded = {
378
+ x: {
379
+ ...xExtent
380
+ },
381
+ y: yExtents.map(extent => ({
382
+ ...extent
383
+ })),
384
+ color: {
385
+ ...colorExtent
386
+ },
387
+ categories
388
+ };
389
+ const acc = resolved.reduce((current, {
390
+ renderer,
391
+ options: series
392
+ }) => renderer.extendDomain(series, current), seeded);
393
+ const bandOnContinuous = xKind !== 'band' && resolved.some(({
394
+ renderer
395
+ }) => renderer.scales.x === 'band');
396
+ const pad = bandOnContinuous ? halfStepPadding(resolved) : 0;
397
+ const xDomain = pad > 0 ? {
398
+ min: acc.x.min - pad,
399
+ max: acc.x.max + pad
400
+ } : acc.x;
401
+ const yDomains = acc.y;
402
+ const spacing = ((_options$chart = options.chart) === null || _options$chart === void 0 ? void 0 : _options$chart.spacing) ?? DEFAULT_SPACING;
403
+ const [spaceTop, spaceRight, spaceBottom, spaceLeft] = spacing;
404
+ const labelsOnTop = xAxisOptions.opposite === true;
405
+ const labelStyle = (_xAxisOptions$labels = xAxisOptions.labels) === null || _xAxisOptions$labels === void 0 ? void 0 : _xAxisOptions$labels.style;
406
+ // A tick label is drawn a tick's length plus a fixed drop away from the axis
407
+ // line, and `labels.y` pushes it further still. Reserving only its text height
408
+ // leaves it hanging over the chart edge, where the svg clips it.
409
+ const xLabelPush = labelsOnTop ? Math.max(0, -(((_xAxisOptions$labels2 = xAxisOptions.labels) === null || _xAxisOptions$labels2 === void 0 ? void 0 : _xAxisOptions$labels2.y) ?? 0)) : Math.max(0, ((_xAxisOptions$labels3 = xAxisOptions.labels) === null || _xAxisOptions$labels3 === void 0 ? void 0 : _xAxisOptions$labels3.y) ?? 0);
410
+ const xLabelHeight = axisless || ((_xAxisOptions$labels4 = xAxisOptions.labels) === null || _xAxisOptions$labels4 === void 0 ? void 0 : _xAxisOptions$labels4.enabled) === false ? 0 : textHeight(labelStyle) + (xAxisOptions.tickLength ?? 0) + (labelsOnTop ? AXIS_LABEL_GAP : X_AXIS_LABEL_DROP) + xLabelPush;
411
+ const legendEnabled = ((_options$legend = options.legend) === null || _options$legend === void 0 ? void 0 : _options$legend.enabled) !== false;
412
+ const legend = legendEnabled ? all.flatMap(({
413
+ renderer,
414
+ options: series,
415
+ index,
416
+ hidden: off
417
+ }) => {
418
+ const items = renderer.legendItems(series, index, colors);
419
+ return items.map((item, ordinal) => ({
420
+ ...item,
421
+ color: item.color || colorAt(colors, items.length > 1 ? ordinal : index),
422
+ visible: !off && !(hidden !== null && hidden !== void 0 && hidden.has(hiddenKey(index, item.pointIndex)))
423
+ }));
424
+ }) : [];
425
+ const legendMetrics = measureLegend(legend, options.legend);
426
+ const legendHeight = legendMetrics.height;
427
+ const legendAtTop = (((_options$legend2 = options.legend) === null || _options$legend2 === void 0 ? void 0 : _options$legend2.verticalAlign) ?? 'bottom') === 'top';
428
+ const titles = layoutTitles(options, size, spacing);
429
+ const titleHeight = titles.height;
430
+ const yLabelsShown = !axisless && ((_yAxes$ = yAxes[0]) === null || _yAxes$ === void 0 ? void 0 : _yAxes$.visible) !== false && ((_yAxes$2 = yAxes[0]) === null || _yAxes$2 === void 0 || (_yAxes$2 = _yAxes$2.labels) === null || _yAxes$2 === void 0 ? void 0 : _yAxes$2.enabled) !== false;
431
+ const markerReach = resolved.some(({
432
+ options: series
433
+ }) => {
434
+ var _series$marker;
435
+ return ((_series$marker = series.marker) === null || _series$marker === void 0 ? void 0 : _series$marker.enabled) !== false;
436
+ }) ? MARKER_REACH : 0;
437
+ // The topmost tick label is centred on the plot's top edge, so half of it hangs
438
+ // above the plot: without this the highest grid line and its label sit on the
439
+ // chart border.
440
+ const topOverflow = Math.max(yLabelsShown ? Math.ceil(textHeight((_yAxes$3 = yAxes[0]) === null || _yAxes$3 === void 0 || (_yAxes$3 = _yAxes$3.labels) === null || _yAxes$3 === void 0 ? void 0 : _yAxes$3.style) / 2) : 0, axisless ? 0 : markerReach);
441
+ // Point and stack labels are drawn above the shape they belong to, so the plot
442
+ // has to end below the chart edge — otherwise the topmost value sits on the
443
+ // border, or gets pushed inside the shape to avoid being cut off.
444
+ const outsideLabels = axisless ? undefined : resolved.find(({
445
+ options: series,
446
+ renderer
447
+ }) => {
448
+ var _series$dataLabels;
449
+ return ((_series$dataLabels = series.dataLabels) === null || _series$dataLabels === void 0 ? void 0 : _series$dataLabels.enabled) && !series.stacking && renderer.scales.y === 'linear';
450
+ });
451
+ const stackLabels = yAxes.find(axis => {
452
+ var _axis$stackLabels;
453
+ return (_axis$stackLabels = axis.stackLabels) === null || _axis$stackLabels === void 0 ? void 0 : _axis$stackLabels.enabled;
454
+ });
455
+ const labelHeadroom = (() => {
456
+ if (outsideLabels) {
457
+ var _outsideLabels$option;
458
+ return textHeight((_outsideLabels$option = outsideLabels.options.dataLabels) === null || _outsideLabels$option === void 0 ? void 0 : _outsideLabels$option.style) + VALUE_LABEL_HEADROOM;
459
+ }
460
+ if (stackLabels && !axisless) {
461
+ var _stackLabels$stackLab;
462
+ return textHeight((_stackLabels$stackLab = stackLabels.stackLabels) === null || _stackLabels$stackLab === void 0 ? void 0 : _stackLabels$stackLab.style) + VALUE_LABEL_HEADROOM;
463
+ }
464
+ return 0;
465
+ })();
466
+ const marginTop = spaceTop + titleHeight + (legendAtTop ? legendHeight : 0) + (labelsOnTop ? xLabelHeight : 0) + Math.max(labelHeadroom, topOverflow);
467
+ const marginBottom = spaceBottom + (labelsOnTop ? 0 : xLabelHeight) + (legendAtTop ? 0 : legendHeight);
468
+ const measureGutter = estimate => {
469
+ var _axis$labels8, _axis$categories3, _axis$title;
470
+ const axis = yAxes[0] ?? {};
471
+ if (((_axis$labels8 = axis.labels) === null || _axis$labels8 === void 0 ? void 0 : _axis$labels8.enabled) === false) {
472
+ return estimate;
473
+ }
474
+ const labels = (_axis$categories3 = axis.categories) !== null && _axis$categories3 !== void 0 && _axis$categories3.length ? [...axis.categories] : axisTicks(axis, createLinearScale({
475
+ domain: (() => {
476
+ const [min, max] = settled(yDomains[0] ?? emptyExtent(), [0, 1]);
477
+ return [axis.min ?? min, axis.max ?? max];
478
+ })(),
479
+ range: [size.height - marginBottom, marginTop],
480
+ nice: true,
481
+ tickHint: axis.tickAmount ?? 5,
482
+ allowDecimals: axis.allowDecimals
483
+ }), axis.tickAmount ?? 5, undefined, 'y', numberFormat).map(tick => tick.label);
484
+ const widest = labels.reduce((widest_, label) => {
485
+ var _axis$labels9;
486
+ return Math.max(widest_, measureText(label, (_axis$labels9 = axis.labels) === null || _axis$labels9 === void 0 ? void 0 : _axis$labels9.style));
487
+ }, 0);
488
+ const title = (_axis$title = axis.title) !== null && _axis$title !== void 0 && _axis$title.text && (axis.title.rotation ?? -90) !== 0 ? textHeight(axis.title.style) + AXIS_TITLE_GAP : 0;
489
+ return Math.ceil(widest) + AXIS_GUTTER_PADDING + title;
490
+ };
491
+ const gutter = axisless || ((_yAxes$4 = yAxes[0]) === null || _yAxes$4 === void 0 ? void 0 : _yAxes$4.visible) === false ? 0 : measureGutter(32);
492
+ const legendAlign = ((_options$legend3 = options.legend) === null || _options$legend3 === void 0 ? void 0 : _options$legend3.align) ?? 'left';
493
+ const legendBesideLeft = legendMetrics.beside && legendAlign === 'left';
494
+ // Highcharts margins replace the computed edge distance rather than adding to it.
495
+ const left = ((_options$chart2 = options.chart) === null || _options$chart2 === void 0 ? void 0 : _options$chart2.marginLeft) ?? spaceLeft + gutter + (legendBesideLeft ? legendMetrics.width : 0);
496
+ const right = ((_options$chart3 = options.chart) === null || _options$chart3 === void 0 ? void 0 : _options$chart3.marginRight) ?? spaceRight + (legendMetrics.beside && !legendBesideLeft ? legendMetrics.width : 0);
497
+ const top = ((_options$chart4 = options.chart) === null || _options$chart4 === void 0 ? void 0 : _options$chart4.marginTop) ?? marginTop;
498
+ const bottom = ((_options$chart5 = options.chart) === null || _options$chart5 === void 0 ? void 0 : _options$chart5.marginBottom) ?? marginBottom;
499
+ const plot = {
500
+ x: left,
501
+ y: top,
502
+ width: Math.max(0, size.width - left - right),
503
+ height: Math.max(0, size.height - top - bottom)
504
+ };
505
+ if (plot.width <= 0 || plot.height <= 0) {
506
+ return null;
507
+ }
508
+ const xHint = xAxisOptions.tickPixelInterval ? Math.max(2, Math.round(plot.width / xAxisOptions.tickPixelInterval)) : Math.max(2, Math.min(8, Math.round(plot.width / 90)));
509
+ const xScale = buildXScale(xAxisOptions, xKind, xDomain, categories, [plot.x, plot.x + plot.width], xHint);
510
+ const timePattern = xKind === 'time' && isContinuous(xScale) ? defaultTimeFormat(...xScale.domain()) : undefined;
511
+ const slots = computeSlots({
512
+ series: resolved.filter(({
513
+ renderer
514
+ }) => renderer.scales.x === 'band'),
515
+ scale: xScale,
516
+ plotWidth: plot.width,
517
+ stacks
518
+ });
519
+ const yAxisModels = yAxes.reduce((built, axis, index) => {
520
+ var _axis$categories4;
521
+ const hint = axis.tickAmount ?? 5;
522
+ const target = axis.linkedTo === undefined ? undefined : built[axis.linkedTo];
523
+ const range = (_axis$categories4 = axis.categories) !== null && _axis$categories4 !== void 0 && _axis$categories4.length ? [plot.y, plot.y + plot.height] : [plot.y + plot.height, plot.y];
524
+ const scale = buildYScale(axis, yDomains[index] ?? emptyExtent(), range, hint, target === null || target === void 0 ? void 0 : target.scale);
525
+ built.push({
526
+ id: `y${index}`,
527
+ options: axis,
528
+ scale,
529
+ ticks: axisTicks(axis, scale, hint, undefined, 'y', numberFormat)
530
+ });
531
+ return built;
532
+ }, []);
533
+ const colorScale = buildColorScale(options, acc.color, colors);
534
+ return {
535
+ numberFormat,
536
+ spacing,
537
+ plot,
538
+ axisless,
539
+ xAxis: {
540
+ id: 'x0',
541
+ options: xAxisOptions,
542
+ scale: xScale,
543
+ ticks: axisTicks(xAxisOptions, xScale, xHint, timePattern, 'x', numberFormat, [plot.x, plot.x + plot.width])
544
+ },
545
+ yAxes: yAxisModels,
546
+ titles: titles.entries,
547
+ colorScale,
548
+ series: all,
549
+ legend,
550
+ colors,
551
+ categories,
552
+ stacks,
553
+ slots
554
+ };
555
+ }
556
+ export { isBand, isContinuous };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Series palette, in the order series are drawn. These are the design system's
3
+ * service and brand colours rather than a set invented here, so a chart matches
4
+ * the product it sits in. The order alternates hue family — blue, orange, green,
5
+ * purple — so that neighbouring series stay apart from each other.
6
+ */
7
+ export declare const DEFAULT_COLORS: readonly string[];
8
+ export declare const DEFAULT_GRID_COLOR: string;
9
+ export declare const DEFAULT_AXIS_COLOR: string;
10
+ export declare const DEFAULT_TEXT_COLOR: string;
11
+ export declare const DEFAULT_MUTED_TEXT_COLOR: string;
12
+ export declare const DEFAULT_SURFACE_COLOR: string;
13
+ export declare const DEFAULT_TOOLTIP_SURFACE_COLOR: string;
14
+ export declare const DEFAULT_TOOLTIP_TEXT_COLOR: string;
15
+ export declare const colorAt: (colors: readonly string[], index: number) => string;
@@ -0,0 +1,17 @@
1
+ import { Color } from '@pushwoosh/kit-constants';
2
+ /**
3
+ * Series palette, in the order series are drawn. These are the design system's
4
+ * service and brand colours rather than a set invented here, so a chart matches
5
+ * the product it sits in. The order alternates hue family — blue, orange, green,
6
+ * purple — so that neighbouring series stay apart from each other.
7
+ */
8
+ export const DEFAULT_COLORS = [Color.PRIMARY, Color.WARNING, Color.SUCCESS, Color.EXCEPTIONAL, Color.BRIGHT, Color.DANGER, Color.BRAND_GREEN, Color.SOFT, Color.BRAND_BLUE, Color.BRAND_BLACK];
9
+ export const DEFAULT_GRID_COLOR = Color.DIVIDER;
10
+ export const DEFAULT_AXIS_COLOR = Color.FORM;
11
+ export const DEFAULT_TEXT_COLOR = Color.MAIN;
12
+ export const DEFAULT_MUTED_TEXT_COLOR = Color.LOCKED;
13
+ export const DEFAULT_SURFACE_COLOR = Color.CLEAR;
14
+ // Chart tooltips follow the product hint style: dark bubble, light text.
15
+ export const DEFAULT_TOOLTIP_SURFACE_COLOR = Color.BRAND_BLACK;
16
+ export const DEFAULT_TOOLTIP_TEXT_COLOR = Color.CLEAR;
17
+ export const colorAt = (colors, index) => colors[index % colors.length] ?? DEFAULT_COLORS[index % DEFAULT_COLORS.length];
@@ -0,0 +1,8 @@
1
+ import { type AxisOptions, type ChartOptions, type SeriesOptionsBase } from '../types/options';
2
+ export declare const toAxisArray: (axis: AxisOptions | readonly AxisOptions[] | undefined) => AxisOptions[];
3
+ export declare function resolveSeries(options: ChartOptions): SeriesOptionsBase[];
4
+ export declare function resolveAxes(options: ChartOptions): {
5
+ xAxes: AxisOptions[];
6
+ yAxes: AxisOptions[];
7
+ };
8
+ export declare const isSeriesVisible: (series: SeriesOptionsBase) => boolean;
@@ -0,0 +1,44 @@
1
+ function merge(...layers) {
2
+ const result = {};
3
+ layers.forEach(layer => {
4
+ if (!layer) {
5
+ return;
6
+ }
7
+ Object.entries(layer).forEach(([key, value]) => {
8
+ if (value === undefined) {
9
+ return;
10
+ }
11
+ const existing = result[key];
12
+ const bothPlain = isPlainObject(existing) && isPlainObject(value);
13
+ result[key] = bothPlain ? merge(existing, value) : value;
14
+ });
15
+ });
16
+ return result;
17
+ }
18
+ const isPlainObject = value => typeof value === 'object' && value !== null && !Array.isArray(value);
19
+ export const toAxisArray = axis => {
20
+ if (!axis) {
21
+ return [{}];
22
+ }
23
+ return Array.isArray(axis) ? [...axis] : [axis];
24
+ };
25
+ export function resolveSeries(options) {
26
+ var _options$chart;
27
+ const defaultType = ((_options$chart = options.chart) === null || _options$chart === void 0 ? void 0 : _options$chart.type) ?? 'line';
28
+ const plotOptions = options.plotOptions ?? {};
29
+ return (options.series ?? []).map(series => {
30
+ const type = series.type ?? defaultType;
31
+ return merge({
32
+ type
33
+ }, plotOptions.series, plotOptions[type], series, {
34
+ type
35
+ });
36
+ });
37
+ }
38
+ export function resolveAxes(options) {
39
+ return {
40
+ xAxes: toAxisArray(options.xAxis),
41
+ yAxes: toAxisArray(options.yAxis)
42
+ };
43
+ }
44
+ export const isSeriesVisible = series => series.visible !== false;
@@ -0,0 +1,5 @@
1
+ import { type AnySeriesRenderer } from '../types/renderer';
2
+ export declare function registerSeries(...renderers: AnySeriesRenderer[]): void;
3
+ export declare function clearSeriesRegistry(): void;
4
+ export declare function resolveRenderers(local?: readonly AnySeriesRenderer[]): ReadonlyMap<string, AnySeriesRenderer>;
5
+ export declare function getRenderer(renderers: ReadonlyMap<string, AnySeriesRenderer>, type: string): AnySeriesRenderer | undefined;
@@ -0,0 +1,32 @@
1
+ const globalRegistry = new Map();
2
+ const isDev = process.env.NODE_ENV !== 'production';
3
+ export function registerSeries(...renderers) {
4
+ renderers.forEach(renderer => {
5
+ const existing = globalRegistry.get(renderer.type);
6
+ if (existing === renderer) {
7
+ return;
8
+ }
9
+ if (existing && isDev) {
10
+ throw new Error(`[kit-charts] Two different renderers registered for series type "${renderer.type}".`);
11
+ }
12
+ globalRegistry.set(renderer.type, renderer);
13
+ });
14
+ }
15
+ export function clearSeriesRegistry() {
16
+ globalRegistry.clear();
17
+ }
18
+ export function resolveRenderers(local) {
19
+ if (!(local !== null && local !== void 0 && local.length)) {
20
+ return globalRegistry;
21
+ }
22
+ const resolved = new Map(globalRegistry);
23
+ local.forEach(renderer => resolved.set(renderer.type, renderer));
24
+ return resolved;
25
+ }
26
+ export function getRenderer(renderers, type) {
27
+ const renderer = renderers.get(type);
28
+ if (!renderer && isDev) {
29
+ console.error(`[kit-charts] No renderer for series type "${type}". ` + `Import it and call registerSeries: ` + `import { ${type} } from '@pushwoosh/kit-charts/series/${type}'.`);
30
+ }
31
+ return renderer;
32
+ }