@cdc/chart 4.24.9 → 4.24.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 (65) hide show
  1. package/LICENSE +201 -0
  2. package/dist/cdcchart.js +43919 -40370
  3. package/index.html +1 -1
  4. package/package.json +2 -2
  5. package/src/CdcChart.tsx +129 -108
  6. package/src/_stories/Chart.Legend.Gradient.stories.tsx +33 -0
  7. package/src/_stories/Chart.stories.tsx +28 -0
  8. package/src/_stories/ChartAxisLabels.stories.tsx +20 -0
  9. package/src/_stories/ChartAxisTitles.stories.tsx +53 -0
  10. package/src/_stories/ChartPrefixSuffix.stories.tsx +151 -0
  11. package/src/_stories/_mock/horizontal_bar.json +257 -0
  12. package/src/_stories/_mock/large_x_axis_labels.json +261 -0
  13. package/src/_stories/_mock/paired-bar.json +262 -0
  14. package/src/_stories/_mock/pie_with_data.json +255 -0
  15. package/src/_stories/_mock/simplified_line.json +1510 -0
  16. package/src/components/Annotations/components/AnnotationDraggable.tsx +0 -3
  17. package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
  18. package/src/components/Axis/Categorical.Axis.tsx +22 -4
  19. package/src/components/BarChart/components/BarChart.Horizontal.tsx +95 -16
  20. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +41 -17
  21. package/src/components/BarChart/components/BarChart.Vertical.tsx +78 -20
  22. package/src/components/BarChart/helpers/index.ts +23 -4
  23. package/src/components/BrushChart.tsx +3 -2
  24. package/src/components/DeviationBar.jsx +58 -8
  25. package/src/components/EditorPanel/EditorPanel.tsx +63 -40
  26. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +8 -25
  27. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +21 -4
  28. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +297 -35
  29. package/src/components/EditorPanel/components/panels.scss +4 -6
  30. package/src/components/EditorPanel/editor-panel.scss +0 -8
  31. package/src/components/EditorPanel/helpers/tests/updateFieldRankByValue.test.ts +38 -0
  32. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +42 -0
  33. package/src/components/EditorPanel/useEditorPermissions.ts +1 -0
  34. package/src/components/ForestPlot/ForestPlot.tsx +2 -3
  35. package/src/components/ForestPlot/ForestPlotProps.ts +2 -0
  36. package/src/components/Legend/Legend.Component.tsx +16 -16
  37. package/src/components/Legend/Legend.Suppression.tsx +25 -20
  38. package/src/components/Legend/Legend.tsx +0 -2
  39. package/src/components/Legend/helpers/index.ts +16 -19
  40. package/src/components/LegendWrapper.tsx +3 -1
  41. package/src/components/LineChart/components/LineChart.Circle.tsx +10 -0
  42. package/src/components/LinearChart.tsx +740 -562
  43. package/src/components/PairedBarChart.jsx +50 -10
  44. package/src/components/PieChart/PieChart.tsx +1 -6
  45. package/src/components/Regions/components/Regions.tsx +33 -19
  46. package/src/components/ZoomBrush.tsx +25 -6
  47. package/src/coreStyles_chart.scss +3 -0
  48. package/src/data/initial-state.js +6 -2
  49. package/src/helpers/configHelpers.ts +28 -0
  50. package/src/helpers/handleRankByValue.ts +15 -0
  51. package/src/helpers/sizeHelpers.ts +25 -0
  52. package/src/helpers/tests/handleRankByValue.test.ts +37 -0
  53. package/src/helpers/tests/sizeHelpers.test.ts +80 -0
  54. package/src/hooks/useColorPalette.js +10 -2
  55. package/src/hooks/useLegendClasses.ts +4 -0
  56. package/src/hooks/useScales.ts +31 -3
  57. package/src/hooks/useTooltip.tsx +9 -5
  58. package/src/index.jsx +1 -0
  59. package/src/scss/DataTable.scss +5 -4
  60. package/src/scss/main.scss +57 -52
  61. package/src/types/ChartConfig.ts +38 -16
  62. package/src/types/ChartContext.ts +18 -14
  63. package/src/_stories/Chart.Legend.Gradient.tsx +0 -19
  64. package/src/_stories/ChartBrush.stories.tsx +0 -19
  65. package/src/components/LinearChart.jsx +0 -817
@@ -6,9 +6,10 @@ import { Text } from '@visx/text'
6
6
 
7
7
  import ConfigContext from '../ConfigContext'
8
8
  import { getContrastColor } from '@cdc/core/helpers/cove/accessibility'
9
+ import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
9
10
 
10
11
  const PairedBarChart = ({ width, height, originalWidth }) => {
11
- const { config, colorScale, transformedData: data, formatNumber, seriesHighlight, getTextWidth } = useContext(ConfigContext)
12
+ const { config, colorScale, transformedData: data, formatNumber, seriesHighlight } = useContext(ConfigContext)
12
13
 
13
14
  if (!config || config?.series?.length < 2) return
14
15
 
@@ -79,14 +80,27 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
79
80
  }
80
81
  `}
81
82
  </style>
82
- <svg id='cdc-visualization__paired-bar-chart' width={originalWidth} height={height} viewBox={`0 0 ${width + Number(config.runtime.yAxis.size)} ${height}`} role='img' tabIndex={0}>
83
+ <svg
84
+ id='cdc-visualization__paired-bar-chart'
85
+ width={originalWidth}
86
+ height={height}
87
+ viewBox={`0 0 ${width + Number(config.runtime.yAxis.size)} ${height}`}
88
+ role='img'
89
+ tabIndex={0}
90
+ >
83
91
  <title>{`Paired bar chart graphic with the title ${config.title ? config.title : 'No Title Found'}`}</title>
84
92
  <Group top={0} left={Number(config.xAxis.size)}>
85
93
  {data
86
94
  .filter(item => config.series[0].dataKey === groupOne.dataKey)
87
95
  .map((d, index) => {
88
- let transparentBar = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(config.series[0].dataKey) === -1
89
- let displayBar = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(config.series[0].dataKey) !== -1
96
+ let transparentBar =
97
+ config.legend.behavior === 'highlight' &&
98
+ seriesHighlight.length > 0 &&
99
+ seriesHighlight.indexOf(config.series[0].dataKey) === -1
100
+ let displayBar =
101
+ config.legend.behavior === 'highlight' ||
102
+ seriesHighlight.length === 0 ||
103
+ seriesHighlight.indexOf(config.series[0].dataKey) !== -1
90
104
  let barWidth = xScale(d[config.series[0].dataKey])
91
105
  let barHeight = Number(config.barHeight) ? Number(config.barHeight) : 25
92
106
  // update bar Y to give dynamic Y when user applyes BarSpace
@@ -95,7 +109,10 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
95
109
  const totalheight = (Number(config.barSpace) + barHeight + borderWidth) * data.length
96
110
  config.heights.horizontal = totalheight
97
111
  // check if text fits inside of the bar including suffix/prefix,comma,fontSize ..etc
98
- const textWidth = getTextWidth(formatNumber(d[groupOne.dataKey], 'left'), `normal ${fontSize[config.fontSize]}px sans-serif`)
112
+ const textWidth = getTextWidth(
113
+ formatNumber(d[groupOne.dataKey], 'left'),
114
+ `normal ${fontSize[config.fontSize]}px sans-serif`
115
+ )
99
116
  const textFits = textWidth < barWidth - 5 // minus padding dx(5)
100
117
 
101
118
  return (
@@ -119,7 +136,14 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
119
136
  tabIndex={-1}
120
137
  />
121
138
  {config.yAxis.displayNumbersOnBar && displayBar && (
122
- <Text textAnchor={textFits ? 'start' : 'end'} dx={textFits ? 5 : -5} verticalAnchor='middle' x={halfWidth - barWidth} y={y + config.barHeight / 2} fill={textFits ? groupOne.labelColor : '#000'}>
139
+ <Text
140
+ textAnchor={textFits ? 'start' : 'end'}
141
+ dx={textFits ? 5 : -5}
142
+ verticalAnchor='middle'
143
+ x={halfWidth - barWidth}
144
+ y={y + config.barHeight / 2}
145
+ fill={textFits ? groupOne.labelColor : '#000'}
146
+ >
123
147
  {formatNumber(d[groupOne.dataKey], 'left')}
124
148
  </Text>
125
149
  )}
@@ -131,8 +155,14 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
131
155
  .filter(item => config.series[1].dataKey === groupTwo.dataKey)
132
156
  .map((d, index) => {
133
157
  let barWidth = xScale(d[config.series[1].dataKey])
134
- let transparentBar = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(config.series[1].dataKey) === -1
135
- let displayBar = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(config.series[1].dataKey) !== -1
158
+ let transparentBar =
159
+ config.legend.behavior === 'highlight' &&
160
+ seriesHighlight.length > 0 &&
161
+ seriesHighlight.indexOf(config.series[1].dataKey) === -1
162
+ let displayBar =
163
+ config.legend.behavior === 'highlight' ||
164
+ seriesHighlight.length === 0 ||
165
+ seriesHighlight.indexOf(config.series[1].dataKey) !== -1
136
166
  let barHeight = config.barHeight ? Number(config.barHeight) : 25
137
167
  // update bar Y to give dynamic Y when user applyes BarSpace
138
168
  let y = 0
@@ -140,7 +170,10 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
140
170
  const totalheight = (Number(config.barSpace) + barHeight + borderWidth) * data.length
141
171
  config.heights.horizontal = totalheight
142
172
  // check if text fits inside of the bar including suffix/prefix,comma,fontSize ..etc
143
- const textWidth = getTextWidth(formatNumber(d[groupTwo.dataKey], 'left'), `normal ${fontSize[config.fontSize]}px sans-serif`)
173
+ const textWidth = getTextWidth(
174
+ formatNumber(d[groupTwo.dataKey], 'left'),
175
+ `normal ${fontSize[config.fontSize]}px sans-serif`
176
+ )
144
177
  const isTextFits = textWidth < barWidth - 5 // minus padding dx(5)
145
178
 
146
179
  return (
@@ -171,7 +204,14 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
171
204
  tabIndex={-1}
172
205
  />
173
206
  {config.yAxis.displayNumbersOnBar && displayBar && (
174
- <Text textAnchor={isTextFits ? 'end' : 'start'} dx={isTextFits ? -5 : 5} verticalAnchor='middle' x={halfWidth + barWidth} y={y + config.barHeight / 2} fill={isTextFits ? groupTwo.labelColor : '#000'}>
207
+ <Text
208
+ textAnchor={isTextFits ? 'end' : 'start'}
209
+ dx={isTextFits ? -5 : 5}
210
+ verticalAnchor='middle'
211
+ x={halfWidth + barWidth}
212
+ y={y + config.barHeight / 2}
213
+ fill={isTextFits ? groupTwo.labelColor : '#000'}
214
+ >
175
215
  {formatNumber(d[groupTwo.dataKey], 'left')}
176
216
  </Text>
177
217
  )}
@@ -38,9 +38,6 @@ const PieChart = props => {
38
38
  config,
39
39
  colorScale,
40
40
  currentViewport,
41
- dimensions,
42
- highlight,
43
- highlightReset,
44
41
  seriesHighlight,
45
42
  isDraggingAnnotation
46
43
  } = useContext(ConfigContext)
@@ -232,8 +229,6 @@ const PieChart = props => {
232
229
  }
233
230
  }, [seriesHighlight]) // eslint-disable-line
234
231
 
235
- const createLegendLabels = createFormatLabels(config, [], _data, _colorScale)
236
-
237
232
  const getSvgClasses = () => {
238
233
  let classes = ['animated-pie', 'group']
239
234
  if (config.animate === false || animatedPie) {
@@ -280,7 +275,7 @@ const PieChart = props => {
280
275
  <TooltipWithBounds
281
276
  key={Math.random()}
282
277
  className={'tooltip cdc-open-viz-module'}
283
- left={tooltipLeft}
278
+ left={tooltipLeft + centerX - radius}
284
279
  top={tooltipTop}
285
280
  >
286
281
  <ul>
@@ -19,7 +19,18 @@ type RegionsProps = {
19
19
  }
20
20
 
21
21
  // TODO: should regions be removed on categorical axis?
22
- const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGroup = 1, yMax, handleTooltipMouseOff, handleTooltipMouseOver, handleTooltipClick, tooltipData, showTooltip, hideTooltip }) => {
22
+ const Regions: React.FC<RegionsProps> = ({
23
+ xScale,
24
+ barWidth = 0,
25
+ totalBarsInGroup = 1,
26
+ yMax,
27
+ handleTooltipMouseOff,
28
+ handleTooltipMouseOver,
29
+ handleTooltipClick,
30
+ tooltipData,
31
+ showTooltip,
32
+ hideTooltip
33
+ }) => {
23
34
  const { parseDate, config } = useContext<ChartContext>(ConfigContext)
24
35
 
25
36
  const { runtime, regions, visualizationType, orientation, xAxis } = config
@@ -44,7 +55,10 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
44
55
  const previousDays = Number(region.from) || 0
45
56
  const categoricalDomain = domain.map(d => formatDate(config.xAxis.dateParseFormat, new Date(d)))
46
57
  const d = region.toType === 'Last Date' ? new Date(domain[domain.length - 1]).getTime() : new Date(region.to) // on categorical charts force leading zero 03/15/2016 vs 3/15/2016 for valid date format
47
- const to = config.xAxis.type === 'categorical' ? formatDate(config.xAxis.dateParseFormat, d) : formatDate(config.xAxis.dateParseFormat, d)
58
+ const to =
59
+ config.xAxis.type === 'categorical'
60
+ ? formatDate(config.xAxis.dateParseFormat, d)
61
+ : formatDate(config.xAxis.dateParseFormat, d)
48
62
  const toDate = new Date(to)
49
63
  from = new Date(toDate.setDate(toDate.getDate() - Number(previousDays)))
50
64
 
@@ -120,7 +134,12 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
120
134
  }
121
135
  if (region.toType === 'Last Date') {
122
136
  const lastDate = domain[domain.length - 1]
123
- to = Number(xScale(lastDate) + ((visualizationType === 'Bar' || visualizationType === 'Combo') && config.xAxis.type === 'date' ? barWidth * totalBarsInGroup : 0))
137
+ to = Number(
138
+ xScale(lastDate) +
139
+ ((visualizationType === 'Bar' || visualizationType === 'Combo') && config.xAxis.type === 'date'
140
+ ? barWidth * totalBarsInGroup
141
+ : 0)
142
+ )
124
143
  }
125
144
 
126
145
  if (visualizationType === 'Line' || visualizationType === 'Area Chart') {
@@ -152,27 +171,22 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
152
171
  if (!from) return null
153
172
  if (!to) return null
154
173
 
155
- const TopRegionBorderShape = () => {
156
- return (
157
- <path
158
- stroke='#333'
159
- d={`M${from} -5
160
- L${from} 5
161
- M${from} 0
162
- L${to} 0
163
- M${to} -5
164
- L${to} 5`}
165
- />
166
- )
167
- }
168
-
169
174
  const HighlightedArea = () => {
170
175
  return <rect x={from} y={0} width={width} height={yMax} fill={region.background} opacity={0.3} />
171
176
  }
172
177
 
173
178
  return (
174
- <Group height={100} fill='red' className='regions regions-group--line zzz' key={region.label} onMouseMove={handleTooltipMouseOver} onMouseLeave={handleTooltipMouseOff} handleTooltipClick={handleTooltipClick} tooltipData={JSON.stringify(tooltipData)} showTooltip={showTooltip}>
175
- <TopRegionBorderShape />
179
+ <Group
180
+ height={100}
181
+ fill='red'
182
+ className='regions regions-group--line zzz'
183
+ key={region.label}
184
+ onMouseMove={handleTooltipMouseOver}
185
+ onMouseLeave={handleTooltipMouseOff}
186
+ handleTooltipClick={handleTooltipClick}
187
+ tooltipData={JSON.stringify(tooltipData)}
188
+ showTooltip={showTooltip}
189
+ >
176
190
  <HighlightedArea />
177
191
  <Text x={from + width / 2} y={5} fill={region.color} verticalAnchor='start' textAnchor='middle'>
178
192
  {region.label}
@@ -7,6 +7,7 @@ import ConfigContext from '../ConfigContext'
7
7
  import { ScaleLinear, ScaleBand } from 'd3-scale'
8
8
  import { isDateScale } from '@cdc/core/helpers/cove/date'
9
9
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
10
+ import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
10
11
 
11
12
  interface Props {
12
13
  xScaleBrush: ScaleLinear<number, number>
@@ -15,7 +16,7 @@ interface Props {
15
16
  yMax: number
16
17
  }
17
18
  const ZoomBrush: FC<Props> = props => {
18
- const { tableData, config, parseDate, formatDate, setBrushConfig, getTextWidth, dashboardConfig } = useContext(ConfigContext)
19
+ const { tableData, config, parseDate, formatDate, setBrushConfig, dashboardConfig } = useContext(ConfigContext)
19
20
  const sharedFilters = dashboardConfig?.dashboard?.sharedFilters ?? []
20
21
  const isDashboardFilters = sharedFilters?.length > 0
21
22
  const { fontSize } = useBarChart()
@@ -175,7 +176,6 @@ const ZoomBrush: FC<Props> = props => {
175
176
  <BrushHandle
176
177
  left={Number(config.runtime.yAxis.size)}
177
178
  showTooltip={showTooltip}
178
- getTextWidth={getTextWidth}
179
179
  pixelDistance={textProps.endPosition - textProps.startPosition}
180
180
  textProps={textProps}
181
181
  fontSize={fontSize[config.fontSize]}
@@ -202,7 +202,7 @@ const ZoomBrush: FC<Props> = props => {
202
202
  }
203
203
 
204
204
  const BrushHandle = props => {
205
- const { x, isBrushActive, isBrushing, className, textProps, fontSize, showTooltip, left, getTextWidth } = props
205
+ const { x, isBrushActive, isBrushing, className, textProps, fontSize, showTooltip, left } = props
206
206
  const pathWidth = 8
207
207
  if (!isBrushActive) {
208
208
  return null
@@ -217,15 +217,34 @@ const BrushHandle = props => {
217
217
  return (
218
218
  <>
219
219
  {showTooltip && (
220
- <Text x={(Number(textProps.xMax) - textWidth) / 2} dy={-12} pointerEvents='visiblePainted' fontSize={fontSize / 1.1}>
220
+ <Text
221
+ x={(Number(textProps.xMax) - textWidth) / 2}
222
+ dy={-12}
223
+ pointerEvents='visiblePainted'
224
+ fontSize={fontSize / 1.1}
225
+ >
221
226
  {tooltipText}
222
227
  </Text>
223
228
  )}
224
229
  <Group left={x + pathWidth / 2} top={-2}>
225
- <Text pointerEvents='visiblePainted' dominantBaseline='hanging' x={isLeft ? 55 : -50} y={25} verticalAnchor='start' textAnchor={textAnchor} fontSize={fontSize / 1.4}>
230
+ <Text
231
+ pointerEvents='visiblePainted'
232
+ dominantBaseline='hanging'
233
+ x={isLeft ? 55 : -50}
234
+ y={25}
235
+ verticalAnchor='start'
236
+ textAnchor={textAnchor}
237
+ fontSize={fontSize / 1.4}
238
+ >
226
239
  {isLeft ? textProps.startValue : textProps.endValue}
227
240
  </Text>
228
- <path cursor='ew-resize' d='M0.5,10A6,6 0 0 1 6.5,16V14A6,6 0 0 1 0.5,20ZM2.5,18V12M4.5,18V12' fill={'#297EF1'} strokeWidth='1' transform={transform}></path>
241
+ <path
242
+ cursor='ew-resize'
243
+ d='M0.5,10A6,6 0 0 1 6.5,16V14A6,6 0 0 1 0.5,20ZM2.5,18V12M4.5,18V12'
244
+ fill={'#297EF1'}
245
+ strokeWidth='1'
246
+ transform={transform}
247
+ ></path>
229
248
  </Group>
230
249
  </>
231
250
  )
@@ -0,0 +1,3 @@
1
+ @import '@cdc/core/styles/base';
2
+ @import '@cdc/core/styles/heading-colors';
3
+ @import '@cdc/core/styles/v2/themes/color-definitions';
@@ -28,6 +28,7 @@ export default {
28
28
  showDownloadButton: false,
29
29
  showMissingDataLabel: true,
30
30
  showSuppressedSymbol: true,
31
+ showZeroValueData: true,
31
32
  hideNullValue: true
32
33
  },
33
34
  padding: {
@@ -122,10 +123,12 @@ export default {
122
123
  tickLabelColor: '#333',
123
124
  tickColor: '#333',
124
125
  numTicks: '',
125
- labelOffset: 65,
126
+ labelOffset: 0,
126
127
  axisPadding: 200,
127
128
  target: 0,
128
- maxTickRotation: 0
129
+ maxTickRotation: 0,
130
+ padding: 5,
131
+ showYearsOnce: false
129
132
  },
130
133
  table: {
131
134
  label: 'Data Table',
@@ -135,6 +138,7 @@ export default {
135
138
  caption: '',
136
139
  showDownloadUrl: false,
137
140
  showDataTableLink: true,
141
+ showDownloadLinkBelow: true,
138
142
  indexLabel: '',
139
143
  download: false,
140
144
  showVertical: true,
@@ -0,0 +1,28 @@
1
+ import { cloneDeep } from 'lodash'
2
+ import { ChartConfig } from '../types/ChartConfig'
3
+
4
+ /* editConfigKeys
5
+ * Add edit or update config keys
6
+ * keyUpdates: { path: string[], value: any }[]
7
+ * path is the array of keys needed to reach the value to be updated
8
+ * value is the new value to be set
9
+ * if the key does not exist, it will be created
10
+ */
11
+ export function editConfigKeys(config: ChartConfig, keyUpdates: { path: string[]; value: any }[]): ChartConfig {
12
+ const configDeepCopy = cloneDeep(config)
13
+
14
+ const newConfig = keyUpdates.reduce((acc, { path, value }) => {
15
+ const pathCopy = [...path]
16
+ const lastKey = pathCopy.pop()
17
+ const target = pathCopy.reduce((target, key) => {
18
+ if (!target[key]) {
19
+ target[key] = {}
20
+ }
21
+ return target[key]
22
+ }, acc)
23
+ target[lastKey] = value
24
+ return acc
25
+ }, configDeepCopy)
26
+
27
+ return newConfig
28
+ }
@@ -0,0 +1,15 @@
1
+ import { ChartConfig } from '../types/ChartConfig'
2
+
3
+ const getNumericValue = number => {
4
+ if (typeof number === 'string') return parseFloat(number.replace(/,/g, ''))
5
+ return Number(number)
6
+ }
7
+
8
+ export const handleRankByValue = (data, passedConfig: ChartConfig) => {
9
+ if (passedConfig.rankByValue) {
10
+ const series = passedConfig.series[0].dataKey
11
+ const sorted = data.sort((a, b) => getNumericValue(a[series]) - getNumericValue(b[series]))
12
+ return passedConfig.rankByValue === 'asc' ? sorted : sorted.reverse()
13
+ }
14
+ return data
15
+ }
@@ -0,0 +1,25 @@
1
+ import { isMobileHeightViewport } from '@cdc/core/helpers/viewports'
2
+ import { ChartConfig, ViewportSize } from '../types/ChartConfig'
3
+
4
+ export function getOrientation(
5
+ { orientation, heights, visualizationType }: Pick<ChartConfig, 'orientation' | 'heights' | 'visualizationType'>,
6
+ currentViewport: ViewportSize
7
+ ): 'vertical' | 'horizontal' | 'mobileVertical' {
8
+ const isForestPlot = visualizationType === 'Forest Plot'
9
+ const useVertical = orientation === 'vertical' || isForestPlot
10
+ const useMobileVertical = heights?.mobileVertical && isMobileHeightViewport(currentViewport)
11
+ const responsiveVertical = useMobileVertical ? 'mobileVertical' : 'vertical'
12
+
13
+ return useVertical ? responsiveVertical : 'horizontal'
14
+ }
15
+ export function calcInitialHeight(
16
+ { heights, orientation, visualizationType }: Pick<ChartConfig, 'heights' | 'orientation' | 'visualizationType'>,
17
+ currentViewport: ViewportSize
18
+ ): number {
19
+ // if no heights are provided assume config has not been loaded
20
+ if (!heights) return 0
21
+
22
+ const renderedOrientation = getOrientation({ orientation, heights, visualizationType }, currentViewport)
23
+ const height = Number(heights?.[renderedOrientation])
24
+ return isNaN(height) ? 0 : height
25
+ }
@@ -0,0 +1,37 @@
1
+ import { handleRankByValue } from '../handleRankByValue'
2
+ import { ChartConfig } from '../../types/ChartConfig'
3
+
4
+ describe('handleRankByValue', () => {
5
+ it('should sort the data in ascending order when rankByValue is "asc"', () => {
6
+ const data = [{ value: 3 }, { value: 1 }, { value: 2 }]
7
+ const config: ChartConfig = {
8
+ rankByValue: 'asc',
9
+ series: [{ dataKey: 'value' }]
10
+ }
11
+
12
+ const result = handleRankByValue(data, config)
13
+ expect(result).toEqual([{ value: 1 }, { value: 2 }, { value: 3 }])
14
+ })
15
+
16
+ it('should sort the data in descending order when rankByValue is "desc"', () => {
17
+ const data = [{ value: 3 }, { value: 1 }, { value: 2 }]
18
+ const config: ChartConfig = {
19
+ rankByValue: 'desc',
20
+ series: [{ dataKey: 'value' }]
21
+ }
22
+
23
+ const result = handleRankByValue(data, config)
24
+ expect(result).toEqual([{ value: 3 }, { value: 2 }, { value: 1 }])
25
+ })
26
+
27
+ it('should handle numeric strings correctly', () => {
28
+ const data = [{ value: '3' }, { value: '1' }, { value: '2' }]
29
+ const config: ChartConfig = {
30
+ rankByValue: 'asc',
31
+ series: [{ dataKey: 'value' }]
32
+ }
33
+
34
+ const result = handleRankByValue(data, config)
35
+ expect(result).toEqual([{ value: '1' }, { value: '2' }, { value: '3' }])
36
+ })
37
+ })
@@ -0,0 +1,80 @@
1
+ import { calcInitialHeight, getOrientation } from '../sizeHelpers'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { ChartOrientation, VisualizationType } from '../../types/ChartConfig'
4
+
5
+ describe('sizeHelpers', () => {
6
+ describe('getOrientation', () => {
7
+ it("should return 'vertical' when orientation is vertical", () => {
8
+ const config = {
9
+ orientation: 'vertical' as ChartOrientation,
10
+ heights: { mobileVertical: 0, vertical: 0, horizontal: 0 },
11
+ visualizationType: 'Bar' as VisualizationType
12
+ }
13
+ expect(getOrientation(config, 'md')).toBe('vertical')
14
+ })
15
+
16
+ it("should return 'horizontal' when orientation is horizontal", () => {
17
+ const config = {
18
+ orientation: 'horizontal' as ChartOrientation,
19
+ heights: { mobileVertical: 0, vertical: 0, horizontal: 0 },
20
+ visualizationType: 'Bar' as VisualizationType
21
+ }
22
+ expect(getOrientation(config, 'md')).toBe('horizontal')
23
+ })
24
+ it("should return 'vertical' when orientation is horizontal but visualizationType is 'Forest Plot'", () => {
25
+ const config = {
26
+ orientation: 'horizontal' as ChartOrientation,
27
+ heights: { mobileVertical: 0, vertical: 0, horizontal: 0 },
28
+ visualizationType: 'Forest Plot' as VisualizationType
29
+ }
30
+ expect(getOrientation(config, 'md')).toBe('vertical')
31
+ })
32
+
33
+ it('should return mobileVertical when viewport is mobile height', () => {
34
+ const config = {
35
+ orientation: 'vertical' as ChartOrientation,
36
+ heights: { mobileVertical: 100, vertical: 0, horizontal: 0 },
37
+ visualizationType: 'Bar' as VisualizationType
38
+ }
39
+ expect(getOrientation(config, 'xxs')).toBe('mobileVertical')
40
+ })
41
+ })
42
+ })
43
+
44
+ describe('calcInitialHeight', () => {
45
+ it('should return 0 when no heights are provided', () => {
46
+ const config = {
47
+ heights: undefined,
48
+ orientation: 'vertical' as ChartOrientation,
49
+ visualizationType: 'Bar' as VisualizationType
50
+ }
51
+ expect(calcInitialHeight(config, 'md')).toBe(0)
52
+ })
53
+
54
+ it('should return vertical height when orientation is vertical', () => {
55
+ const config = {
56
+ orientation: 'vertical' as ChartOrientation,
57
+ heights: { mobileVertical: 0, vertical: 100, horizontal: 0 },
58
+ visualizationType: 'Bar' as VisualizationType
59
+ }
60
+ expect(calcInitialHeight(config, 'md')).toBe(100)
61
+ })
62
+
63
+ it('should return horizontal height when orientation is horizontal', () => {
64
+ const config = {
65
+ orientation: 'horizontal' as ChartOrientation,
66
+ heights: { mobileVertical: 0, vertical: 0, horizontal: 100 },
67
+ visualizationType: 'Bar' as VisualizationType
68
+ }
69
+ expect(calcInitialHeight(config, 'md')).toBe(100)
70
+ })
71
+
72
+ it('should return mobileVertical height when viewport is mobile height', () => {
73
+ const config = {
74
+ heights: { mobileVertical: 100, vertical: 0, horizontal: 0 },
75
+ orientation: 'vertical' as ChartOrientation,
76
+ visualizationType: 'Bar' as VisualizationType
77
+ }
78
+ expect(calcInitialHeight(config, 'xxs')).toBe(100)
79
+ })
80
+ })
@@ -5,11 +5,14 @@ export const useColorPalette = (config, updateConfig) => {
5
5
  let twoColorPalettes = []
6
6
  let sequential = []
7
7
  let nonSequential = []
8
+ const accessibleColors = []
8
9
 
9
10
  // Get two color palettes if visualization type is Paired Bar
10
11
  if (config.visualizationType === 'Paired Bar' || config.visualizationType === 'Deviation Bar') {
11
12
  const isReversed = config.twoColor.isPaletteReversed
12
- twoColorPalettes = Object.keys(twoColorPalette).filter(name => (isReversed ? name.endsWith('reverse') : !name.endsWith('reverse')))
13
+ twoColorPalettes = Object.keys(twoColorPalette).filter(name =>
14
+ isReversed ? name.endsWith('reverse') : !name.endsWith('reverse')
15
+ )
13
16
  } else {
14
17
  // Get sequential and non-sequential palettes for other visualization types
15
18
  const seqPalettes = []
@@ -18,6 +21,7 @@ export const useColorPalette = (config, updateConfig) => {
18
21
  for (const paletteName in colorPalettesChart) {
19
22
  const isSequential = paletteName.startsWith('sequential')
20
23
  const isQualitative = paletteName.startsWith('qualitative')
24
+ const colorblindsafe = paletteName.startsWith('colorblindsafe')
21
25
  const isReversed = paletteName.endsWith('reverse')
22
26
 
23
27
  if (isSequential && ((!config.isPaletteReversed && !isReversed) || (config.isPaletteReversed && isReversed))) {
@@ -27,6 +31,9 @@ export const useColorPalette = (config, updateConfig) => {
27
31
  if (isQualitative && ((!config.isPaletteReversed && !isReversed) || (config.isPaletteReversed && isReversed))) {
28
32
  nonSeqPalettes.push(paletteName)
29
33
  }
34
+ if (colorblindsafe && ((!config.isPaletteReversed && !isReversed) || (config.isPaletteReversed && isReversed))) {
35
+ accessibleColors.push(paletteName)
36
+ }
30
37
  }
31
38
 
32
39
  sequential = seqPalettes
@@ -64,5 +71,6 @@ export const useColorPalette = (config, updateConfig) => {
64
71
  }, [config.isPaletteReversed])
65
72
 
66
73
  // Return all palettes
67
- return { twoColorPalettes, sequential, nonSequential }
74
+
75
+ return { twoColorPalettes, sequential, nonSequential, accessibleColors }
68
76
  }
@@ -60,6 +60,10 @@ const useLegendClasses = (config: ConfigType) => {
60
60
  containerClasses.push('no-border')
61
61
  }
62
62
 
63
+ if (config.legend.hideBorder.topBottom && ['top'].includes(config.legend.position)) {
64
+ containerClasses.push('p-0')
65
+ }
66
+
63
67
  return {
64
68
  containerClasses,
65
69
  innerClasses
@@ -72,7 +72,10 @@ const useScales = (properties: useScaleProps) => {
72
72
  let xAxisMin = Math.min(...xAxisDataMapped.map(Number))
73
73
  let xAxisMax = Math.max(...xAxisDataMapped.map(Number))
74
74
  xAxisMin -= (config.xAxis.padding ? config.xAxis.padding * 0.01 : 0) * (xAxisMax - xAxisMin)
75
- xAxisMax += (config.xAxis.padding ? config.xAxis.padding * 0.01 : 0) * (xAxisMax - xAxisMin)
75
+ xAxisMax +=
76
+ visualizationType === 'Line'
77
+ ? 0
78
+ : (config.xAxis.padding ? config.xAxis.padding * 0.01 : 0) * (xAxisMax - xAxisMin)
76
79
  xScale = scaleTime({
77
80
  domain: [xAxisMin, xAxisMax],
78
81
  range: [0, xMax]
@@ -264,14 +267,34 @@ const useScales = (properties: useScaleProps) => {
264
267
 
265
268
  export default useScales
266
269
 
267
- export const getTickValues = (xAxisDataMapped, xScale, num) => {
270
+ export const getFirstDayOfMonth = ms => {
271
+ const date = new Date(ms)
272
+ return new Date(date.getFullYear(), date.getMonth(), 1).getTime()
273
+ }
274
+
275
+ export const dateFormatHasMonthButNoDays = dateFormat => {
276
+ return (
277
+ (dateFormat.includes('%b') ||
278
+ dateFormat.includes('%B') ||
279
+ dateFormat.includes('%m') ||
280
+ dateFormat.includes('%-m') ||
281
+ dateFormat.includes('%_m')) &&
282
+ !dateFormat.includes('%d') &&
283
+ !dateFormat.includes('%-d') &&
284
+ !dateFormat.includes('%_d') &&
285
+ !dateFormat.includes('%e')
286
+ )
287
+ }
288
+
289
+ export const getTickValues = (xAxisDataMapped, xScale, num, config) => {
268
290
  const xDomain = xScale.domain()
269
291
 
270
292
  if (xScale.type === 'time') {
271
293
  const xDomainMax = xAxisDataMapped[xAxisDataMapped.length - 1]
272
294
  const xDomainMin = xAxisDataMapped[0]
295
+
273
296
  const step = (xDomainMax - xDomainMin) / (num - 1)
274
- const tickValues = []
297
+ let tickValues = []
275
298
  for (let i = xDomainMax; i >= xDomainMin; i -= step) {
276
299
  tickValues.push(i)
277
300
  }
@@ -280,6 +303,11 @@ export const getTickValues = (xAxisDataMapped, xScale, num) => {
280
303
  }
281
304
  tickValues.reverse()
282
305
 
306
+ // Use first days of months when showing months without days
307
+ if (dateFormatHasMonthButNoDays(config.xAxis.dateDisplayFormat)) {
308
+ tickValues = tickValues.map(tv => getFirstDayOfMonth(tv))
309
+ }
310
+
283
311
  return tickValues
284
312
  }
285
313
 
@@ -58,10 +58,7 @@ export const useTooltip = props => {
58
58
  const showMissingDataValue = config.general.showMissingDataLabel && (!value || value === 'null')
59
59
  let formattedValue = seriesKey === config.xAxis.dataKey ? value : formatNumber(value, getAxisPosition(seriesKey))
60
60
 
61
- formattedValue =
62
- showMissingDataValue && (config.visualizationSubType === 'stacked' ? !config.general.hideNullValue : true)
63
- ? 'N/A'
64
- : formattedValue
61
+ formattedValue = showMissingDataValue ? 'N/A' : formattedValue
65
62
 
66
63
  return formattedValue
67
64
  }
@@ -199,7 +196,14 @@ export const useTooltip = props => {
199
196
  ?.flatMap(seriesKey => {
200
197
  const value = resolvedScaleValues[0]?.[seriesKey]
201
198
  const formattedValue = getFormattedValue(seriesKey, value, config, getAxisPosition)
202
- return [[seriesKey, formattedValue, getAxisPosition(seriesKey)]]
199
+ if (
200
+ (value === null || value === undefined || value === '' || formattedValue === 'N/A') &&
201
+ config.general.hideNullValue
202
+ ) {
203
+ return []
204
+ } else {
205
+ return [[seriesKey, formattedValue, getAxisPosition(seriesKey)]]
206
+ }
203
207
  })
204
208
  )
205
209
  }