@cdc/chart 4.25.8 → 4.25.10

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 (89) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/dist/cdcchart.js +37524 -35243
  3. package/examples/feature/__data__/planet-example-data.json +0 -30
  4. package/examples/grouped-bar-test.json +400 -0
  5. package/examples/private/d.json +382 -0
  6. package/examples/private/example-2.json +49784 -0
  7. package/examples/private/f2.json +1 -0
  8. package/examples/private/f4.json +1577 -0
  9. package/examples/private/forecast.json +1180 -0
  10. package/examples/private/lollipop.json +468 -0
  11. package/examples/private/new.json +48756 -0
  12. package/examples/private/pie-chart-legend.json +904 -0
  13. package/examples/suppressed_tooltip.json +480 -0
  14. package/index.html +10 -22
  15. package/package.json +25 -7
  16. package/src/CdcChart.tsx +1 -2
  17. package/src/CdcChartComponent.tsx +174 -32
  18. package/src/_stories/Chart.Anchors.stories.tsx +2 -2
  19. package/src/_stories/Chart.BoxPlot.stories.tsx +1 -1
  20. package/src/_stories/Chart.CI.stories.tsx +1 -1
  21. package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
  22. package/src/_stories/Chart.DynamicSeries.stories.tsx +2 -2
  23. package/src/_stories/Chart.Filters.stories.tsx +2 -2
  24. package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
  25. package/src/_stories/Chart.Patterns.stories.tsx +19 -0
  26. package/src/_stories/Chart.ScatterPlot.stories.tsx +1 -1
  27. package/src/_stories/Chart.stories.tsx +8 -5
  28. package/src/_stories/Chart.tooltip.stories.tsx +1 -1
  29. package/src/_stories/ChartAnnotation.stories.tsx +1 -1
  30. package/src/_stories/ChartAxisLabels.stories.tsx +2 -2
  31. package/src/_stories/ChartAxisTitles.stories.tsx +2 -2
  32. package/src/_stories/ChartEditor.stories.tsx +60 -60
  33. package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
  34. package/src/_stories/ChartLine.Symbols.stories.tsx +1 -1
  35. package/src/_stories/ChartPrefixSuffix.stories.tsx +2 -2
  36. package/src/_stories/_mock/stacked-pattern-test.json +520 -0
  37. package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -0
  38. package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
  39. package/src/components/BarChart/components/BarChart.Horizontal.tsx +159 -20
  40. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +138 -5
  41. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +215 -73
  42. package/src/components/BarChart/components/BarChart.Vertical.tsx +153 -21
  43. package/src/components/BarChart/helpers/index.ts +43 -4
  44. package/src/components/BarChart/helpers/lollipopColors.ts +27 -0
  45. package/src/components/BarChart/helpers/useBarChart.ts +25 -3
  46. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +2 -1
  47. package/src/components/DeviationBar.jsx +9 -6
  48. package/src/components/EditorPanel/EditorPanel.tsx +364 -39
  49. package/src/components/EditorPanel/EditorPanelContext.ts +3 -0
  50. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +414 -0
  51. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +28 -20
  52. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +115 -120
  53. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  54. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +0 -8
  55. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +49 -48
  56. package/src/components/Forecasting/Forecasting.tsx +36 -6
  57. package/src/components/ForestPlot/ForestPlot.tsx +11 -7
  58. package/src/components/ForestPlot/ForestPlotProps.ts +1 -1
  59. package/src/components/Legend/Legend.Component.tsx +106 -13
  60. package/src/components/Legend/helpers/createFormatLabels.tsx +230 -171
  61. package/src/components/LegendWrapper.tsx +1 -1
  62. package/src/components/LineChart/components/LineChart.Circle.tsx +2 -2
  63. package/src/components/LineChart/index.tsx +2 -2
  64. package/src/components/LinearChart.tsx +22 -5
  65. package/src/components/PairedBarChart.jsx +6 -4
  66. package/src/components/PieChart/PieChart.tsx +170 -54
  67. package/src/components/Sankey/components/Sankey.tsx +7 -1
  68. package/src/components/ScatterPlot/ScatterPlot.jsx +32 -4
  69. package/src/data/initial-state.js +315 -293
  70. package/src/helpers/buildForecastPaletteMappings.ts +112 -0
  71. package/src/helpers/buildForecastPaletteOptions.ts +109 -0
  72. package/src/helpers/getColorScale.ts +72 -8
  73. package/src/helpers/getNewRuntime.ts +1 -1
  74. package/src/helpers/getTransformedData.ts +1 -1
  75. package/src/hooks/useChartHoverAnalytics.tsx +44 -0
  76. package/src/hooks/useReduceData.ts +105 -70
  77. package/src/hooks/useTooltip.tsx +57 -15
  78. package/src/index.jsx +0 -2
  79. package/src/scss/main.scss +12 -0
  80. package/src/store/chart.reducer.ts +1 -1
  81. package/src/test/CdcChart.test.jsx +8 -3
  82. package/src/types/ChartConfig.ts +30 -6
  83. package/src/types/ChartContext.ts +1 -0
  84. package/vite.config.js +1 -1
  85. package/vitest.config.ts +16 -0
  86. package/src/coreStyles_chart.scss +0 -3
  87. package/src/helpers/configHelpers.ts +0 -28
  88. package/src/helpers/generateColorsArray.ts +0 -8
  89. package/src/hooks/useColorPalette.js +0 -76
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Builds a palette lookup map with backward-compatibility mappings for forecast charts
3
+ *
4
+ * This function creates multiple naming format mappings to support:
5
+ * - V1 configs using old naming like "Sequential Blue" (Title Case with spaces)
6
+ * - V2 configs using new naming like "sequential-blue" (lowercase with hyphens)
7
+ * - MPX legacy aliases (v1 only)
8
+ *
9
+ * @param processedPalettes - The palette data processed through updatePaletteNames
10
+ * @param paletteVersion - The palette version (1 or 2) from the config
11
+ * @returns A palette map with keys in various formats pointing to color arrays
12
+ */
13
+ export const buildForecastPaletteMappings = (
14
+ processedPalettes: Record<string, string[]>,
15
+ paletteVersion: number
16
+ ): Record<string, string[]> => {
17
+ const paletteMap: Record<string, string[]> = {}
18
+
19
+ // Create base mappings with multiple naming formats for backward compatibility:
20
+ // - sequential-blue (hyphenated)
21
+ // - sequential_blue (underscore)
22
+ // - Sequential Blue (with spaces)
23
+ Object.keys(processedPalettes).forEach(key => {
24
+ const value = processedPalettes[key]
25
+ // Original key
26
+ paletteMap[key] = value
27
+ // Lowercase with hyphens
28
+ paletteMap[key.toLowerCase().replace(/ /g, '-')] = value
29
+ // Lowercase with underscores
30
+ paletteMap[key.toLowerCase().replace(/ /g, '_')] = value
31
+ // Original key variations
32
+ paletteMap[key.replace(/_/g, '-')] = value
33
+ paletteMap[key.toLowerCase()] = value
34
+ })
35
+
36
+ if (paletteVersion === 1) {
37
+ // V1: Add MPX legacy aliases
38
+ // Note: Sequential Blue Two was aliased as "Sequential Blue 2 (MPX)" and Sequential Orange as "Sequential Orange (MPX)"
39
+ const MPX_ALIASES: Record<string, string[]> = {
40
+ 'sequential-blue-two': ['sequential-blue-2-(mpx)', 'sequential-blue-2-(MPX)'],
41
+ 'sequential-blue-tworeverse': ['sequential-blue-2-(mpx)reverse', 'sequential-blue-2-(MPX)reverse'],
42
+ 'sequential-orange': ['sequential-orange-(mpx)', 'sequential-orange-(MPX)'],
43
+ 'sequential-orangereverse': ['sequential-orange-(mpx)reverse', 'sequential-orange-(MPX)reverse']
44
+ }
45
+
46
+ Object.entries(MPX_ALIASES).forEach(([canonical, aliases]) => {
47
+ const palette = paletteMap[canonical]
48
+ if (palette) {
49
+ aliases.forEach(alias => {
50
+ paletteMap[alias] = palette
51
+ })
52
+ }
53
+ })
54
+ } else {
55
+ // V2: Add backward compatibility mappings for migrated configs
56
+ // Map old v1 sequential palette names (Title Case with spaces) to v2 palettes
57
+ // Also includes MPX aliases since users may have selected those in v1
58
+ const V2_MIGRATION_ALIASES: Record<string, string[]> = {
59
+ 'sequential-blue': [
60
+ 'Sequential Blue',
61
+ 'sequential blue',
62
+ 'Sequential Blue Two',
63
+ 'Sequential Blue Three',
64
+ 'sequential-blue-2-(mpx)',
65
+ 'sequential-blue-2-(MPX)',
66
+ 'Sequential Blue 2 (MPX)'
67
+ ],
68
+ 'sequential-bluereverse': [
69
+ 'Sequential Blue Reverse',
70
+ 'sequential bluereverse',
71
+ 'Sequential Blue Two Reverse',
72
+ 'Sequential Blue Three Reverse',
73
+ 'sequential-blue-2-(mpx)reverse',
74
+ 'sequential-blue-2-(MPX)reverse',
75
+ 'Sequential Blue 2 (MPX) Reverse'
76
+ ],
77
+ 'sequential-green': ['Sequential Green', 'sequential green'],
78
+ 'sequential-greenreverse': ['Sequential Green Reverse', 'sequential greenreverse'],
79
+ 'sequential-orange': [
80
+ 'Sequential Orange',
81
+ 'sequential orange',
82
+ 'Sequential Orange Two',
83
+ 'sequential-orange-(mpx)',
84
+ 'sequential-orange-(MPX)',
85
+ 'Sequential Orange (MPX)'
86
+ ],
87
+ 'sequential-orangereverse': [
88
+ 'Sequential Orange Reverse',
89
+ 'sequential orangereverse',
90
+ 'Sequential Orange Two Reverse',
91
+ 'sequential-orange-(mpx)reverse',
92
+ 'sequential-orange-(MPX)reverse',
93
+ 'Sequential Orange (MPX) Reverse'
94
+ ],
95
+ 'sequential-purple': ['Sequential Purple', 'sequential purple'],
96
+ 'sequential-purplereverse': ['Sequential Purple Reverse', 'sequential purplereverse'],
97
+ 'sequential-teal': ['Sequential Teal', 'sequential teal'],
98
+ 'sequential-tealreverse': ['Sequential Teal Reverse', 'sequential tealreverse']
99
+ }
100
+
101
+ Object.entries(V2_MIGRATION_ALIASES).forEach(([canonical, aliases]) => {
102
+ const palette = paletteMap[canonical]
103
+ if (palette) {
104
+ aliases.forEach(alias => {
105
+ paletteMap[alias] = palette
106
+ })
107
+ }
108
+ })
109
+ }
110
+
111
+ return paletteMap
112
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Builds user-friendly palette options for forecast color dropdowns
3
+ *
4
+ * This function takes processed palette data and transforms it into a format
5
+ * suitable for dropdown UI with clean, readable display names.
6
+ *
7
+ * @param processedPalettes - The palette data processed through updatePaletteNames
8
+ * @param paletteVersion - The palette version (1 or 2) from the config
9
+ * @returns An object with kebab-case keys and user-friendly display names as values
10
+ */
11
+ export const buildForecastPaletteOptions = (
12
+ processedPalettes: Record<string, string[]>,
13
+ paletteVersion: number
14
+ ): Record<string, string> => {
15
+ const paletteOptions: Record<string, string> = {}
16
+
17
+ // Create user-friendly options with clean, readable names
18
+ Object.keys(processedPalettes).forEach(key => {
19
+ // Clean up the display name:
20
+ // 1. Replace underscores with spaces
21
+ // 2. Add space before "reverse" if it's concatenated
22
+ // 3. Capitalize first letter of each word
23
+ const cleanName = key
24
+ .replace(/_/g, ' ') // Replace underscores with spaces
25
+ .replace(/reverse$/i, ' Reverse') // Add space before reverse
26
+ .replace(/\b\w/g, char => char.toUpperCase()) // Capitalize first letter of each word
27
+
28
+ // Use lowercase with hyphens as the key for consistency with existing saved values
29
+ // Convert both underscores and spaces to hyphens
30
+ const displayKey = key.replace(/_/g, '-').replace(/ /g, '-').toLowerCase()
31
+ paletteOptions[displayKey] = cleanName
32
+ })
33
+
34
+ // Add MPX aliases for v1 backward compatibility (these were historical names)
35
+ if (paletteVersion === 1) {
36
+ // Map "Sequential Blue Two" to MPX alias
37
+ if (paletteOptions['sequential-blue-two']) {
38
+ paletteOptions['sequential-blue-2-(mpx)'] = 'Sequential Blue 2 (MPX)'
39
+ }
40
+ if (paletteOptions['sequential-blue-tworeverse']) {
41
+ paletteOptions['sequential-blue-2-(mpx)reverse'] = 'Sequential Blue 2 (MPX) Reverse'
42
+ }
43
+ // Map "Sequential Orange" to MPX alias
44
+ if (paletteOptions['sequential-orange']) {
45
+ paletteOptions['sequential-orange-(mpx)'] = 'Sequential Orange (MPX)'
46
+ }
47
+ if (paletteOptions['sequential-orangereverse']) {
48
+ paletteOptions['sequential-orange-(mpx)reverse'] = 'Sequential Orange (MPX) Reverse'
49
+ }
50
+ } else {
51
+ // V2 backward compatibility: add options for old v1 sequential palette names
52
+ // These are for migrated configs that still have v1-style names like "Sequential Blue"
53
+ if (paletteOptions['sequential-blue']) {
54
+ paletteOptions['sequential-blue'] = 'Sequential Blue'
55
+ }
56
+ if (paletteOptions['sequential-green']) {
57
+ paletteOptions['sequential-green'] = 'Sequential Green'
58
+ }
59
+ if (paletteOptions['sequential-orange']) {
60
+ paletteOptions['sequential-orange'] = 'Sequential Orange'
61
+ }
62
+ if (paletteOptions['sequential-purple']) {
63
+ paletteOptions['sequential-purple'] = 'Sequential Purple'
64
+ }
65
+ if (paletteOptions['sequential-teal']) {
66
+ paletteOptions['sequential-teal'] = 'Sequential Teal'
67
+ }
68
+ }
69
+
70
+ return paletteOptions
71
+ }
72
+
73
+ /**
74
+ * Normalizes a palette value to match the standardized hyphenated format
75
+ * and migrates v1 palette names to v2 equivalents
76
+ *
77
+ * @param value - The palette name to normalize
78
+ * @param paletteVersion - The palette version (1 or 2) from the config
79
+ * @returns The normalized palette name in lowercase with hyphens, or 'Select' if empty
80
+ */
81
+ export const normalizePaletteValue = (value: string | undefined, paletteVersion: number = 1): string => {
82
+ if (!value) return 'Select'
83
+
84
+ // Convert to lowercase with hyphens for consistent matching
85
+ const normalized = value.toLowerCase().replace(/ /g, '-').replace(/_/g, '-')
86
+
87
+ // If v2, migrate v1-only palette names to their v2 equivalents
88
+ if (paletteVersion === 2) {
89
+ const V1_TO_V2_MIGRATION: Record<string, string> = {
90
+ // Sequential Blue variants → sequential-blue
91
+ 'sequential-blue-two': 'sequential-blue',
92
+ 'sequential-blue-three': 'sequential-blue',
93
+ 'sequential-blue-2-(mpx)': 'sequential-blue',
94
+ 'sequential-blue-tworeverse': 'sequential-bluereverse',
95
+ 'sequential-blue-threereverse': 'sequential-bluereverse',
96
+ 'sequential-blue-2-(mpx)reverse': 'sequential-bluereverse',
97
+
98
+ // Sequential Orange variants → sequential-orange
99
+ 'sequential-orange-two': 'sequential-orange',
100
+ 'sequential-orange-(mpx)': 'sequential-orange',
101
+ 'sequential-orange-tworeverse': 'sequential-orangereverse',
102
+ 'sequential-orange-(mpx)reverse': 'sequential-orangereverse'
103
+ }
104
+
105
+ return V1_TO_V2_MIGRATION[normalized] || normalized
106
+ }
107
+
108
+ return normalized
109
+ }
@@ -1,20 +1,84 @@
1
- import { colorPalettesChart as colorPalettes, twoColorPalette } from '@cdc/core/data/colorPalettes'
1
+ import { twoColorPalette } from '@cdc/core/data/colorPalettes'
2
+ import { filterChartColorPalettes } from '@cdc/core/helpers/filterColorPalettes'
3
+ import { getColorPaletteVersion } from '@cdc/core/helpers/getColorPaletteVersion'
2
4
  import { scaleOrdinal } from '@visx/scale'
3
5
  import { ChartConfig } from '../types/ChartConfig'
6
+ import { paletteMigrationMap } from '@cdc/core/helpers/palettes/migratePaletteName'
7
+ import { getFallbackColorPalette, migratePaletteWithMap } from '@cdc/core/helpers/palettes/utils'
8
+ import {
9
+ v2ColorDistribution,
10
+ divergentColorDistribution,
11
+ colorblindColorDistribution
12
+ } from '@cdc/core/helpers/palettes/colorDistributions'
4
13
 
5
14
  export const getColorScale = (config: ChartConfig): ((value: string) => string) => {
6
15
  const configPalette = ['Paired Bar', 'Deviation Bar'].includes(config.visualizationType)
7
16
  ? config.twoColor.palette
8
- : config.palette
9
- const allPalettes: Record<string, string[]> = { ...colorPalettes, ...twoColorPalette }
10
- let palette = config.customColors || allPalettes[configPalette]
11
- let numberOfKeys = config.runtime.seriesKeys.length
17
+ : config.general?.palette?.name
18
+ const colorPalettes = filterChartColorPalettes(config)
19
+
20
+ // Get the correct version of two-color palettes
21
+ const version = getColorPaletteVersion(config)
22
+ const versionKey = `v${version}`
23
+ const versionedTwoColorPalette = twoColorPalette[versionKey] || twoColorPalette.v2
24
+
25
+ // For paired/deviation bars, only use two-color palettes
26
+ const palettesSource = ['Paired Bar', 'Deviation Bar'].includes(config.visualizationType)
27
+ ? versionedTwoColorPalette
28
+ : colorPalettes
29
+
30
+ const allPalettes: Record<string, string[]> = { ...versionedTwoColorPalette, ...colorPalettes }
31
+
32
+ // Migrate old palette name if needed
33
+ const migratedPaletteName = configPalette ? configPalette : getFallbackColorPalette(config)
34
+
35
+ let palette =
36
+ config.general?.palette?.customColors ||
37
+ palettesSource[migratePaletteWithMap(migratedPaletteName, paletteMigrationMap, false)] ||
38
+ palettesSource[configPalette]
12
39
 
13
- while (numberOfKeys > palette.length) {
14
- palette = palette.concat(palette)
40
+ // Fallback to a default palette if none found
41
+ if (!palette) {
42
+ console.warn(`Palette "${configPalette}" not found, falling back to default`)
43
+ palette = Object.values(allPalettes)[0] || ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd']
15
44
  }
16
45
 
17
- palette = palette.slice(0, numberOfKeys)
46
+ let numberOfKeys = config.runtime.seriesKeys.length
47
+
48
+ // Apply enhanced color distribution (same logic as pie charts)
49
+ const paletteVersion = getColorPaletteVersion(config)
50
+
51
+ // Skip enhanced distribution if not v2, too many keys, or wrong palette length
52
+ if (paletteVersion !== 2 || numberOfKeys > 9 || palette.length !== 9) {
53
+ // Use existing logic for v1 palettes and other cases
54
+ while (numberOfKeys > palette.length) {
55
+ palette = palette.concat(palette)
56
+ }
57
+ palette = palette.slice(0, numberOfKeys)
58
+ } else {
59
+ // Apply enhanced distribution for v2 palettes
60
+ const isSequential = configPalette && configPalette.includes('sequential')
61
+ const isDivergent = configPalette && configPalette.includes('divergent')
62
+ const isColorblindSafe =
63
+ configPalette && (configPalette.includes('colorblindsafe') || configPalette.includes('qualitative_standard'))
64
+
65
+ // Determine which distribution to use based on palette type
66
+ let distributionMap = null
67
+ if (isDivergent) {
68
+ distributionMap = divergentColorDistribution
69
+ } else if (isColorblindSafe) {
70
+ distributionMap = colorblindColorDistribution
71
+ } else if (isSequential) {
72
+ distributionMap = v2ColorDistribution
73
+ }
74
+
75
+ if (distributionMap && distributionMap[numberOfKeys]) {
76
+ const distributionIndices = distributionMap[numberOfKeys]
77
+ palette = distributionIndices.map((index: number) => palette[index])
78
+ } else {
79
+ palette = palette.slice(0, numberOfKeys)
80
+ }
81
+ }
18
82
 
19
83
  return scaleOrdinal({
20
84
  domain: config.runtime.seriesLabelsAll,
@@ -1,7 +1,7 @@
1
1
  import _ from 'lodash'
2
2
 
3
3
  export const getNewRuntime = (visualizationConfig, newFilteredData) => {
4
- const runtime = _.cloneDeep(visualizationConfig.runtime) || {}
4
+ const runtime = visualizationConfig.runtime ? { ...visualizationConfig.runtime } : {}
5
5
  runtime.series = []
6
6
  runtime.seriesLabels = {}
7
7
  runtime.seriesLabelsAll = []
@@ -14,7 +14,7 @@ export const getTransformedData = ({
14
14
  const data =
15
15
  Array.isArray(brushData) && brushData.length > 0
16
16
  ? brushData
17
- : Array.isArray(filteredData) && filteredData.length > 0
17
+ : Array.isArray(filteredData)
18
18
  ? filteredData
19
19
  : excludedData
20
20
 
@@ -0,0 +1,44 @@
1
+ import { useRef } from 'react'
2
+ import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
3
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
4
+
5
+ type UseChartHoverAnalyticsParams = {
6
+ config: any
7
+ interactionLabel?: string
8
+ }
9
+
10
+ /**
11
+ * Hook to track analytics when user enters the chart area
12
+ * Fires once per chart entry, not on every hover interaction
13
+ */
14
+ export const useChartHoverAnalytics = ({ config, interactionLabel = '' }: UseChartHoverAnalyticsParams) => {
15
+ const hasTrackedRef = useRef(false)
16
+
17
+ const handleChartMouseEnter = () => {
18
+ // Only track if we have an interaction label and haven't tracked yet
19
+ if (!interactionLabel || hasTrackedRef.current) return
20
+
21
+ // Publish the analytics event
22
+ publishAnalyticsEvent({
23
+ vizType: config?.type,
24
+ vizSubType: getVizSubType(config),
25
+ eventType: 'chart_hover',
26
+ eventAction: 'hover',
27
+ eventLabel: interactionLabel,
28
+ vizTitle: getVizTitle(config)
29
+ })
30
+
31
+ // Mark as tracked so we don't fire again
32
+ hasTrackedRef.current = true
33
+ }
34
+
35
+ const handleChartMouseLeave = () => {
36
+ // Reset tracking when mouse leaves so next entry will track
37
+ hasTrackedRef.current = false
38
+ }
39
+
40
+ return {
41
+ handleChartMouseEnter,
42
+ handleChartMouseLeave
43
+ }
44
+ }
@@ -1,37 +1,74 @@
1
1
  import isNumber from '@cdc/core/helpers/isNumber'
2
+ import { useMemo } from 'react'
2
3
 
3
4
  function useReduceData(config, data) {
4
- const isBar = config.series.every(({ type }) => type === 'Bar')
5
- const isAllLine = config.series.every(({ type }) => ['Line', 'dashed-sm', 'dashed-md', 'dashed-lg'].includes(type))
6
- const sumYValues = seriesKeys => xValue =>
7
- seriesKeys.reduce((yTotal, k) => (isNaN(Number(xValue[k])) ? yTotal : yTotal + Number(xValue[k])), 0)
8
- const getSeriesKey = seriesKey => {
9
- const series = config.runtime.series.find(item => item.dataKey === seriesKey)
10
- return series?.dynamicCategory ? series.originalDataKey : seriesKey
11
- }
12
- const getMaxValueFromData = () => {
13
- let max = Math.max(
14
- ...data?.map(d =>
15
- Math.max(
16
- ...config.runtime.seriesKeys.map(key => {
17
- const seriesKey = getSeriesKey(key)
18
- return isNumber(d[seriesKey]) ? Number(cleanChars(d[seriesKey])) : 0
19
- })
20
- )
21
- )
22
- )
5
+ return useMemo(() => {
6
+ if (!data || !config?.runtime?.seriesKeys) {
7
+ return { minValue: 0, maxValue: 0, existPositiveValue: false, isAllLine: false }
8
+ }
9
+
10
+ const isBar = config.series.every(({ type }) => type === 'Bar')
11
+ const isAllLine = config.series.every(({ type }) => ['Line', 'dashed-sm', 'dashed-md', 'dashed-lg'].includes(type))
12
+
13
+ const cleanChars = value => {
14
+ if (value === null || value === '') {
15
+ return ''
16
+ }
17
+ return typeof value === 'string' ? value.replace(/[,$]/g, '') : value
18
+ }
19
+
20
+ const getSeriesKey = seriesKey => {
21
+ const series = config.runtime.series.find(item => item.dataKey === seriesKey)
22
+ return series?.dynamicCategory ? series.originalDataKey : seriesKey
23
+ }
24
+
25
+ const seriesKeysMap = new Map()
26
+ config.runtime.seriesKeys.forEach(key => {
27
+ seriesKeysMap.set(key, getSeriesKey(key))
28
+ })
29
+
30
+ let minValue = Infinity
31
+ let maxValue = -Infinity
32
+ let existPositiveValue = false
33
+ const stackedTotals = []
34
+
35
+ for (let i = 0; i < data.length; i++) {
36
+ const row = data[i]
37
+ let rowMax = -Infinity
38
+ let rowMin = Infinity
39
+ let stackedSum = 0
40
+
41
+ for (const key of config.runtime.seriesKeys) {
42
+ const seriesKey = seriesKeysMap.get(key)
43
+ const cleanValue = cleanChars(row[seriesKey])
44
+
45
+ if (isNumber(cleanValue)) {
46
+ const numValue = Number(cleanValue)
47
+
48
+ if (numValue > rowMax) rowMax = numValue
49
+ if (numValue < rowMin) rowMin = numValue
50
+
51
+ if (numValue >= 0) existPositiveValue = true
52
+
53
+ if (!isNaN(numValue)) stackedSum += numValue
54
+ }
55
+ }
56
+
57
+ if (rowMax > maxValue) maxValue = rowMax
58
+ if (rowMin < minValue) minValue = rowMin
59
+
60
+ if (!isNaN(stackedSum)) stackedTotals.push(stackedSum)
61
+ }
23
62
 
24
63
  if (
25
64
  (config.visualizationType === 'Bar' || (config.visualizationType === 'Combo' && isBar)) &&
26
65
  config.visualizationSubType === 'stacked'
27
66
  ) {
28
- const yTotals = data.map(sumYValues(config.runtime.seriesKeys)).filter(num => !isNaN(num))
29
- max = Math.max(...yTotals)
67
+ maxValue = Math.max(...stackedTotals)
30
68
  }
31
69
 
32
70
  if (config.visualizationSubType === 'stacked' && config.visualizationType === 'Area Chart') {
33
- const yTotals = data.map(sumYValues(config.runtime.seriesKeys))
34
- max = Math.max(...yTotals)
71
+ maxValue = Math.max(...stackedTotals)
35
72
  }
36
73
 
37
74
  if (
@@ -39,62 +76,60 @@ function useReduceData(config, data) {
39
76
  config.series &&
40
77
  config.series.dataKey
41
78
  ) {
42
- max = Math.max(
43
- ...data.map(d => (isNumber(d[config.series.dataKey]) ? Number(cleanChars(d[config.series.dataKey])) : 0))
44
- )
79
+ let specialMax = -Infinity
80
+ for (const row of data) {
81
+ const cleanValue = cleanChars(row[config.series.dataKey])
82
+ if (isNumber(cleanValue)) {
83
+ const numValue = Number(cleanValue)
84
+ if (numValue > specialMax) specialMax = numValue
85
+ }
86
+ }
87
+ maxValue = specialMax
45
88
  }
46
89
 
47
90
  if (config.visualizationType === 'Combo' && config.visualizationSubType === 'stacked' && !isBar) {
48
91
  if (config.runtime.barSeriesKeys && config.runtime.lineSeriesKeys) {
49
- const yTotals = data.map(sumYValues(config.runtime.barSeriesKeys))
50
-
51
- const lineMax = Math.max(
52
- ...data.map(d => Math.max(...config.runtime.lineSeriesKeys.map(key => Number(cleanChars(d[key])))))
53
- )
54
- const barMax = Math.max(...yTotals)
55
-
56
- max = Math.max(barMax, lineMax)
92
+ let barMax = -Infinity
93
+ let lineMax = -Infinity
94
+
95
+ for (const row of data) {
96
+ let barSum = 0
97
+ let rowLineMax = -Infinity
98
+
99
+ for (const key of config.runtime.barSeriesKeys) {
100
+ const cleanValue = cleanChars(row[key])
101
+ if (isNumber(cleanValue)) {
102
+ const numValue = Number(cleanValue)
103
+ if (!isNaN(numValue)) barSum += numValue
104
+ }
105
+ }
106
+
107
+ for (const key of config.runtime.lineSeriesKeys) {
108
+ const cleanValue = cleanChars(row[key])
109
+ if (isNumber(cleanValue)) {
110
+ const numValue = Number(cleanValue)
111
+ if (numValue > rowLineMax) rowLineMax = numValue
112
+ }
113
+ }
114
+
115
+ if (barSum > barMax) barMax = barSum
116
+ if (rowLineMax > lineMax) lineMax = rowLineMax
117
+ }
118
+
119
+ maxValue = Math.max(barMax, lineMax)
57
120
  }
58
121
  }
59
122
 
60
- return max
61
- }
62
-
63
- const getMinValueFromData = () => {
64
- const minNumberFromData = Math.min(
65
- ...data.map(d =>
66
- Math.min(
67
- ...config.runtime.seriesKeys.map(key => {
68
- const seriesKey = getSeriesKey(key)
69
- return isNumber(d[seriesKey]) ? Number(cleanChars(d[seriesKey])) : Infinity
70
- })
71
- )
72
- )
73
- )
74
-
75
- return String(minNumberFromData)
76
- }
77
-
78
- const findPositiveNum = () => {
79
- if (!config.runtime.seriesKeys) {
80
- return false
81
- }
82
- return config.runtime.seriesKeys.some(key => data.some(d => d[getSeriesKey(key)] >= 0))
83
- }
123
+ if (minValue === Infinity) minValue = 0
124
+ if (maxValue === -Infinity) maxValue = 0
84
125
 
85
- const cleanChars = value => {
86
- if (value === null || value === '') {
87
- return ''
126
+ return {
127
+ minValue: Number(minValue),
128
+ maxValue: Number(maxValue),
129
+ existPositiveValue,
130
+ isAllLine
88
131
  }
89
-
90
- return typeof value === 'string' ? value.replace(/[,$]/g, '') : value
91
- }
92
-
93
- const maxValue = Number(getMaxValueFromData())
94
- const minValue = Number(getMinValueFromData())
95
- const existPositiveValue = findPositiveNum()
96
-
97
- return { minValue, maxValue, existPositiveValue, isAllLine }
132
+ }, [config, data])
98
133
  }
99
134
 
100
135
  export default useReduceData