@cdc/core 4.25.11 → 4.26.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/.claude/agents/qa-test-developer.md +126 -0
  2. package/CLAUDE.local.md +67 -0
  3. package/_stories/Gallery.Charts.stories.tsx +300 -0
  4. package/_stories/Gallery.DataBite.stories.tsx +79 -0
  5. package/_stories/Gallery.Maps.stories.tsx +239 -0
  6. package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
  7. package/_stories/PageART.stories.tsx +193 -0
  8. package/_stories/PageBRFSS.stories.tsx +294 -0
  9. package/_stories/PageCancerRegistries.stories.tsx +199 -0
  10. package/_stories/PageEasternEquineEncephalitis.stories.tsx +216 -0
  11. package/_stories/PageExcessiveAlcoholUse.stories.tsx +201 -0
  12. package/_stories/PageMaternalMortality.stories.tsx +193 -0
  13. package/_stories/PageOralHealth.stories.tsx +201 -0
  14. package/_stories/PageRespiratory.stories.tsx +332 -0
  15. package/_stories/PageSmokingTobacco.stories.tsx +200 -0
  16. package/_stories/PageStateDiabetesProfiles.stories.tsx +201 -0
  17. package/_stories/PageWastewater.stories.tsx +477 -0
  18. package/_stories/VegaImport.stories.tsx +401 -0
  19. package/_stories/vega-fixtures/bars-with-line.json +444 -0
  20. package/_stories/vega-fixtures/bars.json +58 -0
  21. package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
  22. package/_stories/vega-fixtures/combo.json +68 -0
  23. package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
  24. package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
  25. package/_stories/vega-fixtures/horizontal-bar.json +427 -0
  26. package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
  27. package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
  28. package/_stories/vega-fixtures/lines.json +227 -0
  29. package/_stories/vega-fixtures/measles-bars.json +348 -0
  30. package/_stories/vega-fixtures/measles-map.json +11101 -0
  31. package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
  32. package/_stories/vega-fixtures/multi-dataset.json +255 -0
  33. package/_stories/vega-fixtures/no-data.json +14 -0
  34. package/_stories/vega-fixtures/pie-chart.json +94 -0
  35. package/_stories/vega-fixtures/repeat-spec.json +47 -0
  36. package/_stories/vega-fixtures/stacked-area.json +222 -0
  37. package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
  38. package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
  39. package/_stories/vega-fixtures/stacked-bars.json +212 -0
  40. package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
  41. package/_stories/vega-fixtures/warning-combo.json +59 -0
  42. package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
  43. package/assets/icon-chart-area.svg +1 -0
  44. package/assets/icon-chart-radar.svg +23 -0
  45. package/assets/icon-magnifying-glass.svg +5 -0
  46. package/assets/icon-warming-stripes.svg +13 -0
  47. package/assets/logo2.svg +31 -0
  48. package/components/AdvancedEditor/AdvancedEditor.tsx +4 -0
  49. package/components/AdvancedEditor/EmbedEditor.tsx +513 -0
  50. package/components/ComboBox/ComboBox.tsx +345 -0
  51. package/components/ComboBox/combobox.styles.css +185 -0
  52. package/components/ComboBox/index.ts +1 -0
  53. package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
  54. package/components/DataTable/DataTable.tsx +132 -58
  55. package/components/DataTable/data-table.css +216 -215
  56. package/components/DataTable/helpers/getSeriesName.ts +6 -0
  57. package/components/DataTable/helpers/mapCellMatrix.tsx +14 -6
  58. package/components/EditorPanel/ColumnsEditor.tsx +37 -19
  59. package/components/EditorPanel/DataTableEditor.tsx +51 -25
  60. package/components/EditorPanel/EditorPanel.styles.css +16 -0
  61. package/components/EditorPanel/EditorPanel.tsx +144 -0
  62. package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
  63. package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
  64. package/components/EditorPanel/Inputs.tsx +33 -7
  65. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +14 -6
  66. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +240 -175
  67. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
  68. package/components/EditorPanel/sections/VisualSection.tsx +169 -0
  69. package/components/Filters/Filters.tsx +31 -5
  70. package/components/Filters/helpers/getNestedOptions.ts +2 -1
  71. package/components/Filters/helpers/handleSorting.ts +1 -1
  72. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +84 -2
  73. package/components/Layout/components/Visualization/index.tsx +27 -1
  74. package/components/Layout/components/Visualization/visualizations.scss +7 -0
  75. package/components/Legend/Legend.Gradient.tsx +1 -1
  76. package/components/MediaControls.tsx +53 -28
  77. package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
  78. package/components/_stories/DataTable.stories.tsx +1 -0
  79. package/components/ui/Icon.tsx +3 -1
  80. package/components/ui/Title/index.tsx +30 -2
  81. package/components/ui/Title/title.styles.css +42 -0
  82. package/data/colorPalettes.ts +18 -5
  83. package/data/mapColorPalettes.ts +10 -0
  84. package/devTemplate/dev.js +235 -0
  85. package/devTemplate/index.html +30 -0
  86. package/devTemplate/preview.html +1503 -0
  87. package/devTemplate/sidebar.css +151 -0
  88. package/dist/cove-main.css +2803 -4448
  89. package/dist/cove-main.css.map +1 -1
  90. package/generateViteConfig.js +118 -2
  91. package/helpers/DataTransform.ts +1 -5
  92. package/helpers/addValuesToFilters.ts +6 -1
  93. package/helpers/cove/date.ts +33 -1
  94. package/helpers/cove/string.ts +29 -0
  95. package/helpers/coveUpdateWorker.ts +21 -12
  96. package/helpers/embed/embedCodeGenerator.ts +80 -0
  97. package/helpers/embed/embedHelper.js +158 -0
  98. package/helpers/embed/filterUtils.ts +121 -0
  99. package/helpers/embed/index.ts +21 -0
  100. package/helpers/embed/urlValidation.ts +119 -0
  101. package/helpers/filterVizData.ts +6 -1
  102. package/helpers/getFileExtension.ts +0 -6
  103. package/helpers/getUniqueValues.ts +19 -0
  104. package/helpers/hashObj.ts +25 -0
  105. package/helpers/isRightAlignedTableValue.js +5 -0
  106. package/helpers/metrics/helpers.ts +1 -0
  107. package/helpers/metrics/types.ts +3 -0
  108. package/helpers/palettes/colorDistributions.ts +1 -1
  109. package/helpers/palettes/utils.ts +12 -12
  110. package/helpers/parseCsvWithQuotes.ts +15 -14
  111. package/helpers/pivotData.ts +2 -2
  112. package/helpers/prepareScreenshot.ts +288 -0
  113. package/helpers/queryStringUtils.ts +29 -0
  114. package/helpers/testing.ts +44 -0
  115. package/helpers/tests/DataTransform.test.ts +125 -0
  116. package/helpers/tests/date.test.ts +64 -0
  117. package/helpers/tests/prepareScreenshot.test.ts +414 -0
  118. package/helpers/tests/queryStringUtils.test.ts +381 -0
  119. package/helpers/tests/testStandaloneBuild.ts +23 -5
  120. package/helpers/useDataVizClasses.ts +0 -1
  121. package/helpers/vegaConfig.ts +1 -1
  122. package/helpers/vegaConfigImport.ts +160 -0
  123. package/helpers/ver/4.26.1.ts +80 -0
  124. package/helpers/ver/4.26.2.ts +84 -0
  125. package/helpers/ver/tests/4.26.1.test.ts +105 -0
  126. package/helpers/ver/tests/4.26.2.test.ts +298 -0
  127. package/helpers/viewports.ts +2 -0
  128. package/hooks/useDataColumns.ts +63 -0
  129. package/hooks/useFilterManagement.ts +94 -0
  130. package/hooks/useLegendSeparators.ts +26 -0
  131. package/hooks/useListManagement.ts +192 -0
  132. package/package.json +29 -33
  133. package/styles/_button-section.scss +0 -3
  134. package/styles/v2/components/editor.scss +9 -9
  135. package/styles/v2/utils/_grid.scss +8 -3
  136. package/types/Annotation.ts +10 -11
  137. package/types/Axis.ts +1 -0
  138. package/types/ForecastingSeriesKey.ts +1 -0
  139. package/types/General.ts +2 -0
  140. package/types/MarkupInclude.ts +1 -0
  141. package/types/Palette.ts +21 -0
  142. package/types/Series.ts +3 -0
  143. package/types/Table.ts +1 -0
  144. package/types/Visualization.ts +7 -0
  145. package/types/VizFilter.ts +1 -0
  146. package/LICENSE +0 -201
  147. package/_stories/StoryRenderingTests.stories.tsx +0 -164
@@ -0,0 +1,84 @@
1
+ import cloneConfig from '../cloneConfig'
2
+ import { DashboardConfig } from '@cdc/dashboard/src/types/DashboardConfig'
3
+
4
+ const migrateAnnotationDimensions = config => {
5
+ if (config.annotations && Array.isArray(config.annotations)) {
6
+ // Calculate chart area height for Y conversion (matches calcInitialHeight)
7
+ const isHorizontal = config.orientation === 'horizontal'
8
+ const chartAreaHeight = isHorizontal
9
+ ? Number(config.heights?.horizontal) || 750 // default horizontal height
10
+ : Number(config.heights?.vertical) || 300 // default vertical height
11
+
12
+ config.annotations = config.annotations.map(annotation => {
13
+ if (!annotation) return annotation
14
+ if (annotation.y !== undefined && chartAreaHeight > 0) {
15
+ // Convert Y from pixels to percentage using the chart area height
16
+ annotation.y = (annotation.y / chartAreaHeight) * 100
17
+ } else {
18
+ annotation.y = 50
19
+ }
20
+
21
+ // Delete savedDimensions to preserve old dx/dy behavior (fixed pixel offsets).
22
+ // The scaling function falls back to raw dx/dy when savedDimensions is missing.
23
+ // Once user drags the annotation, savedDimensions will be set with current chart
24
+ // dimensions, enabling responsive scaling from that point forward.
25
+ delete annotation.savedDimensions
26
+
27
+ return annotation
28
+ })
29
+ }
30
+
31
+ if (config.type === 'dashboard' && config.visualizations) {
32
+ Object.values((config as DashboardConfig).visualizations).forEach(visualization => {
33
+ migrateAnnotationDimensions(visualization)
34
+ })
35
+ }
36
+ }
37
+
38
+ const migrateAnnotationDataModel = config => {
39
+ if (config.annotations && Array.isArray(config.annotations)) {
40
+ config.annotations = config.annotations.map(annotation => {
41
+ if (!annotation) return annotation
42
+ // Set all existing annotations to fixed mode
43
+ annotation.anchorMode = 'fixed'
44
+
45
+ // Delete xKey entirely - old format stored timestamps for dates,
46
+ // but new dataX expects raw data values. Format is incompatible.
47
+ // User can re-enable data mode which will set fresh dataX value.
48
+ delete annotation.xKey
49
+
50
+ // Delete yKey entirely (Y will be calculated dynamically in data mode)
51
+ delete annotation.yKey
52
+
53
+ // Delete empty seriesKey - it would cause yScale(undefined) errors in data mode
54
+ if (!annotation.seriesKey) {
55
+ delete annotation.seriesKey
56
+ }
57
+
58
+ // Delete deprecated properties
59
+ delete annotation.snapToNearestPoint
60
+ delete annotation.originalX
61
+ delete annotation.originalDX
62
+ delete annotation.originalY
63
+
64
+ return annotation
65
+ })
66
+ }
67
+
68
+ if (config.type === 'dashboard' && config.visualizations) {
69
+ Object.values((config as DashboardConfig).visualizations).forEach(visualization => {
70
+ migrateAnnotationDataModel(visualization)
71
+ })
72
+ }
73
+ }
74
+
75
+ const update_4_26_2 = config => {
76
+ const ver = '4.26.2'
77
+ const newConfig = cloneConfig(config)
78
+ migrateAnnotationDimensions(newConfig)
79
+ migrateAnnotationDataModel(newConfig)
80
+ newConfig.version = ver
81
+ return newConfig
82
+ }
83
+
84
+ export default update_4_26_2
@@ -0,0 +1,105 @@
1
+ import update_4_26_1 from '../4.26.1'
2
+ import { expect, describe, it } from 'vitest'
3
+
4
+ describe('update_4_26_1', () => {
5
+ describe('normalizeFilterParents', () => {
6
+ it('should convert string parents to array in shared filters', () => {
7
+ const config: any = {
8
+ type: 'dashboard',
9
+ version: '4.26.0',
10
+ dashboard: {
11
+ sharedFilters: [
12
+ {
13
+ type: 'datafilter',
14
+ parents: 'parent-filter-id'
15
+ }
16
+ ]
17
+ }
18
+ }
19
+
20
+ const result = update_4_26_1(config)
21
+
22
+ expect(result.dashboard.sharedFilters[0].parents).toEqual(['parent-filter-id'])
23
+ expect(result.version).toBe('4.26.1')
24
+ })
25
+
26
+ it('should leave array parents unchanged', () => {
27
+ const config: any = {
28
+ type: 'dashboard',
29
+ version: '4.26.0',
30
+ dashboard: {
31
+ sharedFilters: [
32
+ {
33
+ type: 'datafilter',
34
+ parents: ['parent1', 'parent2']
35
+ }
36
+ ]
37
+ }
38
+ }
39
+
40
+ const result = update_4_26_1(config)
41
+
42
+ expect(result.dashboard.sharedFilters[0].parents).toEqual(['parent1', 'parent2'])
43
+ })
44
+ })
45
+
46
+ describe('removeOldBrushKeys', () => {
47
+ it('should remove brush config from chart', () => {
48
+ const config: any = {
49
+ type: 'chart',
50
+ version: '4.26.0',
51
+ brush: { enabled: true }
52
+ }
53
+
54
+ const result = update_4_26_1(config)
55
+
56
+ expect(result.brush).toBeUndefined()
57
+ })
58
+
59
+ it('should remove brush config from dashboard visualizations', () => {
60
+ const config: any = {
61
+ type: 'dashboard',
62
+ version: '4.26.0',
63
+ visualizations: {
64
+ chart1: {
65
+ type: 'chart',
66
+ brush: { enabled: true }
67
+ }
68
+ }
69
+ }
70
+
71
+ const result = update_4_26_1(config)
72
+
73
+ expect(result.visualizations.chart1.brush).toBeUndefined()
74
+ })
75
+ })
76
+
77
+ describe('combined migrations', () => {
78
+ it('should run all migrations together', () => {
79
+ const config: any = {
80
+ type: 'dashboard',
81
+ version: '4.26.0',
82
+ dashboard: {
83
+ sharedFilters: [
84
+ {
85
+ type: 'datafilter',
86
+ parents: 'parent-id'
87
+ }
88
+ ]
89
+ },
90
+ visualizations: {
91
+ chart1: {
92
+ type: 'chart',
93
+ brush: { enabled: true }
94
+ }
95
+ }
96
+ }
97
+
98
+ const result = update_4_26_1(config)
99
+
100
+ expect(result.dashboard.sharedFilters[0].parents).toEqual(['parent-id'])
101
+ expect(result.visualizations.chart1.brush).toBeUndefined()
102
+ expect(result.version).toBe('4.26.1')
103
+ })
104
+ })
105
+ })
@@ -0,0 +1,298 @@
1
+ import update_4_26_2 from '../4.26.2'
2
+ import { expect, describe, it } from 'vitest'
3
+
4
+ describe('update_4_26_2', () => {
5
+ describe('migrateAnnotationDimensions', () => {
6
+ it('should convert Y position from absolute pixels to percentage and delete savedDimensions', () => {
7
+ const config: any = {
8
+ type: 'chart',
9
+ version: '4.26.1',
10
+ heights: { vertical: 400 },
11
+ annotations: [
12
+ {
13
+ text: 'Test Annotation',
14
+ x: 50,
15
+ y: 200,
16
+ savedDimensions: [800, 560], // old format: will be deleted
17
+ dx: 10,
18
+ dy: -10
19
+ }
20
+ ]
21
+ }
22
+
23
+ const result = update_4_26_2(config)
24
+
25
+ // Y is now calculated as percentage of heights.vertical (400)
26
+ expect(result.annotations[0].y).toBe(50) // 200 / 400 * 100 = 50
27
+ expect(result.annotations[0].x).toBe(50)
28
+ // savedDimensions should be deleted to preserve old dx/dy behavior
29
+ expect(result.annotations[0].savedDimensions).toBeUndefined()
30
+ // dx/dy should be preserved unchanged
31
+ expect(result.annotations[0].dx).toBe(10)
32
+ expect(result.annotations[0].dy).toBe(-10)
33
+ expect(result.version).toBe('4.26.2')
34
+ })
35
+
36
+ it('should handle multiple annotations with different Y positions', () => {
37
+ const config: any = {
38
+ type: 'chart',
39
+ version: '4.26.1',
40
+ heights: { vertical: 500 },
41
+ annotations: [
42
+ {
43
+ text: 'Top',
44
+ x: 25,
45
+ y: 100,
46
+ savedDimensions: [1000, 700]
47
+ },
48
+ {
49
+ text: 'Middle',
50
+ x: 75,
51
+ y: 250,
52
+ savedDimensions: [1000, 700]
53
+ }
54
+ ]
55
+ }
56
+
57
+ const result = update_4_26_2(config)
58
+
59
+ // Y calculated as percentage of heights.vertical (500)
60
+ expect(result.annotations[0].y).toBe(20) // 100 / 500 * 100 = 20
61
+ expect(result.annotations[1].y).toBe(50) // 250 / 500 * 100 = 50
62
+ // savedDimensions should be deleted
63
+ expect(result.annotations[0].savedDimensions).toBeUndefined()
64
+ expect(result.annotations[1].savedDimensions).toBeUndefined()
65
+ })
66
+
67
+ it('should use default vertical height (300) when heights not specified', () => {
68
+ const config: any = {
69
+ type: 'chart',
70
+ version: '4.26.1',
71
+ annotations: [
72
+ {
73
+ text: 'No heights config',
74
+ x: 50,
75
+ y: 150,
76
+ savedDimensions: [800, 560]
77
+ }
78
+ ]
79
+ }
80
+
81
+ const result = update_4_26_2(config)
82
+
83
+ // Uses default vertical height of 300
84
+ expect(result.annotations[0].y).toBe(50) // 150 / 300 * 100 = 50
85
+ expect(result.annotations[0].savedDimensions).toBeUndefined()
86
+ })
87
+
88
+ it('should use horizontal height for horizontal orientation', () => {
89
+ const config: any = {
90
+ type: 'chart',
91
+ version: '4.26.1',
92
+ orientation: 'horizontal',
93
+ heights: { horizontal: 750 },
94
+ annotations: [
95
+ {
96
+ text: 'Horizontal chart annotation',
97
+ x: 50,
98
+ y: 375,
99
+ savedDimensions: [800, 900]
100
+ }
101
+ ]
102
+ }
103
+
104
+ const result = update_4_26_2(config)
105
+
106
+ // Uses heights.horizontal for horizontal charts
107
+ expect(result.annotations[0].y).toBe(50) // 375 / 750 * 100 = 50
108
+ expect(result.annotations[0].savedDimensions).toBeUndefined()
109
+ })
110
+
111
+ it('should handle dashboard configs with nested visualizations', () => {
112
+ const config: any = {
113
+ type: 'dashboard',
114
+ version: '4.26.1',
115
+ visualizations: {
116
+ chart1: {
117
+ type: 'chart',
118
+ heights: { vertical: 300 },
119
+ annotations: [
120
+ {
121
+ text: 'Chart 1 Annotation',
122
+ x: 30,
123
+ y: 150,
124
+ savedDimensions: [600, 450]
125
+ }
126
+ ]
127
+ }
128
+ }
129
+ }
130
+
131
+ const result = update_4_26_2(config)
132
+
133
+ expect(result.visualizations.chart1.annotations[0].y).toBe(50) // 150 / 300 * 100 = 50
134
+ expect(result.visualizations.chart1.annotations[0].savedDimensions).toBeUndefined()
135
+ })
136
+
137
+ it('should preserve all other annotation properties and delete xKey entirely', () => {
138
+ const config: any = {
139
+ type: 'chart',
140
+ version: '4.26.1',
141
+ heights: { vertical: 500 },
142
+ annotations: [
143
+ {
144
+ text: 'Complex Annotation',
145
+ x: 50,
146
+ y: 250,
147
+ savedDimensions: [1000, 700],
148
+ dx: 20,
149
+ dy: -30,
150
+ xKey: 1577836800000,
151
+ yKey: '42',
152
+ seriesKey: 'series1',
153
+ marker: 'arrow'
154
+ }
155
+ ]
156
+ }
157
+
158
+ const result = update_4_26_2(config)
159
+
160
+ const annotation = result.annotations[0]
161
+ expect(annotation.y).toBe(50) // 250 / 500 * 100 = 50
162
+ expect(annotation.text).toBe('Complex Annotation')
163
+ expect(annotation.dx).toBe(20)
164
+ expect(annotation.dy).toBe(-30)
165
+ // xKey is deleted entirely (not renamed to dataX) because old format
166
+ // stored timestamps but new dataX expects raw data values
167
+ expect(annotation.xKey).toBeUndefined()
168
+ expect(annotation.dataX).toBeUndefined()
169
+ expect(annotation.yKey).toBeUndefined()
170
+ // Non-empty seriesKey is preserved
171
+ expect(annotation.seriesKey).toBe('series1')
172
+ expect(annotation.savedDimensions).toBeUndefined()
173
+ })
174
+
175
+ it('should delete empty seriesKey', () => {
176
+ const config: any = {
177
+ type: 'chart',
178
+ version: '4.26.1',
179
+ heights: { vertical: 300 },
180
+ annotations: [
181
+ {
182
+ text: 'Annotation with empty seriesKey',
183
+ x: 50,
184
+ y: 150,
185
+ seriesKey: ''
186
+ }
187
+ ]
188
+ }
189
+
190
+ const result = update_4_26_2(config)
191
+
192
+ // Empty seriesKey should be deleted to prevent yScale(undefined) errors
193
+ expect(result.annotations[0].seriesKey).toBeUndefined()
194
+ })
195
+
196
+ it('should handle config with no annotations', () => {
197
+ const config: any = {
198
+ type: 'chart',
199
+ version: '4.26.1',
200
+ title: 'Chart without annotations'
201
+ }
202
+
203
+ const result = update_4_26_2(config)
204
+
205
+ expect(result.annotations).toBeUndefined()
206
+ expect(result.title).toBe('Chart without annotations')
207
+ })
208
+
209
+ it('should handle annotation without savedDimensions', () => {
210
+ const config: any = {
211
+ type: 'chart',
212
+ version: '4.26.1',
213
+ heights: { vertical: 300 },
214
+ annotations: [
215
+ {
216
+ text: 'No savedDimensions',
217
+ x: 50,
218
+ y: 150
219
+ }
220
+ ]
221
+ }
222
+
223
+ const result = update_4_26_2(config)
224
+
225
+ expect(result.annotations[0].y).toBe(50) // 150 / 300 * 100 = 50
226
+ expect(result.annotations[0].savedDimensions).toBeUndefined()
227
+ })
228
+ })
229
+
230
+ describe('combined migrations', () => {
231
+ it('should run all migrations together', () => {
232
+ const config: any = {
233
+ type: 'dashboard',
234
+ version: '4.26.1',
235
+ dashboard: {
236
+ sharedFilters: [
237
+ {
238
+ type: 'datafilter',
239
+ parents: 'parent-id'
240
+ }
241
+ ]
242
+ },
243
+ visualizations: {
244
+ chart1: {
245
+ type: 'chart',
246
+ heights: { vertical: 400 },
247
+ brush: { enabled: true },
248
+ annotations: [
249
+ {
250
+ x: 50,
251
+ y: 200,
252
+ savedDimensions: [800, 560]
253
+ }
254
+ ]
255
+ }
256
+ }
257
+ }
258
+
259
+ const result = update_4_26_2(config)
260
+
261
+ expect(result.visualizations.chart1.annotations[0].y).toBe(50) // 200 / 400 * 100 = 50
262
+ expect(result.visualizations.chart1.annotations[0].anchorMode).toBe('fixed')
263
+ expect(result.visualizations.chart1.annotations[0].savedDimensions).toBeUndefined()
264
+ expect(result.visualizations.chart1.annotations[0].dataX).toBeUndefined()
265
+ expect(result.version).toBe('4.26.2')
266
+ })
267
+
268
+ it('should migrate data model along with Y position', () => {
269
+ const config: any = {
270
+ type: 'chart',
271
+ version: '4.26.1',
272
+ heights: { vertical: 400 },
273
+ annotations: [
274
+ {
275
+ x: 50,
276
+ y: 200,
277
+ savedDimensions: [800, 560],
278
+ xKey: 1577836800000,
279
+ yKey: '42',
280
+ snapToNearestPoint: true
281
+ }
282
+ ]
283
+ }
284
+
285
+ const result = update_4_26_2(config)
286
+
287
+ const annotation = result.annotations[0]
288
+ expect(annotation.y).toBe(50) // 200 / 400 * 100 = 50
289
+ expect(annotation.anchorMode).toBe('fixed')
290
+ // xKey is deleted entirely (not renamed to dataX) - format incompatible
291
+ expect(annotation.dataX).toBeUndefined()
292
+ expect(annotation.xKey).toBeUndefined()
293
+ expect(annotation.yKey).toBeUndefined()
294
+ expect(annotation.snapToNearestPoint).toBeUndefined()
295
+ expect(annotation.savedDimensions).toBeUndefined()
296
+ })
297
+ })
298
+ })
@@ -15,4 +15,6 @@ export const isMobileTerritoryViewport = currentViewport => isBelowBreakpoint('s
15
15
 
16
16
  export const isMobileFontViewport = currentViewport => isBelowBreakpoint('sm', currentViewport)
17
17
 
18
+ export const isMobileAnnotationViewport = currentViewport => isBelowBreakpoint('sm', currentViewport)
19
+
18
20
  export const isMobileSmallMultiplesViewport = currentViewport => isBelowBreakpoint('md', currentViewport)
@@ -0,0 +1,63 @@
1
+ import { useMemo } from 'react'
2
+
3
+ export interface UseDataColumnsOptions {
4
+ /** Columns to exclude from the result */
5
+ excludeColumns?: string[]
6
+ /** Include only columns with specific data types */
7
+ dataTypes?: ('string' | 'number' | 'boolean' | 'date')[]
8
+ }
9
+
10
+ /**
11
+ * Extracts unique column names from data with memoization
12
+ *
13
+ * Performance optimization: Replaces the common getColumns() pattern
14
+ * that was duplicated across multiple packages and called multiple times per render.
15
+ *
16
+ * @param data - Array of data objects
17
+ * @param options - Optional configuration for filtering columns
18
+ * @returns Sorted array of unique column names
19
+ *
20
+ * @example
21
+ * // Basic usage
22
+ * const columns = useDataColumns(data)
23
+ *
24
+ * @example
25
+ * // With exclusions
26
+ * const columns = useDataColumns(data, { excludeColumns: [config.groupBy] })
27
+ *
28
+ * @example
29
+ * // Filter by data type
30
+ * const numericColumns = useDataColumns(data, { dataTypes: ['number'] })
31
+ */
32
+ export const useDataColumns = (data: any[], options?: UseDataColumnsOptions): string[] => {
33
+ const { excludeColumns = [], dataTypes } = options || {}
34
+
35
+ return useMemo(() => {
36
+ if (!data?.length) return []
37
+
38
+ const columnsSet = new Set<string>()
39
+
40
+ // Single iteration through all rows (optimized from previous pattern)
41
+ data.forEach(row => {
42
+ Object.keys(row).forEach(columnName => {
43
+ if (excludeColumns.includes(columnName)) return
44
+
45
+ // Optional: filter by data type
46
+ if (dataTypes && dataTypes.length > 0) {
47
+ const value = row[columnName]
48
+ const valueType = typeof value
49
+ if (!dataTypes.includes(valueType as any)) return
50
+ }
51
+
52
+ columnsSet.add(columnName)
53
+ })
54
+ })
55
+
56
+ return Array.from(columnsSet).sort()
57
+ }, [
58
+ data,
59
+ // Stringify arrays for stable dependency tracking
60
+ excludeColumns.join(','),
61
+ dataTypes?.join(',')
62
+ ])
63
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Custom hook for managing filter operations in visualizations
3
+ *
4
+ * Provides common filter management functionality including:
5
+ * - Adding new filters
6
+ * - Removing filters
7
+ * - Updating filter properties
8
+ * - Getting unique column values for filter options
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * const { addNewFilter, removeFilter, updateFilterProp, getFilterColumnValues } =
13
+ * useFilterManagement(config, updateConfig, data)
14
+ *
15
+ * // Add a new filter
16
+ * <Button onClick={addNewFilter}>Add Filter</Button>
17
+ *
18
+ * // Remove a filter
19
+ * <button onClick={() => removeFilter(index)}>Remove</button>
20
+ *
21
+ * // Update filter property
22
+ * <Select onChange={e => updateFilterProp('columnName', index, e.target.value)} />
23
+ *
24
+ * // Get column values for filter dropdown
25
+ * <Select options={getFilterColumnValues(index)} />
26
+ * ```
27
+ */
28
+ export const useFilterManagement = <TConfig extends { filters?: any[] }>(
29
+ config: TConfig,
30
+ updateConfig: (config: TConfig) => void,
31
+ data: any[]
32
+ ) => {
33
+ /**
34
+ * Adds a new empty filter to the config
35
+ */
36
+ const addNewFilter = () => {
37
+ const filters = config.filters ? [...config.filters] : []
38
+ filters.push({ values: [] })
39
+ updateConfig({ ...config, filters })
40
+ }
41
+
42
+ /**
43
+ * Removes a filter at the specified index
44
+ * @param index - The index of the filter to remove
45
+ */
46
+ const removeFilter = (index: number) => {
47
+ const filters = [...(config.filters || [])]
48
+ filters.splice(index, 1)
49
+ updateConfig({ ...config, filters })
50
+ }
51
+
52
+ /**
53
+ * Updates a specific property of a filter
54
+ * @param name - The property name to update
55
+ * @param index - The index of the filter
56
+ * @param value - The new value for the property
57
+ */
58
+ const updateFilterProp = (name: string, index: number, value: any) => {
59
+ const filters = [...(config.filters || [])]
60
+ filters[index][name] = value
61
+ updateConfig({ ...config, filters })
62
+ }
63
+
64
+ /**
65
+ * Gets unique values from a data column for filter options
66
+ * @param index - The index of the filter (to get its columnName)
67
+ * @returns Array of unique, sorted values from the specified column
68
+ */
69
+ const getFilterColumnValues = (index: number): any[] => {
70
+ const filterDataOptions: any[] = []
71
+ const filterColumnName = config.filters?.[index]?.columnName
72
+
73
+ // Return empty array if no column name or no data
74
+ if (!filterColumnName || !data || !Array.isArray(data) || data.length === 0) {
75
+ return filterDataOptions
76
+ }
77
+
78
+ data.forEach(function (row: any) {
79
+ if (undefined !== row[filterColumnName] && -1 === filterDataOptions.indexOf(row[filterColumnName])) {
80
+ filterDataOptions.push(row[filterColumnName])
81
+ }
82
+ })
83
+ filterDataOptions.sort()
84
+
85
+ return filterDataOptions
86
+ }
87
+
88
+ return {
89
+ addNewFilter,
90
+ removeFilter,
91
+ updateFilterProp,
92
+ getFilterColumnValues
93
+ }
94
+ }
@@ -0,0 +1,26 @@
1
+ import { clamp } from 'lodash'
2
+
3
+ // TODO: generalize this to be used in legends other than linear block gradient
4
+
5
+ const LEGEND_SEPARATOR_SIZE = 0.02
6
+ const LEGEND_SEPARATOR_SIZE_MAX = 20
7
+ const LEGEND_SEPARATOR_SIZE_MIN = 8
8
+
9
+ const useLegendSeparators = (separators, legendWidth, allowsLegendSeparators) => {
10
+ const legendSeparators = allowsLegendSeparators
11
+ ? separators?.replace(' ', '').split(',').map(Number).filter(Boolean) || []
12
+ : []
13
+ const separatorSize = clamp(legendWidth * LEGEND_SEPARATOR_SIZE, LEGEND_SEPARATOR_SIZE_MIN, LEGEND_SEPARATOR_SIZE_MAX)
14
+ const legendSeparatorsToSubtract = legendSeparators.length * separatorSize
15
+ const getTickSeparatorsAdjustment = (index: number) =>
16
+ legendSeparators.reduce((acc, separators) => (index >= separators ? acc + separatorSize : acc), 0)
17
+
18
+ return {
19
+ legendSeparators,
20
+ separatorSize,
21
+ legendSeparatorsToSubtract,
22
+ getTickSeparatorsAdjustment
23
+ }
24
+ }
25
+
26
+ export default useLegendSeparators