@cdc/chart 4.24.10 → 4.24.12-2

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 (94) hide show
  1. package/dist/cdcchart.js +35019 -34301
  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/ehdi.json +29939 -0
  22. package/examples/private/test.json +493 -0
  23. package/index.html +10 -7
  24. package/package.json +2 -2
  25. package/src/CdcChart.tsx +132 -152
  26. package/src/_stories/Chart.Anchors.stories.tsx +31 -0
  27. package/src/_stories/Chart.CustomColors.stories.tsx +19 -0
  28. package/src/_stories/Chart.DynamicSeries.stories.tsx +34 -0
  29. package/src/_stories/Chart.Legend.Gradient.stories.tsx +42 -1
  30. package/src/_stories/Chart.stories.tsx +37 -6
  31. package/src/_stories/ChartAxisLabels.stories.tsx +4 -1
  32. package/src/_stories/ChartEditor.stories.tsx +27 -0
  33. package/src/_stories/ChartLine.Suppression.stories.tsx +25 -0
  34. package/src/_stories/ChartPrefixSuffix.stories.tsx +8 -0
  35. package/{examples/feature/area/area-chart-date-city-temperature.json → src/_stories/_mock/area_chart_stacked.json} +125 -27
  36. package/src/_stories/_mock/boxplot_multiseries.json +647 -0
  37. package/src/_stories/_mock/dynamic_series_bar_config.json +723 -0
  38. package/src/_stories/_mock/dynamic_series_config.json +979 -0
  39. package/src/_stories/_mock/line_chart_dynamic_ci.json +493 -0
  40. package/src/_stories/_mock/line_chart_non_dynamic_ci.json +522 -0
  41. package/{examples/feature/scatterplot/scatterplot.json → src/_stories/_mock/scatterplot_mock.json} +62 -92
  42. package/src/_stories/_mock/short_dates.json +288 -0
  43. package/src/_stories/_mock/suppression_mock.json +1549 -0
  44. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +15 -3
  45. package/src/components/Axis/Categorical.Axis.tsx +2 -2
  46. package/src/components/BarChart/components/BarChart.Horizontal.tsx +46 -37
  47. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +43 -9
  48. package/src/components/BarChart/components/BarChart.Vertical.tsx +53 -47
  49. package/src/components/BarChart/helpers/getBarData.ts +28 -0
  50. package/src/components/BarChart/helpers/index.ts +1 -2
  51. package/src/components/BarChart/helpers/tests/getBarData.test.ts +74 -0
  52. package/src/components/BoxPlot/BoxPlot.tsx +131 -0
  53. package/src/components/BoxPlot/helpers/index.ts +54 -0
  54. package/src/components/BrushChart.tsx +23 -26
  55. package/src/components/EditorPanel/EditorPanel.tsx +117 -139
  56. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +3 -3
  57. package/src/components/EditorPanel/components/Panels/Panel.BoxPlot.tsx +51 -6
  58. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +40 -9
  59. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
  60. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +122 -56
  61. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -2
  62. package/src/components/EditorPanel/useEditorPermissions.ts +20 -2
  63. package/src/components/Legend/Legend.Component.tsx +11 -12
  64. package/src/components/Legend/Legend.tsx +16 -16
  65. package/src/components/Legend/helpers/getLegendClasses.ts +59 -0
  66. package/src/components/Legend/helpers/index.ts +2 -1
  67. package/src/components/Legend/tests/getLegendClasses.test.ts +115 -0
  68. package/src/components/LineChart/components/LineChart.Circle.tsx +1 -1
  69. package/src/components/LineChart/helpers.ts +49 -43
  70. package/src/components/LineChart/index.tsx +135 -83
  71. package/src/components/LinearChart.tsx +196 -181
  72. package/src/components/PieChart/PieChart.tsx +7 -1
  73. package/src/components/Sankey/components/ColumnList.tsx +19 -0
  74. package/src/components/Sankey/components/Sankey.tsx +479 -0
  75. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +33 -0
  76. package/src/components/Sankey/index.tsx +1 -492
  77. package/src/components/Sankey/sankey.scss +22 -21
  78. package/src/components/Sankey/types/index.ts +1 -1
  79. package/src/components/Sankey/useSankeyAlert.tsx +60 -0
  80. package/src/components/ScatterPlot/ScatterPlot.jsx +20 -4
  81. package/src/data/initial-state.js +7 -12
  82. package/src/helpers/countNumOfTicks.ts +57 -0
  83. package/src/helpers/getQuartiles.ts +15 -18
  84. package/src/hooks/useMinMax.ts +44 -16
  85. package/src/hooks/useReduceData.ts +43 -10
  86. package/src/hooks/useScales.ts +90 -35
  87. package/src/hooks/useTooltip.tsx +59 -50
  88. package/src/scss/DataTable.scss +5 -0
  89. package/src/scss/main.scss +6 -20
  90. package/src/types/ChartConfig.ts +6 -19
  91. package/src/types/ChartContext.ts +4 -1
  92. package/src/types/ForestPlot.ts +8 -0
  93. package/src/components/BoxPlot/BoxPlot.jsx +0 -111
  94. 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,56 +405,49 @@ 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
423
413
 
424
- const padding = orientation === 'horizontal' ? Number(config.xAxis.size) : Number(config.yAxis.size)
425
- const fontSize = { small: 16, medium: 18, large: 20 }
414
+ if (!yAxisAutoPadding) return
415
+ setYAxisAutoPadding(0)
416
+ }, [maxValue])
417
+ useEffect(() => {
418
+ if (orientation === 'horizontal') return
426
419
 
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
- }
420
+ const maxValueIsGreaterThanTopGridLine = maxValue > Math.max(...yScale.ticks(handleNumTicks))
432
421
 
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
- }
422
+ if (!maxValueIsGreaterThanTopGridLine || !labelsOverflow) return
423
+ const ticks = yScale.ticks(handleNumTicks)
424
+ const tickGap = ticks.length === 1 ? ticks[0] : ticks[1] - ticks[0]
425
+ const nextTick = Math.max(...yScale.ticks(handleNumTicks)) + tickGap
426
+ const divideBy = minValue < 0 ? maxValue / 2 : maxValue
427
+ const calculatedPadding = (nextTick - maxValue) / divideBy
440
428
 
441
- const onMouseMove = event => {
442
- const svgRect = event.currentTarget.getBoundingClientRect()
443
- const x = event.clientX - svgRect.left
444
- const y = event.clientY - svgRect.top
429
+ // if auto padding is too close to next tick, add one more ticks worth of padding
430
+ const PADDING_THRESHOLD = 0.025
431
+ const newPadding =
432
+ calculatedPadding > PADDING_THRESHOLD ? calculatedPadding : calculatedPadding + tickGap / divideBy
445
433
 
446
- setPoint({
447
- x,
448
- y
449
- })
450
- }
434
+ /* sometimes even though the padding is getting to the next tick exactly,
435
+ d3 still doesn't show the tick. we add 0.1 to ensure to tip it over the edge */
436
+ setYAxisAutoPadding(newPadding * 100 + 0.1)
437
+ }, [maxValue, labelsOverflow, yScale, handleNumTicks])
451
438
 
439
+ // Render Functions
452
440
  const generatePairedBarAxis = () => {
453
441
  const axisMaxHeight = bottomLabelStart + BOTTOM_LABEL_PADDING
454
442
 
455
443
  const getTickPositions = (ticks, xScale) => {
456
444
  if (!ticks.length) return false
457
- // filterout first index
445
+ // filter out first index
458
446
  const filteredTicks = ticks.filter(tick => tick.index !== 0)
459
447
  const numberOfTicks = filteredTicks?.length
460
448
  const xMaxHalf = xScale.range()[0] || xMax / 2
461
449
  const tickWidthAll = filteredTicks.map(tick =>
462
- getTextWidth(formatNumber(tick.value, 'left'), `normal ${fontSize[config.fontSize]}px sans-serif`)
450
+ getTextWidth(formatNumber(tick.value, 'left'), `normal ${fontSizes[config.fontSize]}px sans-serif`)
463
451
  )
464
452
  const accumulator = 100
465
453
  const sumOfTickWidth = tickWidthAll.reduce((a, b) => a + b, accumulator)
@@ -498,7 +486,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
498
486
  {props.ticks.map((tick, i) => {
499
487
  const textWidth = getTextWidth(
500
488
  formatNumber(tick.value, 'left'),
501
- `normal ${fontSize[config.fontSize]}px sans-serif`
489
+ `normal ${fontSizes[config.fontSize]}px sans-serif`
502
490
  )
503
491
  const isTicksOverlapping = getTickPositions(props.ticks, g1xScale)
504
492
  const maxTickRotation = Number(config.xAxis.maxTickRotation) || 90
@@ -550,7 +538,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
550
538
  {props.ticks.map((tick, i) => {
551
539
  const textWidth = getTextWidth(
552
540
  formatNumber(tick.value, 'left'),
553
- `normal ${fontSize[config.fontSize]}px sans-serif`
541
+ `normal ${fontSizes[config.fontSize]}px sans-serif`
554
542
  )
555
543
  const isTicksOverlapping = getTickPositions(props.ticks, g2xScale)
556
544
  const maxTickRotation = Number(config.xAxis.maxTickRotation) || 90
@@ -611,7 +599,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
611
599
  ref={svgRef}
612
600
  onMouseMove={onMouseMove}
613
601
  width={parentWidth}
614
- height={parentHeight}
602
+ height={isNoDataAvailable ? 1 : parentHeight}
615
603
  className={`linear ${config.animate ? 'animated' : ''} ${animatedChart && config.animate ? 'animate' : ''} ${
616
604
  debugSvg && 'debug'
617
605
  } ${isDraggingAnnotation && 'dragging-annotation'}`}
@@ -626,7 +614,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
626
614
  <AxisLeft
627
615
  scale={yScale}
628
616
  left={Number(runtime.yAxis.size) - config.yAxis.axisPadding}
629
- numTicks={handleNumTicks()}
617
+ numTicks={handleNumTicks}
630
618
  >
631
619
  {props => {
632
620
  const axisCenter =
@@ -690,7 +678,17 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
690
678
  showTooltip={showTooltip}
691
679
  />
692
680
  )}
693
- {visualizationType === 'Box Plot' && <BoxPlot xScale={xScale} yScale={yScale} />}
681
+ {visualizationType === 'Box Plot' && (
682
+ <BoxPlot
683
+ seriesScale={seriesScale}
684
+ xMax={xMax}
685
+ yMax={yMax}
686
+ min={min}
687
+ max={max}
688
+ xScale={xScale}
689
+ yScale={yScale}
690
+ />
691
+ )}
694
692
  {((visualizationType === 'Area Chart' && config.visualizationSubType === 'regular') ||
695
693
  visualizationType === 'Combo') && (
696
694
  <AreaChart
@@ -778,20 +776,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
778
776
  isBrush={false}
779
777
  />
780
778
  )}
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
779
  {visualizationType === 'Forest Plot' && (
796
780
  <ForestPlot
797
781
  xScale={xScale}
@@ -814,16 +798,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
814
798
  />
815
799
  )}
816
800
  {/*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
- )}
801
+ {config.brush.active && config.xAxis.type !== 'categorical' && <BrushChart xMax={xMax} yMax={yMax} />}
827
802
  {/* Line chart */}
828
803
  {/* TODO: Make this just line or combo? */}
829
804
  {!['Paired Bar', 'Box Plot', 'Area Chart', 'Scatter Plot', 'Deviation Bar', 'Forecasting', 'Bar'].includes(
@@ -845,14 +820,19 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
845
820
  {/* y anchors */}
846
821
  {config.yAxis.anchors &&
847
822
  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)
823
+ let position = yScale(anchor.value)
824
+ let middleOffset = 0
825
+
851
826
  if (!anchor.value) return
852
- const middleOffset =
853
- orientation === 'horizontal' && visualizationType === 'Bar' ? config.barHeight / 4 : 0
827
+ if (config.yAxis.labelPlacement === 'Below Bar') {
828
+ const textOffset = -6.5
829
+ middleOffset = textOffset + Number(config.series.length * config.barHeight) / config.series.length
830
+ } else {
831
+ const paddingOffset = 8
832
+ middleOffset = paddingOffset
833
+ }
854
834
 
855
- if (!anchorPosition) return
835
+ if (!position) return
856
836
 
857
837
  return (
858
838
  // prettier-ignore
@@ -861,8 +841,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
861
841
  strokeDasharray={handleLineType(anchor.lineStyle)}
862
842
  stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
863
843
  className='anchor-y'
864
- from={{ x: 0 + padding, y: anchorPosition - middleOffset}}
865
- to={{ x: width - config.yAxis.rightAxisSize, y: anchorPosition - middleOffset }}
844
+ from={{ x: 0 + padding, y: position - middleOffset}}
845
+ to={{ x: width - config.yAxis.rightAxisSize, y: position - middleOffset }}
866
846
  />
867
847
  )
868
848
  })}
@@ -874,10 +854,19 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
874
854
  newX = yAxis
875
855
  }
876
856
 
877
- let anchorPosition = isDateScale(newX) ? xScale(parseDate(anchor.value, false)) : xScale(anchor.value)
857
+ const getAnchorPosition = (): number | undefined => {
858
+ let position: number | undefined
878
859
 
879
- // have to move up
880
- // const padding = orientation === 'horizontal' ? Number(config.xAxis.size) : Number(config.yAxis.size)
860
+ position = isDateScale(newX) ? xScale(parseDate(anchor.value, false)) : xScale(anchor.value)
861
+ if (config.xAxis.type === 'categorical' || config.xAxis.type === 'date') {
862
+ position = position
863
+ ? position + (newX.type === 'categorical' || newX.type === 'date' ? xScale.bandwidth() : 0) / 2
864
+ : 0
865
+ }
866
+ return position
867
+ }
868
+
869
+ let anchorPosition = getAnchorPosition()
881
870
 
882
871
  if (!anchorPosition) return
883
872
 
@@ -939,7 +928,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
939
928
  />
940
929
  </Group>
941
930
  )}
942
- {config.filters && config.filters.values.length === 0 && data.length === 0 && (
931
+ {isNoDataAvailable && (
943
932
  <Text
944
933
  x={Number(config.yAxis.size) + Number(xMax / 2)}
945
934
  y={initialHeight / 2 - (config.xAxis.padding || 0) / 2}
@@ -1002,7 +991,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1002
991
  label={runtime.yAxis.label || runtime.yAxis.label}
1003
992
  stroke='#333'
1004
993
  tickFormat={handleLeftTickFormatting}
1005
- numTicks={handleNumTicks()}
994
+ numTicks={handleNumTicks}
1006
995
  >
1007
996
  {props => {
1008
997
  const axisCenter =
@@ -1345,43 +1334,63 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1345
1334
  tickFormat={handleBottomTickFormatting}
1346
1335
  scale={xScale}
1347
1336
  stroke='#333'
1348
- numTicks={countNumOfTicks('xAxis')}
1337
+ numTicks={useDateSpanMonths ? dateSpanMonths : xTickCount}
1349
1338
  tickStroke='#333'
1350
1339
  tickValues={
1351
1340
  config.xAxis.manual
1352
- ? getTickValues(
1353
- xAxisDataMapped,
1354
- xScale,
1355
- config.xAxis.type === 'date-time' ? countNumOfTicks('xAxis') : getManualStep(),
1356
- config
1357
- )
1341
+ ? getTickValues(xAxisDataMapped, xScale, isDateTime ? xTickCount : getManualStep(), config)
1342
+ : config.xAxis.type === 'date'
1343
+ ? xAxisDataMapped
1358
1344
  : undefined
1359
1345
  }
1360
1346
  >
1361
1347
  {props => {
1348
+ // For these charts, we generated all ticks in tickValues above, and now need to filter/shift them
1349
+ // so the last tick is always labeled
1350
+ if (config.xAxis.type === 'date' && !config.xAxis.manual) {
1351
+ props.ticks = filterAndShiftLinearDateTicks(config, props, xAxisDataMapped, formatDate)
1352
+ }
1353
+
1354
+ const distanceBetweenTicks =
1355
+ useDateSpanMonths &&
1356
+ xScale
1357
+ .ticks(xTickCount)
1358
+ .map(t => props.ticks.findIndex(tick => tick.value.getTime() === t.getTime()))
1359
+ .slice(0, 2)
1360
+ .reduce((acc, curr) => curr - acc)
1361
+
1362
+ // filter out every [distanceBetweenTicks] tick starting from the end, so the last tick is always labeled
1363
+ const filteredTicks = useDateSpanMonths
1364
+ ? [...props.ticks]
1365
+ .reverse()
1366
+ .filter((_, i) => i % distanceBetweenTicks === 0)
1367
+ .reverse()
1368
+ .map((tick, i, arr) => ({
1369
+ ...tick,
1370
+ // reformat in case showYearsOnce, since first month of year may have changed
1371
+ formattedValue: handleBottomTickFormatting(tick.value, i, arr)
1372
+ }))
1373
+ : props.ticks
1374
+
1362
1375
  const axisMaxHeight = bottomLabelStart + BOTTOM_LABEL_PADDING
1363
1376
 
1364
- const axisCenter =
1365
- config.visualizationType !== 'Forest Plot'
1366
- ? (props.axisToPoint.x - props.axisFromPoint.x) / 2
1367
- : dimensions[0] / 2
1368
1377
  const containsMultipleWords = inputString => /\s/.test(inputString)
1369
- const ismultiLabel = props.ticks.some(tick => containsMultipleWords(tick.value))
1378
+ const ismultiLabel = filteredTicks.some(tick => containsMultipleWords(tick.value))
1370
1379
 
1371
1380
  // Calculate sumOfTickWidth here, before map function
1372
1381
  const tickWidthMax = Math.max(
1373
- ...props.ticks.map(tick =>
1374
- getTextWidth(tick.formattedValue, `normal ${fontSize[config.fontSize]}px sans-serif`)
1382
+ ...filteredTicks.map(tick =>
1383
+ getTextWidth(tick.formattedValue, `normal ${fontSizes[config.fontSize]}px sans-serif`)
1375
1384
  )
1376
1385
  )
1377
1386
  // const marginTop = 20 // moved to top bc need for yMax calcs
1378
1387
  const accumulator = ismultiLabel ? 180 : 100
1379
1388
 
1380
- const textWidths = props.ticks.map(tick =>
1381
- getTextWidth(tick.formattedValue, `normal ${fontSize[config.fontSize]}px sans-serif`)
1389
+ const textWidths = filteredTicks.map(tick =>
1390
+ getTextWidth(tick.formattedValue, `normal ${fontSizes[config.fontSize]}px sans-serif`)
1382
1391
  )
1383
1392
  const sumOfTickWidth = textWidths.reduce((a, b) => a + b, accumulator)
1384
- const spaceBetweenEachTick = (xMax - sumOfTickWidth) / (props.ticks.length - 1)
1393
+ const spaceBetweenEachTick = (xMax - sumOfTickWidth) / (filteredTicks.length - 1)
1385
1394
 
1386
1395
  // Check if ticks are overlapping
1387
1396
  // Determine the position of each tick
@@ -1409,6 +1418,16 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1409
1418
  areTicksTouching = true
1410
1419
  }
1411
1420
 
1421
+ // force wrap it last tick is close to the end of the axis
1422
+ const lastTickWidth = textWidths[textWidths.length - 1]
1423
+ const lastTickPosition = positions[positions.length - 1] + lastTickWidth
1424
+ const lastTickEnd = lastTickPosition + lastTickWidth / 2
1425
+ const lastTickEndThreshold = xMax - lastTickWidth
1426
+
1427
+ if (lastTickEnd > lastTickEndThreshold) {
1428
+ areTicksTouching = true
1429
+ }
1430
+
1412
1431
  const dynamicMarginTop =
1413
1432
  areTicksTouching && config.isResponsiveTicks ? tickWidthMax + DEFAULT_TICK_LENGTH + 20 : 0
1414
1433
 
@@ -1417,15 +1436,11 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1417
1436
 
1418
1437
  return (
1419
1438
  <Group className='bottom-axis' width={dimensions[0]}>
1420
- {props.ticks.map((tick, i, propsTicks) => {
1439
+ {filteredTicks.map((tick, i, propsTicks) => {
1421
1440
  // when using LogScale show major ticks values only
1422
1441
  const showTick = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
1423
1442
  const tickLength = showTick === 'block' ? 16 : DEFAULT_TICK_LENGTH
1424
1443
  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
1444
  const limitedWidth = 100 / propsTicks.length
1430
1445
  //reset rotations by updating config
1431
1446
  config.yAxis.tickRotation =
@@ -1476,7 +1491,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1476
1491
  <Text
1477
1492
  innerRef={xAxisTitleRef}
1478
1493
  className='x-axis-title-label'
1479
- x={axisCenter}
1494
+ x={xMax / 2}
1480
1495
  y={isForestPlot ? 0 /* set via ref */ : axisMaxHeight}
1481
1496
  textAnchor='middle'
1482
1497
  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>