@cdc/chart 4.26.4 → 4.26.6

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 (141) hide show
  1. package/CONFIG.md +353 -29
  2. package/LICENSE +201 -0
  3. package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
  4. package/dist/cdcchart.js +37885 -35117
  5. package/examples/feature/heatmap/average-age-categorical.json +313 -0
  6. package/examples/feature/heatmap/calendar-heatmap.json +134 -0
  7. package/examples/feature/heatmap/cell-values-bottom-axis.json +181 -0
  8. package/examples/feature/heatmap/sparse-aggregation.json +158 -0
  9. package/examples/private/bad-y-axis.json +543 -0
  10. package/examples/private/bar-chart-negative-value.json +455 -0
  11. package/examples/private/bump-chart.json +1116 -0
  12. package/examples/private/test-123.json +624 -0
  13. package/package.json +4 -3
  14. package/src/CdcChart.tsx +5 -4
  15. package/src/CdcChartComponent.tsx +166 -62
  16. package/src/_stories/Chart.AltText.stories.tsx +136 -0
  17. package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
  18. package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
  19. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +183 -0
  20. package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +29 -0
  21. package/src/_stories/Chart.YAxisAutoMaxStrategy.stories.tsx +52 -0
  22. package/src/_stories/Chart.YAxisFilterDomain.stories.tsx +90 -0
  23. package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -0
  24. package/src/_stories/ChartAxisTitles.smoke.stories.tsx +13 -0
  25. package/src/_stories/ChartBar.Editor.stories.tsx +63 -0
  26. package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
  27. package/src/_stories/ChartBrush.smoke.stories.tsx +13 -4
  28. package/src/_stories/_mock/alt_text_metadata.json +38 -0
  29. package/src/_stories/_mock/annotation_event-line_mock.json +1064 -0
  30. package/src/_stories/_mock/heatmap-average-age-categorical.json +315 -0
  31. package/src/_stories/_mock/heatmap.json +156 -0
  32. package/src/_stories/_mock/reported_pertussis_cases.json +1643 -0
  33. package/src/_stories/_mock/sankey_mock.json +78 -0
  34. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +1 -1
  35. package/src/components/Annotations/components/AnnotationDraggable.styles.css +22 -1
  36. package/src/components/Annotations/components/AnnotationDraggable.tsx +220 -120
  37. package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
  38. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +198 -0
  39. package/src/components/Axis/LeftAxis.tsx +17 -11
  40. package/src/components/Axis/LeftAxisGridlines.tsx +22 -12
  41. package/src/components/Axis/RightAxis.tsx +3 -0
  42. package/src/components/Axis/tests/AxisTickValues.test.tsx +86 -0
  43. package/src/components/Brush/BrushSelector.tsx +88 -187
  44. package/src/components/Brush/brushInteraction.ts +22 -0
  45. package/src/components/Brush/tests/brushInteraction.test.ts +38 -0
  46. package/src/components/EditorPanel/EditorPanel.tsx +677 -185
  47. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +114 -52
  48. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
  49. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +1 -0
  50. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +5 -0
  51. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -2
  52. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.test.tsx +98 -0
  53. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +10 -0
  54. package/src/components/EditorPanel/components/Panels/Panel.Visual.test.tsx +484 -0
  55. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +4 -1
  56. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +262 -0
  57. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +32 -0
  58. package/src/components/EditorPanel/components/panels.scss +10 -0
  59. package/src/components/EditorPanel/editor-panel.scss +47 -0
  60. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +61 -0
  61. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +74 -0
  62. package/src/components/EditorPanel/useEditorPermissions.test.tsx +315 -0
  63. package/src/components/EditorPanel/useEditorPermissions.ts +77 -21
  64. package/src/components/HeatMap/components/HeatMap.tsx +767 -0
  65. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +128 -0
  66. package/src/components/HeatMap/heatmap.constants.ts +13 -0
  67. package/src/components/HeatMap/heatmap.css +85 -0
  68. package/src/components/HeatMap/helpers/index.ts +261 -0
  69. package/src/components/HeatMap/index.tsx +5 -0
  70. package/src/components/HeatMap/tests/HeatMap.test.tsx +1072 -0
  71. package/src/components/HeatMap/tests/helpers.test.ts +167 -0
  72. package/src/components/Legend/Legend.Component.tsx +2 -2
  73. package/src/components/Legend/helpers/createFormatLabels.test.ts +88 -0
  74. package/src/components/Legend/helpers/createFormatLabels.tsx +6 -1
  75. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +19 -22
  76. package/src/components/LineChart/helpers.test.ts +574 -0
  77. package/src/components/LineChart/helpers.ts +39 -16
  78. package/src/components/LineChart/index.tsx +28 -5
  79. package/src/components/LinearChart/tests/LinearChart.test.tsx +137 -33
  80. package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
  81. package/src/components/LinearChart.tsx +103 -47
  82. package/src/components/PieChart/PieChart.tsx +4 -1
  83. package/src/components/RadarChart/RadarChart.tsx +3 -1
  84. package/src/components/Regions/components/Regions.tsx +195 -41
  85. package/src/components/Sankey/components/Sankey.tsx +7 -3
  86. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
  87. package/src/components/ScatterPlot/ScatterPlot.jsx +3 -3
  88. package/src/components/SmallMultiples/SmallMultipleTile.tsx +33 -3
  89. package/src/components/SmallMultiples/SmallMultiples.css +12 -0
  90. package/src/components/SmallMultiples/SmallMultiples.tsx +50 -15
  91. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
  92. package/src/components/SmallMultiples/tests/SmallMultiples.test.tsx +170 -0
  93. package/src/components/Sparkline/components/SparkLine.tsx +4 -9
  94. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +1 -1
  95. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +2 -2
  96. package/src/data/initial-state.js +12 -0
  97. package/src/helpers/axisLabelFontSize.ts +8 -0
  98. package/src/helpers/bumpChartTooltip.ts +54 -0
  99. package/src/helpers/calculateLeftYAxisWidth.ts +13 -3
  100. package/src/helpers/colorAssignmentHelpers.ts +102 -0
  101. package/src/helpers/dataHelpers.ts +11 -1
  102. package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
  103. package/src/helpers/getAxisMaxOverride.ts +16 -0
  104. package/src/helpers/getCleanTopTickMax.ts +64 -0
  105. package/src/helpers/getColorScale.ts +18 -9
  106. package/src/helpers/getHasBoundarySuppression.ts +26 -0
  107. package/src/helpers/getMinMax.ts +26 -25
  108. package/src/helpers/getYAxisDomainData.ts +38 -0
  109. package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
  110. package/src/helpers/handleChartAriaLabels.ts +23 -10
  111. package/src/helpers/hasSpacedInlineLabel.ts +10 -0
  112. package/src/helpers/seriesColumnSettings.ts +5 -4
  113. package/src/helpers/seriesHighlight.ts +26 -0
  114. package/src/helpers/smallMultiplesHelpers.ts +5 -0
  115. package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
  116. package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
  117. package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
  118. package/src/helpers/tests/dataHelpers.test.ts +135 -0
  119. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
  120. package/src/helpers/tests/getAxisMaxOverride.test.ts +36 -0
  121. package/src/helpers/tests/getCleanTopTickMax.test.ts +165 -0
  122. package/src/helpers/tests/getColorScale.test.ts +197 -0
  123. package/src/helpers/tests/getMinMax.test.ts +211 -0
  124. package/src/helpers/tests/getYAxisDomainData.test.ts +119 -0
  125. package/src/helpers/tests/getYAxisFinalizationEligibility.test.ts +185 -0
  126. package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
  127. package/src/helpers/tests/hasSpacedInlineLabel.test.ts +75 -0
  128. package/src/helpers/tests/seriesColumnSettings.test.ts +16 -2
  129. package/src/helpers/tests/seriesHighlight.test.ts +61 -0
  130. package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
  131. package/src/hooks/tests/useRightAxis.test.tsx +127 -0
  132. package/src/hooks/tests/useScales.test.tsx +383 -0
  133. package/src/hooks/useRightAxis.ts +30 -13
  134. package/src/hooks/useScales.ts +48 -21
  135. package/src/hooks/useTooltip.tsx +3 -1
  136. package/src/index.jsx +5 -2
  137. package/src/scss/main.scss +32 -49
  138. package/src/test/CdcChart.dataTable.test.tsx +122 -0
  139. package/src/types/ChartConfig.ts +24 -1
  140. package/src/types/ChartContext.ts +5 -0
  141. package/src/helpers/getYAxisAutoPadding.ts +0 -53
@@ -0,0 +1,262 @@
1
+ import { useEffect, useMemo, useRef } from 'react'
2
+ import cloneConfig from '@cdc/core/helpers/cloneConfig'
3
+ import { Select } from '@cdc/core/components/EditorPanel/Inputs'
4
+ import Tooltip from '@cdc/core/components/ui/Tooltip'
5
+ import Icon from '@cdc/core/components/ui/Icon'
6
+ import { DataColorSelector } from '@cdc/core/components/DataColorSelector'
7
+ import { getColorPaletteVersion } from '@cdc/core/helpers/getColorPaletteVersion'
8
+ import { getCurrentPaletteName, migratePaletteWithMap } from '@cdc/core/helpers/palettes/utils'
9
+ import { paletteMigrationMap } from '@cdc/core/helpers/palettes/migratePaletteName'
10
+ import { getColorScale } from '../../../../helpers/getColorScale'
11
+ import { ChartConfig } from '../../../../types/ChartConfig'
12
+ import {
13
+ getSeriesColorAssignmentItems,
14
+ getSeriesColorAssignments,
15
+ isSeriesColorAssignmentModeByValue,
16
+ supportsSeriesColorAssignments,
17
+ type SeriesColorAssignment,
18
+ type SeriesColorAssignmentItem
19
+ } from '../../../../helpers/colorAssignmentHelpers'
20
+
21
+ type SeriesColorAssignmentsProps = {
22
+ config: ChartConfig
23
+ updateConfig: (config: ChartConfig) => void
24
+ colorPalettes: Record<string, any>
25
+ }
26
+
27
+ const SeriesColorAssignments = ({ config, updateConfig, colorPalettes }: SeriesColorAssignmentsProps) => {
28
+ const seriesColorAssignmentItems = useMemo(() => getSeriesColorAssignmentItems(config), [config])
29
+ const seriesColorAssignments = getSeriesColorAssignments(config)
30
+ const seriesColorAssignmentMap = useMemo(
31
+ () => new Map(seriesColorAssignments.map(assignment => [assignment.key, assignment.color])),
32
+ [seriesColorAssignments]
33
+ )
34
+ const seriesColorAssignmentItemsFingerprint = seriesColorAssignmentItems
35
+ .map(({ key, label }) => `${key}:${label}`)
36
+ .join('|')
37
+ const seriesColorAssignmentsFingerprint = seriesColorAssignments.map(({ key, color }) => `${key}:${color}`).join('|')
38
+ const seriesColorAssignmentSupported = supportsSeriesColorAssignments(config)
39
+ const seriesColorAssignmentMode = config.general?.palette?.colorAssignmentMode || 'ordered'
40
+ const isSeriesColorAssignmentByValue = isSeriesColorAssignmentModeByValue(config)
41
+ const currentPaletteName = getCurrentPaletteName(config)
42
+ const customColors = config.general?.palette?.customColorsOrdered || config.general?.palette?.customColors
43
+ const hasCustomColors = Array.isArray(customColors) && customColors.length > 0
44
+
45
+ const getOrderedColorScaleConfig = () => ({
46
+ ...config,
47
+ general: {
48
+ ...config.general,
49
+ palette: {
50
+ ...config.general?.palette,
51
+ colorAssignmentMode: 'ordered' as const
52
+ }
53
+ }
54
+ })
55
+
56
+ const orderedColorScaleConfig = useMemo(getOrderedColorScaleConfig, [config])
57
+ const orderedColorScale = useMemo(() => getColorScale(orderedColorScaleConfig), [orderedColorScaleConfig])
58
+ const distributedPaletteColorOptions = useMemo(() => {
59
+ const colors = (orderedColorScale as any)?.range?.() || []
60
+ return Array.from(new Set(colors.filter(Boolean)))
61
+ }, [orderedColorScale])
62
+ const paletteColorOptions = useMemo(() => {
63
+ if (hasCustomColors) {
64
+ return Array.from(new Set(customColors.filter(Boolean)))
65
+ }
66
+
67
+ const version = getColorPaletteVersion(config)
68
+ const versionedPalettes = colorPalettes?.[`v${version}`] || colorPalettes?.v2 || colorPalettes || {}
69
+ const paletteName = migratePaletteWithMap(currentPaletteName, paletteMigrationMap, false)
70
+ const colors =
71
+ versionedPalettes[paletteName] || versionedPalettes[currentPaletteName] || distributedPaletteColorOptions
72
+
73
+ return Array.from(new Set(colors.filter(Boolean)))
74
+ }, [colorPalettes, config, customColors, hasCustomColors, currentPaletteName, distributedPaletteColorOptions])
75
+ const paletteSourceFingerprint = hasCustomColors
76
+ ? 'custom-colors'
77
+ : `${currentPaletteName}|${paletteColorOptions.join('|')}`
78
+ const previousPaletteSource = useRef<{ hasCustomColors: boolean; fingerprint: string } | null>(null)
79
+
80
+ const getColorForAssignmentItem = ({ label }: SeriesColorAssignmentItem, index: number): string => {
81
+ const orderedColor = orderedColorScale(label)
82
+ return paletteColorOptions.includes(orderedColor)
83
+ ? orderedColor
84
+ : distributedPaletteColorOptions[index % distributedPaletteColorOptions.length] ||
85
+ paletteColorOptions[index % paletteColorOptions.length] ||
86
+ '#000000'
87
+ }
88
+
89
+ const getAssignmentsFromCurrentColors = (): SeriesColorAssignment[] =>
90
+ seriesColorAssignmentItems.map((item, index) => ({
91
+ key: item.key,
92
+ color: getColorForAssignmentItem(item, index)
93
+ }))
94
+
95
+ const getAssignmentsWithMissingCurrentColors = (): SeriesColorAssignment[] | null => {
96
+ const assignments = [...seriesColorAssignments]
97
+ let hasMissingAssignment = false
98
+
99
+ seriesColorAssignmentItems.forEach((item, index) => {
100
+ const existingIndex = assignments.findIndex(assignment => assignment.key === item.key)
101
+ const nextAssignment = { key: item.key, color: getColorForAssignmentItem(item, index) }
102
+
103
+ if (existingIndex === -1) {
104
+ assignments.push(nextAssignment)
105
+ hasMissingAssignment = true
106
+ } else if (!assignments[existingIndex]?.color) {
107
+ assignments[existingIndex] = nextAssignment
108
+ hasMissingAssignment = true
109
+ }
110
+ })
111
+
112
+ return hasMissingAssignment ? assignments : null
113
+ }
114
+
115
+ const updateSeriesColorAssignmentMode = (mode: 'ordered' | 'by-value') => {
116
+ const _state = cloneConfig(config)
117
+ if (!_state.general.palette) {
118
+ _state.general.palette = {}
119
+ }
120
+
121
+ _state.general.palette.colorAssignmentMode = mode
122
+ if (mode === 'by-value') {
123
+ _state.general.palette.colorAssignments = getAssignmentsFromCurrentColors()
124
+ }
125
+
126
+ updateConfig(_state)
127
+ }
128
+
129
+ const resetSeriesColorAssignmentsToCurrentColors = () => {
130
+ const _state = cloneConfig(config)
131
+ if (!_state.general.palette) {
132
+ _state.general.palette = {}
133
+ }
134
+
135
+ _state.general.palette.colorAssignmentMode = 'by-value'
136
+ _state.general.palette.colorAssignments = getAssignmentsFromCurrentColors()
137
+ updateConfig(_state)
138
+ }
139
+
140
+ const updateSeriesColorAssignment = (key: string, color: string) => {
141
+ const _state = cloneConfig(config)
142
+ if (!_state.general.palette) {
143
+ _state.general.palette = {}
144
+ }
145
+
146
+ const currentSeriesKeys = new Set(seriesColorAssignmentItems.map(({ key }) => key))
147
+ const assignments = seriesColorAssignments.filter(assignment => currentSeriesKeys.has(assignment.key))
148
+ const existingIndex = assignments.findIndex(assignment => assignment.key === key)
149
+ if (existingIndex === -1) {
150
+ assignments.push({ key, color })
151
+ } else {
152
+ assignments[existingIndex] = { key, color }
153
+ }
154
+
155
+ _state.general.palette.colorAssignmentMode = 'by-value'
156
+ _state.general.palette.colorAssignments = assignments
157
+ updateConfig(_state)
158
+ }
159
+
160
+ useEffect(() => {
161
+ const currentSource = { hasCustomColors, fingerprint: paletteSourceFingerprint }
162
+
163
+ if (!seriesColorAssignmentSupported || !isSeriesColorAssignmentByValue) {
164
+ previousPaletteSource.current = currentSource
165
+ return
166
+ }
167
+
168
+ const shouldResetFromPalette =
169
+ previousPaletteSource.current !== null &&
170
+ !hasCustomColors &&
171
+ paletteColorOptions.length > 0 &&
172
+ (previousPaletteSource.current.hasCustomColors ||
173
+ previousPaletteSource.current.fingerprint !== paletteSourceFingerprint)
174
+
175
+ previousPaletteSource.current = currentSource
176
+
177
+ if (shouldResetFromPalette) {
178
+ resetSeriesColorAssignmentsToCurrentColors()
179
+ return
180
+ }
181
+
182
+ const assignmentsWithMissingCurrentColors = getAssignmentsWithMissingCurrentColors()
183
+ if (assignmentsWithMissingCurrentColors) {
184
+ const _state = cloneConfig(config)
185
+ if (!_state.general.palette) {
186
+ _state.general.palette = {}
187
+ }
188
+
189
+ _state.general.palette.colorAssignmentMode = 'by-value'
190
+ _state.general.palette.colorAssignments = assignmentsWithMissingCurrentColors
191
+ updateConfig(_state)
192
+ }
193
+ }, [
194
+ hasCustomColors,
195
+ isSeriesColorAssignmentByValue,
196
+ paletteSourceFingerprint,
197
+ seriesColorAssignmentItemsFingerprint,
198
+ seriesColorAssignmentsFingerprint,
199
+ seriesColorAssignmentSupported
200
+ ])
201
+
202
+ if (!seriesColorAssignmentSupported) return null
203
+
204
+ return (
205
+ <fieldset className='series-color-assignments'>
206
+ <Select
207
+ value={seriesColorAssignmentMode}
208
+ label='Color Assignment Mode'
209
+ tooltip={
210
+ <Tooltip style={{ textTransform: 'none' }}>
211
+ <Tooltip.Target>
212
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
213
+ </Tooltip.Target>
214
+ <Tooltip.Content>
215
+ <p>
216
+ Toggling this determines how colors in the palette are assigned. Colors can be assigned in order or can
217
+ be associated with specific series.
218
+ </p>
219
+ </Tooltip.Content>
220
+ </Tooltip>
221
+ }
222
+ fieldName='colorAssignmentMode'
223
+ section='general'
224
+ subsection='palette'
225
+ updateField={(_section, _subsection, _fieldName, value) => {
226
+ updateSeriesColorAssignmentMode(value as 'ordered' | 'by-value')
227
+ }}
228
+ options={[
229
+ { label: 'Use palette order', value: 'ordered' },
230
+ { label: 'Assign colors to series', value: 'by-value' }
231
+ ]}
232
+ />
233
+ {isSeriesColorAssignmentByValue && (
234
+ <div className='series-color-assignments__rows'>
235
+ {seriesColorAssignmentItems.map(({ key, label }) => {
236
+ const assignedColor = seriesColorAssignmentMap.get(key)
237
+ const color = assignedColor || orderedColorScale(label) || paletteColorOptions[0] || '#000000'
238
+
239
+ return (
240
+ <div className='series-color-assignments__row' key={key}>
241
+ <label className='series-color-assignments__label' title={`Series key: ${key}`}>
242
+ {label}
243
+ </label>
244
+ <DataColorSelector
245
+ aria-label={`Color for ${label}`}
246
+ value={color}
247
+ colors={paletteColorOptions}
248
+ allowNone={false}
249
+ allowCustom={false}
250
+ showCustomValue={false}
251
+ onChange={nextColor => updateSeriesColorAssignment(key, nextColor)}
252
+ />
253
+ </div>
254
+ )
255
+ })}
256
+ </div>
257
+ )}
258
+ </fieldset>
259
+ )
260
+ }
261
+
262
+ export default SeriesColorAssignments
@@ -0,0 +1,32 @@
1
+ .series-color-assignments {
2
+ border: 1px solid #d6d7d9;
3
+ border-radius: 4px;
4
+ margin-top: 1rem;
5
+ padding: 0.875rem;
6
+ }
7
+
8
+ .series-color-assignments__rows {
9
+ display: grid;
10
+ gap: 0.5rem;
11
+ margin-top: 0.75rem;
12
+ }
13
+
14
+ .series-color-assignments__row {
15
+ align-items: center;
16
+ display: grid;
17
+ gap: 0.5rem;
18
+ grid-template-columns: minmax(0, 1fr) auto;
19
+ }
20
+
21
+ .series-color-assignments__label {
22
+ color: #222;
23
+ display: block;
24
+ font-size: 0.875rem;
25
+ overflow: hidden;
26
+ text-overflow: ellipsis;
27
+ white-space: nowrap;
28
+ }
29
+
30
+ .series-color-assignments__row .data-color-selector__trigger {
31
+ justify-self: end;
32
+ }
@@ -83,3 +83,13 @@
83
83
  .annotation-group {
84
84
  border: 1px solid black;
85
85
  }
86
+
87
+ .small-multiples-color-assignment-warning {
88
+ background: #fff7e6;
89
+ border-left: 3px solid #a65f00;
90
+ color: #3d2a00;
91
+ font-size: 0.875rem;
92
+ line-height: 1.4;
93
+ margin: 0.75rem 0 1rem;
94
+ padding: 0.5rem 0.75rem;
95
+ }
@@ -1,4 +1,51 @@
1
1
  .cove-visualization.type-chart {
2
+ .axis-domain-group {
3
+ border: 1px solid var(--lightGray);
4
+ border-radius: 4px;
5
+ margin-top: 1em;
6
+ padding: 0.75em;
7
+
8
+ > label,
9
+ > .checkbox {
10
+ margin-top: 0.9em;
11
+ }
12
+ }
13
+
14
+ .axis-domain-group__title {
15
+ display: block;
16
+ font-size: 0.85em;
17
+ font-weight: 700;
18
+ margin-bottom: 0.75em;
19
+ text-transform: uppercase;
20
+ }
21
+
22
+ .axis-domain-group__row {
23
+ display: flex;
24
+ gap: 0.75em;
25
+ }
26
+
27
+ .axis-domain-group__field {
28
+ flex: 1 1 0;
29
+ min-width: 0;
30
+
31
+ > label {
32
+ margin-top: 0 !important;
33
+ }
34
+ }
35
+
36
+ .axis-domain-number {
37
+ max-width: 96px;
38
+ min-width: auto;
39
+ width: 100%;
40
+ }
41
+
42
+ .axis-domain-warning {
43
+ color: red;
44
+ display: block;
45
+ font-size: 0.85em;
46
+ margin-top: 0.25em;
47
+ }
48
+
2
49
  .viewport-overrides {
3
50
  button {
4
51
  width: 100%;
@@ -0,0 +1,61 @@
1
+ import { type ChartConfig, type VisualizationType } from '../../../types/ChartConfig'
2
+ import { HEATMAP_CONFIG_DEFAULTS } from '../../HeatMap/heatmap.constants'
3
+
4
+ export const getVisualizationTypeConfigUpdate = (
5
+ config: ChartConfig,
6
+ visualizationType: VisualizationType
7
+ ): ChartConfig | undefined => {
8
+ if (visualizationType === 'Forecasting' && config.xAxis.type === 'categorical') {
9
+ return {
10
+ ...config,
11
+ visualizationType,
12
+ xAxis: {
13
+ ...config.xAxis,
14
+ type: 'date',
15
+ dateParseFormat: config.xAxis.dateParseFormat || '%Y-%m-%d',
16
+ dateDisplayFormat: config.xAxis.dateDisplayFormat || '%Y-%m-%d'
17
+ }
18
+ }
19
+ }
20
+
21
+ if (visualizationType === 'HeatMap') {
22
+ return {
23
+ ...config,
24
+ visualizationType,
25
+ series: (config.series || []).map(series => ({
26
+ ...series,
27
+ type: 'HeatMap',
28
+ axis: 'Left'
29
+ })),
30
+ yAxis: {
31
+ ...config.yAxis,
32
+ type: 'categorical',
33
+ titlePlacement: 'side'
34
+ },
35
+ heatmap: {
36
+ cellPadding: config.heatmap?.cellPadding ?? HEATMAP_CONFIG_DEFAULTS.cellPadding,
37
+ rowLabelGap: config.heatmap?.rowLabelGap ?? HEATMAP_CONFIG_DEFAULTS.rowLabelGap,
38
+ columnLabelGap: config.heatmap?.columnLabelGap ?? HEATMAP_CONFIG_DEFAULTS.columnLabelGap,
39
+ colorBucketCount: config.heatmap?.colorBucketCount ?? HEATMAP_CONFIG_DEFAULTS.colorBucketCount,
40
+ xAxisPosition: config.heatmap?.xAxisPosition ?? HEATMAP_CONFIG_DEFAULTS.xAxisPosition,
41
+ showCellValues: config.heatmap?.showCellValues ?? HEATMAP_CONFIG_DEFAULTS.showCellValues
42
+ },
43
+ legend: {
44
+ ...config.legend,
45
+ position: 'top',
46
+ style: 'gradient',
47
+ subStyle: 'linear blocks'
48
+ },
49
+ general: {
50
+ ...config.general,
51
+ palette: config.general?.palette || {
52
+ isReversed: false,
53
+ version: '2.0',
54
+ name: 'sequential_blue'
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ return undefined
61
+ }
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { type ChartConfig } from '../../../../types/ChartConfig'
3
+ import { HEATMAP_CONFIG_DEFAULTS } from '../../../HeatMap/heatmap.constants'
4
+ import { getVisualizationTypeConfigUpdate } from '../getVisualizationTypeConfigUpdate'
5
+
6
+ const buildConfig = (overrides: Partial<ChartConfig> = {}) =>
7
+ ({
8
+ visualizationType: 'Bar',
9
+ xAxis: {
10
+ type: 'categorical'
11
+ },
12
+ yAxis: {
13
+ type: 'linear'
14
+ },
15
+ series: [{ dataKey: 'Value' }],
16
+ legend: {
17
+ position: 'bottom',
18
+ style: 'circles',
19
+ subStyle: 'linear blocks'
20
+ },
21
+ general: {},
22
+ ...overrides
23
+ } as ChartConfig)
24
+
25
+ describe('getVisualizationTypeConfigUpdate', () => {
26
+ it('does not build a special config update for chart types without transition rules', () => {
27
+ expect(getVisualizationTypeConfigUpdate(buildConfig(), 'Line')).toBeUndefined()
28
+ })
29
+
30
+ it('normalizes categorical x-axis settings when switching to Forecasting', () => {
31
+ const updatedConfig = getVisualizationTypeConfigUpdate(buildConfig(), 'Forecasting')
32
+
33
+ expect(updatedConfig?.visualizationType).toBe('Forecasting')
34
+ expect(updatedConfig?.xAxis.type).toBe('date')
35
+ expect(updatedConfig?.xAxis.dateParseFormat).toBe('%Y-%m-%d')
36
+ expect(updatedConfig?.xAxis.dateDisplayFormat).toBe('%Y-%m-%d')
37
+ })
38
+
39
+ it('applies HeatMap defaults and required editor config when switching to HeatMap', () => {
40
+ const updatedConfig = getVisualizationTypeConfigUpdate(buildConfig(), 'HeatMap')
41
+
42
+ expect(updatedConfig?.visualizationType).toBe('HeatMap')
43
+ expect(updatedConfig?.series).toEqual([{ dataKey: 'Value', type: 'HeatMap', axis: 'Left' }])
44
+ expect(updatedConfig?.yAxis.type).toBe('categorical')
45
+ expect(updatedConfig?.yAxis.titlePlacement).toBe('side')
46
+ expect(updatedConfig?.heatmap).toEqual(HEATMAP_CONFIG_DEFAULTS)
47
+ expect(updatedConfig?.legend.position).toBe('top')
48
+ expect(updatedConfig?.legend.style).toBe('gradient')
49
+ expect(updatedConfig?.legend.subStyle).toBe('linear blocks')
50
+ expect(updatedConfig?.general.palette).toEqual({
51
+ isReversed: false,
52
+ version: '2.0',
53
+ name: 'sequential_blue'
54
+ })
55
+ })
56
+
57
+ it('preserves selected rows when switching back to HeatMap from another chart type', () => {
58
+ const updatedConfig = getVisualizationTypeConfigUpdate(
59
+ buildConfig({
60
+ visualizationType: 'Bar',
61
+ series: [
62
+ { dataKey: 'North', name: 'North', type: 'Bar', axis: 'Left', tooltip: true },
63
+ { dataKey: 'South', name: 'South', type: 'Bar', axis: 'Left', tooltip: true }
64
+ ]
65
+ }),
66
+ 'HeatMap'
67
+ )
68
+
69
+ expect(updatedConfig?.series).toEqual([
70
+ { dataKey: 'North', name: 'North', type: 'HeatMap', axis: 'Left', tooltip: true },
71
+ { dataKey: 'South', name: 'South', type: 'HeatMap', axis: 'Left', tooltip: true }
72
+ ])
73
+ })
74
+ })