@cdc/chart 4.24.10 → 4.24.12

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 (93) hide show
  1. package/dist/cdcchart.js +34651 -33978
  2. package/examples/feature/boxplot/boxplot-data.json +88 -22
  3. package/examples/feature/boxplot/boxplot.json +540 -16
  4. package/examples/feature/boxplot/testing.csv +7 -7
  5. package/examples/feature/sankey/sankey-example-data.json +126 -14
  6. package/examples/feature/tests-date-exclusions/date-exclusions-config.json +372 -12
  7. package/examples/private/DEV-8850-2.json +493 -0
  8. package/examples/private/DEV-9822.json +574 -0
  9. package/examples/private/DEV-9840.json +553 -0
  10. package/examples/private/DEV-9850-3.json +461 -0
  11. package/examples/private/chart.json +1084 -0
  12. package/examples/private/ci_formatted.json +202 -0
  13. package/examples/private/ci_issue.json +3016 -0
  14. package/examples/private/completed.json +634 -0
  15. package/examples/private/dem-data-long.csv +20 -0
  16. package/examples/private/dem-data-long.json +36 -0
  17. package/examples/private/demographic_data.csv +157 -0
  18. package/examples/private/demographic_data.json +2654 -0
  19. package/examples/private/demographic_dynamic.json +443 -0
  20. package/examples/private/demographic_standard.json +560 -0
  21. package/examples/private/test.json +493 -0
  22. package/index.html +10 -7
  23. package/package.json +2 -2
  24. package/src/CdcChart.tsx +132 -152
  25. package/src/_stories/Chart.Anchors.stories.tsx +31 -0
  26. package/src/_stories/Chart.CustomColors.stories.tsx +19 -0
  27. package/src/_stories/Chart.DynamicSeries.stories.tsx +34 -0
  28. package/src/_stories/Chart.Legend.Gradient.stories.tsx +42 -1
  29. package/src/_stories/Chart.stories.tsx +37 -6
  30. package/src/_stories/ChartAxisLabels.stories.tsx +4 -1
  31. package/src/_stories/ChartEditor.stories.tsx +27 -0
  32. package/src/_stories/ChartLine.Suppression.stories.tsx +25 -0
  33. package/src/_stories/ChartPrefixSuffix.stories.tsx +8 -0
  34. package/{examples/feature/area/area-chart-date-city-temperature.json → src/_stories/_mock/area_chart_stacked.json} +125 -27
  35. package/src/_stories/_mock/boxplot_multiseries.json +647 -0
  36. package/src/_stories/_mock/dynamic_series_bar_config.json +723 -0
  37. package/src/_stories/_mock/dynamic_series_config.json +979 -0
  38. package/src/_stories/_mock/line_chart_dynamic_ci.json +493 -0
  39. package/src/_stories/_mock/line_chart_non_dynamic_ci.json +522 -0
  40. package/{examples/feature/scatterplot/scatterplot.json → src/_stories/_mock/scatterplot_mock.json} +62 -92
  41. package/src/_stories/_mock/short_dates.json +288 -0
  42. package/src/_stories/_mock/suppression_mock.json +1549 -0
  43. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +15 -3
  44. package/src/components/Axis/Categorical.Axis.tsx +2 -2
  45. package/src/components/BarChart/components/BarChart.Horizontal.tsx +46 -37
  46. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +43 -9
  47. package/src/components/BarChart/components/BarChart.Vertical.tsx +53 -47
  48. package/src/components/BarChart/helpers/getBarData.ts +28 -0
  49. package/src/components/BarChart/helpers/index.ts +1 -2
  50. package/src/components/BarChart/helpers/tests/getBarData.test.ts +74 -0
  51. package/src/components/BoxPlot/BoxPlot.tsx +131 -0
  52. package/src/components/BoxPlot/helpers/index.ts +54 -0
  53. package/src/components/BrushChart.tsx +23 -26
  54. package/src/components/EditorPanel/EditorPanel.tsx +117 -139
  55. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +3 -3
  56. package/src/components/EditorPanel/components/Panels/Panel.BoxPlot.tsx +51 -6
  57. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +40 -9
  58. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
  59. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +122 -56
  60. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -2
  61. package/src/components/EditorPanel/useEditorPermissions.ts +20 -2
  62. package/src/components/Legend/Legend.Component.tsx +11 -12
  63. package/src/components/Legend/Legend.tsx +16 -16
  64. package/src/components/Legend/helpers/getLegendClasses.ts +59 -0
  65. package/src/components/Legend/helpers/index.ts +2 -1
  66. package/src/components/Legend/tests/getLegendClasses.test.ts +115 -0
  67. package/src/components/LineChart/components/LineChart.Circle.tsx +1 -1
  68. package/src/components/LineChart/helpers.ts +49 -43
  69. package/src/components/LineChart/index.tsx +135 -83
  70. package/src/components/LinearChart.tsx +187 -181
  71. package/src/components/PieChart/PieChart.tsx +7 -1
  72. package/src/components/Sankey/components/ColumnList.tsx +19 -0
  73. package/src/components/Sankey/components/Sankey.tsx +479 -0
  74. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +33 -0
  75. package/src/components/Sankey/index.tsx +1 -492
  76. package/src/components/Sankey/sankey.scss +22 -21
  77. package/src/components/Sankey/types/index.ts +1 -1
  78. package/src/components/Sankey/useSankeyAlert.tsx +60 -0
  79. package/src/components/ScatterPlot/ScatterPlot.jsx +20 -4
  80. package/src/data/initial-state.js +7 -12
  81. package/src/helpers/countNumOfTicks.ts +57 -0
  82. package/src/helpers/getQuartiles.ts +15 -18
  83. package/src/hooks/useMinMax.ts +44 -16
  84. package/src/hooks/useReduceData.ts +43 -10
  85. package/src/hooks/useScales.ts +90 -35
  86. package/src/hooks/useTooltip.tsx +59 -50
  87. package/src/scss/DataTable.scss +5 -0
  88. package/src/scss/main.scss +6 -20
  89. package/src/types/ChartConfig.ts +6 -19
  90. package/src/types/ChartContext.ts +4 -1
  91. package/src/types/ForestPlot.ts +8 -0
  92. package/src/components/BoxPlot/BoxPlot.jsx +0 -111
  93. package/src/hooks/useLegendClasses.ts +0 -72
@@ -35,12 +35,14 @@ import { calcInitialHeight } from '../helpers/sizeHelpers'
35
35
  import useMinMax from '../hooks/useMinMax'
36
36
  import useReduceData from '../hooks/useReduceData'
37
37
  import useRightAxis from '../hooks/useRightAxis'
38
- import useScales, { getTickValues } from '../hooks/useScales'
38
+ import useScales, { getTickValues, filterAndShiftLinearDateTicks } from '../hooks/useScales'
39
39
  import useTopAxis from '../hooks/useTopAxis'
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
+ import { countNumOfTicks } from '../helpers/countNumOfTicks'
44
46
 
45
47
  type LinearChartProps = {
46
48
  parentWidth: number
@@ -50,6 +52,7 @@ type LinearChartProps = {
50
52
  const BOTTOM_LABEL_PADDING = 9
51
53
  const X_TICK_LABEL_PADDING = 3
52
54
  const DEFAULT_TICK_LENGTH = 8
55
+ const MONTH_AS_MS = 1000 * 60 * 60 * 24 * 30
53
56
 
54
57
  const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, parentWidth }, svgRef) => {
55
58
  // prettier-ignore
@@ -100,6 +103,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
100
103
  const [animatedChart, setAnimatedChart] = useState(false)
101
104
  const [point, setPoint] = useState({ x: 0, y: 0 })
102
105
  const [suffixWidth, setSuffixWidth] = useState(0)
106
+ const [yAxisAutoPadding, setYAxisAutoPadding] = useState(0)
103
107
 
104
108
  // REFS
105
109
  const axisBottomRef = useRef(null)
@@ -109,7 +113,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
109
113
  const triggerRef = useRef()
110
114
  const xAxisLabelRefs = useRef([])
111
115
  const xAxisTitleRef = useRef(null)
112
- const prevTickRef = useRef(null)
116
+ const lastMaxValue = useRef(maxValue)
113
117
 
114
118
  const dataRef = useIntersectionObserver(triggerRef, {
115
119
  freezeOnceVisible: false
@@ -120,8 +124,10 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
120
124
  const isHorizontal = orientation === 'horizontal' || config.visualizationType === 'Forest Plot'
121
125
  const isLogarithmicAxis = config.yAxis.type === 'logarithmic'
122
126
  const isForestPlot = visualizationType === 'Forest Plot'
127
+ const isDateTime = config.xAxis.type === 'date-time'
123
128
  const suffixHasNoSpace = !suffix.includes(' ')
124
-
129
+ const labelsOverflow = onlyShowTopPrefixSuffix && !suffixHasNoSpace
130
+ const padding = orientation === 'horizontal' ? Number(config.xAxis.size) : Number(config.yAxis.size)
125
131
  const yLabelOffset = isNaN(parseInt(`${runtime.yAxis.labelOffset}`)) ? 0 : parseInt(`${runtime.yAxis.labelOffset}`)
126
132
 
127
133
  // zero if not forest plot
@@ -130,7 +136,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
130
136
  // height before bottom axis
131
137
  const initialHeight = useMemo(
132
138
  () => calcInitialHeight(config, currentViewport),
133
- [config, currentViewport, parentHeight]
139
+ [config, currentViewport, parentHeight, config.heights?.vertical, config.heights?.horizontal]
134
140
  )
135
141
  const forestHeight = useMemo(() => initialHeight + forestRowsHeight, [initialHeight, forestRowsHeight])
136
142
 
@@ -170,11 +176,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
170
176
  const xMax = width - runtime.yAxis.size - (visualizationType === 'Combo' ? config.yAxis.rightAxisSize : 0)
171
177
  const yMax = initialHeight + forestRowsHeight
172
178
 
173
- const checkLineToBarGraph = () => {
174
- return isConvertLineToBarGraph(config.visualizationType, data, config.allowLineToBarGraph)
175
- }
179
+ const isNoDataAvailable = config.filters && config.filters.values.length === 0 && data.length === 0
176
180
 
177
- // GETTERS & FUNCTIONS
178
181
  const getXAxisData = d =>
179
182
  isDateScale(config.runtime.xAxis)
180
183
  ? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime()
@@ -188,7 +191,14 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
188
191
  const properties = {
189
192
  data,
190
193
  tableData,
191
- config,
194
+ config: {
195
+ ...config,
196
+ yAxis: {
197
+ ...config.yAxis,
198
+ scalePadding: labelsOverflow ? yAxisAutoPadding : config.yAxis.scalePadding,
199
+ enablePadding: labelsOverflow || config.yAxis.enablePadding
200
+ }
201
+ },
192
202
  minValue,
193
203
  maxValue,
194
204
  isAllLine,
@@ -209,6 +219,40 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
209
219
  xMax: parentWidth - Number(config.orientation === 'horizontal' ? config.xAxis.size : config.yAxis.size)
210
220
  })
211
221
 
222
+ const [yTickCount, xTickCount] = ['yAxis', 'xAxis'].map(axis =>
223
+ countNumOfTicks({ axis, max, runtime, currentViewport, isHorizontal, data, config, min })
224
+ )
225
+ const handleNumTicks = isForestPlot ? config.data.length : yTickCount
226
+
227
+ // Tooltip Helpers
228
+ const { tooltipData, showTooltip, hideTooltip, tooltipOpen, tooltipLeft, tooltipTop } = useTooltip()
229
+
230
+ // prettier-ignore
231
+ const {
232
+ handleTooltipMouseOver,
233
+ handleTooltipClick,
234
+ handleTooltipMouseOff,
235
+ TooltipListItem,
236
+ getXValueFromCoordinate
237
+ } = useCoveTooltip({
238
+ xScale,
239
+ yScale,
240
+ showTooltip,
241
+ hideTooltip
242
+ })
243
+ // get the number of months between the first and last date
244
+ const { dataKey } = runtime.xAxis
245
+ const dateSpanMonths =
246
+ data.length && isDateTime
247
+ ? [0, data.length - 1].map(i => parseDate(data[i][dataKey])).reduce((a, b) => Math.abs(a - b)) / MONTH_AS_MS
248
+ : 0
249
+ const useDateSpanMonths = isDateTime && dateSpanMonths > xTickCount
250
+
251
+ // GETTERS & FUNCTIONS
252
+ const checkLineToBarGraph = () => {
253
+ return isConvertLineToBarGraph(config.visualizationType, data, config.allowLineToBarGraph)
254
+ }
255
+
212
256
  const handleLeftTickFormatting = (tick, index, ticks) => {
213
257
  if (isLogarithmicAxis && tick === 0.1) {
214
258
  //when logarithmic scale applied change value of first tick
@@ -227,16 +271,14 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
227
271
  return tick
228
272
  }
229
273
 
230
- const handleBottomTickFormatting = tick => {
274
+ const handleBottomTickFormatting = (tick, i, ticks) => {
231
275
  if (isLogarithmicAxis && tick === 0.1) {
232
276
  // when logarithmic scale applied change value FIRST of tick
233
277
  tick = 0
234
278
  }
235
279
 
236
280
  if (isDateScale(runtime.xAxis) && config.visualizationType !== 'Forest Plot') {
237
- const formattedDate = formatDate(tick, prevTickRef.current)
238
- prevTickRef.current = tick
239
- return formattedDate
281
+ return formatDate(tick, i, ticks)
240
282
  }
241
283
  if (orientation === 'horizontal' && config.visualizationType !== 'Forest Plot')
242
284
  return formatNumber(tick, 'left', shouldAbbreviate)
@@ -254,80 +296,33 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
254
296
  return tick
255
297
  }
256
298
 
257
- const countNumOfTicks = axis => {
258
- let { numTicks } = runtime[axis]
259
- if (runtime[axis].viewportNumTicks && runtime[axis].viewportNumTicks[currentViewport]) {
260
- numTicks = runtime[axis].viewportNumTicks[currentViewport]
261
- }
262
- let tickCount = undefined
263
-
264
- if (axis === 'yAxis') {
265
- tickCount =
266
- isHorizontal && !numTicks
267
- ? data.length
268
- : isHorizontal && numTicks
269
- ? numTicks
270
- : !isHorizontal && !numTicks
271
- ? undefined
272
- : !isHorizontal && numTicks && numTicks
273
- // to fix edge case of small numbers with decimals
274
- if (tickCount === undefined && !config.dataFormat.roundTo) {
275
- // then it is set to Auto
276
- if (Number(max) <= 3) {
277
- tickCount = 2
278
- } else {
279
- tickCount = 4 // same default as standalone components
280
- }
281
- }
282
- if (Number(tickCount) > Number(max)) {
283
- // cap it and round it so its an integer
284
- tickCount = Number(min) < 0 ? Math.round(max) * 2 : Math.round(max)
285
- }
286
- }
287
-
288
- if (axis === 'xAxis') {
289
- tickCount =
290
- isHorizontal && !numTicks
291
- ? undefined
292
- : isHorizontal && numTicks
293
- ? numTicks
294
- : !isHorizontal && !numTicks
295
- ? undefined
296
- : !isHorizontal && numTicks && numTicks
297
- if (isHorizontal && tickCount === undefined && !config.dataFormat.roundTo) {
298
- // then it is set to Auto
299
- // - check for small numbers situation
300
- if (max <= 3) {
301
- tickCount = 2
302
- } else {
303
- tickCount = 4 // same default as standalone components
304
- }
305
- }
299
+ const chartHasTooltipGuides = () => {
300
+ const { visualizationType } = config
301
+ if (visualizationType === 'Combo' && runtime.forecastingSeriesKeys > 0) return true
302
+ if (visualizationType === 'Area Chart') return true
303
+ if (visualizationType === 'Line') return true
304
+ if (visualizationType === 'Bar') return true
305
+ return false
306
+ }
306
307
 
307
- if (config.visualizationType === 'Forest Plot') {
308
- tickCount = config.yAxis.numTicks !== '' ? config.yAxis.numTicks : 4
309
- }
308
+ const getManualStep = () => {
309
+ let manualStep = config.xAxis.manualStep
310
+ if (config.xAxis.viewportStepCount && config.xAxis.viewportStepCount[currentViewport]) {
311
+ manualStep = config.xAxis.viewportStepCount[currentViewport]
310
312
  }
311
-
312
- return tickCount
313
+ return manualStep
313
314
  }
314
315
 
315
- // Tooltip Helpers
316
- const { tooltipData, showTooltip, hideTooltip, tooltipOpen, tooltipLeft, tooltipTop } = useTooltip()
316
+ const onMouseMove = event => {
317
+ const svgRect = event.currentTarget.getBoundingClientRect()
318
+ const x = event.clientX - svgRect.left
319
+ const y = event.clientY - svgRect.top
317
320
 
318
- // prettier-ignore
319
- const {
320
- handleTooltipMouseOver,
321
- handleTooltipClick,
322
- handleTooltipMouseOff,
323
- TooltipListItem,
324
- getXValueFromCoordinate
325
- } = useCoveTooltip({
326
- xScale,
327
- yScale,
328
- showTooltip,
329
- hideTooltip
330
- })
321
+ setPoint({
322
+ x,
323
+ y
324
+ })
325
+ }
331
326
 
332
327
  // EFFECTS
333
328
 
@@ -410,45 +405,29 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
410
405
  const legendIsLeftOrRight =
411
406
  legend?.position !== 'top' && legend?.position !== 'bottom' && !isLegendWrapViewport(currentViewport)
412
407
  legendRef.current.style.transform = legendIsLeftOrRight ? `translateY(${topLabelOnGridlineHeight}px)` : 'none'
413
- }, [axisBottomRef.current, config, bottomLabelStart, brush, currentViewport, topYLabelRef.current])
408
+ }, [axisBottomRef.current, config, bottomLabelStart, brush, currentViewport, topYLabelRef.current, initialHeight])
414
409
 
415
- const chartHasTooltipGuides = () => {
416
- const { visualizationType } = config
417
- if (visualizationType === 'Combo' && runtime.forecastingSeriesKeys > 0) return true
418
- if (visualizationType === 'Area Chart') return true
419
- if (visualizationType === 'Line') return true
420
- if (visualizationType === 'Bar') return true
421
- return false
422
- }
410
+ useEffect(() => {
411
+ if (lastMaxValue.current === maxValue) return
412
+ lastMaxValue.current = maxValue
413
+ if (!yAxisAutoPadding) return
414
+ setYAxisAutoPadding(0)
415
+ }, [maxValue])
416
+ useEffect(() => {
417
+ if (orientation === 'horizontal') return
423
418
 
424
- const padding = orientation === 'horizontal' ? Number(config.xAxis.size) : Number(config.yAxis.size)
425
- const fontSize = { small: 16, medium: 18, large: 20 }
419
+ const maxValueIsGreaterThanTopGridLine = maxValue > Math.max(...yScale.ticks(handleNumTicks))
426
420
 
427
- const handleNumTicks = () => {
428
- // On forest plots we need to return every "study" or y axis value.
429
- if (config.visualizationType === 'Forest Plot') return config.data.length
430
- return countNumOfTicks('yAxis')
431
- }
421
+ if (!maxValueIsGreaterThanTopGridLine || !labelsOverflow) return
432
422
 
433
- const getManualStep = () => {
434
- let manualStep = config.xAxis.manualStep
435
- if (config.xAxis.viewportStepCount && config.xAxis.viewportStepCount[currentViewport]) {
436
- manualStep = config.xAxis.viewportStepCount[currentViewport]
437
- }
438
- return manualStep
439
- }
440
-
441
- const onMouseMove = event => {
442
- const svgRect = event.currentTarget.getBoundingClientRect()
443
- const x = event.clientX - svgRect.left
444
- const y = event.clientY - svgRect.top
423
+ const tickGap = yScale.ticks(handleNumTicks)[1] - yScale.ticks(handleNumTicks)[0]
424
+ const nextTick = Math.max(...yScale.ticks(handleNumTicks)) + tickGap
425
+ const newPadding = minValue < 0 ? (nextTick - maxValue) / maxValue / 2 : (nextTick - maxValue) / maxValue
445
426
 
446
- setPoint({
447
- x,
448
- y
449
- })
450
- }
427
+ setYAxisAutoPadding(newPadding * 100)
428
+ }, [maxValue, labelsOverflow, yScale, handleNumTicks])
451
429
 
430
+ // Render Functions
452
431
  const generatePairedBarAxis = () => {
453
432
  const axisMaxHeight = bottomLabelStart + BOTTOM_LABEL_PADDING
454
433
 
@@ -459,7 +438,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
459
438
  const numberOfTicks = filteredTicks?.length
460
439
  const xMaxHalf = xScale.range()[0] || xMax / 2
461
440
  const tickWidthAll = filteredTicks.map(tick =>
462
- getTextWidth(formatNumber(tick.value, 'left'), `normal ${fontSize[config.fontSize]}px sans-serif`)
441
+ getTextWidth(formatNumber(tick.value, 'left'), `normal ${fontSizes[config.fontSize]}px sans-serif`)
463
442
  )
464
443
  const accumulator = 100
465
444
  const sumOfTickWidth = tickWidthAll.reduce((a, b) => a + b, accumulator)
@@ -498,7 +477,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
498
477
  {props.ticks.map((tick, i) => {
499
478
  const textWidth = getTextWidth(
500
479
  formatNumber(tick.value, 'left'),
501
- `normal ${fontSize[config.fontSize]}px sans-serif`
480
+ `normal ${fontSizes[config.fontSize]}px sans-serif`
502
481
  )
503
482
  const isTicksOverlapping = getTickPositions(props.ticks, g1xScale)
504
483
  const maxTickRotation = Number(config.xAxis.maxTickRotation) || 90
@@ -550,7 +529,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
550
529
  {props.ticks.map((tick, i) => {
551
530
  const textWidth = getTextWidth(
552
531
  formatNumber(tick.value, 'left'),
553
- `normal ${fontSize[config.fontSize]}px sans-serif`
532
+ `normal ${fontSizes[config.fontSize]}px sans-serif`
554
533
  )
555
534
  const isTicksOverlapping = getTickPositions(props.ticks, g2xScale)
556
535
  const maxTickRotation = Number(config.xAxis.maxTickRotation) || 90
@@ -611,7 +590,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
611
590
  ref={svgRef}
612
591
  onMouseMove={onMouseMove}
613
592
  width={parentWidth}
614
- height={parentHeight}
593
+ height={isNoDataAvailable ? 1 : parentHeight}
615
594
  className={`linear ${config.animate ? 'animated' : ''} ${animatedChart && config.animate ? 'animate' : ''} ${
616
595
  debugSvg && 'debug'
617
596
  } ${isDraggingAnnotation && 'dragging-annotation'}`}
@@ -626,7 +605,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
626
605
  <AxisLeft
627
606
  scale={yScale}
628
607
  left={Number(runtime.yAxis.size) - config.yAxis.axisPadding}
629
- numTicks={handleNumTicks()}
608
+ numTicks={handleNumTicks}
630
609
  >
631
610
  {props => {
632
611
  const axisCenter =
@@ -690,7 +669,17 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
690
669
  showTooltip={showTooltip}
691
670
  />
692
671
  )}
693
- {visualizationType === 'Box Plot' && <BoxPlot xScale={xScale} yScale={yScale} />}
672
+ {visualizationType === 'Box Plot' && (
673
+ <BoxPlot
674
+ seriesScale={seriesScale}
675
+ xMax={xMax}
676
+ yMax={yMax}
677
+ min={min}
678
+ max={max}
679
+ xScale={xScale}
680
+ yScale={yScale}
681
+ />
682
+ )}
694
683
  {((visualizationType === 'Area Chart' && config.visualizationSubType === 'regular') ||
695
684
  visualizationType === 'Combo') && (
696
685
  <AreaChart
@@ -778,20 +767,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
778
767
  isBrush={false}
779
768
  />
780
769
  )}
781
- {/* y anchors */}
782
- {config.yAxis.anchors &&
783
- config.yAxis.anchors.map(anchor => {
784
- return (
785
- <Line
786
- strokeDasharray={handleLineType(anchor.lineStyle)}
787
- stroke='rgba(0,0,0,1)'
788
- className='customAnchor'
789
- from={{ x: 0 + config.yAxis.size, y: yScale(anchor.value) }}
790
- to={{ x: xMax, y: yScale(anchor.value) }}
791
- display={runtime.horizontal ? 'none' : 'block'}
792
- />
793
- )
794
- })}
795
770
  {visualizationType === 'Forest Plot' && (
796
771
  <ForestPlot
797
772
  xScale={xScale}
@@ -814,16 +789,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
814
789
  />
815
790
  )}
816
791
  {/*Brush chart */}
817
- {config.brush.active && config.xAxis.type !== 'categorical' && (
818
- <BrushChart
819
- xScaleBrush={xScaleBrush}
820
- yScale={yScale}
821
- xMax={xMax}
822
- yMax={yMax}
823
- xScale={xScale}
824
- seriesScale={seriesScale}
825
- />
826
- )}
792
+ {config.brush.active && config.xAxis.type !== 'categorical' && <BrushChart xMax={xMax} yMax={yMax} />}
827
793
  {/* Line chart */}
828
794
  {/* TODO: Make this just line or combo? */}
829
795
  {!['Paired Bar', 'Box Plot', 'Area Chart', 'Scatter Plot', 'Deviation Bar', 'Forecasting', 'Bar'].includes(
@@ -845,14 +811,19 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
845
811
  {/* y anchors */}
846
812
  {config.yAxis.anchors &&
847
813
  config.yAxis.anchors.map((anchor, index) => {
848
- let anchorPosition = yScale(anchor.value)
849
- // have to move up
850
- // const padding = orientation === 'horizontal' ? Number(config.xAxis.size) : Number(config.yAxis.size)
814
+ let position = yScale(anchor.value)
815
+ let middleOffset = 0
816
+
851
817
  if (!anchor.value) return
852
- const middleOffset =
853
- orientation === 'horizontal' && visualizationType === 'Bar' ? config.barHeight / 4 : 0
818
+ if (config.yAxis.labelPlacement === 'Below Bar') {
819
+ const textOffset = -6.5
820
+ middleOffset = textOffset + Number(config.series.length * config.barHeight) / config.series.length
821
+ } else {
822
+ const paddingOffset = 8
823
+ middleOffset = paddingOffset
824
+ }
854
825
 
855
- if (!anchorPosition) return
826
+ if (!position) return
856
827
 
857
828
  return (
858
829
  // prettier-ignore
@@ -861,8 +832,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
861
832
  strokeDasharray={handleLineType(anchor.lineStyle)}
862
833
  stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
863
834
  className='anchor-y'
864
- from={{ x: 0 + padding, y: anchorPosition - middleOffset}}
865
- to={{ x: width - config.yAxis.rightAxisSize, y: anchorPosition - middleOffset }}
835
+ from={{ x: 0 + padding, y: position - middleOffset}}
836
+ to={{ x: width - config.yAxis.rightAxisSize, y: position - middleOffset }}
866
837
  />
867
838
  )
868
839
  })}
@@ -874,10 +845,19 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
874
845
  newX = yAxis
875
846
  }
876
847
 
877
- let anchorPosition = isDateScale(newX) ? xScale(parseDate(anchor.value, false)) : xScale(anchor.value)
848
+ const getAnchorPosition = (): number | undefined => {
849
+ let position: number | undefined
878
850
 
879
- // have to move up
880
- // const padding = orientation === 'horizontal' ? Number(config.xAxis.size) : Number(config.yAxis.size)
851
+ position = isDateScale(newX) ? xScale(parseDate(anchor.value, false)) : xScale(anchor.value)
852
+ if (config.xAxis.type === 'categorical' || config.xAxis.type === 'date') {
853
+ position = position
854
+ ? position + (newX.type === 'categorical' || newX.type === 'date' ? xScale.bandwidth() : 0) / 2
855
+ : 0
856
+ }
857
+ return position
858
+ }
859
+
860
+ let anchorPosition = getAnchorPosition()
881
861
 
882
862
  if (!anchorPosition) return
883
863
 
@@ -939,7 +919,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
939
919
  />
940
920
  </Group>
941
921
  )}
942
- {config.filters && config.filters.values.length === 0 && data.length === 0 && (
922
+ {isNoDataAvailable && (
943
923
  <Text
944
924
  x={Number(config.yAxis.size) + Number(xMax / 2)}
945
925
  y={initialHeight / 2 - (config.xAxis.padding || 0) / 2}
@@ -1002,7 +982,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1002
982
  label={runtime.yAxis.label || runtime.yAxis.label}
1003
983
  stroke='#333'
1004
984
  tickFormat={handleLeftTickFormatting}
1005
- numTicks={handleNumTicks()}
985
+ numTicks={handleNumTicks}
1006
986
  >
1007
987
  {props => {
1008
988
  const axisCenter =
@@ -1345,43 +1325,63 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1345
1325
  tickFormat={handleBottomTickFormatting}
1346
1326
  scale={xScale}
1347
1327
  stroke='#333'
1348
- numTicks={countNumOfTicks('xAxis')}
1328
+ numTicks={useDateSpanMonths ? dateSpanMonths : xTickCount}
1349
1329
  tickStroke='#333'
1350
1330
  tickValues={
1351
1331
  config.xAxis.manual
1352
- ? getTickValues(
1353
- xAxisDataMapped,
1354
- xScale,
1355
- config.xAxis.type === 'date-time' ? countNumOfTicks('xAxis') : getManualStep(),
1356
- config
1357
- )
1332
+ ? getTickValues(xAxisDataMapped, xScale, isDateTime ? xTickCount : getManualStep(), config)
1333
+ : config.xAxis.type === 'date'
1334
+ ? xAxisDataMapped
1358
1335
  : undefined
1359
1336
  }
1360
1337
  >
1361
1338
  {props => {
1339
+ // For these charts, we generated all ticks in tickValues above, and now need to filter/shift them
1340
+ // so the last tick is always labeled
1341
+ if (config.xAxis.type === 'date' && !config.xAxis.manual) {
1342
+ props.ticks = filterAndShiftLinearDateTicks(config, props, xAxisDataMapped, formatDate)
1343
+ }
1344
+
1345
+ const distanceBetweenTicks =
1346
+ useDateSpanMonths &&
1347
+ xScale
1348
+ .ticks(xTickCount)
1349
+ .map(t => props.ticks.findIndex(tick => tick.value.getTime() === t.getTime()))
1350
+ .slice(0, 2)
1351
+ .reduce((acc, curr) => curr - acc)
1352
+
1353
+ // filter out every [distanceBetweenTicks] tick starting from the end, so the last tick is always labeled
1354
+ const filteredTicks = useDateSpanMonths
1355
+ ? [...props.ticks]
1356
+ .reverse()
1357
+ .filter((_, i) => i % distanceBetweenTicks === 0)
1358
+ .reverse()
1359
+ .map((tick, i, arr) => ({
1360
+ ...tick,
1361
+ // reformat in case showYearsOnce, since first month of year may have changed
1362
+ formattedValue: handleBottomTickFormatting(tick.value, i, arr)
1363
+ }))
1364
+ : props.ticks
1365
+
1362
1366
  const axisMaxHeight = bottomLabelStart + BOTTOM_LABEL_PADDING
1363
1367
 
1364
- const axisCenter =
1365
- config.visualizationType !== 'Forest Plot'
1366
- ? (props.axisToPoint.x - props.axisFromPoint.x) / 2
1367
- : dimensions[0] / 2
1368
1368
  const containsMultipleWords = inputString => /\s/.test(inputString)
1369
- const ismultiLabel = props.ticks.some(tick => containsMultipleWords(tick.value))
1369
+ const ismultiLabel = filteredTicks.some(tick => containsMultipleWords(tick.value))
1370
1370
 
1371
1371
  // Calculate sumOfTickWidth here, before map function
1372
1372
  const tickWidthMax = Math.max(
1373
- ...props.ticks.map(tick =>
1374
- getTextWidth(tick.formattedValue, `normal ${fontSize[config.fontSize]}px sans-serif`)
1373
+ ...filteredTicks.map(tick =>
1374
+ getTextWidth(tick.formattedValue, `normal ${fontSizes[config.fontSize]}px sans-serif`)
1375
1375
  )
1376
1376
  )
1377
1377
  // const marginTop = 20 // moved to top bc need for yMax calcs
1378
1378
  const accumulator = ismultiLabel ? 180 : 100
1379
1379
 
1380
- const textWidths = props.ticks.map(tick =>
1381
- getTextWidth(tick.formattedValue, `normal ${fontSize[config.fontSize]}px sans-serif`)
1380
+ const textWidths = filteredTicks.map(tick =>
1381
+ getTextWidth(tick.formattedValue, `normal ${fontSizes[config.fontSize]}px sans-serif`)
1382
1382
  )
1383
1383
  const sumOfTickWidth = textWidths.reduce((a, b) => a + b, accumulator)
1384
- const spaceBetweenEachTick = (xMax - sumOfTickWidth) / (props.ticks.length - 1)
1384
+ const spaceBetweenEachTick = (xMax - sumOfTickWidth) / (filteredTicks.length - 1)
1385
1385
 
1386
1386
  // Check if ticks are overlapping
1387
1387
  // Determine the position of each tick
@@ -1409,6 +1409,16 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1409
1409
  areTicksTouching = true
1410
1410
  }
1411
1411
 
1412
+ // force wrap it last tick is close to the end of the axis
1413
+ const lastTickWidth = textWidths[textWidths.length - 1]
1414
+ const lastTickPosition = positions[positions.length - 1] + lastTickWidth
1415
+ const lastTickEnd = lastTickPosition + lastTickWidth / 2
1416
+ const lastTickEndThreshold = xMax - lastTickWidth
1417
+
1418
+ if (lastTickEnd > lastTickEndThreshold) {
1419
+ areTicksTouching = true
1420
+ }
1421
+
1412
1422
  const dynamicMarginTop =
1413
1423
  areTicksTouching && config.isResponsiveTicks ? tickWidthMax + DEFAULT_TICK_LENGTH + 20 : 0
1414
1424
 
@@ -1417,15 +1427,11 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1417
1427
 
1418
1428
  return (
1419
1429
  <Group className='bottom-axis' width={dimensions[0]}>
1420
- {props.ticks.map((tick, i, propsTicks) => {
1430
+ {filteredTicks.map((tick, i, propsTicks) => {
1421
1431
  // when using LogScale show major ticks values only
1422
1432
  const showTick = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
1423
1433
  const tickLength = showTick === 'block' ? 16 : DEFAULT_TICK_LENGTH
1424
1434
  const to = { x: tick.to.x, y: tickLength }
1425
- const textWidth = getTextWidth(
1426
- tick.formattedValue,
1427
- `normal ${fontSize[config.fontSize]}px sans-serif`
1428
- )
1429
1435
  const limitedWidth = 100 / propsTicks.length
1430
1436
  //reset rotations by updating config
1431
1437
  config.yAxis.tickRotation =
@@ -1476,7 +1482,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1476
1482
  <Text
1477
1483
  innerRef={xAxisTitleRef}
1478
1484
  className='x-axis-title-label'
1479
- x={axisCenter}
1485
+ x={xMax / 2}
1480
1486
  y={isForestPlot ? 0 /* set via ref */ : axisMaxHeight}
1481
1487
  textAnchor='middle'
1482
1488
  verticalAnchor='start'
@@ -170,6 +170,7 @@ const PieChart = props => {
170
170
  )
171
171
  })}
172
172
  {transitions.map(({ item: arc, key }, i) => {
173
+ const roundTo = Number(config.dataFormat.roundTo) || 0
173
174
  const [centroidX, centroidY] = path.centroid(arc)
174
175
  const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.1
175
176
 
@@ -177,6 +178,11 @@ const PieChart = props => {
177
178
  if (_colorScale(arc.data[config.runtime.xAxis.dataKey])) {
178
179
  textColor = getContrastColor(textColor, _colorScale(arc.data[config.runtime.xAxis.dataKey]))
179
180
  }
181
+ const degrees = ((arc.endAngle - arc.startAngle) * 180) / Math.PI
182
+
183
+ // Calculate the percentage of the full circle (360 degrees)
184
+ const percentageOfCircle = (degrees / 360) * 100
185
+ const roundedPercentage = percentageOfCircle.toFixed(roundTo)
180
186
 
181
187
  return (
182
188
  <animated.g key={`${key}${i}`}>
@@ -189,7 +195,7 @@ const PieChart = props => {
189
195
  textAnchor='middle'
190
196
  pointerEvents='none'
191
197
  >
192
- {Math.round((((arc.endAngle - arc.startAngle) * 180) / Math.PI / 360) * 100) + '%'}
198
+ {roundedPercentage + '%'}
193
199
  </Text>
194
200
  )}
195
201
  </animated.g>
@@ -0,0 +1,19 @@
1
+ interface ColumnData {
2
+ label: string
3
+ value: string
4
+ additional_info: string
5
+ }
6
+
7
+ const ColumnList = ({ columnData }) => {
8
+ return (
9
+ <ul>
10
+ {columnData?.map((entry, index) => (
11
+ <li key={index}>
12
+ {entry.label}: {entry.value} ({entry.additional_info}%)
13
+ </li>
14
+ ))}
15
+ </ul>
16
+ )
17
+ }
18
+
19
+ export default ColumnList