@svgrid/grid 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +33 -0
- package/README.md +39 -0
- package/dist/FlexRender.svelte +96 -0
- package/dist/FlexRender.svelte.d.ts +49 -0
- package/dist/SvGrid.svelte +8742 -0
- package/dist/SvGrid.svelte.d.ts +381 -0
- package/dist/SvGridChart.svelte +653 -0
- package/dist/SvGridChart.svelte.d.ts +25 -0
- package/dist/SvGridChart.test.d.ts +1 -0
- package/dist/SvGridChart.test.js +62 -0
- package/dist/SvGridDropdown.svelte +675 -0
- package/dist/SvGridDropdown.svelte.d.ts +40 -0
- package/dist/a11y.contract.test.d.ts +1 -0
- package/dist/a11y.contract.test.js +39 -0
- package/dist/a11y.d.ts +38 -0
- package/dist/a11y.js +37 -0
- package/dist/a11y.test.d.ts +1 -0
- package/dist/a11y.test.js +44 -0
- package/dist/cell-formatting.d.ts +17 -0
- package/dist/cell-formatting.js +141 -0
- package/dist/cell-formatting.test.d.ts +1 -0
- package/dist/cell-formatting.test.js +234 -0
- package/dist/chart-export.d.ts +14 -0
- package/dist/chart-export.js +112 -0
- package/dist/chart.d.ts +226 -0
- package/dist/chart.js +732 -0
- package/dist/chart.test.d.ts +1 -0
- package/dist/chart.test.js +289 -0
- package/dist/collaboration.d.ts +74 -0
- package/dist/collaboration.js +98 -0
- package/dist/collaboration.test.d.ts +1 -0
- package/dist/collaboration.test.js +65 -0
- package/dist/conditional-formatting.d.ts +91 -0
- package/dist/conditional-formatting.js +170 -0
- package/dist/conditional-formatting.test.d.ts +1 -0
- package/dist/conditional-formatting.test.js +87 -0
- package/dist/core.coverage.test.d.ts +1 -0
- package/dist/core.coverage.test.js +186 -0
- package/dist/core.d.ts +370 -0
- package/dist/core.js +568 -0
- package/dist/core.performance.test.d.ts +1 -0
- package/dist/core.performance.test.js +29 -0
- package/dist/createGrid.svelte.d.ts +6 -0
- package/dist/createGrid.svelte.js +17 -0
- package/dist/createGrid.test.d.ts +1 -0
- package/dist/createGrid.test.js +9 -0
- package/dist/createGridState.svelte.d.ts +3 -0
- package/dist/createGridState.svelte.js +13 -0
- package/dist/editors/cell-editors.d.ts +23 -0
- package/dist/editors/cell-editors.js +97 -0
- package/dist/editors/cell-editors.test.d.ts +1 -0
- package/dist/editors/cell-editors.test.js +75 -0
- package/dist/fill-patterns.d.ts +30 -0
- package/dist/fill-patterns.js +207 -0
- package/dist/fill-patterns.test.d.ts +1 -0
- package/dist/fill-patterns.test.js +81 -0
- package/dist/filtering/excel-filters.d.ts +26 -0
- package/dist/filtering/excel-filters.js +64 -0
- package/dist/filtering/excel-filters.test.d.ts +1 -0
- package/dist/filtering/excel-filters.test.js +116 -0
- package/dist/filtering/locale-filter.test.d.ts +13 -0
- package/dist/filtering/locale-filter.test.js +189 -0
- package/dist/flex-render.d.ts +1 -0
- package/dist/flex-render.js +2 -0
- package/dist/flex-render.test.d.ts +1 -0
- package/dist/flex-render.test.js +142 -0
- package/dist/group-aggregate.test.d.ts +1 -0
- package/dist/group-aggregate.test.js +32 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +27 -0
- package/dist/keyboard.d.ts +8 -0
- package/dist/keyboard.js +91 -0
- package/dist/keyboard.test.d.ts +1 -0
- package/dist/keyboard.test.js +27 -0
- package/dist/merge-objects.d.ts +9 -0
- package/dist/merge-objects.js +28 -0
- package/dist/named-views.d.ts +42 -0
- package/dist/named-views.js +80 -0
- package/dist/named-views.test.d.ts +1 -0
- package/dist/named-views.test.js +57 -0
- package/dist/new-features.test.d.ts +1 -0
- package/dist/new-features.test.js +183 -0
- package/dist/render-component.d.ts +13 -0
- package/dist/render-component.js +14 -0
- package/dist/render-component.test.d.ts +1 -0
- package/dist/render-component.test.js +41 -0
- package/dist/server-data-source.d.ts +76 -0
- package/dist/server-data-source.js +83 -0
- package/dist/server-data-source.test.d.ts +1 -0
- package/dist/server-data-source.test.js +81 -0
- package/dist/sparkline.d.ts +56 -0
- package/dist/sparkline.js +97 -0
- package/dist/sparkline.test.d.ts +1 -0
- package/dist/sparkline.test.js +52 -0
- package/dist/static-functions.d.ts +1 -0
- package/dist/static-functions.js +1 -0
- package/dist/subscribe.d.ts +9 -0
- package/dist/subscribe.js +31 -0
- package/dist/subscribe.test.d.ts +1 -0
- package/dist/subscribe.test.js +93 -0
- package/dist/sv-grid-scrollbar.d.ts +1 -0
- package/dist/sv-grid-scrollbar.js +310 -0
- package/dist/svgrid-wrapper.types.d.ts +341 -0
- package/dist/svgrid-wrapper.types.js +1 -0
- package/dist/svgrid.api-extensions.test.d.ts +14 -0
- package/dist/svgrid.api-extensions.test.js +334 -0
- package/dist/svgrid.api.test.d.ts +15 -0
- package/dist/svgrid.api.test.js +169 -0
- package/dist/svgrid.behavior.test.d.ts +15 -0
- package/dist/svgrid.behavior.test.js +605 -0
- package/dist/svgrid.column-reorder.test.d.ts +15 -0
- package/dist/svgrid.column-reorder.test.js +224 -0
- package/dist/svgrid.features.test.d.ts +1 -0
- package/dist/svgrid.features.test.js +129 -0
- package/dist/svgrid.interaction.test.d.ts +9 -0
- package/dist/svgrid.interaction.test.js +318 -0
- package/dist/svgrid.locale-filtering.test.d.ts +15 -0
- package/dist/svgrid.locale-filtering.test.js +261 -0
- package/dist/svgrid.new-features.wrapper.test.d.ts +1 -0
- package/dist/svgrid.new-features.wrapper.test.js +136 -0
- package/dist/svgrid.row-pinning.test.d.ts +14 -0
- package/dist/svgrid.row-pinning.test.js +250 -0
- package/dist/svgrid.set-filter.test.d.ts +17 -0
- package/dist/svgrid.set-filter.test.js +268 -0
- package/dist/svgrid.wrapper.test.d.ts +1 -0
- package/dist/svgrid.wrapper.test.js +33 -0
- package/dist/test-setup.d.ts +1 -0
- package/dist/test-setup.js +29 -0
- package/dist/transaction.test.d.ts +1 -0
- package/dist/transaction.test.js +89 -0
- package/dist/virtualization/column-virtualizer.d.ts +20 -0
- package/dist/virtualization/column-virtualizer.js +19 -0
- package/dist/virtualization/column-virtualizer.test.d.ts +1 -0
- package/dist/virtualization/column-virtualizer.test.js +25 -0
- package/dist/virtualization/svelte-virtualizer.svelte.d.ts +11 -0
- package/dist/virtualization/svelte-virtualizer.svelte.js +20 -0
- package/dist/virtualization/types.d.ts +28 -0
- package/dist/virtualization/types.js +1 -0
- package/dist/virtualization/virtualizer.d.ts +13 -0
- package/dist/virtualization/virtualizer.js +232 -0
- package/dist/virtualization/virtualizer.test.d.ts +1 -0
- package/dist/virtualization/virtualizer.test.js +41 -0
- package/package.json +69 -0
- package/src/FlexRender.svelte +96 -0
- package/src/SvGrid.svelte +8742 -0
- package/src/SvGridChart.svelte +653 -0
- package/src/SvGridChart.test.ts +69 -0
- package/src/SvGridDropdown.svelte +675 -0
- package/src/a11y.contract.test.ts +49 -0
- package/src/a11y.test.ts +59 -0
- package/src/a11y.ts +59 -0
- package/src/cell-formatting.test.ts +273 -0
- package/src/cell-formatting.ts +169 -0
- package/src/chart-export.ts +144 -0
- package/src/chart.test.ts +311 -0
- package/src/chart.ts +934 -0
- package/src/collaboration.test.ts +74 -0
- package/src/collaboration.ts +161 -0
- package/src/conditional-formatting.test.ts +107 -0
- package/src/conditional-formatting.ts +239 -0
- package/src/core.coverage.test.ts +230 -0
- package/src/core.performance.test.ts +30 -0
- package/src/core.ts +991 -0
- package/src/createGrid.svelte.ts +42 -0
- package/src/createGrid.test.ts +10 -0
- package/src/createGridState.svelte.ts +17 -0
- package/src/editors/cell-editors.test.ts +80 -0
- package/src/editors/cell-editors.ts +127 -0
- package/src/fill-patterns.test.ts +93 -0
- package/src/fill-patterns.ts +229 -0
- package/src/filtering/excel-filters.test.ts +131 -0
- package/src/filtering/excel-filters.ts +101 -0
- package/src/filtering/locale-filter.test.ts +220 -0
- package/src/flex-render.test.ts +143 -0
- package/src/flex-render.ts +3 -0
- package/src/group-aggregate.test.ts +48 -0
- package/src/index.ts +159 -0
- package/src/keyboard.test.ts +59 -0
- package/src/keyboard.ts +97 -0
- package/src/merge-objects.ts +48 -0
- package/src/named-views.test.ts +66 -0
- package/src/named-views.ts +120 -0
- package/src/new-features.test.ts +217 -0
- package/src/render-component.test.ts +51 -0
- package/src/render-component.ts +28 -0
- package/src/server-data-source.test.ts +88 -0
- package/src/server-data-source.ts +163 -0
- package/src/sparkline.test.ts +59 -0
- package/src/sparkline.ts +164 -0
- package/src/static-functions.ts +11 -0
- package/src/subscribe.test.ts +103 -0
- package/src/subscribe.ts +38 -0
- package/src/sv-grid-scrollbar.ts +347 -0
- package/src/svgrid-wrapper.types.ts +382 -0
- package/src/svgrid.api-extensions.test.ts +362 -0
- package/src/svgrid.api.test.ts +192 -0
- package/src/svgrid.behavior.test.ts +657 -0
- package/src/svgrid.column-reorder.test.ts +234 -0
- package/src/svgrid.features.test.ts +157 -0
- package/src/svgrid.interaction.test.ts +355 -0
- package/src/svgrid.locale-filtering.test.ts +259 -0
- package/src/svgrid.new-features.wrapper.test.ts +164 -0
- package/src/svgrid.row-pinning.test.ts +266 -0
- package/src/svgrid.set-filter.test.ts +270 -0
- package/src/svgrid.wrapper.test.ts +35 -0
- package/src/test-setup.ts +37 -0
- package/src/transaction.test.ts +100 -0
- package/src/virtualization/column-virtualizer.test.ts +27 -0
- package/src/virtualization/column-virtualizer.ts +30 -0
- package/src/virtualization/svelte-virtualizer.svelte.ts +24 -0
- package/src/virtualization/types.ts +30 -0
- package/src/virtualization/virtualizer.test.ts +47 -0
- package/src/virtualization/virtualizer.ts +270 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildChart, rowsToChartSpec, niceScale } from './chart';
|
|
3
|
+
describe('niceScale', () => {
|
|
4
|
+
it('rounds a positive domain to nice ticks including 0', () => {
|
|
5
|
+
const s = niceScale(0, 95);
|
|
6
|
+
expect(s.min).toBe(0);
|
|
7
|
+
expect(s.max).toBeGreaterThanOrEqual(95);
|
|
8
|
+
expect(s.ticks[0]).toBe(0);
|
|
9
|
+
});
|
|
10
|
+
it('handles a signed domain (negatives)', () => {
|
|
11
|
+
const s = niceScale(-40, 80);
|
|
12
|
+
expect(s.min).toBeLessThanOrEqual(-40);
|
|
13
|
+
expect(s.max).toBeGreaterThanOrEqual(80);
|
|
14
|
+
expect(s.ticks).toContain(0);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe('buildChart', () => {
|
|
18
|
+
it('bar: one rect per category per series, heights scale to max', () => {
|
|
19
|
+
const g = buildChart({
|
|
20
|
+
type: 'bar',
|
|
21
|
+
categories: ['A', 'B'],
|
|
22
|
+
series: [{ label: 's1', values: [10, 20] }],
|
|
23
|
+
width: 400,
|
|
24
|
+
height: 200,
|
|
25
|
+
});
|
|
26
|
+
expect(g.bars).toHaveLength(2);
|
|
27
|
+
expect(g.bars[1].h).toBeGreaterThan(g.bars[0].h);
|
|
28
|
+
expect(g.xTicks.map((t) => t.label)).toEqual(['A', 'B']);
|
|
29
|
+
expect(g.bars[1]).toMatchObject({ label: 'B', series: 's1', value: 20 });
|
|
30
|
+
});
|
|
31
|
+
it('bar: negative values drop below the zero baseline', () => {
|
|
32
|
+
const g = buildChart({ type: 'bar', categories: ['A', 'B'], series: [{ label: 's', values: [50, -50] }] });
|
|
33
|
+
const baseY = g.yTicks.find((t) => t.value === 0).y;
|
|
34
|
+
// positive bar sits above the baseline, negative bar below it
|
|
35
|
+
expect(g.bars[0].y).toBeLessThan(baseY);
|
|
36
|
+
expect(g.bars[1].y).toBeGreaterThanOrEqual(baseY - 0.01);
|
|
37
|
+
expect(g.bars[1].value).toBe(-50);
|
|
38
|
+
});
|
|
39
|
+
it('stacked bars: same x per category, segment heights stack', () => {
|
|
40
|
+
const g = buildChart({
|
|
41
|
+
type: 'bar',
|
|
42
|
+
stacked: true,
|
|
43
|
+
categories: ['A'],
|
|
44
|
+
series: [
|
|
45
|
+
{ label: 'x', values: [10] },
|
|
46
|
+
{ label: 'y', values: [20] },
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
expect(g.bars).toHaveLength(2);
|
|
50
|
+
// both segments share the same x (stacked, not grouped)
|
|
51
|
+
expect(g.bars[0].x).toBe(g.bars[1].x);
|
|
52
|
+
});
|
|
53
|
+
it('combo + dual axis: bar on left, line on right axis', () => {
|
|
54
|
+
const g = buildChart({
|
|
55
|
+
type: 'bar',
|
|
56
|
+
categories: ['A', 'B'],
|
|
57
|
+
series: [
|
|
58
|
+
{ label: 'rev', values: [100, 200], type: 'bar', axis: 'left' },
|
|
59
|
+
{ label: 'pct', values: [0.4, 0.9], type: 'line', axis: 'right' },
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
expect(g.bars.length).toBe(2);
|
|
63
|
+
expect(g.lines.length).toBe(1);
|
|
64
|
+
expect(g.hasRightAxis).toBe(true);
|
|
65
|
+
expect(g.y2Ticks.length).toBeGreaterThan(1);
|
|
66
|
+
});
|
|
67
|
+
it('line: a path through every point, area closes when type=area', () => {
|
|
68
|
+
const line = buildChart({ type: 'line', categories: ['A', 'B', 'C'], series: [{ label: 's', values: [1, 2, 3] }] });
|
|
69
|
+
expect(line.lines[0].path.startsWith('M')).toBe(true);
|
|
70
|
+
expect(line.lines[0].areaPath).toBe('');
|
|
71
|
+
expect(line.lines[0].label).toBe('s');
|
|
72
|
+
expect(line.lines[0].points.map((p) => p.value)).toEqual([1, 2, 3]);
|
|
73
|
+
const area = buildChart({ type: 'area', categories: ['A', 'B'], series: [{ label: 's', values: [1, 2] }] });
|
|
74
|
+
expect(area.lines[0].areaPath.endsWith('Z')).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
it('line: null / NaN values break the line (gap) and drop the dot', () => {
|
|
77
|
+
const g = buildChart({ type: 'line', categories: ['A', 'B', 'C', 'D'], series: [{ label: 's', values: [1, NaN, 3, 4] }] });
|
|
78
|
+
const pts = g.lines[0].points;
|
|
79
|
+
expect(pts.map((p) => p.defined)).toEqual([true, false, true, true]);
|
|
80
|
+
// a gap => a second "move" command, so the path has two M's
|
|
81
|
+
expect((g.lines[0].path.match(/M/g) ?? []).length).toBe(2);
|
|
82
|
+
});
|
|
83
|
+
it('area: a gap splits the fill into separate closed polygons', () => {
|
|
84
|
+
const g = buildChart({ type: 'area', categories: ['A', 'B', 'C', 'D'], series: [{ label: 's', values: [2, NaN, 4, 5] }] });
|
|
85
|
+
expect((g.lines[0].areaPath.match(/Z/g) ?? []).length).toBe(2);
|
|
86
|
+
});
|
|
87
|
+
it('axis titles reserve gutter space', () => {
|
|
88
|
+
const plain = buildChart({ type: 'bar', categories: ['A'], series: [{ label: 's', values: [1] }] });
|
|
89
|
+
const titled = buildChart({ type: 'bar', categories: ['A'], series: [{ label: 's', values: [1] }], yAxisTitle: 'Revenue', xAxisTitle: 'Region' });
|
|
90
|
+
expect(titled.plot.x).toBeGreaterThan(plain.plot.x); // left gutter wider for the y title
|
|
91
|
+
expect(titled.plot.h).toBeLessThan(plain.plot.h); // bottom gutter wider for the x title
|
|
92
|
+
});
|
|
93
|
+
it('pie: slice percents sum to ~100, slices have a centroid', () => {
|
|
94
|
+
const g = buildChart({ type: 'pie', categories: ['A', 'B', 'C'], series: [{ label: 's', values: [1, 1, 2] }] });
|
|
95
|
+
expect(g.slices).toHaveLength(3);
|
|
96
|
+
expect(g.slices.reduce((a, s) => a + s.percent, 0)).toBeCloseTo(100);
|
|
97
|
+
expect(g.slices[2].percent).toBeCloseTo(50);
|
|
98
|
+
expect(Number.isFinite(g.slices[0].cx)).toBe(true);
|
|
99
|
+
expect(g.donut).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
it('donut: innerRadius produces a centre with the total', () => {
|
|
102
|
+
const g = buildChart({ type: 'pie', innerRadius: 0.6, categories: ['A', 'B'], series: [{ label: 's', values: [3, 7] }] });
|
|
103
|
+
expect(g.donut).not.toBeNull();
|
|
104
|
+
expect(g.donut.total).toBe(10);
|
|
105
|
+
});
|
|
106
|
+
it('rotates x labels when there are many / long categories', () => {
|
|
107
|
+
const many = buildChart({ type: 'bar', categories: Array.from({ length: 12 }, (_, i) => `Cat ${i}`), series: [{ label: 's', values: Array(12).fill(1) }] });
|
|
108
|
+
expect(many.xLabelRotated).toBe(true);
|
|
109
|
+
const few = buildChart({ type: 'bar', categories: ['A', 'B'], series: [{ label: 's', values: [1, 2] }] });
|
|
110
|
+
expect(few.xLabelRotated).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
it('assigns palette colors when series omit color', () => {
|
|
113
|
+
const g = buildChart({ type: 'bar', categories: ['A'], series: [{ label: 'x', values: [1] }, { label: 'y', values: [2] }] });
|
|
114
|
+
expect(g.legend[0].color).not.toBe(g.legend[1].color);
|
|
115
|
+
});
|
|
116
|
+
it('reference line: a horizontal line at the value, included in the domain', () => {
|
|
117
|
+
const g = buildChart({
|
|
118
|
+
type: 'bar',
|
|
119
|
+
categories: ['A', 'B'],
|
|
120
|
+
series: [{ label: 's', values: [10, 20] }],
|
|
121
|
+
referenceLines: [{ value: 25, label: 'Target' }],
|
|
122
|
+
});
|
|
123
|
+
expect(g.referenceLines).toHaveLength(1);
|
|
124
|
+
expect(g.referenceLines[0].label).toBe('Target');
|
|
125
|
+
// domain stretched to include the reference value above the data max
|
|
126
|
+
expect(g.yTicks[g.yTicks.length - 1].value).toBeGreaterThanOrEqual(25);
|
|
127
|
+
});
|
|
128
|
+
it('stacked100: each category fills the full plot height (0..100 axis)', () => {
|
|
129
|
+
const g = buildChart({
|
|
130
|
+
type: 'bar',
|
|
131
|
+
stacked100: true,
|
|
132
|
+
categories: ['A'],
|
|
133
|
+
series: [
|
|
134
|
+
{ label: 'x', values: [1] },
|
|
135
|
+
{ label: 'y', values: [3] },
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
expect(g.yTicks.map((t) => t.value)).toContain(100);
|
|
139
|
+
// two stacked segments whose heights sum to ~ the plot height
|
|
140
|
+
const sum = g.bars.reduce((a, b) => a + b.h, 0);
|
|
141
|
+
expect(sum).toBeCloseTo(g.plot.h, 0);
|
|
142
|
+
// values stay the originals (not the normalized %)
|
|
143
|
+
expect(g.bars.map((b) => b.value).sort()).toEqual([1, 3]);
|
|
144
|
+
});
|
|
145
|
+
it('stacked100 area: the top series reaches the 100% line, not the raw total', () => {
|
|
146
|
+
const g = buildChart({
|
|
147
|
+
type: 'area',
|
|
148
|
+
stacked100: true,
|
|
149
|
+
categories: ['A', 'B'],
|
|
150
|
+
series: [
|
|
151
|
+
{ label: 'x', values: [200, 600] },
|
|
152
|
+
{ label: 'y', values: [600, 200] },
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
const y100 = g.yTicks.find((t) => t.value === 100).y;
|
|
156
|
+
const y0 = g.yTicks.find((t) => t.value === 0).y;
|
|
157
|
+
// top of the (cumulative) second area sits at the 100% gridline for every category
|
|
158
|
+
const topPts = g.lines[1].points;
|
|
159
|
+
for (const p of topPts)
|
|
160
|
+
expect(p.y).toBeCloseTo(y100, 0);
|
|
161
|
+
// and the stack stays within the plot (never below the 0 baseline)
|
|
162
|
+
for (const l of g.lines)
|
|
163
|
+
for (const p of l.points)
|
|
164
|
+
expect(p.y).toBeGreaterThanOrEqual(y100 - 0.5);
|
|
165
|
+
expect(y100).toBeLessThan(y0); // 100% is above 0 on screen
|
|
166
|
+
});
|
|
167
|
+
it('scatter: one dot per point, bubble radius scales with r', () => {
|
|
168
|
+
const g = buildChart({
|
|
169
|
+
type: 'scatter',
|
|
170
|
+
categories: [],
|
|
171
|
+
series: [
|
|
172
|
+
{ label: 'pts', values: [], points: [{ x: 1, y: 1, r: 1 }, { x: 5, y: 8, r: 10 }, { x: 3, y: 4, r: 5 }] },
|
|
173
|
+
],
|
|
174
|
+
});
|
|
175
|
+
expect(g.scatterPoints).toHaveLength(3);
|
|
176
|
+
const byR = [...g.scatterPoints].sort((a, b) => a.r - b.r);
|
|
177
|
+
expect(byR[0].r).toBeLessThan(byR[2].r); // bigger r => bigger bubble
|
|
178
|
+
expect(g.xTicks.length).toBeGreaterThan(1);
|
|
179
|
+
expect(g.yTicks.length).toBeGreaterThan(1);
|
|
180
|
+
});
|
|
181
|
+
it('horizontal bars: bars grow along x, categories run down the y axis', () => {
|
|
182
|
+
const g = buildChart({
|
|
183
|
+
type: 'bar',
|
|
184
|
+
orientation: 'horizontal',
|
|
185
|
+
categories: ['A', 'B'],
|
|
186
|
+
series: [{ label: 's', values: [10, 30] }],
|
|
187
|
+
width: 400,
|
|
188
|
+
height: 200,
|
|
189
|
+
});
|
|
190
|
+
expect(g.orientation).toBe('horizontal');
|
|
191
|
+
expect(g.bars).toHaveLength(2);
|
|
192
|
+
// bigger value => wider bar (grows along x)
|
|
193
|
+
expect(g.bars[1].w).toBeGreaterThan(g.bars[0].w);
|
|
194
|
+
// bars share a left baseline x (value 0), differ in y (one per category band)
|
|
195
|
+
expect(g.bars[0].x).toBeCloseTo(g.bars[1].x, 1);
|
|
196
|
+
expect(g.bars[0].y).not.toBe(g.bars[1].y);
|
|
197
|
+
// category labels live on the left axis, value ticks along the bottom
|
|
198
|
+
expect(g.catTicks.map((t) => t.label)).toEqual(['A', 'B']);
|
|
199
|
+
expect(g.valueTicks.length).toBeGreaterThan(1);
|
|
200
|
+
});
|
|
201
|
+
it('horizontal bars: a reference line becomes a vertical line', () => {
|
|
202
|
+
const g = buildChart({
|
|
203
|
+
type: 'bar',
|
|
204
|
+
orientation: 'horizontal',
|
|
205
|
+
categories: ['A'],
|
|
206
|
+
series: [{ label: 's', values: [10] }],
|
|
207
|
+
referenceLines: [{ value: 20, label: 'Goal' }],
|
|
208
|
+
});
|
|
209
|
+
expect(g.referenceLines).toHaveLength(0);
|
|
210
|
+
expect(g.referenceLinesV).toHaveLength(1);
|
|
211
|
+
expect(g.referenceLinesV[0].label).toBe('Goal');
|
|
212
|
+
expect(Number.isFinite(g.referenceLinesV[0].x)).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
it('horizontal falls back to vertical when a series is not a bar (combo)', () => {
|
|
215
|
+
const g = buildChart({
|
|
216
|
+
type: 'bar',
|
|
217
|
+
orientation: 'horizontal',
|
|
218
|
+
categories: ['A', 'B'],
|
|
219
|
+
series: [
|
|
220
|
+
{ label: 'bar', values: [1, 2] },
|
|
221
|
+
{ label: 'line', values: [3, 4], type: 'line' },
|
|
222
|
+
],
|
|
223
|
+
});
|
|
224
|
+
expect(g.orientation).toBe('vertical');
|
|
225
|
+
});
|
|
226
|
+
it('time axis: irregular date gaps map to proportional x positions', () => {
|
|
227
|
+
const g = buildChart({
|
|
228
|
+
type: 'line',
|
|
229
|
+
xType: 'time',
|
|
230
|
+
categories: ['2024-01-01', '2024-01-02', '2024-02-01'],
|
|
231
|
+
series: [{ label: 's', values: [1, 2, 3] }],
|
|
232
|
+
});
|
|
233
|
+
const xs = g.lines[0].points.map((p) => p.x);
|
|
234
|
+
// gap 1->2 (1 day) is far smaller than gap 2->3 (31 days)
|
|
235
|
+
expect(xs[1] - xs[0]).toBeLessThan(xs[2] - xs[1]);
|
|
236
|
+
expect(g.xLabelRotated).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
describe('rowsToChartSpec', () => {
|
|
240
|
+
const rows = [
|
|
241
|
+
{ region: 'EMEA', product: 'A', revenue: 100, cost: 60 },
|
|
242
|
+
{ region: 'EMEA', product: 'B', revenue: 50, cost: 20 },
|
|
243
|
+
{ region: 'APAC', product: 'A', revenue: 200, cost: 90 },
|
|
244
|
+
];
|
|
245
|
+
it('groups by category and sums one value field', () => {
|
|
246
|
+
const spec = rowsToChartSpec(rows, { type: 'bar', category: 'region', value: 'revenue' });
|
|
247
|
+
expect(spec.categories).toEqual(['EMEA', 'APAC']);
|
|
248
|
+
expect(spec.series).toHaveLength(1);
|
|
249
|
+
expect(spec.series[0].values).toEqual([150, 200]);
|
|
250
|
+
});
|
|
251
|
+
it('multiple value fields => one series each', () => {
|
|
252
|
+
const spec = rowsToChartSpec(rows, { type: 'bar', category: 'region', value: ['revenue', 'cost'] });
|
|
253
|
+
expect(spec.series.map((s) => s.label)).toEqual(['revenue', 'cost']);
|
|
254
|
+
expect(spec.series[1].values).toEqual([80, 90]);
|
|
255
|
+
});
|
|
256
|
+
it('series field => pivot into one series per distinct value', () => {
|
|
257
|
+
const spec = rowsToChartSpec(rows, { type: 'bar', category: 'region', value: 'revenue', series: 'product' });
|
|
258
|
+
expect(spec.series.map((s) => s.label).sort()).toEqual(['A', 'B']);
|
|
259
|
+
const a = spec.series.find((s) => s.label === 'A');
|
|
260
|
+
// EMEA/A = 100, APAC/A = 200
|
|
261
|
+
expect(a.values).toEqual([100, 200]);
|
|
262
|
+
});
|
|
263
|
+
it('supports avg and count reducers', () => {
|
|
264
|
+
expect(rowsToChartSpec(rows, { type: 'bar', category: 'region', value: 'revenue', reduce: 'avg' }).series[0].values).toEqual([75, 200]);
|
|
265
|
+
expect(rowsToChartSpec(rows, { type: 'bar', category: 'region', value: 'revenue', reduce: 'count' }).series[0].values).toEqual([2, 1]);
|
|
266
|
+
});
|
|
267
|
+
it('sorts categories by total value descending', () => {
|
|
268
|
+
const spec = rowsToChartSpec(rows, { type: 'bar', category: 'region', value: 'revenue', sort: 'value-desc' });
|
|
269
|
+
// APAC (200) outranks EMEA (150)
|
|
270
|
+
expect(spec.categories).toEqual(['APAC', 'EMEA']);
|
|
271
|
+
expect(spec.series[0].values).toEqual([200, 150]);
|
|
272
|
+
});
|
|
273
|
+
it('topN buckets the remainder into an "Other" category', () => {
|
|
274
|
+
const many = [
|
|
275
|
+
{ cat: 'A', v: 100 },
|
|
276
|
+
{ cat: 'B', v: 50 },
|
|
277
|
+
{ cat: 'C', v: 30 },
|
|
278
|
+
{ cat: 'D', v: 20 },
|
|
279
|
+
];
|
|
280
|
+
const spec = rowsToChartSpec(many, { type: 'bar', category: 'cat', value: 'v', topN: 2 });
|
|
281
|
+
expect(spec.categories).toEqual(['A', 'B', 'Other']);
|
|
282
|
+
// Other = C + D = 50
|
|
283
|
+
expect(spec.series[0].values).toEqual([100, 50, 50]);
|
|
284
|
+
});
|
|
285
|
+
it('stacked100 flag passes through to the spec', () => {
|
|
286
|
+
const spec = rowsToChartSpec(rows, { type: 'bar', category: 'region', value: ['revenue', 'cost'], stacked100: true });
|
|
287
|
+
expect(spec.stacked100).toBe(true);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real-time collaboration. Presence (who else is here + where their cursor
|
|
3
|
+
* is) and live edits (a change in one client lands in every other), over a
|
|
4
|
+
* pluggable transport. The transport is the only thing tied to infrastructure
|
|
5
|
+
* - swap `broadcastChannelTransport` (same-browser tabs, zero backend) for a
|
|
6
|
+
* WebSocket / WebRTC / CRDT adapter and the controller is unchanged.
|
|
7
|
+
*
|
|
8
|
+
* This is also the natural "multiple agents on one grid" substrate: an AI
|
|
9
|
+
* agent is just another peer posting `edit` messages.
|
|
10
|
+
*/
|
|
11
|
+
export type CollabUser = {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
/** A CSS color for this user's cursor / avatar. */
|
|
15
|
+
color: string;
|
|
16
|
+
};
|
|
17
|
+
export type CollabCell = {
|
|
18
|
+
rowId: string;
|
|
19
|
+
columnId: string;
|
|
20
|
+
};
|
|
21
|
+
export type CollabPresence = CollabUser & {
|
|
22
|
+
cell: CollabCell | null;
|
|
23
|
+
/** Last time we heard from this peer (ms). Used to prune the gone. */
|
|
24
|
+
ts: number;
|
|
25
|
+
};
|
|
26
|
+
export type CollabMessage = {
|
|
27
|
+
kind: 'hello';
|
|
28
|
+
user: CollabUser;
|
|
29
|
+
} | {
|
|
30
|
+
kind: 'presence';
|
|
31
|
+
user: CollabUser;
|
|
32
|
+
cell: CollabCell | null;
|
|
33
|
+
} | {
|
|
34
|
+
kind: 'edit';
|
|
35
|
+
user: CollabUser;
|
|
36
|
+
rowId: string;
|
|
37
|
+
columnId: string;
|
|
38
|
+
value: unknown;
|
|
39
|
+
} | {
|
|
40
|
+
kind: 'bye';
|
|
41
|
+
userId: string;
|
|
42
|
+
};
|
|
43
|
+
export type CollabTransport = {
|
|
44
|
+
post(msg: CollabMessage): void;
|
|
45
|
+
subscribe(handler: (msg: CollabMessage) => void): () => void;
|
|
46
|
+
};
|
|
47
|
+
export type Collaboration = {
|
|
48
|
+
/** Broadcast where this user's cursor is (or null when it leaves). */
|
|
49
|
+
setCell(cell: CollabCell | null): void;
|
|
50
|
+
/** Broadcast a cell edit to every peer. */
|
|
51
|
+
sendEdit(rowId: string, columnId: string, value: unknown): void;
|
|
52
|
+
/** The peers currently present (excludes self). */
|
|
53
|
+
peers(): CollabPresence[];
|
|
54
|
+
dispose(): void;
|
|
55
|
+
};
|
|
56
|
+
export type CollaborationOptions = {
|
|
57
|
+
user: CollabUser;
|
|
58
|
+
transport: CollabTransport;
|
|
59
|
+
/** Fired (with self excluded) whenever the peer set or a cursor changes. */
|
|
60
|
+
onPeersChange?: (peers: CollabPresence[]) => void;
|
|
61
|
+
/** Fired when another user edits a cell - apply it to your data. */
|
|
62
|
+
onRemoteEdit?: (edit: {
|
|
63
|
+
rowId: string;
|
|
64
|
+
columnId: string;
|
|
65
|
+
value: unknown;
|
|
66
|
+
user: CollabUser;
|
|
67
|
+
}) => void;
|
|
68
|
+
/** Drop peers we haven't heard from in this many ms. Default 15000. */
|
|
69
|
+
peerTimeoutMs?: number;
|
|
70
|
+
};
|
|
71
|
+
/** BroadcastChannel transport - live across tabs of the same browser, no
|
|
72
|
+
* backend. No-ops where BroadcastChannel is unavailable (SSR / old env). */
|
|
73
|
+
export declare function broadcastChannelTransport(name: string): CollabTransport;
|
|
74
|
+
export declare function createCollaboration(options: CollaborationOptions): Collaboration;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** BroadcastChannel transport - live across tabs of the same browser, no
|
|
2
|
+
* backend. No-ops where BroadcastChannel is unavailable (SSR / old env). */
|
|
3
|
+
export function broadcastChannelTransport(name) {
|
|
4
|
+
const available = typeof BroadcastChannel !== 'undefined';
|
|
5
|
+
const channel = available ? new BroadcastChannel(name) : null;
|
|
6
|
+
return {
|
|
7
|
+
post(msg) {
|
|
8
|
+
channel?.postMessage(msg);
|
|
9
|
+
},
|
|
10
|
+
subscribe(handler) {
|
|
11
|
+
if (!channel)
|
|
12
|
+
return () => { };
|
|
13
|
+
const listener = (e) => handler(e.data);
|
|
14
|
+
channel.addEventListener('message', listener);
|
|
15
|
+
return () => channel.removeEventListener('message', listener);
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function createCollaboration(options) {
|
|
20
|
+
const { user, transport } = options;
|
|
21
|
+
const timeout = options.peerTimeoutMs ?? 15000;
|
|
22
|
+
const peers = new Map();
|
|
23
|
+
let ownCell = null;
|
|
24
|
+
let disposed = false;
|
|
25
|
+
const list = () => [...peers.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
26
|
+
const notify = () => options.onPeersChange?.(list());
|
|
27
|
+
function upsert(u, cell) {
|
|
28
|
+
if (u.id === user.id)
|
|
29
|
+
return;
|
|
30
|
+
peers.set(u.id, { ...u, cell, ts: Date.now() });
|
|
31
|
+
notify();
|
|
32
|
+
}
|
|
33
|
+
const unsubscribe = transport.subscribe((msg) => {
|
|
34
|
+
if (disposed)
|
|
35
|
+
return;
|
|
36
|
+
switch (msg.kind) {
|
|
37
|
+
case 'hello':
|
|
38
|
+
upsert(msg.user, null);
|
|
39
|
+
// Let the newcomer know we're here (and where our cursor is).
|
|
40
|
+
transport.post({ kind: 'presence', user, cell: ownCell });
|
|
41
|
+
break;
|
|
42
|
+
case 'presence':
|
|
43
|
+
upsert(msg.user, msg.cell);
|
|
44
|
+
break;
|
|
45
|
+
case 'edit':
|
|
46
|
+
upsert(msg.user, { rowId: msg.rowId, columnId: msg.columnId });
|
|
47
|
+
if (msg.user.id !== user.id) {
|
|
48
|
+
options.onRemoteEdit?.({
|
|
49
|
+
rowId: msg.rowId,
|
|
50
|
+
columnId: msg.columnId,
|
|
51
|
+
value: msg.value,
|
|
52
|
+
user: msg.user,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
case 'bye':
|
|
57
|
+
if (peers.delete(msg.userId))
|
|
58
|
+
notify();
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
// Prune peers that went silent (closed tab without a `bye`).
|
|
63
|
+
const pruneTimer = typeof setInterval !== 'undefined'
|
|
64
|
+
? setInterval(() => {
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
let changed = false;
|
|
67
|
+
for (const [id, p] of peers) {
|
|
68
|
+
if (now - p.ts > timeout) {
|
|
69
|
+
peers.delete(id);
|
|
70
|
+
changed = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (changed)
|
|
74
|
+
notify();
|
|
75
|
+
}, Math.max(1000, timeout / 3))
|
|
76
|
+
: null;
|
|
77
|
+
// Announce ourselves.
|
|
78
|
+
transport.post({ kind: 'hello', user });
|
|
79
|
+
return {
|
|
80
|
+
setCell(cell) {
|
|
81
|
+
ownCell = cell;
|
|
82
|
+
transport.post({ kind: 'presence', user, cell });
|
|
83
|
+
},
|
|
84
|
+
sendEdit(rowId, columnId, value) {
|
|
85
|
+
transport.post({ kind: 'edit', user, rowId, columnId, value });
|
|
86
|
+
},
|
|
87
|
+
peers: list,
|
|
88
|
+
dispose() {
|
|
89
|
+
if (disposed)
|
|
90
|
+
return;
|
|
91
|
+
disposed = true;
|
|
92
|
+
transport.post({ kind: 'bye', userId: user.id });
|
|
93
|
+
unsubscribe();
|
|
94
|
+
if (pruneTimer)
|
|
95
|
+
clearInterval(pruneTimer);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createCollaboration, } from './collaboration';
|
|
3
|
+
/** An in-memory bus so two controllers can talk in a test. */
|
|
4
|
+
function makeBus() {
|
|
5
|
+
const handlers = new Set();
|
|
6
|
+
const transportFor = () => ({
|
|
7
|
+
post: (msg) => {
|
|
8
|
+
// deliver async-ish to every subscriber except none (real channels echo
|
|
9
|
+
// to other tabs, not self; our controllers ignore their own id anyway).
|
|
10
|
+
for (const h of [...handlers])
|
|
11
|
+
h(msg);
|
|
12
|
+
},
|
|
13
|
+
subscribe: (h) => {
|
|
14
|
+
handlers.add(h);
|
|
15
|
+
return () => handlers.delete(h);
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
return { transportFor };
|
|
19
|
+
}
|
|
20
|
+
const userA = { id: 'a', name: 'Ada', color: '#f00' };
|
|
21
|
+
const userB = { id: 'b', name: 'Bob', color: '#00f' };
|
|
22
|
+
describe('createCollaboration', () => {
|
|
23
|
+
it('peers see each other after hello', () => {
|
|
24
|
+
const bus = makeBus();
|
|
25
|
+
const a = createCollaboration({ user: userA, transport: bus.transportFor() });
|
|
26
|
+
const b = createCollaboration({ user: userB, transport: bus.transportFor() });
|
|
27
|
+
// A is announced; B's hello reaches A; A replies presence.
|
|
28
|
+
expect(a.peers().map((p) => p.id)).toEqual(['b']);
|
|
29
|
+
expect(b.peers().map((p) => p.id)).toEqual(['a']);
|
|
30
|
+
a.dispose();
|
|
31
|
+
b.dispose();
|
|
32
|
+
});
|
|
33
|
+
it('setCell propagates a remote cursor', () => {
|
|
34
|
+
const bus = makeBus();
|
|
35
|
+
const onPeers = vi.fn();
|
|
36
|
+
const a = createCollaboration({ user: userA, transport: bus.transportFor(), onPeersChange: onPeers });
|
|
37
|
+
const b = createCollaboration({ user: userB, transport: bus.transportFor() });
|
|
38
|
+
b.setCell({ rowId: 'r1', columnId: 'salary' });
|
|
39
|
+
const peerB = a.peers().find((p) => p.id === 'b');
|
|
40
|
+
expect(peerB?.cell).toEqual({ rowId: 'r1', columnId: 'salary' });
|
|
41
|
+
a.dispose();
|
|
42
|
+
b.dispose();
|
|
43
|
+
});
|
|
44
|
+
it('sendEdit fires onRemoteEdit on the other peer only', () => {
|
|
45
|
+
const bus = makeBus();
|
|
46
|
+
const aEdits = vi.fn();
|
|
47
|
+
const bEdits = vi.fn();
|
|
48
|
+
const a = createCollaboration({ user: userA, transport: bus.transportFor(), onRemoteEdit: aEdits });
|
|
49
|
+
const b = createCollaboration({ user: userB, transport: bus.transportFor(), onRemoteEdit: bEdits });
|
|
50
|
+
a.sendEdit('r2', 'name', 'Grace');
|
|
51
|
+
expect(bEdits).toHaveBeenCalledWith(expect.objectContaining({ rowId: 'r2', columnId: 'name', value: 'Grace' }));
|
|
52
|
+
expect(aEdits).not.toHaveBeenCalled(); // not echoed to self
|
|
53
|
+
a.dispose();
|
|
54
|
+
b.dispose();
|
|
55
|
+
});
|
|
56
|
+
it('bye removes a peer', () => {
|
|
57
|
+
const bus = makeBus();
|
|
58
|
+
const a = createCollaboration({ user: userA, transport: bus.transportFor() });
|
|
59
|
+
const b = createCollaboration({ user: userB, transport: bus.transportFor() });
|
|
60
|
+
expect(a.peers()).toHaveLength(1);
|
|
61
|
+
b.dispose();
|
|
62
|
+
expect(a.peers()).toHaveLength(0);
|
|
63
|
+
a.dispose();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conditional formatting engine. Excel-style declarative rules that color a
|
|
3
|
+
* cell by its value - color scales, in-cell data bars, icon sets, and plain
|
|
4
|
+
* predicate rules. This goes beyond `cellClass(ctx)` (which can only toggle
|
|
5
|
+
* static classes) because color scales and data bars need a value computed
|
|
6
|
+
* against the column's min/max range.
|
|
7
|
+
*
|
|
8
|
+
* Everything here is pure: `resolveCellFormat` takes a value + the column's
|
|
9
|
+
* numeric range and returns the visual primitives, so the `<SvGrid>` render
|
|
10
|
+
* component just paints the result and the logic is unit-testable.
|
|
11
|
+
*/
|
|
12
|
+
export type ColorScaleFormat = {
|
|
13
|
+
type: 'colorScale';
|
|
14
|
+
/** 2-stop (min/max) or 3-stop (min/mid/max) gradient. Hex colors. */
|
|
15
|
+
min: string;
|
|
16
|
+
mid?: string;
|
|
17
|
+
max: string;
|
|
18
|
+
/** Fix the scale; otherwise derived from the column's data. */
|
|
19
|
+
minValue?: number;
|
|
20
|
+
maxValue?: number;
|
|
21
|
+
};
|
|
22
|
+
export type DataBarFormat = {
|
|
23
|
+
type: 'dataBar';
|
|
24
|
+
color: string;
|
|
25
|
+
negativeColor?: string;
|
|
26
|
+
minValue?: number;
|
|
27
|
+
maxValue?: number;
|
|
28
|
+
/** Show the cell's text on top of the bar. Default true. */
|
|
29
|
+
showValue?: boolean;
|
|
30
|
+
};
|
|
31
|
+
export type IconSetName = 'arrows' | 'traffic' | 'triangles';
|
|
32
|
+
export type IconSetFormat = {
|
|
33
|
+
type: 'iconSet';
|
|
34
|
+
set?: IconSetName;
|
|
35
|
+
/** Ascending breakpoints. n thresholds => n+1 buckets/icons. */
|
|
36
|
+
thresholds: number[];
|
|
37
|
+
/** Hide the numeric text, show only the icon. Default false. */
|
|
38
|
+
iconOnly?: boolean;
|
|
39
|
+
};
|
|
40
|
+
export type RuleFormat<TData = unknown> = {
|
|
41
|
+
type: 'rule';
|
|
42
|
+
/** Apply the styles below when this returns true. */
|
|
43
|
+
when: (ctx: {
|
|
44
|
+
value: unknown;
|
|
45
|
+
row: TData;
|
|
46
|
+
}) => boolean;
|
|
47
|
+
background?: string;
|
|
48
|
+
color?: string;
|
|
49
|
+
fontWeight?: string | number;
|
|
50
|
+
};
|
|
51
|
+
export type ConditionalFormatSpec<TData = unknown> = ColorScaleFormat | DataBarFormat | IconSetFormat | RuleFormat<TData>;
|
|
52
|
+
/** A format scoped to specific columns (omit `columns` to apply to all). */
|
|
53
|
+
export type ConditionalFormat<TData = unknown> = ConditionalFormatSpec<TData> & {
|
|
54
|
+
columns?: ReadonlyArray<string>;
|
|
55
|
+
};
|
|
56
|
+
export type ResolvedCellFormat = {
|
|
57
|
+
background?: string;
|
|
58
|
+
color?: string;
|
|
59
|
+
fontWeight?: string | number;
|
|
60
|
+
dataBar?: {
|
|
61
|
+
percent: number;
|
|
62
|
+
color: string;
|
|
63
|
+
fromRight: boolean;
|
|
64
|
+
};
|
|
65
|
+
icon?: string;
|
|
66
|
+
iconOnly?: boolean;
|
|
67
|
+
};
|
|
68
|
+
export type ColumnStat = {
|
|
69
|
+
min: number;
|
|
70
|
+
max: number;
|
|
71
|
+
};
|
|
72
|
+
/** Min/max of a column's finite numeric values, or null if none. */
|
|
73
|
+
export declare function computeColumnStat(values: Iterable<unknown>): ColumnStat | null;
|
|
74
|
+
/**
|
|
75
|
+
* Pick a readable text color (near-black or white) for a given hex
|
|
76
|
+
* background, using WCAG relative luminance. Lets a color-scale fill or a
|
|
77
|
+
* tinted rule keep its text legible without the caller hand-picking a color.
|
|
78
|
+
* Returns null when the background isn't a parseable hex (e.g. a CSS var),
|
|
79
|
+
* so the caller can leave the default text color in place.
|
|
80
|
+
*/
|
|
81
|
+
export declare function contrastText(bg: string): string | null;
|
|
82
|
+
/** Linear interpolate two hex colors. Falls back to `a` if parsing fails. */
|
|
83
|
+
export declare function lerpColor(a: string, b: string, t: number): string;
|
|
84
|
+
/**
|
|
85
|
+
* Resolve every format that applies to one cell into a single set of visual
|
|
86
|
+
* primitives. Later-listed formats override earlier ones for the same
|
|
87
|
+
* property, so order your `conditionalFormats` from general to specific.
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveCellFormat<TData = unknown>(value: unknown, row: TData, columnId: string, formats: ReadonlyArray<ConditionalFormat<TData>>, stat: ColumnStat | null): ResolvedCellFormat;
|
|
90
|
+
/** Column ids that need a numeric min/max precomputed (colorScale/dataBar). */
|
|
91
|
+
export declare function formatsNeedingStats(formats: ReadonlyArray<ConditionalFormat<any>>): boolean;
|