@cdc/chart 4.26.1 → 4.26.3

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 (173) hide show
  1. package/CLAUDE.local.md +79 -0
  2. package/LICENSE +201 -0
  3. package/dist/{cdcchart-dgT_1dIT.es.js → cdcchart-DQ00cQCm.es.js} +1 -20
  4. package/dist/cdcchart.js +54742 -49796
  5. package/examples/data/data-with-metadata.json +10 -0
  6. package/examples/default.json +378 -0
  7. package/examples/feature/__data__/horizon-chart-data.json +373 -0
  8. package/examples/feature/annotations/index.json +3 -6
  9. package/examples/feature/horizon/horizon-chart.json +395 -0
  10. package/examples/feature/pie/planet-pie-example-config.json +2 -1
  11. package/examples/line-chart-states.json +1085 -0
  12. package/examples/metadata-variables.json +58 -0
  13. package/examples/private/123.json +694 -0
  14. package/examples/private/anchor-issue.json +4094 -0
  15. package/examples/private/backwards-slider.json +10430 -0
  16. package/examples/private/georgia.csv +160 -0
  17. package/examples/private/timeline-data.json +1 -0
  18. package/examples/private/timeline.json +389 -0
  19. package/examples/radar-chart-simple.json +133 -0
  20. package/examples/radar-chart.json +148 -0
  21. package/index.html +1 -31
  22. package/package.json +57 -59
  23. package/src/CdcChart.tsx +8 -4
  24. package/src/CdcChartComponent.tsx +398 -284
  25. package/src/_stories/Chart.Anchors.stories.tsx +10 -0
  26. package/src/_stories/Chart.BoxPlot.stories.tsx +7 -0
  27. package/src/_stories/Chart.CI.stories.tsx +13 -0
  28. package/src/_stories/Chart.Combo.stories.tsx +17 -0
  29. package/src/_stories/Chart.CustomColors.stories.tsx +78 -0
  30. package/src/_stories/Chart.Defaults.stories.tsx +95 -0
  31. package/src/_stories/Chart.DynamicSeries.stories.tsx +19 -0
  32. package/src/_stories/Chart.Filters.stories.tsx +4 -0
  33. package/src/_stories/Chart.Forecast.stories.tsx +4 -0
  34. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +22 -0
  35. package/src/_stories/Chart.Legend.Gradient.stories.tsx +28 -0
  36. package/src/_stories/Chart.Patterns.stories.tsx +4 -0
  37. package/src/_stories/Chart.PreserveDecimals.stories.tsx +25 -0
  38. package/src/_stories/Chart.Regions.Categorical.stories.tsx +13 -0
  39. package/src/_stories/Chart.Regions.DateScale.stories.tsx +19 -0
  40. package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +25 -10
  41. package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
  42. package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
  43. package/src/_stories/Chart.SmallestLeftAxisMax.stories.tsx +64 -0
  44. package/src/_stories/Chart.stories.tsx +72 -1
  45. package/src/_stories/Chart.tooltip.stories.tsx +7 -0
  46. package/src/_stories/ChartAnnotation.stories.tsx +10 -0
  47. package/src/_stories/ChartAxisLabels.stories.tsx +4 -0
  48. package/src/_stories/ChartAxisTitles.stories.tsx +10 -0
  49. package/src/_stories/ChartBar.Editor.stories.tsx +97 -38
  50. package/src/_stories/ChartBrush.Editor.stories.tsx +11 -25
  51. package/src/_stories/ChartBrush.Matrix.Continuous.stories.tsx +41 -0
  52. package/src/_stories/ChartBrush.Matrix.Date.stories.tsx +114 -0
  53. package/src/_stories/ChartBrush.Matrix.DateTime.stories.tsx +78 -0
  54. package/src/_stories/ChartBrush.stories.tsx +7 -0
  55. package/src/_stories/ChartEditor.Editor.stories.tsx +1 -1
  56. package/src/_stories/ChartEditor.stories.tsx +7 -0
  57. package/src/_stories/ChartLine.QuadrantAngles.stories.tsx +89 -0
  58. package/src/_stories/ChartLine.Suppression.stories.tsx +7 -0
  59. package/src/_stories/ChartLine.Symbols.stories.tsx +4 -0
  60. package/src/_stories/ChartPrefixSuffix.stories.tsx +46 -1
  61. package/src/_stories/TechAdoptionWithLinks.stories.tsx +7 -0
  62. package/src/_stories/_mock/brush_continuous.json +86 -0
  63. package/src/_stories/_mock/brush_date_large.json +176 -0
  64. package/src/_stories/_mock/line_chart_angle_near_zero_fall.json +195 -0
  65. package/src/_stories/_mock/line_chart_angle_near_zero_rise.json +195 -0
  66. package/src/_stories/_mock/line_chart_angle_q1_steep_upward.json +195 -0
  67. package/src/_stories/_mock/line_chart_angle_q2_gentle_downward.json +195 -0
  68. package/src/_stories/_mock/line_chart_angle_q3_steep_downward.json +195 -0
  69. package/src/_stories/_mock/line_chart_angle_q4_gentle_upward.json +195 -0
  70. package/src/_stories/_mock/line_chart_quadrant_angles.json +264 -0
  71. package/src/_stories/_mock/paired-bar-abbr.json +421 -0
  72. package/src/_stories/_mock/pie_custom_colors.json +268 -0
  73. package/src/_stories/_mock/smallest_left_axis_max.json +104 -0
  74. package/src/components/Annotations/components/AnnotationDraggable.styles.css +14 -20
  75. package/src/components/Annotations/components/AnnotationDraggable.tsx +240 -116
  76. package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -2
  77. package/src/components/Annotations/components/AnnotationDropdown.tsx +8 -12
  78. package/src/components/Annotations/components/AnnotationList.styles.css +12 -18
  79. package/src/components/Annotations/components/AnnotationList.tsx +5 -4
  80. package/src/components/Annotations/components/findNearestDatum.ts +75 -85
  81. package/src/components/Annotations/helpers/getVisibleAnnotations.ts +38 -0
  82. package/src/components/Axis/BottomAxis.tsx +277 -0
  83. package/src/components/Axis/LeftAxis.tsx +404 -0
  84. package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
  85. package/src/components/Axis/PairedBarAxis.tsx +192 -0
  86. package/src/components/Axis/README.md +94 -0
  87. package/src/components/Axis/RightAxis.tsx +108 -0
  88. package/src/components/Axis/axis.constants.ts +21 -0
  89. package/src/components/Axis/index.ts +7 -0
  90. package/src/components/BarChart/components/BarChart.Horizontal.tsx +12 -28
  91. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +12 -30
  92. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +12 -31
  93. package/src/components/BarChart/components/BarChart.Vertical.tsx +12 -28
  94. package/src/components/BarChart/components/BarChart.tsx +7 -1
  95. package/src/components/BarChart/helpers/getPatternUrl.ts +94 -0
  96. package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +134 -0
  97. package/src/components/BarChart/helpers/useBarChart.ts +3 -0
  98. package/src/components/Brush/BrushSelector.tsx +155 -22
  99. package/src/components/Brush/MiniChartPreview.tsx +133 -21
  100. package/src/components/EditorPanel/EditorPanel.tsx +81 -54
  101. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +67 -29
  102. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +0 -78
  103. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +120 -2
  104. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +25 -43
  105. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
  106. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +83 -3
  107. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +66 -43
  108. package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
  109. package/src/components/EditorPanel/editor-panel.scss +1 -1
  110. package/src/components/EditorPanel/useEditorPermissions.ts +55 -26
  111. package/src/components/ForestPlot/ForestPlot.tsx +26 -22
  112. package/src/components/HorizonChart/HorizonChart.tsx +131 -0
  113. package/src/components/HorizonChart/components/HorizonBand.tsx +160 -0
  114. package/src/components/HorizonChart/helpers/calculateHorizonBands.ts +27 -0
  115. package/src/components/HorizonChart/helpers/getHorizonLayerColors.ts +40 -0
  116. package/src/components/HorizonChart/index.tsx +3 -0
  117. package/src/components/Legend/Legend.Component.tsx +52 -4
  118. package/src/components/Legend/Legend.tsx +1 -1
  119. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +4 -4
  120. package/src/components/Legend/LegendValueRange.tsx +77 -0
  121. package/src/components/Legend/helpers/createFormatLabels.tsx +16 -2
  122. package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
  123. package/src/components/LineChart/helpers/README.md +292 -0
  124. package/src/components/LineChart/helpers/labelPositioning.test.ts +245 -0
  125. package/src/components/LineChart/helpers/labelPositioning.ts +304 -0
  126. package/src/components/LineChart/index.tsx +44 -8
  127. package/src/components/LinearChart/README.md +109 -0
  128. package/src/components/LinearChart/VisualizationRenderer.tsx +267 -0
  129. package/src/components/LinearChart/linearChart.constants.ts +84 -0
  130. package/src/components/LinearChart/tests/LinearChart.test.tsx +278 -0
  131. package/src/components/LinearChart/tests/mockConfigContext.ts +131 -0
  132. package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
  133. package/src/components/LinearChart.tsx +268 -1057
  134. package/src/components/PieChart/PieChart.tsx +20 -5
  135. package/src/components/RadarChart/RadarAxis.tsx +78 -0
  136. package/src/components/RadarChart/RadarChart.tsx +298 -0
  137. package/src/components/RadarChart/RadarGrid.tsx +64 -0
  138. package/src/components/RadarChart/RadarPolygon.tsx +91 -0
  139. package/src/components/RadarChart/helpers.ts +83 -0
  140. package/src/components/RadarChart/index.tsx +3 -0
  141. package/src/components/Regions/components/Regions.tsx +6 -6
  142. package/src/components/Sankey/components/Sankey.tsx +3 -3
  143. package/src/components/Sankey/sankey.scss +1 -1
  144. package/src/components/SmallMultiples/SmallMultiples.css +5 -5
  145. package/src/components/Sparkline/index.scss +4 -2
  146. package/src/components/WarmingStripes/WarmingStripes.tsx +95 -25
  147. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +8 -8
  148. package/src/data/initial-state.js +37 -15
  149. package/src/data/legacy-defaults.ts +18 -0
  150. package/src/helpers/abbreviateNumber.ts +24 -17
  151. package/src/helpers/getChartPatternId.ts +17 -0
  152. package/src/helpers/getExcludedData.ts +4 -0
  153. package/src/helpers/getMinMax.ts +16 -2
  154. package/src/helpers/handleChartAriaLabels.ts +19 -19
  155. package/src/helpers/handleLineType.ts +22 -18
  156. package/src/helpers/seriesColumnSettings.ts +114 -0
  157. package/src/helpers/tests/countNumOfTicks.test.ts +77 -0
  158. package/src/helpers/tests/seriesColumnSettings.test.ts +84 -0
  159. package/src/hooks/useProgrammaticTooltip.ts +23 -2
  160. package/src/hooks/useRightAxis.ts +14 -0
  161. package/src/hooks/useScales.ts +99 -56
  162. package/src/hooks/useTooltip.tsx +23 -3
  163. package/src/scss/main.scss +157 -79
  164. package/src/selectors/README.md +68 -0
  165. package/src/store/chart.reducer.ts +2 -0
  166. package/src/test/CdcChart.test.jsx +2 -2
  167. package/src/types/ChartConfig.ts +22 -0
  168. package/src/types/ChartContext.ts +1 -0
  169. package/src/types/Horizon.ts +64 -0
  170. package/tests/fixtures/chart-config-with-metadata.json +29 -0
  171. package/tests/fixtures/data-with-metadata.json +10 -0
  172. package/preview.html +0 -1616
  173. package/src/components/Annotations/components/helpers/index.tsx +0 -46
@@ -91,7 +91,7 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
91
91
  ? new Date(domain[domain.length - 1] as string | number).getTime()
92
92
  : new Date(region.to)
93
93
 
94
- const toFormatted = formatDate(config.xAxis.dateParseFormat, toRefDate)
94
+ const toFormatted = formatDate(config.xAxis.dateParseFormat, toRefDate, config.locale)
95
95
  const toDate = new Date(toFormatted)
96
96
  const fromDate = new Date(toDate)
97
97
  fromDate.setDate(fromDate.getDate() - previousDays)
@@ -99,7 +99,7 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
99
99
  let closestValue: unknown
100
100
 
101
101
  if (axisType === 'date') {
102
- const fromTime = new Date(formatDate(xAxis.dateParseFormat, fromDate)).getTime()
102
+ const fromTime = new Date(formatDate(xAxis.dateParseFormat, fromDate, config.locale)).getTime()
103
103
  closestValue = findClosestDate(fromTime, domain as number[], d => d)
104
104
  } else if (axisType === 'categorical') {
105
105
  const fromTime = fromDate.getTime()
@@ -151,7 +151,7 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
151
151
  // For date scale (band), we need to find the value in the domain
152
152
  // Parse the region date to match the format in the domain
153
153
  const date = new Date(region.from)
154
- const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date)).getTime()
154
+ const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date, config.locale)).getTime()
155
155
 
156
156
  // For band scales, find the closest date in the domain
157
157
  const domain = xScale.domain() as number[]
@@ -193,7 +193,7 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
193
193
  return from + Number(config.yAxis.size)
194
194
  }
195
195
  const date = new Date(region.from)
196
- const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date)).getTime()
196
+ const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date, config.locale)).getTime()
197
197
  let from = xScale(parsedDate)
198
198
  // For date-time, xScale returns correct position (no bandwidth), just add left padding
199
199
  return from + Number(config.yAxis.size)
@@ -252,7 +252,7 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
252
252
  }
253
253
  // For date scale (band), we need to find the value in the domain
254
254
  const date = new Date(region.from)
255
- const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date)).getTime()
255
+ const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date, config.locale)).getTime()
256
256
 
257
257
  // For band scales, find the closest date in the domain
258
258
  const domain = xScale.domain() as number[]
@@ -281,7 +281,7 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
281
281
  from = calculatePreviousDaysFrom(region, 'date-time')
282
282
  } else {
283
283
  const date = new Date(region.from)
284
- const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date)).getTime()
284
+ const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date, config.locale)).getTime()
285
285
  from = xScale(parsedDate)
286
286
  }
287
287
  return from - getBarOffset()
@@ -226,7 +226,7 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
226
226
  data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
227
227
  data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
228
228
  >
229
- {typeof node.value === 'number' ? node.value.toLocaleString() : node.value}
229
+ {typeof node.value === 'number' ? node.value.toLocaleString(config.locale) : node.value}
230
230
  </Text>
231
231
  <Text
232
232
  width={linkLength()}
@@ -281,7 +281,7 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
281
281
  >
282
282
  <tspan className={classStyle}>
283
283
  {sankeyConfig.nodeValueStyle.textBefore +
284
- (typeof node.value === 'number' ? node.value.toLocaleString() : node.value) +
284
+ (typeof node.value === 'number' ? node.value.toLocaleString(config.locale) : node.value) +
285
285
  sankeyConfig.nodeValueStyle.textAfter}
286
286
  </tspan>
287
287
  </text>
@@ -394,7 +394,7 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
394
394
  textAnchor='start'
395
395
  style={{ pointerEvents: 'none' }}
396
396
  >
397
- {typeof node.value === 'number' ? node.value.toLocaleString() : node.value}
397
+ {typeof node.value === 'number' ? node.value.toLocaleString(config.locale) : node.value}
398
398
  </Text>
399
399
  <Text
400
400
  x={node.x0! + textPositionHorizontal}
@@ -5,7 +5,7 @@
5
5
  column-gap: 30px;
6
6
  }
7
7
 
8
- .cdc-open-viz-module .sankey-chart {
8
+ .cove-visualization .sankey-chart {
9
9
  --font-size-small: 12px;
10
10
  --font-size-medium: 16px;
11
11
  --font-size-large: 18px;
@@ -1,13 +1,13 @@
1
1
  .small-multiples-container {
2
- width: 100%;
3
2
  display: flex;
4
3
  flex-direction: column;
4
+ width: 100%;
5
5
  }
6
6
 
7
7
  .small-multiples-grid {
8
8
  display: grid;
9
- width: 100%;
10
9
  flex: 1;
10
+ width: 100%;
11
11
  }
12
12
 
13
13
  .small-multiple-tile {
@@ -20,13 +20,13 @@
20
20
  }
21
21
 
22
22
  .tile-title {
23
- margin: 0;
24
23
  font-weight: 700;
25
- text-align: left;
26
24
  line-height: 1.3;
25
+ margin: 0;
26
+ text-align: left;
27
27
  }
28
28
 
29
29
  .tile-chart {
30
- width: 100%;
31
30
  flex-shrink: 0;
31
+ width: 100%;
32
32
  }
@@ -1,3 +1,5 @@
1
- .cdc-open-viz-module.type-chart.lg.type-sparkline .filters-section {
2
- margin: 0;
1
+ .cove-visualization.type-chart.type-sparkline {
2
+ &.lg .filters-section {
3
+ margin: 0;
4
+ }
3
5
  }
@@ -1,4 +1,4 @@
1
- import { useContext, useState } from 'react'
1
+ import { useContext, useState, useCallback, useEffect } from 'react'
2
2
  import ConfigContext from '../../ConfigContext'
3
3
  import { Group } from '@visx/group'
4
4
  import { scaleSequential } from 'd3-scale'
@@ -6,7 +6,6 @@ import { interpolateRgbBasis } from 'd3-interpolate'
6
6
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
7
7
  import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
8
8
  import { filterChartColorPalettes } from '@cdc/core/helpers/filterColorPalettes'
9
- import { getColorPaletteVersion } from '@cdc/core/helpers/getColorPaletteVersion'
10
9
  import { getFallbackColorPalette, migratePaletteWithMap } from '@cdc/core/helpers/palettes/utils'
11
10
  import { paletteMigrationMap } from '@cdc/core/helpers/palettes/migratePaletteName'
12
11
  import { hasTrackedHover, markHoverTracked } from '../../utils/analyticsTracking'
@@ -16,10 +15,26 @@ type WarmingStripesProps = {
16
15
  yScale: any
17
16
  xMax: number
18
17
  yMax: number
18
+ synchronizedXValue?: any
19
+ showTooltip: (args: any) => void
20
+ handleTooltipMouseOff: () => void
19
21
  }
20
22
 
21
- const WarmingStripes = ({ xMax, yMax }: WarmingStripesProps) => {
22
- const { transformedData: data, config, formatNumber, interactionLabel, currentViewport } = useContext(ConfigContext)
23
+ const WarmingStripes = ({
24
+ xMax,
25
+ yMax,
26
+ synchronizedXValue,
27
+ showTooltip,
28
+ handleTooltipMouseOff
29
+ }: WarmingStripesProps) => {
30
+ const {
31
+ transformedData: data,
32
+ config,
33
+ formatNumber,
34
+ interactionLabel,
35
+ currentViewport,
36
+ handleSmallMultipleHover
37
+ } = useContext(ConfigContext)
23
38
 
24
39
  const [currentHover, setCurrentHover] = useState<number | null>(null)
25
40
 
@@ -28,18 +43,14 @@ const WarmingStripes = ({ xMax, yMax }: WarmingStripesProps) => {
28
43
  const valueKey = config.runtime.seriesKeys?.[0]
29
44
  const xAxisDataKey = config.runtime.originalXAxis?.dataKey || config.xAxis?.dataKey
30
45
 
31
- if (!valueKey || !xAxisDataKey || !data || data.length === 0) {
32
- return null
33
- }
34
-
35
46
  // Determine max stripes based on viewport
36
47
  const isMobile = ['xxs', 'xs', 'sm', 'md'].includes(currentViewport)
37
48
  const maxStripes = isMobile ? 60 : 200
38
49
 
39
50
  // Sample data if we have more than the max allowed stripes
40
- let displayData = data
41
- if (data.length > maxStripes) {
42
- const step = data.length / maxStripes
51
+ let displayData = data || []
52
+ if (displayData.length > maxStripes) {
53
+ const step = displayData.length / maxStripes
43
54
  displayData = []
44
55
  for (let i = 0; i < maxStripes; i++) {
45
56
  const index = Math.floor(i * step)
@@ -48,7 +59,7 @@ const WarmingStripes = ({ xMax, yMax }: WarmingStripesProps) => {
48
59
  }
49
60
 
50
61
  // Calculate the min and max values for the color scale
51
- const values = data.map(d => Number(d[valueKey])).filter(v => !isNaN(v))
62
+ const values = displayData.map(d => Number(d[valueKey])).filter(v => !isNaN(v))
52
63
  const minValue = Math.min(...values)
53
64
  const maxValue = Math.max(...values)
54
65
 
@@ -94,15 +105,54 @@ const WarmingStripes = ({ xMax, yMax }: WarmingStripesProps) => {
94
105
  // Calculate stripe width based on available space and display data
95
106
  const stripeWidth = xMax / displayData.length
96
107
 
97
- const handleTooltip = (item: any) => {
98
- const xValue = item[xAxisDataKey]
99
- const yValue = item[valueKey]
100
- const formattedValue = formatNumber(yValue, 'left')
108
+ // Build tooltip data in COVE format and trigger showTooltip
109
+ const showStripeTooltip = useCallback(
110
+ (item: any, index: number, mouseY?: number) => {
111
+ const value = Number(item[valueKey])
112
+ if (isNaN(value)) return
113
+
114
+ const formattedValue = formatNumber(value, 'left')
115
+
116
+ // Pass raw x-axis value — TooltipListItem handles date formatting
117
+ const tooltipItems = [
118
+ [xAxisDataKey, item[xAxisDataKey]],
119
+ [valueKey, formattedValue, 'left']
120
+ ]
121
+
122
+ const dataXPosition = index * stripeWidth + stripeWidth / 2 + Number(config.yAxis.size)
123
+ const dataYPosition = mouseY ?? yMax / 2
124
+
125
+ showTooltip({
126
+ tooltipLeft: dataXPosition + 10,
127
+ tooltipTop: dataYPosition,
128
+ tooltipData: {
129
+ data: tooltipItems,
130
+ dataXPosition: dataXPosition + 10,
131
+ dataYPosition
132
+ }
133
+ })
134
+ },
135
+ [valueKey, xAxisDataKey, stripeWidth, config.yAxis.size, yMax, formatNumber, showTooltip]
136
+ )
137
+
138
+ // Handle incoming synchronized tooltip from sibling small multiple tiles
139
+ useEffect(() => {
140
+ if (synchronizedXValue === null || synchronizedXValue === undefined) {
141
+ setCurrentHover(null)
142
+ handleTooltipMouseOff()
143
+ return
144
+ }
145
+
146
+ const matchIndex = displayData.findIndex(item => String(item[xAxisDataKey]) === String(synchronizedXValue))
147
+
148
+ if (matchIndex >= 0) {
149
+ setCurrentHover(matchIndex)
150
+ showStripeTooltip(displayData[matchIndex], matchIndex)
151
+ }
152
+ }, [synchronizedXValue])
101
153
 
102
- return `<div>
103
- <strong>${config.xAxis.label || xAxisDataKey}:</strong> ${xValue}<br/>
104
- <strong>${config.runtime.seriesLabels?.[valueKey] || valueKey}:</strong> ${formattedValue}
105
- </div>`
154
+ if (!valueKey || !xAxisDataKey || !data || data.length === 0) {
155
+ return null
106
156
  }
107
157
 
108
158
  return (
@@ -126,13 +176,10 @@ const WarmingStripes = ({ xMax, yMax }: WarmingStripesProps) => {
126
176
  fill={fillColor}
127
177
  fillOpacity={isMuted ? 0.5 : 1}
128
178
  stroke='none'
129
- data-tooltip-html={handleTooltip(item)}
130
- data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
131
179
  tabIndex={-1}
132
180
  style={{ cursor: 'pointer', transition: 'fill-opacity 0.2s ease' }}
133
- onMouseEnter={() => {
181
+ onMouseEnter={e => {
134
182
  if (currentHover !== index) {
135
- // Only publish analytics event once per visualization (shared tracking)
136
183
  const vizId = String(config.runtime.uniqueId)
137
184
  if (!hasTrackedHover(vizId)) {
138
185
  publishAnalyticsEvent({
@@ -148,8 +195,31 @@ const WarmingStripes = ({ xMax, yMax }: WarmingStripesProps) => {
148
195
  }
149
196
  setCurrentHover(index)
150
197
  }
198
+
199
+ // Show COVE tooltip using mouse Y position
200
+ const svgEl = (e.currentTarget as SVGRectElement).ownerSVGElement
201
+ const svgRect = svgEl?.getBoundingClientRect()
202
+ const mouseY = svgRect ? e.clientY - svgRect.top : yMax / 2
203
+ showStripeTooltip(item, index, mouseY)
204
+
205
+ if (handleSmallMultipleHover) {
206
+ handleSmallMultipleHover(item[xAxisDataKey], yMax / 2)
207
+ }
208
+ }}
209
+ onMouseMove={e => {
210
+ // Update tooltip Y position as mouse moves within the stripe
211
+ const svgEl = (e.currentTarget as SVGRectElement).ownerSVGElement
212
+ const svgRect = svgEl?.getBoundingClientRect()
213
+ const mouseY = svgRect ? e.clientY - svgRect.top : yMax / 2
214
+ showStripeTooltip(item, index, mouseY)
215
+ }}
216
+ onMouseLeave={() => {
217
+ setCurrentHover(null)
218
+ handleTooltipMouseOff()
219
+ if (handleSmallMultipleHover) {
220
+ handleSmallMultipleHover(null, null)
221
+ }
151
222
  }}
152
- onMouseLeave={() => setCurrentHover(null)}
153
223
  />
154
224
  )
155
225
  })}
@@ -4,16 +4,16 @@
4
4
  }
5
5
 
6
6
  .warming-stripes-gradient-legend__title {
7
+ font-size: 16px;
7
8
  font-weight: 600;
8
9
  margin-bottom: 0.5rem;
9
- font-size: 16px;
10
10
  }
11
11
 
12
12
  .warming-stripes-gradient-legend__description {
13
- margin-top: 0.5rem;
14
- margin-bottom: 1rem;
15
- font-size: 14px;
16
13
  color: #555;
14
+ font-size: 14px;
15
+ margin-bottom: 1rem;
16
+ margin-top: 0.5rem;
17
17
  }
18
18
 
19
19
  .warming-stripes-gradient-legend__container {
@@ -22,14 +22,14 @@
22
22
 
23
23
  .warming-stripes-gradient-legend__svg {
24
24
  display: block;
25
- width: 100%;
26
25
  overflow: visible;
26
+ width: 100%;
27
27
  }
28
28
 
29
29
  .warming-stripes-gradient-legend__series-label {
30
- text-align: center;
31
- margin-top: 0.5rem;
30
+ color: #333;
32
31
  font-size: 14px;
33
32
  font-weight: 500;
34
- color: #333;
33
+ margin-top: 0.5rem;
34
+ text-align: center;
35
35
  }
@@ -45,7 +45,8 @@ const createInitialState = () => {
45
45
  showSuppressedSymbol: true,
46
46
  showZeroValueData: true,
47
47
  hideNullValue: true,
48
- palette: paletteDefaults
48
+ palette: paletteDefaults,
49
+ useIntelligentLineChartLabels: false
49
50
  },
50
51
  padding: {
51
52
  left: 5,
@@ -53,12 +54,12 @@ const createInitialState = () => {
53
54
  },
54
55
  preliminaryData: [],
55
56
  yAxis: {
56
- hideAxis: false,
57
+ hideAxis: true,
57
58
  displayNumbersOnBar: false,
58
59
  hideLabel: false,
59
- hideTicks: false,
60
+ hideTicks: true,
60
61
  size: 50,
61
- gridLines: false,
62
+ gridLines: true,
62
63
  enablePadding: false,
63
64
  min: '',
64
65
  max: '',
@@ -72,7 +73,7 @@ const createInitialState = () => {
72
73
  rightAxisLabelColor: '#1c1d1f',
73
74
  rightAxisTickLabelColor: '#1c1d1f',
74
75
  rightAxisTickColor: '#1c1d1f',
75
- numTicks: '',
76
+ numTicks: 4,
76
77
  axisPadding: 0,
77
78
  scalePadding: 10,
78
79
  tickRotation: 0,
@@ -132,7 +133,8 @@ const createInitialState = () => {
132
133
  labelColor: '#1c1d1f',
133
134
  tickLabelColor: '#1c1d1f',
134
135
  tickColor: '#1c1d1f',
135
- numTicks: '',
136
+ numTicks: 6,
137
+ dateDisplayFormat: '%b. %-d %Y',
136
138
  labelOffset: 0,
137
139
  axisPadding: 200,
138
140
  target: 0,
@@ -141,11 +143,15 @@ const createInitialState = () => {
141
143
  showYearsOnce: false,
142
144
  sortByRecentDate: false,
143
145
  brushActive: false,
144
- brushDefaultRecentDateCount: undefined
146
+ brushDefaultRecentDateCount: undefined,
147
+ viewportNumTicks: {
148
+ xs: 4,
149
+ xxs: 4
150
+ }
145
151
  },
146
152
  table: {
147
153
  label: 'Data Table',
148
- expanded: true,
154
+ expanded: false,
149
155
  limitHeight: false,
150
156
  height: '',
151
157
  caption: '',
@@ -155,7 +161,7 @@ const createInitialState = () => {
155
161
  indexLabel: '',
156
162
  download: false,
157
163
  showVertical: true,
158
- dateDisplayFormat: '',
164
+ dateDisplayFormat: '%B %-d, %Y',
159
165
  showMissingDataLabel: true,
160
166
  showSuppressedSymbol: true,
161
167
  collapsible: true
@@ -195,7 +201,7 @@ const createInitialState = () => {
195
201
  side: false,
196
202
  topBottom: true
197
203
  },
198
- position: 'right',
204
+ position: 'top',
199
205
  orderedValues: [],
200
206
  patterns: {},
201
207
  patternField: ''
@@ -223,7 +229,7 @@ const createInitialState = () => {
223
229
  },
224
230
  labels: false,
225
231
  dataFormat: {
226
- commas: false,
232
+ commas: true,
227
233
  prefix: '',
228
234
  suffix: '',
229
235
  abbreviated: false,
@@ -234,9 +240,13 @@ const createInitialState = () => {
234
240
  filters: [],
235
241
  confidenceKeys: {},
236
242
  visual: {
237
- border: true,
238
- accent: true,
239
- background: true,
243
+ border: false,
244
+ borderColorTheme: false,
245
+ accent: false,
246
+ background: false,
247
+ hideBackgroundColor: false,
248
+ tp5Treatment: false,
249
+ tp5Background: false,
240
250
  verticalHoverLine: false,
241
251
  horizontalHoverLine: false,
242
252
  lineDatapointSymbol: 'none',
@@ -249,7 +259,7 @@ const createInitialState = () => {
249
259
  tooltips: {
250
260
  opacity: 90,
251
261
  singleSeries: false,
252
- dateDisplayFormat: ''
262
+ dateDisplayFormat: '%B %-d, %Y'
253
263
  },
254
264
  forestPlot: {
255
265
  startAt: 0,
@@ -298,6 +308,18 @@ const createInitialState = () => {
298
308
  area: {
299
309
  isStacked: false
300
310
  },
311
+ radar: {
312
+ gridRings: 5,
313
+ showGridRings: true,
314
+ gridRingStyle: 'polygons',
315
+ scaleMin: 0,
316
+ scaleMax: '',
317
+ fillOpacity: 0.3,
318
+ showPoints: true,
319
+ pointRadius: 4,
320
+ strokeWidth: 2,
321
+ axisLabelOffset: 15
322
+ },
301
323
  sankey: {
302
324
  title: {
303
325
  defaultColor: 'black'
@@ -0,0 +1,18 @@
1
+ // Preserves the OLD default values for properties changed in initial-state.js.
2
+ // When the backfill loop fills a missing property, it uses these values instead
3
+ // of the current defaults so that existing configs aren't visually affected.
4
+ //
5
+ // - Changed defaults: record the ORIGINAL value before any changes.
6
+ // - New properties: set to `undefined` so they are not backfilled at all.
7
+ //
8
+ // See backfillDefaults() in @cdc/core for the shared fill logic.
9
+ export const LEGACY_CHART_DEFAULTS: Record<string, Record<string, unknown>> = {
10
+ general: { useIntelligentLineChartLabels: undefined },
11
+ yAxis: { hideAxis: false, hideTicks: false, gridLines: false, numTicks: '' },
12
+ xAxis: { numTicks: '', dateDisplayFormat: undefined, viewportNumTicks: undefined },
13
+ table: { expanded: true, dateDisplayFormat: '' },
14
+ legend: { position: 'right' },
15
+ dataFormat: { commas: false },
16
+ tooltips: { dateDisplayFormat: '' },
17
+ visual: { border: false, accent: false, background: false }
18
+ }
@@ -1,17 +1,24 @@
1
- export const abbreviateNumber = num => {
2
- let unit = ''
3
- let absNum = Math.abs(num)
4
-
5
- if (absNum >= 1e9) {
6
- unit = 'B'
7
- num = num / 1e9
8
- } else if (absNum >= 1e6) {
9
- unit = 'M'
10
- num = num / 1e6
11
- } else if (absNum >= 1e3) {
12
- unit = 'K'
13
- num = num / 1e3
14
- }
15
-
16
- return num + unit
17
- }
1
+ const abbreviationUnits: Record<string, { K: string; M: string; B: string }> = {
2
+ 'es-MX': { K: ' mil', M: ' M', B: ' mil M' }
3
+ }
4
+
5
+ const defaultUnits = { K: 'K', M: 'M', B: 'B' }
6
+
7
+ export const abbreviateNumber = (num, locale?: string) => {
8
+ const units = (locale && abbreviationUnits[locale]) || defaultUnits
9
+ let unit = ''
10
+ let absNum = Math.abs(num)
11
+
12
+ if (absNum >= 1e9) {
13
+ unit = units.B
14
+ num = num / 1e9
15
+ } else if (absNum >= 1e6) {
16
+ unit = units.M
17
+ num = num / 1e6
18
+ } else if (absNum >= 1e3) {
19
+ unit = units.K
20
+ num = num / 1e3
21
+ }
22
+
23
+ return num + unit
24
+ }
@@ -0,0 +1,17 @@
1
+ import { sanitizeToSvgId } from '@cdc/core/helpers/cove/string'
2
+
3
+ const getStableKeyHash = (input: string): string => {
4
+ // djb2 variant, deterministic and cheap for short editor keys
5
+ let hash = 5381
6
+ for (let i = 0; i < input.length; i++) {
7
+ hash = (hash * 33) ^ input.charCodeAt(i)
8
+ }
9
+ return (hash >>> 0).toString(36)
10
+ }
11
+
12
+ export const getChartPatternId = (patternKey: string): string => {
13
+ const rawKey = String(patternKey)
14
+ const sanitizedKey = sanitizeToSvgId(rawKey)
15
+ const hashSuffix = getStableKeyHash(rawKey)
16
+ return `chart-pattern-${sanitizedKey}-${hashSuffix}`
17
+ }
@@ -2,6 +2,10 @@ import { isDateScale } from '@cdc/core/helpers/cove/date'
2
2
  import _ from 'lodash'
3
3
  import { ChartConfig } from '../types/ChartConfig'
4
4
  export const getExcludedData = (newConfig: ChartConfig, data: object[]) => {
5
+ if (!Array.isArray(data)) {
6
+ console.warn('COVE: getExcludedData received non-array data:', typeof data)
7
+ return []
8
+ }
5
9
  let newExcludedData = data
6
10
  if (newConfig.exclusions && newConfig.exclusions.active) {
7
11
  if (newConfig.xAxis.type === 'categorical' && newConfig.exclusions.keys?.length > 0) {
@@ -37,8 +37,8 @@ const getMinMax = ({
37
37
  let leftMax = 0
38
38
  let rightMax = 0
39
39
 
40
- if (!data) {
41
- return { min, max }
40
+ if (!data || !config.runtime) {
41
+ return { min, max, leftMax, rightMax }
42
42
  }
43
43
 
44
44
  const { visualizationType, series } = config
@@ -248,6 +248,20 @@ const getMinMax = ({
248
248
  min = min / 1.1
249
249
  }
250
250
 
251
+ // Enforce smallest left axis max so small-data charts don't show misleading decimal ticks
252
+ const smallestLeftAxisMaxRaw = config.yAxis.smallestLeftAxisMax
253
+ if (smallestLeftAxisMaxRaw !== null && smallestLeftAxisMaxRaw !== '') {
254
+ const smallestLeftAxisMax = Number(smallestLeftAxisMaxRaw)
255
+ if (!Number.isNaN(smallestLeftAxisMax)) {
256
+ if (max < smallestLeftAxisMax) {
257
+ max = smallestLeftAxisMax
258
+ }
259
+ if (leftMax < smallestLeftAxisMax) {
260
+ leftMax = smallestLeftAxisMax
261
+ }
262
+ }
263
+ }
264
+
251
265
  return { min, max, leftMax, rightMax }
252
266
  }
253
267
  export default getMinMax
@@ -1,19 +1,19 @@
1
- export const handleChartAriaLabels = (state, testing = false) => {
2
- if (testing) console.log(`handleChartAriaLabels Testing On:`, state) // eslint-disable-line
3
- try {
4
- if (!state.visualizationType) throw Error('handleChartAriaLabels: no visualization type found in state')
5
- let ariaLabel = ''
6
-
7
- if (state.visualizationType) {
8
- ariaLabel += `${state.visualizationType} chart`
9
- }
10
-
11
- if (state.title && state.visualizationType) {
12
- ariaLabel += ` with the title: ${state.title}`
13
- }
14
-
15
- return ariaLabel
16
- } catch (e) {
17
- console.error('COVE: ', e.message) // eslint-disable-line
18
- }
19
- }
1
+ export const handleChartAriaLabels = state => {
2
+ try {
3
+ if (!state.visualizationType) throw Error('handleChartAriaLabels: no visualization type found in state')
4
+ let ariaLabel = ''
5
+
6
+ if (state.visualizationType) {
7
+ ariaLabel += `${state.visualizationType} chart`
8
+ }
9
+
10
+ if (state.title && state.visualizationType) {
11
+ ariaLabel += ` with the title: ${state.title}`
12
+ }
13
+
14
+ return ariaLabel
15
+ } catch (e) {
16
+ console.error('COVE: ', e.message) // eslint-disable-line
17
+ return 'Data visualization container'
18
+ }
19
+ }