@cdc/chart 4.26.4 → 4.26.6

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 (141) hide show
  1. package/CONFIG.md +353 -29
  2. package/LICENSE +201 -0
  3. package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
  4. package/dist/cdcchart.js +37885 -35117
  5. package/examples/feature/heatmap/average-age-categorical.json +313 -0
  6. package/examples/feature/heatmap/calendar-heatmap.json +134 -0
  7. package/examples/feature/heatmap/cell-values-bottom-axis.json +181 -0
  8. package/examples/feature/heatmap/sparse-aggregation.json +158 -0
  9. package/examples/private/bad-y-axis.json +543 -0
  10. package/examples/private/bar-chart-negative-value.json +455 -0
  11. package/examples/private/bump-chart.json +1116 -0
  12. package/examples/private/test-123.json +624 -0
  13. package/package.json +4 -3
  14. package/src/CdcChart.tsx +5 -4
  15. package/src/CdcChartComponent.tsx +166 -62
  16. package/src/_stories/Chart.AltText.stories.tsx +136 -0
  17. package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
  18. package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
  19. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +183 -0
  20. package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +29 -0
  21. package/src/_stories/Chart.YAxisAutoMaxStrategy.stories.tsx +52 -0
  22. package/src/_stories/Chart.YAxisFilterDomain.stories.tsx +90 -0
  23. package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -0
  24. package/src/_stories/ChartAxisTitles.smoke.stories.tsx +13 -0
  25. package/src/_stories/ChartBar.Editor.stories.tsx +63 -0
  26. package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
  27. package/src/_stories/ChartBrush.smoke.stories.tsx +13 -4
  28. package/src/_stories/_mock/alt_text_metadata.json +38 -0
  29. package/src/_stories/_mock/annotation_event-line_mock.json +1064 -0
  30. package/src/_stories/_mock/heatmap-average-age-categorical.json +315 -0
  31. package/src/_stories/_mock/heatmap.json +156 -0
  32. package/src/_stories/_mock/reported_pertussis_cases.json +1643 -0
  33. package/src/_stories/_mock/sankey_mock.json +78 -0
  34. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +1 -1
  35. package/src/components/Annotations/components/AnnotationDraggable.styles.css +22 -1
  36. package/src/components/Annotations/components/AnnotationDraggable.tsx +220 -120
  37. package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
  38. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +198 -0
  39. package/src/components/Axis/LeftAxis.tsx +17 -11
  40. package/src/components/Axis/LeftAxisGridlines.tsx +22 -12
  41. package/src/components/Axis/RightAxis.tsx +3 -0
  42. package/src/components/Axis/tests/AxisTickValues.test.tsx +86 -0
  43. package/src/components/Brush/BrushSelector.tsx +88 -187
  44. package/src/components/Brush/brushInteraction.ts +22 -0
  45. package/src/components/Brush/tests/brushInteraction.test.ts +38 -0
  46. package/src/components/EditorPanel/EditorPanel.tsx +677 -185
  47. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +114 -52
  48. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
  49. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +1 -0
  50. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +5 -0
  51. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -2
  52. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.test.tsx +98 -0
  53. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +10 -0
  54. package/src/components/EditorPanel/components/Panels/Panel.Visual.test.tsx +484 -0
  55. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +4 -1
  56. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +262 -0
  57. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +32 -0
  58. package/src/components/EditorPanel/components/panels.scss +10 -0
  59. package/src/components/EditorPanel/editor-panel.scss +47 -0
  60. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +61 -0
  61. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +74 -0
  62. package/src/components/EditorPanel/useEditorPermissions.test.tsx +315 -0
  63. package/src/components/EditorPanel/useEditorPermissions.ts +77 -21
  64. package/src/components/HeatMap/components/HeatMap.tsx +767 -0
  65. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +128 -0
  66. package/src/components/HeatMap/heatmap.constants.ts +13 -0
  67. package/src/components/HeatMap/heatmap.css +85 -0
  68. package/src/components/HeatMap/helpers/index.ts +261 -0
  69. package/src/components/HeatMap/index.tsx +5 -0
  70. package/src/components/HeatMap/tests/HeatMap.test.tsx +1072 -0
  71. package/src/components/HeatMap/tests/helpers.test.ts +167 -0
  72. package/src/components/Legend/Legend.Component.tsx +2 -2
  73. package/src/components/Legend/helpers/createFormatLabels.test.ts +88 -0
  74. package/src/components/Legend/helpers/createFormatLabels.tsx +6 -1
  75. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +19 -22
  76. package/src/components/LineChart/helpers.test.ts +574 -0
  77. package/src/components/LineChart/helpers.ts +39 -16
  78. package/src/components/LineChart/index.tsx +28 -5
  79. package/src/components/LinearChart/tests/LinearChart.test.tsx +137 -33
  80. package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
  81. package/src/components/LinearChart.tsx +103 -47
  82. package/src/components/PieChart/PieChart.tsx +4 -1
  83. package/src/components/RadarChart/RadarChart.tsx +3 -1
  84. package/src/components/Regions/components/Regions.tsx +195 -41
  85. package/src/components/Sankey/components/Sankey.tsx +7 -3
  86. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
  87. package/src/components/ScatterPlot/ScatterPlot.jsx +3 -3
  88. package/src/components/SmallMultiples/SmallMultipleTile.tsx +33 -3
  89. package/src/components/SmallMultiples/SmallMultiples.css +12 -0
  90. package/src/components/SmallMultiples/SmallMultiples.tsx +50 -15
  91. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
  92. package/src/components/SmallMultiples/tests/SmallMultiples.test.tsx +170 -0
  93. package/src/components/Sparkline/components/SparkLine.tsx +4 -9
  94. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +1 -1
  95. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +2 -2
  96. package/src/data/initial-state.js +12 -0
  97. package/src/helpers/axisLabelFontSize.ts +8 -0
  98. package/src/helpers/bumpChartTooltip.ts +54 -0
  99. package/src/helpers/calculateLeftYAxisWidth.ts +13 -3
  100. package/src/helpers/colorAssignmentHelpers.ts +102 -0
  101. package/src/helpers/dataHelpers.ts +11 -1
  102. package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
  103. package/src/helpers/getAxisMaxOverride.ts +16 -0
  104. package/src/helpers/getCleanTopTickMax.ts +64 -0
  105. package/src/helpers/getColorScale.ts +18 -9
  106. package/src/helpers/getHasBoundarySuppression.ts +26 -0
  107. package/src/helpers/getMinMax.ts +26 -25
  108. package/src/helpers/getYAxisDomainData.ts +38 -0
  109. package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
  110. package/src/helpers/handleChartAriaLabels.ts +23 -10
  111. package/src/helpers/hasSpacedInlineLabel.ts +10 -0
  112. package/src/helpers/seriesColumnSettings.ts +5 -4
  113. package/src/helpers/seriesHighlight.ts +26 -0
  114. package/src/helpers/smallMultiplesHelpers.ts +5 -0
  115. package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
  116. package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
  117. package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
  118. package/src/helpers/tests/dataHelpers.test.ts +135 -0
  119. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
  120. package/src/helpers/tests/getAxisMaxOverride.test.ts +36 -0
  121. package/src/helpers/tests/getCleanTopTickMax.test.ts +165 -0
  122. package/src/helpers/tests/getColorScale.test.ts +197 -0
  123. package/src/helpers/tests/getMinMax.test.ts +211 -0
  124. package/src/helpers/tests/getYAxisDomainData.test.ts +119 -0
  125. package/src/helpers/tests/getYAxisFinalizationEligibility.test.ts +185 -0
  126. package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
  127. package/src/helpers/tests/hasSpacedInlineLabel.test.ts +75 -0
  128. package/src/helpers/tests/seriesColumnSettings.test.ts +16 -2
  129. package/src/helpers/tests/seriesHighlight.test.ts +61 -0
  130. package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
  131. package/src/hooks/tests/useRightAxis.test.tsx +127 -0
  132. package/src/hooks/tests/useScales.test.tsx +383 -0
  133. package/src/hooks/useRightAxis.ts +30 -13
  134. package/src/hooks/useScales.ts +48 -21
  135. package/src/hooks/useTooltip.tsx +3 -1
  136. package/src/index.jsx +5 -2
  137. package/src/scss/main.scss +32 -49
  138. package/src/test/CdcChart.dataTable.test.tsx +122 -0
  139. package/src/types/ChartConfig.ts +24 -1
  140. package/src/types/ChartContext.ts +5 -0
  141. package/src/helpers/getYAxisAutoPadding.ts +0 -53
@@ -0,0 +1,78 @@
1
+ {
2
+ "type": "chart",
3
+ "visualizationType": "Sankey",
4
+ "title": "Patient Care Pathway",
5
+ "showTitle": true,
6
+ "enableTooltips": true,
7
+ "heights": {
8
+ "vertical": 500,
9
+ "horizontal": 750
10
+ },
11
+ "data": [
12
+ {
13
+ "links": [
14
+ { "source": "Screened", "target": "Eligible", "value": 850 },
15
+ { "source": "Screened", "target": "Ineligible", "value": 150 },
16
+ { "source": "Eligible", "target": "Enrolled", "value": 700 },
17
+ { "source": "Eligible", "target": "Declined", "value": 150 },
18
+ { "source": "Enrolled", "target": "Completed Treatment", "value": 580 },
19
+ { "source": "Enrolled", "target": "Dropped Out", "value": 120 },
20
+ { "source": "Completed Treatment", "target": "Recovered", "value": 460 },
21
+ { "source": "Completed Treatment", "target": "Ongoing Care", "value": 120 }
22
+ ],
23
+ "tooltips": true,
24
+ "storyNodeText": []
25
+ }
26
+ ],
27
+ "sankey": {
28
+ "title": {
29
+ "defaultColor": "black"
30
+ },
31
+ "iterations": 1,
32
+ "rxValue": 0.9,
33
+ "overallSize": {
34
+ "width": 900,
35
+ "height": 700
36
+ },
37
+ "margin": {
38
+ "margin_y": 25,
39
+ "margin_x": 0
40
+ },
41
+ "nodeSize": {
42
+ "nodeWidth": 26,
43
+ "nodeHeight": 40
44
+ },
45
+ "nodePadding": 55,
46
+ "nodeFontColor": "black",
47
+ "nodeColor": {
48
+ "default": "#005eaa",
49
+ "inactive": "#808080"
50
+ },
51
+ "linkColor": {
52
+ "default": "#7bafd4",
53
+ "inactive": "#D3D3D3"
54
+ },
55
+ "opacity": {
56
+ "nodeOpacityDefault": 1.0,
57
+ "nodeOpacityInactive": 0.1,
58
+ "LinkOpacityDefault": 0.6,
59
+ "LinkOpacityInactive": 0.1
60
+ },
61
+ "storyNodeFontColor": "#006778",
62
+ "storyNodeText": [],
63
+ "nodeValueStyle": {
64
+ "textBefore": "(",
65
+ "textAfter": ")"
66
+ },
67
+ "data": []
68
+ },
69
+ "table": {
70
+ "show": false,
71
+ "expanded": false,
72
+ "label": "Data Table",
73
+ "caption": ""
74
+ },
75
+ "legend": {
76
+ "hide": true
77
+ }
78
+ }
@@ -64,7 +64,7 @@
64
64
  "hideTicks": true,
65
65
  "size": "40",
66
66
  "gridLines": true,
67
- "enablePadding": true,
67
+ "enablePadding": false,
68
68
  "min": "",
69
69
  "max": "",
70
70
  "labelColor": "#1c1d1f",
@@ -1,5 +1,5 @@
1
1
  .cove-visualization.type-chart .annotation__desktop-label {
2
- font-size: 16px;
2
+ /* font-size is applied inline so it stays in sync with the axis tick label size and scales by viewport. */
3
3
  line-height: 1.1rem;
4
4
  }
5
5
 
@@ -22,4 +22,25 @@
22
22
  text-align: center;
23
23
  width: 24px;
24
24
  }
25
+
26
+ .annotation__event-line {
27
+ pointer-events: none;
28
+ }
29
+
30
+ .annotation__event-line-label {
31
+ max-width: 186px;
32
+ width: fit-content;
33
+ }
34
+
35
+ /* Label right of the line. */
36
+ .cove-annotation-event-line__label--right {
37
+ padding-left: 0.5rem;
38
+ text-align: left;
39
+ }
40
+
41
+ /* Label left of the line. */
42
+ .cove-annotation-event-line__label--left {
43
+ padding-right: 0.5rem;
44
+ text-align: right;
45
+ }
25
46
  }
@@ -2,7 +2,7 @@ import { useContext, useState } from 'react'
2
2
  import ConfigContext from '../../../ConfigContext'
3
3
  import DOMPurify from 'dompurify'
4
4
  import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
5
- import { isMobileAnnotationViewport } from '@cdc/core/helpers/viewports'
5
+ import { isMobileAnnotationViewport, isMobileFontViewport } from '@cdc/core/helpers/viewports'
6
6
 
7
7
  // helpers
8
8
  import { findNearestDatum } from './findNearestDatum'
@@ -15,6 +15,13 @@ import { LinePath } from '@visx/shape'
15
15
  // styles
16
16
  import './AnnotationDraggable.styles.css'
17
17
 
18
+ export const EVENT_LINE_LABEL_OFFSET = 2
19
+ export const snapEventLineDx = (dx: number) => (dx >= 0 ? EVENT_LINE_LABEL_OFFSET : -EVENT_LINE_LABEL_OFFSET)
20
+
21
+ // Keep annotation label text in sync with axis tick labels (see LinearChart.tsx).
22
+ const TICK_LABEL_FONT_SIZE = 16
23
+ const TICK_LABEL_FONT_SIZE_SMALL = 13
24
+
18
25
  const Annotations = ({
19
26
  xScale,
20
27
  yScale,
@@ -34,7 +41,10 @@ const Annotations = ({
34
41
  const [height] = dimensions
35
42
 
36
43
  const AnnotationComponent = isEditor ? EditableAnnotation : VisxAnnotation
37
- const isMobile = isMobileAnnotationViewport(currentViewport)
44
+ const isMobile = isMobileAnnotationViewport(currentViewport) && config?.general?.mobileAnnotationDisplay !== 'text'
45
+
46
+ // Match the axis tick label font size (and its viewport-based scaling) for visual consistency.
47
+ const tickLabelFontSize = isMobileFontViewport(currentViewport) ? TICK_LABEL_FONT_SIZE_SMALL : TICK_LABEL_FONT_SIZE
38
48
 
39
49
  /**
40
50
  * Scale dx/dy offsets based on savedDimensions vs current dimensions.
@@ -74,9 +84,11 @@ const Annotations = ({
74
84
  visibleAnnotations.map((annotation, annotationIndex) => {
75
85
  const originalIndex = annotations.indexOf(annotation)
76
86
  const text = annotation.text || ''
87
+ const isEventLine = annotation.style === 'event-line'
77
88
 
78
89
  // Calculate scaled dx/dy offsets based on savedDimensions
79
- const { scaledDx, scaledDy } = getScaledOffsets(annotation)
90
+ const { scaledDx: rawScaledDx, scaledDy } = getScaledOffsets(annotation)
91
+ const scaledDx = isEventLine ? snapEventLineDx(rawScaledDx) : rawScaledDx
80
92
 
81
93
  // Default to absolute positioning
82
94
  let annotationX = xScaleAnnotation(annotation.x)
@@ -88,19 +100,22 @@ const Annotations = ({
88
100
  const dataPoint = dataSource.find(d => d[config.xAxis.dataKey] === annotation.dataX)
89
101
 
90
102
  if (dataPoint) {
91
- const dataYValue = dataPoint[annotation.seriesKey]
92
-
93
103
  // For date/date-time axes, convert raw value to timestamp for scale
94
104
  let xScaleInput = annotation.dataX
95
105
  if (config.xAxis.type === 'date' || config.xAxis.type === 'date-time') {
96
106
  xScaleInput = parseDate(xScaleInput, false)?.getTime()
97
107
  }
98
108
 
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)
109
+ // Event-line annotations are series-agnostic; treat as single (non-grouped) anchor.
110
+ const annotationSeries = isEventLine
111
+ ? undefined
112
+ : config.series?.find(s => s.dataKey === annotation.seriesKey)
101
113
  const barSeriesCount = config.series?.filter(s => s.type === 'Bar').length || 0
102
114
  const isGroupedBarAnnotation =
103
- annotationSeries?.type === 'Bar' && barSeriesCount > 1 && config.visualizationSubType !== 'stacked'
115
+ !isEventLine &&
116
+ annotationSeries?.type === 'Bar' &&
117
+ barSeriesCount > 1 &&
118
+ config.visualizationSubType !== 'stacked'
104
119
 
105
120
  if (isGroupedBarAnnotation && seriesScale) {
106
121
  // Position at group start + series offset + half series bar width
@@ -112,17 +127,23 @@ const Annotations = ({
112
127
  annotationX = xScale(xScaleInput) + (xScale.bandwidth?.() / 2 || 0)
113
128
  }
114
129
 
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
- }
130
+ if (isEventLine) {
131
+ // Anchor visx subject at the plot's vertical middle; the line spans full height.
132
+ annotationY = yMax / 2
133
+ } else {
134
+ // Adjust X for arrow markers based on label direction
135
+ if (annotation.marker === 'arrow' && Math.abs(annotation.dx) >= 100) {
136
+ const direction = annotation.dx > 0 ? 1 : -1
137
+ const relevantBandwidth =
138
+ isGroupedBarAnnotation && seriesScale ? seriesScale.bandwidth?.() : xScale.bandwidth?.()
139
+ const nudgeAmount = relevantBandwidth ? relevantBandwidth / 6 : 2
140
+ annotationX += direction * nudgeAmount
141
+ }
123
142
 
124
- // Y position based on marker type
125
- annotationY = yScale(dataYValue) - (annotation.marker === 'circle' ? 0 : 5)
143
+ // Y position based on marker type
144
+ const dataYValue = dataPoint[annotation.seriesKey]
145
+ annotationY = yScale(dataYValue) - (annotation.marker === 'circle' ? 0 : 5)
146
+ }
126
147
  }
127
148
  }
128
149
 
@@ -141,7 +162,7 @@ const Annotations = ({
141
162
  x={annotationX}
142
163
  y={annotationY}
143
164
  canEditLabel={annotation.edit.label || false}
144
- canEditSubject={(annotation.edit.subject && annotation.connectionType !== 'none') || false}
165
+ canEditSubject={(annotation.edit.subject && (isEventLine || annotation.connectionType !== 'none')) || false}
145
166
  labelDragHandleProps={{ r: 15, stroke: 'red' }}
146
167
  subjectDragHandleProps={{ r: 15, stroke: 'red' }}
147
168
  onDragStart={() => {
@@ -149,7 +170,8 @@ const Annotations = ({
149
170
  setLiveDrag({ index: annotationIndex, dx: scaledDx })
150
171
  }}
151
172
  onDragMove={props => {
152
- setLiveDrag({ index: annotationIndex, dx: props.dx })
173
+ const previewDx = isEventLine ? snapEventLineDx(props.dx) : props.dx
174
+ setLiveDrag({ index: annotationIndex, dx: previewDx })
153
175
  }}
154
176
  onDragEnd={props => {
155
177
  onDragStateChange(false)
@@ -166,9 +188,11 @@ const Annotations = ({
166
188
  : annotation.x === xScaleAnnotation.invert(props.x) && annotation.y === yScaleAnnotation.invert(props.y)
167
189
 
168
190
  if (isLabelOnlyDrag) {
191
+ // Event-line snaps dx; callout persists raw drag offset.
192
+ const persistedDx = isEventLine ? snapEventLineDx(props.dx) : props.dx
169
193
  updatedAnnotations[originalIndex] = {
170
194
  ...updatedAnnotations[originalIndex],
171
- dx: props.dx,
195
+ dx: persistedDx,
172
196
  dy: props.dy,
173
197
  savedDimensions: currentDimensions
174
198
  }
@@ -209,107 +233,183 @@ const Annotations = ({
209
233
  })
210
234
  }}
211
235
  >
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: 'fit-content', maxWidth: `${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
- })()}
258
- {annotation.connectionType === 'line' && (
259
- <Connector type='line' pathProps={{ markerStart: `url(#marker-start--${originalIndex})` }} />
260
- )}
261
- {annotation.connectionType === 'elbow' && (
262
- <Connector type='elbow' pathProps={{ markerStart: `url(#marker-start--${originalIndex})` }} />
263
- )}
264
- {annotation.connectionType === 'curve' && (
265
- <LinePath
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}
271
- fill='none'
272
- marker-start={`url(#marker-start--${originalIndex})`}
273
- />
274
- )}
275
- {annotation.marker === 'circle' && (
276
- <CircleSubject className='circle-subject' stroke={APP_FONT_COLOR} radius={8} />
277
- )}
278
- {annotation.marker === 'arrow' && (
279
- <MarkerArrow
280
- fill={APP_FONT_COLOR}
281
- id={`marker-start--${originalIndex}`}
282
- x={annotationX}
283
- y={annotationY}
284
- stroke={APP_FONT_COLOR}
285
- markerWidth={12}
286
- size={10}
287
- strokeWidth={1}
288
- orient='auto-start-reverse'
289
- markerUnits='userSpaceOnUse'
290
- />
291
- )}
292
- {isMobile && (
236
+ {isEventLine ? (
293
237
  <>
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}
238
+ <line
239
+ x1={annotationX}
240
+ x2={annotationX}
241
+ y1={0}
242
+ y2={yMax}
243
+ stroke='#333'
244
+ strokeWidth={1}
245
+ className='annotation__event-line'
246
+ pointerEvents='none'
301
247
  />
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>
248
+ {!isMobile &&
249
+ (() => {
250
+ // Use live dx during drag (already in current space), otherwise use scaled dx
251
+ const currentDx = liveDrag?.index === annotationIndex ? liveDrag.dx : scaledDx
252
+ const onRight = currentDx >= 0
253
+ return (
254
+ <HtmlLabel
255
+ className='annotation__desktop-label'
256
+ containerStyle={{ width: 'fit-content', maxWidth: '186px' }}
257
+ horizontalAnchor={onRight ? 'start' : 'end'}
258
+ verticalAnchor='middle'
259
+ showAnchorLine={false}
260
+ >
261
+ <div
262
+ className={`annotation__event-line-label ${
263
+ onRight
264
+ ? 'cove-annotation-event-line__label--right'
265
+ : 'cove-annotation-event-line__label--left'
266
+ }`}
267
+ style={{
268
+ backgroundColor: `rgba(255, 255, 255, ${
269
+ annotation?.opacity ? Number(annotation?.opacity) / 100 : 1
270
+ })`,
271
+ padding: '6px 8px',
272
+ color: annotation.colors?.label || APP_FONT_COLOR,
273
+ fontSize: tickLabelFontSize
274
+ }}
275
+ tabIndex={0}
276
+ aria-label={`Annotation text that reads: ${annotation.text}`}
277
+ >
278
+ <div dangerouslySetInnerHTML={sanitizedData()} />
279
+ </div>
280
+ </HtmlLabel>
281
+ )
282
+ })()}
283
+ {isMobile && (
284
+ <>
285
+ <circle
286
+ fill='white'
287
+ cx={annotationX + scaledDx}
288
+ cy={annotationY + scaledDy}
289
+ r={12}
290
+ className='annotation__mobile-label annotation__mobile-label-circle'
291
+ stroke={APP_FONT_COLOR}
292
+ />
293
+ <text
294
+ height={16}
295
+ x={annotationX + scaledDx}
296
+ y={annotationY + scaledDy + 1}
297
+ fontSize={14}
298
+ className='annotation__mobile-label'
299
+ alignmentBaseline='middle'
300
+ textAnchor='middle'
301
+ >
302
+ {originalIndex + 1}
303
+ </text>
304
+ </>
305
+ )}
306
+ </>
307
+ ) : (
308
+ <>
309
+ {!isMobile &&
310
+ (() => {
311
+ const labelWidth = config.general.showAnnotationDropdown ? 186 : 150
312
+ // Use live dx during drag (already in current space), otherwise use scaled dx
313
+ const currentDx = liveDrag?.index === annotationIndex ? liveDrag.dx : scaledDx
314
+ const { horizontalAnchor, verticalAnchor } = getAnnotationAnchors(annotationX, currentDx, labelWidth)
315
+ return (
316
+ <HtmlLabel
317
+ className='annotation__desktop-label'
318
+ containerStyle={{ width: 'fit-content', maxWidth: `${labelWidth}px` }}
319
+ horizontalAnchor={horizontalAnchor}
320
+ verticalAnchor={verticalAnchor}
321
+ showAnchorLine={false}
322
+ >
323
+ <div
324
+ style={{
325
+ borderRadius: 5, // Optional: set border radius
326
+ backgroundColor: `rgba(255, 255, 255, ${
327
+ annotation?.opacity ? Number(annotation?.opacity) / 100 : 1
328
+ })`,
329
+ padding: '10px',
330
+ width: 'auto',
331
+ display: config.general.showAnnotationDropdown ? 'inline-flex' : 'flex',
332
+ justifyContent: 'start',
333
+ flexDirection: 'row',
334
+ alignItems: 'center',
335
+ fontSize: tickLabelFontSize
336
+ }}
337
+ // role='presentation'
338
+ tabIndex={0}
339
+ aria-label={`Annotation text that reads: ${annotation.text}`}
340
+ >
341
+ {config?.general?.showAnnotationDropdown && (
342
+ <>
343
+ <p
344
+ className='annotation__has-dropdown-number'
345
+ style={{ margin: '2px 6px', position: 'relative', left: '-4px' }}
346
+ >
347
+ {originalIndex + 1}
348
+ </p>
349
+ </>
350
+ )}
351
+ <div dangerouslySetInnerHTML={sanitizedData()} />
352
+ </div>
353
+ </HtmlLabel>
354
+ )
355
+ })()}
356
+ {annotation.connectionType === 'line' && (
357
+ <Connector type='line' pathProps={{ markerStart: `url(#marker-start--${originalIndex})` }} />
358
+ )}
359
+ {annotation.connectionType === 'elbow' && (
360
+ <Connector type='elbow' pathProps={{ markerStart: `url(#marker-start--${originalIndex})` }} />
361
+ )}
362
+ {annotation.connectionType === 'curve' && (
363
+ <LinePath
364
+ d={`M ${annotationX},${annotationY}
365
+ Q ${annotationX + scaledDx / 2}, ${
366
+ annotationY + scaledDy / 2 + Number(annotation?.bezier) || 0
367
+ } ${annotationX + scaledDx},${annotationY + scaledDy}`}
368
+ stroke={APP_FONT_COLOR}
369
+ fill='none'
370
+ marker-start={`url(#marker-start--${originalIndex})`}
371
+ />
372
+ )}
373
+ {annotation.marker === 'circle' && (
374
+ <CircleSubject className='circle-subject' stroke={APP_FONT_COLOR} radius={8} />
375
+ )}
376
+ {annotation.marker === 'arrow' && (
377
+ <MarkerArrow
378
+ fill={APP_FONT_COLOR}
379
+ id={`marker-start--${originalIndex}`}
380
+ x={annotationX}
381
+ y={annotationY}
382
+ stroke={APP_FONT_COLOR}
383
+ markerWidth={12}
384
+ size={10}
385
+ strokeWidth={1}
386
+ orient='auto-start-reverse'
387
+ markerUnits='userSpaceOnUse'
388
+ />
389
+ )}
390
+ {isMobile && (
391
+ <>
392
+ <circle
393
+ fill='white'
394
+ cx={annotationX + scaledDx}
395
+ cy={annotationY + scaledDy}
396
+ r={12}
397
+ className='annotation__mobile-label annotation__mobile-label-circle'
398
+ stroke={APP_FONT_COLOR}
399
+ />
400
+ <text
401
+ height={16}
402
+ x={annotationX + scaledDx}
403
+ y={annotationY + scaledDy + 1}
404
+ fontSize={14}
405
+ className='annotation__mobile-label'
406
+ alignmentBaseline='middle'
407
+ textAnchor='middle'
408
+ >
409
+ {originalIndex + 1}
410
+ </text>
411
+ </>
412
+ )}
313
413
  </>
314
414
  )}
315
415
  </AnnotationComponent>
@@ -27,8 +27,11 @@ const AnnotationDropdown = () => {
27
27
  const handleSectionClasses = () => {
28
28
  const classes = [`data-table-container`, viewport, `w-100`, 'mt-4']
29
29
 
30
- // Show dropdown in mobile mode or if explicitly enabled
31
- if (isMobile || config.general.showAnnotationDropdown) {
30
+ // When mobile annotations show full text on the chart, the auto mobile dropdown is suppressed.
31
+ // The dropdown then follows showAnnotationDropdown only (matching desktop behavior).
32
+ const mobileShowsText = config.general?.mobileAnnotationDisplay === 'text'
33
+
34
+ if ((isMobile && !mobileShowsText) || config.general.showAnnotationDropdown) {
32
35
  classes.push('d-block')
33
36
  } else {
34
37
  classes.push('d-none')