@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.
- package/CONFIG.md +9 -7
- package/dist/cdcchart.js +32200 -31292
- package/examples/private/flueview.json +22051 -0
- package/package.json +3 -11
- package/src/CdcChart.tsx +2 -2
- package/src/CdcChartComponent.tsx +40 -10
- package/src/_stories/Chart.PieLabels.stories.tsx +47 -0
- package/src/_stories/Chart.smoke.stories.tsx +14 -1
- package/src/_stories/ChartPrefixSuffix.smoke.stories.tsx +28 -1
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +1 -2
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +1 -1
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -1
- package/src/components/BarChart/components/BarChart.Vertical.tsx +1 -2
- package/src/components/BarChart/helpers/getBarData.ts +28 -28
- package/src/components/BarChart/helpers/getBarHeights.ts +2 -2
- package/src/components/BarChart/helpers/tests/useBarChart.test.tsx +98 -0
- package/src/components/BarChart/helpers/useBarChart.ts +4 -5
- package/src/components/BoxPlot/BoxPlot.Horizontal.tsx +7 -6
- package/src/components/BoxPlot/BoxPlot.Vertical.tsx +9 -7
- package/src/components/BoxPlot/helpers/index.ts +4 -3
- package/src/components/Brush/BrushSelector.tsx +1 -1
- package/src/components/EditorPanel/EditorPanel.tsx +90 -29
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +16 -7
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +3 -3
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +19 -4
- package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +3 -3
- package/src/components/EditorPanel/useEditorPermissions.test.tsx +32 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +5 -1
- package/src/components/Forecasting/Forecasting.tsx +1 -1
- package/src/components/Legend/Legend.Component.tsx +54 -59
- package/src/components/Legend/Legend.Suppression.tsx +3 -3
- package/src/components/Legend/LegendGroup/LegendGroup.tsx +0 -1
- package/src/components/LineChart/helpers.ts +0 -2
- package/src/components/LineChart/index.tsx +2 -3
- package/src/components/LinearChart.tsx +4 -3
- package/src/components/PieChart/PieChart.tsx +117 -10
- package/src/components/PieChart/helpers/labelPlacement.test.ts +88 -0
- package/src/components/PieChart/helpers/labelPlacement.ts +95 -0
- package/src/components/ScatterPlot/ScatterPlot.jsx +2 -10
- package/src/data/initial-state.js +2 -0
- package/src/helpers/categoryOrder.ts +60 -0
- package/src/helpers/getBoxPlotConfig.ts +19 -12
- package/src/helpers/getComboChartConfig.ts +0 -1
- package/src/helpers/getExcludedData.ts +0 -1
- package/src/helpers/getHasBoundarySuppression.ts +4 -3
- package/src/helpers/getMinMax.ts +2 -1
- package/src/helpers/getNewRuntime.ts +0 -2
- package/src/helpers/getPiePercent.ts +3 -3
- package/src/helpers/isConvertLineToBarGraph.ts +2 -2
- package/src/helpers/seriesColumnSettings.ts +21 -3
- package/src/helpers/sizeHelpers.ts +0 -2
- package/src/helpers/tests/categoryOrder.test.ts +57 -0
- package/src/helpers/tests/seriesColumnSettings.test.ts +60 -0
- package/src/hooks/tests/useRightAxis.test.tsx +143 -2
- package/src/hooks/tests/useScales.test.tsx +3 -6
- package/src/hooks/tests/useTooltip.test.tsx +132 -0
- package/src/hooks/useRightAxis.ts +19 -26
- package/src/hooks/useTooltip.tsx +14 -10
- package/src/store/chart.reducer.ts +0 -1
- 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
|
|
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 =
|
|
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 ?
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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 =
|
|
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[] =
|
|
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 ||
|
|
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
|
-
{
|
|
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
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
<Tooltip
|
|
3467
|
-
<
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
<
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
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'
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
|
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:
|
|
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 ||
|
|
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 ??
|
|
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
|
-
|
|
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
|
|
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(
|
|
35
|
-
const seriesKeys: string[] =
|
|
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,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
|
-
{(
|
|
119
|
-
<div className={
|
|
120
|
-
{
|
|
121
|
-
{
|
|
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
|
-
<
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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)}
|
|
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)}
|
|
82
|
+
<div style={getStyle(pd.displayGray)} className={baseClass}>
|
|
83
83
|
<span>{pd.lineCode}</span>
|
|
84
84
|
<p>{pd.label}</p>
|
|
85
85
|
</div>
|