@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.
- package/dist/cdcchart.js +35019 -34301
- package/examples/feature/boxplot/boxplot-data.json +88 -22
- package/examples/feature/boxplot/boxplot.json +540 -16
- package/examples/feature/boxplot/testing.csv +7 -7
- package/examples/feature/sankey/sankey-example-data.json +126 -14
- package/examples/feature/tests-date-exclusions/date-exclusions-config.json +372 -12
- package/examples/private/DEV-8850-2.json +493 -0
- package/examples/private/DEV-9822.json +574 -0
- package/examples/private/DEV-9840.json +553 -0
- package/examples/private/DEV-9850-3.json +461 -0
- package/examples/private/chart.json +1084 -0
- package/examples/private/ci_formatted.json +202 -0
- package/examples/private/ci_issue.json +3016 -0
- package/examples/private/completed.json +634 -0
- package/examples/private/dem-data-long.csv +20 -0
- package/examples/private/dem-data-long.json +36 -0
- package/examples/private/demographic_data.csv +157 -0
- package/examples/private/demographic_data.json +2654 -0
- package/examples/private/demographic_dynamic.json +443 -0
- package/examples/private/demographic_standard.json +560 -0
- package/examples/private/ehdi.json +29939 -0
- package/examples/private/test.json +493 -0
- package/index.html +10 -7
- package/package.json +2 -2
- package/src/CdcChart.tsx +132 -152
- package/src/_stories/Chart.Anchors.stories.tsx +31 -0
- package/src/_stories/Chart.CustomColors.stories.tsx +19 -0
- package/src/_stories/Chart.DynamicSeries.stories.tsx +34 -0
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +42 -1
- package/src/_stories/Chart.stories.tsx +37 -6
- package/src/_stories/ChartAxisLabels.stories.tsx +4 -1
- package/src/_stories/ChartEditor.stories.tsx +27 -0
- package/src/_stories/ChartLine.Suppression.stories.tsx +25 -0
- package/src/_stories/ChartPrefixSuffix.stories.tsx +8 -0
- package/{examples/feature/area/area-chart-date-city-temperature.json → src/_stories/_mock/area_chart_stacked.json} +125 -27
- package/src/_stories/_mock/boxplot_multiseries.json +647 -0
- package/src/_stories/_mock/dynamic_series_bar_config.json +723 -0
- package/src/_stories/_mock/dynamic_series_config.json +979 -0
- package/src/_stories/_mock/line_chart_dynamic_ci.json +493 -0
- package/src/_stories/_mock/line_chart_non_dynamic_ci.json +522 -0
- package/{examples/feature/scatterplot/scatterplot.json → src/_stories/_mock/scatterplot_mock.json} +62 -92
- package/src/_stories/_mock/short_dates.json +288 -0
- package/src/_stories/_mock/suppression_mock.json +1549 -0
- package/src/components/AreaChart/components/AreaChart.Stacked.jsx +15 -3
- package/src/components/Axis/Categorical.Axis.tsx +2 -2
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +46 -37
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +43 -9
- package/src/components/BarChart/components/BarChart.Vertical.tsx +53 -47
- package/src/components/BarChart/helpers/getBarData.ts +28 -0
- package/src/components/BarChart/helpers/index.ts +1 -2
- package/src/components/BarChart/helpers/tests/getBarData.test.ts +74 -0
- package/src/components/BoxPlot/BoxPlot.tsx +131 -0
- package/src/components/BoxPlot/helpers/index.ts +54 -0
- package/src/components/BrushChart.tsx +23 -26
- package/src/components/EditorPanel/EditorPanel.tsx +117 -139
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +3 -3
- package/src/components/EditorPanel/components/Panels/Panel.BoxPlot.tsx +51 -6
- package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +40 -9
- package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +122 -56
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -2
- package/src/components/EditorPanel/useEditorPermissions.ts +20 -2
- package/src/components/Legend/Legend.Component.tsx +11 -12
- package/src/components/Legend/Legend.tsx +16 -16
- package/src/components/Legend/helpers/getLegendClasses.ts +59 -0
- package/src/components/Legend/helpers/index.ts +2 -1
- package/src/components/Legend/tests/getLegendClasses.test.ts +115 -0
- package/src/components/LineChart/components/LineChart.Circle.tsx +1 -1
- package/src/components/LineChart/helpers.ts +49 -43
- package/src/components/LineChart/index.tsx +135 -83
- package/src/components/LinearChart.tsx +196 -181
- package/src/components/PieChart/PieChart.tsx +7 -1
- package/src/components/Sankey/components/ColumnList.tsx +19 -0
- package/src/components/Sankey/components/Sankey.tsx +479 -0
- package/src/components/Sankey/helpers/getSankeyTooltip.tsx +33 -0
- package/src/components/Sankey/index.tsx +1 -492
- package/src/components/Sankey/sankey.scss +22 -21
- package/src/components/Sankey/types/index.ts +1 -1
- package/src/components/Sankey/useSankeyAlert.tsx +60 -0
- package/src/components/ScatterPlot/ScatterPlot.jsx +20 -4
- package/src/data/initial-state.js +7 -12
- package/src/helpers/countNumOfTicks.ts +57 -0
- package/src/helpers/getQuartiles.ts +15 -18
- package/src/hooks/useMinMax.ts +44 -16
- package/src/hooks/useReduceData.ts +43 -10
- package/src/hooks/useScales.ts +90 -35
- package/src/hooks/useTooltip.tsx +59 -50
- package/src/scss/DataTable.scss +5 -0
- package/src/scss/main.scss +6 -20
- package/src/types/ChartConfig.ts +6 -19
- package/src/types/ChartContext.ts +4 -1
- package/src/types/ForestPlot.ts +8 -0
- package/src/components/BoxPlot/BoxPlot.jsx +0 -111
- 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
|
|
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
|
|
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
|
-
|
|
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
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
308
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
425
|
-
|
|
414
|
+
if (!yAxisAutoPadding) return
|
|
415
|
+
setYAxisAutoPadding(0)
|
|
416
|
+
}, [maxValue])
|
|
417
|
+
useEffect(() => {
|
|
418
|
+
if (orientation === 'horizontal') return
|
|
426
419
|
|
|
427
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
442
|
-
const
|
|
443
|
-
const
|
|
444
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
//
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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' &&
|
|
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
|
|
849
|
-
|
|
850
|
-
|
|
823
|
+
let position = yScale(anchor.value)
|
|
824
|
+
let middleOffset = 0
|
|
825
|
+
|
|
851
826
|
if (!anchor.value) return
|
|
852
|
-
|
|
853
|
-
|
|
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 (!
|
|
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:
|
|
865
|
-
to={{ x: width - config.yAxis.rightAxisSize, y:
|
|
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
|
-
|
|
857
|
+
const getAnchorPosition = (): number | undefined => {
|
|
858
|
+
let position: number | undefined
|
|
878
859
|
|
|
879
|
-
|
|
880
|
-
|
|
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
|
-
{
|
|
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={
|
|
1337
|
+
numTicks={useDateSpanMonths ? dateSpanMonths : xTickCount}
|
|
1349
1338
|
tickStroke='#333'
|
|
1350
1339
|
tickValues={
|
|
1351
1340
|
config.xAxis.manual
|
|
1352
|
-
? getTickValues(
|
|
1353
|
-
|
|
1354
|
-
|
|
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 =
|
|
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
|
-
...
|
|
1374
|
-
getTextWidth(tick.formattedValue, `normal ${
|
|
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 =
|
|
1381
|
-
getTextWidth(tick.formattedValue, `normal ${
|
|
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) / (
|
|
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
|
-
{
|
|
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={
|
|
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
|
-
{
|
|
198
|
+
{roundedPercentage + '%'}
|
|
193
199
|
</Text>
|
|
194
200
|
)}
|
|
195
201
|
</animated.g>
|