@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
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { render } from '@testing-library/react'
|
|
3
|
-
import { describe, expect, it,
|
|
2
|
+
import { fireEvent, render } from '@testing-library/react'
|
|
3
|
+
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
|
|
4
4
|
import { scaleBand, scaleLinear } from '@visx/scale'
|
|
5
5
|
import AnnotationDraggable, { EVENT_LINE_LABEL_OFFSET, snapEventLineDx } from '../components/AnnotationDraggable'
|
|
6
6
|
import ConfigContext from '../../../ConfigContext'
|
|
7
7
|
import { createMockChartContext } from '../../LinearChart/tests/mockConfigContext'
|
|
8
|
+
import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
|
|
9
|
+
import getViewport from '@cdc/core/helpers/getViewport'
|
|
10
|
+
import { getAnnotationLabelRect } from '../helpers/resolveAnnotationLayout'
|
|
8
11
|
|
|
9
12
|
// jsdom compat for visx (ResizeObserver + SVG bbox).
|
|
10
13
|
vi.stubGlobal(
|
|
11
14
|
'ResizeObserver',
|
|
12
|
-
vi.fn(()
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
vi.fn(function ResizeObserver() {
|
|
16
|
+
return {
|
|
17
|
+
observe: vi.fn(),
|
|
18
|
+
unobserve: vi.fn(),
|
|
19
|
+
disconnect: vi.fn()
|
|
20
|
+
}
|
|
21
|
+
})
|
|
17
22
|
)
|
|
18
23
|
|
|
19
24
|
beforeAll(() => {
|
|
@@ -33,6 +38,23 @@ beforeAll(() => {
|
|
|
33
38
|
}))
|
|
34
39
|
})
|
|
35
40
|
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
vi.restoreAllMocks()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const mockElementRect = (width: number, height: number) =>
|
|
46
|
+
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({
|
|
47
|
+
x: 0,
|
|
48
|
+
y: 0,
|
|
49
|
+
width,
|
|
50
|
+
height,
|
|
51
|
+
top: 0,
|
|
52
|
+
left: 0,
|
|
53
|
+
right: width,
|
|
54
|
+
bottom: height,
|
|
55
|
+
toJSON: () => ({})
|
|
56
|
+
})
|
|
57
|
+
|
|
36
58
|
const buildScales = (xMax: number, yMax: number) => ({
|
|
37
59
|
xScale: scaleBand({ domain: ['Jan', 'Feb'], range: [0, xMax], padding: 0 }),
|
|
38
60
|
yScale: scaleLinear({ domain: [0, 100], range: [yMax, 0] }),
|
|
@@ -40,6 +62,13 @@ const buildScales = (xMax: number, yMax: number) => ({
|
|
|
40
62
|
yScaleAnnotation: scaleLinear({ domain: [0, 100], range: [0, yMax] })
|
|
41
63
|
})
|
|
42
64
|
|
|
65
|
+
const pointerEvent = (type: string, clientX: number, clientY = 0) => {
|
|
66
|
+
const event = new Event(type, { bubbles: true, cancelable: true })
|
|
67
|
+
Object.defineProperty(event, 'clientX', { value: clientX })
|
|
68
|
+
Object.defineProperty(event, 'clientY', { value: clientY })
|
|
69
|
+
return event
|
|
70
|
+
}
|
|
71
|
+
|
|
43
72
|
const baseEventLineAnnotation = {
|
|
44
73
|
text: 'Voting Rights Act',
|
|
45
74
|
style: 'event-line',
|
|
@@ -59,29 +88,35 @@ const baseEventLineAnnotation = {
|
|
|
59
88
|
lineType: 'curveLinear'
|
|
60
89
|
}
|
|
61
90
|
|
|
62
|
-
const
|
|
91
|
+
const buildAnnotationContext = (annotation: any, contextOverrides: Record<string, any> = {}) => {
|
|
63
92
|
const data = [
|
|
64
93
|
{ month: 'Jan', value: 50 },
|
|
65
94
|
{ month: 'Feb', value: 75 }
|
|
66
95
|
]
|
|
67
|
-
|
|
96
|
+
return createMockChartContext(
|
|
68
97
|
{
|
|
69
98
|
annotations: [annotation],
|
|
70
99
|
visualizationType: 'Line',
|
|
71
100
|
xAxis: { type: 'categorical', dataKey: 'month' } as any,
|
|
72
101
|
series: [{ dataKey: 'value', type: 'Line' }] as any,
|
|
73
102
|
data,
|
|
74
|
-
general: {
|
|
103
|
+
general: {
|
|
104
|
+
showAnnotationDropdown: contextOverrides.showAnnotationDropdown ?? false,
|
|
105
|
+
mobileAnnotationDisplay: contextOverrides.mobileAnnotationDisplay || 'symbol'
|
|
106
|
+
} as any
|
|
75
107
|
} as any,
|
|
76
108
|
{
|
|
77
109
|
transformedData: data,
|
|
78
|
-
visibleAnnotations: [annotation]
|
|
110
|
+
visibleAnnotations: [annotation],
|
|
111
|
+
...contextOverrides
|
|
79
112
|
}
|
|
80
113
|
)
|
|
114
|
+
}
|
|
81
115
|
|
|
116
|
+
const annotationTree = (context: any, xMax = 800, yMax = 400) => {
|
|
82
117
|
const scales = buildScales(xMax, yMax)
|
|
83
|
-
return
|
|
84
|
-
<ConfigContext.Provider value={context
|
|
118
|
+
return (
|
|
119
|
+
<ConfigContext.Provider value={context}>
|
|
85
120
|
<svg width={xMax} height={yMax}>
|
|
86
121
|
<AnnotationDraggable
|
|
87
122
|
{...scales}
|
|
@@ -89,14 +124,109 @@ const renderAnnotationDraggable = (annotation: any, xMax = 800, yMax = 400) => {
|
|
|
89
124
|
yMax={yMax}
|
|
90
125
|
seriesScale={undefined}
|
|
91
126
|
svgRef={{ current: null } as any}
|
|
92
|
-
onDragStateChange={() => {}}
|
|
127
|
+
onDragStateChange={context.handleDragStateChange || (() => {})}
|
|
93
128
|
/>
|
|
94
129
|
</svg>
|
|
95
130
|
</ConfigContext.Provider>
|
|
96
131
|
)
|
|
97
132
|
}
|
|
98
133
|
|
|
134
|
+
const renderAnnotationDraggable = (
|
|
135
|
+
annotation: any,
|
|
136
|
+
xMax = 800,
|
|
137
|
+
yMax = 400,
|
|
138
|
+
contextOverrides: Record<string, any> = {}
|
|
139
|
+
) => {
|
|
140
|
+
const context = buildAnnotationContext(annotation, contextOverrides)
|
|
141
|
+
context.handleDragStateChange = contextOverrides.handleDragStateChange || (() => {})
|
|
142
|
+
return render(annotationTree(context, xMax, yMax))
|
|
143
|
+
}
|
|
144
|
+
|
|
99
145
|
describe('AnnotationDraggable - event-line style', () => {
|
|
146
|
+
it.each(['right', 'left'] as const)('shows its dropdown number with the %s-side text label', side => {
|
|
147
|
+
const annotation = { ...baseEventLineAnnotation, labelPosition: side }
|
|
148
|
+
const { container } = renderAnnotationDraggable(annotation, 800, 400, { showAnnotationDropdown: true })
|
|
149
|
+
const label = container.querySelector('.annotation__event-line-label') as HTMLElement
|
|
150
|
+
const badge = label.querySelector('.annotation__has-dropdown-number') as HTMLElement
|
|
151
|
+
|
|
152
|
+
expect(badge?.textContent).toBe('1')
|
|
153
|
+
expect(label.style.display).toBe('inline-flex')
|
|
154
|
+
expect(label.querySelector('.annotation__event-line-label-text')?.textContent).toBe(annotation.text)
|
|
155
|
+
expect(label.classList.contains(`cove-annotation-event-line__label--${side}`)).toBe(true)
|
|
156
|
+
expect(
|
|
157
|
+
badge.compareDocumentPosition(label.querySelector('.annotation__event-line-label-text')!) &
|
|
158
|
+
Node.DOCUMENT_POSITION_FOLLOWING
|
|
159
|
+
).toBeTruthy()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('uses the original annotation index when callouts and event lines are mixed', () => {
|
|
163
|
+
const callout = { ...baseEventLineAnnotation, style: 'callout' }
|
|
164
|
+
const eventLine = { ...baseEventLineAnnotation }
|
|
165
|
+
const context = buildAnnotationContext(eventLine, { showAnnotationDropdown: true })
|
|
166
|
+
context.config.annotations = [callout, eventLine] as any
|
|
167
|
+
context.visibleAnnotations = [eventLine] as any
|
|
168
|
+
|
|
169
|
+
const { container } = render(annotationTree(context))
|
|
170
|
+
expect(container.querySelector('.annotation__event-line-label .annotation__has-dropdown-number')?.textContent).toBe(
|
|
171
|
+
'2'
|
|
172
|
+
)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('keeps the plain event-line text label when the dropdown is off', () => {
|
|
176
|
+
const { container } = renderAnnotationDraggable(baseEventLineAnnotation)
|
|
177
|
+
const label = container.querySelector('.annotation__event-line-label') as HTMLElement
|
|
178
|
+
|
|
179
|
+
expect(label.querySelector('.annotation__has-dropdown-number')).toBeNull()
|
|
180
|
+
expect(label.querySelector('.annotation__event-line-label-text')).toBeNull()
|
|
181
|
+
expect(label.style.display).toBe('')
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('keeps the authored 4em width floor with a dropdown badge', () => {
|
|
185
|
+
const { container } = renderAnnotationDraggable({ ...baseEventLineAnnotation, labelWidthEm: 2 }, 800, 400, {
|
|
186
|
+
showAnnotationDropdown: true
|
|
187
|
+
})
|
|
188
|
+
const label = container.querySelector('.annotation__event-line-label') as HTMLElement
|
|
189
|
+
|
|
190
|
+
expect((label.parentElement as HTMLElement).style.width).toBe('64px')
|
|
191
|
+
expect(label.querySelector('.annotation__has-dropdown-number')).toBeTruthy()
|
|
192
|
+
expect(label.querySelector('.annotation__event-line-label-text')).toBeTruthy()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('shows the dropdown badge at the smallest HTML-label viewport', () => {
|
|
196
|
+
const { container } = renderAnnotationDraggable(baseEventLineAnnotation, 577, 400, {
|
|
197
|
+
showAnnotationDropdown: true,
|
|
198
|
+
vizViewport: 'sm'
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
expect(container.querySelector('.annotation__event-line-label .annotation__has-dropdown-number')?.textContent).toBe(
|
|
202
|
+
'1'
|
|
203
|
+
)
|
|
204
|
+
expect(container.querySelector('.annotation__mobile-label')).toBeNull()
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('uses one mobile symbol number without a desktop badge', () => {
|
|
208
|
+
const { container } = renderAnnotationDraggable(baseEventLineAnnotation, 320, 200, {
|
|
209
|
+
showAnnotationDropdown: true,
|
|
210
|
+
vizViewport: 'xs'
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
expect(container.querySelector('text.annotation__mobile-label')?.textContent).toBe('1')
|
|
214
|
+
expect(container.querySelector('.annotation__has-dropdown-number')).toBeNull()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('shows a badge in mobile full-text mode when the dropdown is on', () => {
|
|
218
|
+
const { container } = renderAnnotationDraggable(baseEventLineAnnotation, 320, 200, {
|
|
219
|
+
showAnnotationDropdown: true,
|
|
220
|
+
mobileAnnotationDisplay: 'text',
|
|
221
|
+
vizViewport: 'xs'
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
expect(container.querySelector('.annotation__event-line-label .annotation__has-dropdown-number')?.textContent).toBe(
|
|
225
|
+
'1'
|
|
226
|
+
)
|
|
227
|
+
expect(container.querySelector('.annotation__mobile-label')).toBeNull()
|
|
228
|
+
})
|
|
229
|
+
|
|
100
230
|
it('renders a full-height vertical line that spans 0..yMax', () => {
|
|
101
231
|
const { container } = renderAnnotationDraggable(baseEventLineAnnotation)
|
|
102
232
|
|
|
@@ -106,14 +236,19 @@ describe('AnnotationDraggable - event-line style', () => {
|
|
|
106
236
|
expect(line?.getAttribute('y2')).toBe('400')
|
|
107
237
|
})
|
|
108
238
|
|
|
109
|
-
it('
|
|
239
|
+
it('uses the application font color for the vertical line and label, ignoring legacy annotation colors', () => {
|
|
110
240
|
const { container } = renderAnnotationDraggable({
|
|
111
241
|
...baseEventLineAnnotation,
|
|
112
242
|
colors: { connector: '#ff0000', label: '#000' }
|
|
113
243
|
})
|
|
114
244
|
|
|
115
245
|
const line = container.querySelector('line.annotation__event-line')
|
|
116
|
-
|
|
246
|
+
const label = container.querySelector('.annotation__event-line-label') as HTMLElement
|
|
247
|
+
const expectedColor = document.createElement('div')
|
|
248
|
+
expectedColor.style.color = APP_FONT_COLOR
|
|
249
|
+
|
|
250
|
+
expect(line?.getAttribute('stroke')).toBe(APP_FONT_COLOR)
|
|
251
|
+
expect(label.style.color).toBe(expectedColor.style.color)
|
|
117
252
|
})
|
|
118
253
|
|
|
119
254
|
it('uses the right-side label class (text-align left) when dx >= 0', () => {
|
|
@@ -122,8 +257,11 @@ describe('AnnotationDraggable - event-line style', () => {
|
|
|
122
257
|
dx: 12
|
|
123
258
|
})
|
|
124
259
|
|
|
125
|
-
|
|
260
|
+
const label = container.querySelector('.cove-annotation-event-line__label--right') as HTMLElement
|
|
261
|
+
expect(label).toBeTruthy()
|
|
126
262
|
expect(container.querySelector('.cove-annotation-event-line__label--left')).toBeFalsy()
|
|
263
|
+
expect(label.dataset.contentAlignment).toBeUndefined()
|
|
264
|
+
expect(label.querySelector('.annotation__label-text')).toBeNull()
|
|
127
265
|
})
|
|
128
266
|
|
|
129
267
|
it('uses the left-side label class (text-align right) when dx < 0', () => {
|
|
@@ -144,6 +282,25 @@ describe('AnnotationDraggable - event-line style', () => {
|
|
|
144
282
|
|
|
145
283
|
expect(container.querySelector('.circle-subject')).toBeFalsy()
|
|
146
284
|
})
|
|
285
|
+
|
|
286
|
+
it('keeps the legacy 186px fit-content cap when width is omitted', () => {
|
|
287
|
+
const { container } = renderAnnotationDraggable(baseEventLineAnnotation, 120)
|
|
288
|
+
const label = container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
289
|
+
const wrapper = label.parentElement as HTMLElement
|
|
290
|
+
|
|
291
|
+
expect(wrapper.style.width).toBe('fit-content')
|
|
292
|
+
expect(wrapper.style.maxWidth).toBe('186px')
|
|
293
|
+
expect(label.style.boxSizing).toBe('')
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('renders an authored event-line width with the 4em floor', () => {
|
|
297
|
+
const { container } = renderAnnotationDraggable({ ...baseEventLineAnnotation, labelWidthEm: 2 })
|
|
298
|
+
const label = container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
299
|
+
const wrapper = label.parentElement as HTMLElement
|
|
300
|
+
|
|
301
|
+
expect(wrapper.style.width).toBe('64px')
|
|
302
|
+
expect(label.style.boxSizing).toBe('border-box')
|
|
303
|
+
})
|
|
147
304
|
})
|
|
148
305
|
|
|
149
306
|
describe('snapEventLineDx', () => {
|
|
@@ -184,15 +341,451 @@ describe('AnnotationDraggable - event-line dx snapping at render', () => {
|
|
|
184
341
|
})
|
|
185
342
|
|
|
186
343
|
describe('AnnotationDraggable - callout style (regression)', () => {
|
|
187
|
-
it('
|
|
344
|
+
it('renders text, connectors, and markers with the application font color', () => {
|
|
188
345
|
const calloutAnnotation = {
|
|
189
346
|
...baseEventLineAnnotation,
|
|
190
347
|
style: 'callout' as const,
|
|
191
348
|
anchorMode: 'fixed' as const,
|
|
192
|
-
dataX: undefined
|
|
349
|
+
dataX: undefined,
|
|
350
|
+
colors: { connector: '#ff0000', label: '#000', marker: '#00ff00' }
|
|
193
351
|
}
|
|
194
352
|
const { container } = renderAnnotationDraggable(calloutAnnotation)
|
|
195
353
|
|
|
196
354
|
expect(container.querySelector('line.annotation__event-line')).toBeFalsy()
|
|
355
|
+
expect(container.querySelector('.visx-annotation-connector')?.getAttribute('stroke')).toBe(APP_FONT_COLOR)
|
|
356
|
+
expect(container.querySelector('marker')?.getAttribute('stroke')).toBe(APP_FONT_COLOR)
|
|
357
|
+
|
|
358
|
+
const label = container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
359
|
+
const expectedColor = document.createElement('div')
|
|
360
|
+
expectedColor.style.color = APP_FONT_COLOR
|
|
361
|
+
expect(label.style.color).toBe(expectedColor.style.color)
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('preserves production dx/dy scaling from saved dimensions', () => {
|
|
365
|
+
const annotation = {
|
|
366
|
+
...baseEventLineAnnotation,
|
|
367
|
+
style: 'callout' as const,
|
|
368
|
+
anchorMode: 'fixed' as const,
|
|
369
|
+
dataX: undefined,
|
|
370
|
+
dx: 100,
|
|
371
|
+
dy: -50,
|
|
372
|
+
savedDimensions: [400, 200]
|
|
373
|
+
}
|
|
374
|
+
const { container } = renderAnnotationDraggable(annotation, 800, 400)
|
|
375
|
+
const label = container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
376
|
+
|
|
377
|
+
expect(label.dataset.labelDx).toBe('200')
|
|
378
|
+
expect(label.dataset.labelDy).toBe('-100')
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
describe('AnnotationDraggable - width and explicit placement', () => {
|
|
383
|
+
const calloutAnnotation = {
|
|
384
|
+
...baseEventLineAnnotation,
|
|
385
|
+
style: 'callout' as const,
|
|
386
|
+
anchorMode: 'fixed' as const,
|
|
387
|
+
dataX: undefined,
|
|
388
|
+
edit: { subject: false, label: true }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
it('keeps legacy fit-content width and automatic anchors when the new fields are omitted', () => {
|
|
392
|
+
const { container } = renderAnnotationDraggable(calloutAnnotation)
|
|
393
|
+
const label = container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
394
|
+
const wrapper = label.parentElement as HTMLElement
|
|
395
|
+
|
|
396
|
+
expect(wrapper.style.width).toBe('fit-content')
|
|
397
|
+
expect(wrapper.style.maxWidth).toBe('150px')
|
|
398
|
+
expect(label.dataset.horizontalAnchor).toBe('start')
|
|
399
|
+
expect(label.dataset.verticalAnchor).toBe('middle')
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('keeps legacy automatic text left aligned while placing the resize handle on the resolved side', () => {
|
|
403
|
+
const annotation = { ...calloutAnnotation, dx: -100, dy: 0 }
|
|
404
|
+
const { getByTestId, container } = renderAnnotationDraggable(annotation, 800, 400, { isEditor: true })
|
|
405
|
+
|
|
406
|
+
expect(getByTestId('annotation-resize-handle')).toHaveClass('annotation__resize-handle--left')
|
|
407
|
+
const label = container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
408
|
+
const text = label.querySelector('.annotation__label-text') as HTMLElement
|
|
409
|
+
expect(label).toHaveClass('annotation__label--editable')
|
|
410
|
+
expect(label.dataset.contentAlignment).toBe('start')
|
|
411
|
+
expect(text).not.toHaveClass('annotation__label-text--right')
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it('uses directional text alignment when autoSide is persisted', () => {
|
|
415
|
+
const annotation = { ...calloutAnnotation, dx: -100, dy: 0, autoSide: 'left' as const }
|
|
416
|
+
const { container } = renderAnnotationDraggable(annotation)
|
|
417
|
+
const label = container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
418
|
+
const text = label.querySelector('.annotation__label-text') as HTMLElement
|
|
419
|
+
|
|
420
|
+
expect(label.dataset.contentAlignment).toBe('end')
|
|
421
|
+
expect(text).toHaveClass('annotation__label-text--right')
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('centers the content block for an automatically resolved vertical label while keeping its text left aligned', () => {
|
|
425
|
+
const annotation = { ...calloutAnnotation, dx: 10, dy: -100, autoSide: 'above' as const, labelWidthEm: 12 }
|
|
426
|
+
const { container } = renderAnnotationDraggable(annotation)
|
|
427
|
+
const label = container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
428
|
+
const text = label.querySelector('.annotation__label-text') as HTMLElement
|
|
429
|
+
|
|
430
|
+
expect(label.dataset.contentAlignment).toBe('center')
|
|
431
|
+
expect(text).toHaveClass('annotation__label-text')
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
it('removes the label circle while retaining the enabled subject circle', () => {
|
|
435
|
+
const annotation = { ...calloutAnnotation, edit: { subject: true, label: true } }
|
|
436
|
+
const { container } = renderAnnotationDraggable(annotation, 800, 400, { isEditor: true })
|
|
437
|
+
|
|
438
|
+
expect(container.querySelectorAll('circle[stroke="red"]')).toHaveLength(1)
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
it('keeps the measured label mounted after label and subject positions are saved', () => {
|
|
442
|
+
const annotation = { ...calloutAnnotation, edit: { subject: true, label: true } }
|
|
443
|
+
const updateConfig = vi.fn()
|
|
444
|
+
const firstRender = renderAnnotationDraggable(annotation, 800, 400, { isEditor: true, updateConfig })
|
|
445
|
+
const initialLabel = firstRender.container.querySelector('g.annotation__desktop-label')
|
|
446
|
+
const label = firstRender.container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
447
|
+
|
|
448
|
+
fireEvent(label, pointerEvent('pointerdown', 0, 0))
|
|
449
|
+
fireEvent(window, pointerEvent('pointermove', 40, 20))
|
|
450
|
+
fireEvent(window, pointerEvent('pointerup', 40, 20))
|
|
451
|
+
|
|
452
|
+
const labelDraggedAnnotation = updateConfig.mock.calls.at(-1)?.[0].annotations[0]
|
|
453
|
+
const labelContext = buildAnnotationContext(labelDraggedAnnotation, { isEditor: true, updateConfig })
|
|
454
|
+
firstRender.rerender(annotationTree(labelContext))
|
|
455
|
+
expect(firstRender.container.querySelector('g.annotation__desktop-label')).toBe(initialLabel)
|
|
456
|
+
|
|
457
|
+
const subjectHandle = firstRender.getByTestId('annotation-subject-drag-handle')
|
|
458
|
+
fireEvent(subjectHandle, pointerEvent('pointerdown', 0, 0))
|
|
459
|
+
fireEvent(window, pointerEvent('pointermove', 30, 15))
|
|
460
|
+
fireEvent(window, pointerEvent('pointerup', 30, 15))
|
|
461
|
+
|
|
462
|
+
const subjectDraggedAnnotation = updateConfig.mock.calls.at(-1)?.[0].annotations[0]
|
|
463
|
+
const subjectContext = buildAnnotationContext(subjectDraggedAnnotation, { isEditor: true, updateConfig })
|
|
464
|
+
firstRender.rerender(annotationTree(subjectContext))
|
|
465
|
+
expect(firstRender.container.querySelector('g.annotation__desktop-label')).toBe(initialLabel)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it('does not outline labels outside editor label-edit mode', () => {
|
|
469
|
+
const updateConfig = vi.fn()
|
|
470
|
+
const { container, queryByTestId } = renderAnnotationDraggable(
|
|
471
|
+
{
|
|
472
|
+
...calloutAnnotation,
|
|
473
|
+
edit: { subject: false, label: false }
|
|
474
|
+
},
|
|
475
|
+
800,
|
|
476
|
+
400,
|
|
477
|
+
{ isEditor: true, updateConfig }
|
|
478
|
+
)
|
|
479
|
+
const label = container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
480
|
+
|
|
481
|
+
expect(label).not.toHaveClass('annotation__label--editable')
|
|
482
|
+
expect(queryByTestId('annotation-resize-handle')).toBeNull()
|
|
483
|
+
fireEvent(label, pointerEvent('pointerdown', 0, 0))
|
|
484
|
+
fireEvent(window, pointerEvent('pointermove', 60, 30))
|
|
485
|
+
fireEvent(window, pointerEvent('pointerup', 60, 30))
|
|
486
|
+
expect(updateConfig).not.toHaveBeenCalled()
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
it('derives pixel widths from saved em values and the responsive font size', () => {
|
|
490
|
+
const wide = { ...calloutAnnotation, labelWidthEm: 12.5 }
|
|
491
|
+
const first = renderAnnotationDraggable(wide, 800, 400, { currentViewport: 'lg', vizViewport: 'lg' })
|
|
492
|
+
const firstLabel = first.container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
493
|
+
expect((firstLabel.parentElement as HTMLElement).style.width).toBe('200px')
|
|
494
|
+
expect(firstLabel.style.fontSize).toBe('16px')
|
|
495
|
+
first.unmount()
|
|
496
|
+
|
|
497
|
+
const second = renderAnnotationDraggable(wide, 400, 400, {
|
|
498
|
+
currentViewport: 'lg',
|
|
499
|
+
vizViewport: 'xxs',
|
|
500
|
+
mobileAnnotationDisplay: 'text'
|
|
501
|
+
})
|
|
502
|
+
const secondLabel = second.container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
503
|
+
expect((secondLabel.parentElement as HTMLElement).style.width).toBe('162.5px')
|
|
504
|
+
expect(secondLabel.style.fontSize).toBe('13px')
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
it.each([
|
|
508
|
+
['left', 'end', 'middle', 'end'],
|
|
509
|
+
['right', 'start', 'middle', 'start'],
|
|
510
|
+
['above', 'middle', 'end', 'center'],
|
|
511
|
+
['below', 'middle', 'start', 'center']
|
|
512
|
+
] as const)(
|
|
513
|
+
'maps %s directly to its Visx anchors and content alignment at plot edges',
|
|
514
|
+
(labelPosition, horizontal, vertical, contentAlignment) => {
|
|
515
|
+
const annotation = {
|
|
516
|
+
...calloutAnnotation,
|
|
517
|
+
x: labelPosition === 'right' ? 100 : 0,
|
|
518
|
+
y: labelPosition === 'below' ? 100 : 0,
|
|
519
|
+
dx: labelPosition === 'left' ? -240 : 240,
|
|
520
|
+
dy: labelPosition === 'above' ? -180 : 180,
|
|
521
|
+
labelPosition,
|
|
522
|
+
labelWidthEm: 12
|
|
523
|
+
}
|
|
524
|
+
const { container } = renderAnnotationDraggable(annotation)
|
|
525
|
+
const label = container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
526
|
+
|
|
527
|
+
expect(label.dataset.horizontalAnchor).toBe(horizontal)
|
|
528
|
+
expect(label.dataset.verticalAnchor).toBe(vertical)
|
|
529
|
+
expect(label.dataset.contentAlignment).toBe(contentAlignment)
|
|
530
|
+
expect(label.querySelector('.annotation__label-text')).toHaveClass(
|
|
531
|
+
contentAlignment === 'end' ? 'annotation__label-text--right' : 'annotation__label-text'
|
|
532
|
+
)
|
|
533
|
+
if (contentAlignment !== 'end') {
|
|
534
|
+
expect(label.querySelector('.annotation__label-text')).not.toHaveClass('annotation__label-text--right')
|
|
535
|
+
}
|
|
536
|
+
expect(label.dataset.resolvedSide).toBe(labelPosition)
|
|
537
|
+
}
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
it('keeps explicit event-line side and placement when the label is dragged', () => {
|
|
541
|
+
const updateConfig = vi.fn()
|
|
542
|
+
const annotation = { ...baseEventLineAnnotation, labelPosition: 'left' as const }
|
|
543
|
+
const { container } = renderAnnotationDraggable(annotation, 800, 400, { isEditor: true, updateConfig })
|
|
544
|
+
const label = container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
545
|
+
const labelGroup = container.querySelector('g.annotation__desktop-label')
|
|
546
|
+
const initialTransform = labelGroup?.getAttribute('transform')
|
|
547
|
+
const initialX = initialTransform?.match(/translate\(([^,]+)/)?.[1]
|
|
548
|
+
|
|
549
|
+
expect(container.querySelector('circle[stroke="red"]')).toBeNull()
|
|
550
|
+
fireEvent(label, pointerEvent('pointerdown', 0, 0))
|
|
551
|
+
fireEvent(window, pointerEvent('pointermove', 80, 40))
|
|
552
|
+
expect(labelGroup?.getAttribute('transform')?.match(/translate\(([^,]+)/)?.[1]).toBe(initialX)
|
|
553
|
+
fireEvent(window, pointerEvent('pointerup', 80, 40))
|
|
554
|
+
|
|
555
|
+
const saved = updateConfig.mock.calls.at(-1)?.[0].annotations[0]
|
|
556
|
+
expect(saved.labelPosition).toBe('left')
|
|
557
|
+
expect(saved.dx).toBe(-EVENT_LINE_LABEL_OFFSET)
|
|
558
|
+
expect(saved.dy).not.toBe(annotation.dy)
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
it('keeps explicit callout placement when the label is dragged', () => {
|
|
562
|
+
const updateConfig = vi.fn()
|
|
563
|
+
const annotation = { ...calloutAnnotation, labelPosition: 'above' as const }
|
|
564
|
+
const { container } = renderAnnotationDraggable(annotation, 800, 400, { isEditor: true, updateConfig })
|
|
565
|
+
const label = container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
566
|
+
|
|
567
|
+
expect(container.querySelector('circle[stroke="red"]')).toBeNull()
|
|
568
|
+
fireEvent(label, pointerEvent('pointerdown', 0, 0))
|
|
569
|
+
expect(label).toHaveClass('annotation__label--dragging')
|
|
570
|
+
fireEvent(window, pointerEvent('pointermove', 60, 30))
|
|
571
|
+
fireEvent(window, pointerEvent('pointerup', 60, 30))
|
|
572
|
+
expect(label).not.toHaveClass('annotation__label--dragging')
|
|
573
|
+
|
|
574
|
+
const saved = updateConfig.mock.calls.at(-1)?.[0].annotations[0]
|
|
575
|
+
expect(saved.labelPosition).toBe('above')
|
|
576
|
+
expect(saved.dx).not.toBe(annotation.dx)
|
|
577
|
+
expect(saved.dy).not.toBe(annotation.dy)
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
it('persists the bounded automatic position used by the label and connector', () => {
|
|
581
|
+
mockElementRect(96, 40)
|
|
582
|
+
const updateConfig = vi.fn()
|
|
583
|
+
const annotation = { ...calloutAnnotation, x: 90, y: 50, dx: 100, dy: 0 }
|
|
584
|
+
const { container } = renderAnnotationDraggable(annotation, 800, 400, { isEditor: true, updateConfig })
|
|
585
|
+
const label = container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
586
|
+
|
|
587
|
+
expect(label.dataset.resolvedSide).toBe('right')
|
|
588
|
+
fireEvent(label, pointerEvent('pointerdown', 0, 0))
|
|
589
|
+
fireEvent(window, pointerEvent('pointermove', 20, 15))
|
|
590
|
+
fireEvent(window, pointerEvent('pointerup', 20, 15))
|
|
591
|
+
|
|
592
|
+
const saved = updateConfig.mock.calls.at(-1)?.[0].annotations[0]
|
|
593
|
+
expect(saved.dx).toBe(-16)
|
|
594
|
+
expect(saved.dy).toBe(15)
|
|
595
|
+
expect(saved.autoSide).toBe('right')
|
|
596
|
+
expect(saved.savedDimensions).toEqual([800, 400])
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
it('keeps the dragged rectangle continuous when its facing edge changes', () => {
|
|
600
|
+
mockElementRect(100, 20)
|
|
601
|
+
const updateConfig = vi.fn()
|
|
602
|
+
const annotation = { ...calloutAnnotation, x: 50, y: 50, dx: 100, dy: 0, labelWidthEm: 6.25 }
|
|
603
|
+
const { container } = renderAnnotationDraggable(annotation, 800, 400, { isEditor: true, updateConfig })
|
|
604
|
+
const label = container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
605
|
+
|
|
606
|
+
fireEvent(label, pointerEvent('pointerdown', 0, 0))
|
|
607
|
+
fireEvent(window, pointerEvent('pointermove', -200, -20))
|
|
608
|
+
expect(label.dataset.resolvedSide).toBe('above')
|
|
609
|
+
fireEvent(window, pointerEvent('pointerup', -200, -20))
|
|
610
|
+
|
|
611
|
+
const saved = updateConfig.mock.calls.at(-1)?.[0].annotations[0]
|
|
612
|
+
expect(saved.autoSide).toBe('above')
|
|
613
|
+
expect(getAnnotationLabelRect(400 + saved.dx, 200 + saved.dy, 100, 20, saved.autoSide)).toEqual({
|
|
614
|
+
left: 300,
|
|
615
|
+
top: 170,
|
|
616
|
+
width: 100,
|
|
617
|
+
height: 20
|
|
618
|
+
})
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
it('keeps a persisted automatic side across chart sizes', () => {
|
|
622
|
+
const annotation = { ...calloutAnnotation, x: 90, y: 50, dx: 100, dy: 0, autoSide: 'right' as const }
|
|
623
|
+
const wide = renderAnnotationDraggable(annotation, 800, 400)
|
|
624
|
+
const wideLabel = wide.container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
625
|
+
expect(wideLabel.dataset.resolvedSide).toBe('right')
|
|
626
|
+
wide.unmount()
|
|
627
|
+
|
|
628
|
+
const narrow = renderAnnotationDraggable(annotation, 300, 400)
|
|
629
|
+
const narrowLabel = narrow.container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
630
|
+
expect(narrowLabel.dataset.resolvedSide).toBe('right')
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
it('uses scaled offsets to choose the legacy automatic side when autoSide is omitted', () => {
|
|
634
|
+
const annotation = {
|
|
635
|
+
...calloutAnnotation,
|
|
636
|
+
dx: -43,
|
|
637
|
+
dy: -38,
|
|
638
|
+
savedDimensions: [1368, 250]
|
|
639
|
+
}
|
|
640
|
+
const taller = renderAnnotationDraggable(annotation, 800, 200)
|
|
641
|
+
const tallerLabel = taller.container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
642
|
+
expect(tallerLabel.dataset.resolvedSide).toBe('above')
|
|
643
|
+
taller.unmount()
|
|
644
|
+
|
|
645
|
+
const shorter = renderAnnotationDraggable(annotation, 800, 100)
|
|
646
|
+
const shorterLabel = shorter.container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
647
|
+
expect(shorterLabel.dataset.resolvedSide).toBe('left')
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
it('stops at the top edge and begins the next drag without dead distance', () => {
|
|
651
|
+
mockElementRect(100, 20)
|
|
652
|
+
const updateConfig = vi.fn()
|
|
653
|
+
const annotation = { ...calloutAnnotation, labelPosition: 'right' as const }
|
|
654
|
+
const rendered = renderAnnotationDraggable(annotation, 800, 400, { isEditor: true, updateConfig })
|
|
655
|
+
let label = rendered.container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
656
|
+
|
|
657
|
+
fireEvent(label, pointerEvent('pointerdown', 0, 0))
|
|
658
|
+
fireEvent(window, pointerEvent('pointermove', 0, -500))
|
|
659
|
+
expect(label.dataset.labelDy).toBe('-190')
|
|
660
|
+
fireEvent(window, pointerEvent('pointerup', 0, -500))
|
|
661
|
+
|
|
662
|
+
const firstSaved = updateConfig.mock.calls.at(-1)?.[0].annotations[0]
|
|
663
|
+
expect(firstSaved.dy).toBe(-190)
|
|
664
|
+
|
|
665
|
+
const secondContext = buildAnnotationContext(firstSaved, { isEditor: true, updateConfig })
|
|
666
|
+
rendered.rerender(annotationTree(secondContext))
|
|
667
|
+
label = rendered.container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
668
|
+
fireEvent(label, pointerEvent('pointerdown', 0, 0))
|
|
669
|
+
fireEvent(window, pointerEvent('pointermove', 0, 5))
|
|
670
|
+
fireEvent(window, pointerEvent('pointerup', 0, 5))
|
|
671
|
+
|
|
672
|
+
const secondSaved = updateConfig.mock.calls.at(-1)?.[0].annotations[0]
|
|
673
|
+
expect(secondSaved.dy).toBe(-185)
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
it('previews resize locally, persists em on release, and does not start label dragging', () => {
|
|
677
|
+
const updateConfig = vi.fn()
|
|
678
|
+
const onDragStateChange = vi.fn()
|
|
679
|
+
const annotation = { ...calloutAnnotation, labelPosition: 'right' as const }
|
|
680
|
+
const { getByTestId, container } = renderAnnotationDraggable(annotation, 800, 400, {
|
|
681
|
+
isEditor: true,
|
|
682
|
+
updateConfig,
|
|
683
|
+
handleDragStateChange: onDragStateChange
|
|
684
|
+
})
|
|
685
|
+
const handle = getByTestId('annotation-resize-handle')
|
|
686
|
+
|
|
687
|
+
fireEvent(handle, pointerEvent('pointerdown', 100))
|
|
688
|
+
fireEvent(window, pointerEvent('pointermove', 350))
|
|
689
|
+
expect(updateConfig).not.toHaveBeenCalled()
|
|
690
|
+
expect(onDragStateChange).not.toHaveBeenCalled()
|
|
691
|
+
const label = container.querySelector('div[aria-label^="Annotation text"]') as HTMLElement
|
|
692
|
+
expect((label.parentElement as HTMLElement).style.width).toBe('388px')
|
|
693
|
+
|
|
694
|
+
fireEvent(window, pointerEvent('pointerup', 350))
|
|
695
|
+
expect(updateConfig).toHaveBeenCalledOnce()
|
|
696
|
+
expect(updateConfig.mock.calls[0][0].annotations[0].labelWidthEm).toBe(24.25)
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
it.each([
|
|
700
|
+
['callout', calloutAnnotation, 6],
|
|
701
|
+
['event-line', baseEventLineAnnotation, 4]
|
|
702
|
+
] as const)('uses the %s resize floor', (_style, annotation, expectedWidthEm) => {
|
|
703
|
+
const updateConfig = vi.fn()
|
|
704
|
+
const { getByTestId } = renderAnnotationDraggable(annotation, 800, 400, {
|
|
705
|
+
isEditor: true,
|
|
706
|
+
updateConfig
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
const handle = getByTestId('annotation-resize-handle')
|
|
710
|
+
fireEvent(handle, pointerEvent('pointerdown', 300))
|
|
711
|
+
fireEvent(window, pointerEvent('pointermove', 0))
|
|
712
|
+
fireEvent(window, pointerEvent('pointerup', 0))
|
|
713
|
+
|
|
714
|
+
expect(updateConfig.mock.calls[0][0].annotations[0].labelWidthEm).toBe(expectedWidthEm)
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
it('leaves mobile symbol annotations unchanged', () => {
|
|
718
|
+
const annotation = {
|
|
719
|
+
...calloutAnnotation,
|
|
720
|
+
labelPosition: 'left' as const,
|
|
721
|
+
labelWidthEm: 12
|
|
722
|
+
}
|
|
723
|
+
const { container, queryByTestId } = renderAnnotationDraggable(annotation, 320, 200, {
|
|
724
|
+
currentViewport: 'lg',
|
|
725
|
+
vizViewport: 'xxs',
|
|
726
|
+
isEditor: true
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
expect(container.querySelector('.annotation__desktop-label')).toBeFalsy()
|
|
730
|
+
expect(container.querySelector('.annotation__mobile-label-circle')).toBeTruthy()
|
|
731
|
+
expect(queryByTestId('annotation-resize-handle')).toBeNull()
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
it.each(['line', 'elbow', 'curve'] as const)(
|
|
735
|
+
'connects the %s path to the mobile number instead of the hidden text layout',
|
|
736
|
+
connectionType => {
|
|
737
|
+
const annotation = {
|
|
738
|
+
...calloutAnnotation,
|
|
739
|
+
connectionType,
|
|
740
|
+
dx: 100,
|
|
741
|
+
dy: 0,
|
|
742
|
+
labelPosition: 'right' as const,
|
|
743
|
+
labelWidthEm: 12
|
|
744
|
+
}
|
|
745
|
+
const { container } = renderAnnotationDraggable(annotation, 320, 200, {
|
|
746
|
+
currentViewport: 'xxs',
|
|
747
|
+
vizViewport: 'xxs'
|
|
748
|
+
})
|
|
749
|
+
const circle = container.querySelector('.annotation__mobile-label-circle')
|
|
750
|
+
const connector =
|
|
751
|
+
connectionType === 'curve'
|
|
752
|
+
? container.querySelector(`path[marker-start="url(#marker-start--0)"]`)
|
|
753
|
+
: container.querySelector('.visx-annotation-connector')
|
|
754
|
+
const pathEnd = connector?.getAttribute('d')?.match(/(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)$/)
|
|
755
|
+
|
|
756
|
+
expect(pathEnd).toBeTruthy()
|
|
757
|
+
expect(Number(pathEnd?.[1])).toBe(Number(circle?.getAttribute('cx')))
|
|
758
|
+
expect(Number(pathEnd?.[2])).toBe(Number(circle?.getAttribute('cy')))
|
|
759
|
+
}
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
it('uses vizViewport ahead of currentViewport for in-chart mobile behavior', () => {
|
|
763
|
+
const { container } = renderAnnotationDraggable(calloutAnnotation, 320, 200, {
|
|
764
|
+
currentViewport: 'xxs',
|
|
765
|
+
vizViewport: 'lg'
|
|
766
|
+
})
|
|
767
|
+
|
|
768
|
+
expect(container.querySelector('.annotation__desktop-label')).toBeTruthy()
|
|
769
|
+
expect(container.querySelector('.annotation__mobile-label-circle')).toBeFalsy()
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
it('preserves the text-to-symbol transition between 577px and 576px', () => {
|
|
773
|
+
const desktopViewport = getViewport(577)
|
|
774
|
+
const mobileViewport = getViewport(576)
|
|
775
|
+
expect(desktopViewport).toBe('sm')
|
|
776
|
+
expect(mobileViewport).toBe('xs')
|
|
777
|
+
|
|
778
|
+
const desktop = renderAnnotationDraggable(calloutAnnotation, 577, 400, {
|
|
779
|
+
currentViewport: desktopViewport,
|
|
780
|
+
vizViewport: desktopViewport
|
|
781
|
+
})
|
|
782
|
+
expect(desktop.container.querySelector('.annotation__desktop-label')).toBeTruthy()
|
|
783
|
+
desktop.unmount()
|
|
784
|
+
|
|
785
|
+
const mobile = renderAnnotationDraggable(calloutAnnotation, 576, 400, {
|
|
786
|
+
currentViewport: mobileViewport,
|
|
787
|
+
vizViewport: mobileViewport
|
|
788
|
+
})
|
|
789
|
+
expect(mobile.container.querySelector('.annotation__mobile-label-circle')).toBeTruthy()
|
|
197
790
|
})
|
|
198
791
|
})
|