@svgrid/grid 2.2.27 → 2.2.29
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/README.md +199 -199
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +179 -179
- package/dist/GridMenus.svelte +782 -734
- package/dist/SvAutoComplete.svelte +165 -165
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +101 -101
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +275 -275
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +160 -160
- package/dist/SvDrawer.svelte +254 -250
- package/dist/SvDropDownList.svelte +374 -374
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGrid.controller.svelte.d.ts +14 -1
- package/dist/SvGrid.controller.svelte.js +112 -28
- package/dist/SvGrid.css +2704 -2723
- package/dist/SvGrid.svelte +3361 -3333
- package/dist/SvGrid.types.d.ts +20 -5
- package/dist/SvGridChart.svelte +1724 -1724
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +696 -696
- package/dist/SvGridSelect.svelte +259 -259
- package/dist/SvGroupCell.svelte +109 -109
- package/dist/SvListBox.svelte +326 -287
- package/dist/SvListBox.svelte.d.ts +10 -1
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +281 -281
- package/dist/SvNumberInput.svelte +168 -168
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +133 -131
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +200 -200
- package/dist/SvSwitchButton.svelte +107 -107
- package/dist/SvTagsInput.svelte +112 -112
- package/dist/SvTextInput.svelte +147 -147
- package/dist/SvTimePicker.svelte +245 -245
- package/dist/SvToaster.svelte +159 -159
- package/dist/SvToggleButton.svelte +85 -85
- package/dist/SvTooltip.svelte +161 -161
- package/dist/SvTour.svelte +204 -204
- package/dist/SvTree.svelte +437 -437
- package/dist/SvTreeSelect.svelte +233 -233
- package/dist/cdn/GridMenus-Da_p7SKS.js +486 -0
- package/dist/cdn/GridMenus-Ds3OpzZT.js +490 -0
- package/dist/cdn/{SvDateTimePicker-B1sDc9Pz.js → SvDateTimePicker-B8GopjhC.js} +2 -2
- package/dist/cdn/{SvDateTimePicker-Bh0FTs1E.js → SvDateTimePicker-DB0aIEk8.js} +2 -2
- package/dist/cdn/{SvGridChart-Bgk4hDs8.js → SvGridChart-BcLCYvw9.js} +1 -1
- package/dist/cdn/{SvGridChart-BKQsO5fZ.js → SvGridChart-BhnFdLd0.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DkCc2Z7b.js → SvGridChartPanel-Cmx9DHAS.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DSkHIQDd.js → SvGridChartPanel-DvjwxLvK.js} +1 -1
- package/dist/cdn/{SvGridChartView-BrbVaELm.js → SvGridChartView-B40DTJSX.js} +1 -1
- package/dist/cdn/{SvGridChartView-i9rWvSKg.js → SvGridChartView-PwqI4s02.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D08sVmAy.js → SvGridDropdown-DjALc_4f.js} +1 -1
- package/dist/cdn/{SvGridDropdown-CGxxopQ4.js → SvGridDropdown-hpnHvOGt.js} +1 -1
- package/dist/cdn/{src-cgs1wcUS.js → src-BLJfdyL0.js} +4034 -3889
- package/dist/cdn/{src-Cw8d9U8j.js → src-DYDCiC0D.js} +7053 -6908
- package/dist/cdn/svgrid.js +9 -9
- package/dist/cdn/svgrid.svelte-external.js +9 -9
- package/dist/chart-export.js +8 -8
- package/dist/column-groups.js +8 -10
- package/dist/column-id.d.ts +9 -0
- package/dist/column-id.js +25 -0
- package/dist/core.js +2 -1
- package/dist/createListbox.svelte.d.ts +15 -2
- package/dist/createListbox.svelte.js +27 -6
- package/dist/editing.d.ts +1 -0
- package/dist/editing.js +45 -10
- package/dist/filtering/excel-filters.d.ts +30 -10
- package/dist/filtering/excel-filters.js +96 -15
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/menus.d.ts +2 -0
- package/dist/menus.js +25 -0
- package/dist/row-resize.js +40 -0
- package/dist/selection.js +1 -1
- package/package.json +10 -10
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +179 -179
- package/src/GridMenus.svelte +782 -734
- package/src/SvAutoComplete.svelte +165 -165
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +101 -101
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +275 -275
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +160 -160
- package/src/SvDrawer.svelte +254 -250
- package/src/SvDrawer.test.ts +18 -1
- package/src/SvDropDownList.svelte +374 -374
- package/src/SvDurationInput.svelte +126 -126
- package/src/SvField.svelte +293 -293
- package/src/SvForm.svelte +437 -437
- package/src/SvForm.test.ts +411 -411
- package/src/SvGrid.controller.svelte.ts +3335 -3239
- package/src/SvGrid.css +2704 -2723
- package/src/SvGrid.svelte +3361 -3333
- package/src/SvGrid.types.ts +1628 -1613
- package/src/SvGridChart.svelte +1724 -1724
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +696 -696
- package/src/SvGridSelect.svelte +259 -259
- package/src/SvGroupCell.svelte +109 -109
- package/src/SvListBox.svelte +326 -287
- package/src/SvMaskedInput.svelte +122 -122
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -97
- package/src/SvMenuList.svelte +146 -146
- package/src/SvMultiSelect.svelte +281 -281
- package/src/SvNavPane.test.ts +15 -4
- package/src/SvNumberInput.svelte +168 -168
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +133 -131
- package/src/SvPopover.svelte +197 -197
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +200 -200
- package/src/SvSwitchButton.svelte +107 -107
- package/src/SvTagsInput.svelte +112 -112
- package/src/SvTextInput.svelte +147 -147
- package/src/SvTimePicker.svelte +245 -245
- package/src/SvToaster.svelte +159 -159
- package/src/SvToaster.test.ts +95 -95
- package/src/SvToggleButton.svelte +85 -85
- package/src/SvTooltip.svelte +161 -161
- package/src/SvTour.svelte +204 -204
- package/src/SvTree.svelte +437 -437
- package/src/SvTreeSelect.svelte +233 -233
- package/src/a11y/dismissable.test.ts +119 -119
- package/src/a11y/dismissable.ts +114 -114
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/ai.test.ts +502 -502
- package/src/ai.ts +1391 -1391
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +895 -895
- package/src/builtin-editors.grid.test.ts +85 -85
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/chart-export.ts +201 -201
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2296 -2296
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/column-groups.test.ts +48 -0
- package/src/column-groups.ts +8 -10
- package/src/column-id.ts +30 -0
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1112 -1111
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCombobox.svelte.ts +191 -191
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +168 -168
- package/src/createForm.svelte.ts +386 -386
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createListbox.svelte.ts +39 -7
- package/src/createMenu.svelte.ts +225 -225
- package/src/createPopoverSelect.svelte.ts +213 -213
- package/src/createSlider.svelte.ts +191 -191
- package/src/createTooltip.svelte.ts +144 -144
- package/src/createTree.svelte.ts +319 -319
- package/src/editing.test.ts +949 -859
- package/src/editing.ts +709 -675
- package/src/editor-contract.ts +157 -157
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +598 -598
- package/src/filter-operators.ts +157 -157
- package/src/filtering/excel-filters.test.ts +44 -1
- package/src/filtering/excel-filters.ts +91 -14
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/index.ts +715 -713
- package/src/js-scroller.svelte.ts +173 -173
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -49
- package/src/list-nav.ts +29 -29
- package/src/list-option.test.ts +56 -56
- package/src/list-option.ts +172 -172
- package/src/menus.test.ts +736 -700
- package/src/menus.ts +679 -652
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/render-component.ts +28 -28
- package/src/row-resize.test.ts +100 -0
- package/src/row-resize.ts +36 -0
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +872 -872
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +600 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -413
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +524 -524
- package/src/svgrid.api-extensions.test.ts +8 -3
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.cell-dom-ids.test.ts +96 -0
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.conditional-stat-scope.test.ts +111 -0
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +308 -0
- package/src/svgrid.filter-menu-scroll.test.ts +3 -1
- package/src/svgrid.group-header-ids.test.ts +121 -0
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +215 -215
- package/src/toast-store.svelte.ts +246 -246
- package/src/toast-store.test.ts +147 -147
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- package/src/ui-range.test.ts +70 -70
- package/src/ui-selection.test.ts +155 -155
- package/src/ui-tier1.test.ts +142 -142
- package/src/virtual.test.ts +88 -88
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
- package/themes/ember.css +71 -71
- package/dist/cdn/GridMenus-BzxEYNtW.js +0 -421
- package/dist/cdn/GridMenus-Dr-nGEXn.js +0 -417
package/src/sparkline.test.ts
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { buildSparkline, toSparklineValues } from './sparkline'
|
|
3
|
-
|
|
4
|
-
describe('toSparklineValues', () => {
|
|
5
|
-
it('passes through number arrays and drops non-finite', () => {
|
|
6
|
-
expect(toSparklineValues([1, 2, NaN, 3])).toEqual([1, 2, 3])
|
|
7
|
-
})
|
|
8
|
-
it('parses comma / space separated strings', () => {
|
|
9
|
-
expect(toSparklineValues('1, 2 3,4')).toEqual([1, 2, 3, 4])
|
|
10
|
-
})
|
|
11
|
-
it('returns [] for empty / unusable input', () => {
|
|
12
|
-
expect(toSparklineValues(null)).toEqual([])
|
|
13
|
-
expect(toSparklineValues('')).toEqual([])
|
|
14
|
-
expect(toSparklineValues({})).toEqual([])
|
|
15
|
-
})
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
describe('buildSparkline', () => {
|
|
19
|
-
it('returns null for empty data', () => {
|
|
20
|
-
expect(buildSparkline([])).toBeNull()
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('builds a line path through every point', () => {
|
|
24
|
-
const g = buildSparkline([0, 5, 10], { type: 'line', width: 100, height: 20 })!
|
|
25
|
-
expect(g.type).toBe('line')
|
|
26
|
-
expect(g.linePath.startsWith('M')).toBe(true)
|
|
27
|
-
// three points => one M + two L commands
|
|
28
|
-
expect((g.linePath.match(/[ML]/g) ?? []).length).toBe(3)
|
|
29
|
-
expect(g.lastPoint).not.toBeNull()
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('closes the area path back to the baseline', () => {
|
|
33
|
-
const g = buildSparkline([1, 2, 3], { type: 'area' })!
|
|
34
|
-
expect(g.areaPath.endsWith('Z')).toBe(true)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('renders a visible mark for a single data point (#84)', () => {
|
|
38
|
-
// A lone point used to produce just "M x,y" (a moveto), which draws
|
|
39
|
-
// nothing. The fix centres it and emits a short segment.
|
|
40
|
-
const g = buildSparkline([5], { type: 'line', width: 100, height: 20 })!
|
|
41
|
-
expect(g.linePath).not.toBe('')
|
|
42
|
-
expect(g.linePath).toMatch(/M[\d.]+,[\d.]+ L[\d.]+,[\d.]+/)
|
|
43
|
-
expect(g.lastPoint).not.toBeNull()
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('emits one bar per value and flags negatives', () => {
|
|
47
|
-
const g = buildSparkline([-2, 4, -1], { type: 'bar' })!
|
|
48
|
-
expect(g.bars).toHaveLength(3)
|
|
49
|
-
expect(g.bars[0]!.negative).toBe(true)
|
|
50
|
-
expect(g.bars[1]!.negative).toBe(false)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('winloss uses sign only, equal-height up/down bars', () => {
|
|
54
|
-
const g = buildSparkline([1, -1, 1], { type: 'winloss', height: 20 })!
|
|
55
|
-
expect(g.bars).toHaveLength(3)
|
|
56
|
-
expect(g.bars[0]!.negative).toBe(false)
|
|
57
|
-
expect(g.bars[1]!.negative).toBe(true)
|
|
58
|
-
// up and down bars are the same height (sign-only chart)
|
|
59
|
-
expect(g.bars[0]!.h).toBeCloseTo(g.bars[1]!.h)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('honors a fixed min/max scale', () => {
|
|
63
|
-
const a = buildSparkline([5, 5, 5], { type: 'line', min: 0, max: 10 })!
|
|
64
|
-
// flat series on a 0..10 scale sits at the vertical middle, not the top
|
|
65
|
-
const ys = a.linePath.match(/,(-?\d+(\.\d+)?)/g)!.map((s) => Number(s.slice(1)))
|
|
66
|
-
expect(new Set(ys).size).toBe(1)
|
|
67
|
-
})
|
|
68
|
-
})
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { buildSparkline, toSparklineValues } from './sparkline'
|
|
3
|
+
|
|
4
|
+
describe('toSparklineValues', () => {
|
|
5
|
+
it('passes through number arrays and drops non-finite', () => {
|
|
6
|
+
expect(toSparklineValues([1, 2, NaN, 3])).toEqual([1, 2, 3])
|
|
7
|
+
})
|
|
8
|
+
it('parses comma / space separated strings', () => {
|
|
9
|
+
expect(toSparklineValues('1, 2 3,4')).toEqual([1, 2, 3, 4])
|
|
10
|
+
})
|
|
11
|
+
it('returns [] for empty / unusable input', () => {
|
|
12
|
+
expect(toSparklineValues(null)).toEqual([])
|
|
13
|
+
expect(toSparklineValues('')).toEqual([])
|
|
14
|
+
expect(toSparklineValues({})).toEqual([])
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('buildSparkline', () => {
|
|
19
|
+
it('returns null for empty data', () => {
|
|
20
|
+
expect(buildSparkline([])).toBeNull()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('builds a line path through every point', () => {
|
|
24
|
+
const g = buildSparkline([0, 5, 10], { type: 'line', width: 100, height: 20 })!
|
|
25
|
+
expect(g.type).toBe('line')
|
|
26
|
+
expect(g.linePath.startsWith('M')).toBe(true)
|
|
27
|
+
// three points => one M + two L commands
|
|
28
|
+
expect((g.linePath.match(/[ML]/g) ?? []).length).toBe(3)
|
|
29
|
+
expect(g.lastPoint).not.toBeNull()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('closes the area path back to the baseline', () => {
|
|
33
|
+
const g = buildSparkline([1, 2, 3], { type: 'area' })!
|
|
34
|
+
expect(g.areaPath.endsWith('Z')).toBe(true)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('renders a visible mark for a single data point (#84)', () => {
|
|
38
|
+
// A lone point used to produce just "M x,y" (a moveto), which draws
|
|
39
|
+
// nothing. The fix centres it and emits a short segment.
|
|
40
|
+
const g = buildSparkline([5], { type: 'line', width: 100, height: 20 })!
|
|
41
|
+
expect(g.linePath).not.toBe('')
|
|
42
|
+
expect(g.linePath).toMatch(/M[\d.]+,[\d.]+ L[\d.]+,[\d.]+/)
|
|
43
|
+
expect(g.lastPoint).not.toBeNull()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('emits one bar per value and flags negatives', () => {
|
|
47
|
+
const g = buildSparkline([-2, 4, -1], { type: 'bar' })!
|
|
48
|
+
expect(g.bars).toHaveLength(3)
|
|
49
|
+
expect(g.bars[0]!.negative).toBe(true)
|
|
50
|
+
expect(g.bars[1]!.negative).toBe(false)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('winloss uses sign only, equal-height up/down bars', () => {
|
|
54
|
+
const g = buildSparkline([1, -1, 1], { type: 'winloss', height: 20 })!
|
|
55
|
+
expect(g.bars).toHaveLength(3)
|
|
56
|
+
expect(g.bars[0]!.negative).toBe(false)
|
|
57
|
+
expect(g.bars[1]!.negative).toBe(true)
|
|
58
|
+
// up and down bars are the same height (sign-only chart)
|
|
59
|
+
expect(g.bars[0]!.h).toBeCloseTo(g.bars[1]!.h)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('honors a fixed min/max scale', () => {
|
|
63
|
+
const a = buildSparkline([5, 5, 5], { type: 'line', min: 0, max: 10 })!
|
|
64
|
+
// flat series on a 0..10 scale sits at the vertical middle, not the top
|
|
65
|
+
const ys = a.linePath.match(/,(-?\d+(\.\d+)?)/g)!.map((s) => Number(s.slice(1)))
|
|
66
|
+
expect(new Set(ys).size).toBe(1)
|
|
67
|
+
})
|
|
68
|
+
})
|
package/src/sparkline.ts
CHANGED
|
@@ -1,169 +1,169 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sparkline geometry. Pure functions that turn an array of numbers into the
|
|
3
|
-
* SVG primitives a tiny in-cell chart needs - no DOM, so it is trivially
|
|
4
|
-
* unit-testable and the `<SvGrid>` render component just paints the result.
|
|
5
|
-
*
|
|
6
|
-
* Four chart types, mirroring the de-facto enterprise-grid set:
|
|
7
|
-
* - `line` a single polyline
|
|
8
|
-
* - `area` a polyline plus a filled area down to the baseline
|
|
9
|
-
* - `bar` one column per value, scaled from the dataset min..max
|
|
10
|
-
* - `winloss` fixed-height up/down bars (sign only), e.g. W/L streaks
|
|
11
|
-
*/
|
|
12
|
-
export type SparklineType = 'line' | 'area' | 'bar' | 'winloss'
|
|
13
|
-
|
|
14
|
-
export type SparklineConfig = {
|
|
15
|
-
type?: SparklineType
|
|
16
|
-
/** Stroke (line/area) or positive fill (bar/winloss). */
|
|
17
|
-
color?: string
|
|
18
|
-
/** Fill for negative bars / losses. Defaults to a red. */
|
|
19
|
-
negativeColor?: string
|
|
20
|
-
width?: number
|
|
21
|
-
height?: number
|
|
22
|
-
/** Fix the value scale instead of deriving it from the row's own data. */
|
|
23
|
-
min?: number
|
|
24
|
-
max?: number
|
|
25
|
-
lineWidth?: number
|
|
26
|
-
/** Draw a dot on the last point (line/area). Default true. */
|
|
27
|
-
lastPoint?: boolean
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export type SparklineBar = {
|
|
31
|
-
x: number
|
|
32
|
-
y: number
|
|
33
|
-
w: number
|
|
34
|
-
h: number
|
|
35
|
-
negative: boolean
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export type SparklineGeometry = {
|
|
39
|
-
width: number
|
|
40
|
-
height: number
|
|
41
|
-
type: SparklineType
|
|
42
|
-
color: string
|
|
43
|
-
negativeColor: string
|
|
44
|
-
lineWidth: number
|
|
45
|
-
/** `line` / `area`: the polyline through every point. */
|
|
46
|
-
linePath: string
|
|
47
|
-
/** `area`: closed path filled down to the baseline. */
|
|
48
|
-
areaPath: string
|
|
49
|
-
/** `bar` / `winloss`: one rect per value. */
|
|
50
|
-
bars: SparklineBar[]
|
|
51
|
-
/** `line` / `area`: the final point, for the end-cap dot. */
|
|
52
|
-
lastPoint: { x: number; y: number } | null
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const DEFAULTS = {
|
|
56
|
-
width: 88,
|
|
57
|
-
height: 22,
|
|
58
|
-
color: 'var(--sg-accent, #2563eb)',
|
|
59
|
-
negativeColor: '#ef4444',
|
|
60
|
-
lineWidth: 1.5,
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Coerce loose cell values (arrays, comma strings) into a number array. */
|
|
64
|
-
export function toSparklineValues(value: unknown): number[] {
|
|
65
|
-
if (Array.isArray(value)) {
|
|
66
|
-
return value.map((v) => Number(v)).filter((n) => Number.isFinite(n))
|
|
67
|
-
}
|
|
68
|
-
if (typeof value === 'string' && value.trim()) {
|
|
69
|
-
return value
|
|
70
|
-
.split(/[,\s]+/)
|
|
71
|
-
.map((v) => Number(v))
|
|
72
|
-
.filter((n) => Number.isFinite(n))
|
|
73
|
-
}
|
|
74
|
-
return []
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function buildSparkline(
|
|
78
|
-
values: number[],
|
|
79
|
-
cfg: SparklineConfig = {},
|
|
80
|
-
): SparklineGeometry | null {
|
|
81
|
-
if (!values.length) return null
|
|
82
|
-
|
|
83
|
-
const width = cfg.width ?? DEFAULTS.width
|
|
84
|
-
const height = cfg.height ?? DEFAULTS.height
|
|
85
|
-
const type = cfg.type ?? 'line'
|
|
86
|
-
const color = cfg.color ?? DEFAULTS.color
|
|
87
|
-
const negativeColor = cfg.negativeColor ?? DEFAULTS.negativeColor
|
|
88
|
-
const lineWidth = cfg.lineWidth ?? DEFAULTS.lineWidth
|
|
89
|
-
const pad = Math.ceil(lineWidth) + 1
|
|
90
|
-
const innerW = Math.max(1, width - pad * 2)
|
|
91
|
-
const innerH = Math.max(1, height - pad * 2)
|
|
92
|
-
|
|
93
|
-
const base: Omit<
|
|
94
|
-
SparklineGeometry,
|
|
95
|
-
'linePath' | 'areaPath' | 'bars' | 'lastPoint'
|
|
96
|
-
> = { width, height, type, color, negativeColor, lineWidth }
|
|
97
|
-
|
|
98
|
-
if (type === 'winloss') {
|
|
99
|
-
const n = values.length
|
|
100
|
-
const gap = n > 1 ? Math.min(2, innerW / n / 3) : 0
|
|
101
|
-
const slot = innerW / n
|
|
102
|
-
const barW = Math.max(1, slot - gap)
|
|
103
|
-
const mid = height / 2
|
|
104
|
-
const h = innerH / 2
|
|
105
|
-
const bars: SparklineBar[] = values.map((v, i) => {
|
|
106
|
-
const x = pad + i * slot + (slot - barW) / 2
|
|
107
|
-
if (v > 0) return { x, y: mid - h, w: barW, h, negative: false }
|
|
108
|
-
if (v < 0) return { x, y: mid, w: barW, h, negative: true }
|
|
109
|
-
return { x, y: mid - 1, w: barW, h: 2, negative: false }
|
|
110
|
-
})
|
|
111
|
-
return { ...base, linePath: '', areaPath: '', bars, lastPoint: null }
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const min = cfg.min ?? Math.min(...values)
|
|
115
|
-
const max = cfg.max ?? Math.max(...values)
|
|
116
|
-
const span = max - min || 1
|
|
117
|
-
|
|
118
|
-
if (type === 'bar') {
|
|
119
|
-
const n = values.length
|
|
120
|
-
const gap = n > 1 ? Math.min(2, innerW / n / 3) : 0
|
|
121
|
-
const slot = innerW / n
|
|
122
|
-
const barW = Math.max(1, slot - gap)
|
|
123
|
-
// Bars grow from a zero baseline when the data spans zero, else from min.
|
|
124
|
-
const baseVal = min < 0 && max > 0 ? 0 : min
|
|
125
|
-
const baseY = pad + innerH - ((baseVal - min) / span) * innerH
|
|
126
|
-
const bars: SparklineBar[] = values.map((v, i) => {
|
|
127
|
-
const x = pad + i * slot + (slot - barW) / 2
|
|
128
|
-
const vy = pad + innerH - ((v - min) / span) * innerH
|
|
129
|
-
const top = Math.min(vy, baseY)
|
|
130
|
-
const h = Math.max(1, Math.abs(vy - baseY))
|
|
131
|
-
return { x, y: top, w: barW, h, negative: v < baseVal }
|
|
132
|
-
})
|
|
133
|
-
return { ...base, linePath: '', areaPath: '', bars, lastPoint: null }
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// line / area
|
|
137
|
-
const n = values.length
|
|
138
|
-
const stepX = n > 1 ? innerW / (n - 1) : 0
|
|
139
|
-
const pts = values.map((v, i) => ({
|
|
140
|
-
// A single point has no span to walk, so centre it rather than pinning it
|
|
141
|
-
// to the left edge.
|
|
142
|
-
x: n === 1 ? pad + innerW / 2 : pad + i * stepX,
|
|
143
|
-
y: pad + innerH - ((v - min) / span) * innerH,
|
|
144
|
-
}))
|
|
145
|
-
const linePath =
|
|
146
|
-
n === 1
|
|
147
|
-
? // One point produces only a moveto, which draws nothing. Emit a tiny
|
|
148
|
-
// horizontal segment so a single value still renders a visible mark.
|
|
149
|
-
`M${round(pts[0]!.x - 1)},${round(pts[0]!.y)} L${round(pts[0]!.x + 1)},${round(pts[0]!.y)}`
|
|
150
|
-
: pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${round(p.x)},${round(p.y)}`).join(' ')
|
|
151
|
-
const first = pts[0]!
|
|
152
|
-
const last = pts[pts.length - 1]!
|
|
153
|
-
const baseY = pad + innerH
|
|
154
|
-
const areaPath =
|
|
155
|
-
type === 'area'
|
|
156
|
-
? `${linePath} L${round(last.x)},${round(baseY)} L${round(first.x)},${round(baseY)} Z`
|
|
157
|
-
: ''
|
|
158
|
-
return {
|
|
159
|
-
...base,
|
|
160
|
-
linePath,
|
|
161
|
-
areaPath,
|
|
162
|
-
bars: [],
|
|
163
|
-
lastPoint: cfg.lastPoint === false ? null : { x: last.x, y: last.y },
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function round(n: number): number {
|
|
168
|
-
return Math.round(n * 100) / 100
|
|
169
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Sparkline geometry. Pure functions that turn an array of numbers into the
|
|
3
|
+
* SVG primitives a tiny in-cell chart needs - no DOM, so it is trivially
|
|
4
|
+
* unit-testable and the `<SvGrid>` render component just paints the result.
|
|
5
|
+
*
|
|
6
|
+
* Four chart types, mirroring the de-facto enterprise-grid set:
|
|
7
|
+
* - `line` a single polyline
|
|
8
|
+
* - `area` a polyline plus a filled area down to the baseline
|
|
9
|
+
* - `bar` one column per value, scaled from the dataset min..max
|
|
10
|
+
* - `winloss` fixed-height up/down bars (sign only), e.g. W/L streaks
|
|
11
|
+
*/
|
|
12
|
+
export type SparklineType = 'line' | 'area' | 'bar' | 'winloss'
|
|
13
|
+
|
|
14
|
+
export type SparklineConfig = {
|
|
15
|
+
type?: SparklineType
|
|
16
|
+
/** Stroke (line/area) or positive fill (bar/winloss). */
|
|
17
|
+
color?: string
|
|
18
|
+
/** Fill for negative bars / losses. Defaults to a red. */
|
|
19
|
+
negativeColor?: string
|
|
20
|
+
width?: number
|
|
21
|
+
height?: number
|
|
22
|
+
/** Fix the value scale instead of deriving it from the row's own data. */
|
|
23
|
+
min?: number
|
|
24
|
+
max?: number
|
|
25
|
+
lineWidth?: number
|
|
26
|
+
/** Draw a dot on the last point (line/area). Default true. */
|
|
27
|
+
lastPoint?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type SparklineBar = {
|
|
31
|
+
x: number
|
|
32
|
+
y: number
|
|
33
|
+
w: number
|
|
34
|
+
h: number
|
|
35
|
+
negative: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type SparklineGeometry = {
|
|
39
|
+
width: number
|
|
40
|
+
height: number
|
|
41
|
+
type: SparklineType
|
|
42
|
+
color: string
|
|
43
|
+
negativeColor: string
|
|
44
|
+
lineWidth: number
|
|
45
|
+
/** `line` / `area`: the polyline through every point. */
|
|
46
|
+
linePath: string
|
|
47
|
+
/** `area`: closed path filled down to the baseline. */
|
|
48
|
+
areaPath: string
|
|
49
|
+
/** `bar` / `winloss`: one rect per value. */
|
|
50
|
+
bars: SparklineBar[]
|
|
51
|
+
/** `line` / `area`: the final point, for the end-cap dot. */
|
|
52
|
+
lastPoint: { x: number; y: number } | null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const DEFAULTS = {
|
|
56
|
+
width: 88,
|
|
57
|
+
height: 22,
|
|
58
|
+
color: 'var(--sg-accent, #2563eb)',
|
|
59
|
+
negativeColor: '#ef4444',
|
|
60
|
+
lineWidth: 1.5,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Coerce loose cell values (arrays, comma strings) into a number array. */
|
|
64
|
+
export function toSparklineValues(value: unknown): number[] {
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
return value.map((v) => Number(v)).filter((n) => Number.isFinite(n))
|
|
67
|
+
}
|
|
68
|
+
if (typeof value === 'string' && value.trim()) {
|
|
69
|
+
return value
|
|
70
|
+
.split(/[,\s]+/)
|
|
71
|
+
.map((v) => Number(v))
|
|
72
|
+
.filter((n) => Number.isFinite(n))
|
|
73
|
+
}
|
|
74
|
+
return []
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildSparkline(
|
|
78
|
+
values: number[],
|
|
79
|
+
cfg: SparklineConfig = {},
|
|
80
|
+
): SparklineGeometry | null {
|
|
81
|
+
if (!values.length) return null
|
|
82
|
+
|
|
83
|
+
const width = cfg.width ?? DEFAULTS.width
|
|
84
|
+
const height = cfg.height ?? DEFAULTS.height
|
|
85
|
+
const type = cfg.type ?? 'line'
|
|
86
|
+
const color = cfg.color ?? DEFAULTS.color
|
|
87
|
+
const negativeColor = cfg.negativeColor ?? DEFAULTS.negativeColor
|
|
88
|
+
const lineWidth = cfg.lineWidth ?? DEFAULTS.lineWidth
|
|
89
|
+
const pad = Math.ceil(lineWidth) + 1
|
|
90
|
+
const innerW = Math.max(1, width - pad * 2)
|
|
91
|
+
const innerH = Math.max(1, height - pad * 2)
|
|
92
|
+
|
|
93
|
+
const base: Omit<
|
|
94
|
+
SparklineGeometry,
|
|
95
|
+
'linePath' | 'areaPath' | 'bars' | 'lastPoint'
|
|
96
|
+
> = { width, height, type, color, negativeColor, lineWidth }
|
|
97
|
+
|
|
98
|
+
if (type === 'winloss') {
|
|
99
|
+
const n = values.length
|
|
100
|
+
const gap = n > 1 ? Math.min(2, innerW / n / 3) : 0
|
|
101
|
+
const slot = innerW / n
|
|
102
|
+
const barW = Math.max(1, slot - gap)
|
|
103
|
+
const mid = height / 2
|
|
104
|
+
const h = innerH / 2
|
|
105
|
+
const bars: SparklineBar[] = values.map((v, i) => {
|
|
106
|
+
const x = pad + i * slot + (slot - barW) / 2
|
|
107
|
+
if (v > 0) return { x, y: mid - h, w: barW, h, negative: false }
|
|
108
|
+
if (v < 0) return { x, y: mid, w: barW, h, negative: true }
|
|
109
|
+
return { x, y: mid - 1, w: barW, h: 2, negative: false }
|
|
110
|
+
})
|
|
111
|
+
return { ...base, linePath: '', areaPath: '', bars, lastPoint: null }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const min = cfg.min ?? Math.min(...values)
|
|
115
|
+
const max = cfg.max ?? Math.max(...values)
|
|
116
|
+
const span = max - min || 1
|
|
117
|
+
|
|
118
|
+
if (type === 'bar') {
|
|
119
|
+
const n = values.length
|
|
120
|
+
const gap = n > 1 ? Math.min(2, innerW / n / 3) : 0
|
|
121
|
+
const slot = innerW / n
|
|
122
|
+
const barW = Math.max(1, slot - gap)
|
|
123
|
+
// Bars grow from a zero baseline when the data spans zero, else from min.
|
|
124
|
+
const baseVal = min < 0 && max > 0 ? 0 : min
|
|
125
|
+
const baseY = pad + innerH - ((baseVal - min) / span) * innerH
|
|
126
|
+
const bars: SparklineBar[] = values.map((v, i) => {
|
|
127
|
+
const x = pad + i * slot + (slot - barW) / 2
|
|
128
|
+
const vy = pad + innerH - ((v - min) / span) * innerH
|
|
129
|
+
const top = Math.min(vy, baseY)
|
|
130
|
+
const h = Math.max(1, Math.abs(vy - baseY))
|
|
131
|
+
return { x, y: top, w: barW, h, negative: v < baseVal }
|
|
132
|
+
})
|
|
133
|
+
return { ...base, linePath: '', areaPath: '', bars, lastPoint: null }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// line / area
|
|
137
|
+
const n = values.length
|
|
138
|
+
const stepX = n > 1 ? innerW / (n - 1) : 0
|
|
139
|
+
const pts = values.map((v, i) => ({
|
|
140
|
+
// A single point has no span to walk, so centre it rather than pinning it
|
|
141
|
+
// to the left edge.
|
|
142
|
+
x: n === 1 ? pad + innerW / 2 : pad + i * stepX,
|
|
143
|
+
y: pad + innerH - ((v - min) / span) * innerH,
|
|
144
|
+
}))
|
|
145
|
+
const linePath =
|
|
146
|
+
n === 1
|
|
147
|
+
? // One point produces only a moveto, which draws nothing. Emit a tiny
|
|
148
|
+
// horizontal segment so a single value still renders a visible mark.
|
|
149
|
+
`M${round(pts[0]!.x - 1)},${round(pts[0]!.y)} L${round(pts[0]!.x + 1)},${round(pts[0]!.y)}`
|
|
150
|
+
: pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${round(p.x)},${round(p.y)}`).join(' ')
|
|
151
|
+
const first = pts[0]!
|
|
152
|
+
const last = pts[pts.length - 1]!
|
|
153
|
+
const baseY = pad + innerH
|
|
154
|
+
const areaPath =
|
|
155
|
+
type === 'area'
|
|
156
|
+
? `${linePath} L${round(last.x)},${round(baseY)} L${round(first.x)},${round(baseY)} Z`
|
|
157
|
+
: ''
|
|
158
|
+
return {
|
|
159
|
+
...base,
|
|
160
|
+
linePath,
|
|
161
|
+
areaPath,
|
|
162
|
+
bars: [],
|
|
163
|
+
lastPoint: cfg.lastPoint === false ? null : { x: last.x, y: last.y },
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function round(n: number): number {
|
|
168
|
+
return Math.round(n * 100) / 100
|
|
169
|
+
}
|