@cdc/chart 4.25.8 → 4.25.10

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 (89) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/dist/cdcchart.js +37524 -35243
  3. package/examples/feature/__data__/planet-example-data.json +0 -30
  4. package/examples/grouped-bar-test.json +400 -0
  5. package/examples/private/d.json +382 -0
  6. package/examples/private/example-2.json +49784 -0
  7. package/examples/private/f2.json +1 -0
  8. package/examples/private/f4.json +1577 -0
  9. package/examples/private/forecast.json +1180 -0
  10. package/examples/private/lollipop.json +468 -0
  11. package/examples/private/new.json +48756 -0
  12. package/examples/private/pie-chart-legend.json +904 -0
  13. package/examples/suppressed_tooltip.json +480 -0
  14. package/index.html +10 -22
  15. package/package.json +25 -7
  16. package/src/CdcChart.tsx +1 -2
  17. package/src/CdcChartComponent.tsx +174 -32
  18. package/src/_stories/Chart.Anchors.stories.tsx +2 -2
  19. package/src/_stories/Chart.BoxPlot.stories.tsx +1 -1
  20. package/src/_stories/Chart.CI.stories.tsx +1 -1
  21. package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
  22. package/src/_stories/Chart.DynamicSeries.stories.tsx +2 -2
  23. package/src/_stories/Chart.Filters.stories.tsx +2 -2
  24. package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
  25. package/src/_stories/Chart.Patterns.stories.tsx +19 -0
  26. package/src/_stories/Chart.ScatterPlot.stories.tsx +1 -1
  27. package/src/_stories/Chart.stories.tsx +8 -5
  28. package/src/_stories/Chart.tooltip.stories.tsx +1 -1
  29. package/src/_stories/ChartAnnotation.stories.tsx +1 -1
  30. package/src/_stories/ChartAxisLabels.stories.tsx +2 -2
  31. package/src/_stories/ChartAxisTitles.stories.tsx +2 -2
  32. package/src/_stories/ChartEditor.stories.tsx +60 -60
  33. package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
  34. package/src/_stories/ChartLine.Symbols.stories.tsx +1 -1
  35. package/src/_stories/ChartPrefixSuffix.stories.tsx +2 -2
  36. package/src/_stories/_mock/stacked-pattern-test.json +520 -0
  37. package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -0
  38. package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
  39. package/src/components/BarChart/components/BarChart.Horizontal.tsx +159 -20
  40. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +138 -5
  41. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +215 -73
  42. package/src/components/BarChart/components/BarChart.Vertical.tsx +153 -21
  43. package/src/components/BarChart/helpers/index.ts +43 -4
  44. package/src/components/BarChart/helpers/lollipopColors.ts +27 -0
  45. package/src/components/BarChart/helpers/useBarChart.ts +25 -3
  46. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +2 -1
  47. package/src/components/DeviationBar.jsx +9 -6
  48. package/src/components/EditorPanel/EditorPanel.tsx +364 -39
  49. package/src/components/EditorPanel/EditorPanelContext.ts +3 -0
  50. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +414 -0
  51. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +28 -20
  52. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +115 -120
  53. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  54. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +0 -8
  55. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +49 -48
  56. package/src/components/Forecasting/Forecasting.tsx +36 -6
  57. package/src/components/ForestPlot/ForestPlot.tsx +11 -7
  58. package/src/components/ForestPlot/ForestPlotProps.ts +1 -1
  59. package/src/components/Legend/Legend.Component.tsx +106 -13
  60. package/src/components/Legend/helpers/createFormatLabels.tsx +230 -171
  61. package/src/components/LegendWrapper.tsx +1 -1
  62. package/src/components/LineChart/components/LineChart.Circle.tsx +2 -2
  63. package/src/components/LineChart/index.tsx +2 -2
  64. package/src/components/LinearChart.tsx +22 -5
  65. package/src/components/PairedBarChart.jsx +6 -4
  66. package/src/components/PieChart/PieChart.tsx +170 -54
  67. package/src/components/Sankey/components/Sankey.tsx +7 -1
  68. package/src/components/ScatterPlot/ScatterPlot.jsx +32 -4
  69. package/src/data/initial-state.js +315 -293
  70. package/src/helpers/buildForecastPaletteMappings.ts +112 -0
  71. package/src/helpers/buildForecastPaletteOptions.ts +109 -0
  72. package/src/helpers/getColorScale.ts +72 -8
  73. package/src/helpers/getNewRuntime.ts +1 -1
  74. package/src/helpers/getTransformedData.ts +1 -1
  75. package/src/hooks/useChartHoverAnalytics.tsx +44 -0
  76. package/src/hooks/useReduceData.ts +105 -70
  77. package/src/hooks/useTooltip.tsx +57 -15
  78. package/src/index.jsx +0 -2
  79. package/src/scss/main.scss +12 -0
  80. package/src/store/chart.reducer.ts +1 -1
  81. package/src/test/CdcChart.test.jsx +8 -3
  82. package/src/types/ChartConfig.ts +30 -6
  83. package/src/types/ChartContext.ts +1 -0
  84. package/vite.config.js +1 -1
  85. package/vitest.config.ts +16 -0
  86. package/src/coreStyles_chart.scss +0 -3
  87. package/src/helpers/configHelpers.ts +0 -28
  88. package/src/helpers/generateColorsArray.ts +0 -8
  89. package/src/hooks/useColorPalette.js +0 -76
@@ -1,4 +1,4 @@
1
- import { useContext } from 'react'
1
+ import { useContext, useRef } from 'react'
2
2
  // Local imports
3
3
  import parse from 'html-react-parser'
4
4
  import ConfigContext from '../ConfigContext'
@@ -8,10 +8,12 @@ import { isDateScale } from '@cdc/core/helpers/cove/date'
8
8
  // Third-party library imports
9
9
  import { localPoint } from '@visx/event'
10
10
  import { bisector } from 'd3-array'
11
- import _ from 'lodash'
11
+ import _, { get } from 'lodash'
12
12
  import { getHorizontalBarHeights } from '../components/BarChart/helpers/getBarHeights'
13
13
 
14
14
  export const useTooltip = props => {
15
+ // Track the last X-axis value to prevent duplicate analytics events
16
+ const lastAnalyticsXValue = useRef<string | number | null>(null)
15
17
  const {
16
18
  tableData: data,
17
19
  config,
@@ -23,7 +25,7 @@ export const useTooltip = props => {
23
25
  setSharedFilter,
24
26
  isDraggingAnnotation
25
27
  } = useContext<ChartContext>(ConfigContext)
26
- const { xScale, yScale, seriesScale, showTooltip, hideTooltip } = props
28
+ const { xScale, yScale, seriesScale, showTooltip, hideTooltip, interactionLabel = '' } = props
27
29
  const { xAxis, visualizationType, orientation, yAxis, runtime } = config
28
30
 
29
31
  const Y_AXIS_SIZE = Number(config.yAxis.size || 0)
@@ -84,6 +86,7 @@ export const useTooltip = props => {
84
86
 
85
87
  const resolvedScaleValues = getResolvedScaleValues([x, y])
86
88
  const singleSeriesValue = getYValueFromCoordinate(y, resolvedScaleValues)
89
+
87
90
  const columnsWithTooltips = []
88
91
  const tooltipItems = [] as any[][]
89
92
  for (const [colKey, column] of Object.entries(config.columns)) {
@@ -154,6 +157,10 @@ export const useTooltip = props => {
154
157
  return position
155
158
  }
156
159
  if (!config.tooltips.singleSeries || visualizationType === 'Line') {
160
+ // Collect analytics data for all series
161
+ const analyticsSeriesData: string[] = []
162
+ let xAxisValue: string | number | null = null
163
+
157
164
  tooltipItems.push(
158
165
  ...getIncludedTooltipSeries()
159
166
  ?.filter(seriesKey => {
@@ -168,6 +175,7 @@ export const useTooltip = props => {
168
175
  const seriesObjWithName = config.runtime.series.find(
169
176
  series => series.dataKey === seriesKey && series.name !== undefined
170
177
  )
178
+
171
179
  if (
172
180
  (value === null || value === undefined || value === '' || formattedValue === 'N/A') &&
173
181
  config.general.hideNullValue
@@ -181,6 +189,29 @@ export const useTooltip = props => {
181
189
  })
182
190
  )
183
191
 
192
+ // Publish a single analytics event with all tooltip data
193
+ // Only publish if the X-axis value has changed (different from last hover)
194
+ if (analyticsSeriesData.length > 0 && xAxisValue !== lastAnalyticsXValue.current) {
195
+ lastAnalyticsXValue.current = xAxisValue
196
+
197
+ // Extract series names for the series field
198
+ const seriesNames = analyticsSeriesData.map(item => item.split(':')[0].trim()).join(', ')
199
+
200
+ const specifics = xAxisValue
201
+ ? `series: ${seriesNames}, x: ${xAxisValue}, ${analyticsSeriesData.join(', ')}`
202
+ : `series: ${seriesNames}, ${analyticsSeriesData.join(', ')}`
203
+
204
+ publishAnalyticsEvent({
205
+ vizType: config?.type,
206
+ vizSubType: getVizSubType(config),
207
+ eventType: `chart_hover`,
208
+ eventAction: 'hover',
209
+ eventLabel: interactionLabel || 'unknown',
210
+ vizTitle: getVizTitle(config),
211
+ specifics
212
+ })
213
+ }
214
+
184
215
  const runtimeSeries =
185
216
  config.tooltips.singleSeries && visualizationType === 'Line'
186
217
  ? [_.find(config.runtime.series, d => d.dataKey === singleSeriesValue)]
@@ -239,6 +270,9 @@ export const useTooltip = props => {
239
270
  * @returns {void} - The tooltip information is hidden
240
271
  */
241
272
  const handleTooltipMouseOff = () => {
273
+ // Reset the analytics tracking when mouse leaves
274
+ lastAnalyticsXValue.current = null
275
+
242
276
  if (config.visualizationType === 'Area Chart') {
243
277
  setTimeout(() => {
244
278
  hideTooltip()
@@ -571,18 +605,26 @@ export const useTooltip = props => {
571
605
 
572
606
  // TOOLTIP BODY
573
607
  // handle suppressed tooltip items
574
- const { label, displayGray } =
575
- (config.visualizationSubType !== 'stacked' &&
576
- config.general.showSuppressedSymbol &&
577
- config.preliminaryData?.find(
578
- pd =>
579
- pd.label &&
580
- pd.type === 'suppression' &&
581
- pd.displayTooltip &&
582
- value === pd.value &&
583
- (!pd.column || key === pd.column)
584
- )) ||
585
- {}
608
+ const shouldCheckSuppression = config.visualizationSubType !== 'stacked'
609
+ let suppressionEntry
610
+ if (shouldCheckSuppression && config.preliminaryData) {
611
+ suppressionEntry = config.preliminaryData.find(
612
+ pd =>
613
+ pd.label &&
614
+ pd.type === 'suppression' &&
615
+ pd.displayTooltip &&
616
+ value === pd.value &&
617
+ (!pd.column || key === pd.column)
618
+ )
619
+ }
620
+
621
+ // Remove suppressed items entirely if not showing symbols
622
+ if (suppressionEntry && !config.general.showSuppressedSymbol) {
623
+ return null
624
+ }
625
+
626
+ const { label, displayGray } = suppressionEntry || {}
627
+
586
628
  let newValue = label || value
587
629
  const style = displayGray ? { color: '#8b8b8a' } : {}
588
630
 
package/src/index.jsx CHANGED
@@ -1,9 +1,7 @@
1
1
  import React from 'react'
2
2
  import ReactDOM from 'react-dom/client'
3
3
 
4
- import './coreStyles_chart.scss'
5
4
  import '@cdc/core/styles/cove-main.scss'
6
- import 'react-tooltip/dist/react-tooltip.css'
7
5
 
8
6
  import CdcChart from './CdcChart'
9
7
 
@@ -741,3 +741,15 @@
741
741
  .cdc-open-viz-module .debug {
742
742
  border: 2px solid red;
743
743
  }
744
+
745
+ // Pattern Legend Styles - using Bootstrap classes for layout, CSS for gaps only
746
+ .legend-patterns {
747
+ gap: var(--space-between-legend-item-rows);
748
+ margin-top: var(--space-between-legend-item-rows);
749
+
750
+ // When in row layout (top/bottom), override gap for proper row/column spacing
751
+ &.flex-row {
752
+ row-gap: var(--space-between-legend-item-rows);
753
+ column-gap: var(--space-between-legend-item-columns);
754
+ }
755
+ }
@@ -25,7 +25,7 @@ export const getInitialState = (configObj: ChartConfig): ChartState => {
25
25
  return {
26
26
  isLoading: true,
27
27
  config: defaults,
28
- stateData: _.cloneDeep(configObj?.data) || [],
28
+ stateData: configObj?.data || [],
29
29
  colorScale: null,
30
30
  excludedData: undefined,
31
31
  filteredData: undefined,
@@ -1,6 +1,11 @@
1
- // Placeholder test until we add them in.
1
+ import path from 'path'
2
+ import { testStandaloneBuild } from '@cdc/core/helpers/tests/testStandaloneBuild.ts'
3
+ import { describe, it, expect } from 'vitest'
4
+
2
5
  describe('Chart', () => {
3
- it('has a test.', async () => {
4
- return true
6
+ it('Can be built in isolation', async () => {
7
+ const pkgDir = path.join(__dirname, '..')
8
+ const result = testStandaloneBuild(pkgDir)
9
+ expect(result).toBe(true)
5
10
  })
6
11
  })
@@ -1,4 +1,5 @@
1
1
  import { Axis } from '@cdc/core/types/Axis'
2
+ import { MarkupConfig } from '@cdc/core/types/MarkupVariable'
2
3
  import { type ForestPlotConfigSettings } from './ForestPlot'
3
4
  import { type Column } from '@cdc/core/types/Column'
4
5
  import { type Series } from '@cdc/core/types/Series'
@@ -6,7 +7,17 @@ import { Runtime } from '@cdc/core/types/Runtime'
6
7
  import { FilterBehavior } from '@cdc/core/types/FilterBehavior'
7
8
  import { Table } from '@cdc/core/types/Table'
8
9
  import { BoxPlot } from '@cdc/core/types/BoxPlot'
9
- import { General } from '@cdc/core/types/General'
10
+ import { General as CoreGeneral } from '@cdc/core/types/General'
11
+
12
+ // Extend the core General type to include palette information for charts
13
+ export type General = CoreGeneral & {
14
+ palette?: {
15
+ name?: string
16
+ version?: string
17
+ isReversed?: boolean
18
+ customColors?: string[]
19
+ }
20
+ }
10
21
  import { type Link } from './../components/Sankey/types'
11
22
  import { type DataDescription } from '@cdc/core/types/DataDescription'
12
23
  import { type Legend as CoreLegend } from '@cdc/core/types/Legend'
@@ -96,6 +107,17 @@ export type Legend = CoreLegend & {
96
107
  }
97
108
  groupBy: string
98
109
  separators?: string
110
+ patterns?: {
111
+ [key: string]: {
112
+ label?: string
113
+ color?: string
114
+ shape?: string
115
+ dataKey?: string
116
+ dataValue?: string
117
+ contrastCheck?: boolean
118
+ patternSize?: number
119
+ }
120
+ }
99
121
  }
100
122
 
101
123
  type Visual = {
@@ -125,7 +147,6 @@ export type AllChartsConfig = {
125
147
  colorMatchLineSeriesLabels: boolean
126
148
  columns: ChartColumns
127
149
  confidenceKeys: ConfidenceInterval
128
- customColors: string[]
129
150
  data: Object[]
130
151
  dataUrl: string
131
152
  dataCutoff: number
@@ -225,12 +246,13 @@ export type AllChartsConfig = {
225
246
  default: string
226
247
  }
227
248
  }
228
- }
249
+ } & MarkupConfig
229
250
 
230
251
  export type ForestPlotConfig = {
231
252
  visualizationType: 'Forest Plot'
232
253
  forestPlot: ForestPlotConfigSettings
233
- } & AllChartsConfig
254
+ } & AllChartsConfig &
255
+ MarkupConfig
234
256
 
235
257
  export type LineChartConfig = {
236
258
  allowLineToBarGraph: boolean
@@ -238,7 +260,8 @@ export type LineChartConfig = {
238
260
  isolatedDotsSameSize: boolean
239
261
  lineDatapointStyle: 'hidden' | 'always show' | 'hover'
240
262
  visualizationType: 'Line'
241
- } & AllChartsConfig
263
+ } & AllChartsConfig &
264
+ MarkupConfig
242
265
 
243
266
  export type SankeyLink = {
244
267
  depth: number
@@ -279,6 +302,7 @@ export type SankeyChartConfig = {
279
302
  }
280
303
  ]
281
304
  visualizationType: 'Sankey'
282
- } & AllChartsConfig
305
+ } & AllChartsConfig &
306
+ MarkupConfig
283
307
 
284
308
  export type ChartConfig = SankeyChartConfig | LineChartConfig | ForestPlotConfig | AllChartsConfig
@@ -23,6 +23,7 @@ type SharedChartContext = {
23
23
  handleDragStateChange: (isDragging: any) => void
24
24
  highlight?: Function
25
25
  handleShowAll?: Function
26
+ interactionLabel?: string
26
27
  // whether or not the chart is viewed within the editor screen
27
28
  isEditor?: boolean
28
29
  // whether or not the user is dragging an annotation
package/vite.config.js CHANGED
@@ -1,4 +1,4 @@
1
- import GenerateViteConfig from '../../generateViteConfig.js'
1
+ import GenerateViteConfig from '@cdc/core/generateViteConfig.js'
2
2
  import { moduleName } from './package.json'
3
3
 
4
4
  export default GenerateViteConfig(moduleName)
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'jsdom',
6
+ globals: true,
7
+ setupFiles: ['../../vitest.setup.ts'],
8
+ exclude: [
9
+ '**/node_modules/**',
10
+ '**/dist/**',
11
+ '**/.storybook/**',
12
+ '**/*.stories.*',
13
+ '**/storybook-static/**'
14
+ ]
15
+ }
16
+ })
@@ -1,3 +0,0 @@
1
- @import '@cdc/core/styles/base';
2
- @import '@cdc/core/styles/heading-colors';
3
- @import '@cdc/core/styles/v2/themes/color-definitions';
@@ -1,28 +0,0 @@
1
- import { cloneDeep } from 'lodash'
2
- import { ChartConfig } from '../types/ChartConfig'
3
-
4
- /* editConfigKeys
5
- * Add edit or update config keys
6
- * keyUpdates: { path: string[], value: any }[]
7
- * path is the array of keys needed to reach the value to be updated
8
- * value is the new value to be set
9
- * if the key does not exist, it will be created
10
- */
11
- export function editConfigKeys(config: ChartConfig, keyUpdates: { path: string[]; value: any }[]): ChartConfig {
12
- const configDeepCopy = cloneDeep(config)
13
-
14
- const newConfig = keyUpdates.reduce((acc, { path, value }) => {
15
- const pathCopy = [...path]
16
- const lastKey = pathCopy.pop()
17
- const target = pathCopy.reduce((target, key) => {
18
- if (!target[key]) {
19
- target[key] = {}
20
- }
21
- return target[key]
22
- }, acc)
23
- target[lastKey] = value
24
- return acc
25
- }, configDeepCopy)
26
-
27
- return newConfig
28
- }
@@ -1,8 +0,0 @@
1
- import chroma from 'chroma-js'
2
-
3
- export const generateColorsArray = (color = '#000000', special = false) => {
4
- let colorObj = chroma(color)
5
- let hoverColor = special ? colorObj.brighten(0.5).hex() : colorObj.saturate(1.3).hex()
6
-
7
- return [color, hoverColor, colorObj.darken(0.3).hex()]
8
- }
@@ -1,76 +0,0 @@
1
- import { colorPalettesChart, twoColorPalette } from '@cdc/core/data/colorPalettes'
2
- import { useEffect } from 'react'
3
-
4
- export const useColorPalette = (config, updateConfig) => {
5
- let twoColorPalettes = []
6
- let sequential = []
7
- let nonSequential = []
8
- const accessibleColors = []
9
-
10
- // Get two color palettes if visualization type is Paired Bar
11
- if (config.visualizationType === 'Paired Bar' || config.visualizationType === 'Deviation Bar') {
12
- const isReversed = config.twoColor.isPaletteReversed
13
- twoColorPalettes = Object.keys(twoColorPalette).filter(name =>
14
- isReversed ? name.endsWith('reverse') : !name.endsWith('reverse')
15
- )
16
- } else {
17
- // Get sequential and non-sequential palettes for other visualization types
18
- const seqPalettes = []
19
- const nonSeqPalettes = []
20
-
21
- for (const paletteName in colorPalettesChart) {
22
- const isSequential = paletteName.startsWith('sequential')
23
- const isQualitative = paletteName.startsWith('qualitative')
24
- const colorblindsafe = paletteName.startsWith('colorblindsafe')
25
- const isReversed = paletteName.endsWith('reverse')
26
-
27
- if (isSequential && ((!config.isPaletteReversed && !isReversed) || (config.isPaletteReversed && isReversed))) {
28
- seqPalettes.push(paletteName)
29
- }
30
-
31
- if (isQualitative && ((!config.isPaletteReversed && !isReversed) || (config.isPaletteReversed && isReversed))) {
32
- nonSeqPalettes.push(paletteName)
33
- }
34
- if (colorblindsafe && ((!config.isPaletteReversed && !isReversed) || (config.isPaletteReversed && isReversed))) {
35
- accessibleColors.push(paletteName)
36
- }
37
- }
38
-
39
- sequential = seqPalettes
40
- nonSequential = nonSeqPalettes
41
- }
42
-
43
- // Update pairedBar.palette based on isPaletteReversed
44
- useEffect(() => {
45
- let palette = ''
46
-
47
- if (config.twoColor.isPaletteReversed && !config.twoColor.palette.endsWith('reverse')) {
48
- palette = config.twoColor.palette + 'reverse'
49
- }
50
-
51
- if (!config.twoColor.isPaletteReversed && config.twoColor.palette.endsWith('reverse')) {
52
- palette = config.twoColor.palette.slice(0, -7)
53
- }
54
-
55
- updateConfig({ ...config, twoColor: { ...config.twoColor, palette: palette } })
56
- }, [config.twoColor.isPaletteReversed])
57
-
58
- // Update palette based on isPaletteReversed
59
- useEffect(() => {
60
- let palette = ''
61
-
62
- if (config.isPaletteReversed && !config.palette.endsWith('reverse')) {
63
- palette = config.palette + 'reverse'
64
- }
65
-
66
- if (!config.isPaletteReversed && config.palette.endsWith('reverse')) {
67
- palette = config.palette.slice(0, -7)
68
- }
69
-
70
- updateConfig({ ...config, palette: palette })
71
- }, [config.isPaletteReversed])
72
-
73
- // Return all palettes
74
-
75
- return { twoColorPalettes, sequential, nonSequential, accessibleColors }
76
- }