@cdc/chart 4.26.6 → 4.26.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONFIG.md +504 -368
- package/dist/cdcchart-CYc-Kayd.es.js +22488 -0
- package/dist/cdcchart-DCKn8NmI.es.js +20695 -0
- package/dist/cdcchart-Dyui9nk_.es.js +45828 -0
- package/dist/cdcchart.js +4 -96498
- package/examples/__data__/vega-remote-data.json +6 -0
- package/examples/dendrogram.json +18 -0
- package/examples/feature/bar/bar-chart-race-annual-change.json +68 -0
- package/examples/feature/heatmap/average-age-categorical.json +2 -2
- package/examples/feature/heatmap/calendar-heatmap.json +2 -2
- package/examples/feature/heatmap/cell-values-bottom-axis.json +2 -2
- package/examples/feature/heatmap/sparse-aggregation.json +2 -2
- package/examples/feature/line/line-chart-race.json +50 -0
- package/examples/feature/sankey/sankey-example-data.json +40 -1441
- package/examples/feature/sankey/sankey_chart_data.csv +9 -20
- package/examples/network.json +31 -0
- package/examples/private/box-plot.json +573 -0
- package/examples/private/custom-color-issue.json +625 -0
- package/examples/private/custom-colors.json +522 -0
- package/examples/private/cutoff.json +10750 -0
- package/examples/private/flueview.json +22051 -0
- package/examples/private/overlay-legend.json +485 -0
- package/examples/private/pie-test.json +469 -0
- package/examples/vega-remote.json +22 -0
- package/package.json +11 -13
- package/src/CdcChart.tsx +2 -2
- package/src/CdcChartComponent.tsx +444 -217
- package/src/_stories/Chart.BarRace.stories.tsx +443 -0
- package/src/_stories/Chart.BoxPlotNumericSort.stories.tsx +189 -0
- package/src/_stories/Chart.BoxPlotStatsParity.stories.tsx +207 -0
- package/src/_stories/Chart.ColorDistribution.stories.tsx +299 -0
- package/src/_stories/Chart.DataCutoff.stories.tsx +147 -0
- package/src/_stories/Chart.DataTableDownloadFilename.stories.tsx +60 -0
- package/src/_stories/Chart.Dendrogram.stories.tsx +373 -0
- package/src/_stories/Chart.Forecast.CILegend.stories.tsx +236 -0
- package/src/_stories/Chart.HeatMap.stories.tsx +170 -4
- package/src/_stories/Chart.Legend.AxisAlign.stories.tsx +119 -0
- package/src/_stories/Chart.LineRace.stories.tsx +149 -0
- package/src/_stories/Chart.Network.stories.tsx +432 -0
- package/src/_stories/Chart.PaletteConversion.Editor.stories.tsx +208 -0
- package/src/_stories/Chart.Patterns.smoke.stories.tsx +31 -0
- package/src/_stories/Chart.PercentageValue.stories.tsx +311 -0
- package/src/_stories/Chart.PieLabels.stories.tsx +49 -0
- package/src/_stories/Chart.RankByValueMutation.stories.tsx +206 -0
- package/src/_stories/Chart.RequiredFields.stories.tsx +681 -0
- package/src/_stories/Chart.Sankey.stories.tsx +417 -0
- package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +47 -0
- package/src/_stories/Chart.SmallMultiplesNumericTileOrder.stories.tsx +137 -0
- package/src/_stories/Chart.TooltipSeriesLabels.stories.tsx +43 -0
- package/src/_stories/Chart.VegaRemoteData.stories.tsx +31 -0
- package/src/_stories/Chart.smoke.stories.tsx +14 -1
- package/src/_stories/ChartAnnotation.Editor.stories.tsx +78 -0
- package/src/_stories/ChartAxisTitles.smoke.stories.tsx +48 -0
- package/src/_stories/ChartBar.Editor.stories.tsx +499 -57
- package/src/_stories/ChartBrush.Editor.stories.tsx +46 -67
- package/src/_stories/ChartBrush.Regression.stories.tsx +135 -0
- package/src/_stories/ChartEditor.Editor.stories.tsx +33 -3
- package/src/_stories/ChartLine.EffectCompatibility.stories.tsx +120 -0
- package/src/_stories/ChartPrefixSuffix.smoke.stories.tsx +28 -1
- package/src/_stories/_mock/annotation_event-line_mock.json +2 -2
- package/src/_stories/_mock/editor-test-configs.json +20 -10
- package/src/_stories/_mock/editor-test-datasets.json +38 -42
- package/src/_stories/_mock/heatmap-average-age-categorical.json +2 -2
- package/src/_stories/_mock/heatmap.json +2 -2
- package/src/_stories/_mock/legend-axis-align-stacked-bar.json +125 -0
- package/src/_stories/_mock/sankey_mock.json +23 -27
- package/src/components/Annotations/components/AnnotationDraggable.styles.css +52 -0
- package/src/components/Annotations/components/AnnotationDraggable.tsx +639 -139
- package/src/components/Annotations/components/AnnotationDropdown.styles.css +44 -4
- package/src/components/Annotations/components/AnnotationDropdown.tsx +10 -32
- package/src/components/Annotations/helpers/resolveAnnotationLayout.ts +212 -0
- package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +612 -19
- package/src/components/Annotations/tests/AnnotationDropdown.test.tsx +50 -0
- package/src/components/Annotations/tests/resolveAnnotationLayout.test.ts +203 -0
- package/src/components/Axis/BottomAxis.tsx +1 -1
- package/src/components/Axis/Categorical.Axis.tsx +3 -3
- package/src/components/Axis/RightAxis.tsx +21 -13
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +39 -8
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +13 -4
- package/src/components/BarChart/components/BarChart.StackedVertical.test.ts +28 -0
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +30 -26
- package/src/components/BarChart/components/BarChart.Vertical.tsx +38 -28
- package/src/components/BarChart/components/PortionPatternOverlay.tsx +61 -0
- package/src/components/BarChart/components/context.tsx +6 -0
- package/src/components/BarChart/helpers/getBarData.ts +28 -28
- package/src/components/BarChart/helpers/getBarHeights.ts +2 -2
- package/src/components/BarChart/helpers/getPatternUrl.ts +2 -5
- package/src/components/BarChart/helpers/index.ts +8 -0
- package/src/components/BarChart/helpers/portionPattern.ts +243 -0
- package/src/components/BarChart/helpers/tests/addMinimumBarHeights.test.ts +42 -0
- package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +13 -0
- package/src/components/BarChart/helpers/tests/portionPattern.test.ts +280 -0
- package/src/components/BarChart/helpers/tests/useBarChart.test.tsx +202 -0
- package/src/components/BarChart/helpers/useBarChart.ts +60 -60
- package/src/components/BarChartRace/BarChartRace.test.tsx +199 -0
- package/src/components/BarChartRace/BarChartRace.tsx +230 -0
- package/src/components/BarChartRace/BarChartRaceFallback.test.tsx +61 -0
- package/src/components/BarChartRace/BarChartRaceFallback.tsx +48 -0
- package/src/components/BarChartRace/bar-chart-race.css +160 -0
- package/src/components/BarChartRace/helpers.test.ts +114 -0
- package/src/components/BarChartRace/helpers.ts +185 -0
- package/src/components/BarChartRace/index.ts +3 -0
- package/src/components/BoxPlot/BoxPlot.Horizontal.tsx +7 -6
- package/src/components/BoxPlot/BoxPlot.Vertical.tsx +9 -7
- package/src/components/BoxPlot/helpers/index.ts +13 -83
- package/src/components/Brush/BrushSelector.tsx +10 -24
- package/src/components/Brush/MiniChartPreview.tsx +33 -1
- package/src/components/Dendrogram/components/Dendrogram.tsx +374 -0
- package/src/components/Dendrogram/dendrogram.css +113 -0
- package/src/components/Dendrogram/helpers/layoutDendrogram.test.ts +95 -0
- package/src/components/Dendrogram/helpers/layoutDendrogram.ts +56 -0
- package/src/components/Dendrogram/helpers/prepareDendrogramData.test.ts +135 -0
- package/src/components/Dendrogram/helpers/prepareDendrogramData.ts +166 -0
- package/src/components/Dendrogram/helpers/resolveDendrogramLabels.test.ts +196 -0
- package/src/components/Dendrogram/helpers/resolveDendrogramLabels.ts +418 -0
- package/src/components/Dendrogram/index.tsx +1 -0
- package/src/components/Dendrogram/types/index.ts +66 -0
- package/src/components/EditorPanel/EditorPanel.tsx +1111 -805
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +50 -5
- package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.css +10 -0
- package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.tsx +211 -0
- package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +19 -8
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +104 -61
- package/src/components/EditorPanel/components/Panels/Panel.Network.css +9 -0
- package/src/components/EditorPanel/components/Panels/Panel.Network.tsx +202 -0
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +236 -36
- package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +6 -1
- package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +89 -105
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +21 -6
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +1 -1
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +8 -66
- package/src/components/EditorPanel/components/Panels/RacingControls.test.tsx +131 -0
- package/src/components/EditorPanel/components/Panels/RacingControls.tsx +185 -0
- package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +2 -2
- package/src/components/EditorPanel/components/Panels/index.tsx +4 -0
- package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +20 -7
- package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +25 -2
- package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +5 -4
- package/src/components/EditorPanel/useEditorPermissions.test.tsx +94 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +75 -25
- package/src/components/Forecasting/Forecasting.tsx +16 -7
- package/src/components/HeatMap/components/HeatMap.tsx +511 -178
- package/src/components/HeatMap/heatmap.constants.ts +7 -3
- package/src/components/HeatMap/heatmap.css +101 -2
- package/src/components/HeatMap/helpers/layout.ts +80 -0
- package/src/components/HeatMap/tests/HeatMap.test.tsx +339 -16
- package/src/components/HeatMap/tests/helpers.test.ts +79 -0
- package/src/components/Legend/Legend.Component.tsx +163 -148
- package/src/components/Legend/Legend.Suppression.tsx +3 -3
- package/src/components/Legend/LegendGroup/LegendGroup.styles.css +7 -0
- package/src/components/Legend/LegendGroup/LegendGroup.tsx +47 -35
- package/src/components/Legend/helpers/createFormatLabels.test.ts +226 -0
- package/src/components/Legend/helpers/createFormatLabels.tsx +45 -51
- package/src/components/LineChart/components/LineChart.Circle.tsx +3 -3
- package/src/components/LineChart/helpers.test.ts +126 -38
- package/src/components/LineChart/helpers.ts +9 -6
- package/src/components/LineChart/index.tsx +71 -27
- package/src/components/LineChart/tests/LineChart.test.tsx +630 -0
- package/src/components/LineChartRace/LineChartRace.test.tsx +275 -0
- package/src/components/LineChartRace/LineChartRace.tsx +134 -0
- package/src/components/LineChartRace/LineChartRaceFallback.tsx +28 -0
- package/src/components/LineChartRace/helpers.test.ts +88 -0
- package/src/components/LineChartRace/helpers.ts +157 -0
- package/src/components/LineChartRace/index.ts +3 -0
- package/src/components/LineChartRace/line-chart-race.css +16 -0
- package/src/components/LinearChart/ValueAxisAnchors.tsx +58 -0
- package/src/components/LinearChart/VisualizationRenderer.tsx +18 -23
- package/src/components/LinearChart/tests/LinearChart.test.tsx +412 -13
- package/src/components/LinearChart/tests/VisualizationRenderer.test.tsx +141 -0
- package/src/components/LinearChart/tests/mockConfigContext.ts +2 -1
- package/src/components/LinearChart.tsx +85 -54
- package/src/components/Network/components/Network.tsx +372 -0
- package/src/components/Network/helpers/layoutNetwork.test.ts +68 -0
- package/src/components/Network/helpers/layoutNetwork.ts +87 -0
- package/src/components/Network/helpers/prepareNetworkData.test.ts +116 -0
- package/src/components/Network/helpers/prepareNetworkData.ts +101 -0
- package/src/components/Network/helpers/resolveNetworkLabels.test.ts +112 -0
- package/src/components/Network/helpers/resolveNetworkLabels.ts +206 -0
- package/src/components/Network/index.tsx +3 -0
- package/src/components/Network/network.css +107 -0
- package/src/components/Network/types/index.ts +56 -0
- package/src/components/PairedBarChart.jsx +5 -10
- package/src/components/PairedBarChart.test.tsx +71 -0
- package/src/components/PieChart/PieChart.tsx +121 -13
- package/src/components/PieChart/helpers/labelPlacement.test.ts +88 -0
- package/src/components/PieChart/helpers/labelPlacement.ts +95 -0
- package/src/components/RacingChartRenderer.tsx +44 -0
- package/src/components/Regions/components/Regions.test.tsx +179 -0
- package/src/components/Regions/components/Regions.tsx +104 -69
- package/src/components/Regions/index.tsx +2 -3
- package/src/components/Sankey/components/Sankey.tsx +264 -437
- package/src/components/Sankey/helpers/getSankeyTooltip.ts +12 -0
- package/src/components/Sankey/helpers/prepareSankeyData.test.ts +71 -0
- package/src/components/Sankey/helpers/prepareSankeyData.ts +90 -0
- package/src/components/Sankey/sankey.scss +57 -116
- package/src/components/Sankey/types/index.ts +41 -4
- package/src/components/ScatterPlot/ScatterPlot.jsx +28 -12
- package/src/components/ScatterPlot/ScatterPlot.test.tsx +60 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +12 -12
- package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +7 -5
- package/src/components/raceTiming.ts +18 -0
- package/src/data/initial-state.js +79 -31
- package/src/helpers/applyChartTypeContract.ts +71 -0
- package/src/helpers/axisLabelFontSize.ts +2 -2
- package/src/helpers/boxPlotStats.ts +60 -0
- package/src/helpers/bumpChartTooltip.ts +2 -1
- package/src/helpers/calculateHorizontalBarCategoryLabelWidth.test.ts +37 -0
- package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +3 -2
- package/src/helpers/calculateRightYAxisWidth.ts +60 -0
- package/src/helpers/categoryOrder.ts +60 -0
- package/src/helpers/colorAssignmentHelpers.ts +2 -2
- package/src/helpers/createNewChartConfig.ts +56 -0
- package/src/helpers/dataDrivenYAxisCategories.ts +18 -0
- package/src/helpers/forecastLegend.ts +88 -0
- package/src/helpers/getBoxPlotConfig.ts +26 -38
- package/src/helpers/getChartTypeDefaultPalette.ts +29 -0
- package/src/helpers/getColorCodeCategoryColorMap.ts +26 -0
- package/src/helpers/getColorScale.ts +31 -22
- package/src/helpers/getComboChartConfig.ts +0 -1
- package/src/helpers/getDynamicYAxisCategories.ts +69 -0
- package/src/helpers/getExcludedData.ts +0 -1
- package/src/helpers/getHasBoundarySuppression.ts +4 -3
- package/src/helpers/getMinMax.ts +11 -8
- package/src/helpers/getNewRuntime.ts +4 -3
- package/src/helpers/getPiePercent.ts +3 -3
- package/src/helpers/getStackedSeriesMax.ts +51 -0
- package/src/helpers/getV21ChartDistributionColors.ts +57 -0
- package/src/helpers/handleRankByValue.ts +15 -15
- package/src/helpers/isConvertLineToBarGraph.ts +2 -2
- package/src/helpers/seriesColumnSettings.ts +31 -3
- package/src/helpers/sizeHelpers.ts +4 -5
- package/src/helpers/smallMultiplesHelpers.ts +10 -13
- package/src/helpers/tests/applyChartTypeContract.test.ts +37 -0
- package/src/helpers/tests/boxPlotStats.test.ts +31 -0
- package/src/helpers/tests/bumpChartTooltip.test.ts +29 -0
- package/src/helpers/tests/calculateRightYAxisWidth.test.ts +105 -0
- package/src/helpers/tests/categoryOrder.test.ts +57 -0
- package/src/helpers/tests/countNumOfTicks.test.ts +11 -0
- package/src/helpers/tests/createNewChartConfig.test.ts +182 -0
- package/src/helpers/tests/getBoxPlotConfig.test.ts +70 -0
- package/src/helpers/tests/getColorCodeCategoryColorMap.test.ts +113 -0
- package/src/helpers/tests/getColorScale.test.ts +226 -1
- package/src/helpers/tests/getDynamicYAxisCategories.test.ts +61 -0
- package/src/helpers/tests/getMinMax.test.ts +41 -0
- package/src/helpers/tests/getStackedSeriesMax.test.ts +26 -0
- package/src/helpers/tests/handleRankByValue.test.ts +55 -37
- package/src/helpers/tests/seriesColumnSettings.test.ts +83 -0
- package/src/helpers/tests/smallMultiplesHelpers.test.ts +37 -1
- package/src/helpers/tests/tooltipHelpers.test.ts +80 -0
- package/src/helpers/tooltipHelpers.ts +16 -0
- package/src/hooks/tests/useRightAxis.test.tsx +143 -2
- package/src/hooks/tests/useScales.test.tsx +3 -6
- package/src/hooks/tests/useTooltip.test.tsx +484 -0
- package/src/hooks/usePrefersReducedMotion.ts +22 -0
- package/src/hooks/useReduceData.ts +4 -36
- package/src/hooks/useRightAxis.ts +19 -26
- package/src/hooks/useScales.ts +15 -0
- package/src/hooks/useTooltip.tsx +106 -71
- package/src/scss/main.scss +74 -2
- package/src/store/chart.reducer.ts +4 -4
- package/src/test/CdcChart.dataTable.test.tsx +582 -14
- package/src/test/CdcChart.dendrogram.test.tsx +277 -0
- package/src/test/CdcChart.finalization.test.tsx +143 -0
- package/src/test/CdcChart.network.test.tsx +271 -0
- package/src/types/ChartConfig.ts +91 -54
- package/src/types/ChartContext.ts +5 -0
- package/src/types/Horizon.ts +1 -1
- package/LICENSE +0 -201
- package/dist/cdcchart-BWVIUcAF.es.js +0 -4802
- package/src/_stories/Chart.Sankey.smoke.stories.tsx +0 -62
- package/src/components/Sankey/components/ColumnList.tsx +0 -19
- package/src/components/Sankey/helpers/getSankeyTooltip.tsx +0 -34
- package/src/components/Sankey/useSankeyAlert.tsx +0 -60
- package/src/data/legacy-defaults.ts +0 -18
- package/src/helpers/ensureSpecialChartAxisTypes.ts +0 -11
- package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +0 -30
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render } from '@testing-library/react'
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import ConfigContext from '../ConfigContext'
|
|
5
|
+
import PairedBarChart from './PairedBarChart'
|
|
6
|
+
import { createMockChartContext, createMockConfig } from './LinearChart/tests/mockConfigContext'
|
|
7
|
+
|
|
8
|
+
vi.mock('@cdc/core/helpers/cove/accessibility', () => ({
|
|
9
|
+
getContrastColor: () => '#000'
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
vi.mock('@cdc/core/helpers/getTextWidth', () => ({
|
|
13
|
+
getTextWidth: () => 10
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
const renderPairedBar = (casesLabel: string) => {
|
|
17
|
+
const baseConfig = createMockConfig()
|
|
18
|
+
const data = [{ Year: '2024', Cases: 10, Deaths: 2 }]
|
|
19
|
+
const config = createMockConfig({
|
|
20
|
+
visualizationType: 'Paired Bar',
|
|
21
|
+
orientation: 'horizontal',
|
|
22
|
+
barHasBorder: 'false',
|
|
23
|
+
barHeight: 25,
|
|
24
|
+
barSpace: 15,
|
|
25
|
+
xAxis: { ...baseConfig.xAxis, dataKey: 'Year', size: '50', type: 'categorical' },
|
|
26
|
+
yAxis: { ...baseConfig.yAxis, displayNumbersOnBar: false },
|
|
27
|
+
series: [
|
|
28
|
+
{ dataKey: 'Cases', name: 'Reported Cases', type: 'Paired Bar', axis: 'Left', tooltip: true },
|
|
29
|
+
{ dataKey: 'Deaths', name: 'Reported Deaths', type: 'Paired Bar', axis: 'Left', tooltip: true }
|
|
30
|
+
] as any,
|
|
31
|
+
columns: {
|
|
32
|
+
Cases: { name: 'Cases', label: casesLabel },
|
|
33
|
+
Deaths: { name: 'Deaths', label: 'Death Rate' }
|
|
34
|
+
} as any,
|
|
35
|
+
runtime: {
|
|
36
|
+
...baseConfig.runtime,
|
|
37
|
+
seriesLabels: { Cases: 'Reported Cases', Deaths: 'Reported Deaths' }
|
|
38
|
+
} as any
|
|
39
|
+
})
|
|
40
|
+
const context = createMockChartContext(config, {
|
|
41
|
+
transformedData: data,
|
|
42
|
+
colorScale: () => '#005ea8',
|
|
43
|
+
formatNumber: value => String(value)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return render(
|
|
47
|
+
<ConfigContext.Provider value={context}>
|
|
48
|
+
<PairedBarChart width={400} height={100} originalWidth={450} yAxisWidth={50} />
|
|
49
|
+
</ConfigContext.Provider>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('PairedBarChart tooltips', () => {
|
|
54
|
+
it('uses the customized series column label without rendering an undefined heading', () => {
|
|
55
|
+
const { container } = renderPairedBar('Case Rate')
|
|
56
|
+
const tooltip = container.querySelector('.bar.group-1')?.getAttribute('data-tooltip-html') || ''
|
|
57
|
+
|
|
58
|
+
expect(tooltip).toContain('Year: 2024')
|
|
59
|
+
expect(tooltip).toContain('Case Rate: 10')
|
|
60
|
+
expect(tooltip).not.toContain('Reported Cases')
|
|
61
|
+
expect(tooltip).not.toContain('undefined:')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('falls back to the Series Name when the Label matches the data key', () => {
|
|
65
|
+
const { container } = renderPairedBar('Cases')
|
|
66
|
+
const tooltip = container.querySelector('.bar.group-1')?.getAttribute('data-tooltip-html') || ''
|
|
67
|
+
|
|
68
|
+
expect(tooltip).toContain('Reported Cases: 10')
|
|
69
|
+
expect(tooltip).not.toContain('undefined:')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -4,11 +4,10 @@ import { animated, useTransition, to } from '@react-spring/web'
|
|
|
4
4
|
// visx
|
|
5
5
|
import { Pie } from '@visx/shape'
|
|
6
6
|
import { Group } from '@visx/group'
|
|
7
|
-
import { Text } from '@visx/text'
|
|
8
7
|
import { useTooltip, TooltipWithBounds } from '@visx/tooltip'
|
|
9
8
|
import { colorPalettesChart as colorPalettes } from '@cdc/core/data/colorPalettes'
|
|
10
9
|
import { getPaletteColors } from '@cdc/core/helpers/palettes/utils'
|
|
11
|
-
import {
|
|
10
|
+
import { getColorPaletteMajorVersion } from '@cdc/core/helpers/getColorPaletteMajorVersion'
|
|
12
11
|
import {
|
|
13
12
|
v2ColorDistribution,
|
|
14
13
|
divergentColorDistribution,
|
|
@@ -25,6 +24,9 @@ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
|
25
24
|
import { scaleOrdinal } from '@visx/scale'
|
|
26
25
|
import { getContrastColor } from '@cdc/core/helpers/cove/accessibility'
|
|
27
26
|
import { type TooltipDisplayData } from '../../helpers/tooltipHelpers'
|
|
27
|
+
import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
|
|
28
|
+
import { getPieLabelPosition } from './helpers/labelPlacement'
|
|
29
|
+
import { getV21ChartDistributionColors } from '../../helpers/getV21ChartDistributionColors'
|
|
28
30
|
|
|
29
31
|
type TooltipData = TooltipDisplayData
|
|
30
32
|
|
|
@@ -34,6 +36,37 @@ type PieChartProps = {
|
|
|
34
36
|
interactionLabel?: string
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
const ENHANCED_PIE_LABEL_FONT_SIZE = 13
|
|
40
|
+
const ENHANCED_PIE_LABEL_LINE_HEIGHT = 15
|
|
41
|
+
const ENHANCED_PIE_LABEL_HEIGHT = ENHANCED_PIE_LABEL_LINE_HEIGHT * 2
|
|
42
|
+
const ENHANCED_PIE_LABEL_FONT = `${ENHANCED_PIE_LABEL_FONT_SIZE}px sans-serif`
|
|
43
|
+
const ENHANCED_PIE_LABEL_MIN_ANGLE = 0.01
|
|
44
|
+
const LEGACY_PERCENT_LABEL_MIN_ANGLE = 0.1
|
|
45
|
+
const MIN_RADIUS_WITH_LABELS = 55
|
|
46
|
+
const MIN_LABEL_GUTTER = 96
|
|
47
|
+
const OUTSIDE_LABEL_OFFSET = 22
|
|
48
|
+
const LABEL_GUTTER_PADDING = OUTSIDE_LABEL_OFFSET + 16
|
|
49
|
+
const VERTICAL_LABEL_GUTTER = 44
|
|
50
|
+
|
|
51
|
+
const pieLabelTextWidthCache = new Map<string, number>()
|
|
52
|
+
const MAX_PIE_LABEL_TEXT_CACHE = 2000
|
|
53
|
+
|
|
54
|
+
const measurePieLabelText = (text: string) => {
|
|
55
|
+
const cached = pieLabelTextWidthCache.get(text)
|
|
56
|
+
if (cached !== undefined) return cached
|
|
57
|
+
|
|
58
|
+
const measured =
|
|
59
|
+
typeof document === 'undefined'
|
|
60
|
+
? Math.ceil(text.length * ENHANCED_PIE_LABEL_FONT_SIZE * 0.6)
|
|
61
|
+
: getTextWidth(text, ENHANCED_PIE_LABEL_FONT) ?? Math.ceil(text.length * ENHANCED_PIE_LABEL_FONT_SIZE * 0.6)
|
|
62
|
+
|
|
63
|
+
// Avoid unbounded growth in long-lived sessions with many unique labels.
|
|
64
|
+
if (pieLabelTextWidthCache.size >= MAX_PIE_LABEL_TEXT_CACHE) pieLabelTextWidthCache.clear()
|
|
65
|
+
|
|
66
|
+
pieLabelTextWidthCache.set(text, measured)
|
|
67
|
+
return measured
|
|
68
|
+
}
|
|
69
|
+
|
|
37
70
|
const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) => {
|
|
38
71
|
const { interactionLabel = '' } = props
|
|
39
72
|
const {
|
|
@@ -70,6 +103,7 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
|
|
|
70
103
|
const pivotKey = dataNeedsPivot ? 'pivotColumn' : undefined
|
|
71
104
|
const showPercentage = config.dataFormat.showPiePercent
|
|
72
105
|
const labelForCalcArea = 'Calculated Area'
|
|
106
|
+
const showCategoryPercentageLabels = Boolean(config.labels)
|
|
73
107
|
|
|
74
108
|
const _data = useMemo(() => {
|
|
75
109
|
let baseData = []
|
|
@@ -124,7 +158,10 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
|
|
|
124
158
|
|
|
125
159
|
// Helper function to determine enhanced distribution type and apply it
|
|
126
160
|
const applyEnhancedColorDistribution = (config, palette, numberOfKeys) => {
|
|
127
|
-
const
|
|
161
|
+
const v21DistributionColors = getV21ChartDistributionColors(config, palette, numberOfKeys)
|
|
162
|
+
if (v21DistributionColors) return v21DistributionColors
|
|
163
|
+
|
|
164
|
+
const version = getColorPaletteMajorVersion(config)
|
|
128
165
|
const configPalette = config.general?.palette?.name || config.palette
|
|
129
166
|
|
|
130
167
|
// Skip enhanced distribution if not v2, too many keys, or wrong palette length
|
|
@@ -136,7 +173,6 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
|
|
|
136
173
|
const isDivergent = configPalette && configPalette.includes('divergent')
|
|
137
174
|
const isColorblindSafe =
|
|
138
175
|
configPalette && (configPalette.includes('colorblindsafe') || configPalette.includes('qualitative_standard'))
|
|
139
|
-
|
|
140
176
|
// Determine which distribution to use based on palette type
|
|
141
177
|
let distributionMap = null
|
|
142
178
|
if (isDivergent) {
|
|
@@ -217,6 +253,7 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
|
|
|
217
253
|
config.xAxis.dataKey,
|
|
218
254
|
config.general?.palette?.name,
|
|
219
255
|
config.general?.palette?.isReversed,
|
|
256
|
+
config.general?.palette?.version,
|
|
220
257
|
config.general?.palette?.customColors,
|
|
221
258
|
config.general?.palette?.customColorsOrdered,
|
|
222
259
|
config.palette
|
|
@@ -269,9 +306,25 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
|
|
|
269
306
|
|
|
270
307
|
let roundedPercentage = percentageToDisplay.toFixed(roundTo) + '%'
|
|
271
308
|
// add missing pie part
|
|
272
|
-
if (arc.data[config.xAxis.dataKey] === labelForCalcArea && config.dataFormat.showPiePercent) {
|
|
309
|
+
if (arc.data[config.runtime.xAxis.dataKey] === labelForCalcArea && config.dataFormat.showPiePercent) {
|
|
273
310
|
roundedPercentage = '**'
|
|
274
311
|
}
|
|
312
|
+
let categoryLabel = ''
|
|
313
|
+
let labelPosition = { placement: 'inside' as const, x: 0, y: 0, textAnchor: 'middle' as const }
|
|
314
|
+
|
|
315
|
+
if (showCategoryPercentageLabels) {
|
|
316
|
+
categoryLabel = String(arc.data[config.runtime.xAxis.dataKey] ?? '')
|
|
317
|
+
const labelWidth = Math.max(measurePieLabelText(categoryLabel), measurePieLabelText(roundedPercentage))
|
|
318
|
+
labelPosition = getPieLabelPosition({
|
|
319
|
+
startAngle: arc.startAngle,
|
|
320
|
+
endAngle: arc.endAngle,
|
|
321
|
+
innerRadius: radius - donutThickness,
|
|
322
|
+
outerRadius: radius,
|
|
323
|
+
labelWidth,
|
|
324
|
+
labelHeight: ENHANCED_PIE_LABEL_HEIGHT,
|
|
325
|
+
isDonut: config.pieType === 'Donut'
|
|
326
|
+
})
|
|
327
|
+
}
|
|
275
328
|
|
|
276
329
|
// Determine if this slice should be muted based on legend behavior
|
|
277
330
|
const isHighlighted =
|
|
@@ -279,6 +332,8 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
|
|
|
279
332
|
const shouldMute = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && !isHighlighted
|
|
280
333
|
const sliceOpacity = shouldMute ? 0.3 : 1
|
|
281
334
|
const textOpacity = shouldMute ? 0.3 : 1
|
|
335
|
+
const labelFill = labelPosition.placement === 'inside' ? textColor : '#000000'
|
|
336
|
+
const labelMinAngle = showCategoryPercentageLabels ? ENHANCED_PIE_LABEL_MIN_ANGLE : LEGACY_PERCENT_LABEL_MIN_ANGLE
|
|
282
337
|
|
|
283
338
|
return (
|
|
284
339
|
<Group key={key} className={`slice-${CSS.escape(String(key))}`}>
|
|
@@ -302,7 +357,7 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
|
|
|
302
357
|
/>
|
|
303
358
|
|
|
304
359
|
{/* ── the percentage label */}
|
|
305
|
-
{arc.endAngle - arc.startAngle >
|
|
360
|
+
{arc.endAngle - arc.startAngle > labelMinAngle && !showCategoryPercentageLabels && (
|
|
306
361
|
<animated.text
|
|
307
362
|
transform={to([styles.startAngle, styles.endAngle], (start: number, end: number) => {
|
|
308
363
|
const [x, y] = path.centroid({
|
|
@@ -321,6 +376,37 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
|
|
|
321
376
|
{roundedPercentage}
|
|
322
377
|
</animated.text>
|
|
323
378
|
)}
|
|
379
|
+
|
|
380
|
+
{arc.endAngle - arc.startAngle > labelMinAngle && showCategoryPercentageLabels && (
|
|
381
|
+
<animated.text
|
|
382
|
+
className={`pie-label pie-label--${labelPosition.placement}`}
|
|
383
|
+
transform={to([styles.startAngle, styles.endAngle], (start: number, end: number) => {
|
|
384
|
+
const midAngle = start + (end - start) / 2
|
|
385
|
+
const innerRadius = radius - donutThickness
|
|
386
|
+
const outsideOffset = OUTSIDE_LABEL_OFFSET
|
|
387
|
+
const labelRadius =
|
|
388
|
+
labelPosition.placement === 'inside'
|
|
389
|
+
? innerRadius + (radius - innerRadius) * 0.62
|
|
390
|
+
: radius + outsideOffset
|
|
391
|
+
const x = Math.sin(midAngle) * labelRadius
|
|
392
|
+
const y = -Math.cos(midAngle) * labelRadius
|
|
393
|
+
return `translate(${x},${y})`
|
|
394
|
+
})}
|
|
395
|
+
textAnchor={labelPosition.textAnchor}
|
|
396
|
+
pointerEvents='none'
|
|
397
|
+
fill={labelFill}
|
|
398
|
+
opacity={textOpacity}
|
|
399
|
+
style={{ fontSize: ENHANCED_PIE_LABEL_FONT_SIZE, fontFamily: 'sans-serif' }}
|
|
400
|
+
>
|
|
401
|
+
<title>{`${categoryLabel}: ${roundedPercentage}`}</title>
|
|
402
|
+
<tspan x={0} dy={-ENHANCED_PIE_LABEL_LINE_HEIGHT / 2}>
|
|
403
|
+
{categoryLabel}
|
|
404
|
+
</tspan>
|
|
405
|
+
<tspan x={0} dy={ENHANCED_PIE_LABEL_LINE_HEIGHT}>
|
|
406
|
+
{roundedPercentage}
|
|
407
|
+
</tspan>
|
|
408
|
+
</animated.text>
|
|
409
|
+
)}
|
|
324
410
|
</Group>
|
|
325
411
|
)
|
|
326
412
|
})
|
|
@@ -334,11 +420,33 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
|
|
|
334
420
|
}
|
|
335
421
|
|
|
336
422
|
const height = config.heights.vertical
|
|
337
|
-
|
|
338
|
-
|
|
423
|
+
const maxPieLabelWidth = useMemo(() => {
|
|
424
|
+
if (!showCategoryPercentageLabels) return 0
|
|
425
|
+
|
|
426
|
+
const roundTo = Number(config.dataFormat.roundTo) || 0
|
|
427
|
+
const percentExample = `${(100).toFixed(roundTo)}%`
|
|
428
|
+
|
|
429
|
+
return _data.reduce((widestLabel, datum) => {
|
|
430
|
+
const categoryLabel = String(datum[config.runtime.xAxis.dataKey] ?? '')
|
|
431
|
+
return Math.max(widestLabel, measurePieLabelText(categoryLabel), measurePieLabelText(percentExample))
|
|
432
|
+
}, 0)
|
|
433
|
+
}, [_data, config.dataFormat.roundTo, config.runtime.xAxis.dataKey, showCategoryPercentageLabels])
|
|
434
|
+
const maxHorizontalLabelGutter = showCategoryPercentageLabels
|
|
435
|
+
? Math.max(0, (width - MIN_RADIUS_WITH_LABELS * 2) / 2)
|
|
436
|
+
: 0
|
|
437
|
+
const labelGutter = showCategoryPercentageLabels
|
|
438
|
+
? Math.min(Math.max(maxPieLabelWidth + LABEL_GUTTER_PADDING, MIN_LABEL_GUTTER), maxHorizontalLabelGutter)
|
|
439
|
+
: 0
|
|
440
|
+
const availableDiameter = showCategoryPercentageLabels
|
|
441
|
+
? Math.min(width - labelGutter * 2, height - VERTICAL_LABEL_GUTTER * 2)
|
|
442
|
+
: Math.min(width, height)
|
|
443
|
+
|
|
444
|
+
const radius = showCategoryPercentageLabels ? Math.max(0, availableDiameter / 2) : Math.min(width, height) / 2
|
|
445
|
+
const svgWidth = showCategoryPercentageLabels ? width : radius * 2
|
|
446
|
+
const svgLeftOffset = Math.max((props.parentWidth - svgWidth) / 2, 0)
|
|
339
447
|
const centerY = height / 2
|
|
340
|
-
const centerX =
|
|
341
|
-
const donutThickness = config.pieType === 'Donut' ? 75 : radius
|
|
448
|
+
const centerX = showCategoryPercentageLabels ? svgWidth / 2 : radius
|
|
449
|
+
const donutThickness = config.pieType === 'Donut' ? Math.min(75, radius * 0.65) : radius
|
|
342
450
|
|
|
343
451
|
useEffect(() => {
|
|
344
452
|
if (seriesHighlight.length > 0 && config.legend.behavior !== 'highlight') {
|
|
@@ -389,7 +497,7 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
|
|
|
389
497
|
<>
|
|
390
498
|
<ErrorBoundary component='PieChart'>
|
|
391
499
|
<svg
|
|
392
|
-
width={
|
|
500
|
+
width={svgWidth}
|
|
393
501
|
height={height}
|
|
394
502
|
className={getSvgClasses()}
|
|
395
503
|
role='img'
|
|
@@ -400,7 +508,7 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
|
|
|
400
508
|
handleChartMouseLeave()
|
|
401
509
|
}}
|
|
402
510
|
>
|
|
403
|
-
<Group top={centerY} left={
|
|
511
|
+
<Group top={centerY} left={centerX}>
|
|
404
512
|
{/* prettier-ignore */}
|
|
405
513
|
<Pie
|
|
406
514
|
data={filteredData || _data}
|
|
@@ -435,7 +543,7 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
|
|
|
435
543
|
}) !important`}</style>
|
|
436
544
|
<TooltipWithBounds
|
|
437
545
|
className={'tooltip cove-visualization'}
|
|
438
|
-
left={tooltipLeft +
|
|
546
|
+
left={tooltipLeft + svgLeftOffset}
|
|
439
547
|
top={tooltipTop}
|
|
440
548
|
>
|
|
441
549
|
<ul>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getPieLabelPlacement, getPieLabelPosition } from './labelPlacement'
|
|
3
|
+
|
|
4
|
+
describe('pie label placement', () => {
|
|
5
|
+
it('keeps labels inside regular pie slices when the slice has enough room', () => {
|
|
6
|
+
expect(
|
|
7
|
+
getPieLabelPlacement({
|
|
8
|
+
startAngle: 0,
|
|
9
|
+
endAngle: Math.PI / 2,
|
|
10
|
+
innerRadius: 0,
|
|
11
|
+
outerRadius: 120,
|
|
12
|
+
labelWidth: 50,
|
|
13
|
+
labelHeight: 28,
|
|
14
|
+
isDonut: false
|
|
15
|
+
})
|
|
16
|
+
).toBe('inside')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('moves regular pie labels outside when they are too wide for the slice', () => {
|
|
20
|
+
expect(
|
|
21
|
+
getPieLabelPlacement({
|
|
22
|
+
startAngle: 0,
|
|
23
|
+
endAngle: Math.PI / 8,
|
|
24
|
+
innerRadius: 0,
|
|
25
|
+
outerRadius: 120,
|
|
26
|
+
labelWidth: 80,
|
|
27
|
+
labelHeight: 28,
|
|
28
|
+
isDonut: false
|
|
29
|
+
})
|
|
30
|
+
).toBe('outside')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('always places donut labels outside the partition', () => {
|
|
34
|
+
expect(
|
|
35
|
+
getPieLabelPlacement({
|
|
36
|
+
startAngle: 0,
|
|
37
|
+
endAngle: Math.PI,
|
|
38
|
+
innerRadius: 60,
|
|
39
|
+
outerRadius: 120,
|
|
40
|
+
labelWidth: 20,
|
|
41
|
+
labelHeight: 20,
|
|
42
|
+
isDonut: true
|
|
43
|
+
})
|
|
44
|
+
).toBe('outside')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('anchors outside labels by side of the pie', () => {
|
|
48
|
+
const rightSide = getPieLabelPosition({
|
|
49
|
+
startAngle: 0,
|
|
50
|
+
endAngle: Math.PI / 8,
|
|
51
|
+
innerRadius: 0,
|
|
52
|
+
outerRadius: 120,
|
|
53
|
+
labelWidth: 80,
|
|
54
|
+
labelHeight: 28,
|
|
55
|
+
isDonut: false
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const leftSide = getPieLabelPosition({
|
|
59
|
+
startAngle: Math.PI,
|
|
60
|
+
endAngle: Math.PI + Math.PI / 8,
|
|
61
|
+
innerRadius: 0,
|
|
62
|
+
outerRadius: 120,
|
|
63
|
+
labelWidth: 80,
|
|
64
|
+
labelHeight: 28,
|
|
65
|
+
isDonut: false
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
expect(rightSide.placement).toBe('outside')
|
|
69
|
+
expect(rightSide.textAnchor).toBe('start')
|
|
70
|
+
expect(leftSide.placement).toBe('outside')
|
|
71
|
+
expect(leftSide.textAnchor).toBe('end')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('uses the same angle orientation as the arc generator', () => {
|
|
75
|
+
const topLabel = getPieLabelPosition({
|
|
76
|
+
startAngle: 0,
|
|
77
|
+
endAngle: Math.PI / 12,
|
|
78
|
+
innerRadius: 0,
|
|
79
|
+
outerRadius: 120,
|
|
80
|
+
labelWidth: 80,
|
|
81
|
+
labelHeight: 28,
|
|
82
|
+
isDonut: false
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
expect(topLabel.placement).toBe('outside')
|
|
86
|
+
expect(topLabel.y).toBeLessThan(0)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export type PieLabelPlacement = 'inside' | 'outside'
|
|
2
|
+
|
|
3
|
+
export type PieLabelAnchor = 'start' | 'middle' | 'end'
|
|
4
|
+
|
|
5
|
+
export type PieLabelPosition = {
|
|
6
|
+
placement: PieLabelPlacement
|
|
7
|
+
x: number
|
|
8
|
+
y: number
|
|
9
|
+
textAnchor: PieLabelAnchor
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type PieLabelPlacementOptions = {
|
|
13
|
+
startAngle: number
|
|
14
|
+
endAngle: number
|
|
15
|
+
innerRadius: number
|
|
16
|
+
outerRadius: number
|
|
17
|
+
labelWidth: number
|
|
18
|
+
labelHeight: number
|
|
19
|
+
isDonut: boolean
|
|
20
|
+
insidePadding?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type PieLabelPositionOptions = PieLabelPlacementOptions & {
|
|
24
|
+
outsideOffset?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DEFAULT_INSIDE_PADDING = 12
|
|
28
|
+
const DEFAULT_OUTSIDE_OFFSET = 22
|
|
29
|
+
|
|
30
|
+
const getMidAngle = (startAngle: number, endAngle: number) => startAngle + (endAngle - startAngle) / 2
|
|
31
|
+
const getArcPoint = (angle: number, radius: number) => ({
|
|
32
|
+
x: Math.sin(angle) * radius,
|
|
33
|
+
y: -Math.cos(angle) * radius
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export const getPieLabelPlacement = ({
|
|
37
|
+
startAngle,
|
|
38
|
+
endAngle,
|
|
39
|
+
innerRadius,
|
|
40
|
+
outerRadius,
|
|
41
|
+
labelWidth,
|
|
42
|
+
labelHeight,
|
|
43
|
+
isDonut,
|
|
44
|
+
insidePadding = DEFAULT_INSIDE_PADDING
|
|
45
|
+
}: PieLabelPlacementOptions): PieLabelPlacement => {
|
|
46
|
+
if (isDonut) return 'outside'
|
|
47
|
+
|
|
48
|
+
const angle = Math.abs(endAngle - startAngle)
|
|
49
|
+
const labelRadius = innerRadius + (outerRadius - innerRadius) * 0.62
|
|
50
|
+
const availableChord = 2 * labelRadius * Math.sin(angle / 2)
|
|
51
|
+
const availableRadialDepth = outerRadius - innerRadius
|
|
52
|
+
|
|
53
|
+
const hasEnoughArcWidth = labelWidth + insidePadding <= availableChord
|
|
54
|
+
const hasEnoughRadialDepth = labelHeight + insidePadding <= availableRadialDepth
|
|
55
|
+
|
|
56
|
+
return hasEnoughArcWidth && hasEnoughRadialDepth ? 'inside' : 'outside'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const getPieLabelPosition = ({
|
|
60
|
+
startAngle,
|
|
61
|
+
endAngle,
|
|
62
|
+
innerRadius,
|
|
63
|
+
outerRadius,
|
|
64
|
+
labelWidth,
|
|
65
|
+
labelHeight,
|
|
66
|
+
isDonut,
|
|
67
|
+
insidePadding,
|
|
68
|
+
outsideOffset = DEFAULT_OUTSIDE_OFFSET
|
|
69
|
+
}: PieLabelPositionOptions): PieLabelPosition => {
|
|
70
|
+
const placement = getPieLabelPlacement({
|
|
71
|
+
startAngle,
|
|
72
|
+
endAngle,
|
|
73
|
+
innerRadius,
|
|
74
|
+
outerRadius,
|
|
75
|
+
labelWidth,
|
|
76
|
+
labelHeight,
|
|
77
|
+
isDonut,
|
|
78
|
+
insidePadding
|
|
79
|
+
})
|
|
80
|
+
const midAngle = getMidAngle(startAngle, endAngle)
|
|
81
|
+
const labelRadius =
|
|
82
|
+
placement === 'inside' ? innerRadius + (outerRadius - innerRadius) * 0.62 : outerRadius + outsideOffset
|
|
83
|
+
const { x, y } = getArcPoint(midAngle, labelRadius)
|
|
84
|
+
|
|
85
|
+
if (placement === 'inside') {
|
|
86
|
+
return { placement, x, y, textAnchor: 'middle' }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
placement,
|
|
91
|
+
x,
|
|
92
|
+
y,
|
|
93
|
+
textAnchor: x < 0 ? 'end' : 'start'
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type ReactNode, type RefObject } from 'react'
|
|
2
|
+
import ParentSize from '@visx/responsive/lib/components/ParentSize'
|
|
3
|
+
import BarChartRace, { BarChartRaceFallback, type BarRaceEligibility } from './BarChartRace'
|
|
4
|
+
import LineChartRace, { LineChartRaceFallback, type LineRaceEligibility } from './LineChartRace'
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
barRace: BarRaceEligibility
|
|
8
|
+
family: 'Bar' | 'Line'
|
|
9
|
+
lineRace: LineRaceEligibility
|
|
10
|
+
parentRef: RefObject<HTMLDivElement>
|
|
11
|
+
renderTopYAxisTitles: () => ReactNode
|
|
12
|
+
svgRef: RefObject<SVGAElement>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const RacingChartRenderer = ({ barRace, family, lineRace, parentRef, renderTopYAxisTitles, svgRef }: Props) => {
|
|
16
|
+
const eligibility = family === 'Bar' ? barRace : lineRace
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
{(family === 'Line' || !eligibility.eligible) && renderTopYAxisTitles()}
|
|
21
|
+
<div ref={parentRef} style={{ width: '100%' }}>
|
|
22
|
+
<ParentSize>
|
|
23
|
+
{parent => {
|
|
24
|
+
if (family === 'Bar') {
|
|
25
|
+
return barRace.eligible ? (
|
|
26
|
+
<BarChartRace parentWidth={parent.width} race={barRace} />
|
|
27
|
+
) : (
|
|
28
|
+
<BarChartRaceFallback ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return lineRace.eligible ? (
|
|
33
|
+
<LineChartRace ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} race={lineRace} />
|
|
34
|
+
) : (
|
|
35
|
+
<LineChartRaceFallback ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
|
|
36
|
+
)
|
|
37
|
+
}}
|
|
38
|
+
</ParentSize>
|
|
39
|
+
</div>
|
|
40
|
+
</>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default RacingChartRenderer
|