@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
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdc/chart",
|
|
3
|
-
"version": "4.26.
|
|
3
|
+
"version": "4.26.7",
|
|
4
4
|
"description": "React component for visualizing tabular data in various types of charts",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Matthew Pallansch <mpallansch@adittech.com>",
|
|
7
7
|
"bugs": "https://github.com/CDCgov/cdc-open-viz/issues",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@cdc/core": "^4.26.
|
|
9
|
+
"@cdc/core": "^4.26.7",
|
|
10
10
|
"@hello-pangea/dnd": "^16.2.0",
|
|
11
11
|
"@react-spring/web": "^9.7.5",
|
|
12
12
|
"@rollup/plugin-dsv": "^3.0.2",
|
|
@@ -14,14 +14,11 @@
|
|
|
14
14
|
"@visx/axis": "^3.12.0",
|
|
15
15
|
"@visx/brush": "^3.12.0",
|
|
16
16
|
"@visx/curve": "^3.12.0",
|
|
17
|
-
"@visx/drag": "^3.12.0",
|
|
18
17
|
"@visx/event": "^3.12.0",
|
|
19
18
|
"@visx/glyph": "^3.12.0",
|
|
20
|
-
"@visx/gradient": "^3.12.0",
|
|
21
19
|
"@visx/heatmap": "^3.12.0",
|
|
22
20
|
"@visx/legend": "^3.12.0",
|
|
23
21
|
"@visx/marker": "^3.12.0",
|
|
24
|
-
"@visx/mock-data": "^3.12.0",
|
|
25
22
|
"@visx/pattern": "^3.12.0",
|
|
26
23
|
"@visx/responsive": "^3.12.0",
|
|
27
24
|
"@visx/scale": "^3.12.0",
|
|
@@ -32,20 +29,16 @@
|
|
|
32
29
|
"@vitejs/plugin-react": "^5.1.2",
|
|
33
30
|
"chroma-js": "^3.1.2",
|
|
34
31
|
"d3-array": "^3.2.4",
|
|
35
|
-
"d3-format": "^3.1.2",
|
|
36
32
|
"d3-sankey": "^0.12.3",
|
|
37
33
|
"d3-time-format": "^4.1.0",
|
|
38
34
|
"dompurify": "^3.4.0",
|
|
39
35
|
"html-react-parser": "^5.2.3",
|
|
40
|
-
"js-base64": "^2.5.2",
|
|
41
36
|
"lodash": "^4.17.23",
|
|
42
|
-
"papaparse": "^5.5.2",
|
|
43
37
|
"react-accessible-accordion": "^5.0.1",
|
|
44
38
|
"react-icons": "^5.5.0",
|
|
45
39
|
"react-tooltip": "5.8.2-beta.3",
|
|
46
40
|
"resize-observer-polyfill": "^1.5.1",
|
|
47
41
|
"sass": "^1.89.2",
|
|
48
|
-
"use-debounce": "^10.1.0",
|
|
49
42
|
"vite": "^7.3.1",
|
|
50
43
|
"vite-plugin-css-injected-by-js": "^2.4.0",
|
|
51
44
|
"vite-plugin-svgr": "^4.2.0",
|
|
@@ -53,11 +46,10 @@
|
|
|
53
46
|
},
|
|
54
47
|
"devDependencies": {
|
|
55
48
|
"@types/d3-array": "^3.2.1",
|
|
56
|
-
"@types/d3-format": "^3.0.4",
|
|
57
49
|
"@types/d3-sankey": "^0.12.4",
|
|
58
50
|
"@types/d3-time-format": "^4.0.3"
|
|
59
51
|
},
|
|
60
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "ca89fe0eb083a52a425fa5971bbe74716b5bb0f9",
|
|
61
53
|
"main": "dist/cdcchart",
|
|
62
54
|
"moduleName": "CdcChart",
|
|
63
55
|
"peerDependencies": {
|
package/src/CdcChart.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import { parseCsvWithQuotes } from '@cdc/core/helpers/parseCsvWithQuotes'
|
|
|
7
7
|
import { addCsvCacheBuster } from '@cdc/core/helpers/csvCacheBuster'
|
|
8
8
|
import { extractDataAndMetadata } from '@cdc/core/helpers/extractDataAndMetadata'
|
|
9
9
|
import Loading from '@cdc/core/components/Loading'
|
|
10
|
-
import
|
|
10
|
+
import isEqual from 'lodash/isEqual'
|
|
11
11
|
import EditorContext from '@cdc/core/contexts/EditorContext'
|
|
12
12
|
interface CdcChartProps {
|
|
13
13
|
configUrl?: string
|
|
@@ -90,7 +90,7 @@ const CdcChartWrapper: React.FC<CdcChartProps> = ({
|
|
|
90
90
|
}, [configUrl, loadConfig])
|
|
91
91
|
|
|
92
92
|
useEffect(() => {
|
|
93
|
-
if (!
|
|
93
|
+
if (!isEqual(prevFiltersRef.current, config.filters)) {
|
|
94
94
|
prevFiltersRef.current = config.filters
|
|
95
95
|
reloadFilteredData()
|
|
96
96
|
}
|
|
@@ -101,6 +101,7 @@ import { prepareSmallMultiplesDataTable } from './helpers/smallMultiplesHelpers'
|
|
|
101
101
|
import { calcInitialHeight } from './helpers/sizeHelpers'
|
|
102
102
|
import { ensureSpecialChartAxisTypes } from './helpers/ensureSpecialChartAxisTypes'
|
|
103
103
|
import { findColumnConfigByName } from './helpers/seriesColumnSettings'
|
|
104
|
+
import { sortByCategoryOrder } from './helpers/categoryOrder'
|
|
104
105
|
|
|
105
106
|
// styles
|
|
106
107
|
import './scss/main.scss'
|
|
@@ -231,6 +232,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
|
|
|
231
232
|
config.enableMarkupVariables,
|
|
232
233
|
config.markupVariables,
|
|
233
234
|
config.data,
|
|
235
|
+
config.dataMetadata,
|
|
234
236
|
config.filters,
|
|
235
237
|
title,
|
|
236
238
|
config.superTitle,
|
|
@@ -263,14 +265,30 @@ const CdcChart: React.FC<CdcChartProps> = ({
|
|
|
263
265
|
const legendId = useId()
|
|
264
266
|
|
|
265
267
|
const convertLineToBarGraph = isConvertLineToBarGraph(config, filteredData)
|
|
268
|
+
const xAxisDataKey = config.xAxis?.dataKey
|
|
269
|
+
const xAxisType = config.xAxis?.type
|
|
270
|
+
const xAxisCategoryOrderType = config.xAxis?.categoryOrderType
|
|
271
|
+
const xAxisCategoryOrder = config.xAxis?.categoryOrder
|
|
272
|
+
const categoryOrderConfig = useMemo(
|
|
273
|
+
() =>
|
|
274
|
+
({
|
|
275
|
+
xAxis: {
|
|
276
|
+
dataKey: xAxisDataKey,
|
|
277
|
+
type: xAxisType,
|
|
278
|
+
categoryOrderType: xAxisCategoryOrderType,
|
|
279
|
+
categoryOrder: xAxisCategoryOrder
|
|
280
|
+
}
|
|
281
|
+
} as Pick<ChartConfig, 'xAxis'>),
|
|
282
|
+
[xAxisDataKey, xAxisType, xAxisCategoryOrderType, xAxisCategoryOrder]
|
|
283
|
+
)
|
|
266
284
|
|
|
267
285
|
// Declaratively calculate series keys for pie charts based on filtered data
|
|
268
286
|
const pieSeriesKeys = useMemo(() => {
|
|
269
|
-
if (
|
|
287
|
+
if (visualizationType !== 'Pie' || !xAxisDataKey) return null
|
|
270
288
|
const data = filteredData?.length > 0 ? filteredData : excludedData
|
|
271
289
|
if (!data) return null
|
|
272
|
-
return uniq(data.map(d => d[
|
|
273
|
-
}, [
|
|
290
|
+
return uniq(sortByCategoryOrder(data, categoryOrderConfig).map(d => d[xAxisDataKey]))
|
|
291
|
+
}, [visualizationType, xAxisDataKey, categoryOrderConfig, filteredData, excludedData])
|
|
274
292
|
|
|
275
293
|
const prepareConfig = (loadedConfig: ChartConfig) => {
|
|
276
294
|
// Create defaults without version to avoid overriding legacy configs
|
|
@@ -458,13 +476,15 @@ const CdcChart: React.FC<CdcChartProps> = ({
|
|
|
458
476
|
if (newConfig.visualizationType === 'Pie') {
|
|
459
477
|
// Use the same data that will be passed to PieChart (after exclusions and filters)
|
|
460
478
|
const pieData = currentData.length > 0 ? currentData : newExcludedData
|
|
461
|
-
newConfig.runtime.seriesKeys = uniq(pieData.map(d => d[newConfig.xAxis.dataKey]))
|
|
479
|
+
newConfig.runtime.seriesKeys = uniq(sortByCategoryOrder(pieData, newConfig).map(d => d[newConfig.xAxis.dataKey]))
|
|
462
480
|
newConfig.runtime.seriesLabelsAll = newConfig.runtime.seriesKeys
|
|
463
481
|
newConfig.runtime.isPieChart = true // Flag to know when to use derived keys
|
|
464
482
|
} else if (newConfig.visualizationType === 'Radar') {
|
|
465
483
|
// Radar chart: seriesKeys are the entity names from xAxis.dataKey
|
|
466
484
|
const radarData = currentData.length > 0 ? currentData : newExcludedData
|
|
467
|
-
newConfig.runtime.seriesKeys = uniq(
|
|
485
|
+
newConfig.runtime.seriesKeys = uniq(
|
|
486
|
+
sortByCategoryOrder(radarData, newConfig).map(d => d[newConfig.xAxis.dataKey])
|
|
487
|
+
)
|
|
468
488
|
newConfig.runtime.seriesLabelsAll = newConfig.runtime.seriesKeys
|
|
469
489
|
} else {
|
|
470
490
|
const finalData = dataOverride || newConfig.formattedData || newConfig.data
|
|
@@ -777,6 +797,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
|
|
|
777
797
|
}
|
|
778
798
|
|
|
779
799
|
const configDataDependency = configObj?.data?.length ? configObj.data : null
|
|
800
|
+
const configDataMetadataDependency = configObj?.dataMetadata ? JSON.stringify(configObj.dataMetadata) : null
|
|
780
801
|
const configExclusionsDependency = [
|
|
781
802
|
configObj?.xAxis?.type,
|
|
782
803
|
configObj?.xAxis?.dataKey,
|
|
@@ -807,7 +828,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
|
|
|
807
828
|
}
|
|
808
829
|
|
|
809
830
|
load()
|
|
810
|
-
}, [configDataDependency, configExclusionsDependency])
|
|
831
|
+
}, [configDataDependency, configDataMetadataDependency, configExclusionsDependency])
|
|
811
832
|
|
|
812
833
|
/**
|
|
813
834
|
* When cove has a config and container ref publish the cove_loaded event.
|
|
@@ -1058,6 +1079,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
|
|
|
1058
1079
|
rightSuffix,
|
|
1059
1080
|
bottomPrefix,
|
|
1060
1081
|
bottomSuffix,
|
|
1082
|
+
bottomCommas,
|
|
1061
1083
|
bottomAbbreviated,
|
|
1062
1084
|
preserveOriginalDecimals
|
|
1063
1085
|
}
|
|
@@ -1153,7 +1175,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
|
|
|
1153
1175
|
|
|
1154
1176
|
if (
|
|
1155
1177
|
(axis === 'left' && commas && abbreviated && shouldAbbreviate) ||
|
|
1156
|
-
(axis === 'bottom' &&
|
|
1178
|
+
(axis === 'bottom' && bottomCommas && bottomAbbreviated && shouldAbbreviate)
|
|
1157
1179
|
) {
|
|
1158
1180
|
num = num // eslint-disable-line
|
|
1159
1181
|
} else {
|
|
@@ -1243,9 +1265,14 @@ const CdcChart: React.FC<CdcChartProps> = ({
|
|
|
1243
1265
|
return config?.xAxis?.dataKey ? transform.cleanData(data, excludedKey, keysToClean) : data
|
|
1244
1266
|
}
|
|
1245
1267
|
|
|
1268
|
+
const orderedTableData = useMemo(
|
|
1269
|
+
() => sortByCategoryOrder(filteredData || excludedData, categoryOrderConfig),
|
|
1270
|
+
[filteredData, excludedData, categoryOrderConfig]
|
|
1271
|
+
)
|
|
1272
|
+
|
|
1246
1273
|
const getTableRuntimeData = () => {
|
|
1247
1274
|
if (visualizationType === 'Sankey') return config?.data?.[0]?.tableData
|
|
1248
|
-
const data =
|
|
1275
|
+
const data = orderedTableData
|
|
1249
1276
|
if (config.visualizationType === 'Pie' && !config.dataFormat?.showPiePercent) {
|
|
1250
1277
|
return getPiePercent(data, config?.yAxis?.dataKey)
|
|
1251
1278
|
}
|
|
@@ -1271,7 +1298,10 @@ const CdcChart: React.FC<CdcChartProps> = ({
|
|
|
1271
1298
|
}
|
|
1272
1299
|
|
|
1273
1300
|
// Transform and clean data for chart rendering
|
|
1274
|
-
const transformedData =
|
|
1301
|
+
const transformedData = sortByCategoryOrder(
|
|
1302
|
+
getTransformedData({ brushData: state.brushData, filteredData, excludedData, clean }),
|
|
1303
|
+
config
|
|
1304
|
+
)
|
|
1275
1305
|
const configYAxisDomainData = (config as ChartConfig).yAxisDomainData
|
|
1276
1306
|
const yAxisDomainData = useMemo(() => {
|
|
1277
1307
|
if (Array.isArray(configYAxisDomainData) && configYAxisDomainData.length > 0) {
|
|
@@ -1723,7 +1753,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
|
|
|
1723
1753
|
setSharedFilter,
|
|
1724
1754
|
setSharedFilterValue,
|
|
1725
1755
|
svgRef,
|
|
1726
|
-
tableData:
|
|
1756
|
+
tableData: orderedTableData,
|
|
1727
1757
|
transformedData,
|
|
1728
1758
|
twoColorPalette,
|
|
1729
1759
|
unfilteredData: stateData,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { expect } from 'storybook/test'
|
|
3
|
+
import Chart from '../CdcChart'
|
|
4
|
+
import pieConfig from './_mock/pie_with_data.json'
|
|
5
|
+
import { editConfigKeys } from '@cdc/core/helpers/configHelpers'
|
|
6
|
+
import { assertVisualizationRendered, waitForPresence } from '@cdc/core/helpers/testing'
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof Chart> = {
|
|
9
|
+
title: 'Components/Templates/Chart/Pie Labels',
|
|
10
|
+
component: Chart
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Story = StoryObj<typeof Chart>
|
|
14
|
+
|
|
15
|
+
export const CategoryPercentageLabels: Story = {
|
|
16
|
+
args: {
|
|
17
|
+
config: editConfigKeys(pieConfig, [
|
|
18
|
+
{ path: ['labels'], value: true },
|
|
19
|
+
{ path: ['legend', 'hide'], value: true }
|
|
20
|
+
]),
|
|
21
|
+
isEditor: true
|
|
22
|
+
},
|
|
23
|
+
play: async ({ canvasElement }) => {
|
|
24
|
+
await assertVisualizationRendered(canvasElement)
|
|
25
|
+
await waitForPresence('.pie-label', canvasElement)
|
|
26
|
+
expect(canvasElement.querySelector('line')).toBeNull()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const DonutCategoryPercentageLabels: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
config: editConfigKeys(pieConfig, [
|
|
33
|
+
{ path: ['labels'], value: true },
|
|
34
|
+
{ path: ['legend', 'hide'], value: true },
|
|
35
|
+
{ path: ['pieType'], value: 'Donut' }
|
|
36
|
+
]),
|
|
37
|
+
isEditor: true
|
|
38
|
+
},
|
|
39
|
+
play: async ({ canvasElement }) => {
|
|
40
|
+
await assertVisualizationRendered(canvasElement)
|
|
41
|
+
await waitForPresence('.pie-label--outside', canvasElement)
|
|
42
|
+
expect(canvasElement.querySelector('.pie-label--inside')).toBeNull()
|
|
43
|
+
expect(canvasElement.querySelector('line')).toBeNull()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default meta
|
|
@@ -26,6 +26,19 @@ const meta: Meta<typeof Chart> = {
|
|
|
26
26
|
|
|
27
27
|
type Story = StoryObj<typeof Chart>
|
|
28
28
|
|
|
29
|
+
const multipleLinesStoryData = [
|
|
30
|
+
{ week_end: '2024-01-06', Adults: 45.1, Children: 29.3, pathogen: 'COVID-19' },
|
|
31
|
+
{ week_end: '2024-01-13', Adults: 46.7, Children: 30.2, pathogen: 'COVID-19' },
|
|
32
|
+
{ week_end: '2024-01-20', Adults: 48.0, Children: 31.0, pathogen: 'COVID-19' }
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
const multipleLinesConfig = editConfigKeys(multipleLines, [
|
|
36
|
+
{ path: ['tooltips', 'dateDisplayFormat'], value: '%b. %d %Y' },
|
|
37
|
+
{ path: ['formattedData'], value: multipleLinesStoryData },
|
|
38
|
+
{ path: ['dataUrl'], value: undefined },
|
|
39
|
+
{ path: ['runtimeDataUrl'], value: undefined }
|
|
40
|
+
])
|
|
41
|
+
|
|
29
42
|
export const line_Chart_Two_Points_Regression_Test: Story = {
|
|
30
43
|
args: {
|
|
31
44
|
config: lineChartTwoPointsRegressionTest,
|
|
@@ -47,7 +60,7 @@ export const line_Chart_Two_Points_New_Chart: Story = {
|
|
|
47
60
|
|
|
48
61
|
export const multiple_lines: Story = {
|
|
49
62
|
args: {
|
|
50
|
-
config:
|
|
63
|
+
config: multipleLinesConfig
|
|
51
64
|
},
|
|
52
65
|
play: async ({ canvasElement }) => {
|
|
53
66
|
await assertVisualizationRendered(canvasElement)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { expect } from 'storybook/test'
|
|
2
3
|
import barConfig from './_mock/line_chart_two_points_new_chart.json'
|
|
3
4
|
import annotationConfig from './_mock/annotation_category_mock.json'
|
|
4
5
|
import areaPrefix from './_mock/annotation_category_mock.json'
|
|
@@ -7,7 +8,7 @@ import scatterPlotConfig from './_mock/scatterplot_mock.json'
|
|
|
7
8
|
|
|
8
9
|
import Chart from '../CdcChartComponent'
|
|
9
10
|
import { editConfigKeys } from '@cdc/core/helpers/configHelpers'
|
|
10
|
-
import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
|
|
11
|
+
import { assertVisualizationRendered, waitForPresence } from '@cdc/core/helpers/testing'
|
|
11
12
|
|
|
12
13
|
const meta: Meta<typeof Chart> = {
|
|
13
14
|
title: 'Components/Templates/Chart/Prefix Suffix',
|
|
@@ -16,6 +17,11 @@ const meta: Meta<typeof Chart> = {
|
|
|
16
17
|
|
|
17
18
|
type Story = StoryObj<typeof Chart>
|
|
18
19
|
|
|
20
|
+
const getSvgTextValues = (root: ParentNode | null, selector: string = 'text') => {
|
|
21
|
+
if (!root) return []
|
|
22
|
+
return Array.from(root.querySelectorAll(selector)).map(node => node.textContent?.trim() || '')
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
export const Inline_Label: Story = {
|
|
20
26
|
args: {
|
|
21
27
|
config: editConfigKeys(barConfig, [
|
|
@@ -198,4 +204,25 @@ export const ScatterPlot_Bottom_Commas: Story = {
|
|
|
198
204
|
}
|
|
199
205
|
}
|
|
200
206
|
|
|
207
|
+
export const ScatterPlot_Bottom_Commas_And_Abbreviated: Story = {
|
|
208
|
+
args: {
|
|
209
|
+
config: editConfigKeys(scatterPlotConfig, [
|
|
210
|
+
{ path: ['dataFormat', 'bottomCommas'], value: true },
|
|
211
|
+
{ path: ['dataFormat', 'bottomAbbreviated'], value: true },
|
|
212
|
+
{ path: ['dataFormat', 'bottomPrefix'], value: '$' },
|
|
213
|
+
{ path: ['dataFormat', 'commas'], value: false },
|
|
214
|
+
{ path: ['dataFormat', 'abbreviated'], value: false }
|
|
215
|
+
])
|
|
216
|
+
},
|
|
217
|
+
play: async ({ canvasElement }) => {
|
|
218
|
+
await assertVisualizationRendered(canvasElement)
|
|
219
|
+
await waitForPresence('svg g.bottom-axis', canvasElement)
|
|
220
|
+
|
|
221
|
+
const svgText = getSvgTextValues(canvasElement, 'svg g.bottom-axis text')
|
|
222
|
+
const hasBottomAxisCurrencyAbbreviation = svgText.some(text => /^\$\d+(\.\d+)?K$/.test(text))
|
|
223
|
+
|
|
224
|
+
expect(hasBottomAxisCurrencyAbbreviation).toBe(true)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
201
228
|
export default meta
|
|
@@ -21,7 +21,6 @@ import isNumber from '@cdc/core/helpers/isNumber'
|
|
|
21
21
|
// Local context and types
|
|
22
22
|
import BarChartContext, { BarChartContextValues } from './context'
|
|
23
23
|
import { ChartContext } from '../../../types/ChartContext'
|
|
24
|
-
import _ from 'lodash'
|
|
25
24
|
import { getBarData } from '../helpers/getBarData'
|
|
26
25
|
import { getHorizontalBarHeights } from '../helpers/getBarHeights'
|
|
27
26
|
import { getPatternUrl as getPatternUrlForBar } from '../helpers/getPatternUrl'
|
|
@@ -76,7 +75,7 @@ const BarChartHorizontal = () => {
|
|
|
76
75
|
<defs>
|
|
77
76
|
{Object.entries(config.legend.patterns).map(([key, pattern]) => {
|
|
78
77
|
const patternId = getChartPatternId(key)
|
|
79
|
-
const size = pattern.patternSize ||
|
|
78
|
+
const size = pattern.patternSize || 10
|
|
80
79
|
|
|
81
80
|
switch (pattern.shape) {
|
|
82
81
|
case 'circles':
|
|
@@ -59,7 +59,7 @@ const BarChartStackedHorizontal = () => {
|
|
|
59
59
|
<defs>
|
|
60
60
|
{Object.entries(config.legend.patterns).map(([key, pattern]) => {
|
|
61
61
|
const patternId = getChartPatternId(key)
|
|
62
|
-
const size = pattern.patternSize ||
|
|
62
|
+
const size = pattern.patternSize || 10
|
|
63
63
|
|
|
64
64
|
switch (pattern.shape) {
|
|
65
65
|
case 'circles':
|
|
@@ -44,7 +44,7 @@ const BarChartStackedVertical = () => {
|
|
|
44
44
|
<defs>
|
|
45
45
|
{Object.entries(config.legend.patterns).map(([key, pattern]) => {
|
|
46
46
|
const patternId = getChartPatternId(key)
|
|
47
|
-
const size = pattern.patternSize ||
|
|
47
|
+
const size = pattern.patternSize || 10
|
|
48
48
|
|
|
49
49
|
switch (pattern.shape) {
|
|
50
50
|
case 'circles':
|
|
@@ -19,7 +19,6 @@ import createBarElement from '@cdc/core/components/createBarElement'
|
|
|
19
19
|
import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
|
|
20
20
|
// Types
|
|
21
21
|
import { type ChartContext } from '../../../types/ChartContext'
|
|
22
|
-
import _ from 'lodash'
|
|
23
22
|
import { getBarData } from '../helpers/getBarData'
|
|
24
23
|
import { getPatternUrl as getPatternUrlForBar } from '../helpers/getPatternUrl'
|
|
25
24
|
import { getChartPatternId } from '../../../helpers/getChartPatternId'
|
|
@@ -90,7 +89,7 @@ const BarChartVertical = () => {
|
|
|
90
89
|
<defs>
|
|
91
90
|
{Object.entries(config.legend.patterns).map(([key, pattern]) => {
|
|
92
91
|
const patternId = getChartPatternId(key)
|
|
93
|
-
const size = pattern.patternSize ||
|
|
92
|
+
const size = pattern.patternSize || 10
|
|
94
93
|
|
|
95
94
|
switch (pattern.shape) {
|
|
96
95
|
case 'circles':
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { TransformedData } from '../../../types/ChartContext'
|
|
3
|
-
import { ChartConfig } from '../../../types/ChartConfig'
|
|
4
|
-
|
|
5
|
-
export const getBarData = (config: ChartConfig, data: TransformedData[], hasConfidenceInterval: boolean) => {
|
|
6
|
-
const dynamicSeries = config.series.find(s => s.dynamicCategory)
|
|
7
|
-
if (!dynamicSeries) return data
|
|
8
|
-
const { dynamicCategory, dataKey } = dynamicSeries
|
|
9
|
-
const xAxisKey = config.runtime.originalXAxis.dataKey
|
|
10
|
-
const xAxisGroupDataLookup =
|
|
11
|
-
return Object.values(xAxisGroupDataLookup).map(group => {
|
|
12
|
-
return group.reduce((acc, datum) => {
|
|
13
|
-
const dataValue = datum[dataKey]
|
|
14
|
-
const dataCategory = datum[dynamicCategory]
|
|
15
|
-
if (hasConfidenceInterval) {
|
|
16
|
-
const { lower, upper } = config.confidenceKeys
|
|
17
|
-
if (!acc.CI) acc.CI = {}
|
|
18
|
-
const lowerValue = datum[lower]
|
|
19
|
-
const upperValue = datum[upper]
|
|
20
|
-
acc.CI[dataCategory] = { lower: lowerValue, upper: upperValue }
|
|
21
|
-
}
|
|
22
|
-
acc[dataCategory] = dataValue
|
|
23
|
-
acc[xAxisKey] = datum[xAxisKey]
|
|
24
|
-
acc.dynamicData = true
|
|
25
|
-
return acc
|
|
26
|
-
}, {})
|
|
27
|
-
})
|
|
28
|
-
}
|
|
1
|
+
import groupBy from 'lodash/groupBy'
|
|
2
|
+
import { TransformedData } from '../../../types/ChartContext'
|
|
3
|
+
import { ChartConfig } from '../../../types/ChartConfig'
|
|
4
|
+
|
|
5
|
+
export const getBarData = (config: ChartConfig, data: TransformedData[], hasConfidenceInterval: boolean) => {
|
|
6
|
+
const dynamicSeries = config.series.find(s => s.dynamicCategory)
|
|
7
|
+
if (!dynamicSeries) return data
|
|
8
|
+
const { dynamicCategory, dataKey } = dynamicSeries
|
|
9
|
+
const xAxisKey = config.runtime.originalXAxis.dataKey
|
|
10
|
+
const xAxisGroupDataLookup = groupBy(data, xAxisKey)
|
|
11
|
+
return Object.values(xAxisGroupDataLookup).map(group => {
|
|
12
|
+
return group.reduce((acc, datum) => {
|
|
13
|
+
const dataValue = datum[dataKey]
|
|
14
|
+
const dataCategory = datum[dynamicCategory]
|
|
15
|
+
if (hasConfidenceInterval) {
|
|
16
|
+
const { lower, upper } = config.confidenceKeys
|
|
17
|
+
if (!acc.CI) acc.CI = {}
|
|
18
|
+
const lowerValue = datum[lower]
|
|
19
|
+
const upperValue = datum[upper]
|
|
20
|
+
acc.CI[dataCategory] = { lower: lowerValue, upper: upperValue }
|
|
21
|
+
}
|
|
22
|
+
acc[dataCategory] = dataValue
|
|
23
|
+
acc[xAxisKey] = datum[xAxisKey]
|
|
24
|
+
acc.dynamicData = true
|
|
25
|
+
return acc
|
|
26
|
+
}, {})
|
|
27
|
+
})
|
|
28
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { APP_FONT_SIZE } from '@cdc/core/helpers/constants'
|
|
2
|
-
import
|
|
2
|
+
import round from 'lodash/round'
|
|
3
3
|
import { ChartConfig } from '../../../types/ChartConfig'
|
|
4
4
|
|
|
5
5
|
export const getHorizontalBarHeights = <T>(config: Partial<ChartConfig>, bars: (T & { index })[]) => {
|
|
@@ -41,7 +41,7 @@ export const getHorizontalBarHeights = <T>(config: Partial<ChartConfig>, bars: (
|
|
|
41
41
|
// return new updated bars/groupes
|
|
42
42
|
return bars.map((bar, i) => {
|
|
43
43
|
// set bars Y dynamically to handle space between bars
|
|
44
|
-
const y = bar.index === 0 ? 0 :
|
|
44
|
+
const y = bar.index === 0 ? 0 : round(totalBarHeight * i)
|
|
45
45
|
return { ...bar, y, height: barHeight }
|
|
46
46
|
})
|
|
47
47
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { createMockChartContext, createMockConfig } from '../../../LinearChart/tests/mockConfigContext'
|
|
4
|
+
import { useBarChart } from '../useBarChart'
|
|
5
|
+
|
|
6
|
+
const baseConfig = createMockConfig()
|
|
7
|
+
|
|
8
|
+
const makeConfigContext = (countColumnOverrides: Record<string, unknown> = {}) => {
|
|
9
|
+
const config = createMockConfig({
|
|
10
|
+
visualizationType: 'Bar',
|
|
11
|
+
orientation: 'horizontal',
|
|
12
|
+
barHeight: 25,
|
|
13
|
+
xAxis: {
|
|
14
|
+
...baseConfig.xAxis,
|
|
15
|
+
type: 'categorical',
|
|
16
|
+
dataKey: 'Cause of death'
|
|
17
|
+
},
|
|
18
|
+
yAxis: {
|
|
19
|
+
...baseConfig.yAxis,
|
|
20
|
+
labelPlacement: 'Below Bar'
|
|
21
|
+
},
|
|
22
|
+
series: [{ dataKey: 'Percentage', type: 'Bar', axis: 'Left', tooltip: true }] as any,
|
|
23
|
+
columns: {
|
|
24
|
+
Count: {
|
|
25
|
+
name: 'Count',
|
|
26
|
+
label: 'Count',
|
|
27
|
+
prefix: '',
|
|
28
|
+
suffix: '',
|
|
29
|
+
roundToPlace: 0,
|
|
30
|
+
commas: true,
|
|
31
|
+
dataTable: true,
|
|
32
|
+
tooltips: true,
|
|
33
|
+
...countColumnOverrides
|
|
34
|
+
}
|
|
35
|
+
} as any,
|
|
36
|
+
dataFormat: {
|
|
37
|
+
abbreviated: false,
|
|
38
|
+
bottomAbbreviated: false,
|
|
39
|
+
bottomPrefix: '',
|
|
40
|
+
bottomRoundTo: 0,
|
|
41
|
+
bottomSuffix: '',
|
|
42
|
+
bottomCommas: false,
|
|
43
|
+
commas: false,
|
|
44
|
+
prefix: '',
|
|
45
|
+
preserveOriginalDecimals: false,
|
|
46
|
+
rightPrefix: '',
|
|
47
|
+
rightRoundTo: 0,
|
|
48
|
+
rightSuffix: '',
|
|
49
|
+
roundTo: 1,
|
|
50
|
+
suffix: '%'
|
|
51
|
+
} as any,
|
|
52
|
+
runtime: {
|
|
53
|
+
...baseConfig.runtime,
|
|
54
|
+
xAxis: {
|
|
55
|
+
...baseConfig.runtime.xAxis,
|
|
56
|
+
type: 'categorical',
|
|
57
|
+
dataKey: 'Cause of death'
|
|
58
|
+
},
|
|
59
|
+
yAxis: {
|
|
60
|
+
...baseConfig.runtime.yAxis,
|
|
61
|
+
dataKey: 'Percentage'
|
|
62
|
+
},
|
|
63
|
+
series: [{ dataKey: 'Percentage', type: 'Bar', axis: 'Left', tooltip: true }],
|
|
64
|
+
seriesKeys: ['Percentage'],
|
|
65
|
+
seriesLabels: { Percentage: 'Percentage' },
|
|
66
|
+
seriesLabelsAll: ['Percentage']
|
|
67
|
+
} as any
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return createMockChartContext(config, {
|
|
71
|
+
tableData: [
|
|
72
|
+
{
|
|
73
|
+
'Cause of death': 'Cardiovascular conditions',
|
|
74
|
+
Percentage: 22,
|
|
75
|
+
Count: 146
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
updateConfig: vi.fn()
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe('useBarChart', () => {
|
|
83
|
+
it('does not apply the left-axis suffix to an additional single-series tooltip column', () => {
|
|
84
|
+
const configContext = makeConfigContext()
|
|
85
|
+
const { result } = renderHook(() => useBarChart(vi.fn(), vi.fn(), configContext))
|
|
86
|
+
|
|
87
|
+
expect(result.current.getAdditionalColumn('Percentage', 'Cardiovascular conditions')).toBe('Count : 146 <br/>')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('applies an additional column suffix when one is configured', () => {
|
|
91
|
+
const configContext = makeConfigContext({ suffix: ' deaths' })
|
|
92
|
+
const { result } = renderHook(() => useBarChart(vi.fn(), vi.fn(), configContext))
|
|
93
|
+
|
|
94
|
+
expect(result.current.getAdditionalColumn('Percentage', 'Cardiovascular conditions')).toBe(
|
|
95
|
+
'Count : 146 deaths <br/>'
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -6,7 +6,7 @@ import { getPaletteColors } from '@cdc/core/helpers/palettes/utils'
|
|
|
6
6
|
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
7
7
|
import { getVizSubType, getVizTitle } from '@cdc/core/helpers/metrics/utils'
|
|
8
8
|
import { isMobileFontViewport } from '@cdc/core/helpers/viewports'
|
|
9
|
-
import { getSeriesOwnedColumnNames } from '../../../helpers/seriesColumnSettings'
|
|
9
|
+
import { getSeriesColumnFormattingParams, getSeriesOwnedColumnNames } from '../../../helpers/seriesColumnSettings'
|
|
10
10
|
|
|
11
11
|
export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, configContext) => {
|
|
12
12
|
const {
|
|
@@ -184,10 +184,9 @@ export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, confi
|
|
|
184
184
|
if (seriesOwnedColumnNames.includes(colConfig.name || colKeys)) return
|
|
185
185
|
if (series && colConfig.series && colConfig.series !== series && !colConfig.tooltips) return
|
|
186
186
|
const formattingParams = {
|
|
187
|
-
addColPrefix:
|
|
188
|
-
addColSuffix:
|
|
189
|
-
|
|
190
|
-
addColCommas: config.columns[colKeys].commas
|
|
187
|
+
addColPrefix: '',
|
|
188
|
+
addColSuffix: '',
|
|
189
|
+
...getSeriesColumnFormattingParams(config.columns[colKeys])
|
|
191
190
|
}
|
|
192
191
|
|
|
193
192
|
const formattedValue = formatColNumber(
|
|
@@ -5,7 +5,8 @@ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
|
5
5
|
import ConfigContext from '../../ConfigContext'
|
|
6
6
|
import { handleTooltip, createPlots } from './helpers/index'
|
|
7
7
|
import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
|
|
8
|
-
import
|
|
8
|
+
import map from 'lodash/map'
|
|
9
|
+
import round from 'lodash/round'
|
|
9
10
|
|
|
10
11
|
const BoxPlotHorizontal = ({ xScale, yScale, seriesScale }) => {
|
|
11
12
|
const { config, transformedData: data, colorScale, seriesHighlight } = useContext(ConfigContext)
|
|
@@ -63,7 +64,7 @@ const BoxPlotHorizontal = ({ xScale, yScale, seriesScale }) => {
|
|
|
63
64
|
maxProps={{ stroke: defaultColor, strokeWidth: 1, opacity: fillOpacity }}
|
|
64
65
|
medianProps={{ stroke: defaultColor, strokeWidth: 1, opacity: fillOpacity }}
|
|
65
66
|
outliers={
|
|
66
|
-
config.boxplot.plotOutlierValues ?
|
|
67
|
+
config.boxplot.plotOutlierValues ? map(plot.columnOutliers[item.dataKey], item => item) : []
|
|
67
68
|
}
|
|
68
69
|
outlierProps={{
|
|
69
70
|
style: {
|
|
@@ -78,10 +79,10 @@ const BoxPlotHorizontal = ({ xScale, yScale, seriesScale }) => {
|
|
|
78
79
|
config.boxplot,
|
|
79
80
|
plot.columnCategory,
|
|
80
81
|
item.dataKey,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
round(plot.q1[item.dataKey], config.dataFormat.roundTo),
|
|
83
|
+
round(plot.q3[item.dataKey], config.dataFormat.roundTo),
|
|
84
|
+
round(plot.median[item.dataKey], config.dataFormat.roundTo),
|
|
85
|
+
round(plot.iqr[item.dataKey], config.dataFormat.roundTo),
|
|
85
86
|
config.xAxis.label,
|
|
86
87
|
defaultColor
|
|
87
88
|
),
|