@cdc/chart 4.24.2 → 4.24.4

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 (60) hide show
  1. package/dist/cdcchart.js +47933 -36918
  2. package/examples/chart-regression-1.json +378 -0
  3. package/examples/chart-regression-2.json +2360 -0
  4. package/examples/feature/filters/url-filter.json +1076 -0
  5. package/examples/feature/line/line-chart.json +362 -37
  6. package/examples/feature/regions/index.json +50 -4
  7. package/examples/feature/sankey/sankey-example-data.json +1364 -0
  8. package/examples/feature/sankey/sankey_chart_data.csv +20 -0
  9. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-stacked.json +306 -19
  10. package/examples/region-issue.json +2065 -0
  11. package/examples/sparkline.json +868 -0
  12. package/examples/test.json +5409 -0
  13. package/index.html +130 -123
  14. package/package.json +4 -2
  15. package/src/CdcChart.tsx +178 -94
  16. package/src/_stories/ChartEditor.stories.tsx +14 -3
  17. package/src/_stories/_mock/url_filter.json +1076 -0
  18. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +2 -1
  19. package/src/components/AreaChart/components/AreaChart.jsx +2 -1
  20. package/src/components/BarChart/components/BarChart.Horizontal.tsx +46 -63
  21. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +36 -56
  22. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +32 -39
  23. package/src/components/BarChart/components/BarChart.Vertical.tsx +44 -59
  24. package/src/components/BoxPlot/BoxPlot.jsx +2 -1
  25. package/src/components/DeviationBar.jsx +3 -3
  26. package/src/components/EditorPanel/EditorPanel.tsx +1684 -1564
  27. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +1 -1
  28. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +107 -0
  29. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +48 -4
  30. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +41 -0
  31. package/src/components/EditorPanel/components/Panels/index.tsx +9 -7
  32. package/src/components/EditorPanel/components/panels.scss +11 -0
  33. package/src/components/EditorPanel/editor-panel.scss +0 -724
  34. package/src/components/EditorPanel/useEditorPermissions.js +40 -14
  35. package/src/components/Legend/Legend.Component.tsx +43 -63
  36. package/src/components/Legend/Legend.tsx +8 -4
  37. package/src/components/LineChart/LineChartProps.ts +1 -0
  38. package/src/components/LineChart/helpers.ts +2 -2
  39. package/src/components/LineChart/index.tsx +7 -7
  40. package/src/components/LinearChart.jsx +11 -31
  41. package/src/components/PairedBarChart.jsx +6 -10
  42. package/src/components/PieChart/PieChart.tsx +3 -3
  43. package/src/components/Regions/components/Regions.tsx +120 -78
  44. package/src/components/Sankey/index.tsx +434 -0
  45. package/src/components/Sankey/sankey.scss +153 -0
  46. package/src/components/Sankey/types/index.ts +16 -0
  47. package/src/components/ScatterPlot/ScatterPlot.jsx +1 -0
  48. package/src/components/Sparkline/{SparkLine.jsx → components/SparkLine.tsx} +14 -30
  49. package/src/components/Sparkline/index.scss +3 -0
  50. package/src/components/Sparkline/index.tsx +1 -1
  51. package/src/components/ZoomBrush.tsx +2 -1
  52. package/src/data/initial-state.js +46 -2
  53. package/src/helpers/computeMarginBottom.ts +2 -1
  54. package/src/helpers/tests/computeMarginBottom.test.ts +2 -1
  55. package/src/hooks/useBarChart.js +5 -2
  56. package/src/hooks/useScales.ts +47 -18
  57. package/src/hooks/useTooltip.tsx +9 -8
  58. package/src/scss/main.scss +33 -29
  59. package/src/types/ChartConfig.ts +32 -14
  60. package/src/types/ChartContext.ts +7 -0
@@ -14,18 +14,19 @@ export const useEditorPermissions = () => {
14
14
  'Combo',
15
15
  'Deviation Bar',
16
16
  'Forecasting',
17
- 'Forest Plot',
17
+ // 'Forest Plot',
18
18
  'Line',
19
19
  'Paired Bar',
20
20
  'Pie',
21
21
  'Scatter Plot',
22
- 'Spark Line'
22
+ 'Spark Line',
23
+ 'Sankey'
23
24
  ]
24
25
 
25
26
  const headerColors = ['theme-blue', 'theme-purple', 'theme-brown', 'theme-teal', 'theme-pink', 'theme-orange', 'theme-slate', 'theme-indigo', 'theme-cyan', 'theme-green', 'theme-amber']
26
27
 
27
28
  const visSupportsDateCategoryAxis = () => {
28
- const disabledCharts = ['Forest Plot']
29
+ const disabledCharts = ['Forest Plot', 'Sankey']
29
30
  if (disabledCharts.includes(visualizationType)) return false
30
31
  return true
31
32
  }
@@ -43,13 +44,13 @@ export const useEditorPermissions = () => {
43
44
  }
44
45
 
45
46
  const visHasLabelOnData = () => {
46
- const disabledCharts = ['Area Chart', 'Box Plot', 'Pie', 'Scatter Plot', 'Forest Plot', 'Spark Line']
47
+ const disabledCharts = ['Area Chart', 'Box Plot', 'Pie', 'Scatter Plot', 'Forest Plot', 'Spark Line', 'Sankey']
47
48
  if (disabledCharts.includes(visualizationType)) return false
48
49
  return true
49
50
  }
50
51
 
51
52
  const visCanAnimate = () => {
52
- const disabledCharts = ['Area Chart', 'Scatter Plot', 'Box Plot', 'Forest Plot', 'Spark Line']
53
+ const disabledCharts = ['Area Chart', 'Scatter Plot', 'Box Plot', 'Forest Plot', 'Spark Line', 'Sankey']
53
54
  if (disabledCharts.includes(visualizationType)) return false
54
55
  return true
55
56
  }
@@ -62,6 +63,8 @@ export const useEditorPermissions = () => {
62
63
  return false
63
64
  case 'Spark Line':
64
65
  return false
66
+ case 'Sankey':
67
+ return false
65
68
  default:
66
69
  return true
67
70
  }
@@ -107,6 +110,8 @@ export const useEditorPermissions = () => {
107
110
 
108
111
  const visHasDataCutoff = () => {
109
112
  switch (visualizationType) {
113
+ case 'Sankey':
114
+ return false
110
115
  case 'Forest Plot':
111
116
  return false
112
117
  case 'Box Plot':
@@ -120,8 +125,13 @@ export const useEditorPermissions = () => {
120
125
  }
121
126
  }
122
127
 
128
+ const visHasSelectableLegendValues = !['Box Plot', 'Forest Plot', 'Spark Line'].includes(visualizationType)
129
+ const visHasLegendAxisAlign = () => {
130
+ return visualizationType === 'Bar' && visualizationSubType === 'stacked' && config.legend.behavior === 'isolate'
131
+ }
132
+
123
133
  const visSupportsTooltipOpacity = () => {
124
- const disabledCharts = ['Spark Line']
134
+ const disabledCharts = ['Spark Line', 'Sankey']
125
135
  if (disabledCharts.includes(visualizationType)) return false
126
136
  return true
127
137
  }
@@ -133,19 +143,19 @@ export const useEditorPermissions = () => {
133
143
  }
134
144
 
135
145
  const visSupportsSequentialPallete = () => {
136
- const disabledCharts = ['Paired Bar', 'Deviation Bar', 'Forest Plot', 'Forecasting']
146
+ const disabledCharts = ['Paired Bar', 'Deviation Bar', 'Forest Plot', 'Forecasting', 'Sankey']
137
147
  if (disabledCharts.includes(visualizationType)) return false
138
148
  return true
139
149
  }
140
150
 
141
151
  const visSupportsNonSequentialPallete = () => {
142
- const disabledCharts = ['Paired Bar', 'Deviation Bar', 'Forest Plot', 'Forecasting']
152
+ const disabledCharts = ['Paired Bar', 'Deviation Bar', 'Forest Plot', 'Forecasting', 'Sankey']
143
153
  if (disabledCharts.includes(visualizationType)) return false
144
154
  return true
145
155
  }
146
156
 
147
157
  const visSupportsReverseColorPalette = () => {
148
- const disabledCharts = ['Forest Plot', 'Paired Bar', 'Deviation Bar']
158
+ const disabledCharts = ['Forest Plot', 'Paired Bar', 'Deviation Bar', 'Sankey']
149
159
  if (disabledCharts.includes(visualizationType)) return false
150
160
  return true
151
161
  }
@@ -187,7 +197,7 @@ export const useEditorPermissions = () => {
187
197
  }
188
198
 
189
199
  const visSupportsRegions = () => {
190
- const disabledCharts = ['Forest Plot', 'Pie', 'Paired Bar', 'Spark Line']
200
+ const disabledCharts = ['Forest Plot', 'Pie', 'Paired Bar', 'Spark Line', 'Sankey']
191
201
  if (disabledCharts.includes(visualizationType)) return false
192
202
  return true
193
203
  }
@@ -205,7 +215,7 @@ export const useEditorPermissions = () => {
205
215
  }
206
216
 
207
217
  const visSupportsFilters = () => {
208
- const disabledCharts = ['Forest Plot']
218
+ const disabledCharts = ['Forest Plot', 'Sankey']
209
219
  if (disabledCharts.includes(visualizationType)) return false
210
220
  return true
211
221
  }
@@ -258,7 +268,7 @@ export const useEditorPermissions = () => {
258
268
  }
259
269
 
260
270
  const visSupportsLeftValueAxis = () => {
261
- const disabledCharts = ['Spark Line']
271
+ const disabledCharts = ['Spark Line', 'Sankey']
262
272
  if (disabledCharts.includes(visualizationType)) return false
263
273
  return true
264
274
  }
@@ -270,13 +280,13 @@ export const useEditorPermissions = () => {
270
280
  }
271
281
 
272
282
  const visSupportsDateCategoryHeight = () => {
273
- const disabledCharts = ['Spark Line']
283
+ const disabledCharts = ['Spark Line', 'Sankey']
274
284
  if (disabledCharts.includes(visualizationType)) return false
275
285
  return true
276
286
  }
277
287
 
278
288
  const visSupportsDateCategoryAxisPadding = () => {
279
- return config.xAxis.type === 'date' && config.xAxis.sortDates
289
+ return config.xAxis.type === 'date-time'
280
290
  }
281
291
 
282
292
  const visSupportsReactTooltip = () => {
@@ -285,6 +295,19 @@ export const useEditorPermissions = () => {
285
295
  }
286
296
  }
287
297
 
298
+ const visSupportsPreliminaryData = () => {
299
+ // check if Line added in Combo
300
+ const lineExist = config?.series.some(item => ['Line', 'dashed-sm', 'dashed-md', 'dashed-lg'].includes(item?.type))
301
+ if (visualizationType === 'Line') {
302
+ return true
303
+ }
304
+
305
+ if (visualizationType === 'Combo' && lineExist) {
306
+ return true
307
+ }
308
+ return false
309
+ }
310
+
288
311
  return {
289
312
  enabledChartTypes,
290
313
  headerColors,
@@ -295,6 +318,7 @@ export const useEditorPermissions = () => {
295
318
  visHasLabelOnData,
296
319
  visHasDataSuppression,
297
320
  visHasLegend,
321
+ visHasLegendAxisAlign,
298
322
  visHasBrushChart,
299
323
  visHasNumbersOnBars,
300
324
  visSupportsBarSpace,
@@ -312,6 +336,7 @@ export const useEditorPermissions = () => {
312
336
  visSupportsFootnotes,
313
337
  visSupportsLeftValueAxis,
314
338
  visSupportsNonSequentialPallete,
339
+ visSupportsPreliminaryData,
315
340
  visSupportsRankByValue,
316
341
  visSupportsRegions,
317
342
  visSupportsResponsiveTicks,
@@ -319,6 +344,7 @@ export const useEditorPermissions = () => {
319
344
  visSupportsSequentialPallete,
320
345
  visSupportsSuperTitle,
321
346
  visSupportsTooltipLines,
347
+ visHasSelectableLegendValues,
322
348
  visSupportsTooltipOpacity,
323
349
  visSupportsValueAxisGridLines,
324
350
  visSupportsValueAxisLabels,
@@ -1,16 +1,15 @@
1
1
  import parse from 'html-react-parser'
2
2
  import { LegendOrdinal, LegendItem, LegendLabel } from '@visx/legend'
3
3
  import LegendCircle from '@cdc/core/components/LegendCircle'
4
-
4
+ import Button from '@cdc/core/components/elements/Button'
5
5
  import useLegendClasses from '../../hooks/useLegendClasses'
6
6
  import { useHighlightedBars } from '../../hooks/useHighlightedBars'
7
7
  import { handleLineType } from '../../helpers/handleLineType'
8
8
  import { Line } from '@visx/shape'
9
- import { scaleOrdinal } from '@visx/scale'
10
9
  import { Label } from '../../types/Label'
11
10
  import { ChartConfig } from '../../types/ChartConfig'
12
11
  import { ColorScale } from '../../types/ChartContext'
13
- import { Group } from '@visx/group'
12
+ import { forwardRef } from 'react'
14
13
 
15
14
  interface LegendProps {
16
15
  config: ChartConfig
@@ -18,42 +17,17 @@ interface LegendProps {
18
17
  seriesHighlight: string[]
19
18
  highlight: Function
20
19
  highlightReset: Function
21
- currentViewport: string
20
+ currentViewport: 'lg' | 'md' | 'sm' | 'xs' | 'xxs'
22
21
  formatLabels: (labels: Label[]) => Label[]
22
+ ref: React.Ref<() => void>
23
+ skipId: string
23
24
  }
24
25
 
25
26
  /* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
26
- const Legend: React.FC<LegendProps> = ({ config, colorScale, seriesHighlight, highlight, highlightReset, currentViewport, formatLabels }) => {
27
+ const Legend: React.FC<LegendProps> = forwardRef(({ config, colorScale, seriesHighlight, highlight, highlightReset, currentViewport, formatLabels, skipId = 'legend' }, ref) => {
27
28
  const { innerClasses, containerClasses } = useLegendClasses(config)
28
29
  const { runtime, orientation, legend } = config
29
30
  if (!legend) return null
30
- // create fn to reverse labels while legend is Bottom. Legend-right , legend-left works by default.
31
- const displayScale = scaleOrdinal({
32
- domain: config.suppressedData?.map(d => d.label),
33
- range: ['none'],
34
- unknown: 'block'
35
- })
36
-
37
- const renderDashes = style => {
38
- const dashCount = style === 'Dashed Small' ? 3 : 2
39
- const dashClass = `dashes ${style.toLowerCase().replace(' ', '-')}`
40
-
41
- return (
42
- <div className={dashClass}>
43
- {Array.from({ length: dashCount }, (_, i) => (
44
- <span key={i}>-</span>
45
- ))}
46
- </div>
47
- )
48
- }
49
- const renderDashesOrCircle = style => {
50
- if (['Dashed Small', 'Dashed Medium', 'Dashed Large'].includes(style)) {
51
- return renderDashes(style)
52
- } else if (style === 'Open Circles') {
53
- return <div className='dashes open-circles'></div>
54
- }
55
- }
56
-
57
31
  const isBottomOrSmallViewport = legend.position === 'bottom' || ['sm', 'xs', 'xxs'].includes(currentViewport)
58
32
 
59
33
  const legendClasses = {
@@ -64,11 +38,13 @@ const Legend: React.FC<LegendProps> = ({ config, colorScale, seriesHighlight, hi
64
38
  const { HighLightedBarUtils } = useHighlightedBars(config)
65
39
 
66
40
  let highLightedLegendItems = HighLightedBarUtils.findDuplicates(config.highlightedBarValues)
41
+ const fontSize = ['sm', 'xs', 'xxs'].includes(currentViewport) ? { fontSize: '11px' } : null
67
42
 
68
43
  return (
69
- <aside style={legendClasses} id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
70
- {legend.label && <h2>{parse(legend.label)}</h2>}
71
- {legend.description && <p>{parse(legend.description)}</p>}
44
+ <aside ref={ref} style={legendClasses} id={skipId || 'legend'} className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
45
+ {legend.label && <h3>{parse(legend.label)}</h3>}
46
+ {legend.description && <p style={fontSize}>{parse(legend.description)}</p>}
47
+
72
48
  <LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
73
49
  {labels => {
74
50
  return (
@@ -101,27 +77,31 @@ const Legend: React.FC<LegendProps> = ({ config, colorScale, seriesHighlight, hi
101
77
  className={className.join(' ')}
102
78
  tabIndex={0}
103
79
  key={`legend-quantile-${i}`}
104
- onKeyPress={e => {
80
+ onKeyDown={e => {
105
81
  if (e.key === 'Enter') {
82
+ e.preventDefault()
106
83
  highlight(label)
107
84
  }
108
85
  }}
109
- onClick={() => {
86
+ onClick={e => {
87
+ e.preventDefault()
110
88
  highlight(label)
111
89
  }}
90
+ role='button'
112
91
  >
113
- {config.visualizationType === 'Line' && config.legend.lineMode ? (
114
- <svg width={40} height={20}>
115
- <Line from={{ x: 10, y: 10 }} to={{ x: 40, y: 10 }} stroke={label.value} strokeWidth={2} strokeDasharray={handleLineType(config.series[i]?.type ? config.series[i]?.type : '')} />
116
- </svg>
117
- ) : (
118
- <div style={{ display: 'flex', flexDirection: 'column' }}>
119
- <LegendCircle margin='0' fill={label.value} display={displayScale(label.datum)} />
120
- <div style={{ marginTop: '2px', marginRight: '6px' }}>{label.icon}</div>
121
- </div>
122
- )}
123
-
124
- <LegendLabel align='left' margin='0 0 0 4px'>
92
+ <div>
93
+ {config.visualizationType === 'Line' && config.legend.lineMode ? (
94
+ <svg width={40} height={20}>
95
+ <Line from={{ x: 10, y: 10 }} to={{ x: 40, y: 10 }} stroke={label.value} strokeWidth={2} strokeDasharray={handleLineType(config.series[i]?.type ? config.series[i]?.type : '')} />
96
+ </svg>
97
+ ) : (
98
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
99
+ <LegendCircle viewport={currentViewport} margin='0' fill={label.value} display={true} />
100
+ </div>
101
+ )}
102
+ </div>
103
+
104
+ <LegendLabel style={fontSize} align='left' margin='0 0 0 4px'>
125
105
  {label.text}
126
106
  </LegendLabel>
127
107
  </LegendItem>
@@ -142,12 +122,14 @@ const Legend: React.FC<LegendProps> = ({ config, colorScale, seriesHighlight, hi
142
122
  className={className}
143
123
  tabIndex={0}
144
124
  key={`legend-quantile-${i}`}
145
- onKeyPress={e => {
125
+ onKeyDown={e => {
146
126
  if (e.key === 'Enter') {
127
+ e.preventDefault()
147
128
  highlight(bar.legendLabel)
148
129
  }
149
130
  }}
150
- onClick={() => {
131
+ onClick={e => {
132
+ e.preventDefault()
151
133
  highlight(bar.legendLabel)
152
134
  }}
153
135
  >
@@ -158,15 +140,10 @@ const Legend: React.FC<LegendProps> = ({ config, colorScale, seriesHighlight, hi
158
140
  </LegendItem>
159
141
  )
160
142
  })}
161
- {seriesHighlight.length > 0 && (
162
- <button className={`legend-reset ${config.theme}`} onClick={labels => highlightReset(labels)} tabIndex={0}>
163
- Reset
164
- </button>
165
- )}
166
143
  </div>
167
144
 
168
145
  <>
169
- {config?.preliminaryData?.some(pd => pd.label) && config.visualizationType === 'Line' && (
146
+ {config?.preliminaryData?.some(pd => pd.label) && ['Line', 'Combo'].includes(config.visualizationType) && (
170
147
  <>
171
148
  <hr></hr>
172
149
  <div className={config.legend.singleRow && isBottomOrSmallViewport ? 'legend-container__inner bottom single-row' : ''}>
@@ -174,11 +151,9 @@ const Legend: React.FC<LegendProps> = ({ config, colorScale, seriesHighlight, hi
174
151
  return (
175
152
  <>
176
153
  {pd.label && (
177
- <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
178
- <svg style={{ width: '50px' }} key={index} height={'23px'}>
179
- {pd.style.includes('Dashed') ? <Line from={{ x: 10, y: 10 }} to={{ x: 40, y: 10 }} stroke={'#000'} strokeWidth={2} strokeDasharray={handleLineType(pd.style)} /> : <circle r={6} strokeWidth={2} stroke={'#000'} cx={22} cy={10} fill='transparent' />}
180
- </svg>
181
- <span style={{}}> {pd.label}</span>
154
+ <div key={index} className='legend-preliminary'>
155
+ <svg>{pd.style.includes('Dashed') ? <Line from={{ x: 10, y: 10 }} to={{ x: 40, y: 10 }} stroke={'#000'} strokeWidth={2} strokeDasharray={handleLineType(pd.style)} /> : <circle r={6} strokeWidth={2} stroke={'#000'} cx={22} cy={10} fill='transparent' />}</svg>
156
+ <span> {pd.label}</span>
182
157
  </div>
183
158
  )}
184
159
  </>
@@ -192,8 +167,13 @@ const Legend: React.FC<LegendProps> = ({ config, colorScale, seriesHighlight, hi
192
167
  )
193
168
  }}
194
169
  </LegendOrdinal>
170
+ {seriesHighlight.length > 0 && (
171
+ <Button onClick={labels => highlightReset(labels)} style={{ marginTop: '1rem' }}>
172
+ Reset
173
+ </Button>
174
+ )}
195
175
  </aside>
196
176
  )
197
- }
177
+ })
198
178
 
199
179
  export default Legend
@@ -1,10 +1,10 @@
1
- import { useContext } from 'react'
1
+ import { useContext, forwardRef } from 'react'
2
2
  import ConfigContext from '../../ConfigContext'
3
3
  import LegendComponent from './Legend.Component'
4
4
  import { createFormatLabels } from './helpers/createFormatLabels'
5
5
 
6
6
  /* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
7
- const Legend = () => {
7
+ const Legend = forwardRef((props, ref) => {
8
8
  // prettier-ignore
9
9
  const {
10
10
  config,
@@ -22,7 +22,11 @@ const Legend = () => {
22
22
 
23
23
  const createLegendLabels = createFormatLabels(config, tableData, data, colorScale)
24
24
 
25
- return !['Box Plot', 'Pie'].includes(config.visualizationType) && <LegendComponent config={config} colorScale={colorScale} seriesHighlight={seriesHighlight} highlight={highlight} highlightReset={highlightReset} currentViewport={currentViewport} formatLabels={createLegendLabels} />
26
- }
25
+ return (
26
+ !['Box Plot', 'Pie'].includes(config.visualizationType) && (
27
+ <LegendComponent ref={ref} skipId={props.skipId || 'legend'} config={config} colorScale={colorScale} seriesHighlight={seriesHighlight} highlight={highlight} highlightReset={highlightReset} currentViewport={currentViewport} formatLabels={createLegendLabels} />
28
+ )
29
+ )
30
+ })
27
31
 
28
32
  export default Legend
@@ -22,6 +22,7 @@ export interface PreliminaryDataItem {
22
22
  column: string
23
23
  value: string
24
24
  seriesKey: string
25
+ label: string
25
26
  }
26
27
 
27
28
  export interface DataItem {
@@ -1,7 +1,7 @@
1
1
  import { type PreliminaryDataItem, DataItem, StyleProps, Style } from './LineChartProps'
2
2
 
3
3
  export const createStyles = (props: StyleProps): Style[] => {
4
- const { preliminaryData, data, stroke, handleLineType, lineType, seriesKey } = props
4
+ const { preliminaryData, data, stroke, strokeWidth, handleLineType, lineType, seriesKey } = props
5
5
 
6
6
  const validPreliminaryData: PreliminaryDataItem[] = preliminaryData.filter(pd => pd.seriesKey && pd.column && pd.value && pd.type && pd.style)
7
7
  const getMatchingPd = (point: DataItem): PreliminaryDataItem => validPreliminaryData.find(pd => pd.seriesKey === seriesKey && point[pd.column] === pd.value && pd.type === 'effect' && pd.style !== 'Open Circles')
@@ -9,7 +9,7 @@ export const createStyles = (props: StyleProps): Style[] => {
9
9
  let styles: Style[] = []
10
10
  const createStyle = (lineStyle): Style => ({
11
11
  stroke: stroke,
12
- strokeWidth: 2,
12
+ strokeWidth: strokeWidth,
13
13
  strokeDasharray: lineStyle
14
14
  })
15
15
 
@@ -52,7 +52,7 @@ const LineChart = (props: LineChartProps) => {
52
52
 
53
53
  return (
54
54
  <ErrorBoundary component='LineChart'>
55
- <Group left={config.runtime.yAxis.size ? parseInt(config.runtime.yAxis.size) : 66}>
55
+ <Group left={config.runtime.yAxis.size}>
56
56
  {' '}
57
57
  {/* left - expects a number not a string */}
58
58
  {(config.runtime.lineSeriesKeys || config.runtime.seriesKeys).map((seriesKey, index) => {
@@ -62,7 +62,7 @@ const LineChart = (props: LineChartProps) => {
62
62
  let displayArea = legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(seriesKey) !== -1
63
63
  const circleData = filterCircles(config.preliminaryData, rawData, seriesKey)
64
64
  // styles for preliminary Data items
65
- let styles = createStyles({ preliminaryData: config.preliminaryData, data: tableData, stroke: colorScale(config.runtime.seriesLabels[seriesKey]), handleLineType, lineType, seriesKey })
65
+ let styles = createStyles({ preliminaryData: config.preliminaryData, data: tableData, stroke: colorScale(config.runtime.seriesLabels[seriesKey]), strokeWidth: seriesData[0].weight || 2, handleLineType, lineType, seriesKey })
66
66
 
67
67
  let xPos = d => {
68
68
  return xScale(getXAxisData(d)) + (xScale.bandwidth ? xScale.bandwidth() / 2 : 0)
@@ -166,7 +166,7 @@ const LineChart = (props: LineChartProps) => {
166
166
  {config?.preliminaryData?.some(d => d.value && d.column) ? (
167
167
  <SplitLinePath
168
168
  curve={allCurves[seriesData[0].lineType]}
169
- segments={(config.xAxis.type === 'date' && config.xAxis.sortDates
169
+ segments={(config.xAxis.type === 'date-time'
170
170
  ? data.sort((d1, d2) => {
171
171
  let x1 = getXAxisData(d1)
172
172
  let x2 = getXAxisData(d2)
@@ -190,7 +190,7 @@ const LineChart = (props: LineChartProps) => {
190
190
  <LinePath
191
191
  curve={allCurves[seriesData[0].lineType]}
192
192
  data={
193
- config.xAxis.type === 'date' && config.xAxis.sortDates
193
+ config.xAxis.type === 'date-time'
194
194
  ? data.sort((d1, d2) => {
195
195
  let x1 = getXAxisData(d1)
196
196
  let x2 = getXAxisData(d2)
@@ -203,7 +203,7 @@ const LineChart = (props: LineChartProps) => {
203
203
  x={d => xPos(d)}
204
204
  y={d => (seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(Number(getYAxisData(d, seriesKey))))}
205
205
  stroke={colorScale(config.runtime.seriesLabels[seriesKey])}
206
- strokeWidth={2}
206
+ strokeWidth={seriesData[0].weight || 2}
207
207
  strokeOpacity={1}
208
208
  shapeRendering='geometricPrecision'
209
209
  strokeDasharray={lineType ? handleLineType(lineType) : 0}
@@ -216,14 +216,14 @@ const LineChart = (props: LineChartProps) => {
216
216
 
217
217
  {/* circles for preliminaryData data */}
218
218
  {circleData.map((d, i) => {
219
- return <circle key={i} cx={xPos(d)} cy={yScale(Number(getYAxisData(d, seriesKey)))} r={6} strokeWidth={2} stroke={colorScale ? colorScale(config.runtime.seriesLabels[seriesKey]) : '#000'} fill='#fff' />
219
+ return <circle key={i} cx={xPos(d)} cy={seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(Number(getYAxisData(d, seriesKey)))} r={6} strokeWidth={seriesData[0].weight || 2} stroke={colorScale ? colorScale(config.runtime.seriesLabels[seriesKey]) : '#000'} fill='#fff' />
220
220
  })}
221
221
 
222
222
  {/* ANIMATED LINE */}
223
223
  {config.animate && (
224
224
  <LinePath
225
225
  className='animation'
226
- curve={seriesData.lineType}
226
+ curve={allCurves[seriesData[0].lineType]}
227
227
  data={data}
228
228
  x={d => xPos(d)}
229
229
  y={d => (seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(Number(getYAxisData(d, seriesKey))))}
@@ -7,6 +7,7 @@ import { Line, Bar } from '@visx/shape'
7
7
  import { Text } from '@visx/text'
8
8
  import { Tooltip as ReactTooltip } from 'react-tooltip'
9
9
  import { useTooltip, TooltipWithBounds } from '@visx/tooltip'
10
+ import { isDateScale } from '@cdc/core/helpers/cove/date'
10
11
 
11
12
  // CDC Components
12
13
  import { AreaChart, AreaChartStacked } from './AreaChart'
@@ -27,7 +28,7 @@ import Regions from './Regions'
27
28
  import useMinMax from '../hooks/useMinMax'
28
29
  import useReduceData from '../hooks/useReduceData'
29
30
  import useRightAxis from '../hooks/useRightAxis'
30
- import useScales from '../hooks/useScales'
31
+ import useScales, { getTickValues } from '../hooks/useScales'
31
32
  import useTopAxis from '../hooks/useTopAxis'
32
33
  import { useTooltip as useCoveTooltip } from '../hooks/useTooltip'
33
34
  import { useEditorPermissions } from './EditorPanel/useEditorPermissions'
@@ -36,32 +37,10 @@ import { useEditorPermissions } from './EditorPanel/useEditorPermissions'
36
37
  import ZoomBrush from './ZoomBrush'
37
38
 
38
39
  const LinearChart = props => {
39
- const {
40
- isEditor,
41
- isDashboard,
42
- computeMarginBottom,
43
- transformedData: data,
44
- dimensions,
45
- config,
46
- parseDate,
47
- formatDate,
48
- currentViewport,
49
- formatNumber,
50
- handleChartAriaLabels,
51
- updateConfig,
52
- handleLineType,
53
- rawData,
54
- capitalize,
55
- setSharedFilter,
56
- setSharedFilterValue,
57
- getTextWidth,
58
- isDebug
59
- } = useContext(ConfigContext)
40
+ const { transformedData: data, dimensions, config, parseDate, formatDate, currentViewport, formatNumber, handleChartAriaLabels, updateConfig, handleLineType, getTextWidth } = useContext(ConfigContext)
60
41
  // todo: start destructuring this file for conciseness
61
42
  const { visualizationType, visualizationSubType, orientation, xAxis, yAxis, runtime, debugSvg } = config
62
43
 
63
- const getDate = d => new Date(d[config.xAxis.dataKey])
64
-
65
44
  // configure width
66
45
  let [width] = dimensions
67
46
  if (config && config.legend && !config.legend.hide && config.legend.position !== 'bottom' && ['lg', 'md'].includes(currentViewport)) {
@@ -99,7 +78,7 @@ const LinearChart = props => {
99
78
  })
100
79
 
101
80
  // getters & functions
102
- const getXAxisData = d => (config.runtime.xAxis.type === 'date' ? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime() : d[config.runtime.originalXAxis.dataKey])
81
+ const getXAxisData = d => (isDateScale(config.runtime.xAxis) ? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime() : d[config.runtime.originalXAxis.dataKey])
103
82
  const getYAxisData = (d, seriesKey) => d[seriesKey]
104
83
  const xAxisDataMapped = config.brush.active && config.brush.data?.length ? config.brush.data.map(d => getXAxisData(d)) : data.map(d => getXAxisData(d))
105
84
  const section = config.orientation === 'horizontal' || config.visualizationType === 'Forest Plot' ? 'yAxis' : 'xAxis'
@@ -122,7 +101,7 @@ const LinearChart = props => {
122
101
 
123
102
  if (config.data && !config.data[index] && visualizationType === 'Forest Plot') return
124
103
  if (config.visualizationType === 'Forest Plot') return config.data[index][config.xAxis.dataKey]
125
- if (runtime.yAxis.type === 'date') return formatDate(parseDate(tick))
104
+ if (isDateScale(runtime.yAxis)) return formatDate(parseDate(tick))
126
105
  if (orientation === 'vertical') return formatNumber(tick, 'left', shouldAbbreviate)
127
106
  return tick
128
107
  }
@@ -133,7 +112,7 @@ const LinearChart = props => {
133
112
  tick = 0
134
113
  }
135
114
 
136
- if (runtime.xAxis.type === 'date' && config.visualizationType !== 'Forest Plot') return formatDate(tick)
115
+ if (isDateScale(runtime.xAxis) && config.visualizationType !== 'Forest Plot') return formatDate(tick)
137
116
  if (orientation === 'horizontal' && config.visualizationType !== 'Forest Plot') return formatNumber(tick, 'left', shouldAbbreviate)
138
117
  if (config.xAxis.type === 'continuous' && config.visualizationType !== 'Forest Plot') return formatNumber(tick, 'bottom', shouldAbbreviate)
139
118
  if (config.visualizationType === 'Forest Plot') return formatNumber(tick, 'left', config.dataFormat.abbreviated, config.runtime.xAxis.prefix, config.runtime.xAxis.suffix, Number(config.dataFormat.roundTo))
@@ -388,7 +367,7 @@ const LinearChart = props => {
388
367
  {/* X axis */}
389
368
  {visualizationType !== 'Paired Bar' && visualizationType !== 'Spark Line' && (
390
369
  <AxisBottom
391
- top={runtime.horizontal && config.visualizationType !== 'Forest Plot' ? Number(heightHorizontal) + Number(config.xAxis.axisPadding) : config.visualizationType === 'Forest Plot' ? yMax + Number(config.xAxis.axisPadding) : yMax + Number(config.xAxis.axisPadding)}
370
+ top={runtime.horizontal && config.visualizationType !== 'Forest Plot' ? Number(heightHorizontal) + Number(config.xAxis.axisPadding) : config.visualizationType === 'Forest Plot' ? yMax + Number(config.xAxis.axisPadding) : yMax}
392
371
  left={config.visualizationType !== 'Forest Plot' ? Number(runtime.yAxis.size) : 0}
393
372
  label={runtime.xAxis.label}
394
373
  tickFormat={handleBottomTickFormatting}
@@ -396,6 +375,7 @@ const LinearChart = props => {
396
375
  stroke='#333'
397
376
  numTicks={countNumOfTicks('xAxis')}
398
377
  tickStroke='#333'
378
+ tickValues={config.xAxis.manual ? getTickValues(xAxisDataMapped, xScale, config.xAxis.type === 'date-time' ? countNumOfTicks('xAxis') : config.xAxis.manualStep) : undefined}
399
379
  >
400
380
  {props => {
401
381
  const axisCenter = config.visualizationType !== 'Forest Plot' ? (props.axisToPoint.x - props.axisFromPoint.x) / 2 : dimensions[0] / 2
@@ -502,7 +482,7 @@ const LinearChart = props => {
502
482
  )}
503
483
  {visualizationType === 'Paired Bar' && (
504
484
  <>
505
- <AxisBottom top={yMax} left={Number(runtime.yAxis.size)} label={runtime.xAxis.label} tickFormat={runtime.xAxis.type === 'date' ? formatDate : formatNumber} scale={g1xScale} stroke='#333' tickStroke='#333' numTicks={runtime.xAxis.numTicks || undefined}>
485
+ <AxisBottom top={yMax} left={Number(runtime.yAxis.size)} label={runtime.xAxis.label} tickFormat={isDateScale(runtime.xAxis) ? formatDate : formatNumber} scale={g1xScale} stroke='#333' tickStroke='#333' numTicks={runtime.xAxis.numTicks || undefined}>
506
486
  {props => {
507
487
  return (
508
488
  <Group className='bottom-axis'>
@@ -529,7 +509,7 @@ const LinearChart = props => {
529
509
  top={yMax}
530
510
  left={Number(runtime.yAxis.size)}
531
511
  label={runtime.xAxis.label}
532
- tickFormat={runtime.xAxis.type === 'date' ? formatDate : runtime.xAxis.dataKey !== 'Year' ? formatNumber : tick => tick}
512
+ tickFormat={isDateScale(runtime.xAxis) ? formatDate : runtime.xAxis.dataKey !== 'Year' ? formatNumber : tick => tick}
533
513
  scale={g2xScale}
534
514
  stroke='#333'
535
515
  tickStroke='#333'
@@ -708,7 +688,7 @@ const LinearChart = props => {
708
688
  newX = yAxis
709
689
  }
710
690
 
711
- let anchorPosition = newX.type === 'date' ? xScale(parseDate(anchor.value, false)) : xScale(anchor.value)
691
+ let anchorPosition = isDateScale(newX) ? xScale(parseDate(anchor.value, false)) : xScale(anchor.value)
712
692
 
713
693
  // have to move up
714
694
  // const padding = orientation === 'horizontal' ? Number(config.xAxis.size) : Number(config.yAxis.size)
@@ -5,7 +5,7 @@ import { scaleLinear } from '@visx/scale'
5
5
  import { Text } from '@visx/text'
6
6
 
7
7
  import ConfigContext from '../ConfigContext'
8
- import chroma from 'chroma-js'
8
+ import { getContrastColor } from '@cdc/core/helpers/cove/accessibility'
9
9
 
10
10
  const PairedBarChart = ({ width, height, originalWidth }) => {
11
11
  const { config, colorScale, transformedData: data, formatNumber, seriesHighlight, getTextWidth } = useContext(ConfigContext)
@@ -47,15 +47,8 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
47
47
  })
48
48
 
49
49
  // Set label color
50
- let labelColor = '#000000'
51
-
52
- if (groupOne.color && chroma.contrast(labelColor, groupOne.color) < 4.9) {
53
- groupOne.labelColor = '#FFFFFF'
54
- }
55
-
56
- if (groupTwo.color && chroma.contrast(labelColor, groupTwo.color) < 4.9) {
57
- groupTwo.labelColor = '#FFFFFF'
58
- }
50
+ groupOne.labelColor = groupOne.color ? getContrastColor('#000', groupOne.color) : '#000'
51
+ groupTwo.labelColor = groupTwo.color ? getContrastColor('#000', groupTwo.color) : '#000'
59
52
 
60
53
  const label = config.yAxis.label ? `${config.yAxis.label}: ` : ''
61
54
 
@@ -87,6 +80,7 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
87
80
  `}
88
81
  </style>
89
82
  <svg id='cdc-visualization__paired-bar-chart' width={originalWidth} height={height} viewBox={`0 0 ${width + Number(config.runtime.yAxis.size)} ${height}`} role='img' tabIndex={0}>
83
+ <title>{`Paired bar chart graphic with the title ${config.title ? config.title : 'No Title Found'}`}</title>
90
84
  <Group top={0} left={Number(config.xAxis.size)}>
91
85
  {data
92
86
  .filter(item => config.series[0].dataKey === groupOne.dataKey)
@@ -122,6 +116,7 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
122
116
  strokeWidth={borderWidth}
123
117
  opacity={transparentBar ? 0.5 : 1}
124
118
  display={displayBar ? 'block' : 'none'}
119
+ tabIndex={-1}
125
120
  />
126
121
  {config.yAxis.displayNumbersOnBar && displayBar && (
127
122
  <Text textAnchor={textFits ? 'start' : 'end'} dx={textFits ? 5 : -5} verticalAnchor='middle' x={halfWidth - barWidth} y={y + config.barHeight / 2} fill={textFits ? groupOne.labelColor : '#000'}>
@@ -173,6 +168,7 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
173
168
  stroke='#333'
174
169
  opacity={transparentBar ? 0.5 : 1}
175
170
  display={displayBar ? 'block' : 'none'}
171
+ tabIndex={-1}
176
172
  />
177
173
  {config.yAxis.displayNumbersOnBar && displayBar && (
178
174
  <Text textAnchor={isTextFits ? 'end' : 'start'} dx={isTextFits ? -5 : 5} verticalAnchor='middle' x={halfWidth + barWidth} y={y + config.barHeight / 2} fill={isTextFits ? groupTwo.labelColor : '#000'}>