@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,60 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { expect, userEvent, waitFor, within } from 'storybook/test'
|
|
3
|
+
import { editConfigKeys } from '@cdc/core/helpers/configHelpers'
|
|
4
|
+
import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
|
|
5
|
+
import Chart from '../CdcChart'
|
|
6
|
+
import simplifiedLine from './_mock/simplified_line.json'
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof Chart> = {
|
|
9
|
+
title: 'Components/Templates/Data Table Download Filename',
|
|
10
|
+
component: Chart
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Story = StoryObj<typeof Chart>
|
|
14
|
+
|
|
15
|
+
const captureImageDownloadName = async (downloadButton: HTMLElement) => {
|
|
16
|
+
const downloads: string[] = []
|
|
17
|
+
const originalClick = HTMLAnchorElement.prototype.click
|
|
18
|
+
|
|
19
|
+
HTMLAnchorElement.prototype.click = function () {
|
|
20
|
+
if (this.download) downloads.push(this.download)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await userEvent.click(downloadButton)
|
|
25
|
+
await waitFor(() => expect(downloads.length).toBeGreaterThan(0), { timeout: 10000 })
|
|
26
|
+
return downloads[0]
|
|
27
|
+
} finally {
|
|
28
|
+
HTMLAnchorElement.prototype.click = originalClick
|
|
29
|
+
document.body.querySelectorAll('a[download]').forEach(anchor => anchor.remove())
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const ChartImageDownloadUsesTableFilenameFallback: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
config: editConfigKeys(simplifiedLine, [
|
|
36
|
+
{ path: ['title'], value: '' },
|
|
37
|
+
{ path: ['dataFileSourceType'], value: 'file' },
|
|
38
|
+
{ path: ['dataUrl'], value: '' },
|
|
39
|
+
{ path: ['dataFileName'], value: '' },
|
|
40
|
+
{ path: ['table', 'show'], value: true },
|
|
41
|
+
{ path: ['table', 'expanded'], value: true },
|
|
42
|
+
{ path: ['table', 'download'], value: true },
|
|
43
|
+
{ path: ['table', 'showDownloadImgButton'], value: true },
|
|
44
|
+
{ path: ['table', 'downloadFileName'], value: 'Chart Table Report.csv' },
|
|
45
|
+
{ path: ['table', 'showDownloadLinkBelow'], value: false }
|
|
46
|
+
]),
|
|
47
|
+
isEditor: false
|
|
48
|
+
},
|
|
49
|
+
play: async ({ canvasElement }) => {
|
|
50
|
+
await assertVisualizationRendered(canvasElement)
|
|
51
|
+
|
|
52
|
+
const canvas = within(canvasElement)
|
|
53
|
+
const downloadButton = await canvas.findByRole('button', { name: 'Download Chart as Image' })
|
|
54
|
+
const downloadName = await captureImageDownloadName(downloadButton)
|
|
55
|
+
|
|
56
|
+
expect(downloadName).toMatch(/^chart-table-report-\d{4}-\d{2}-\d{2}\.png$/)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default meta
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { expect, fireEvent, userEvent, within } from 'storybook/test'
|
|
3
|
+
import {
|
|
4
|
+
assertVisualizationRendered,
|
|
5
|
+
openAccordion,
|
|
6
|
+
performAndAssert,
|
|
7
|
+
waitForEditor,
|
|
8
|
+
waitForPresence
|
|
9
|
+
} from '@cdc/core/helpers/testing'
|
|
10
|
+
import Chart from '../CdcChartComponent'
|
|
11
|
+
import { createNewChartConfig } from '../helpers/createNewChartConfig'
|
|
12
|
+
|
|
13
|
+
const data = [
|
|
14
|
+
{ node: 'Public Health', parent: '', style: 'solid', nodeColor: '#005eaa' },
|
|
15
|
+
{ node: 'Programs', parent: 'Public Health', style: 'solid', nodeColor: '#4b830d' },
|
|
16
|
+
{ node: 'Partners', parent: 'Public Health', style: 'dashed', nodeColor: '#712177' },
|
|
17
|
+
{ node: 'Clinics', parent: 'Partners', style: 'solid', nodeColor: '#c05600' }
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const dendrogramConfig = createNewChartConfig({
|
|
21
|
+
type: 'chart',
|
|
22
|
+
visualizationType: 'Dendrogram',
|
|
23
|
+
title: 'Public Health Hierarchy',
|
|
24
|
+
enableTooltips: true,
|
|
25
|
+
data,
|
|
26
|
+
dendrogram: {
|
|
27
|
+
columns: { node: 'node', parent: 'parent', style: 'style', nodeColor: 'nodeColor' },
|
|
28
|
+
orientation: 'horizontal',
|
|
29
|
+
height: 420,
|
|
30
|
+
nodeRadius: 6,
|
|
31
|
+
nodeColor: '#005eaa',
|
|
32
|
+
linkColor: '#333333',
|
|
33
|
+
leafSpacing: 40,
|
|
34
|
+
depthSpacing: 140
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const labelCollisionData = [
|
|
39
|
+
{ node: 'WWWW Public Health Operations', parent: '', style: 'solid', nodeColor: '#005eaa' },
|
|
40
|
+
{
|
|
41
|
+
node: 'Community Prevention Programs',
|
|
42
|
+
parent: 'WWWW Public Health Operations',
|
|
43
|
+
style: 'solid',
|
|
44
|
+
nodeColor: '#4b830d'
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
node: '区域 Clinical and Laboratory Partners 🙂',
|
|
48
|
+
parent: 'WWWW Public Health Operations',
|
|
49
|
+
style: 'dashed',
|
|
50
|
+
nodeColor: '#712177'
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
node: 'Regional Public Health Laboratories',
|
|
54
|
+
parent: '区域 Clinical and Laboratory Partners 🙂',
|
|
55
|
+
style: 'dashed',
|
|
56
|
+
nodeColor: '#c05600'
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
const labelOverlapsLink = (label: SVGTextElement, link: SVGPathElement) => {
|
|
61
|
+
const labelBounds = label.getBBox()
|
|
62
|
+
const labelTransform = label.getCTM()
|
|
63
|
+
const linkTransform = link.getCTM()
|
|
64
|
+
if (!labelTransform || !linkTransform) {
|
|
65
|
+
throw new Error('Dendrogram collision characterization requires SVG label and link CTMs.')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const overlapsAtDistance = (distance: number) => {
|
|
69
|
+
const point = link
|
|
70
|
+
.getPointAtLength(distance)
|
|
71
|
+
.matrixTransform(linkTransform)
|
|
72
|
+
.matrixTransform(labelTransform.inverse())
|
|
73
|
+
if (
|
|
74
|
+
point.x >= labelBounds.x &&
|
|
75
|
+
point.x <= labelBounds.x + labelBounds.width &&
|
|
76
|
+
point.y >= labelBounds.y &&
|
|
77
|
+
point.y <= labelBounds.y + labelBounds.height
|
|
78
|
+
) {
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const linkLength = link.getTotalLength()
|
|
85
|
+
for (let distance = 0; distance < linkLength; distance += 1) {
|
|
86
|
+
if (overlapsAtDistance(distance)) return true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return overlapsAtDistance(linkLength)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const meta: Meta<typeof Chart> = {
|
|
93
|
+
title: 'Components/Templates/Chart/Dendrogram',
|
|
94
|
+
component: Chart
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default meta
|
|
98
|
+
type Story = StoryObj<typeof Chart>
|
|
99
|
+
|
|
100
|
+
export const Dendrogram_Runtime: Story = {
|
|
101
|
+
args: { config: dendrogramConfig, isEditor: false },
|
|
102
|
+
play: async ({ canvasElement }) => {
|
|
103
|
+
await assertVisualizationRendered(canvasElement)
|
|
104
|
+
const svg = await waitForPresence('.dendrogram-chart', canvasElement)
|
|
105
|
+
expect(svg.querySelectorAll('.dendrogram-chart__node')).toHaveLength(4)
|
|
106
|
+
expect(svg.querySelectorAll('.dendrogram-chart__link')).toHaveLength(3)
|
|
107
|
+
expect(svg.querySelectorAll('.dendrogram-chart__link--dashed')).toHaveLength(1)
|
|
108
|
+
expect(window.getComputedStyle(svg.querySelector('.dendrogram-chart__label')!).fontSize).toBe('16px')
|
|
109
|
+
const linkStyles = within(canvasElement).getByRole('group', { name: 'Link styles' })
|
|
110
|
+
expect(within(linkStyles).getByText('Solid')).toBeVisible()
|
|
111
|
+
expect(within(linkStyles).getByText('Dashed')).toBeVisible()
|
|
112
|
+
await userEvent.click(within(canvasElement).getByRole('button', { name: 'Zoom in' }))
|
|
113
|
+
await userEvent.click(within(canvasElement).getByRole('button', { name: 'Reset view' }))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const Dendrogram_Narrow: Story = {
|
|
118
|
+
args: { config: dendrogramConfig, isEditor: false },
|
|
119
|
+
decorators: [
|
|
120
|
+
Story => (
|
|
121
|
+
<div style={{ width: 360 }}>
|
|
122
|
+
<Story />
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
],
|
|
126
|
+
play: async ({ canvasElement }) => {
|
|
127
|
+
await assertVisualizationRendered(canvasElement)
|
|
128
|
+
const svg = await waitForPresence('.dendrogram-chart', canvasElement)
|
|
129
|
+
expect(svg.querySelectorAll('.dendrogram-chart__node')).toHaveLength(4)
|
|
130
|
+
expect(within(canvasElement).getByRole('button', { name: 'Zoom in' })).toBeVisible()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const Dendrogram_Label_Collision_And_Link_Styles: Story = {
|
|
135
|
+
args: {
|
|
136
|
+
config: createNewChartConfig({
|
|
137
|
+
...dendrogramConfig,
|
|
138
|
+
title: 'Dendrogram Label and Link Style Characterization',
|
|
139
|
+
data: labelCollisionData,
|
|
140
|
+
dendrogram: {
|
|
141
|
+
...dendrogramConfig.dendrogram,
|
|
142
|
+
height: 360,
|
|
143
|
+
linkStyleLabels: {
|
|
144
|
+
solid: ' Established relationships across programs ',
|
|
145
|
+
dashed: ' Provisional partnerships under review '
|
|
146
|
+
},
|
|
147
|
+
leafSpacing: 32,
|
|
148
|
+
depthSpacing: 150
|
|
149
|
+
}
|
|
150
|
+
}),
|
|
151
|
+
isEditor: false
|
|
152
|
+
},
|
|
153
|
+
decorators: [
|
|
154
|
+
Story => (
|
|
155
|
+
<div style={{ width: 700 }}>
|
|
156
|
+
<Story />
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
],
|
|
160
|
+
play: async ({ canvasElement }) => {
|
|
161
|
+
await assertVisualizationRendered(canvasElement)
|
|
162
|
+
|
|
163
|
+
const labels = [...canvasElement.querySelectorAll<SVGTextElement>('.dendrogram-chart__label')]
|
|
164
|
+
const links = [
|
|
165
|
+
...canvasElement.querySelectorAll<SVGPathElement>('.dendrogram-chart__viewport .dendrogram-chart__link')
|
|
166
|
+
]
|
|
167
|
+
expect(labels).toHaveLength(labelCollisionData.length)
|
|
168
|
+
expect(getComputedStyle(labels[0]).fontSize).toBe('16px')
|
|
169
|
+
expect(labels.some(label => links.some(link => labelOverlapsLink(label, link)))).toBe(false)
|
|
170
|
+
|
|
171
|
+
const canvas = within(canvasElement)
|
|
172
|
+
const linkStyles = canvas.getByRole('group', { name: 'Link styles' })
|
|
173
|
+
expect(within(linkStyles).getByText('Established relationships across programs')).toBeVisible()
|
|
174
|
+
expect(within(linkStyles).getByText('Provisional partnerships under review')).toBeVisible()
|
|
175
|
+
expect(getComputedStyle(linkStyles).overflowX).toBe('auto')
|
|
176
|
+
expect(getComputedStyle(linkStyles).whiteSpace).toBe('nowrap')
|
|
177
|
+
expect(getComputedStyle(linkStyles.querySelector('.dendrogram-chart__link-key-item')!).flexShrink).toBe('0')
|
|
178
|
+
|
|
179
|
+
const legendLines = linkStyles.querySelectorAll('line')
|
|
180
|
+
expect(legendLines).toHaveLength(2)
|
|
181
|
+
const renderedSolid = canvasElement.querySelector(
|
|
182
|
+
'.dendrogram-chart__viewport .dendrogram-chart__link:not(.dendrogram-chart__link--dashed)'
|
|
183
|
+
)!
|
|
184
|
+
const renderedDashed = canvasElement.querySelector('.dendrogram-chart__viewport .dendrogram-chart__link--dashed')!
|
|
185
|
+
expect(getComputedStyle(legendLines[0]).strokeDasharray).toBe(getComputedStyle(renderedSolid).strokeDasharray)
|
|
186
|
+
expect(getComputedStyle(legendLines[1]).strokeDasharray).toBe(getComputedStyle(renderedDashed).strokeDasharray)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const Dendrogram_Vertical_Label_Collision: Story = {
|
|
191
|
+
args: {
|
|
192
|
+
config: createNewChartConfig({
|
|
193
|
+
...dendrogramConfig,
|
|
194
|
+
title: 'Vertical Dendrogram Label Collision Characterization',
|
|
195
|
+
data: labelCollisionData,
|
|
196
|
+
dendrogram: {
|
|
197
|
+
...dendrogramConfig.dendrogram,
|
|
198
|
+
orientation: 'vertical',
|
|
199
|
+
height: 480,
|
|
200
|
+
leafSpacing: 56,
|
|
201
|
+
depthSpacing: 150
|
|
202
|
+
}
|
|
203
|
+
}),
|
|
204
|
+
isEditor: false
|
|
205
|
+
},
|
|
206
|
+
decorators: [
|
|
207
|
+
Story => (
|
|
208
|
+
<div style={{ width: 760 }}>
|
|
209
|
+
<Story />
|
|
210
|
+
</div>
|
|
211
|
+
)
|
|
212
|
+
],
|
|
213
|
+
play: async ({ canvasElement }) => {
|
|
214
|
+
await assertVisualizationRendered(canvasElement)
|
|
215
|
+
const labels = [...canvasElement.querySelectorAll<SVGTextElement>('.dendrogram-chart__label')]
|
|
216
|
+
const links = [
|
|
217
|
+
...canvasElement.querySelectorAll<SVGPathElement>('.dendrogram-chart__viewport .dendrogram-chart__link')
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
expect(labels).toHaveLength(labelCollisionData.length)
|
|
221
|
+
expect(labels.some(label => links.some(link => labelOverlapsLink(label, link)))).toBe(false)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export const Dendrogram_Editor: Story = {
|
|
226
|
+
args: { config: dendrogramConfig, isEditor: true },
|
|
227
|
+
play: async ({ canvasElement }) => {
|
|
228
|
+
const canvas = within(canvasElement)
|
|
229
|
+
await waitForEditor(canvas)
|
|
230
|
+
await openAccordion(canvas, 'Dendrogram')
|
|
231
|
+
expect(canvas.queryByLabelText('Show Tooltips')).not.toBeInTheDocument()
|
|
232
|
+
expect(canvas.queryByLabelText('Horizontal Alignment')).not.toBeInTheDocument()
|
|
233
|
+
expect(canvas.queryByLabelText('Vertical Alignment')).not.toBeInTheDocument()
|
|
234
|
+
expect(canvas.queryByLabelText('Show Node Labels')).not.toBeInTheDocument()
|
|
235
|
+
|
|
236
|
+
const nodeColumn = canvas.getByLabelText('Node ID Column')
|
|
237
|
+
await performAndAssert(
|
|
238
|
+
'Node mapping validates the hierarchy',
|
|
239
|
+
() => canvasElement.querySelector('[role="alert"]')?.textContent || '',
|
|
240
|
+
() => userEvent.selectOptions(nodeColumn, 'nodeColor'),
|
|
241
|
+
(_before, after) => after.includes('references missing parent')
|
|
242
|
+
)
|
|
243
|
+
await userEvent.selectOptions(nodeColumn, 'node')
|
|
244
|
+
|
|
245
|
+
const parentColumn = canvas.getByLabelText('Parent ID Column')
|
|
246
|
+
await performAndAssert(
|
|
247
|
+
'Parent mapping validates the hierarchy',
|
|
248
|
+
() => canvasElement.querySelector('[role="alert"]')?.textContent || '',
|
|
249
|
+
() => userEvent.selectOptions(parentColumn, 'style'),
|
|
250
|
+
(_before, after) => after.includes('references missing parent')
|
|
251
|
+
)
|
|
252
|
+
await userEvent.selectOptions(parentColumn, 'parent')
|
|
253
|
+
|
|
254
|
+
const linkStyleColumn = canvas.getByLabelText('Link Style Column (Optional)')
|
|
255
|
+
await performAndAssert(
|
|
256
|
+
'Link style mapping changes rendered dashes',
|
|
257
|
+
() => canvasElement.querySelectorAll('.dendrogram-chart__viewport .dendrogram-chart__link--dashed').length,
|
|
258
|
+
() => userEvent.selectOptions(linkStyleColumn, ''),
|
|
259
|
+
(before, after) => before === 1 && after === 0
|
|
260
|
+
)
|
|
261
|
+
await userEvent.selectOptions(linkStyleColumn, 'style')
|
|
262
|
+
|
|
263
|
+
const getLegendCaptions = () =>
|
|
264
|
+
[...canvasElement.querySelectorAll('.dendrogram-chart__link-key-item span')].map(item => item.textContent)
|
|
265
|
+
const solidLinkLabel = canvas.getByLabelText('Solid Link Legend Label')
|
|
266
|
+
await performAndAssert(
|
|
267
|
+
'Solid link legend label updates the rendered key',
|
|
268
|
+
getLegendCaptions,
|
|
269
|
+
async () => {
|
|
270
|
+
await userEvent.clear(solidLinkLabel)
|
|
271
|
+
await userEvent.type(solidLinkLabel, 'Established')
|
|
272
|
+
},
|
|
273
|
+
(before, after) => before.includes('Solid') && after.includes('Established')
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
const dashedLinkLabel = canvas.getByLabelText('Dashed Link Legend Label')
|
|
277
|
+
await performAndAssert(
|
|
278
|
+
'Dashed link legend label updates the rendered key',
|
|
279
|
+
getLegendCaptions,
|
|
280
|
+
async () => {
|
|
281
|
+
await userEvent.clear(dashedLinkLabel)
|
|
282
|
+
await userEvent.type(dashedLinkLabel, 'Provisional')
|
|
283
|
+
},
|
|
284
|
+
(before, after) => before.includes('Dashed') && after.includes('Provisional')
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
const nodeColorColumn = canvas.getByLabelText('Node Color Column (Optional)')
|
|
288
|
+
await performAndAssert(
|
|
289
|
+
'Node color mapping changes rendered fills',
|
|
290
|
+
() =>
|
|
291
|
+
[...canvasElement.querySelectorAll('.dendrogram-chart__node circle')].map(node => node.getAttribute('fill')),
|
|
292
|
+
() => userEvent.selectOptions(nodeColorColumn, ''),
|
|
293
|
+
(before, after) => new Set(before).size > 1 && new Set(after).size === 1
|
|
294
|
+
)
|
|
295
|
+
await userEvent.selectOptions(nodeColorColumn, 'nodeColor')
|
|
296
|
+
|
|
297
|
+
const orientation = canvas.getByLabelText('Orientation')
|
|
298
|
+
await performAndAssert(
|
|
299
|
+
'Vertical orientation changes node placement',
|
|
300
|
+
() => canvasElement.querySelector('.dendrogram-chart__node')?.getAttribute('transform'),
|
|
301
|
+
() => userEvent.selectOptions(orientation, 'vertical'),
|
|
302
|
+
(before, after) => Boolean(before && after && before !== after && after.startsWith('translate('))
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
const connectionType = canvas.getByLabelText('Connection Type')
|
|
306
|
+
const getLinkPath = () =>
|
|
307
|
+
canvasElement.querySelector('.dendrogram-chart__viewport .dendrogram-chart__link')?.getAttribute('d') || ''
|
|
308
|
+
await performAndAssert(
|
|
309
|
+
'Straight connections use direct line segments',
|
|
310
|
+
getLinkPath,
|
|
311
|
+
() => userEvent.selectOptions(connectionType, 'line'),
|
|
312
|
+
(before, after) => before.includes('C') && after.includes('L') && !after.includes('C')
|
|
313
|
+
)
|
|
314
|
+
await performAndAssert(
|
|
315
|
+
'Elbow connections use orthogonal line segments',
|
|
316
|
+
getLinkPath,
|
|
317
|
+
() => userEvent.selectOptions(connectionType, 'elbow'),
|
|
318
|
+
(before, after) => before !== after && after.split('L').length === 4
|
|
319
|
+
)
|
|
320
|
+
await performAndAssert(
|
|
321
|
+
'Curved connections restore the default path',
|
|
322
|
+
getLinkPath,
|
|
323
|
+
() => userEvent.selectOptions(connectionType, 'curve'),
|
|
324
|
+
(before, after) => before.includes('L') && after.includes('C')
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
const height = canvas.getByLabelText('Height')
|
|
328
|
+
await performAndAssert(
|
|
329
|
+
'Height changes the chart container',
|
|
330
|
+
() => (canvasElement.querySelector('.dendrogram-chart__container') as HTMLElement)?.style.height,
|
|
331
|
+
() => fireEvent.change(height, { target: { value: '480' } }),
|
|
332
|
+
(before, after) => before === '420px' && after === '480px'
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
const nodeRadius = canvas.getByLabelText('Node Radius')
|
|
336
|
+
await performAndAssert(
|
|
337
|
+
'Node radius changes rendered circles',
|
|
338
|
+
() => canvasElement.querySelector('.dendrogram-chart__node circle')?.getAttribute('r'),
|
|
339
|
+
() => fireEvent.change(nodeRadius, { target: { value: '10' } }),
|
|
340
|
+
(before, after) => before === '6' && after === '10'
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
const leafSpacing = canvas.getByLabelText('Leaf Spacing')
|
|
344
|
+
await performAndAssert(
|
|
345
|
+
'Leaf spacing changes cross-axis placement',
|
|
346
|
+
() => canvasElement.querySelectorAll('.dendrogram-chart__node')[3]?.getAttribute('transform'),
|
|
347
|
+
() => fireEvent.change(leafSpacing, { target: { value: '45' } }),
|
|
348
|
+
(before, after) => Boolean(before && after && before !== after)
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
const depthSpacing = canvas.getByLabelText('Depth Spacing')
|
|
352
|
+
await performAndAssert(
|
|
353
|
+
'Depth spacing changes hierarchy-level placement',
|
|
354
|
+
() => canvasElement.querySelectorAll('.dendrogram-chart__node')[3]?.getAttribute('transform'),
|
|
355
|
+
() => fireEvent.change(depthSpacing, { target: { value: '150' } }),
|
|
356
|
+
(before, after) => Boolean(before && after && before !== after)
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export const Dendrogram_Validation: Story = {
|
|
362
|
+
args: {
|
|
363
|
+
config: createNewChartConfig({
|
|
364
|
+
...dendrogramConfig,
|
|
365
|
+
data: [{ node: 'Child', parent: 'Missing' }]
|
|
366
|
+
}),
|
|
367
|
+
isEditor: false
|
|
368
|
+
},
|
|
369
|
+
play: async ({ canvasElement }) => {
|
|
370
|
+
await waitForPresence('[role="alert"]', canvasElement)
|
|
371
|
+
expect(within(canvasElement).getByRole('alert')).toHaveTextContent('references missing parent')
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { expect, fireEvent, fn, userEvent, waitFor, within } from 'storybook/test'
|
|
3
|
+
import cloneConfig from '@cdc/core/helpers/cloneConfig'
|
|
4
|
+
import { assertVisualizationRendered, openAccordion, performAndAssert, waitForEditor } from '@cdc/core/helpers/testing'
|
|
5
|
+
|
|
6
|
+
import Chart from '../CdcChartComponent'
|
|
7
|
+
import forecastComboWithGaps from './_mock/forecast_combo_with_gaps.json'
|
|
8
|
+
|
|
9
|
+
const meta: Meta<typeof Chart> = {
|
|
10
|
+
title: 'Components/Templates/Chart/Forecast/Confidence Interval Legend',
|
|
11
|
+
component: Chart
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default meta
|
|
15
|
+
type Story = StoryObj<typeof Chart>
|
|
16
|
+
|
|
17
|
+
const buildConfig = () => {
|
|
18
|
+
const config = cloneConfig(forecastComboWithGaps) as any
|
|
19
|
+
config.title = 'Forecast Confidence Intervals'
|
|
20
|
+
config.visualizationType = 'Forecasting'
|
|
21
|
+
config.legend.hide = false
|
|
22
|
+
config.legend.position = 'bottom'
|
|
23
|
+
config.legend.expandForecastConfidenceIntervals = true
|
|
24
|
+
config.xAxis.label = 'Date'
|
|
25
|
+
config.xAxis.tickRotation = 0
|
|
26
|
+
config.yAxis.label = 'Cases'
|
|
27
|
+
config.yAxis.max = ''
|
|
28
|
+
config.general.palette = { name: 'qualitative_standard', version: '2.0', isReversed: false }
|
|
29
|
+
config.columns = Object.fromEntries(
|
|
30
|
+
['Lower 50%', 'Upper 50%', 'Lower 90%', 'Upper 90%'].map(name => [
|
|
31
|
+
name,
|
|
32
|
+
{
|
|
33
|
+
name,
|
|
34
|
+
label: name,
|
|
35
|
+
dataTable: true,
|
|
36
|
+
tooltips: name.includes('90%'),
|
|
37
|
+
prefix: '',
|
|
38
|
+
suffix: '',
|
|
39
|
+
roundToPlace: 0,
|
|
40
|
+
commas: false
|
|
41
|
+
}
|
|
42
|
+
])
|
|
43
|
+
)
|
|
44
|
+
config.series = [
|
|
45
|
+
{
|
|
46
|
+
dataKey: 'Stage',
|
|
47
|
+
type: 'Forecasting',
|
|
48
|
+
axis: 'Left',
|
|
49
|
+
tooltip: false,
|
|
50
|
+
stageColumn: 'Stage',
|
|
51
|
+
stages: [
|
|
52
|
+
{ key: 'Estimate', color: 'sequential-blue' },
|
|
53
|
+
{ key: 'Forecast', color: 'sequential-orange' }
|
|
54
|
+
],
|
|
55
|
+
confidenceIntervals: [
|
|
56
|
+
{ high: 'Upper 90%', low: 'Lower 90%' },
|
|
57
|
+
{ high: 'Upper 50%', low: 'Lower 50%' }
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
config.data = [
|
|
62
|
+
{
|
|
63
|
+
'Target End Date': '01/01/2026',
|
|
64
|
+
Stage: 'Estimate',
|
|
65
|
+
'Lower 50%': 90,
|
|
66
|
+
'Upper 50%': 110,
|
|
67
|
+
'Lower 90%': 75,
|
|
68
|
+
'Upper 90%': 125
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
'Target End Date': '01/08/2026',
|
|
72
|
+
Stage: 'Estimate',
|
|
73
|
+
'Lower 50%': 98,
|
|
74
|
+
'Upper 50%': 118,
|
|
75
|
+
'Lower 90%': 82,
|
|
76
|
+
'Upper 90%': 135
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
'Target End Date': '01/15/2026',
|
|
80
|
+
Stage: 'Estimate',
|
|
81
|
+
'Lower 50%': 105,
|
|
82
|
+
'Upper 50%': 126,
|
|
83
|
+
'Lower 90%': 88,
|
|
84
|
+
'Upper 90%': 144
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
'Target End Date': '01/22/2026',
|
|
88
|
+
Stage: 'Forecast',
|
|
89
|
+
'Lower 50%': 101,
|
|
90
|
+
'Upper 50%': 130,
|
|
91
|
+
'Lower 90%': 80,
|
|
92
|
+
'Upper 90%': 154
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
'Target End Date': '01/29/2026',
|
|
96
|
+
Stage: 'Forecast',
|
|
97
|
+
'Lower 50%': 96,
|
|
98
|
+
'Upper 50%': 132,
|
|
99
|
+
'Lower 90%': 72,
|
|
100
|
+
'Upper 90%': 160
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
'Target End Date': '02/05/2026',
|
|
104
|
+
Stage: 'Forecast',
|
|
105
|
+
'Lower 50%': 91,
|
|
106
|
+
'Upper 50%': 136,
|
|
107
|
+
'Lower 90%': 65,
|
|
108
|
+
'Upper 90%': 168
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
return config
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const tooltipConfigUpdates = fn()
|
|
115
|
+
|
|
116
|
+
const showForecastTooltip = async (canvasElement: HTMLElement) => {
|
|
117
|
+
const hitArea = canvasElement.querySelector<SVGRectElement>('.forecasting-items rect')
|
|
118
|
+
expect(hitArea).toBeTruthy()
|
|
119
|
+
const bounds = hitArea!.getBoundingClientRect()
|
|
120
|
+
|
|
121
|
+
fireEvent.mouseOut(hitArea!)
|
|
122
|
+
await waitFor(() => expect(canvasElement.querySelector('.tooltip.cove-visualization')).toBeNull())
|
|
123
|
+
fireEvent.mouseMove(hitArea!, {
|
|
124
|
+
clientX: bounds.left + bounds.width * 0.65,
|
|
125
|
+
clientY: bounds.top + bounds.height * 0.5
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
await waitFor(() => expect(canvasElement.querySelector('.tooltip.cove-visualization')).toBeTruthy())
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const getTooltipBoundsState = (canvasElement: HTMLElement) => {
|
|
132
|
+
const tooltipText = canvasElement.querySelector('.tooltip.cove-visualization')?.textContent || ''
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
has50Bounds: tooltipText.includes('Lower 50%') && tooltipText.includes('Upper 50%'),
|
|
136
|
+
has90Bounds: tooltipText.includes('Lower 90%') && tooltipText.includes('Upper 90%')
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const LegendRowsAndIndependentInteraction: Story = {
|
|
141
|
+
args: { config: buildConfig(), isEditor: false },
|
|
142
|
+
play: async ({ canvasElement }) => {
|
|
143
|
+
await assertVisualizationRendered(canvasElement)
|
|
144
|
+
const canvas = within(canvasElement)
|
|
145
|
+
const legendItems = canvas.getAllByRole('button').filter(button => button.closest('[aria-label="legend"]'))
|
|
146
|
+
|
|
147
|
+
expect(legendItems.map(item => item.textContent?.trim())).toEqual([
|
|
148
|
+
'50% CI Estimate',
|
|
149
|
+
'50% CI Forecast',
|
|
150
|
+
'90% CI Estimate',
|
|
151
|
+
'90% CI Forecast'
|
|
152
|
+
])
|
|
153
|
+
expect(canvas.queryByRole('button', { name: 'Show All' })).not.toBeInTheDocument()
|
|
154
|
+
|
|
155
|
+
const innerMarker = legendItems[0].querySelector<HTMLElement>('span.legend-item')
|
|
156
|
+
const outerMarker = legendItems[2].querySelector<HTMLElement>('span.legend-item')
|
|
157
|
+
expect(innerMarker).toBeTruthy()
|
|
158
|
+
expect(outerMarker).toBeTruthy()
|
|
159
|
+
expect(getComputedStyle(innerMarker!).backgroundColor).not.toBe(getComputedStyle(outerMarker!).backgroundColor)
|
|
160
|
+
|
|
161
|
+
await userEvent.click(legendItems[0])
|
|
162
|
+
await waitFor(() => {
|
|
163
|
+
expect(legendItems[0]).toHaveClass('highlighted')
|
|
164
|
+
legendItems.slice(1).forEach(item => expect(item).toHaveClass('inactive'))
|
|
165
|
+
expect(canvas.getByRole('button', { name: 'Show All' })).toBeInTheDocument()
|
|
166
|
+
|
|
167
|
+
const forecastBands = Array.from(canvasElement.querySelectorAll<SVGGElement>('[data-forecast-legend-key]'))
|
|
168
|
+
const activeBands = forecastBands.filter(band => band.dataset.forecastActive === 'true')
|
|
169
|
+
expect(activeBands).toHaveLength(1)
|
|
170
|
+
expect(activeBands[0].dataset.forecastLegendKey).toBe('forecast-ci:Estimate:percentage%3A50')
|
|
171
|
+
expect(activeBands[0].querySelector('path')).not.toHaveAttribute('fill', 'transparent')
|
|
172
|
+
forecastBands
|
|
173
|
+
.filter(band => band !== activeBands[0])
|
|
174
|
+
.forEach(band => expect(band.querySelector('path')).toHaveAttribute('fill', 'transparent'))
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
await showForecastTooltip(canvasElement)
|
|
178
|
+
const tooltipText = canvasElement.querySelector('.tooltip.cove-visualization')?.textContent || ''
|
|
179
|
+
expect(getTooltipBoundsState(canvasElement)).toEqual({ has50Bounds: false, has90Bounds: true })
|
|
180
|
+
expect(tooltipText.match(/Lower 90%/g)).toHaveLength(1)
|
|
181
|
+
expect(tooltipText.match(/Upper 90%/g)).toHaveLength(1)
|
|
182
|
+
|
|
183
|
+
for (const legendItem of legendItems.slice(1)) {
|
|
184
|
+
await userEvent.click(legendItem)
|
|
185
|
+
}
|
|
186
|
+
await waitFor(() => {
|
|
187
|
+
legendItems.forEach(item => expect(item).toHaveClass('highlighted'))
|
|
188
|
+
expect(canvas.queryByRole('button', { name: 'Show All' })).not.toBeInTheDocument()
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const TooltipAuthoring: Story = {
|
|
194
|
+
args: { config: buildConfig(), isEditor: true, setConfig: tooltipConfigUpdates },
|
|
195
|
+
play: async ({ canvasElement }) => {
|
|
196
|
+
tooltipConfigUpdates.mockClear()
|
|
197
|
+
const canvas = within(canvasElement)
|
|
198
|
+
await waitForEditor(canvas)
|
|
199
|
+
await openAccordion(canvas, 'Columns')
|
|
200
|
+
|
|
201
|
+
const getColumnTooltipToggle = (columnName: string) => {
|
|
202
|
+
const columnButton = canvas
|
|
203
|
+
.getAllByRole('button', { name: new RegExp(columnName) })
|
|
204
|
+
.find(button => button.classList.contains('accordion__button')) as HTMLElement
|
|
205
|
+
expect(columnButton).toBeTruthy()
|
|
206
|
+
return within(columnButton.closest('.series-item--chart') as HTMLElement).getByRole('checkbox', {
|
|
207
|
+
name: 'Show in tooltip'
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await showForecastTooltip(canvasElement)
|
|
212
|
+
expect(getTooltipBoundsState(canvasElement)).toEqual({ has50Bounds: false, has90Bounds: true })
|
|
213
|
+
|
|
214
|
+
await performAndAssert(
|
|
215
|
+
'Include both 50% confidence interval bounds in tooltips',
|
|
216
|
+
() => getTooltipBoundsState(canvasElement),
|
|
217
|
+
async () => {
|
|
218
|
+
await userEvent.click(getColumnTooltipToggle('Lower 50%'))
|
|
219
|
+
await userEvent.click(getColumnTooltipToggle('Upper 50%'))
|
|
220
|
+
await showForecastTooltip(canvasElement)
|
|
221
|
+
},
|
|
222
|
+
(before, after) => !before.has50Bounds && after.has50Bounds && after.has90Bounds
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
await performAndAssert(
|
|
226
|
+
'Remove both 50% confidence interval bounds from tooltips',
|
|
227
|
+
() => getTooltipBoundsState(canvasElement),
|
|
228
|
+
async () => {
|
|
229
|
+
await userEvent.click(getColumnTooltipToggle('Lower 50%'))
|
|
230
|
+
await userEvent.click(getColumnTooltipToggle('Upper 50%'))
|
|
231
|
+
await showForecastTooltip(canvasElement)
|
|
232
|
+
},
|
|
233
|
+
(before, after) => before.has50Bounds && !after.has50Bounds && after.has90Bounds
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
}
|