@cdc/chart 4.25.8 → 4.25.11
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/.claude/settings.local.json +9 -0
- package/dist/{cdcchart-1a1724a1.es.js → cdcchart-dgT_1dIT.es.js} +136 -151
- package/dist/cdcchart.js +44236 -40355
- package/examples/feature/__data__/planet-example-data.json +0 -30
- package/examples/feature/boxplot/valid-boxplot.csv +38 -17
- package/examples/grouped-bar-test.json +400 -0
- package/examples/private/DEV-11825.json +573 -0
- package/examples/private/d.json +382 -0
- package/examples/private/example-2.json +49784 -0
- package/examples/private/f2.json +1 -0
- package/examples/private/f4.json +1577 -0
- package/examples/private/forecast.json +1180 -0
- package/examples/private/lollipop.json +468 -0
- package/examples/private/na.json +913 -0
- package/examples/private/new.json +48756 -0
- package/examples/private/pie-chart-legend.json +904 -0
- package/examples/private/test-data.csv +28 -0
- package/examples/suppressed_tooltip.json +480 -0
- package/index.html +2 -133
- package/package.json +25 -7
- package/src/CdcChart.tsx +9 -13
- package/src/CdcChartComponent.tsx +403 -92
- package/src/_stories/Chart.Anchors.stories.tsx +2 -2
- package/src/_stories/Chart.BoxPlot.stories.tsx +1 -1
- package/src/_stories/Chart.CI.stories.tsx +1 -1
- package/src/_stories/Chart.Combo.stories.tsx +18 -0
- package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
- package/src/_stories/Chart.DynamicSeries.stories.tsx +2 -2
- package/src/_stories/Chart.Filters.stories.tsx +2 -2
- package/src/_stories/Chart.Forecast.stories.tsx +36 -0
- package/src/_stories/Chart.HTMLInDataTable.stories.tsx +520 -0
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
- package/src/_stories/Chart.Patterns.stories.tsx +20 -0
- package/src/_stories/Chart.PreserveDecimals.stories.tsx +220 -0
- package/src/_stories/Chart.ScatterPlot.stories.tsx +1 -1
- package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
- package/src/_stories/Chart.stories.tsx +8 -5
- package/src/_stories/Chart.tooltip.stories.tsx +1 -1
- package/src/_stories/ChartAnnotation.stories.tsx +7 -4
- package/src/_stories/ChartAxisLabels.stories.tsx +2 -2
- package/src/_stories/ChartAxisTitles.stories.tsx +2 -2
- package/src/_stories/ChartBar.Editor.stories.tsx +3580 -0
- package/src/_stories/ChartEditor.Editor.stories.tsx +658 -0
- package/src/_stories/ChartEditor.stories.tsx +59 -60
- package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
- package/src/_stories/ChartLine.Symbols.stories.tsx +1 -1
- package/src/_stories/ChartPrefixSuffix.stories.tsx +2 -2
- package/src/_stories/_mock/combo.json +451 -0
- package/src/_stories/_mock/editor-test-configs.json +376 -0
- package/src/_stories/_mock/editor-test-datasets.json +477 -0
- package/src/_stories/_mock/editor-tests/bar-chart-editor-test.json +255 -0
- package/src/_stories/_mock/editor-tests/bar-chart-general-test.json +267 -0
- package/src/_stories/_mock/editor-tests/bar-chart-test.json +237 -0
- package/src/_stories/_mock/forecast_combo_with_gaps.json +913 -0
- package/src/_stories/_mock/pie_config.json +257 -62
- package/src/_stories/_mock/small_multiples/small_multiples_bars.json +1944 -0
- package/src/_stories/_mock/small_multiples/small_multiples_big_data_bars.json +1114 -0
- package/src/_stories/_mock/small_multiples/small_multiples_lines.json +2646 -0
- package/src/_stories/_mock/small_multiples/small_multiples_lines_colors.json +1305 -0
- package/src/_stories/_mock/small_multiples/small_multiples_stacked_bars.json +1936 -0
- package/src/_stories/_mock/stacked-pattern-test.json +520 -0
- package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -0
- package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
- package/src/components/Annotations/components/findNearestDatum.ts +6 -41
- package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -6
- package/src/components/AreaChart/index.tsx +1 -2
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +161 -22
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +138 -5
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +215 -73
- package/src/components/BarChart/components/BarChart.Vertical.tsx +155 -22
- package/src/components/BarChart/helpers/index.ts +43 -4
- package/src/components/BarChart/helpers/lollipopColors.ts +27 -0
- package/src/components/BarChart/helpers/useBarChart.ts +25 -3
- package/src/components/BoxPlot/BoxPlot.Vertical.tsx +2 -1
- package/src/components/BoxPlot/helpers/index.ts +3 -3
- package/src/components/Brush/BrushChart.tsx +1 -1
- package/src/components/DeviationBar.jsx +9 -6
- package/src/components/EditorPanel/EditorPanel.tsx +563 -229
- package/src/components/EditorPanel/EditorPanelContext.ts +3 -0
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +96 -111
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +19 -1
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +461 -0
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +80 -67
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +422 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +188 -139
- package/src/components/EditorPanel/components/Panels/index.tsx +5 -1
- package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +0 -8
- package/src/components/EditorPanel/editor-panel.scss +0 -20
- package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +49 -48
- package/src/components/EditorPanel/useEditorPermissions.ts +7 -15
- package/src/components/Forecasting/Forecasting.tsx +175 -27
- package/src/components/ForestPlot/ForestPlot.tsx +11 -7
- package/src/components/ForestPlot/ForestPlotProps.ts +1 -1
- package/src/components/Legend/Legend.Component.tsx +114 -14
- package/src/components/Legend/helpers/createFormatLabels.tsx +230 -171
- package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
- package/src/components/LegendWrapper.tsx +1 -1
- package/src/components/LineChart/LineChartProps.ts +0 -3
- package/src/components/LineChart/components/LineChart.Circle.tsx +2 -2
- package/src/components/LineChart/helpers.ts +1 -1
- package/src/components/LineChart/index.tsx +38 -15
- package/src/components/LinearChart.tsx +96 -84
- package/src/components/PairedBarChart.jsx +6 -4
- package/src/components/PieChart/PieChart.tsx +170 -54
- package/src/components/Regions/components/Regions.tsx +3 -24
- package/src/components/Sankey/components/Sankey.tsx +7 -1
- package/src/components/Sankey/types/index.ts +1 -1
- package/src/components/ScatterPlot/ScatterPlot.jsx +32 -4
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +198 -0
- package/src/components/SmallMultiples/SmallMultiples.css +32 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +271 -0
- package/src/components/SmallMultiples/index.ts +2 -0
- package/src/data/initial-state.js +327 -293
- package/src/helpers/buildForecastPaletteMappings.ts +112 -0
- package/src/helpers/buildForecastPaletteOptions.ts +71 -0
- package/src/helpers/getColorScale.ts +82 -8
- package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +14 -7
- package/src/helpers/getNewRuntime.ts +1 -1
- package/src/helpers/getTransformedData.ts +1 -1
- package/src/helpers/getYAxisAutoPadding.ts +53 -0
- package/src/helpers/smallMultiplesHelpers.ts +529 -0
- package/src/hooks/useChartHoverAnalytics.tsx +44 -0
- package/src/hooks/useProgrammaticTooltip.ts +96 -0
- package/src/hooks/useReduceData.ts +105 -70
- package/src/hooks/useScales.ts +88 -34
- package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
- package/src/hooks/useTooltip.tsx +116 -29
- package/src/index.jsx +0 -2
- package/src/scss/main.scss +13 -80
- package/src/store/chart.actions.ts +2 -0
- package/src/store/chart.reducer.ts +5 -1
- package/src/test/CdcChart.test.jsx +8 -3
- package/src/types/ChartConfig.ts +53 -11
- package/src/types/ChartContext.ts +4 -0
- package/vite.config.js +1 -1
- package/vitest.config.ts +16 -0
- package/src/_stories/_mock/pie_data.json +0 -218
- package/src/components/AreaChart/components/AreaChart.jsx +0 -109
- package/src/coreStyles_chart.scss +0 -3
- package/src/helpers/configHelpers.ts +0 -28
- package/src/helpers/generateColorsArray.ts +0 -8
- package/src/helpers/sort.ts +0 -7
- package/src/hooks/useActiveElement.js +0 -19
- package/src/hooks/useChartClasses.js +0 -41
- package/src/hooks/useColorPalette.js +0 -76
package/src/hooks/useTooltip.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useContext } from 'react'
|
|
1
|
+
import { useContext, useRef, useLayoutEffect } from 'react'
|
|
2
2
|
// Local imports
|
|
3
3
|
import parse from 'html-react-parser'
|
|
4
4
|
import ConfigContext from '../ConfigContext'
|
|
@@ -8,10 +8,12 @@ import { isDateScale } from '@cdc/core/helpers/cove/date'
|
|
|
8
8
|
// Third-party library imports
|
|
9
9
|
import { localPoint } from '@visx/event'
|
|
10
10
|
import { bisector } from 'd3-array'
|
|
11
|
-
import _ from 'lodash'
|
|
11
|
+
import _, { get } from 'lodash'
|
|
12
12
|
import { getHorizontalBarHeights } from '../components/BarChart/helpers/getBarHeights'
|
|
13
13
|
|
|
14
14
|
export const useTooltip = props => {
|
|
15
|
+
// Track the last X-axis value to prevent duplicate analytics events
|
|
16
|
+
const lastAnalyticsXValue = useRef<string | number | null>(null)
|
|
15
17
|
const {
|
|
16
18
|
tableData: data,
|
|
17
19
|
config,
|
|
@@ -23,9 +25,17 @@ export const useTooltip = props => {
|
|
|
23
25
|
setSharedFilter,
|
|
24
26
|
isDraggingAnnotation
|
|
25
27
|
} = useContext<ChartContext>(ConfigContext)
|
|
26
|
-
const { xScale, yScale, seriesScale, showTooltip, hideTooltip } = props
|
|
28
|
+
const { xScale, yScale, seriesScale, showTooltip, hideTooltip, interactionLabel = '' } = props
|
|
27
29
|
const { xAxis, visualizationType, orientation, yAxis, runtime } = config
|
|
28
30
|
|
|
31
|
+
// Track the latest xScale in a ref to prevent stale closures
|
|
32
|
+
const xScaleRef = useRef(xScale)
|
|
33
|
+
|
|
34
|
+
// Update ref whenever xScale prop changes
|
|
35
|
+
useLayoutEffect(() => {
|
|
36
|
+
xScaleRef.current = xScale
|
|
37
|
+
}, [xScale])
|
|
38
|
+
|
|
29
39
|
const Y_AXIS_SIZE = Number(config.yAxis.size || 0)
|
|
30
40
|
|
|
31
41
|
// function handles only Single series hovered data tooltips
|
|
@@ -84,6 +94,7 @@ export const useTooltip = props => {
|
|
|
84
94
|
|
|
85
95
|
const resolvedScaleValues = getResolvedScaleValues([x, y])
|
|
86
96
|
const singleSeriesValue = getYValueFromCoordinate(y, resolvedScaleValues)
|
|
97
|
+
|
|
87
98
|
const columnsWithTooltips = []
|
|
88
99
|
const tooltipItems = [] as any[][]
|
|
89
100
|
for (const [colKey, column] of Object.entries(config.columns)) {
|
|
@@ -154,6 +165,10 @@ export const useTooltip = props => {
|
|
|
154
165
|
return position
|
|
155
166
|
}
|
|
156
167
|
if (!config.tooltips.singleSeries || visualizationType === 'Line') {
|
|
168
|
+
// Collect analytics data for all series
|
|
169
|
+
const analyticsSeriesData: string[] = []
|
|
170
|
+
let xAxisValue: string | number | null = null
|
|
171
|
+
|
|
157
172
|
tooltipItems.push(
|
|
158
173
|
...getIncludedTooltipSeries()
|
|
159
174
|
?.filter(seriesKey => {
|
|
@@ -168,6 +183,7 @@ export const useTooltip = props => {
|
|
|
168
183
|
const seriesObjWithName = config.runtime.series.find(
|
|
169
184
|
series => series.dataKey === seriesKey && series.name !== undefined
|
|
170
185
|
)
|
|
186
|
+
|
|
171
187
|
if (
|
|
172
188
|
(value === null || value === undefined || value === '' || formattedValue === 'N/A') &&
|
|
173
189
|
config.general.hideNullValue
|
|
@@ -181,6 +197,29 @@ export const useTooltip = props => {
|
|
|
181
197
|
})
|
|
182
198
|
)
|
|
183
199
|
|
|
200
|
+
// Publish a single analytics event with all tooltip data
|
|
201
|
+
// Only publish if the X-axis value has changed (different from last hover)
|
|
202
|
+
if (analyticsSeriesData.length > 0 && xAxisValue !== lastAnalyticsXValue.current) {
|
|
203
|
+
lastAnalyticsXValue.current = xAxisValue
|
|
204
|
+
|
|
205
|
+
// Extract series names for the series field
|
|
206
|
+
const seriesNames = analyticsSeriesData.map(item => item.split(':')[0].trim()).join(', ')
|
|
207
|
+
|
|
208
|
+
const specifics = xAxisValue
|
|
209
|
+
? `series: ${seriesNames}, x: ${xAxisValue}, ${analyticsSeriesData.join(', ')}`
|
|
210
|
+
: `series: ${seriesNames}, ${analyticsSeriesData.join(', ')}`
|
|
211
|
+
|
|
212
|
+
publishAnalyticsEvent({
|
|
213
|
+
vizType: config?.type,
|
|
214
|
+
vizSubType: getVizSubType(config),
|
|
215
|
+
eventType: `chart_hover`,
|
|
216
|
+
eventAction: 'hover',
|
|
217
|
+
eventLabel: interactionLabel || 'unknown',
|
|
218
|
+
vizTitle: getVizTitle(config),
|
|
219
|
+
specifics
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
184
223
|
const runtimeSeries =
|
|
185
224
|
config.tooltips.singleSeries && visualizationType === 'Line'
|
|
186
225
|
? [_.find(config.runtime.series, d => d.dataKey === singleSeriesValue)]
|
|
@@ -239,6 +278,9 @@ export const useTooltip = props => {
|
|
|
239
278
|
* @returns {void} - The tooltip information is hidden
|
|
240
279
|
*/
|
|
241
280
|
const handleTooltipMouseOff = () => {
|
|
281
|
+
// Reset the analytics tracking when mouse leaves
|
|
282
|
+
lastAnalyticsXValue.current = null
|
|
283
|
+
|
|
242
284
|
if (config.visualizationType === 'Area Chart') {
|
|
243
285
|
setTimeout(() => {
|
|
244
286
|
hideTooltip()
|
|
@@ -255,15 +297,15 @@ export const useTooltip = props => {
|
|
|
255
297
|
*/
|
|
256
298
|
const getXValueFromCoordinateDate = x => {
|
|
257
299
|
if (config.xAxis.type === 'categorical' || config.visualizationType === 'Combo') {
|
|
258
|
-
let eachBand =
|
|
300
|
+
let eachBand = xScaleRef.current.step()
|
|
259
301
|
let numerator = x
|
|
260
302
|
const index = Math.floor(Number(numerator) / eachBand)
|
|
261
|
-
return
|
|
303
|
+
return xScaleRef.current.domain()[index - 1] // fixes off by 1 error
|
|
262
304
|
}
|
|
263
305
|
|
|
264
306
|
if (isDateScale(config.xAxis) && config.visualizationType !== 'Combo') {
|
|
265
307
|
const bisectDate = bisector(d => parseDate(d[config.xAxis.dataKey])).left
|
|
266
|
-
const x0 =
|
|
308
|
+
const x0 = xScaleRef.current.invert(xScaleRef.current(x))
|
|
267
309
|
const index = bisectDate(config.data, x0, 1)
|
|
268
310
|
const val = parseDate(config.data[index - 1][config.xAxis.dataKey])
|
|
269
311
|
return val
|
|
@@ -275,12 +317,12 @@ export const useTooltip = props => {
|
|
|
275
317
|
* @function getXValueFromCoordinate
|
|
276
318
|
* @returns {String} - the closest x value to the cursor position
|
|
277
319
|
*/
|
|
278
|
-
const getXValueFromCoordinate =
|
|
320
|
+
const getXValueFromCoordinate = x => {
|
|
279
321
|
if (visualizationType === 'Pie') return
|
|
280
322
|
if (orientation === 'horizontal') return
|
|
281
323
|
|
|
282
324
|
// Check the type of x equal to point or if the type of xAxis is equal to continuous or date
|
|
283
|
-
if (
|
|
325
|
+
if (xScaleRef.current.type === 'point' || xAxis.type === 'continuous' || isDateScale(xAxis)) {
|
|
284
326
|
// Find the closest x value by calculating the minimum distance
|
|
285
327
|
let closestX = null
|
|
286
328
|
let minDistance = Number.MAX_VALUE
|
|
@@ -288,9 +330,11 @@ export const useTooltip = props => {
|
|
|
288
330
|
|
|
289
331
|
const barThicknessOffset = config.xAxis.type === 'date' ? xScale.bandwidth() / 2 : 0
|
|
290
332
|
data.forEach(d => {
|
|
291
|
-
const xPosition = isDateScale(xAxis)
|
|
333
|
+
const xPosition = isDateScale(xAxis)
|
|
334
|
+
? xScaleRef.current(parseDate(d[xAxis.dataKey]))
|
|
335
|
+
: xScaleRef.current(d[xAxis.dataKey])
|
|
292
336
|
let bwOffset = config.barHeight
|
|
293
|
-
const distance = Math.abs(Number(xPosition + barThicknessOffset - offset
|
|
337
|
+
const distance = Math.abs(Number(xPosition + barThicknessOffset - offset))
|
|
294
338
|
|
|
295
339
|
if (distance <= minDistance) {
|
|
296
340
|
minDistance = distance
|
|
@@ -300,16 +344,50 @@ export const useTooltip = props => {
|
|
|
300
344
|
return closestX
|
|
301
345
|
}
|
|
302
346
|
|
|
347
|
+
// For band scales, find which band the mouse x-coordinate falls within
|
|
303
348
|
if (config.xAxis.type === 'categorical' || visualizationType === 'Combo') {
|
|
304
|
-
|
|
305
|
-
|
|
349
|
+
const domain = xScaleRef.current.domain()
|
|
350
|
+
const bandwidth = xScaleRef.current.bandwidth()
|
|
306
351
|
|
|
307
|
-
let
|
|
308
|
-
|
|
309
|
-
|
|
352
|
+
let closestValue = null
|
|
353
|
+
let minDistance = Number.MAX_VALUE
|
|
354
|
+
|
|
355
|
+
domain.forEach(value => {
|
|
356
|
+
const bandStart = xScaleRef.current(value)
|
|
357
|
+
const bandCenter = bandStart + bandwidth / 2
|
|
358
|
+
const distance = Math.abs(x - bandCenter)
|
|
359
|
+
|
|
360
|
+
if (distance < minDistance) {
|
|
361
|
+
minDistance = distance
|
|
362
|
+
closestValue = value
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
return closestValue
|
|
310
367
|
}
|
|
311
368
|
}
|
|
312
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Helper for converting data value to pixel coordinate (inverse of getXValueFromCoordinate)
|
|
372
|
+
* @function getCoordinateFromXValue
|
|
373
|
+
* @param {any} xAxisValue - X-axis data value (date, number, or category)
|
|
374
|
+
* @returns {number} - pixel coordinate for the data value
|
|
375
|
+
*/
|
|
376
|
+
const getCoordinateFromXValue = xAxisValue => {
|
|
377
|
+
if (visualizationType === 'Pie') return 0
|
|
378
|
+
if (orientation === 'horizontal') return 0
|
|
379
|
+
|
|
380
|
+
// Convert data value to pixel coordinate using current xScale
|
|
381
|
+
let pixelX = isDateScale(xAxis) ? xScaleRef.current(parseDate(xAxisValue)) : xScaleRef.current(xAxisValue)
|
|
382
|
+
|
|
383
|
+
// For band scales (bar charts, categorical axes), add bandwidth offset to point to center of bar
|
|
384
|
+
if (xScaleRef.current.bandwidth) {
|
|
385
|
+
pixelX += xScaleRef.current.bandwidth() / 2
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return pixelX
|
|
389
|
+
}
|
|
390
|
+
|
|
313
391
|
const findClosest = (dataArray: [any, number][], mouseXorY) => {
|
|
314
392
|
let dataColumn: Object
|
|
315
393
|
dataArray.find(([d, xOrY]) => {
|
|
@@ -397,7 +475,7 @@ export const useTooltip = props => {
|
|
|
397
475
|
const eventSvgCoords = localPoint(e)
|
|
398
476
|
const { x } = eventSvgCoords
|
|
399
477
|
if (!x) throw new Error('COVE: no x value in handleTooltipClick.')
|
|
400
|
-
let closestXScaleValue = getXValueFromCoordinate(x
|
|
478
|
+
let closestXScaleValue = getXValueFromCoordinate(x)
|
|
401
479
|
let datum = config.data?.filter(item => item[config.xAxis.dataKey] === closestXScaleValue)
|
|
402
480
|
if (!closestXScaleValue) throw new Error('COVE: no closest x scale value in handleTooltipClick')
|
|
403
481
|
if (isDateScale(xAxis) && closestXScaleValue) {
|
|
@@ -468,7 +546,7 @@ export const useTooltip = props => {
|
|
|
468
546
|
const dataWithXScale = dataToSearch.map(
|
|
469
547
|
d => [d, seriesScale(d[dynamicSeries.dynamicCategory])] as [Object, number]
|
|
470
548
|
)
|
|
471
|
-
const xOffset = x - Y_AXIS_SIZE -
|
|
549
|
+
const xOffset = x - Y_AXIS_SIZE - xScaleRef.current(closestXScaleValue)
|
|
472
550
|
dataToSearch = [findClosest(dataWithXScale, xOffset)]
|
|
473
551
|
}
|
|
474
552
|
}
|
|
@@ -571,18 +649,26 @@ export const useTooltip = props => {
|
|
|
571
649
|
|
|
572
650
|
// TOOLTIP BODY
|
|
573
651
|
// handle suppressed tooltip items
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
652
|
+
const shouldCheckSuppression = config.visualizationSubType !== 'stacked'
|
|
653
|
+
let suppressionEntry
|
|
654
|
+
if (shouldCheckSuppression && config.preliminaryData) {
|
|
655
|
+
suppressionEntry = config.preliminaryData.find(
|
|
656
|
+
pd =>
|
|
657
|
+
pd.label &&
|
|
658
|
+
pd.type === 'suppression' &&
|
|
659
|
+
pd.displayTooltip &&
|
|
660
|
+
value === pd.value &&
|
|
661
|
+
(!pd.column || key === pd.column)
|
|
662
|
+
)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Remove suppressed items entirely if not showing symbols
|
|
666
|
+
if (suppressionEntry && !config.general.showSuppressedSymbol) {
|
|
667
|
+
return null
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const { label, displayGray } = suppressionEntry || {}
|
|
671
|
+
|
|
586
672
|
let newValue = label || value
|
|
587
673
|
const style = displayGray ? { color: '#8b8b8a' } : {}
|
|
588
674
|
|
|
@@ -603,6 +689,7 @@ export const useTooltip = props => {
|
|
|
603
689
|
getIncludedTooltipSeries,
|
|
604
690
|
getXValueFromCoordinate,
|
|
605
691
|
getXValueFromCoordinateDate,
|
|
692
|
+
getCoordinateFromXValue,
|
|
606
693
|
handleTooltipClick,
|
|
607
694
|
handleTooltipMouseOff,
|
|
608
695
|
handleTooltipMouseOver,
|
package/src/index.jsx
CHANGED
package/src/scss/main.scss
CHANGED
|
@@ -1,52 +1,4 @@
|
|
|
1
|
-
@import '@cdc/core/styles/
|
|
2
|
-
|
|
3
|
-
@mixin breakpoint($class) {
|
|
4
|
-
@if $class == xs {
|
|
5
|
-
@media (max-width: 767px) {
|
|
6
|
-
@content;
|
|
7
|
-
}
|
|
8
|
-
} @else if $class == sm {
|
|
9
|
-
@media (min-width: 768px) {
|
|
10
|
-
@content;
|
|
11
|
-
}
|
|
12
|
-
} @else if $class == md {
|
|
13
|
-
@media (min-width: 960px) {
|
|
14
|
-
@content;
|
|
15
|
-
}
|
|
16
|
-
} @else if $class == lg {
|
|
17
|
-
@media (min-width: 1300px) {
|
|
18
|
-
@content;
|
|
19
|
-
}
|
|
20
|
-
} @else {
|
|
21
|
-
@warn "Breakpoint mixin supports: xs, sm, md, lg";
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
@mixin breakpointClass($class) {
|
|
26
|
-
@if $class == xs {
|
|
27
|
-
&.xs,
|
|
28
|
-
&.xxs {
|
|
29
|
-
@content;
|
|
30
|
-
}
|
|
31
|
-
} @else if $class == sm {
|
|
32
|
-
&.sm,
|
|
33
|
-
&.md,
|
|
34
|
-
&.lg {
|
|
35
|
-
@content;
|
|
36
|
-
}
|
|
37
|
-
} @else if $class == md {
|
|
38
|
-
&.md,
|
|
39
|
-
&.lg {
|
|
40
|
-
@content;
|
|
41
|
-
}
|
|
42
|
-
} @else if $class == lg {
|
|
43
|
-
&.lg {
|
|
44
|
-
@content;
|
|
45
|
-
}
|
|
46
|
-
} @else {
|
|
47
|
-
@warn "Breakpoint Class mixin supports: xs, sm, md, lg";
|
|
48
|
-
}
|
|
49
|
-
}
|
|
1
|
+
@import '@cdc/core/styles/v2/utils/breakpoints';
|
|
50
2
|
|
|
51
3
|
.form-container {
|
|
52
4
|
overflow-y: auto;
|
|
@@ -91,37 +43,6 @@
|
|
|
91
43
|
|
|
92
44
|
border-radius: 3px;
|
|
93
45
|
|
|
94
|
-
.checkbox-group {
|
|
95
|
-
padding: 16px;
|
|
96
|
-
border: 1px solid #c4c4c4;
|
|
97
|
-
border-radius: 8px;
|
|
98
|
-
margin-top: 8px;
|
|
99
|
-
margin-bottom: 24px;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
.loader {
|
|
103
|
-
width: 100%;
|
|
104
|
-
text-align: center;
|
|
105
|
-
display: inline-block;
|
|
106
|
-
animation: spin 1s linear infinite;
|
|
107
|
-
|
|
108
|
-
&::before {
|
|
109
|
-
content: '\21BB';
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
.warning-icon {
|
|
114
|
-
position: relative;
|
|
115
|
-
top: 2px;
|
|
116
|
-
width: 15px;
|
|
117
|
-
height: 15px;
|
|
118
|
-
margin-left: 5px;
|
|
119
|
-
|
|
120
|
-
path {
|
|
121
|
-
fill: #d8000c;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
46
|
.chart-description {
|
|
126
47
|
margin-bottom: 20px;
|
|
127
48
|
}
|
|
@@ -741,3 +662,15 @@
|
|
|
741
662
|
.cdc-open-viz-module .debug {
|
|
742
663
|
border: 2px solid red;
|
|
743
664
|
}
|
|
665
|
+
|
|
666
|
+
// Pattern Legend Styles - using Bootstrap classes for layout, CSS for gaps only
|
|
667
|
+
.legend-patterns {
|
|
668
|
+
gap: var(--space-between-legend-item-rows);
|
|
669
|
+
margin-top: var(--space-between-legend-item-rows);
|
|
670
|
+
|
|
671
|
+
// When in row layout (top/bottom), override gap for proper row/column spacing
|
|
672
|
+
&.flex-row {
|
|
673
|
+
row-gap: var(--space-between-legend-item-rows);
|
|
674
|
+
column-gap: var(--space-between-legend-item-columns);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
@@ -12,6 +12,7 @@ type SET_EXCLUDED_DATA = Action<'SET_EXCLUDED_DATA', object[]>
|
|
|
12
12
|
type SET_FILTERED_DATA = Action<'SET_FILTERED_DATA', object[]>
|
|
13
13
|
type SET_SERIES_HIGHLIGHT = Action<'SET_SERIES_HIGHLIGHT', string[]>
|
|
14
14
|
type SET_VIEWPORT = Action<'SET_VIEWPORT', string>
|
|
15
|
+
type SET_VIZ_VIEWPORT = Action<'SET_VIZ_VIEWPORT', string>
|
|
15
16
|
type SET_DIMENSIONS = Action<'SET_DIMENSIONS', DimensionsType>
|
|
16
17
|
type SET_CONTAINER = Action<'SET_CONTAINER', object>
|
|
17
18
|
type SET_LOADED_EVENT = Action<'SET_LOADED_EVENT', boolean>
|
|
@@ -26,6 +27,7 @@ type ChartActions =
|
|
|
26
27
|
| SET_FILTERED_DATA
|
|
27
28
|
| SET_SERIES_HIGHLIGHT
|
|
28
29
|
| SET_VIEWPORT
|
|
30
|
+
| SET_VIZ_VIEWPORT
|
|
29
31
|
| SET_DIMENSIONS
|
|
30
32
|
| SET_CONTAINER
|
|
31
33
|
| SET_LOADED_EVENT
|
|
@@ -13,6 +13,7 @@ type ChartState = {
|
|
|
13
13
|
filteredData: object[]
|
|
14
14
|
seriesHighlight: string[]
|
|
15
15
|
currentViewport: ViewportSize
|
|
16
|
+
vizViewport: ViewportSize
|
|
16
17
|
dimensions: DimensionsType
|
|
17
18
|
container: HTMLElement | null
|
|
18
19
|
coveLoadedEventRan: boolean
|
|
@@ -25,13 +26,14 @@ export const getInitialState = (configObj: ChartConfig): ChartState => {
|
|
|
25
26
|
return {
|
|
26
27
|
isLoading: true,
|
|
27
28
|
config: defaults,
|
|
28
|
-
stateData:
|
|
29
|
+
stateData: configObj?.data || [],
|
|
29
30
|
colorScale: null,
|
|
30
31
|
excludedData: undefined,
|
|
31
32
|
filteredData: undefined,
|
|
32
33
|
seriesHighlight:
|
|
33
34
|
configObj && configObj?.legend?.seriesHighlight?.length ? [...configObj?.legend?.seriesHighlight] : [],
|
|
34
35
|
currentViewport: 'lg',
|
|
36
|
+
vizViewport: 'lg',
|
|
35
37
|
dimensions: [0, 0],
|
|
36
38
|
container: null,
|
|
37
39
|
coveLoadedEventRan: false,
|
|
@@ -61,6 +63,8 @@ export const reducer = (state: ChartState, action: ChartActions): ChartState =>
|
|
|
61
63
|
return { ...state, seriesHighlight: action.payload }
|
|
62
64
|
case 'SET_VIEWPORT':
|
|
63
65
|
return { ...state, currentViewport: action.payload }
|
|
66
|
+
case 'SET_VIZ_VIEWPORT':
|
|
67
|
+
return { ...state, vizViewport: action.payload }
|
|
64
68
|
case 'SET_DIMENSIONS':
|
|
65
69
|
return { ...state, dimensions: action.payload }
|
|
66
70
|
case 'SET_CONTAINER':
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { testStandaloneBuild } from '@cdc/core/helpers/tests/testStandaloneBuild.ts'
|
|
3
|
+
import { describe, it, expect } from 'vitest'
|
|
4
|
+
|
|
2
5
|
describe('Chart', () => {
|
|
3
|
-
it('
|
|
4
|
-
|
|
6
|
+
it('Can be built in isolation', async () => {
|
|
7
|
+
const pkgDir = path.join(__dirname, '..')
|
|
8
|
+
const result = testStandaloneBuild(pkgDir)
|
|
9
|
+
expect(result).toBe(true)
|
|
5
10
|
})
|
|
6
11
|
})
|
package/src/types/ChartConfig.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Axis } from '@cdc/core/types/Axis'
|
|
2
|
+
import { MarkupConfig } from '@cdc/core/types/MarkupVariable'
|
|
2
3
|
import { type ForestPlotConfigSettings } from './ForestPlot'
|
|
3
4
|
import { type Column } from '@cdc/core/types/Column'
|
|
4
5
|
import { type Series } from '@cdc/core/types/Series'
|
|
@@ -6,7 +7,18 @@ import { Runtime } from '@cdc/core/types/Runtime'
|
|
|
6
7
|
import { FilterBehavior } from '@cdc/core/types/FilterBehavior'
|
|
7
8
|
import { Table } from '@cdc/core/types/Table'
|
|
8
9
|
import { BoxPlot } from '@cdc/core/types/BoxPlot'
|
|
9
|
-
import { General } from '@cdc/core/types/General'
|
|
10
|
+
import { General as CoreGeneral } from '@cdc/core/types/General'
|
|
11
|
+
|
|
12
|
+
// Extend the core General type to include palette information for charts
|
|
13
|
+
type General = CoreGeneral & {
|
|
14
|
+
palette?: {
|
|
15
|
+
name?: string
|
|
16
|
+
version?: string
|
|
17
|
+
isReversed?: boolean
|
|
18
|
+
customColors?: string[]
|
|
19
|
+
customColorsOrdered?: string[]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
10
22
|
import { type Link } from './../components/Sankey/types'
|
|
11
23
|
import { type DataDescription } from '@cdc/core/types/DataDescription'
|
|
12
24
|
import { type Legend as CoreLegend } from '@cdc/core/types/Legend'
|
|
@@ -19,7 +31,7 @@ import { Version } from '@cdc/core/types/Version'
|
|
|
19
31
|
import Footnotes from '@cdc/core/types/Footnotes'
|
|
20
32
|
|
|
21
33
|
export type ViewportSize = 'xxs' | 'xs' | 'sm' | 'md' | 'lg'
|
|
22
|
-
|
|
34
|
+
type ChartColumns = Record<string, Column>
|
|
23
35
|
export type ChartOrientation = 'vertical' | 'horizontal'
|
|
24
36
|
export type VisualizationType =
|
|
25
37
|
| 'Area Chart'
|
|
@@ -64,6 +76,7 @@ type DataFormat = {
|
|
|
64
76
|
bottomSuffix: string
|
|
65
77
|
commas: boolean
|
|
66
78
|
prefix: string
|
|
79
|
+
preserveOriginalDecimals?: boolean
|
|
67
80
|
rightCommas: boolean
|
|
68
81
|
rightPrefix: string
|
|
69
82
|
rightRoundTo: number
|
|
@@ -80,7 +93,7 @@ type Exclusions = {
|
|
|
80
93
|
dateEnd: string
|
|
81
94
|
}
|
|
82
95
|
|
|
83
|
-
|
|
96
|
+
type Legend = CoreLegend & {
|
|
84
97
|
seriesHighlight: string[]
|
|
85
98
|
unified: boolean
|
|
86
99
|
hideSuppressionLink: boolean
|
|
@@ -96,6 +109,17 @@ export type Legend = CoreLegend & {
|
|
|
96
109
|
}
|
|
97
110
|
groupBy: string
|
|
98
111
|
separators?: string
|
|
112
|
+
patterns?: {
|
|
113
|
+
[key: string]: {
|
|
114
|
+
label?: string
|
|
115
|
+
color?: string
|
|
116
|
+
shape?: string
|
|
117
|
+
dataKey?: string
|
|
118
|
+
dataValue?: string
|
|
119
|
+
contrastCheck?: boolean
|
|
120
|
+
patternSize?: number
|
|
121
|
+
}
|
|
122
|
+
}
|
|
99
123
|
}
|
|
100
124
|
|
|
101
125
|
type Visual = {
|
|
@@ -125,7 +149,6 @@ export type AllChartsConfig = {
|
|
|
125
149
|
colorMatchLineSeriesLabels: boolean
|
|
126
150
|
columns: ChartColumns
|
|
127
151
|
confidenceKeys: ConfidenceInterval
|
|
128
|
-
customColors: string[]
|
|
129
152
|
data: Object[]
|
|
130
153
|
dataUrl: string
|
|
131
154
|
dataCutoff: number
|
|
@@ -171,8 +194,22 @@ export type AllChartsConfig = {
|
|
|
171
194
|
runtimeDataUrl: string
|
|
172
195
|
series: Series
|
|
173
196
|
showLineSeriesLabels: boolean
|
|
197
|
+
showAreaUnderLine?: boolean
|
|
174
198
|
showSidebar: boolean
|
|
175
199
|
showTitle: boolean
|
|
200
|
+
smallMultiples?: {
|
|
201
|
+
mode?: 'by-column' | 'by-series'
|
|
202
|
+
tileColumn?: string
|
|
203
|
+
tilesPerRowDesktop?: number
|
|
204
|
+
tilesPerRowMobile?: number
|
|
205
|
+
tileOrderType?: 'asc' | 'desc' | 'custom'
|
|
206
|
+
tileOrder?: string[]
|
|
207
|
+
tileTitles?: { [key: string]: string }
|
|
208
|
+
independentYAxis?: boolean
|
|
209
|
+
colorMode?: 'same' | 'different'
|
|
210
|
+
synchronizedTooltips?: boolean
|
|
211
|
+
showAreaUnderLine?: boolean
|
|
212
|
+
}
|
|
176
213
|
sortData: 'ascending' | 'descending'
|
|
177
214
|
stackedAreaChartLineType: string
|
|
178
215
|
suppressedData?: { label: string; icon: string; value: string }[]
|
|
@@ -196,6 +233,8 @@ export type AllChartsConfig = {
|
|
|
196
233
|
visualizationSubType: string
|
|
197
234
|
xAxis: Axis
|
|
198
235
|
yAxis: Axis
|
|
236
|
+
hideXAxisLabel?: boolean
|
|
237
|
+
hideYAxisLabel?: boolean
|
|
199
238
|
xScale: Function
|
|
200
239
|
yScale: Function
|
|
201
240
|
regions: Region[]
|
|
@@ -225,12 +264,13 @@ export type AllChartsConfig = {
|
|
|
225
264
|
default: string
|
|
226
265
|
}
|
|
227
266
|
}
|
|
228
|
-
}
|
|
267
|
+
} & MarkupConfig
|
|
229
268
|
|
|
230
|
-
|
|
269
|
+
type ForestPlotConfig = {
|
|
231
270
|
visualizationType: 'Forest Plot'
|
|
232
271
|
forestPlot: ForestPlotConfigSettings
|
|
233
|
-
} & AllChartsConfig
|
|
272
|
+
} & AllChartsConfig &
|
|
273
|
+
MarkupConfig
|
|
234
274
|
|
|
235
275
|
export type LineChartConfig = {
|
|
236
276
|
allowLineToBarGraph: boolean
|
|
@@ -238,9 +278,10 @@ export type LineChartConfig = {
|
|
|
238
278
|
isolatedDotsSameSize: boolean
|
|
239
279
|
lineDatapointStyle: 'hidden' | 'always show' | 'hover'
|
|
240
280
|
visualizationType: 'Line'
|
|
241
|
-
} & AllChartsConfig
|
|
281
|
+
} & AllChartsConfig &
|
|
282
|
+
MarkupConfig
|
|
242
283
|
|
|
243
|
-
|
|
284
|
+
type SankeyLink = {
|
|
244
285
|
depth: number
|
|
245
286
|
height: number
|
|
246
287
|
id: string
|
|
@@ -261,7 +302,7 @@ type StoryNode = {
|
|
|
261
302
|
segmentTextBefore: string
|
|
262
303
|
}
|
|
263
304
|
|
|
264
|
-
|
|
305
|
+
type SankeyChartConfig = {
|
|
265
306
|
enableTooltips: boolean
|
|
266
307
|
data: [
|
|
267
308
|
{
|
|
@@ -279,6 +320,7 @@ export type SankeyChartConfig = {
|
|
|
279
320
|
}
|
|
280
321
|
]
|
|
281
322
|
visualizationType: 'Sankey'
|
|
282
|
-
} & AllChartsConfig
|
|
323
|
+
} & AllChartsConfig &
|
|
324
|
+
MarkupConfig
|
|
283
325
|
|
|
284
326
|
export type ChartConfig = SankeyChartConfig | LineChartConfig | ForestPlotConfig | AllChartsConfig
|
|
@@ -16,13 +16,16 @@ type SharedChartContext = {
|
|
|
16
16
|
clean: Function
|
|
17
17
|
colorScale?: ColorScale
|
|
18
18
|
config: ChartConfig
|
|
19
|
+
convertLineToBarGraph?: boolean
|
|
19
20
|
currentViewport?: 'lg' | 'md' | 'sm' | 'xs' | 'xxs'
|
|
21
|
+
vizViewport?: 'lg' | 'md' | 'sm' | 'xs' | 'xxs'
|
|
20
22
|
dashboardConfig?: DashboardConfig
|
|
21
23
|
// process top level chart aria label for each chart type
|
|
22
24
|
handleChartAriaLabels: (config: any) => string
|
|
23
25
|
handleDragStateChange: (isDragging: any) => void
|
|
24
26
|
highlight?: Function
|
|
25
27
|
handleShowAll?: Function
|
|
28
|
+
interactionLabel?: string
|
|
26
29
|
// whether or not the chart is viewed within the editor screen
|
|
27
30
|
isEditor?: boolean
|
|
28
31
|
// whether or not the user is dragging an annotation
|
|
@@ -32,6 +35,7 @@ type SharedChartContext = {
|
|
|
32
35
|
parentRef?: React.RefObject<HTMLDivElement>
|
|
33
36
|
setLegendIsolateValues?: Function
|
|
34
37
|
svgRef?: React.RefObject<SVGSVGElement>
|
|
38
|
+
handleSmallMultipleHover?: (xAxisValue: any, yCoordinate: number) => void
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
// Line Chart Specific Context
|
package/vite.config.js
CHANGED
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
environment: 'jsdom',
|
|
6
|
+
globals: true,
|
|
7
|
+
setupFiles: ['../../vitest.setup.ts'],
|
|
8
|
+
exclude: [
|
|
9
|
+
'**/node_modules/**',
|
|
10
|
+
'**/dist/**',
|
|
11
|
+
'**/.storybook/**',
|
|
12
|
+
'**/*.stories.*',
|
|
13
|
+
'**/storybook-static/**'
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
})
|