@cdc/chart 4.24.10 → 4.24.12

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 (93) hide show
  1. package/dist/cdcchart.js +34651 -33978
  2. package/examples/feature/boxplot/boxplot-data.json +88 -22
  3. package/examples/feature/boxplot/boxplot.json +540 -16
  4. package/examples/feature/boxplot/testing.csv +7 -7
  5. package/examples/feature/sankey/sankey-example-data.json +126 -14
  6. package/examples/feature/tests-date-exclusions/date-exclusions-config.json +372 -12
  7. package/examples/private/DEV-8850-2.json +493 -0
  8. package/examples/private/DEV-9822.json +574 -0
  9. package/examples/private/DEV-9840.json +553 -0
  10. package/examples/private/DEV-9850-3.json +461 -0
  11. package/examples/private/chart.json +1084 -0
  12. package/examples/private/ci_formatted.json +202 -0
  13. package/examples/private/ci_issue.json +3016 -0
  14. package/examples/private/completed.json +634 -0
  15. package/examples/private/dem-data-long.csv +20 -0
  16. package/examples/private/dem-data-long.json +36 -0
  17. package/examples/private/demographic_data.csv +157 -0
  18. package/examples/private/demographic_data.json +2654 -0
  19. package/examples/private/demographic_dynamic.json +443 -0
  20. package/examples/private/demographic_standard.json +560 -0
  21. package/examples/private/test.json +493 -0
  22. package/index.html +10 -7
  23. package/package.json +2 -2
  24. package/src/CdcChart.tsx +132 -152
  25. package/src/_stories/Chart.Anchors.stories.tsx +31 -0
  26. package/src/_stories/Chart.CustomColors.stories.tsx +19 -0
  27. package/src/_stories/Chart.DynamicSeries.stories.tsx +34 -0
  28. package/src/_stories/Chart.Legend.Gradient.stories.tsx +42 -1
  29. package/src/_stories/Chart.stories.tsx +37 -6
  30. package/src/_stories/ChartAxisLabels.stories.tsx +4 -1
  31. package/src/_stories/ChartEditor.stories.tsx +27 -0
  32. package/src/_stories/ChartLine.Suppression.stories.tsx +25 -0
  33. package/src/_stories/ChartPrefixSuffix.stories.tsx +8 -0
  34. package/{examples/feature/area/area-chart-date-city-temperature.json → src/_stories/_mock/area_chart_stacked.json} +125 -27
  35. package/src/_stories/_mock/boxplot_multiseries.json +647 -0
  36. package/src/_stories/_mock/dynamic_series_bar_config.json +723 -0
  37. package/src/_stories/_mock/dynamic_series_config.json +979 -0
  38. package/src/_stories/_mock/line_chart_dynamic_ci.json +493 -0
  39. package/src/_stories/_mock/line_chart_non_dynamic_ci.json +522 -0
  40. package/{examples/feature/scatterplot/scatterplot.json → src/_stories/_mock/scatterplot_mock.json} +62 -92
  41. package/src/_stories/_mock/short_dates.json +288 -0
  42. package/src/_stories/_mock/suppression_mock.json +1549 -0
  43. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +15 -3
  44. package/src/components/Axis/Categorical.Axis.tsx +2 -2
  45. package/src/components/BarChart/components/BarChart.Horizontal.tsx +46 -37
  46. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +43 -9
  47. package/src/components/BarChart/components/BarChart.Vertical.tsx +53 -47
  48. package/src/components/BarChart/helpers/getBarData.ts +28 -0
  49. package/src/components/BarChart/helpers/index.ts +1 -2
  50. package/src/components/BarChart/helpers/tests/getBarData.test.ts +74 -0
  51. package/src/components/BoxPlot/BoxPlot.tsx +131 -0
  52. package/src/components/BoxPlot/helpers/index.ts +54 -0
  53. package/src/components/BrushChart.tsx +23 -26
  54. package/src/components/EditorPanel/EditorPanel.tsx +117 -139
  55. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +3 -3
  56. package/src/components/EditorPanel/components/Panels/Panel.BoxPlot.tsx +51 -6
  57. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +40 -9
  58. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
  59. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +122 -56
  60. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -2
  61. package/src/components/EditorPanel/useEditorPermissions.ts +20 -2
  62. package/src/components/Legend/Legend.Component.tsx +11 -12
  63. package/src/components/Legend/Legend.tsx +16 -16
  64. package/src/components/Legend/helpers/getLegendClasses.ts +59 -0
  65. package/src/components/Legend/helpers/index.ts +2 -1
  66. package/src/components/Legend/tests/getLegendClasses.test.ts +115 -0
  67. package/src/components/LineChart/components/LineChart.Circle.tsx +1 -1
  68. package/src/components/LineChart/helpers.ts +49 -43
  69. package/src/components/LineChart/index.tsx +135 -83
  70. package/src/components/LinearChart.tsx +187 -181
  71. package/src/components/PieChart/PieChart.tsx +7 -1
  72. package/src/components/Sankey/components/ColumnList.tsx +19 -0
  73. package/src/components/Sankey/components/Sankey.tsx +479 -0
  74. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +33 -0
  75. package/src/components/Sankey/index.tsx +1 -492
  76. package/src/components/Sankey/sankey.scss +22 -21
  77. package/src/components/Sankey/types/index.ts +1 -1
  78. package/src/components/Sankey/useSankeyAlert.tsx +60 -0
  79. package/src/components/ScatterPlot/ScatterPlot.jsx +20 -4
  80. package/src/data/initial-state.js +7 -12
  81. package/src/helpers/countNumOfTicks.ts +57 -0
  82. package/src/helpers/getQuartiles.ts +15 -18
  83. package/src/hooks/useMinMax.ts +44 -16
  84. package/src/hooks/useReduceData.ts +43 -10
  85. package/src/hooks/useScales.ts +90 -35
  86. package/src/hooks/useTooltip.tsx +59 -50
  87. package/src/scss/DataTable.scss +5 -0
  88. package/src/scss/main.scss +6 -20
  89. package/src/types/ChartConfig.ts +6 -19
  90. package/src/types/ChartContext.ts +4 -1
  91. package/src/types/ForestPlot.ts +8 -0
  92. package/src/components/BoxPlot/BoxPlot.jsx +0 -111
  93. package/src/hooks/useLegendClasses.ts +0 -72
@@ -1,6 +1,11 @@
1
1
  import { memo, useContext } from 'react'
2
2
  import { useEditorPermissions } from '../../useEditorPermissions.js'
3
- import { AccordionItem, AccordionItemHeading, AccordionItemPanel, AccordionItemButton } from 'react-accessible-accordion'
3
+ import {
4
+ AccordionItem,
5
+ AccordionItemHeading,
6
+ AccordionItemPanel,
7
+ AccordionItemButton
8
+ } from 'react-accessible-accordion'
4
9
  import { type ChartConfig } from '../../../../types/ChartConfig.js'
5
10
  import { TextField, Select } from '@cdc/core/components/EditorPanel/Inputs'
6
11
  import Tooltip from '@cdc/core/components/ui/Tooltip'
@@ -58,7 +63,7 @@ const RegionSettings = memo(({ config, updateConfig }: { config: ChartConfig; up
58
63
  <div className='edit-block' key={`region-${i}`}>
59
64
  <button
60
65
  type='button'
61
- className='remove-column'
66
+ className='btn btn-danger remove-column'
62
67
  onClick={event => {
63
68
  event.preventDefault()
64
69
  removeColumn(i)
@@ -68,8 +73,18 @@ const RegionSettings = memo(({ config, updateConfig }: { config: ChartConfig; up
68
73
  </button>
69
74
  <TextField value={label} label='Region Label' fieldName='label' i={i} updateField={updateField} />
70
75
  <div className='two-col-inputs'>
71
- <TextField value={color} label='Text Color' fieldName='color' updateField={(section, subsection, fieldName, value) => regionUpdate(fieldName, value, i)} />
72
- <TextField value={background} label='Background' fieldName='background' updateField={(section, subsection, fieldName, value) => regionUpdate(fieldName, value, i)} />
76
+ <TextField
77
+ value={color}
78
+ label='Text Color'
79
+ fieldName='color'
80
+ updateField={(section, subsection, fieldName, value) => regionUpdate(fieldName, value, i)}
81
+ />
82
+ <TextField
83
+ value={background}
84
+ label='Background'
85
+ fieldName='background'
86
+ updateField={(section, subsection, fieldName, value) => regionUpdate(fieldName, value, i)}
87
+ />
73
88
  </div>
74
89
 
75
90
  <Select
@@ -91,11 +106,17 @@ const RegionSettings = memo(({ config, updateConfig }: { config: ChartConfig; up
91
106
  options={fromOptions}
92
107
  />
93
108
 
94
- {(config.regions[i].fromType === 'Fixed' || config.regions[i].fromType === 'Previous Days' || !config.regions[i].fromType) && (
109
+ {(config.regions[i].fromType === 'Fixed' ||
110
+ config.regions[i].fromType === 'Previous Days' ||
111
+ !config.regions[i].fromType) && (
95
112
  <>
96
113
  <TextField
97
114
  value={from}
98
- label={config.regions[i].fromType === 'Fixed' || !config.regions[i]?.fromType ? 'From Value' : 'Previous Number of Days'}
115
+ label={
116
+ config.regions[i].fromType === 'Fixed' || !config.regions[i]?.fromType
117
+ ? 'From Value'
118
+ : 'Previous Number of Days'
119
+ }
99
120
  fieldName='from'
100
121
  updateField={(section, subsection, fieldName, value) => regionUpdate(fieldName, value, i)}
101
122
  tooltip={
@@ -104,7 +125,10 @@ const RegionSettings = memo(({ config, updateConfig }: { config: ChartConfig; up
104
125
  <Icon display='question' style={{ marginLeft: '0.5rem' }} />
105
126
  </Tooltip.Target>
106
127
  <Tooltip.Content>
107
- <p>When using categorical (linear scale) match the data set value. When using date (linear / date time scale) match the x-axis value.</p>
128
+ <p>
129
+ When using categorical (linear scale) match the data set value. When using date (linear / date
130
+ time scale) match the x-axis value.
131
+ </p>
108
132
  </Tooltip.Content>
109
133
  </Tooltip>
110
134
  }
@@ -131,13 +155,20 @@ const RegionSettings = memo(({ config, updateConfig }: { config: ChartConfig; up
131
155
  options={toOptions}
132
156
  />
133
157
 
134
- {(config.regions[i].toType === 'Fixed' || !config.regions[i].toType) && <TextField value={to} label='To Value' fieldName='to' updateField={(section, subsection, fieldName, value) => regionUpdate(fieldName, value, i)} />}
158
+ {(config.regions[i].toType === 'Fixed' || !config.regions[i].toType) && (
159
+ <TextField
160
+ value={to}
161
+ label='To Value'
162
+ fieldName='to'
163
+ updateField={(section, subsection, fieldName, value) => regionUpdate(fieldName, value, i)}
164
+ />
165
+ )}
135
166
  </div>
136
167
  ))}
137
168
  {!config.regions && <p style={{ textAlign: 'center' }}>There are currently no regions.</p>}
138
169
  <button
139
170
  type='button'
140
- className='btn full-width'
171
+ className='btn btn-primary full-width'
141
172
  onClick={e => {
142
173
  e.preventDefault()
143
174
  addColumn()
@@ -11,7 +11,7 @@ import {
11
11
  } from 'react-accessible-accordion'
12
12
  import EditorPanelContext, { type EditorPanelContext as EPContext } from '../../EditorPanelContext'
13
13
 
14
- const SankeySettings = () => {
14
+ const SankeySettings: React.FC<PanelProps> = props => {
15
15
  const { config, updateConfig } = useContext(ConfigContext)
16
16
  const data = config.data?.[0]
17
17
  const { updateField } = useContext<EPContext>(EditorPanelContext)
@@ -109,7 +109,7 @@ const SankeySettings = () => {
109
109
  onChange={e => updateStoryNode('segmentTextAfter', e.target.value, i)}
110
110
  />
111
111
  </label>
112
- <Button onClick={e => removeStoryNode(i)} className='btn' style={{ background: 'tomato' }}>
112
+ <Button onClick={e => removeStoryNode(i)} className='btn btn-danger full-width'>
113
113
  Remove Story Node
114
114
  </Button>
115
115
  </div>
@@ -117,7 +117,7 @@ const SankeySettings = () => {
117
117
  {data?.storyNodeText?.length < 3 && (
118
118
  <button
119
119
  type='button'
120
- className='btn full-width'
120
+ className='btn btn-primary full-width'
121
121
  onClick={e => {
122
122
  e.preventDefault()
123
123
  addStoryNode()
@@ -1,4 +1,4 @@
1
- import React, { useContext } from 'react'
1
+ import React, { useContext, useMemo } from 'react'
2
2
  import ConfigContext from '../../../../ConfigContext'
3
3
 
4
4
  // Core
@@ -7,10 +7,18 @@ import Check from '@cdc/core/assets/icon-check.svg'
7
7
  import { approvedCurveTypes } from '@cdc/core/helpers/lineChartHelpers'
8
8
  import { sequentialPalettes } from '@cdc/core/data/colorPalettes'
9
9
  import Icon from '@cdc/core/components/ui/Icon'
10
+ import { Select } from '@cdc/core/components/EditorPanel/Inputs'
10
11
 
11
12
  // Third Party
12
- import { Accordion, AccordionItem, AccordionItemHeading, AccordionItemPanel, AccordionItemButton } from 'react-accessible-accordion'
13
+ import {
14
+ Accordion,
15
+ AccordionItem,
16
+ AccordionItemHeading,
17
+ AccordionItemPanel,
18
+ AccordionItemButton
19
+ } from 'react-accessible-accordion'
13
20
  import { Draggable } from '@hello-pangea/dnd'
21
+ import Tooltip from '@cdc/core/components/ui/Tooltip'
14
22
 
15
23
  const SeriesContext = React.createContext({})
16
24
 
@@ -48,7 +56,11 @@ const SeriesWrapper = props => {
48
56
  updateConfig({ ...config, series })
49
57
  }
50
58
 
51
- return <SeriesContext.Provider value={{ updateSeries, supportedRightAxisTypes, getColumns, selectComponent }}>{props.children}</SeriesContext.Provider>
59
+ return (
60
+ <SeriesContext.Provider value={{ updateSeries, supportedRightAxisTypes, getColumns, selectComponent }}>
61
+ {props.children}
62
+ </SeriesContext.Provider>
63
+ )
52
64
  }
53
65
 
54
66
  const SeriesDropdownLineType = props => {
@@ -245,7 +257,9 @@ const SeriesDropdownForecastColor = props => {
245
257
  <InputSelect
246
258
  key={`${stage}--${stageIndex}`}
247
259
  initial='Select an option'
248
- value={config.series?.[index].stages?.[stageIndex].color ? config.series?.[index].stages?.[stageIndex].color : 'Select'}
260
+ value={
261
+ config.series?.[index].stages?.[stageIndex].color ? config.series?.[index].stages?.[stageIndex].color : 'Select'
262
+ }
249
263
  label={`${stage.key} Series Color`}
250
264
  onChange={event => {
251
265
  const copyOfSeries = [...config.series] // copy the entire series array
@@ -315,55 +329,33 @@ const SeriesDropdownConfidenceInterval = props => {
315
329
  <AccordionItemPanel>
316
330
  <div className='input-group'>
317
331
  <label htmlFor='showInTooltip'>Show In Tooltip</label>
318
- <div className={'cove-input__checkbox--small'} onClick={e => updateShowInTooltip(e, index, ciIndex)}>
319
- <div className={`cove-input__checkbox-box${'blue' ? ' custom-color' : ''}`} style={{ backgroundColor: '' }}>
332
+ <div
333
+ className={'cove-input__checkbox--small'}
334
+ onClick={e => updateShowInTooltip(e, index, ciIndex)}
335
+ >
336
+ <div
337
+ className={`cove-input__checkbox-box${'blue' ? ' custom-color' : ''}`}
338
+ style={{ backgroundColor: '' }}
339
+ >
320
340
  {showInTooltip && <Check className='' style={{ fill: '#025eaa' }} />}
321
341
  </div>
322
- <input className='cove-input--hidden' type='checkbox' name={'showInTooltip'} checked={showInTooltip ? showInTooltip : false} readOnly />
342
+ <input
343
+ className='cove-input--hidden'
344
+ type='checkbox'
345
+ name={'showInTooltip'}
346
+ checked={showInTooltip ? showInTooltip : false}
347
+ readOnly
348
+ />
323
349
  </div>
324
350
  </div>
325
351
 
326
- {/* <label>
327
- High Label
328
- <input
329
- type='text'
330
- key={`series-ci-high-label-${index}`}
331
- value={series.confidenceIntervals[index]?.highLabel ? series.confidenceIntervals[index]?.highLabel : ''}
332
- onChange={e => {
333
- const copiedConfidenceArray = [...config.series[index].confidenceIntervals]
334
- copiedConfidenceArray[ciIndex].highLabel = e.target.value
335
- const copyOfSeries = [...config.series] // copy the entire series array
336
- copyOfSeries[index] = { ...copyOfSeries[index], confidenceIntervals: copiedConfidenceArray }
337
- updateConfig({
338
- ...config,
339
- series: copyOfSeries
340
- })
341
- }}
342
- />
343
- </label> */}
344
-
345
- {/* <label>
346
- Low label
347
- <input
348
- type='text'
349
- key={`series-ci-high-label-${index}`}
350
- value={series.confidenceIntervals[index]?.lowLabel ? series.confidenceIntervals[index]?.lowLabel : ''}
351
- onChange={e => {
352
- const copiedConfidenceArray = [...config.series[index].confidenceIntervals]
353
- copiedConfidenceArray[ciIndex].lowLabel = e.target.value
354
- const copyOfSeries = [...config.series] // copy the entire series array
355
- copyOfSeries[index] = { ...copyOfSeries[index], confidenceIntervals: copiedConfidenceArray }
356
- updateConfig({
357
- ...config,
358
- series: copyOfSeries
359
- })
360
- }}
361
- />
362
- </label> */}
363
-
364
352
  <InputSelect
365
353
  initial='Select an option'
366
- value={config.series[index].confidenceIntervals[ciIndex].low ? config.series[index].confidenceIntervals[ciIndex].low : 'Select'}
354
+ value={
355
+ config.series[index].confidenceIntervals[ciIndex].low
356
+ ? config.series[index].confidenceIntervals[ciIndex].low
357
+ : 'Select'
358
+ }
367
359
  label='Low Confidence Interval'
368
360
  onChange={e => {
369
361
  const copiedConfidenceArray = [...config.series[index].confidenceIntervals]
@@ -379,7 +371,11 @@ const SeriesDropdownConfidenceInterval = props => {
379
371
  />
380
372
  <InputSelect
381
373
  initial='Select an option'
382
- value={config.series[index].confidenceIntervals[ciIndex].high ? config.series[index].confidenceIntervals[ciIndex].high : 'Select'}
374
+ value={
375
+ config.series[index].confidenceIntervals[ciIndex].high
376
+ ? config.series[index].confidenceIntervals[ciIndex].high
377
+ : 'Select'
378
+ }
383
379
  label='High Confidence Interval'
384
380
  onChange={e => {
385
381
  const copiedConfidenceArray = [...config.series[index].confidenceIntervals]
@@ -399,7 +395,7 @@ const SeriesDropdownConfidenceInterval = props => {
399
395
  })}
400
396
  </Accordion>
401
397
  <button
402
- className='btn full-width'
398
+ className='btn btn-primary full-width'
403
399
  onClick={e => {
404
400
  e.preventDefault()
405
401
  let copiedIndex = null
@@ -409,7 +405,10 @@ const SeriesDropdownConfidenceInterval = props => {
409
405
  copiedIndex = []
410
406
  }
411
407
  const copyOfSeries = [...config.series] // copy the entire series array
412
- copyOfSeries[index] = { ...copyOfSeries[index], confidenceIntervals: [...copiedIndex, { high: '', low: '' }] } // update the nested array
408
+ copyOfSeries[index] = {
409
+ ...copyOfSeries[index],
410
+ confidenceIntervals: [...copiedIndex, { high: '', low: '' }]
411
+ } // update the nested array
413
412
  updateConfig({
414
413
  ...config,
415
414
  series: copyOfSeries
@@ -468,7 +467,19 @@ const SeriesInputWeight = props => {
468
467
  const SeriesInputName = props => {
469
468
  const { series, index: i } = props
470
469
  const { config, updateConfig } = useContext(ConfigContext)
471
- const adjustableNameSeriesTypes = ['Bump Chart', 'Bar', 'Line', 'Area Chart', 'Combo', 'Deviation', 'Paired', 'Scatter', 'dashed-sm', 'dashed-md', 'dashed-lg']
470
+ const adjustableNameSeriesTypes = [
471
+ 'Bump Chart',
472
+ 'Bar',
473
+ 'Line',
474
+ 'Area Chart',
475
+ 'Combo',
476
+ 'Deviation',
477
+ 'Paired',
478
+ 'Scatter',
479
+ 'dashed-sm',
480
+ 'dashed-md',
481
+ 'dashed-lg'
482
+ ]
472
483
 
473
484
  if (!adjustableNameSeriesTypes.includes(series.type)) return
474
485
 
@@ -532,7 +543,13 @@ const SeriesDisplayInTooltip = props => {
532
543
  <div className={`cove-input__checkbox-box${'blue' ? ' custom-color' : ''}`} style={{ backgroundColor: '' }}>
533
544
  {series.tooltip && <Check className='' style={{ fill: '#025eaa' }} />}
534
545
  </div>
535
- <input className='cove-input--hidden' type='checkbox' name={`series-tooltip--${index}`} checked={series.tooltip ? series.tooltip : false} readOnly />
546
+ <input
547
+ className='cove-input--hidden'
548
+ type='checkbox'
549
+ name={`series-tooltip--${index}`}
550
+ checked={series.tooltip ? series.tooltip : false}
551
+ readOnly
552
+ />
536
553
  </div>
537
554
  </div>
538
555
  </>
@@ -591,17 +608,33 @@ const SeriesButtonRemove = props => {
591
608
 
592
609
  const SeriesItem = props => {
593
610
  const { config } = useContext(ConfigContext)
594
-
611
+ const { updateSeries, getColumns } = useContext(SeriesContext)
595
612
  const { series, getItemStyle, sortableItemStyles, chartsWithOptions, index: i } = props
596
-
613
+ const showDynamicCategory =
614
+ ['Bar', 'Line'].includes(config.visualizationType) &&
615
+ config.visualizationSubType !== 'Stacked' &&
616
+ !config.series.find(s => s.dynamicCategory && s.dataKey !== series.dataKey)
597
617
  return (
598
618
  <Draggable key={series.dataKey} draggableId={`draggableFilter-${series.dataKey}`} index={i}>
599
619
  {(provided, snapshot) => (
600
- <div key={i} className={snapshot.isDragging ? 'currently-dragging' : ''} style={getItemStyle(snapshot.isDragging, provided.draggableProps.style, sortableItemStyles)} ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
620
+ <div
621
+ key={i}
622
+ className={snapshot.isDragging ? 'currently-dragging' : ''}
623
+ style={getItemStyle(snapshot.isDragging, provided.draggableProps.style, sortableItemStyles)}
624
+ ref={provided.innerRef}
625
+ {...provided.draggableProps}
626
+ {...provided.dragHandleProps}
627
+ >
601
628
  <Accordion allowZeroExpanded>
602
629
  <AccordionItem className='series-item series-item--chart'>
603
630
  <AccordionItemHeading className='series-item__title'>
604
- <AccordionItemButton className={chartsWithOptions.includes(config.visualizationType) ? 'accordion__button' : 'accordion__button hide-arrow'}>
631
+ <AccordionItemButton
632
+ className={
633
+ chartsWithOptions.includes(config.visualizationType)
634
+ ? 'accordion__button'
635
+ : 'accordion__button hide-arrow'
636
+ }
637
+ >
605
638
  <Icon display='move' size={15} style={{ cursor: 'default' }} />
606
639
  {series.dataKey}
607
640
  <Series.Button.Remove series={series} index={i} />
@@ -610,6 +643,30 @@ const SeriesItem = props => {
610
643
  {chartsWithOptions.includes(config.visualizationType) && (
611
644
  <AccordionItemPanel>
612
645
  <Series.Input.Name series={series} index={i} />
646
+ {showDynamicCategory && (
647
+ <Select
648
+ label='Dynamic Category'
649
+ value={series.dynamicCategory}
650
+ options={['- Select - ', ...getColumns().filter(col => series.dataKey !== col)]}
651
+ updateField={(_section, _subsection, _fieldName, value) => {
652
+ if (value === '- Select -') value = ''
653
+ updateSeries(i, value, 'dynamicCategory')
654
+ }}
655
+ tooltip={
656
+ <Tooltip style={{ textTransform: 'none' }}>
657
+ <Tooltip.Target>
658
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
659
+ </Tooltip.Target>
660
+ <Tooltip.Content>
661
+ <p>
662
+ This field is Optional. If you have a dynamic data series you can select the category
663
+ field here. You can only add one dynamic category per visualization.
664
+ </p>
665
+ </Tooltip.Content>
666
+ </Tooltip>
667
+ }
668
+ />
669
+ )}
613
670
  <Series.Input.Weight series={series} index={i} />
614
671
  <Series.Dropdown.SeriesType series={series} index={i} />
615
672
  <Series.Dropdown.AxisPosition series={series} index={i} />
@@ -630,7 +687,16 @@ const SeriesItem = props => {
630
687
  const SeriesList = props => {
631
688
  const { series, getItemStyle, sortableItemStyles, chartsWithOptions } = props
632
689
  return series.map((series, i) => {
633
- return <SeriesItem getItemStyle={getItemStyle} sortableItemStyles={sortableItemStyles} chartsWithOptions={chartsWithOptions} series={series} index={i} key={`series-list-${i}`} />
690
+ return (
691
+ <SeriesItem
692
+ getItemStyle={getItemStyle}
693
+ sortableItemStyles={sortableItemStyles}
694
+ chartsWithOptions={chartsWithOptions}
695
+ series={series}
696
+ index={i}
697
+ key={`series-list-${i}`}
698
+ />
699
+ )
634
700
  })
635
701
  }
636
702
 
@@ -435,8 +435,7 @@ const PanelVisual: FC<PanelProps> = props => {
435
435
  min={15}
436
436
  />
437
437
  )}
438
- {((config.visualizationType === 'Bar' && config.orientation !== 'horizontal') ||
439
- config.visualizationType === 'Combo') && (
438
+ {(config.orientation !== 'horizontal' || config.visualizationType === 'Combo') && (
440
439
  <TextField
441
440
  value={config.barThickness}
442
441
  type='number'
@@ -44,6 +44,18 @@ export const useEditorPermissions = () => {
44
44
  return true
45
45
  }
46
46
 
47
+ const visSupportsDateCategoryAxisMin = () => {
48
+ const enabledCharts = ['Scatter Plot']
49
+ if (enabledCharts.includes(visualizationType)) return true
50
+ return false
51
+ }
52
+
53
+ const visSupportsDateCategoryAxisMax = () => {
54
+ const enabledCharts = ['Scatter Plot']
55
+ if (enabledCharts.includes(visualizationType)) return true
56
+ return false
57
+ }
58
+
47
59
  const visSupportsSuperTitle = () => {
48
60
  const disabledCharts = ['Spark Line']
49
61
  if (disabledCharts.includes(visualizationType)) return false
@@ -88,7 +100,7 @@ export const useEditorPermissions = () => {
88
100
  const visHasLegend = () => {
89
101
  switch (visualizationType) {
90
102
  case 'Box Plot':
91
- return false
103
+ return true
92
104
  case 'Forest Plot':
93
105
  return false
94
106
  case 'Spark Line':
@@ -141,7 +153,6 @@ export const useEditorPermissions = () => {
141
153
  }
142
154
  }
143
155
  const visHasBrushChart = () => {
144
- return false
145
156
  if (config.xAxis.type === 'categorical') return false
146
157
  return ['Line', 'Bar', 'Area Chart', 'Combo'].includes(visualizationType) && orientation === 'vertical'
147
158
  }
@@ -372,6 +383,10 @@ export const useEditorPermissions = () => {
372
383
  )
373
384
  }
374
385
 
386
+ const visSupportsYPadding = () => {
387
+ return !config.dataFormat.onlyShowTopPrefixSuffix || !config.dataFormat.suffix?.includes(' ')
388
+ }
389
+
375
390
  const visHasSingleSeriesTooltip = () => {
376
391
  if (visualizationType === 'Bar' || visualizationType === 'Line') {
377
392
  return true
@@ -414,6 +429,8 @@ export const useEditorPermissions = () => {
414
429
  visSupportsChartHeight,
415
430
  visSupportsMobileChartHeight,
416
431
  visSupportsDateCategoryAxis,
432
+ visSupportsDateCategoryAxisMin,
433
+ visSupportsDateCategoryAxisMax,
417
434
  visSupportsDateCategoryAxisLabel,
418
435
  visSupportsDateCategoryAxisLine,
419
436
  visSupportsDateCategoryAxisTicks,
@@ -443,6 +460,7 @@ export const useEditorPermissions = () => {
443
460
  visSupportsValueAxisMax,
444
461
  visSupportsValueAxisMin,
445
462
  visSupportsDynamicSeries,
463
+ visSupportsYPadding,
446
464
  visHasSingleSeriesTooltip,
447
465
  visHasCategoricalAxis
448
466
  }
@@ -2,14 +2,14 @@ import parse from 'html-react-parser'
2
2
  import { LegendOrdinal, LegendItem, LegendLabel } from '@visx/legend'
3
3
  import LegendShape from '@cdc/core/components/LegendShape'
4
4
  import Button from '@cdc/core/components/elements/Button'
5
- import useLegendClasses from '../../hooks/useLegendClasses'
5
+ import { getLegendClasses } from './helpers/getLegendClasses'
6
6
  import { useHighlightedBars } from '../../hooks/useHighlightedBars'
7
7
  import { handleLineType } from '../../helpers/handleLineType'
8
8
 
9
9
  import { getMarginTop, getGradientConfig, getMarginBottom } from './helpers/index'
10
10
  import { Line } from '@visx/shape'
11
11
  import { Label } from '../../types/Label'
12
- import { ChartConfig } from '../../types/ChartConfig'
12
+ import { ChartConfig, ViewportSize } from '../../types/ChartConfig'
13
13
  import { ColorScale } from '../../types/ChartContext'
14
14
  import { forwardRef, useState } from 'react'
15
15
  import LegendSuppression from './Legend.Suppression'
@@ -17,10 +17,12 @@ import LegendGradient from '@cdc/core/components/Legend/Legend.Gradient'
17
17
  import { DimensionsType } from '@cdc/core/types/Dimensions'
18
18
  import { isLegendWrapViewport } from '@cdc/core/helpers/viewports'
19
19
 
20
+ const LEGEND_PADDING = 30
21
+
20
22
  export interface LegendProps {
21
23
  colorScale: ColorScale
22
24
  config: ChartConfig
23
- currentViewport: 'lg' | 'md' | 'sm' | 'xs' | 'xxs'
25
+ currentViewport: ViewportSize
24
26
  formatLabels: (labels: Label[]) => Label[]
25
27
  highlight: Function
26
28
  highlightReset: Function
@@ -46,7 +48,7 @@ const Legend: React.FC<LegendProps> = forwardRef(
46
48
  },
47
49
  ref
48
50
  ) => {
49
- const { innerClasses, containerClasses } = useLegendClasses(config)
51
+ const { innerClasses, containerClasses } = getLegendClasses(config)
50
52
  const { runtime, legend } = config
51
53
 
52
54
  const [hasSuppression, setHasSuppression] = useState(false)
@@ -78,7 +80,7 @@ const Legend: React.FC<LegendProps> = forwardRef(
78
80
  config={config}
79
81
  {...getGradientConfig(config, formatLabels, colorScale)}
80
82
  dimensions={dimensions}
81
- currentViewport={currentViewport}
83
+ parentPaddingToSubtract={legend.hideBorder ? 0 : LEGEND_PADDING}
82
84
  />
83
85
 
84
86
  <LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
@@ -129,7 +131,7 @@ const Legend: React.FC<LegendProps> = forwardRef(
129
131
  }}
130
132
  role='button'
131
133
  >
132
- <div className='d-flex justify-content-center align-items-center'>
134
+ <>
133
135
  {config.visualizationType === 'Line' && config.legend.style === 'lines' ? (
134
136
  <svg width={40} height={25}>
135
137
  <Line
@@ -141,17 +143,14 @@ const Legend: React.FC<LegendProps> = forwardRef(
141
143
  />
142
144
  </svg>
143
145
  ) : (
144
- <div className='d-flex flex-column mt-1'>
146
+ <>
145
147
  <LegendShape
146
148
  shape={config.legend.style === 'boxes' ? 'square' : 'circle'}
147
- viewport={currentViewport}
148
- margin='0'
149
149
  fill={label.value}
150
- display={true}
151
150
  />
152
- </div>
151
+ </>
153
152
  )}
154
- </div>
153
+ </>
155
154
 
156
155
  <LegendLabel align='left' margin='0 0 0 4px'>
157
156
  {label.text}
@@ -17,6 +17,7 @@ const Legend = forwardRef((props, ref) => {
17
17
  transformedData: data,
18
18
  currentViewport,
19
19
  dimensions,
20
+ getTextWidth
20
21
  } = useContext(ConfigContext)
21
22
  if (!config.legend) return null
22
23
  // create fn to reverse labels while legend is Bottom. Legend-right , legend-left works by default.
@@ -24,22 +25,21 @@ const Legend = forwardRef((props, ref) => {
24
25
  const createLegendLabels = createFormatLabels(config, tableData, data, colorScale)
25
26
 
26
27
  return (
27
- !['Box Plot'].includes(config.visualizationType) && (
28
- <Fragment>
29
- <LegendComponent
30
- dimensions={dimensions}
31
- ref={ref}
32
- skipId={props.skipId || 'legend'}
33
- config={config}
34
- colorScale={colorScale}
35
- seriesHighlight={seriesHighlight}
36
- highlight={highlight}
37
- highlightReset={highlightReset}
38
- currentViewport={currentViewport}
39
- formatLabels={createLegendLabels}
40
- />
41
- </Fragment>
42
- )
28
+ <Fragment>
29
+ <LegendComponent
30
+ getTextWidth={getTextWidth}
31
+ dimensions={dimensions}
32
+ ref={ref}
33
+ skipId={props.skipId || 'legend'}
34
+ config={config}
35
+ colorScale={colorScale}
36
+ seriesHighlight={seriesHighlight}
37
+ highlight={highlight}
38
+ highlightReset={highlightReset}
39
+ currentViewport={currentViewport}
40
+ formatLabels={createLegendLabels}
41
+ />
42
+ </Fragment>
43
43
  )
44
44
  })
45
45
 
@@ -0,0 +1,59 @@
1
+ import { ChartConfig } from './../../../types/ChartConfig'
2
+
3
+ export const getLegendClasses = (config: ChartConfig) => {
4
+ const { position, singleRow, reverseLabelOrder, verticalSorted, hideBorder } = config.legend
5
+ const containerClasses = ['legend-container']
6
+ const innerClasses = ['legend-container__inner']
7
+
8
+ // Handle legend positioning
9
+ switch (position) {
10
+ case 'left':
11
+ containerClasses.push('left')
12
+ break
13
+ case 'right':
14
+ containerClasses.push('right')
15
+ break
16
+ case 'bottom':
17
+ containerClasses.push('bottom')
18
+ innerClasses.push('double-column', 'bottom')
19
+ break
20
+ case 'top':
21
+ containerClasses.push('top')
22
+ innerClasses.push('double-column', 'top')
23
+ break
24
+ }
25
+
26
+ // Handle single row configuration for 'bottom' and 'top' positions
27
+ if (['bottom', 'top'].includes(position) && singleRow) {
28
+ innerClasses.push('single-row')
29
+ }
30
+
31
+ // Reverse label order
32
+ if (reverseLabelOrder) {
33
+ innerClasses.push('d-flex', 'flex-column-reverse')
34
+ }
35
+
36
+ // Vertical sorting for 'bottom' and 'top' positions
37
+ if (['bottom', 'top'].includes(position) && verticalSorted) {
38
+ innerClasses.push('vertical-sorted')
39
+ }
40
+
41
+ // Configure border classes
42
+ if (hideBorder.side && (['right', 'left'].includes(position) || !position)) {
43
+ containerClasses.push('border-0')
44
+ }
45
+
46
+ if (hideBorder.topBottom && ['top', 'bottom'].includes(position)) {
47
+ containerClasses.push('border-0')
48
+ }
49
+
50
+ if (hideBorder.topBottom && ['top'].includes(position)) {
51
+ containerClasses.push('p-0')
52
+ }
53
+
54
+ return {
55
+ containerClasses,
56
+ innerClasses
57
+ }
58
+ }
59
+ export default getLegendClasses