@cdc/chart 4.26.6 → 4.26.7

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/CONFIG.md +9 -7
  2. package/dist/cdcchart.js +32200 -31292
  3. package/examples/private/flueview.json +22051 -0
  4. package/package.json +3 -11
  5. package/src/CdcChart.tsx +2 -2
  6. package/src/CdcChartComponent.tsx +40 -10
  7. package/src/_stories/Chart.PieLabels.stories.tsx +47 -0
  8. package/src/_stories/Chart.smoke.stories.tsx +14 -1
  9. package/src/_stories/ChartPrefixSuffix.smoke.stories.tsx +28 -1
  10. package/src/components/BarChart/components/BarChart.Horizontal.tsx +1 -2
  11. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +1 -1
  12. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -1
  13. package/src/components/BarChart/components/BarChart.Vertical.tsx +1 -2
  14. package/src/components/BarChart/helpers/getBarData.ts +28 -28
  15. package/src/components/BarChart/helpers/getBarHeights.ts +2 -2
  16. package/src/components/BarChart/helpers/tests/useBarChart.test.tsx +98 -0
  17. package/src/components/BarChart/helpers/useBarChart.ts +4 -5
  18. package/src/components/BoxPlot/BoxPlot.Horizontal.tsx +7 -6
  19. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +9 -7
  20. package/src/components/BoxPlot/helpers/index.ts +4 -3
  21. package/src/components/Brush/BrushSelector.tsx +1 -1
  22. package/src/components/EditorPanel/EditorPanel.tsx +90 -29
  23. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +16 -7
  24. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +3 -3
  25. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +19 -4
  26. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +3 -3
  27. package/src/components/EditorPanel/useEditorPermissions.test.tsx +32 -0
  28. package/src/components/EditorPanel/useEditorPermissions.ts +5 -1
  29. package/src/components/Forecasting/Forecasting.tsx +1 -1
  30. package/src/components/Legend/Legend.Component.tsx +54 -59
  31. package/src/components/Legend/Legend.Suppression.tsx +3 -3
  32. package/src/components/Legend/LegendGroup/LegendGroup.tsx +0 -1
  33. package/src/components/LineChart/helpers.ts +0 -2
  34. package/src/components/LineChart/index.tsx +2 -3
  35. package/src/components/LinearChart.tsx +4 -3
  36. package/src/components/PieChart/PieChart.tsx +117 -10
  37. package/src/components/PieChart/helpers/labelPlacement.test.ts +88 -0
  38. package/src/components/PieChart/helpers/labelPlacement.ts +95 -0
  39. package/src/components/ScatterPlot/ScatterPlot.jsx +2 -10
  40. package/src/data/initial-state.js +2 -0
  41. package/src/helpers/categoryOrder.ts +60 -0
  42. package/src/helpers/getBoxPlotConfig.ts +19 -12
  43. package/src/helpers/getComboChartConfig.ts +0 -1
  44. package/src/helpers/getExcludedData.ts +0 -1
  45. package/src/helpers/getHasBoundarySuppression.ts +4 -3
  46. package/src/helpers/getMinMax.ts +2 -1
  47. package/src/helpers/getNewRuntime.ts +0 -2
  48. package/src/helpers/getPiePercent.ts +3 -3
  49. package/src/helpers/isConvertLineToBarGraph.ts +2 -2
  50. package/src/helpers/seriesColumnSettings.ts +21 -3
  51. package/src/helpers/sizeHelpers.ts +0 -2
  52. package/src/helpers/tests/categoryOrder.test.ts +57 -0
  53. package/src/helpers/tests/seriesColumnSettings.test.ts +60 -0
  54. package/src/hooks/tests/useRightAxis.test.tsx +143 -2
  55. package/src/hooks/tests/useScales.test.tsx +3 -6
  56. package/src/hooks/tests/useTooltip.test.tsx +132 -0
  57. package/src/hooks/useRightAxis.ts +19 -26
  58. package/src/hooks/useTooltip.tsx +14 -10
  59. package/src/store/chart.reducer.ts +0 -1
  60. package/src/test/CdcChart.dataTable.test.tsx +94 -0
@@ -7,7 +7,9 @@ import { colorPalettesChart } from '@cdc/core/data/colorPalettes'
7
7
  import { getCurrentPaletteName } from '@cdc/core/helpers/palettes/utils'
8
8
  import { handleTooltip, createPlots } from './helpers/index'
9
9
  import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
10
- import _ from 'lodash'
10
+ import get from 'lodash/get'
11
+ import map from 'lodash/map'
12
+ import round from 'lodash/round'
11
13
 
12
14
  const BoxPlotVertical = ({ xScale, yScale, seriesScale, yAxisWidth }) => {
13
15
  const { config, colorScale, seriesHighlight, transformedData: data } = useContext(ConfigContext)
@@ -19,7 +21,7 @@ const BoxPlotVertical = ({ xScale, yScale, seriesScale, yAxisWidth }) => {
19
21
  const bodyStyles = getComputedStyle(document.body)
20
22
  const defaultColor = APP_FONT_COLOR
21
23
  const constrainedWidth = Math.min(40, boxWidth)
22
- const color_0 = _.get(colorPalettesChart, [getCurrentPaletteName(config), 0], '#000')
24
+ const color_0 = get(colorPalettesChart, [getCurrentPaletteName(config), 0], '#000')
23
25
  const plots = createPlots(data, config)
24
26
  return (
25
27
  <ErrorBoundary component='BoxPlot Vertical'>
@@ -76,7 +78,7 @@ const BoxPlotVertical = ({ xScale, yScale, seriesScale, yAxisWidth }) => {
76
78
  fillOpacity={1}
77
79
  stroke={defaultColor}
78
80
  valueScale={yScale}
79
- outliers={boxplot.plotOutlierValues ? _.map(d.columnOutliers[item.dataKey], item => item) : []}
81
+ outliers={boxplot.plotOutlierValues ? map(d.columnOutliers[item.dataKey], item => item) : []}
80
82
  outlierProps={{
81
83
  style: {
82
84
  fill: defaultColor,
@@ -109,10 +111,10 @@ const BoxPlotVertical = ({ xScale, yScale, seriesScale, yAxisWidth }) => {
109
111
  boxplot,
110
112
  d.columnCategory,
111
113
  item.dataKey,
112
- _.round(d.q1[item.dataKey], config.dataFormat.roundTo),
113
- _.round(d.q3[item.dataKey], config.dataFormat.roundTo),
114
- _.round(d.median[item.dataKey], config.dataFormat.roundTo),
115
- _.round(d.iqr[item.dataKey], config.dataFormat.roundTo),
114
+ round(d.q1[item.dataKey], config.dataFormat.roundTo),
115
+ round(d.q3[item.dataKey], config.dataFormat.roundTo),
116
+ round(d.median[item.dataKey], config.dataFormat.roundTo),
117
+ round(d.iqr[item.dataKey], config.dataFormat.roundTo),
116
118
  config.xAxis.label,
117
119
  defaultColor
118
120
  ),
@@ -1,4 +1,5 @@
1
- import _ from 'lodash'
1
+ import sortBy from 'lodash/sortBy'
2
+ import uniq from 'lodash/uniq'
2
3
  import * as d3 from 'd3-array'
3
4
 
4
5
  interface Plot {
@@ -33,7 +34,7 @@ const calculateBoxPlotStats = (values: number[]) => {
33
34
  if (!values || values.length === 0) return {}
34
35
 
35
36
  // Sort the values
36
- const sortedValues = _.sortBy(values.map(v => Number(v)))
37
+ const sortedValues = sortBy(values.map(v => Number(v)))
37
38
 
38
39
  // Quartiles
39
40
  const firstQuartile = d3.quantile(sortedValues, 0.25) ?? 0
@@ -103,7 +104,7 @@ const calculateNonOutliers = (values: number[], firstQuartile: number, thirdQuar
103
104
  export const createPlots = (data, config) => {
104
105
  const dataKeys = data.map(d => d[config.xAxis.dataKey])
105
106
  const plots: Plot[] = []
106
- const groups: string[] = _.uniq(dataKeys)
107
+ const groups: string[] = uniq(dataKeys)
107
108
 
108
109
  if (groups && groups.length > 0) {
109
110
  groups.forEach(group => {
@@ -128,7 +128,7 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
128
128
  <>
129
129
  {Object.entries(config.legend.patterns).map(([key, pattern]) => {
130
130
  const patternId = getChartPatternId(key)
131
- const size = pattern.patternSize || 8
131
+ const size = pattern.patternSize || 10
132
132
 
133
133
  switch (pattern.shape) {
134
134
  case 'circles':
@@ -15,6 +15,7 @@ import { EditorPanel as BaseEditorPanel } from '@cdc/core/components/EditorPanel
15
15
  import AdvancedEditor from '@cdc/core/components/AdvancedEditor'
16
16
  import Icon from '@cdc/core/components/ui/Icon'
17
17
  import ColumnsEditor from '@cdc/core/components/EditorPanel/ColumnsEditor'
18
+ import CustomSortOrder from '@cdc/core/components/EditorPanel/CustomSortOrder'
18
19
  import DataTableEditor from '@cdc/core/components/EditorPanel/DataTableEditor'
19
20
  import GroupedList from '@cdc/core/components/EditorPanel/GroupedList'
20
21
  import VizFilterEditor from '@cdc/core/components/EditorPanel/VizFilterEditor'
@@ -36,7 +37,6 @@ import { useEditorPermissions } from './useEditorPermissions'
36
37
  import { useHighlightedBars } from '../../hooks/useHighlightedBars'
37
38
  import ConfigContext from '../../ConfigContext'
38
39
  import useReduceData from '../../hooks/useReduceData'
39
- import useRightAxis from '../../hooks/useRightAxis'
40
40
  import WarningImage from '../../images/warning.svg'
41
41
  import getMinMax from '../../helpers/getMinMax'
42
42
 
@@ -61,6 +61,7 @@ import { paletteMigrationMap, twoColorPaletteMigrationMap } from '@cdc/core/help
61
61
  import { isV1Palette, migratePaletteWithMap } from '@cdc/core/helpers/palettes/utils'
62
62
  import { USE_V2_MIGRATION } from '@cdc/core/helpers/constants'
63
63
  import { getSeriesOwnedColumnNames } from '../../helpers/seriesColumnSettings'
64
+ import { getOrderedCategoryValues } from '../../helpers/categoryOrder'
64
65
  import {
65
66
  HEATMAP_CONFIG_DEFAULTS,
66
67
  MAX_HEATMAP_COLOR_BUCKETS,
@@ -898,6 +899,7 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
898
899
  visSupportsPreliminaryData,
899
900
  visSupportsRankByValue,
900
901
  visSupportsResponsiveTicks,
902
+ visSupportsRightValueAxis,
901
903
  visSupportsValueAxisGridLines,
902
904
  visSupportsValueAxisLabels,
903
905
  visSupportsValueAxisLine,
@@ -949,8 +951,6 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
949
951
  }
950
952
  }, [config.visualizationType])
951
953
 
952
- const { hasRightAxis } = useRightAxis({ config: config, yMax: 0, data: config.data, updateConfig })
953
-
954
954
  const getItemStyle = (isDragging, draggableStyle) => ({
955
955
  ...draggableStyle
956
956
  })
@@ -1059,6 +1059,8 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
1059
1059
  xAxis: {
1060
1060
  ...config.xAxis,
1061
1061
  dataKey: newValue,
1062
+ categoryOrder: [],
1063
+ categoryOrderType: 'data',
1062
1064
  ...(autoDetectedDateParseFormat ? { dateParseFormat: autoDetectedDateParseFormat } : {})
1063
1065
  }
1064
1066
  }
@@ -1289,6 +1291,58 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
1289
1291
  return getDataValues(dataKey, true).filter(value => !excludedKeys.includes(String(value)))
1290
1292
  }
1291
1293
 
1294
+ const categoryOrderValues = useMemo(
1295
+ () =>
1296
+ getOrderedCategoryValues(
1297
+ excludedData as Record<string, any>[],
1298
+ config.xAxis?.dataKey,
1299
+ config.xAxis?.categoryOrder
1300
+ ),
1301
+ [excludedData, config.xAxis?.dataKey, config.xAxis?.categoryOrder]
1302
+ )
1303
+
1304
+ const updateCategoryOrderType = (categoryOrderType: 'data' | 'custom') => {
1305
+ updateConfig({
1306
+ ...config,
1307
+ xAxis: {
1308
+ ...config.xAxis,
1309
+ categoryOrderType,
1310
+ categoryOrder: categoryOrderType === 'custom' ? categoryOrderValues : []
1311
+ }
1312
+ })
1313
+ }
1314
+
1315
+ const renderCategoryOrderControls = (label = 'Category Order') => {
1316
+ if (config.xAxis?.type !== 'categorical' || !config.xAxis?.dataKey || !categoryOrderValues.length) return null
1317
+
1318
+ return (
1319
+ <>
1320
+ <Select
1321
+ value={config.xAxis.categoryOrderType || 'data'}
1322
+ section='xAxis'
1323
+ fieldName='categoryOrderType'
1324
+ label={label}
1325
+ options={[
1326
+ { label: 'Data order', value: 'data' },
1327
+ { label: 'Custom', value: 'custom' }
1328
+ ]}
1329
+ updateField={(_section, _subsection, _fieldName, value) => updateCategoryOrderType(value)}
1330
+ />
1331
+ {config.xAxis.categoryOrderType === 'custom' && (
1332
+ <CustomSortOrder
1333
+ column={config.xAxis.dataKey}
1334
+ data={excludedData as Record<string, any>[]}
1335
+ customOrder={config.xAxis.categoryOrder}
1336
+ updateField={updateFieldDeprecated}
1337
+ updateTarget={{ section: 'xAxis', subsection: null, fieldName: 'categoryOrder' }}
1338
+ droppableId='category_order'
1339
+ draggableIdPrefix='categoryOrder'
1340
+ />
1341
+ )}
1342
+ </>
1343
+ )
1344
+ }
1345
+
1292
1346
  // prettier-ignore
1293
1347
  const {
1294
1348
  highlightedBarValues,
@@ -3127,7 +3181,7 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
3127
3181
  </AccordionItem>
3128
3182
  )}
3129
3183
  {/* Right Value Axis Settings */}
3130
- {hasRightAxis && (
3184
+ {visSupportsRightValueAxis() && (
3131
3185
  <AccordionItem>
3132
3186
  <AccordionItemHeading>
3133
3187
  <AccordionItemButton>Right Value Axis</AccordionItemButton>
@@ -3378,6 +3432,9 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
3378
3432
  xAxis: {
3379
3433
  ...config.xAxis,
3380
3434
  type: value,
3435
+ ...(value !== 'categorical'
3436
+ ? { categoryOrder: [], categoryOrderType: 'data' }
3437
+ : {}),
3381
3438
  ...(autoDetectedDateParseFormat
3382
3439
  ? { dateParseFormat: autoDetectedDateParseFormat }
3383
3440
  : {})
@@ -3448,33 +3505,37 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
3448
3505
  </Tooltip>
3449
3506
  }
3450
3507
  />
3508
+ {renderCategoryOrderControls()}
3451
3509
  </>
3452
3510
  )}
3453
3511
 
3454
3512
  {config.visualizationType === 'Pie' && (
3455
- <Select
3456
- value={config.xAxis.dataKey || ''}
3457
- section='xAxis'
3458
- fieldName='dataKey'
3459
- label='Segment Labels'
3460
- initial='Select'
3461
- required={true}
3462
- updateField={updateFieldDeprecated}
3463
- options={getColumns(false)}
3464
- tooltip={
3465
- <Tooltip style={{ textTransform: 'none' }}>
3466
- <Tooltip.Target>
3467
- <Icon display='question' style={{ marginLeft: '0.5rem' }} />
3468
- </Tooltip.Target>
3469
- <Tooltip.Content>
3470
- <p>
3471
- Select the source row or column that contains the segment labels. Depending on the
3472
- data structure, it may be listed as "Key."
3473
- </p>
3474
- </Tooltip.Content>
3475
- </Tooltip>
3476
- }
3477
- />
3513
+ <>
3514
+ <Select
3515
+ value={config.xAxis.dataKey || ''}
3516
+ section='xAxis'
3517
+ fieldName='dataKey'
3518
+ label='Segment Labels'
3519
+ initial='Select'
3520
+ required={true}
3521
+ updateField={updateFieldDeprecated}
3522
+ options={getColumns(false)}
3523
+ tooltip={
3524
+ <Tooltip style={{ textTransform: 'none' }}>
3525
+ <Tooltip.Target>
3526
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
3527
+ </Tooltip.Target>
3528
+ <Tooltip.Content>
3529
+ <p>
3530
+ Select the source row or column that contains the segment labels. Depending on the
3531
+ data structure, it may be listed as "Key."
3532
+ </p>
3533
+ </Tooltip.Content>
3534
+ </Tooltip>
3535
+ }
3536
+ />
3537
+ {renderCategoryOrderControls('Segment Order')}
3538
+ </>
3478
3539
  )}
3479
3540
 
3480
3541
  {config.visualizationType !== 'Pie' && (
@@ -4181,8 +4242,8 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
4181
4242
  <label htmlFor='barHighlight'>Bar Highlighting</label>
4182
4243
  {config.series.length === 1 &&
4183
4244
  highlightedBarValues.map((highlightedBarValue, i) => (
4184
- <fieldset>
4185
- <div className='edit-block' key={`highlighted-bar-${i}`}>
4245
+ <fieldset key={`highlighted-bar-${i}`}>
4246
+ <div className='edit-block'>
4186
4247
  <Button
4187
4248
  className='btn btn-danger'
4188
4249
  onClick={e => handleRemoveHighlightedBar(e, i)}
@@ -308,7 +308,7 @@ const PanelGeneral: FC<PanelProps> = props => {
308
308
  <CheckBox
309
309
  value={config.labels}
310
310
  fieldName='labels'
311
- label='Display label on data'
311
+ label={visualizationType === 'Pie' ? 'Display Category and Percentage Labels' : 'Display label on data'}
312
312
  updateField={updateField}
313
313
  tooltip={
314
314
  <Tooltip style={{ textTransform: 'none' }}>
@@ -316,12 +316,21 @@ const PanelGeneral: FC<PanelProps> = props => {
316
316
  <Icon display='question' style={{ marginLeft: '0.5rem' }} />
317
317
  </Tooltip.Target>
318
318
  <Tooltip.Content>
319
- <p>Recommended set to display for Section 508 compliance.</p>
320
- <hr />
321
- <p>
322
- Selecting this option will <i> not </i> hide the display of "zero value", "suppressed data", or
323
- "missing data" indicators on the chart (if applicable).
324
- </p>
319
+ {visualizationType === 'Pie' ? (
320
+ <p>
321
+ Labels that do not fit and donut labels will display outside of chart. Use a Custom Segment
322
+ Order to resolve overlapping labels.
323
+ </p>
324
+ ) : (
325
+ <>
326
+ <p>Recommended set to display for Section 508 compliance.</p>
327
+ <hr />
328
+ <p>
329
+ Selecting this option will <i> not </i> hide the display of "zero value", "suppressed data",
330
+ or "missing data" indicators on the chart (if applicable).
331
+ </p>
332
+ </>
333
+ )}
325
334
  </Tooltip.Content>
326
335
  </Tooltip>
327
336
  }
@@ -68,7 +68,7 @@ const PanelPatternSettings: FC<PanelProps> = props => {
68
68
  case 'large':
69
69
  return 16
70
70
  default:
71
- return 8
71
+ return 10
72
72
  }
73
73
  }
74
74
 
@@ -192,7 +192,7 @@ const PanelPatternSettings: FC<PanelProps> = props => {
192
192
  shape: 'circles' as const,
193
193
  dataKey: defaultDataKey,
194
194
  dataValue: '',
195
- patternSize: 8, // Default pattern size
195
+ patternSize: 10, // Default pattern size
196
196
  contrastCheck: performContrastCheck(newPatternKey, defaultColor)
197
197
  }
198
198
  }
@@ -437,7 +437,7 @@ const PanelPatternSettings: FC<PanelProps> = props => {
437
437
 
438
438
  <Select
439
439
  label='Pattern Size:'
440
- value={getPatternSizeText(p.patternSize || 8)}
440
+ value={getPatternSizeText(p.patternSize || 10)}
441
441
  options={patternSizes}
442
442
  fieldName={`pattern-size-${domPatternKey}`}
443
443
  updateField={(section, subsection, fieldName, value) =>
@@ -556,6 +556,22 @@ const SeriesColumnSettings = props => {
556
556
  })
557
557
  }
558
558
 
559
+ const updateSeriesRoundToPlace = value => {
560
+ const trimmed = value === undefined || value === null ? '' : String(value).trim()
561
+ if (!trimmed) {
562
+ updateSeriesColumn('roundToPlace', undefined)
563
+ return
564
+ }
565
+
566
+ const numericValue = Number(trimmed)
567
+ if (!Number.isFinite(numericValue)) {
568
+ updateSeriesColumn('roundToPlace', undefined)
569
+ return
570
+ }
571
+
572
+ updateSeriesColumn('roundToPlace', Math.max(0, Math.min(20, Math.round(numericValue))))
573
+ }
574
+
559
575
  return (
560
576
  <>
561
577
  <span className='divider-heading'>Series Column Settings</span>
@@ -586,14 +602,13 @@ const SeriesColumnSettings = props => {
586
602
  />
587
603
  <TextField
588
604
  type='number'
589
- value={columnConfig.roundToPlace ?? 0}
605
+ value={columnConfig.roundToPlace ?? ''}
590
606
  section='seriesColumn'
591
607
  subsection={series.dataKey}
592
608
  fieldName='roundToPlace'
593
609
  label='Round'
594
- updateField={(_section, _subsection, _fieldName, value) =>
595
- updateSeriesColumn('roundToPlace', Number(value) || 0)
596
- }
610
+ updateField={(_section, _subsection, _fieldName, value) => updateSeriesRoundToPlace(value)}
611
+ min={0}
597
612
  />
598
613
  </div>
599
614
  <CheckBox
@@ -1,7 +1,7 @@
1
1
  import DataTransform from '@cdc/core/helpers/DataTransform'
2
2
  import { ChartConfig } from '../../../types/ChartConfig'
3
3
  import cloneConfig from '@cdc/core/helpers/cloneConfig'
4
- import _ from 'lodash'
4
+ import get from 'lodash/get'
5
5
 
6
6
  const transform = new DataTransform()
7
7
 
@@ -31,8 +31,8 @@ export const updateFieldRankByValue = (
31
31
  newConfig.rankByValue = newValue
32
32
 
33
33
  if (config.rankByValue && !newValue) {
34
- const CIkeys: string[] = Object.values(_.get(config, 'confidenceKeys', {})) as string[]
35
- const seriesKeys: string[] = _.get(config, 'series', []).map((s: any) => s.dataKey)
34
+ const CIkeys: string[] = Object.values(get(config, 'confidenceKeys', {})) as string[]
35
+ const seriesKeys: string[] = get(config, 'series', []).map((s: any) => s.dataKey)
36
36
  const keysToClean: string[] = [...(seriesKeys ?? []), ...(CIkeys ?? [])]
37
37
 
38
38
  const cleanData = config?.xAxis?.dataKey
@@ -99,6 +99,38 @@ describe('useEditorPermissions', () => {
99
99
  expect(unsupported.result.current.visSupportsSeriesColorAssignments()).toBe(false)
100
100
  })
101
101
 
102
+ it('shows the data-label control for pie charts', () => {
103
+ const { result } = renderUseEditorPermissions({
104
+ visualizationType: 'Pie'
105
+ })
106
+
107
+ expect(result.current.visHasLabelOnData()).toBe(true)
108
+ })
109
+
110
+ it('shows right value axis controls for vertical Combo charts without requiring a right-axis series', () => {
111
+ const { result } = renderUseEditorPermissions({
112
+ visualizationType: 'Combo',
113
+ orientation: 'vertical',
114
+ series: [{ dataKey: 'Cases', type: 'Bar', axis: 'Left' }] as any
115
+ })
116
+
117
+ expect(result.current.visSupportsRightValueAxis()).toBe(true)
118
+ })
119
+
120
+ it('hides right value axis controls outside vertical Combo charts', () => {
121
+ const horizontalCombo = renderUseEditorPermissions({
122
+ visualizationType: 'Combo',
123
+ orientation: 'horizontal'
124
+ })
125
+ const verticalBar = renderUseEditorPermissions({
126
+ visualizationType: 'Bar',
127
+ orientation: 'vertical'
128
+ })
129
+
130
+ expect(horizontalCombo.result.current.visSupportsRightValueAxis()).toBe(false)
131
+ expect(verticalBar.result.current.visSupportsRightValueAxis()).toBe(false)
132
+ })
133
+
102
134
  it('shows filter-domain behavior when a standalone chart has visible filters and automatic max', () => {
103
135
  const { result } = renderUseEditorPermissions({
104
136
  filters: [{ columnName: 'Region', showDropdown: true } as any],
@@ -82,7 +82,6 @@ export const useEditorPermissions = () => {
82
82
  'Forest Plot',
83
83
  'HeatMap',
84
84
  'Horizon Chart',
85
- 'Pie',
86
85
  'Radar',
87
86
  'Sankey',
88
87
  'Scatter Plot',
@@ -399,6 +398,10 @@ export const useEditorPermissions = () => {
399
398
  return true
400
399
  }
401
400
 
401
+ const visSupportsRightValueAxis = () => {
402
+ return visualizationType === 'Combo' && orientation === 'vertical'
403
+ }
404
+
402
405
  const visSupportsRankByValue = () => {
403
406
  const disabledCharts = ['HeatMap', 'Spark Line']
404
407
  if (disabledCharts.includes(visualizationType)) return false
@@ -530,6 +533,7 @@ export const useEditorPermissions = () => {
530
533
  visSupportsReactTooltip,
531
534
  visSupportsRegions,
532
535
  visSupportsResponsiveTicks,
536
+ visSupportsRightValueAxis,
533
537
  visSupportsReverseColorPalette,
534
538
  visSupportsSequentialPallete,
535
539
  visSupportsSeriesColorAssignments,
@@ -1,5 +1,5 @@
1
1
  import React, { useContext, useMemo } from 'react'
2
- import { replace } from 'lodash'
2
+ import replace from 'lodash/replace'
3
3
  // cdc
4
4
  import ConfigContext from '../../ConfigContext'
5
5
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
@@ -1,7 +1,6 @@
1
1
  import parse from 'html-react-parser'
2
2
  import React, { useMemo } from 'react'
3
3
  import { LegendOrdinal, LegendItem, LegendLabel } from '@visx/legend'
4
- import { PatternLines, PatternCircles, PatternWaves } from '@visx/pattern'
5
4
  import LegendShape from '@cdc/core/components/LegendShape'
6
5
  import Button from '@cdc/core/components/elements/Button'
7
6
  import { getLegendClasses } from './helpers/getLegendClasses'
@@ -22,6 +21,7 @@ import { getHorizonLayerColors, getHorizonMaxValue } from '../../components/Hori
22
21
  import { getSeriesWithData } from '../../helpers/dataHelpers'
23
22
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
24
23
  import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
24
+ import { processMarkupVariables } from '@cdc/core/helpers/markupProcessor'
25
25
 
26
26
  const LEGEND_PADDING = 36
27
27
 
@@ -62,6 +62,42 @@ const Legend: React.FC<LegendProps> = forwardRef(
62
62
  const { innerClasses, containerClasses } = getLegendClasses(config)
63
63
  const { runtime, legend } = config
64
64
  const { series } = runtime
65
+ const processedLegendText = useMemo(() => {
66
+ if (!config.enableMarkupVariables || !config.markupVariables?.length) {
67
+ return {
68
+ label: legend?.label,
69
+ description: legend?.description
70
+ }
71
+ }
72
+
73
+ const markupOptions = {
74
+ isEditor: false,
75
+ filters: config.filters || [],
76
+ locale: config.locale,
77
+ dataMetadata: config.dataMetadata
78
+ }
79
+
80
+ return {
81
+ label: legend?.label
82
+ ? processMarkupVariables(legend.label, data || config.data || [], config.markupVariables, markupOptions)
83
+ .processedContent
84
+ : legend?.label,
85
+ description: legend?.description
86
+ ? processMarkupVariables(legend.description, data || config.data || [], config.markupVariables, markupOptions)
87
+ .processedContent
88
+ : legend?.description
89
+ }
90
+ }, [
91
+ config.enableMarkupVariables,
92
+ config.markupVariables,
93
+ config.filters,
94
+ config.locale,
95
+ config.dataMetadata,
96
+ config.data,
97
+ data,
98
+ legend?.label,
99
+ legend?.description
100
+ ])
65
101
 
66
102
  const seriesWithData = getSeriesWithData(config)
67
103
  // For Radar charts, seriesWithData contains dimension keys but legend shows entity names
@@ -115,10 +151,12 @@ const Legend: React.FC<LegendProps> = forwardRef(
115
151
  aria-label='legend'
116
152
  tabIndex={0}
117
153
  >
118
- {(legend.label || legend.description) && (
119
- <div className={legend.description ? 'mb-3' : 'mb-2'}>
120
- {legend.label && <h3 className='fw-bold cove-prose'>{parse(legend.label)}</h3>}
121
- {legend.description && <p className='mt-2 cove-prose'>{parse(legend.description)}</p>}
154
+ {(processedLegendText.label || processedLegendText.description) && (
155
+ <div className={processedLegendText.description ? 'mb-3' : 'mb-2'}>
156
+ {processedLegendText.label && <h3 className='fw-bold cove-prose'>{parse(processedLegendText.label)}</h3>}
157
+ {processedLegendText.description && (
158
+ <p className='mt-2 cove-prose'>{parse(processedLegendText.description)}</p>
159
+ )}
122
160
  </div>
123
161
  )}
124
162
  <LegendGradient
@@ -298,9 +336,8 @@ const Legend: React.FC<LegendProps> = forwardRef(
298
336
  >
299
337
  {Object.entries(config.legend.patterns).map(([key, pattern]) => {
300
338
  const patternId = `legend-pattern-${key}`
301
- const size = config.legend.patternSize || 8
302
- const legendSize = 16
303
339
  const pColor = (pattern as any)?.color || '#666666'
340
+ const patternSize = (pattern as any)?.patternSize ?? 10
304
341
 
305
342
  return (
306
343
  <LegendItem
@@ -309,58 +346,16 @@ const Legend: React.FC<LegendProps> = forwardRef(
309
346
  tabIndex={0}
310
347
  role='button'
311
348
  >
312
- <span className='me-2'>
313
- <svg width={legendSize} height={legendSize}>
314
- <defs>
315
- {pattern.shape === 'circles' && (
316
- <PatternCircles
317
- id={patternId}
318
- height={size}
319
- width={size}
320
- fill={pColor}
321
- radius={1.25}
322
- />
323
- )}
324
- {pattern.shape === 'lines' && (
325
- <PatternLines
326
- id={patternId}
327
- height={size}
328
- width={size}
329
- stroke={pColor}
330
- strokeWidth={0.75}
331
- orientation={['horizontal']}
332
- />
333
- )}
334
- {pattern.shape === 'diagonalLines' && (
335
- <PatternLines
336
- id={patternId}
337
- height={size}
338
- width={size}
339
- stroke={pColor}
340
- strokeWidth={0.75}
341
- orientation={['diagonalRightToLeft']}
342
- />
343
- )}
344
- {pattern.shape === 'waves' && (
345
- <PatternWaves
346
- id={patternId}
347
- height={size}
348
- width={size}
349
- fill={pColor}
350
- strokeWidth={0.25}
351
- />
352
- )}
353
- </defs>
354
- <circle
355
- fill={`url(#${patternId})`}
356
- r={legendSize / 2}
357
- cx={legendSize / 2}
358
- cy={legendSize / 2}
359
- stroke='#0000004d'
360
- strokeWidth={1}
361
- />
362
- </svg>
363
- </span>
349
+ <LegendShape
350
+ shape={config.legend.style === 'boxes' ? 'square' : 'circle'}
351
+ fill='white'
352
+ patternInfo={{
353
+ pattern: pattern.shape || 'circles',
354
+ patternId,
355
+ size: patternSize,
356
+ color: pColor
357
+ }}
358
+ />
364
359
  <LegendLabel align='left' className='m-0'>
365
360
  {parse(String((pattern as any)?.label || key))}
366
361
  </LegendLabel>
@@ -70,16 +70,16 @@ const LegendSuppression: React.FC<LegendProps> = ({ config, isLegendBottom }) =>
70
70
 
71
71
  if (visualizationType === 'Combo') {
72
72
  return (
73
- <React.Fragment>
73
+ <React.Fragment key={itemKey}>
74
74
  {pd.symbol && pd.iconCode && (
75
- <div style={getStyle(pd.displayGray)} key={itemKey} className={`${baseClass} ${pd.symbol}`}>
75
+ <div style={getStyle(pd.displayGray)} className={`${baseClass} ${pd.symbol}`}>
76
76
  <span className={pd.symbol}>{pd.iconCode}</span>
77
77
  <p className={pd.type}>{pd.label}</p>
78
78
  </div>
79
79
  )}
80
80
 
81
81
  {pd.style && pd.lineCode && (
82
- <div style={getStyle(pd.displayGray)} key={itemKey} className={baseClass}>
82
+ <div style={getStyle(pd.displayGray)} className={baseClass}>
83
83
  <span>{pd.lineCode}</span>
84
84
  <p>{pd.label}</p>
85
85
  </div>