@cdc/chart 4.24.12 → 4.25.1

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 (72) hide show
  1. package/dist/cdcchart.js +79611 -78971
  2. package/examples/feature/boxplot/boxplot.json +2 -157
  3. package/examples/feature/boxplot/testing.csv +23 -38
  4. package/examples/feature/tests-non-numerics/example-combo-bar-nonnumeric.json +394 -30
  5. package/examples/private/ehdi.json +29939 -0
  6. package/examples/private/not-loading.json +360 -0
  7. package/index.html +7 -14
  8. package/package.json +2 -2
  9. package/src/CdcChart.tsx +92 -1512
  10. package/src/CdcChartComponent.tsx +1105 -0
  11. package/src/_stories/Chart.Anchors.stories.tsx +1 -1
  12. package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
  13. package/src/_stories/Chart.DynamicSeries.stories.tsx +1 -1
  14. package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
  15. package/src/_stories/Chart.ScatterPlot.stories.tsx +19 -0
  16. package/src/_stories/Chart.tooltip.stories.tsx +1 -2
  17. package/src/_stories/ChartAnnotation.stories.tsx +1 -1
  18. package/src/_stories/ChartAxisLabels.stories.tsx +1 -1
  19. package/src/_stories/ChartAxisTitles.stories.tsx +1 -1
  20. package/src/_stories/ChartEditor.stories.tsx +1 -1
  21. package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
  22. package/src/_stories/ChartLine.Symbols.stories.tsx +18 -0
  23. package/src/_stories/ChartPrefixSuffix.stories.tsx +1 -1
  24. package/src/_stories/_mock/line_chart_symbols.json +437 -0
  25. package/src/_stories/_mock/scatterplot-image-download.json +1244 -0
  26. package/src/components/Annotations/components/AnnotationDraggable.tsx +3 -11
  27. package/src/components/Annotations/components/AnnotationDropdown.tsx +3 -3
  28. package/src/components/Axis/Categorical.Axis.tsx +3 -4
  29. package/src/components/BarChart/components/BarChart.Horizontal.tsx +14 -5
  30. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +10 -4
  31. package/src/components/BarChart/components/BarChart.Vertical.tsx +2 -2
  32. package/src/components/BoxPlot/BoxPlot.tsx +34 -32
  33. package/src/components/BoxPlot/helpers/index.ts +108 -18
  34. package/src/components/DeviationBar.jsx +2 -6
  35. package/src/components/EditorPanel/EditorPanel.tsx +62 -6
  36. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +4 -0
  37. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +44 -7
  38. package/src/components/ForestPlot/ForestPlot.tsx +176 -26
  39. package/src/components/Legend/Legend.Component.tsx +29 -38
  40. package/src/components/Legend/Legend.Suppression.tsx +3 -5
  41. package/src/components/Legend/Legend.tsx +2 -2
  42. package/src/components/Legend/LegendLine.Shape.tsx +51 -0
  43. package/src/components/Legend/helpers/createFormatLabels.tsx +29 -26
  44. package/src/components/Legend/helpers/getLegendClasses.ts +20 -38
  45. package/src/components/Legend/helpers/index.ts +14 -7
  46. package/src/components/Legend/tests/getLegendClasses.test.ts +3 -20
  47. package/src/components/LineChart/components/LineChart.Circle.tsx +90 -88
  48. package/src/components/LineChart/index.tsx +4 -0
  49. package/src/components/LinearChart.tsx +65 -31
  50. package/src/components/PairedBarChart.jsx +2 -9
  51. package/src/components/ZoomBrush.tsx +5 -7
  52. package/src/data/initial-state.js +6 -3
  53. package/src/helpers/getBoxPlotConfig.ts +68 -0
  54. package/src/helpers/getColorScale.ts +28 -0
  55. package/src/helpers/getComboChartConfig.ts +42 -0
  56. package/src/helpers/getExcludedData.ts +37 -0
  57. package/src/helpers/getTopAxis.ts +7 -0
  58. package/src/hooks/useBarChart.ts +28 -9
  59. package/src/hooks/{useHighlightedBars.js → useHighlightedBars.ts} +2 -1
  60. package/src/hooks/useIntersectionObserver.ts +37 -0
  61. package/src/hooks/useMinMax.ts +4 -0
  62. package/src/hooks/useReduceData.ts +1 -1
  63. package/src/hooks/useTooltip.tsx +9 -1
  64. package/src/index.jsx +1 -0
  65. package/src/scss/DataTable.scss +0 -5
  66. package/src/scss/main.scss +30 -115
  67. package/src/types/ChartConfig.ts +6 -3
  68. package/src/types/ChartContext.ts +1 -3
  69. package/src/helpers/getQuartiles.ts +0 -27
  70. package/src/hooks/useColorScale.ts +0 -50
  71. package/src/hooks/useIntersectionObserver.jsx +0 -29
  72. package/src/hooks/useTopAxis.js +0 -6
@@ -27,7 +27,7 @@ import CategoricalYAxis from './Axis/Categorical.Axis'
27
27
 
28
28
  // Helpers
29
29
  import { isConvertLineToBarGraph } from '../helpers/isConvertLineToBarGraph'
30
- import { isLegendWrapViewport } from '@cdc/core/helpers/viewports'
30
+ import { isLegendWrapViewport, isMobileHeightViewport } from '@cdc/core/helpers/viewports'
31
31
  import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
32
32
  import { calcInitialHeight } from '../helpers/sizeHelpers'
33
33
 
@@ -36,13 +36,13 @@ import useMinMax from '../hooks/useMinMax'
36
36
  import useReduceData from '../hooks/useReduceData'
37
37
  import useRightAxis from '../hooks/useRightAxis'
38
38
  import useScales, { getTickValues, filterAndShiftLinearDateTicks } from '../hooks/useScales'
39
- import useTopAxis from '../hooks/useTopAxis'
39
+ import getTopAxis from '../helpers/getTopAxis'
40
40
  import { useTooltip as useCoveTooltip } from '../hooks/useTooltip'
41
41
  import { useEditorPermissions } from './EditorPanel/useEditorPermissions'
42
42
  import Annotation from './Annotations'
43
43
  import { BlurStrokeText } from '@cdc/core/components/BlurStrokeText'
44
- import { fontSizes } from '@cdc/core/helpers/cove/fontSettings'
45
44
  import { countNumOfTicks } from '../helpers/countNumOfTicks'
45
+ import _ from 'lodash'
46
46
 
47
47
  type LinearChartProps = {
48
48
  parentWidth: number
@@ -50,9 +50,14 @@ type LinearChartProps = {
50
50
  }
51
51
 
52
52
  const BOTTOM_LABEL_PADDING = 9
53
- const X_TICK_LABEL_PADDING = 3
53
+ const X_TICK_LABEL_PADDING = 4.5
54
54
  const DEFAULT_TICK_LENGTH = 8
55
55
  const MONTH_AS_MS = 1000 * 60 * 60 * 24 * 30
56
+ const TICK_LABEL_FONT_SIZE = 16
57
+ const TICK_LABEL_FONT_SIZE_SMALL = 13
58
+ const AXIS_LABEL_FONT_SIZE = 18
59
+ const AXIS_LABEL_FONT_SIZE_SMALL = 14
60
+ const TICK_LABEL_MARGIN_RIGHT = 4.5
56
61
 
57
62
  const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, parentWidth }, svgRef) => {
58
63
  // prettier-ignore
@@ -99,7 +104,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
99
104
  // HOOKS % STATES
100
105
  const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(config, data)
101
106
  const { visSupportsReactTooltip } = useEditorPermissions()
102
- const { hasTopAxis } = useTopAxis(config)
107
+ const { hasTopAxis } = getTopAxis(config)
103
108
  const [animatedChart, setAnimatedChart] = useState(false)
104
109
  const [point, setPoint] = useState({ x: 0, y: 0 })
105
110
  const [suffixWidth, setSuffixWidth] = useState(0)
@@ -129,6 +134,9 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
129
134
  const labelsOverflow = onlyShowTopPrefixSuffix && !suffixHasNoSpace
130
135
  const padding = orientation === 'horizontal' ? Number(config.xAxis.size) : Number(config.yAxis.size)
131
136
  const yLabelOffset = isNaN(parseInt(`${runtime.yAxis.labelOffset}`)) ? 0 : parseInt(`${runtime.yAxis.labelOffset}`)
137
+ const tickLabelFontSize = isMobileHeightViewport(currentViewport) ? TICK_LABEL_FONT_SIZE_SMALL : TICK_LABEL_FONT_SIZE
138
+ const axisLabelFontSize = isMobileHeightViewport(currentViewport) ? AXIS_LABEL_FONT_SIZE_SMALL : AXIS_LABEL_FONT_SIZE
139
+ const GET_TEXT_WIDTH_FONT = `normal ${tickLabelFontSize}px Nunito, sans-serif`
132
140
 
133
141
  // zero if not forest plot
134
142
  const forestRowsHeight = isForestPlot ? config.data.length * config.forestPlot.rowHeight : 0
@@ -410,21 +418,37 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
410
418
  useEffect(() => {
411
419
  if (lastMaxValue.current === maxValue) return
412
420
  lastMaxValue.current = maxValue
421
+
413
422
  if (!yAxisAutoPadding) return
414
423
  setYAxisAutoPadding(0)
415
424
  }, [maxValue])
425
+
416
426
  useEffect(() => {
417
427
  if (orientation === 'horizontal') return
428
+ if (!labelsOverflow) return
429
+
430
+ // minimum percentage of the max value that the distance should be from the top grid line
431
+ const MINIMUM_DISTANCE_PERCENTAGE = 0.025
418
432
 
419
- const maxValueIsGreaterThanTopGridLine = maxValue > Math.max(...yScale.ticks(handleNumTicks))
433
+ const topGridLine = Math.max(...yScale.ticks(handleNumTicks))
434
+ const needsPaddingThreshold = topGridLine - maxValue * MINIMUM_DISTANCE_PERCENTAGE
435
+ const maxValueIsGreaterThanThreshold = maxValue > needsPaddingThreshold
420
436
 
421
- if (!maxValueIsGreaterThanTopGridLine || !labelsOverflow) return
437
+ if (!maxValueIsGreaterThanThreshold) return
422
438
 
423
- const tickGap = yScale.ticks(handleNumTicks)[1] - yScale.ticks(handleNumTicks)[0]
439
+ const ticks = yScale.ticks(handleNumTicks)
440
+ const tickGap = ticks.length === 1 ? ticks[0] : ticks[1] - ticks[0]
424
441
  const nextTick = Math.max(...yScale.ticks(handleNumTicks)) + tickGap
425
- const newPadding = minValue < 0 ? (nextTick - maxValue) / maxValue / 2 : (nextTick - maxValue) / maxValue
442
+ const divideBy = minValue < 0 ? maxValue / 2 : maxValue
443
+ const calculatedPadding = (nextTick - maxValue) / divideBy
426
444
 
427
- setYAxisAutoPadding(newPadding * 100)
445
+ // if auto padding is too close to next tick, add one more ticks worth of padding
446
+ const newPadding =
447
+ calculatedPadding > MINIMUM_DISTANCE_PERCENTAGE ? calculatedPadding : calculatedPadding + tickGap / divideBy
448
+
449
+ /* sometimes even though the padding is getting to the next tick exactly,
450
+ d3 still doesn't show the tick. we add 0.1 to ensure to tip it over the edge */
451
+ setYAxisAutoPadding(newPadding * 100 + 0.1)
428
452
  }, [maxValue, labelsOverflow, yScale, handleNumTicks])
429
453
 
430
454
  // Render Functions
@@ -433,12 +457,12 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
433
457
 
434
458
  const getTickPositions = (ticks, xScale) => {
435
459
  if (!ticks.length) return false
436
- // filterout first index
460
+ // filter out first index
437
461
  const filteredTicks = ticks.filter(tick => tick.index !== 0)
438
462
  const numberOfTicks = filteredTicks?.length
439
463
  const xMaxHalf = xScale.range()[0] || xMax / 2
440
464
  const tickWidthAll = filteredTicks.map(tick =>
441
- getTextWidth(formatNumber(tick.value, 'left'), `normal ${fontSizes[config.fontSize]}px sans-serif`)
465
+ getTextWidth(formatNumber(tick.value, 'left'), GET_TEXT_WIDTH_FONT)
442
466
  )
443
467
  const accumulator = 100
444
468
  const sumOfTickWidth = tickWidthAll.reduce((a, b) => a + b, accumulator)
@@ -475,10 +499,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
475
499
  return (
476
500
  <Group className='bottom-axis'>
477
501
  {props.ticks.map((tick, i) => {
478
- const textWidth = getTextWidth(
479
- formatNumber(tick.value, 'left'),
480
- `normal ${fontSizes[config.fontSize]}px sans-serif`
481
- )
482
502
  const isTicksOverlapping = getTickPositions(props.ticks, g1xScale)
483
503
  const maxTickRotation = Number(config.xAxis.maxTickRotation) || 90
484
504
  const isResponsiveTicks = config.isResponsiveTicks && isTicksOverlapping
@@ -497,6 +517,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
497
517
  angle={-angle}
498
518
  verticalAnchor={angle ? 'middle' : 'start'}
499
519
  textAnchor={textAnchor}
520
+ fontSize={tickLabelFontSize}
500
521
  >
501
522
  {formatNumber(tick.value, 'left')}
502
523
  </Text>
@@ -527,10 +548,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
527
548
  <>
528
549
  <Group className='bottom-axis'>
529
550
  {props.ticks.map((tick, i) => {
530
- const textWidth = getTextWidth(
531
- formatNumber(tick.value, 'left'),
532
- `normal ${fontSizes[config.fontSize]}px sans-serif`
533
- )
534
551
  const isTicksOverlapping = getTickPositions(props.ticks, g2xScale)
535
552
  const maxTickRotation = Number(config.xAxis.maxTickRotation) || 90
536
553
  const isResponsiveTicks = config.isResponsiveTicks && isTicksOverlapping
@@ -548,6 +565,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
548
565
  angle={-angle}
549
566
  verticalAnchor={angle ? 'middle' : 'start'}
550
567
  textAnchor={textAnchor}
568
+ fontSize={tickLabelFontSize}
551
569
  >
552
570
  {formatNumber(tick.value, 'left')}
553
571
  </Text>
@@ -565,6 +583,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
565
583
  stroke='#333'
566
584
  textAnchor={'middle'}
567
585
  verticalAnchor='start'
586
+ fontSize={axisLabelFontSize}
568
587
  >
569
588
  {runtime.xAxis.label}
570
589
  </Text>
@@ -641,6 +660,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
641
660
  transform={`translate(${-1 * runtime.yAxis.size + yLabelOffset}, ${axisCenter}) rotate(-90)`}
642
661
  fontWeight='bold'
643
662
  fill={config.yAxis.labelColor}
663
+ fontSize={axisLabelFontSize}
644
664
  >
645
665
  {props.label}
646
666
  </Text>
@@ -1034,7 +1054,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1034
1054
  const lastTick = props.ticks.length - 1 === i
1035
1055
  const hideTopTick = lastTick && onlyShowTopPrefixSuffix && suffix && !suffixHasNoSpace
1036
1056
  const valueOnLinePadding = hideAxis ? -8 : -12
1037
- const labelXPadding = labelsAboveGridlines ? valueOnLinePadding : 2
1057
+ const labelXPadding = labelsAboveGridlines ? valueOnLinePadding : TICK_LABEL_MARGIN_RIGHT
1038
1058
  const labelYPadding = labelsAboveGridlines ? 4 : 0
1039
1059
  const labelX = tick.to.x - labelXPadding
1040
1060
  const labelY = tick.to.y - labelYPadding
@@ -1051,6 +1071,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1051
1071
  to={isLogarithmicAxis ? to : tick.to}
1052
1072
  stroke={config.yAxis.tickColor}
1053
1073
  display={orientation === 'horizontal' ? 'none' : 'block'}
1074
+ fontSize={tickLabelFontSize}
1054
1075
  />
1055
1076
  )}
1056
1077
 
@@ -1068,6 +1089,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1068
1089
  }) rotate(-${config.runtime.horizontal ? config.runtime.yAxis.tickRotation || 0 : 0})`}
1069
1090
  verticalAnchor={'start'}
1070
1091
  textAnchor={'end'}
1092
+ fontSize={tickLabelFontSize}
1071
1093
  >
1072
1094
  {tick.formattedValue}
1073
1095
  </Text>
@@ -1083,6 +1105,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1083
1105
  }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
1084
1106
  verticalAnchor={'start'}
1085
1107
  textAnchor={'end'}
1108
+ fontSize={tickLabelFontSize}
1086
1109
  >
1087
1110
  {tick.formattedValue}
1088
1111
  </Text>
@@ -1097,6 +1120,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1097
1120
  }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
1098
1121
  textAnchor={'end'}
1099
1122
  verticalAnchor='middle'
1123
+ fontSize={tickLabelFontSize}
1100
1124
  >
1101
1125
  {tick.formattedValue}
1102
1126
  </Text>
@@ -1112,6 +1136,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1112
1136
  }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
1113
1137
  textAnchor={'end'}
1114
1138
  verticalAnchor='middle'
1139
+ fontSize={tickLabelFontSize}
1115
1140
  >
1116
1141
  {tick.formattedValue}
1117
1142
  </Text>
@@ -1130,6 +1155,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1130
1155
  verticalAnchor={config.runtime.horizontal ? 'start' : 'middle'}
1131
1156
  textAnchor={config.runtime.horizontal ? 'start' : 'end'}
1132
1157
  fill={config.yAxis.tickLabelColor}
1158
+ fontSize={tickLabelFontSize}
1133
1159
  >
1134
1160
  {config.runtime.seriesLabelsAll[tick.formattedValue - 1]}
1135
1161
  </Text>
@@ -1172,6 +1198,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1172
1198
  paintOrder={'stroke'} // keeps stroke under fill
1173
1199
  strokeLinejoin='round'
1174
1200
  style={{ whiteSpace: 'pre-wrap' }} // prevents leading spaces from being trimmed
1201
+ fontSize={tickLabelFontSize}
1175
1202
  >
1176
1203
  {suffix}
1177
1204
  </BlurStrokeText>
@@ -1193,6 +1220,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1193
1220
  strokeLinejoin='round'
1194
1221
  paintOrder={'stroke'} // keeps stroke under fill
1195
1222
  style={{ whiteSpace: 'pre-wrap' }} // prevents leading spaces from being trimmed
1223
+ fontSize={tickLabelFontSize}
1196
1224
  >
1197
1225
  {`${tick.formattedValue}${combineDomSuffixWithValue ? suffix : ''}`}
1198
1226
  </BlurStrokeText>
@@ -1208,6 +1236,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1208
1236
  transform={`translate(${-1 * runtime.yAxis.size + yLabelOffset}, ${axisCenter}) rotate(-90)`}
1209
1237
  fontWeight='bold'
1210
1238
  fill={config.yAxis.labelColor}
1239
+ fontSize={axisLabelFontSize}
1211
1240
  >
1212
1241
  {props.label}
1213
1242
  </Text>
@@ -1270,6 +1299,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1270
1299
  verticalAnchor={runtime.horizontal ? 'start' : 'middle'}
1271
1300
  textAnchor={'start'}
1272
1301
  fill={config.yAxis.rightAxisTickLabelColor}
1302
+ fontSize={tickLabelFontSize}
1273
1303
  >
1274
1304
  {tick.formattedValue}
1275
1305
  </Text>
@@ -1289,6 +1319,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1289
1319
  }, ${axisCenter}) rotate(-90)`}
1290
1320
  fontWeight='bold'
1291
1321
  fill={config.yAxis.rightAxisLabelColor}
1322
+ fontSize={axisLabelFontSize}
1292
1323
  >
1293
1324
  {props.label}
1294
1325
  </Text>
@@ -1328,17 +1359,18 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1328
1359
  numTicks={useDateSpanMonths ? dateSpanMonths : xTickCount}
1329
1360
  tickStroke='#333'
1330
1361
  tickValues={
1331
- config.xAxis.manual
1362
+ config.runtime.xAxis.manual
1332
1363
  ? getTickValues(xAxisDataMapped, xScale, isDateTime ? xTickCount : getManualStep(), config)
1333
- : config.xAxis.type === 'date'
1364
+ : config.runtime.xAxis.type === 'date'
1334
1365
  ? xAxisDataMapped
1335
1366
  : undefined
1336
1367
  }
1337
1368
  >
1338
1369
  {props => {
1370
+ const hasDynamicCategory = config.series.some(s => s.dynamicCategory)
1339
1371
  // For these charts, we generated all ticks in tickValues above, and now need to filter/shift them
1340
1372
  // so the last tick is always labeled
1341
- if (config.xAxis.type === 'date' && !config.xAxis.manual) {
1373
+ if (config.runtime.xAxis.type === 'date' && !config.runtime.xAxis.manual && !hasDynamicCategory) {
1342
1374
  props.ticks = filterAndShiftLinearDateTicks(config, props, xAxisDataMapped, formatDate)
1343
1375
  }
1344
1376
 
@@ -1346,7 +1378,11 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1346
1378
  useDateSpanMonths &&
1347
1379
  xScale
1348
1380
  .ticks(xTickCount)
1349
- .map(t => props.ticks.findIndex(tick => tick.value.getTime() === t.getTime()))
1381
+ .map(t =>
1382
+ props.ticks.findIndex(
1383
+ tick => (typeof tick.value === 'number' ? tick.value : tick.value.getTime()) === t.getTime()
1384
+ )
1385
+ )
1350
1386
  .slice(0, 2)
1351
1387
  .reduce((acc, curr) => curr - acc)
1352
1388
 
@@ -1370,16 +1406,12 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1370
1406
 
1371
1407
  // Calculate sumOfTickWidth here, before map function
1372
1408
  const tickWidthMax = Math.max(
1373
- ...filteredTicks.map(tick =>
1374
- getTextWidth(tick.formattedValue, `normal ${fontSizes[config.fontSize]}px sans-serif`)
1375
- )
1409
+ ...filteredTicks.map(tick => getTextWidth(tick.formattedValue, GET_TEXT_WIDTH_FONT))
1376
1410
  )
1377
1411
  // const marginTop = 20 // moved to top bc need for yMax calcs
1378
1412
  const accumulator = ismultiLabel ? 180 : 100
1379
1413
 
1380
- const textWidths = filteredTicks.map(tick =>
1381
- getTextWidth(tick.formattedValue, `normal ${fontSizes[config.fontSize]}px sans-serif`)
1382
- )
1414
+ const textWidths = filteredTicks.map(tick => getTextWidth(tick.formattedValue, GET_TEXT_WIDTH_FONT))
1383
1415
  const sumOfTickWidth = textWidths.reduce((a, b) => a + b, accumulator)
1384
1416
  const spaceBetweenEachTick = (xMax - sumOfTickWidth) / (filteredTicks.length - 1)
1385
1417
 
@@ -1471,6 +1503,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1471
1503
  : undefined
1472
1504
  }
1473
1505
  fill={config.xAxis.tickLabelColor}
1506
+ fontSize={tickLabelFontSize}
1474
1507
  >
1475
1508
  {tick.formattedValue}
1476
1509
  </Text>
@@ -1488,6 +1521,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1488
1521
  verticalAnchor='start'
1489
1522
  fontWeight='bold'
1490
1523
  fill={config.xAxis.labelColor}
1524
+ fontSize={axisLabelFontSize}
1491
1525
  >
1492
1526
  {props.label}
1493
1527
  </Text>
@@ -15,7 +15,6 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
15
15
 
16
16
  const borderWidth = config.barHasBorder === 'true' ? 1 : 0
17
17
  const halfWidth = width / 2
18
- const fontSize = { small: 16, medium: 18, large: 20 }
19
18
  const offset = 1.02 // Offset of the left bar from the Axis
20
19
 
21
20
  const groupOne = {
@@ -109,10 +108,7 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
109
108
  const totalheight = (Number(config.barSpace) + barHeight + borderWidth) * data.length
110
109
  config.heights.horizontal = totalheight
111
110
  // check if text fits inside of the bar including suffix/prefix,comma,fontSize ..etc
112
- const textWidth = getTextWidth(
113
- formatNumber(d[groupOne.dataKey], 'left'),
114
- `normal ${fontSize[config.fontSize]}px sans-serif`
115
- )
111
+ const textWidth = getTextWidth(formatNumber(d[groupOne.dataKey], 'left'))
116
112
  const textFits = textWidth < barWidth - 5 // minus padding dx(5)
117
113
 
118
114
  return (
@@ -170,10 +166,7 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
170
166
  const totalheight = (Number(config.barSpace) + barHeight + borderWidth) * data.length
171
167
  config.heights.horizontal = totalheight
172
168
  // check if text fits inside of the bar including suffix/prefix,comma,fontSize ..etc
173
- const textWidth = getTextWidth(
174
- formatNumber(d[groupTwo.dataKey], 'left'),
175
- `normal ${fontSize[config.fontSize]}px sans-serif`
176
- )
169
+ const textWidth = getTextWidth(formatNumber(d[groupTwo.dataKey], 'left'))
177
170
  const isTextFits = textWidth < barWidth - 5 // minus padding dx(5)
178
171
 
179
172
  return (
@@ -1,13 +1,13 @@
1
1
  import { Brush } from '@visx/brush'
2
2
  import { Group } from '@visx/group'
3
3
  import { Text } from '@visx/text'
4
- import { useBarChart } from '../hooks/useBarChart'
5
4
  import { FC, useContext, useEffect, useRef, useState } from 'react'
6
5
  import ConfigContext from '../ConfigContext'
7
6
  import { ScaleLinear, ScaleBand } from 'd3-scale'
8
7
  import { isDateScale } from '@cdc/core/helpers/cove/date'
9
8
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
10
9
  import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
10
+ import { appFontSize } from '@cdc/core/helpers/cove/fontSettings'
11
11
 
12
12
  interface Props {
13
13
  xScaleBrush: ScaleLinear<number, number>
@@ -19,7 +19,6 @@ const ZoomBrush: FC<Props> = props => {
19
19
  const { tableData, config, parseDate, formatDate, setBrushConfig, dashboardConfig } = useContext(ConfigContext)
20
20
  const sharedFilters = dashboardConfig?.dashboard?.sharedFilters ?? []
21
21
  const isDashboardFilters = sharedFilters?.length > 0
22
- const { fontSize } = useBarChart()
23
22
  const [showTooltip, setShowTooltip] = useState(false)
24
23
  const [brushKey, setBrushKey] = useState(0)
25
24
  const brushRef = useRef(null)
@@ -178,7 +177,6 @@ const ZoomBrush: FC<Props> = props => {
178
177
  showTooltip={showTooltip}
179
178
  pixelDistance={textProps.endPosition - textProps.startPosition}
180
179
  textProps={textProps}
181
- fontSize={fontSize[config.fontSize]}
182
180
  {...props}
183
181
  isBrushing={brushRef.current?.state.isBrushing}
184
182
  />
@@ -202,7 +200,7 @@ const ZoomBrush: FC<Props> = props => {
202
200
  }
203
201
 
204
202
  const BrushHandle = props => {
205
- const { x, isBrushActive, isBrushing, className, textProps, fontSize, showTooltip, left } = props
203
+ const { x, isBrushActive, isBrushing, className, textProps, showTooltip, left } = props
206
204
  const pathWidth = 8
207
205
  if (!isBrushActive) {
208
206
  return null
@@ -212,7 +210,7 @@ const BrushHandle = props => {
212
210
  const transform = isLeft ? 'scale(-1, 1)' : 'translate(0,0)'
213
211
  const textAnchor = isLeft ? 'end' : 'start'
214
212
  const tooltipText = isLeft ? ` Drag edges to focus on a specific segment ` : ''
215
- const textWidth = getTextWidth(tooltipText, `normal ${fontSize / 1.1}px sans-serif`)
213
+ const textWidth = getTextWidth(tooltipText, `normal ${appFontSize / 1.1}px sans-serif`)
216
214
 
217
215
  return (
218
216
  <>
@@ -221,7 +219,7 @@ const BrushHandle = props => {
221
219
  x={(Number(textProps.xMax) - textWidth) / 2}
222
220
  dy={-12}
223
221
  pointerEvents='visiblePainted'
224
- fontSize={fontSize / 1.1}
222
+ fontSize={appFontSize / 1.1}
225
223
  >
226
224
  {tooltipText}
227
225
  </Text>
@@ -234,7 +232,7 @@ const BrushHandle = props => {
234
232
  y={25}
235
233
  verticalAnchor='start'
236
234
  textAnchor={textAnchor}
237
- fontSize={fontSize / 1.4}
235
+ fontSize={appFontSize / 1.4}
238
236
  >
239
237
  {isLeft ? textProps.startValue : textProps.endValue}
240
238
  </Text>
@@ -11,10 +11,9 @@ export default {
11
11
  showDownloadMediaButton: false,
12
12
  theme: 'theme-blue',
13
13
  animate: false,
14
- fontSize: 'medium',
15
14
  lineDatapointStyle: 'hover',
16
15
  lineDatapointColor: 'Same as Line',
17
- barHasBorder: 'false',
16
+ barHasBorder: 'true',
18
17
  isLollipopChart: false,
19
18
  lollipopShape: 'circle',
20
19
  lollipopColorStyle: 'two-tone',
@@ -167,6 +166,7 @@ export default {
167
166
  seriesHighlight: [],
168
167
  style: 'circles',
169
168
  subStyle: 'linear blocks',
169
+ shape: 'circle',
170
170
  tickRotation: '',
171
171
  hideBorder: {
172
172
  side: false,
@@ -198,13 +198,16 @@ export default {
198
198
  bottomPrefix: '',
199
199
  bottomAbbreviated: false
200
200
  },
201
+ filters: [],
201
202
  confidenceKeys: {},
202
203
  visual: {
203
204
  border: true,
204
205
  accent: true,
205
206
  background: true,
206
207
  verticalHoverLine: false,
207
- horizontalHoverLine: false
208
+ horizontalHoverLine: false,
209
+ lineDatapointSymbol: 'none',
210
+ maximumShapeAmount: 7
208
211
  },
209
212
  useLogScale: false,
210
213
  filterBehavior: 'Filter Change',
@@ -0,0 +1,68 @@
1
+ import _ from 'lodash'
2
+ import { ChartConfig } from '../types/ChartConfig'
3
+ import * as d3 from 'd3-array'
4
+
5
+ export const getBoxPlotConfig = (newConfig: ChartConfig, data: object[]) => {
6
+ const combinedData = data
7
+ const groups = _.uniq(_.map(combinedData, newConfig.xAxis.dataKey))
8
+ const seriesKeys = _.map(newConfig.series, 'dataKey')
9
+ const plots: any[] = []
10
+
11
+ groups.forEach(g => {
12
+ seriesKeys.forEach(seriesKey => {
13
+ try {
14
+ if (!g) throw new Error('No groups resolved in box plots')
15
+
16
+ // Start handle operations on combinedData
17
+ const { count, sortedData } = _.chain(combinedData)
18
+ // Filter by xAxis data key
19
+ .filter(item => item[newConfig.xAxis.dataKey] === g)
20
+ // perform multiple operations on the filtered data
21
+ .thru(filteredData => ({
22
+ count: filteredData.length,
23
+ sortedData: _.map(filteredData, item => Number(item[seriesKey])).sort()
24
+ }))
25
+ // get the results from the chain
26
+ .value()
27
+
28
+ if (!sortedData) throw new Error('boxplots dont have data yet')
29
+ if (!plots) throw new Error('boxplots dont have plots yet')
30
+
31
+ const q1 = d3.quantile(sortedData, 0.25)
32
+ const q3 = d3.quantile(sortedData, 0.75)
33
+
34
+ const iqr = q3 - q1
35
+ const lowerBounds = q1 - 1.5 * iqr
36
+ const upperBounds = q3 + 1.5 * iqr
37
+ const nonOutliers = sortedData.filter(value => value >= lowerBounds && value <= upperBounds)
38
+ plots.push({
39
+ columnCategory: g,
40
+ columnMax: d3.max(nonOutliers),
41
+ columnThirdQuartile: _.round(q3, newConfig.dataFormat.roundTo),
42
+ columnMedian: Number(d3.median(sortedData)).toFixed(newConfig.dataFormat.roundTo),
43
+ columnFirstQuartile: _.round(q1, newConfig.dataFormat.roundTo),
44
+ columnMin: _.min(nonOutliers),
45
+ columnCount: count,
46
+ columnSd: Number(d3.deviation(sortedData)).toFixed(newConfig.dataFormat.roundTo),
47
+ columnMean: Number(d3.mean(sortedData)).toFixed(newConfig.dataFormat.roundTo),
48
+ columnIqr: _.round(iqr, newConfig.dataFormat.roundTo),
49
+ values: sortedData,
50
+ columnLowerBounds: lowerBounds,
51
+ columnUpperBounds: upperBounds,
52
+ columnOutliers: _.filter(sortedData, value => value < lowerBounds || value > upperBounds),
53
+ columnNonOutliers: _.filter(sortedData, value => value >= lowerBounds && value <= upperBounds)
54
+ })
55
+ } catch (e) {
56
+ console.error('COVE: ', e.message) // eslint-disable-line
57
+ }
58
+ })
59
+ })
60
+
61
+ // Generate a flat list of categories based on seriesKeys and groups
62
+ const categories =
63
+ seriesKeys.length > 1
64
+ ? _.flatMap(groups, value => _.map(seriesKeys, key => `${_.capitalize(key)} - ${_.capitalize(value)}`))
65
+ : groups
66
+
67
+ return [plots, categories]
68
+ }
@@ -0,0 +1,28 @@
1
+ import { colorPalettesChart as colorPalettes, twoColorPalette } from '@cdc/core/data/colorPalettes'
2
+ import { scaleOrdinal } from '@visx/scale'
3
+ import { ChartConfig } from '../types/ChartConfig'
4
+
5
+ export const getColorScale = (config: ChartConfig): ((value: string) => string) => {
6
+ const configPalette = ['Paired Bar', 'Deviation Bar'].includes(config.visualizationType)
7
+ ? 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
12
+ let newColorScale
13
+
14
+ while (numberOfKeys > palette.length) {
15
+ palette = palette.concat(palette)
16
+ }
17
+
18
+ palette = palette.slice(0, numberOfKeys)
19
+
20
+ newColorScale = () =>
21
+ scaleOrdinal({
22
+ domain: config.runtime.seriesLabelsAll,
23
+ range: palette,
24
+ unknown: null
25
+ })
26
+
27
+ return newColorScale
28
+ }
@@ -0,0 +1,42 @@
1
+ import _ from 'lodash'
2
+ import { ChartConfig } from '../types/ChartConfig'
3
+ import * as d3 from 'd3-array'
4
+
5
+ export const getComboChartConfig = (newConfig: ChartConfig) => {
6
+ if (newConfig.visualizationType !== 'Combo' || !newConfig.series) return
7
+
8
+ const runtimeKeys = {
9
+ barSeriesKeys: [],
10
+ lineSeriesKeys: [],
11
+ areaSeriesKeys: [],
12
+ forecastingSeriesKeys: []
13
+ }
14
+
15
+ // Define a mapping of series types to runtime keys
16
+ const seriesTypeMap = new Map([
17
+ ['Area Chart', 'areaSeriesKeys'],
18
+ ['Forecasting', 'forecastingSeriesKeys'],
19
+ ['Bar', 'barSeriesKeys'],
20
+ ['Combo', 'barSeriesKeys'],
21
+ ['Line', 'lineSeriesKeys'],
22
+ ['dashed-sm', 'lineSeriesKeys'],
23
+ ['dashed-md', 'lineSeriesKeys'],
24
+ ['dashed-lg', 'lineSeriesKeys']
25
+ ])
26
+
27
+ newConfig.series.forEach(series => {
28
+ const runtimeKey = seriesTypeMap.get(series.type)
29
+ if (runtimeKey) {
30
+ const valueToPush = runtimeKey === 'barSeriesKeys' || runtimeKey === 'lineSeriesKeys' ? series.dataKey : series
31
+ runtimeKeys[runtimeKey].push(valueToPush)
32
+ }
33
+
34
+ // Change Combo series type to Bar
35
+ if (series.type === 'Combo') {
36
+ series.type = 'Bar'
37
+ }
38
+ })
39
+
40
+ // Assign the processed runtime keys to the configuration
41
+ return { ...newConfig.runtime, ...runtimeKeys }
42
+ }
@@ -0,0 +1,37 @@
1
+ import { isDateScale } from '@cdc/core/helpers/cove/date'
2
+ import _ from 'lodash'
3
+ import { ChartConfig } from '../types/ChartConfig'
4
+ export const getExcludedData = (newConfig: ChartConfig, data: object[]) => {
5
+ let newExcludedData = data
6
+ if (newConfig.exclusions && newConfig.exclusions.active) {
7
+ if (newConfig.xAxis.type === 'categorical' && newConfig.exclusions.keys?.length > 0) {
8
+ newExcludedData = data.filter(e => !newConfig.exclusions.keys.includes(e[newConfig.xAxis.dataKey]))
9
+ } else if (
10
+ isDateScale(newConfig.xAxis) &&
11
+ (newConfig.exclusions.dateStart || newConfig.exclusions.dateEnd) &&
12
+ newConfig.xAxis.dateParseFormat
13
+ ) {
14
+ // Filter dates
15
+ const timestamp = e => new Date(e).getTime()
16
+
17
+ let startDate = timestamp(newConfig.exclusions.dateStart)
18
+ let endDate = timestamp(newConfig.exclusions.dateEnd) + 86399999 //Increase by 24h in ms (86400000ms - 1ms) to include selected end date for .getTime() comparative
19
+
20
+ let startDateValid = undefined !== typeof startDate && false === isNaN(startDate)
21
+ let endDateValid = undefined !== typeof endDate && false === isNaN(endDate)
22
+
23
+ if (startDateValid && endDateValid) {
24
+ newExcludedData = data.filter(
25
+ e => timestamp(e[newConfig.xAxis.dataKey]) >= startDate && timestamp(e[newConfig.xAxis.dataKey]) <= endDate
26
+ )
27
+ } else if (startDateValid) {
28
+ newExcludedData = data.filter(e => timestamp(e[newConfig.xAxis.dataKey]) >= startDate)
29
+ } else if (endDateValid) {
30
+ newExcludedData = data.filter(e => timestamp(e[newConfig.xAxis.dataKey]) <= endDate)
31
+ }
32
+ } else {
33
+ newExcludedData = data
34
+ }
35
+ }
36
+ return newExcludedData
37
+ }
@@ -0,0 +1,7 @@
1
+ export default function getTopAxis(config) {
2
+ // When to show top axis
3
+ const hasTopAxis =
4
+ config.visualizationType === 'Bar' || config.visualizationType === 'Combo' || config.visualizationType === 'Line'
5
+
6
+ return { hasTopAxis }
7
+ }