@cdc/chart 4.25.11 → 4.26.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/CLAUDE.local.md +79 -0
  2. package/dist/{cdcchart-dgT_1dIT.es.js → cdcchart-DQ00cQCm.es.js} +1 -20
  3. package/dist/cdcchart.js +51401 -50814
  4. package/examples/default.json +378 -0
  5. package/examples/feature/__data__/horizon-chart-data.json +373 -0
  6. package/examples/feature/annotations/index.json +3 -6
  7. package/examples/feature/horizon/horizon-chart.json +395 -0
  8. package/examples/feature/pie/planet-pie-example-config.json +48 -2
  9. package/examples/line-chart-states.json +1085 -0
  10. package/examples/private/123.json +694 -0
  11. package/examples/private/DEV-12100.json +1303 -0
  12. package/examples/private/anchor-issue.json +4094 -0
  13. package/examples/private/backwards-slider.json +10430 -0
  14. package/examples/private/cat-y.json +1235 -0
  15. package/examples/private/data-points.json +228 -0
  16. package/examples/private/georgia.csv +160 -0
  17. package/examples/private/height.json +3915 -0
  18. package/examples/private/links.json +569 -0
  19. package/examples/private/quadrant.txt +30 -0
  20. package/examples/private/test-forecast.json +5510 -0
  21. package/examples/private/timeline-data.json +1 -0
  22. package/examples/private/timeline.json +389 -0
  23. package/examples/private/warming-stripe-test.json +2578 -0
  24. package/examples/private/warming-stripes.json +4763 -0
  25. package/examples/radar-chart-simple.json +133 -0
  26. package/examples/radar-chart.json +148 -0
  27. package/examples/tech-adoption-with-links.json +560 -0
  28. package/index.html +1 -36
  29. package/package.json +59 -60
  30. package/src/CdcChartComponent.tsx +206 -89
  31. package/src/_stories/Chart.Anchors.stories.tsx +10 -0
  32. package/src/_stories/Chart.BoxPlot.stories.tsx +7 -0
  33. package/src/_stories/Chart.CI.stories.tsx +13 -0
  34. package/src/_stories/Chart.Combo.stories.tsx +17 -0
  35. package/src/_stories/Chart.CustomColors.stories.tsx +4 -0
  36. package/src/_stories/Chart.DynamicSeries.stories.tsx +19 -0
  37. package/src/_stories/Chart.Filters.stories.tsx +4 -0
  38. package/src/_stories/Chart.Forecast.stories.tsx +4 -0
  39. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +22 -0
  40. package/src/_stories/Chart.Legend.Gradient.stories.tsx +28 -0
  41. package/src/_stories/Chart.Patterns.stories.tsx +4 -0
  42. package/src/_stories/Chart.PreserveDecimals.stories.tsx +25 -0
  43. package/src/_stories/Chart.Regions.Categorical.stories.tsx +161 -0
  44. package/src/_stories/Chart.Regions.DateScale.stories.tsx +216 -0
  45. package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +312 -0
  46. package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
  47. package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
  48. package/src/_stories/Chart.stories.tsx +45 -0
  49. package/src/_stories/Chart.tooltip.stories.tsx +7 -0
  50. package/src/_stories/ChartAnnotation.stories.tsx +10 -0
  51. package/src/_stories/ChartAxisLabels.stories.tsx +4 -0
  52. package/src/_stories/ChartAxisTitles.stories.tsx +10 -0
  53. package/src/_stories/ChartBar.Editor.stories.tsx +11 -6
  54. package/src/_stories/ChartBrush.Editor.stories.tsx +295 -0
  55. package/src/_stories/ChartBrush.Matrix.Continuous.stories.tsx +41 -0
  56. package/src/_stories/ChartBrush.Matrix.Date.stories.tsx +114 -0
  57. package/src/_stories/ChartBrush.Matrix.DateTime.stories.tsx +78 -0
  58. package/src/_stories/ChartBrush.stories.tsx +57 -0
  59. package/src/_stories/ChartEditor.Editor.stories.tsx +3 -5
  60. package/src/_stories/ChartEditor.stories.tsx +7 -0
  61. package/src/_stories/ChartLine.QuadrantAngles.stories.tsx +89 -0
  62. package/src/_stories/ChartLine.Suppression.stories.tsx +7 -0
  63. package/src/_stories/ChartLine.Symbols.stories.tsx +4 -0
  64. package/src/_stories/ChartPrefixSuffix.stories.tsx +46 -1
  65. package/src/_stories/TechAdoptionWithLinks.stories.tsx +34 -0
  66. package/src/_stories/_mock/brush_continuous.json +86 -0
  67. package/src/_stories/_mock/brush_date_large.json +176 -0
  68. package/src/_stories/_mock/brush_enabled.json +326 -0
  69. package/src/_stories/_mock/brush_mock.json +2 -69
  70. package/src/_stories/_mock/horizontal-bars-dynamic-y-axis.json +413 -0
  71. package/src/_stories/_mock/line_chart_angle_near_zero_fall.json +195 -0
  72. package/src/_stories/_mock/line_chart_angle_near_zero_rise.json +195 -0
  73. package/src/_stories/_mock/line_chart_angle_q1_steep_upward.json +195 -0
  74. package/src/_stories/_mock/line_chart_angle_q2_gentle_downward.json +195 -0
  75. package/src/_stories/_mock/line_chart_angle_q3_steep_downward.json +195 -0
  76. package/src/_stories/_mock/line_chart_angle_q4_gentle_upward.json +195 -0
  77. package/src/_stories/_mock/line_chart_quadrant_angles.json +264 -0
  78. package/src/components/Annotations/components/AnnotationDraggable.styles.css +11 -17
  79. package/src/components/Annotations/components/AnnotationDraggable.tsx +240 -116
  80. package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -2
  81. package/src/components/Annotations/components/AnnotationDropdown.tsx +8 -12
  82. package/src/components/Annotations/components/AnnotationList.styles.css +4 -10
  83. package/src/components/Annotations/components/AnnotationList.tsx +5 -4
  84. package/src/components/Annotations/components/findNearestDatum.ts +75 -85
  85. package/src/components/Annotations/helpers/getVisibleAnnotations.ts +38 -0
  86. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +1 -2
  87. package/src/components/Axis/BottomAxis.tsx +270 -0
  88. package/src/components/Axis/Categorical.Axis.tsx +6 -7
  89. package/src/components/Axis/LeftAxis.tsx +404 -0
  90. package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
  91. package/src/components/Axis/PairedBarAxis.tsx +186 -0
  92. package/src/components/Axis/README.md +94 -0
  93. package/src/components/Axis/RightAxis.tsx +108 -0
  94. package/src/components/Axis/axis.constants.ts +21 -0
  95. package/src/components/Axis/index.ts +7 -0
  96. package/src/components/BarChart/components/BarChart.Horizontal.tsx +178 -24
  97. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +3 -1
  98. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -0
  99. package/src/components/BarChart/components/BarChart.Vertical.tsx +6 -8
  100. package/src/components/BarChart/components/BarChart.tsx +7 -1
  101. package/src/components/BarChart/components/context.tsx +1 -0
  102. package/src/components/BarChart/helpers/useBarChart.ts +14 -2
  103. package/src/components/Brush/BrushSelector.tsx +1390 -0
  104. package/src/components/Brush/MiniChartPreview.tsx +400 -0
  105. package/src/components/DeviationBar.jsx +9 -7
  106. package/src/components/EditorPanel/EditorPanel.tsx +2734 -2595
  107. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +60 -22
  108. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +56 -34
  109. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +137 -30
  110. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +2 -0
  111. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
  112. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +0 -1
  113. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +30 -25
  114. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +42 -28
  115. package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
  116. package/src/components/EditorPanel/useEditorPermissions.ts +81 -39
  117. package/src/components/HorizonChart/HorizonChart.tsx +131 -0
  118. package/src/components/HorizonChart/components/HorizonBand.tsx +160 -0
  119. package/src/components/HorizonChart/helpers/calculateHorizonBands.ts +27 -0
  120. package/src/components/HorizonChart/helpers/getHorizonLayerColors.ts +40 -0
  121. package/src/components/HorizonChart/index.tsx +3 -0
  122. package/src/components/Legend/Legend.Component.tsx +52 -4
  123. package/src/components/Legend/Legend.tsx +4 -3
  124. package/src/components/Legend/LegendValueRange.tsx +77 -0
  125. package/src/components/Legend/helpers/createFormatLabels.tsx +164 -2
  126. package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
  127. package/src/components/Legend/helpers/index.ts +10 -6
  128. package/src/components/LineChart/helpers/README.md +292 -0
  129. package/src/components/LineChart/helpers/labelPositioning.test.ts +245 -0
  130. package/src/components/LineChart/helpers/labelPositioning.ts +304 -0
  131. package/src/components/LineChart/index.tsx +44 -8
  132. package/src/components/LinearChart/README.md +109 -0
  133. package/src/components/LinearChart/VisualizationRenderer.tsx +267 -0
  134. package/src/components/LinearChart/linearChart.constants.ts +84 -0
  135. package/src/components/LinearChart/tests/LinearChart.test.tsx +201 -0
  136. package/src/components/LinearChart/tests/mockConfigContext.ts +129 -0
  137. package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
  138. package/src/components/LinearChart.tsx +338 -1082
  139. package/src/components/PairedBarChart.jsx +20 -3
  140. package/src/components/PieChart/PieChart.tsx +1 -1
  141. package/src/components/RadarChart/RadarAxis.tsx +78 -0
  142. package/src/components/RadarChart/RadarChart.tsx +298 -0
  143. package/src/components/RadarChart/RadarGrid.tsx +64 -0
  144. package/src/components/RadarChart/RadarPolygon.tsx +91 -0
  145. package/src/components/RadarChart/helpers.ts +83 -0
  146. package/src/components/RadarChart/index.tsx +3 -0
  147. package/src/components/Regions/components/Regions.tsx +365 -122
  148. package/src/components/ScatterPlot/ScatterPlot.jsx +2 -2
  149. package/src/components/SmallMultiples/SmallMultipleTile.tsx +5 -1
  150. package/src/components/WarmingStripes/WarmingStripes.tsx +230 -0
  151. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +35 -0
  152. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +104 -0
  153. package/src/components/WarmingStripes/index.tsx +3 -0
  154. package/src/data/initial-state.js +17 -2
  155. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +57 -0
  156. package/src/helpers/getExcludedData.ts +4 -0
  157. package/src/helpers/getMinMax.ts +12 -7
  158. package/src/helpers/handleChartAriaLabels.ts +19 -19
  159. package/src/helpers/handleLineType.ts +22 -18
  160. package/src/helpers/sizeHelpers.ts +0 -20
  161. package/src/helpers/smallMultiplesHelpers.ts +1 -1
  162. package/src/hooks/useChartHoverAnalytics.tsx +10 -9
  163. package/src/hooks/useProgrammaticTooltip.ts +23 -2
  164. package/src/hooks/useScales.ts +18 -1
  165. package/src/hooks/useTooltip.tsx +34 -10
  166. package/src/scss/DataTable.scss +0 -4
  167. package/src/scss/main.scss +22 -3
  168. package/src/selectors/README.md +68 -0
  169. package/src/store/chart.reducer.ts +2 -0
  170. package/src/test/CdcChart.test.jsx +1 -1
  171. package/src/types/ChartConfig.ts +21 -0
  172. package/src/types/ChartContext.ts +1 -0
  173. package/src/types/Horizon.ts +64 -0
  174. package/src/types/Label.ts +1 -0
  175. package/src/utils/analyticsTracking.ts +19 -0
  176. package/LICENSE +0 -201
  177. package/src/components/Annotations/components/helpers/index.tsx +0 -46
  178. package/src/components/Brush/BrushChart.tsx +0 -128
  179. package/src/components/Brush/BrushController.tsx +0 -71
  180. package/src/components/Brush/types.tsx +0 -8
  181. package/src/components/BrushChart.tsx +0 -223
@@ -1,46 +1,130 @@
1
- import { useContext, useEffect, useRef, useState } from 'react'
1
+ import { useContext, useState } from 'react'
2
2
  import ConfigContext from '../../../ConfigContext'
3
3
  import DOMPurify from 'dompurify'
4
+ import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
5
+ import { isMobileAnnotationViewport } from '@cdc/core/helpers/viewports'
4
6
 
5
7
  // helpers
6
8
  import { findNearestDatum } from './findNearestDatum'
7
9
 
8
- // prettier-ignore
9
- import {
10
- applyBandScaleOffset,
11
- handleConnectionHorizontalType,
12
- handleConnectionVerticalType,
13
- handleMobileXPosition,
14
- handleMobileYPosition,
15
- handleTextX,
16
- handleTextY
17
- } from './helpers'
18
-
19
10
  // visx
20
11
  import { HtmlLabel, CircleSubject, EditableAnnotation, Connector, Annotation as VisxAnnotation } from '@visx/annotation'
21
- import { Drag } from '@visx/drag'
22
12
  import { MarkerArrow } from '@visx/marker'
23
13
  import { LinePath } from '@visx/shape'
24
14
 
25
15
  // styles
26
16
  import './AnnotationDraggable.styles.css'
27
17
 
28
- const Annotations = ({ xScale, yScale, xScaleAnnotation, xMax, svgRef, onDragStateChange }) => {
18
+ const Annotations = ({
19
+ xScale,
20
+ yScale,
21
+ xScaleAnnotation,
22
+ yScaleAnnotation,
23
+ xMax,
24
+ yMax,
25
+ seriesScale,
26
+ svgRef,
27
+ onDragStateChange
28
+ }) => {
29
29
  // prettier-ignore
30
- const { config, dimensions, isEditor, updateConfig, colorScale } = useContext(ConfigContext)
30
+ const { config, dimensions, isEditor, updateConfig, colorScale, transformedData, parseDate, currentViewport, visibleAnnotations } = useContext(ConfigContext)
31
31
 
32
32
  // destructure config items here...
33
- const { annotations } = config
33
+ const { annotations, visualizationType } = config
34
34
  const [height] = dimensions
35
35
 
36
36
  const AnnotationComponent = isEditor ? EditableAnnotation : VisxAnnotation
37
+ const isMobile = isMobileAnnotationViewport(currentViewport)
38
+
39
+ /**
40
+ * Scale dx/dy offsets based on savedDimensions vs current dimensions.
41
+ * This ensures label positions scale proportionally when chart is resized.
42
+ * Falls back to unscaled values if savedDimensions is missing (backward compatible).
43
+ */
44
+ const getScaledOffsets = (annotation: { dx: number; dy: number; savedDimensions?: [number, number] }) => {
45
+ const [savedWidth, savedHeight] = annotation.savedDimensions || []
46
+
47
+ const scaledDx = savedWidth && savedWidth > 0 ? (annotation.dx / savedWidth) * xMax : annotation.dx
48
+ const scaledDy = savedHeight && savedHeight > 0 ? (annotation.dy / savedHeight) * yMax : annotation.dy
49
+
50
+ return { scaledDx, scaledDy }
51
+ }
52
+
53
+ // Track live drag position for real-time anchor calculations
54
+ const [liveDrag, setLiveDrag] = useState<{ index: number; dx: number } | null>(null)
55
+
56
+ // Helper to determine label anchoring when near chart edges
57
+ const getAnnotationAnchors = (annotationX: number, dx: number, labelWidth: number) => {
58
+ const endpointX = annotationX + dx
59
+
60
+ const leftOverflow = dx < 0 && endpointX - labelWidth < 0
61
+ const rightOverflow = dx > 0 && endpointX + labelWidth > xMax
62
+
63
+ if (leftOverflow || rightOverflow) {
64
+ // Center label above the endpoint
65
+ return { horizontalAnchor: 'middle' as const, verticalAnchor: 'end' as const }
66
+ }
67
+
68
+ // Let visx auto-decide
69
+ return { horizontalAnchor: null, verticalAnchor: null }
70
+ }
37
71
 
38
72
  return (
39
- annotations &&
40
- annotations.map((annotation, index) => {
73
+ visibleAnnotations &&
74
+ visibleAnnotations.map((annotation, annotationIndex) => {
75
+ const originalIndex = annotations.indexOf(annotation)
41
76
  const text = annotation.text || ''
42
77
 
43
- const annotationX = xScaleAnnotation(annotation.x)
78
+ // Calculate scaled dx/dy offsets based on savedDimensions
79
+ const { scaledDx, scaledDy } = getScaledOffsets(annotation)
80
+
81
+ // Default to absolute positioning
82
+ let annotationX = xScaleAnnotation(annotation.x)
83
+ let annotationY = yScaleAnnotation(annotation.y)
84
+
85
+ // Override with data-anchored positioning if applicable
86
+ if (annotation.anchorMode === 'data' && annotation.dataX !== undefined) {
87
+ const dataSource = transformedData || config.data
88
+ const dataPoint = dataSource.find(d => d[config.xAxis.dataKey] === annotation.dataX)
89
+
90
+ if (dataPoint) {
91
+ const dataYValue = dataPoint[annotation.seriesKey]
92
+
93
+ // For date/date-time axes, convert raw value to timestamp for scale
94
+ let xScaleInput = annotation.dataX
95
+ if (config.xAxis.type === 'date' || config.xAxis.type === 'date-time') {
96
+ xScaleInput = parseDate(xScaleInput, false)?.getTime()
97
+ }
98
+
99
+ // Check if this annotation's series is a bar in a grouped bar situation
100
+ const annotationSeries = config.series?.find(s => s.dataKey === annotation.seriesKey)
101
+ const barSeriesCount = config.series?.filter(s => s.type === 'Bar').length || 0
102
+ const isGroupedBarAnnotation =
103
+ annotationSeries?.type === 'Bar' && barSeriesCount > 1 && config.visualizationSubType !== 'stacked'
104
+
105
+ if (isGroupedBarAnnotation && seriesScale) {
106
+ // Position at group start + series offset + half series bar width
107
+ const seriesOffset = seriesScale(annotation.seriesKey) || 0
108
+ const seriesBandwidth = seriesScale.bandwidth?.() || 0
109
+ annotationX = xScale(xScaleInput) + seriesOffset + seriesBandwidth / 2
110
+ } else {
111
+ // For lines, areas, single bars, etc - center on the data point
112
+ annotationX = xScale(xScaleInput) + (xScale.bandwidth?.() / 2 || 0)
113
+ }
114
+
115
+ // Adjust X for arrow markers based on label direction
116
+ if (annotation.marker === 'arrow' && Math.abs(annotation.dx) >= 100) {
117
+ const direction = annotation.dx > 0 ? 1 : -1
118
+ const relevantBandwidth =
119
+ isGroupedBarAnnotation && seriesScale ? seriesScale.bandwidth?.() : xScale.bandwidth?.()
120
+ const nudgeAmount = relevantBandwidth ? relevantBandwidth / 6 : 2
121
+ annotationX += direction * nudgeAmount
122
+ }
123
+
124
+ // Y position based on marker type
125
+ annotationY = yScale(dataYValue) - (annotation.marker === 'circle' ? 0 : 5)
126
+ }
127
+ }
44
128
 
45
129
  // sanitize the text for setting dangerouslySetInnerHTML
46
130
  const sanitizedData = () => ({
@@ -49,47 +133,72 @@ const Annotations = ({ xScale, yScale, xScaleAnnotation, xMax, svgRef, onDragSta
49
133
 
50
134
  return (
51
135
  <AnnotationComponent
52
- key={`annotation-${index}`}
53
- width={200}
54
- height={height}
55
- dx={annotation.dx} // label position
56
- dy={annotation.dy} // label postion
136
+ key={`annotation-${originalIndex}-${annotation.x}-${annotation.y}-${annotation.dx}-${annotation.dy}`}
137
+ width={xMax}
138
+ height={yMax}
139
+ dx={scaledDx} // label position (scaled to current chart dimensions)
140
+ dy={scaledDy} // label position (scaled to current chart dimensions)
57
141
  x={annotationX}
58
- y={annotation.y}
142
+ y={annotationY}
59
143
  canEditLabel={annotation.edit.label || false}
60
144
  canEditSubject={(annotation.edit.subject && annotation.connectionType !== 'none') || false}
61
- onDragStart={() => onDragStateChange(true)}
145
+ labelDragHandleProps={{ r: 15, stroke: 'red' }}
146
+ subjectDragHandleProps={{ r: 15, stroke: 'red' }}
147
+ onDragStart={() => {
148
+ onDragStateChange(true)
149
+ setLiveDrag({ index: annotationIndex, dx: scaledDx })
150
+ }}
151
+ onDragMove={props => {
152
+ setLiveDrag({ index: annotationIndex, dx: props.dx })
153
+ }}
62
154
  onDragEnd={props => {
63
155
  onDragStateChange(false)
156
+ setLiveDrag(null)
64
157
 
65
158
  let updatedAnnotations = [...annotations]
66
159
 
67
- if (annotation.x === xScaleAnnotation.invert(props.x) && annotation.y === props.y) {
68
- updatedAnnotations[index] = { ...updatedAnnotations[index], dx: props.dx, dy: props.dy }
160
+ // Current chart dimensions to save with the annotation
161
+ const currentDimensions: [number, number] = [xMax, yMax]
162
+
163
+ const isLabelOnlyDrag =
164
+ annotation.anchorMode === 'data'
165
+ ? annotationX === props.x && annotationY === props.y
166
+ : annotation.x === xScaleAnnotation.invert(props.x) && annotation.y === yScaleAnnotation.invert(props.y)
167
+
168
+ if (isLabelOnlyDrag) {
169
+ updatedAnnotations[originalIndex] = {
170
+ ...updatedAnnotations[originalIndex],
171
+ dx: props.dx,
172
+ dy: props.dy,
173
+ savedDimensions: currentDimensions
174
+ }
69
175
  } else {
70
- if (annotation.snapToNearestPoint) {
71
- let nearestDatum = findNearestDatum(
72
- {
73
- data: config.data,
74
- xScale,
75
- yScale,
76
- config,
77
- xMax: xMax - config.yAxis.size / 2,
78
- annotationSeriesKey: annotation.seriesKey
79
- },
80
- props.x
81
- )
82
-
83
- updatedAnnotations[index] = {
84
- ...updatedAnnotations[index],
85
- x: xScaleAnnotation.invert(xScale(nearestDatum.x)),
86
- y: yScale(nearestDatum.y)
176
+ if (annotation.anchorMode === 'data') {
177
+ let nearestDatum = findNearestDatum({
178
+ data: transformedData || config.data,
179
+ xScale,
180
+ xAxisType: config.xAxis.type,
181
+ xAxisDataKey: config.xAxis.dataKey,
182
+ seriesKey: annotation.seriesKey,
183
+ xPixel: props.x,
184
+ parseDate
185
+ })
186
+
187
+ if (nearestDatum) {
188
+ updatedAnnotations[originalIndex] = {
189
+ ...updatedAnnotations[originalIndex],
190
+ dataX: nearestDatum.x,
191
+ x: xScaleAnnotation.invert(props.x),
192
+ y: yScaleAnnotation.invert(props.y),
193
+ savedDimensions: currentDimensions
194
+ }
87
195
  }
88
196
  } else {
89
- updatedAnnotations[index] = {
90
- ...updatedAnnotations[index],
197
+ updatedAnnotations[originalIndex] = {
198
+ ...updatedAnnotations[originalIndex],
91
199
  x: xScaleAnnotation.invert(props.x),
92
- y: props.y
200
+ y: yScaleAnnotation.invert(props.y),
201
+ savedDimensions: currentDimensions
93
202
  }
94
203
  }
95
204
  }
@@ -100,94 +209,109 @@ const Annotations = ({ xScale, yScale, xScaleAnnotation, xMax, svgRef, onDragSta
100
209
  })
101
210
  }}
102
211
  >
103
- <HtmlLabel
104
- className='annotation__desktop-label'
105
- showAnchorLine={false}
106
- horizontalAnchor={handleConnectionHorizontalType(annotation, xScale, config)}
107
- verticalAnchor={handleConnectionVerticalType(annotation, xScale, config)}
108
- >
109
- <div
110
- style={{
111
- borderRadius: 5, // Optional: set border radius
112
- backgroundColor: `rgba(255, 255, 255, ${annotation?.opacity ? Number(annotation?.opacity) / 100 : 1})`,
113
- padding: '10px',
114
- width: 'auto',
115
- display: config.general.showAnnotationDropdown ? 'inline-flex' : 'flex',
116
- justifyContent: 'start',
117
- flexDirection: 'row'
118
- }}
119
- // role='presentation'
120
- tabIndex={0}
121
- aria-label={`Annotation text that reads: ${annotation.text}`}
122
- >
123
- {config?.general?.showAnnotationDropdown && (
124
- <>
125
- <p className='annotation__has-dropdown-number' style={{ margin: '2px 6px' }}>
126
- {index + 1}
127
- </p>
128
- </>
129
- )}
130
- <div dangerouslySetInnerHTML={sanitizedData()} />
131
- </div>
132
- </HtmlLabel>
212
+ {!isMobile &&
213
+ (() => {
214
+ const labelWidth = config.general.showAnnotationDropdown ? 186 : 150
215
+ // Use live dx during drag (already in current space), otherwise use scaled dx
216
+ const currentDx = liveDrag?.index === annotationIndex ? liveDrag.dx : scaledDx
217
+ const { horizontalAnchor, verticalAnchor } = getAnnotationAnchors(annotationX, currentDx, labelWidth)
218
+ return (
219
+ <HtmlLabel
220
+ className='annotation__desktop-label'
221
+ containerStyle={{ width: `${labelWidth}px` }}
222
+ horizontalAnchor={horizontalAnchor}
223
+ verticalAnchor={verticalAnchor}
224
+ showAnchorLine={false}
225
+ >
226
+ <div
227
+ style={{
228
+ borderRadius: 5, // Optional: set border radius
229
+ backgroundColor: `rgba(255, 255, 255, ${
230
+ annotation?.opacity ? Number(annotation?.opacity) / 100 : 1
231
+ })`,
232
+ padding: '10px',
233
+ width: 'auto',
234
+ display: config.general.showAnnotationDropdown ? 'inline-flex' : 'flex',
235
+ justifyContent: 'start',
236
+ flexDirection: 'row',
237
+ alignItems: 'center'
238
+ }}
239
+ // role='presentation'
240
+ tabIndex={0}
241
+ aria-label={`Annotation text that reads: ${annotation.text}`}
242
+ >
243
+ {config?.general?.showAnnotationDropdown && (
244
+ <>
245
+ <p
246
+ className='annotation__has-dropdown-number'
247
+ style={{ margin: '2px 6px', position: 'relative', left: '-4px' }}
248
+ >
249
+ {originalIndex + 1}
250
+ </p>
251
+ </>
252
+ )}
253
+ <div dangerouslySetInnerHTML={sanitizedData()} />
254
+ </div>
255
+ </HtmlLabel>
256
+ )
257
+ })()}
133
258
  {annotation.connectionType === 'line' && (
134
- <Connector type='line' pathProps={{ markerStart: `url(#marker-start--${index})` }} />
259
+ <Connector type='line' pathProps={{ markerStart: `url(#marker-start--${originalIndex})` }} />
135
260
  )}
136
261
  {annotation.connectionType === 'elbow' && (
137
- <Connector type='elbow' pathProps={{ markerStart: `url(#marker-start--${index})` }} />
262
+ <Connector type='elbow' pathProps={{ markerStart: `url(#marker-start--${originalIndex})` }} />
138
263
  )}
139
264
  {annotation.connectionType === 'curve' && (
140
265
  <LinePath
141
- d={`M ${annotationX},${annotation.y}
142
- Q ${annotationX + annotation.dx / 2}, ${
143
- annotation.y + annotation.dy / 2 + Number(annotation?.bezier) || 0
144
- } ${annotationX + annotation.dx},${annotation.y + annotation.dy}`}
145
- stroke='black'
146
- strokeWidth='2'
266
+ d={`M ${annotationX},${annotationY}
267
+ Q ${annotationX + scaledDx / 2}, ${
268
+ annotationY + scaledDy / 2 + Number(annotation?.bezier) || 0
269
+ } ${annotationX + scaledDx},${annotationY + scaledDy}`}
270
+ stroke={APP_FONT_COLOR}
147
271
  fill='none'
148
- marker-start={`url(#marker-start--${index})`}
272
+ marker-start={`url(#marker-start--${originalIndex})`}
149
273
  />
150
274
  )}
151
275
  {annotation.marker === 'circle' && (
152
- <CircleSubject
153
- id={`marker-start--${index}`}
154
- className='circle-subject'
155
- stroke={colorScale(annotation.seriesKey)}
156
- radius={8}
157
- />
276
+ <CircleSubject className='circle-subject' stroke={APP_FONT_COLOR} radius={8} />
158
277
  )}
159
278
  {annotation.marker === 'arrow' && (
160
279
  <MarkerArrow
161
- fill='black'
162
- id={`marker-start--${index}`}
280
+ fill={APP_FONT_COLOR}
281
+ id={`marker-start--${originalIndex}`}
163
282
  x={annotationX}
164
- y={annotation.y}
165
- stroke='#333'
166
- markerWidth={10}
283
+ y={annotationY}
284
+ stroke={APP_FONT_COLOR}
285
+ markerWidth={12}
167
286
  size={10}
168
287
  strokeWidth={1}
169
288
  orient='auto-start-reverse'
170
289
  markerUnits='userSpaceOnUse'
171
290
  />
172
291
  )}
173
- <circle
174
- fill='white'
175
- cx={annotationX + annotation.dx}
176
- cy={annotation.y + annotation.dy}
177
- r={16}
178
- className='annotation__mobile-label annotation__mobile-label-circle'
179
- stroke={colorScale(annotation.seriesKey)}
180
- />
181
- <text
182
- height={16}
183
- x={annotationX + annotation.dx}
184
- y={annotation.y + annotation.dy}
185
- className='annotation__mobile-label'
186
- alignmentBaseline='middle'
187
- textAnchor='middle'
188
- >
189
- {index + 1}
190
- </text>
292
+ {isMobile && (
293
+ <>
294
+ <circle
295
+ fill='white'
296
+ cx={annotationX + scaledDx}
297
+ cy={annotationY + scaledDy}
298
+ r={12}
299
+ className='annotation__mobile-label annotation__mobile-label-circle'
300
+ stroke={APP_FONT_COLOR}
301
+ />
302
+ <text
303
+ height={16}
304
+ x={annotationX + scaledDx}
305
+ y={annotationY + scaledDy + 1}
306
+ fontSize={14}
307
+ className='annotation__mobile-label'
308
+ alignmentBaseline='middle'
309
+ textAnchor='middle'
310
+ >
311
+ {originalIndex + 1}
312
+ </text>
313
+ </>
314
+ )}
191
315
  </AnnotationComponent>
192
316
  )
193
317
  })
@@ -1,6 +1,5 @@
1
- .cdc-open-viz-module {
1
+ .cdc-open-viz-module.type-chart {
2
2
  .annotation__dropdown-list {
3
- border: 1px solid red;
4
3
  list-style: none;
5
4
  }
6
5
 
@@ -3,17 +3,12 @@ import ConfigContext from '../../../ConfigContext'
3
3
  import './AnnotationDropdown.styles.css'
4
4
  import Icon from '@cdc/core/components/ui/Icon'
5
5
  import Annotation from '..'
6
- import { APP_FONT_SIZE } from '@cdc/core/helpers/constants'
6
+ import { isMobileAnnotationViewport } from '@cdc/core/helpers/viewports'
7
7
 
8
8
  const AnnotationDropdown = () => {
9
9
  const { currentViewport: viewport, config } = useContext(ConfigContext)
10
10
  const [expanded, setExpanded] = useState(false)
11
-
12
- const titleFontSize = ['sm', 'xs', 'xxs'].includes(viewport) ? '13px' : `${APP_FONT_SIZE}px`
13
-
14
- const {
15
- config: { annotations }
16
- } = useContext(ConfigContext)
11
+ const isMobile = isMobileAnnotationViewport(viewport)
17
12
 
18
13
  const limitHeight = {
19
14
  maxHeight: config.table.limitHeight && `${config.table.height}px`,
@@ -30,11 +25,13 @@ const AnnotationDropdown = () => {
30
25
  }
31
26
 
32
27
  const handleSectionClasses = () => {
33
- const classes = [`data-table-container`, viewport, `d-block`, `d-lg-none`, `w-100`]
28
+ const classes = [`data-table-container`, viewport, `w-100`, 'mt-4']
34
29
 
35
- if (config.general.showAnnotationDropdown) {
36
- classes.push('d-lg-block')
37
- classes.splice(classes.indexOf('d-lg-none'), 1)
30
+ // Show dropdown in mobile mode or if explicitly enabled
31
+ if (isMobile || config.general.showAnnotationDropdown) {
32
+ classes.push('d-block')
33
+ } else {
34
+ classes.push('d-none')
38
35
  }
39
36
  return classes.join(' ')
40
37
  }
@@ -43,7 +40,6 @@ const AnnotationDropdown = () => {
43
40
  <>
44
41
  <section className={handleSectionClasses()}>
45
42
  <div
46
- style={{ fontSize: titleFontSize }}
47
43
  role='button'
48
44
  className={handleAccordionClassName()}
49
45
  onClick={() => {
@@ -1,16 +1,15 @@
1
- .cdc-open-viz-module {
1
+ .cdc-open-viz-module.type-chart {
2
2
  .annotation__title-circle {
3
3
  display: flex;
4
4
  justify-content: center;
5
5
  align-items: center;
6
6
  border-radius: 50%;
7
7
  padding: 8px;
8
- width: 16px;
9
- height: 16px;
8
+ width: 24px;
9
+ height: 24px;
10
10
  margin-right: 5px;
11
11
  line-height: 1.5rem;
12
-
13
- border: 2px solid #666;
12
+ border: 1px solid #666;
14
13
  text-align: center;
15
14
  font-size: 14px;
16
15
  }
@@ -23,11 +22,6 @@
23
22
 
24
23
  .annotation__title-wrapper .annotation__title-text {
25
24
  margin-left: 5px;
26
- font-size: 16px;
27
- }
28
-
29
- .annotation__subtext {
30
- font-size: 12px;
31
25
  }
32
26
 
33
27
  .annotation-list {
@@ -8,7 +8,7 @@ type AnnotationListProps = {
8
8
  }
9
9
 
10
10
  const AnnotationList: React.FC<AnnotationListProps> = ({ useBootstrapVisibilityClasses = true }) => {
11
- const { config } = useContext(ConfigContext)
11
+ const { config, visibleAnnotations } = useContext(ConfigContext)
12
12
  const annotations = config.annotations || []
13
13
 
14
14
  const ulClasses = () => {
@@ -19,7 +19,8 @@ const AnnotationList: React.FC<AnnotationListProps> = ({ useBootstrapVisibilityC
19
19
  return classes.join(' ')
20
20
  }
21
21
 
22
- const annotationListItems = annotations.map((annotation, annotationIndex) => {
22
+ const annotationListItems = visibleAnnotations.map(annotation => {
23
+ const originalIndex = annotations.indexOf(annotation)
23
24
  const text = annotation.text || ''
24
25
 
25
26
  // sanitize the text for setting dangerouslySetInnerHTML
@@ -27,9 +28,9 @@ const AnnotationList: React.FC<AnnotationListProps> = ({ useBootstrapVisibilityC
27
28
  __html: DOMPurify.sanitize(text)
28
29
  })
29
30
  return (
30
- <li key={`annotation-li-item__annotationIndex`}>
31
+ <li key={`annotation-li-item__${originalIndex}`}>
31
32
  <div className='annotation__title-wrapper'>
32
- <div className='annotation__title-circle'>{annotationIndex + 1}</div>
33
+ <div className='annotation__title-circle'>{originalIndex + 1}</div>
33
34
  <p className='annotation__subtext' dangerouslySetInnerHTML={sanitizedData()} />
34
35
  </div>
35
36
  </li>