@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,2 @@
1
+ export declare function formatTime(pattern: string, timestamp: number): string;
2
+ export declare function applyValueFormat(format: string, value: number | string): string;
@@ -0,0 +1,38 @@
1
+ const MONTHS_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
2
+ const MONTHS_LONG = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
3
+ const DAYS_SHORT = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
4
+ const DAYS_LONG = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
5
+ const pad = (value, length = 2) => String(value).padStart(length, '0');
6
+ const TOKENS = {
7
+ a: date => DAYS_SHORT[date.getUTCDay()],
8
+ A: date => DAYS_LONG[date.getUTCDay()],
9
+ b: date => MONTHS_SHORT[date.getUTCMonth()],
10
+ B: date => MONTHS_LONG[date.getUTCMonth()],
11
+ d: date => pad(date.getUTCDate()),
12
+ e: date => String(date.getUTCDate()).padStart(2, ' '),
13
+ m: date => pad(date.getUTCMonth() + 1),
14
+ y: date => pad(date.getUTCFullYear() % 100),
15
+ Y: date => String(date.getUTCFullYear()),
16
+ H: date => pad(date.getUTCHours()),
17
+ M: date => pad(date.getUTCMinutes()),
18
+ S: date => pad(date.getUTCSeconds()),
19
+ L: date => pad(date.getUTCMilliseconds(), 3)
20
+ };
21
+ export function formatTime(pattern, timestamp) {
22
+ if (!Number.isFinite(timestamp)) {
23
+ return '';
24
+ }
25
+ const date = new Date(timestamp);
26
+ return pattern.replace(/%(.)/g, (match, token) => {
27
+ const format = TOKENS[token];
28
+ return format ? format(date) : match;
29
+ });
30
+ }
31
+ export function applyValueFormat(format, value) {
32
+ return format.replace(/\{value(?::([^}]*))?\}/g, (_match, pattern) => {
33
+ if (pattern && typeof value === 'number') {
34
+ return formatTime(pattern, value);
35
+ }
36
+ return String(value);
37
+ });
38
+ }
@@ -0,0 +1,19 @@
1
+ import { type NormalizedPoint } from '../data/points';
2
+ import { type RenderContext } from '../types/renderer';
3
+ import { type ContinuousScale } from '../types/scale';
4
+ export interface Projected {
5
+ point: NormalizedPoint;
6
+ px: number;
7
+ py: number;
8
+ baseY: number;
9
+ }
10
+ export type ProjectedSlot = Projected | null;
11
+ export declare function projectX(scale: RenderContext['scales']['x'], point: NormalizedPoint): number;
12
+ /** Value renderers need numbers on y; a categorical y axis is a misconfiguration. */
13
+ export declare const continuousY: (ctx: RenderContext) => ContinuousScale | null;
14
+ export declare function baselineY(ctx: RenderContext): number;
15
+ export declare function projectSeries(ctx: RenderContext): ProjectedSlot[];
16
+ export declare const drawable: (slots: readonly ProjectedSlot[]) => Projected[];
17
+ export declare function runs(slots: readonly ProjectedSlot[]): Projected[][];
18
+ export declare function toLinePath(slots: readonly ProjectedSlot[]): string;
19
+ export declare function toAreaPath(slots: readonly ProjectedSlot[]): string;
@@ -0,0 +1,76 @@
1
+ export function projectX(scale, point) {
2
+ if (scale.kind === 'band') {
3
+ const band = scale;
4
+ return band.map(point.band ?? String(point.name ?? point.x)) + band.bandwidth() / 2;
5
+ }
6
+ return scale.map(Number(point.x));
7
+ }
8
+ /** Value renderers need numbers on y; a categorical y axis is a misconfiguration. */
9
+ export const continuousY = ctx => ctx.scales.y.kind === 'band' ? null : ctx.scales.y;
10
+ export function baselineY(ctx) {
11
+ const scale = continuousY(ctx);
12
+ const top = ctx.plot.y;
13
+ const bottom = ctx.plot.y + ctx.plot.height;
14
+ const zero = scale ? scale.map(0) : NaN;
15
+ if (!Number.isFinite(zero)) {
16
+ return bottom;
17
+ }
18
+ return Math.min(bottom, Math.max(top, zero));
19
+ }
20
+ export function projectSeries(ctx) {
21
+ const base = baselineY(ctx);
22
+ const yScale = continuousY(ctx);
23
+ if (!yScale) {
24
+ return [];
25
+ }
26
+ return ctx.points.map((point, index) => {
27
+ var _ctx$stacked;
28
+ const stacked = (_ctx$stacked = ctx.stacked) === null || _ctx$stacked === void 0 ? void 0 : _ctx$stacked[index];
29
+ if (point.y === null || ctx.hiddenPoints.has(point.index) || ctx.stacked && !stacked) {
30
+ return null;
31
+ }
32
+ const px = projectX(ctx.scales.x, point);
33
+ const py = yScale.map(stacked ? stacked.to : point.y);
34
+ const baseY = stacked ? yScale.map(stacked.from) : base;
35
+ if (!Number.isFinite(px) || !Number.isFinite(py) || !Number.isFinite(baseY)) {
36
+ return null;
37
+ }
38
+ return {
39
+ point,
40
+ px,
41
+ py,
42
+ baseY
43
+ };
44
+ });
45
+ }
46
+ export const drawable = slots => slots.filter(slot => slot !== null);
47
+ export function runs(slots) {
48
+ const result = [];
49
+ let current = [];
50
+ slots.forEach(slot => {
51
+ if (slot === null) {
52
+ if (current.length) {
53
+ result.push(current);
54
+ current = [];
55
+ }
56
+ return;
57
+ }
58
+ current.push(slot);
59
+ });
60
+ if (current.length) {
61
+ result.push(current);
62
+ }
63
+ return result;
64
+ }
65
+ const move = projected => `M${projected.px.toFixed(2)} ${projected.py.toFixed(2)}`;
66
+ const lineTo = (x, y) => `L${x.toFixed(2)} ${y.toFixed(2)}`;
67
+ export function toLinePath(slots) {
68
+ return runs(slots).map(run => run.map((projected, index) => index === 0 ? move(projected) : lineTo(projected.px, projected.py)).join('')).join('');
69
+ }
70
+ export function toAreaPath(slots) {
71
+ return runs(slots).map(run => {
72
+ const top = run.map((projected, index) => index === 0 ? move(projected) : lineTo(projected.px, projected.py)).join('');
73
+ const bottom = [...run].reverse().map(projected => lineTo(projected.px, projected.baseY)).join('');
74
+ return `${top}${bottom}Z`;
75
+ }).join('');
76
+ }
@@ -0,0 +1,21 @@
1
+ import { type Point, type Rect, type Size, type Spacing } from '../types/geometry';
2
+ import { type LegendOptions } from '../types/options';
3
+ import { type LegendItem } from '../types/renderer';
4
+ export interface LegendMetrics {
5
+ /** Horizontal space the legend needs when it sits beside the plot, else 0. */
6
+ width: number;
7
+ /** Vertical space the legend needs when it sits above or below the plot, else 0. */
8
+ height: number;
9
+ rowHeight: number;
10
+ vertical: boolean;
11
+ beside: boolean;
12
+ }
13
+ export declare function measureLegend(items: readonly LegendItem[], options: LegendOptions | undefined): LegendMetrics;
14
+ export interface LegendLayout {
15
+ metrics: LegendMetrics;
16
+ positions: Point[];
17
+ }
18
+ /**
19
+ * Symbol anchor for every item. `y` is the symbol centre line; labels sit on it.
20
+ */
21
+ export declare function layoutLegend(items: readonly LegendItem[], options: LegendOptions | undefined, plot: Rect, size: Size, spacing: Spacing): LegendLayout;
@@ -0,0 +1,103 @@
1
+ import { measureText, textHeight } from './measure-text';
2
+ import { LEGEND_BLOCK_GAP, LEGEND_ROW_GAP } from './metrics';
3
+ const itemWidth = (item, options) => {
4
+ const symbolSize = (options === null || options === void 0 ? void 0 : options.symbolHeight) ?? 8;
5
+ return symbolSize + ((options === null || options === void 0 ? void 0 : options.symbolPadding) ?? 6) + measureText(item.label, options === null || options === void 0 ? void 0 : options.itemStyle);
6
+ };
7
+ export function measureLegend(items, options) {
8
+ const rowHeight = textHeight(options === null || options === void 0 ? void 0 : options.itemStyle);
9
+ const vertical = ((options === null || options === void 0 ? void 0 : options.layout) ?? 'horizontal') === 'vertical';
10
+ // A middle-aligned legend has nowhere to go above or below the plot, so it
11
+ // takes a column at the side instead — the same trade Highcharts makes.
12
+ const beside = ((options === null || options === void 0 ? void 0 : options.verticalAlign) ?? 'bottom') === 'middle';
13
+ if (!items.length || (options === null || options === void 0 ? void 0 : options.enabled) === false) {
14
+ return {
15
+ width: 0,
16
+ height: 0,
17
+ rowHeight,
18
+ vertical,
19
+ beside
20
+ };
21
+ }
22
+ const widest = items.reduce((max, item) => Math.max(max, itemWidth(item, options)), 0);
23
+ if (beside) {
24
+ return {
25
+ width: Math.ceil(widest) + LEGEND_BLOCK_GAP,
26
+ height: 0,
27
+ rowHeight,
28
+ vertical,
29
+ beside
30
+ };
31
+ }
32
+ const rows = vertical ? items.length : 1;
33
+ return {
34
+ width: 0,
35
+ height: rows * (rowHeight + LEGEND_ROW_GAP) + LEGEND_BLOCK_GAP - LEGEND_ROW_GAP,
36
+ rowHeight,
37
+ vertical,
38
+ beside
39
+ };
40
+ }
41
+ /**
42
+ * Symbol anchor for every item. `y` is the symbol centre line; labels sit on it.
43
+ */
44
+ export function layoutLegend(items, options, plot, size, spacing) {
45
+ const metrics = measureLegend(items, options);
46
+ const distance = (options === null || options === void 0 ? void 0 : options.itemDistance) ?? 16;
47
+ const align = (options === null || options === void 0 ? void 0 : options.align) ?? 'left';
48
+ const shiftX = (options === null || options === void 0 ? void 0 : options.x) ?? 0;
49
+ const shiftY = (options === null || options === void 0 ? void 0 : options.y) ?? 0;
50
+ const widths = items.map(item => itemWidth(item, options));
51
+ const rowStep = metrics.rowHeight + LEGEND_ROW_GAP;
52
+ if (metrics.vertical || metrics.beside) {
53
+ const column = Math.max(...widths, 0);
54
+ const startX = (() => {
55
+ if (align === 'right') {
56
+ return size.width - column - LEGEND_BLOCK_GAP / 2;
57
+ }
58
+ if (align === 'center') {
59
+ return plot.x + (plot.width - column) / 2;
60
+ }
61
+ return plot.x;
62
+ })();
63
+ const block = items.length * rowStep - LEGEND_ROW_GAP;
64
+ const startY = (() => {
65
+ if (metrics.beside) {
66
+ return plot.y + (plot.height - block) / 2 + metrics.rowHeight / 2;
67
+ }
68
+ return ((options === null || options === void 0 ? void 0 : options.verticalAlign) ?? 'bottom') === 'top' ? plot.y - metrics.height + metrics.rowHeight : plot.y + plot.height + metrics.rowHeight + LEGEND_ROW_GAP;
69
+ })();
70
+ return {
71
+ metrics,
72
+ positions: items.map((_item, index) => ({
73
+ x: startX + shiftX,
74
+ y: startY + index * rowStep + shiftY
75
+ }))
76
+ };
77
+ }
78
+ const total = widths.reduce((sum, width) => sum + width, 0) + distance * Math.max(0, items.length - 1);
79
+ const startX = (() => {
80
+ if (align === 'center') {
81
+ return plot.x + (plot.width - total) / 2;
82
+ }
83
+ if (align === 'right') {
84
+ return plot.x + plot.width - total;
85
+ }
86
+ return plot.x;
87
+ })();
88
+ // Sit on the last row inside the chart's bottom spacing, not on the canvas
89
+ // edge: flush with the edge, the row's own descenders and its focus ring are
90
+ // cut off by the viewport.
91
+ const y = ((options === null || options === void 0 ? void 0 : options.verticalAlign) ?? 'bottom') === 'top' ? plot.y - metrics.height + metrics.rowHeight : size.height - spacing[2] - metrics.rowHeight / 2;
92
+ const offsets = widths.reduce((acc, _width, index) => {
93
+ acc.push(index === 0 ? startX : acc[index - 1] + widths[index - 1] + distance);
94
+ return acc;
95
+ }, []);
96
+ return {
97
+ metrics,
98
+ positions: offsets.map(x => ({
99
+ x: x + shiftX,
100
+ y: y + shiftY
101
+ }))
102
+ };
103
+ }
@@ -0,0 +1,9 @@
1
+ import { type TextStyle } from '../types/options';
2
+ export declare const DEFAULT_FONT_SIZE: string;
3
+ export declare const DEFAULT_FONT_FAMILY: string;
4
+ export declare const DEFAULT_TITLE_FONT_SIZE: string;
5
+ export declare const DEFAULT_SUBTITLE_FONT_SIZE: string;
6
+ export declare const toCssFont: (style?: TextStyle) => string;
7
+ export declare function measureText(text: string, style?: TextStyle): number;
8
+ export declare const textHeight: (style?: TextStyle) => number;
9
+ export declare function clearTextMeasurementCache(): void;
@@ -0,0 +1,57 @@
1
+ import { FontFamily, FontSize } from '@pushwoosh/kit-constants';
2
+ const cache = new Map();
3
+ let context;
4
+ const getContext = () => {
5
+ if (context !== undefined) {
6
+ return context;
7
+ }
8
+ try {
9
+ context = document.createElement('canvas').getContext('2d') ?? null;
10
+ } catch {
11
+ context = null;
12
+ }
13
+ return context;
14
+ };
15
+ export const DEFAULT_FONT_SIZE = FontSize.SMALL;
16
+ export const DEFAULT_FONT_FAMILY = FontFamily.MAIN;
17
+ export const DEFAULT_TITLE_FONT_SIZE = FontSize.HEADING2;
18
+ export const DEFAULT_SUBTITLE_FONT_SIZE = FontSize.SMALL;
19
+ export const toCssFont = style => {
20
+ const weight = (style === null || style === void 0 ? void 0 : style.fontWeight) ?? 'normal';
21
+ const size = (style === null || style === void 0 ? void 0 : style.fontSize) ?? DEFAULT_FONT_SIZE;
22
+ const family = (style === null || style === void 0 ? void 0 : style.fontFamily) ?? DEFAULT_FONT_FAMILY;
23
+ return `${weight} ${size} ${family}`;
24
+ };
25
+ const parseFontSize = font => {
26
+ const match = /(\d+(?:\.\d+)?)px/.exec(font);
27
+ return match ? Number(match[1]) : 12;
28
+ };
29
+ export function measureText(text, style) {
30
+ if (!text) {
31
+ return 0;
32
+ }
33
+ const font = toCssFont(style);
34
+ const key = `${font}\u0000${text}`;
35
+ const cached = cache.get(key);
36
+ if (cached !== undefined) {
37
+ return cached;
38
+ }
39
+ const ctx = getContext();
40
+ let width;
41
+ if (ctx) {
42
+ ctx.font = font;
43
+ width = ctx.measureText(text).width;
44
+ } else {
45
+ width = 0;
46
+ }
47
+ if (!width) {
48
+ width = text.length * parseFontSize(font) * 0.55;
49
+ }
50
+ cache.set(key, width);
51
+ return width;
52
+ }
53
+ export const textHeight = style => Math.ceil(parseFontSize(toCssFont(style)) * 1.2);
54
+ export function clearTextMeasurementCache() {
55
+ cache.clear();
56
+ context = undefined;
57
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Every distance the chart chrome uses. They live together because a chart reads
3
+ * as sloppy the moment each component invents its own gap: the numbers below are
4
+ * one 2/4/6/8/12/16 scale, and changing one here changes it everywhere it means
5
+ * the same thing.
6
+ */
7
+ /** Half the height of a text line — centres a label on the tick it belongs to. */
8
+ export declare const TEXT_BASELINE_SHIFT = 4;
9
+ /** Tick label to the plot edge, on either axis. */
10
+ export declare const AXIS_LABEL_GAP = 8;
11
+ /** Baseline of an x axis label below the axis line, tick length aside. */
12
+ export declare const X_AXIS_LABEL_DROP = 14;
13
+ /** Air added to the tick label column when measuring the y axis gutter. */
14
+ export declare const AXIS_GUTTER_PADDING = 10;
15
+ /** A rotated axis title to the column of tick labels it stands beside. */
16
+ export declare const AXIS_TITLE_GAP = 6;
17
+ /** An x axis title below its labels. */
18
+ export declare const X_AXIS_TITLE_GAP = 16;
19
+ /** A value label to the shape it labels. */
20
+ export declare const VALUE_LABEL_GAP = 6;
21
+ /** A value label to the chart edge — never let one sit on the border. */
22
+ export declare const CHART_EDGE_GAP = 4;
23
+ /** Air above the plot for value labels, on top of the label's own height. */
24
+ export declare const VALUE_LABEL_HEADROOM = 6;
25
+ /** How far a point marker reaches past its point at the default radius. */
26
+ export declare const MARKER_REACH = 5;
27
+ /** Between legend rows. */
28
+ export declare const LEGEND_ROW_GAP = 4;
29
+ /** Between the legend block and the plot it belongs to. */
30
+ export declare const LEGEND_BLOCK_GAP = 12;
31
+ /** The hint badge beside a title: its side, corner radius and gap after the text. */
32
+ export declare const HINT_ICON_SIZE = 16;
33
+ export declare const HINT_ICON_RADIUS = 4;
34
+ export declare const HINT_ICON_GAP = 6;
35
+ /** A wrapping explanation gets more room than a value readout. */
36
+ export declare const HINT_PADDING = 12;
37
+ /** Between the title block and the plot below it. */
38
+ export declare const TITLE_BLOCK_GAP = 10;
39
+ /** Tooltip beak: both its height and half its width. */
40
+ export declare const TOOLTIP_TAIL = 7;
41
+ /** Tooltip to the cursor, when it follows the pointer instead of a point. */
42
+ export declare const TOOLTIP_CURSOR_OFFSET = 12;
43
+ /** Keep a floating element this far inside the viewport. */
44
+ export declare const VIEWPORT_MARGIN = 8;
45
+ /** Tooltip inner padding. */
46
+ export declare const TOOLTIP_PADDING = 8;
47
+ /** Tooltip corner radius. */
48
+ export declare const TOOLTIP_RADIUS = 6;
49
+ /** Distance from a plot band or plot line to its own label. */
50
+ export declare const PLOT_LABEL_GAP = 6;
51
+ /**
52
+ * Puts a stroke where the device can draw it in one row of pixels: half a stroke
53
+ * either side of the position has to land on a pixel boundary, so an odd stroke
54
+ * sits on a half pixel and an even one on a whole. A 1px line centred on a whole
55
+ * pixel is otherwise split between the two rows either side of it and reads as
56
+ * two grey rows rather than one dark one.
57
+ *
58
+ * Deliberately the same arithmetic as Highcharts' own `crisp` — nearest offset
59
+ * rather than always the one above, and a fractional stroke handled by its own
60
+ * remainder. Neither library looks at `devicePixelRatio`: the snapping is in css
61
+ * pixels, so it is exact at whole ratios and merely consistent at the rest.
62
+ */
63
+ export declare const crisp: (position: number, strokeWidth?: number) => number;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Every distance the chart chrome uses. They live together because a chart reads
3
+ * as sloppy the moment each component invents its own gap: the numbers below are
4
+ * one 2/4/6/8/12/16 scale, and changing one here changes it everywhere it means
5
+ * the same thing.
6
+ */
7
+ /** Half the height of a text line — centres a label on the tick it belongs to. */
8
+ export const TEXT_BASELINE_SHIFT = 4;
9
+ /** Tick label to the plot edge, on either axis. */
10
+ export const AXIS_LABEL_GAP = 8;
11
+ /** Baseline of an x axis label below the axis line, tick length aside. */
12
+ export const X_AXIS_LABEL_DROP = 14;
13
+ /** Air added to the tick label column when measuring the y axis gutter. */
14
+ export const AXIS_GUTTER_PADDING = 10;
15
+ /** A rotated axis title to the column of tick labels it stands beside. */
16
+ export const AXIS_TITLE_GAP = 6;
17
+ /** An x axis title below its labels. */
18
+ export const X_AXIS_TITLE_GAP = 16;
19
+ /** A value label to the shape it labels. */
20
+ export const VALUE_LABEL_GAP = 6;
21
+ /** A value label to the chart edge — never let one sit on the border. */
22
+ export const CHART_EDGE_GAP = 4;
23
+ /** Air above the plot for value labels, on top of the label's own height. */
24
+ export const VALUE_LABEL_HEADROOM = 6;
25
+ /** How far a point marker reaches past its point at the default radius. */
26
+ export const MARKER_REACH = 5;
27
+ /** Between legend rows. */
28
+ export const LEGEND_ROW_GAP = 4;
29
+ /** Between the legend block and the plot it belongs to. */
30
+ export const LEGEND_BLOCK_GAP = 12;
31
+ /** The hint badge beside a title: its side, corner radius and gap after the text. */
32
+ export const HINT_ICON_SIZE = 16;
33
+ export const HINT_ICON_RADIUS = 4;
34
+ export const HINT_ICON_GAP = 6;
35
+ /** A wrapping explanation gets more room than a value readout. */
36
+ export const HINT_PADDING = 12;
37
+ /** Between the title block and the plot below it. */
38
+ export const TITLE_BLOCK_GAP = 10;
39
+ /** Tooltip beak: both its height and half its width. */
40
+ export const TOOLTIP_TAIL = 7;
41
+ /** Tooltip to the cursor, when it follows the pointer instead of a point. */
42
+ export const TOOLTIP_CURSOR_OFFSET = 12;
43
+ /** Keep a floating element this far inside the viewport. */
44
+ export const VIEWPORT_MARGIN = 8;
45
+ /** Tooltip inner padding. */
46
+ export const TOOLTIP_PADDING = 8;
47
+ /** Tooltip corner radius. */
48
+ export const TOOLTIP_RADIUS = 6;
49
+ /** Distance from a plot band or plot line to its own label. */
50
+ export const PLOT_LABEL_GAP = 6;
51
+ /**
52
+ * Puts a stroke where the device can draw it in one row of pixels: half a stroke
53
+ * either side of the position has to land on a pixel boundary, so an odd stroke
54
+ * sits on a half pixel and an even one on a whole. A 1px line centred on a whole
55
+ * pixel is otherwise split between the two rows either side of it and reads as
56
+ * two grey rows rather than one dark one.
57
+ *
58
+ * Deliberately the same arithmetic as Highcharts' own `crisp` — nearest offset
59
+ * rather than always the one above, and a fractional stroke handled by its own
60
+ * remainder. Neither library looks at `devicePixelRatio`: the snapping is in css
61
+ * pixels, so it is exact at whole ratios and merely consistent at the rest.
62
+ */
63
+ export const crisp = (position, strokeWidth = 1) => {
64
+ const offset = strokeWidth % 2 / 2;
65
+ return Math.round(position - offset) + offset;
66
+ };
@@ -0,0 +1,11 @@
1
+ import { type StackInput } from '../stack/stack';
2
+ import { type SlotGeometry } from '../types/geometry';
3
+ import { type AnyScale } from '../types/scale';
4
+ export declare function stepOf(scale: AnyScale, series: readonly StackInput[], plotWidth: number): number;
5
+ export interface SlotInput {
6
+ series: readonly StackInput[];
7
+ scale: AnyScale;
8
+ plotWidth: number;
9
+ stacks: ReadonlyMap<number, unknown>;
10
+ }
11
+ export declare function computeSlots({ series, scale, plotWidth, stacks, }: SlotInput): Map<number, SlotGeometry>;
@@ -0,0 +1,86 @@
1
+ const DEFAULT_GROUP_PADDING = 0.2;
2
+ const DEFAULT_POINT_PADDING = 0.1;
3
+ const MIN_WIDTH = 1;
4
+ export function stepOf(scale, series, plotWidth) {
5
+ if (scale.kind === 'band') {
6
+ return scale.step();
7
+ }
8
+ if (scale.kind === 'color') {
9
+ return plotWidth / 2;
10
+ }
11
+ const continuous = scale;
12
+ const positions = new Set();
13
+ series.forEach(({
14
+ points
15
+ }) => {
16
+ points.forEach(point => {
17
+ if (typeof point.x === 'number' && Number.isFinite(point.x)) {
18
+ positions.add(point.x);
19
+ }
20
+ });
21
+ });
22
+ if (positions.size < 2) {
23
+ return plotWidth / 2;
24
+ }
25
+ const sorted = [...positions].sort((left, right) => left - right);
26
+ let smallest = Infinity;
27
+ for (let i = 1; i < sorted.length; i += 1) {
28
+ smallest = Math.min(smallest, sorted[i] - sorted[i - 1]);
29
+ }
30
+ const step = Math.abs(continuous.map(sorted[0] + smallest) - continuous.map(sorted[0]));
31
+ return Number.isFinite(step) && step > 0 ? step : plotWidth / 2;
32
+ }
33
+ function slotIndices(series, stacks) {
34
+ const indices = new Map();
35
+ const stackSlot = new Map();
36
+ let next = 0;
37
+ series.forEach(entry => {
38
+ if (!stacks.has(entry.index)) {
39
+ indices.set(entry.index, next);
40
+ next += 1;
41
+ return;
42
+ }
43
+ const key = `${entry.options.yAxis ?? 0}:${entry.options.stacking ?? ''}`;
44
+ const existing = stackSlot.get(key);
45
+ if (existing === undefined) {
46
+ stackSlot.set(key, next);
47
+ indices.set(entry.index, next);
48
+ next += 1;
49
+ } else {
50
+ indices.set(entry.index, existing);
51
+ }
52
+ });
53
+ return {
54
+ indices,
55
+ count: Math.max(1, next)
56
+ };
57
+ }
58
+ export function computeSlots({
59
+ series,
60
+ scale,
61
+ plotWidth,
62
+ stacks
63
+ }) {
64
+ const result = new Map();
65
+ if (!series.length) {
66
+ return result;
67
+ }
68
+ const step = stepOf(scale, series, plotWidth);
69
+ const {
70
+ indices,
71
+ count
72
+ } = slotIndices(series, stacks);
73
+ series.forEach(entry => {
74
+ const groupPadding = entry.options.groupPadding ?? DEFAULT_GROUP_PADDING;
75
+ const pointPadding = entry.options.pointPadding ?? DEFAULT_POINT_PADDING;
76
+ const groupWidth = step * (1 - 2 * groupPadding);
77
+ const slotWidth = groupWidth / count;
78
+ const index = indices.get(entry.index) ?? 0;
79
+ result.set(entry.index, {
80
+ width: Math.max(MIN_WIDTH, slotWidth * (1 - 2 * pointPadding)),
81
+ offset: slotWidth * (index + 0.5) - groupWidth / 2,
82
+ step
83
+ });
84
+ });
85
+ return result;
86
+ }
@@ -0,0 +1,30 @@
1
+ import { type Size, type Spacing } from '../types/geometry';
2
+ import { type ChartOptions, type TextStyle } from '../types/options';
3
+ declare const ANCHOR: {
4
+ readonly left: "start";
5
+ readonly center: "middle";
6
+ readonly right: "end";
7
+ };
8
+ export type TitleAnchor = typeof ANCHOR[keyof typeof ANCHOR];
9
+ export interface TitleEntry {
10
+ key: 'title' | 'subtitle' | 'caption';
11
+ text: string;
12
+ x: number;
13
+ y: number;
14
+ anchor: TitleAnchor;
15
+ style: TextStyle | undefined;
16
+ fontSize: string;
17
+ /** Set when the title carries a hint: centre of the info icon after the text. */
18
+ hint?: {
19
+ text: string;
20
+ x: number;
21
+ y: number;
22
+ };
23
+ }
24
+ export interface TitleBlock {
25
+ /** Vertical space the titles take above the plot, gap included. */
26
+ height: number;
27
+ entries: TitleEntry[];
28
+ }
29
+ export declare function layoutTitles(options: ChartOptions, size: Size, spacing: Spacing): TitleBlock;
30
+ export {};