@cdc/chart 4.24.12 → 4.25.2-25
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/dist/cdcchart.js +79900 -78999
- package/examples/feature/boxplot/boxplot.json +2 -157
- package/examples/feature/boxplot/testing.csv +23 -38
- package/examples/feature/tests-non-numerics/example-combo-bar-nonnumeric.json +579 -49
- package/examples/private/ehdi.json +29939 -0
- package/examples/private/line-issue.json +497 -0
- package/examples/private/not-loading.json +360 -0
- package/index.html +11 -15
- package/package.json +2 -2
- package/src/CdcChart.tsx +92 -1512
- package/src/CdcChartComponent.tsx +1113 -0
- package/src/ConfigContext.tsx +6 -1
- package/src/_stories/Chart.Anchors.stories.tsx +1 -1
- package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
- package/src/_stories/Chart.DynamicSeries.stories.tsx +17 -2
- package/src/_stories/Chart.Filters.stories.tsx +19 -0
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
- package/src/_stories/Chart.ScatterPlot.stories.tsx +19 -0
- package/src/_stories/Chart.tooltip.stories.tsx +1 -2
- package/src/_stories/ChartAnnotation.stories.tsx +1 -1
- package/src/_stories/ChartAxisLabels.stories.tsx +1 -1
- package/src/_stories/ChartAxisTitles.stories.tsx +1 -1
- package/src/_stories/ChartEditor.stories.tsx +1 -1
- package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
- package/src/_stories/ChartLine.Symbols.stories.tsx +18 -0
- package/src/_stories/ChartPrefixSuffix.stories.tsx +1 -1
- package/src/_stories/_mock/line_chart_symbols.json +437 -0
- package/src/_stories/_mock/scatterplot-image-download.json +1244 -0
- package/src/components/Annotations/components/AnnotationDraggable.tsx +3 -11
- package/src/components/Annotations/components/AnnotationDropdown.tsx +3 -3
- package/src/components/Axis/Categorical.Axis.tsx +3 -4
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +14 -5
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +10 -4
- package/src/components/BarChart/components/BarChart.Vertical.tsx +5 -7
- package/src/components/BarChart/components/BarChart.jsx +24 -4
- package/src/components/BarChart/components/context.tsx +1 -0
- package/src/components/BoxPlot/BoxPlot.tsx +34 -32
- package/src/components/BoxPlot/helpers/index.ts +108 -18
- package/src/components/BrushChart.tsx +44 -24
- package/src/components/DeviationBar.jsx +2 -6
- package/src/components/EditorPanel/EditorPanel.tsx +64 -8
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +4 -0
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +3 -1
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +44 -7
- package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +6 -1
- package/src/components/ForestPlot/ForestPlot.tsx +176 -26
- package/src/components/Legend/Legend.Component.tsx +29 -38
- package/src/components/Legend/Legend.Suppression.tsx +3 -5
- package/src/components/Legend/Legend.tsx +2 -2
- package/src/components/Legend/LegendLine.Shape.tsx +51 -0
- package/src/components/Legend/helpers/createFormatLabels.tsx +29 -26
- package/src/components/Legend/helpers/getLegendClasses.ts +20 -38
- package/src/components/Legend/helpers/index.ts +22 -9
- package/src/components/Legend/tests/getLegendClasses.test.ts +3 -20
- package/src/components/LineChart/components/LineChart.Circle.tsx +104 -94
- package/src/components/LineChart/index.tsx +6 -2
- package/src/components/LinearChart.tsx +77 -43
- package/src/components/PairedBarChart.jsx +2 -9
- package/src/components/ZoomBrush.tsx +5 -7
- package/src/data/initial-state.js +6 -3
- package/src/helpers/getBoxPlotConfig.ts +68 -0
- package/src/helpers/getColorScale.ts +24 -0
- package/src/helpers/getComboChartConfig.ts +42 -0
- package/src/helpers/getExcludedData.ts +37 -0
- package/src/helpers/getTopAxis.ts +7 -0
- package/src/helpers/isConvertLineToBarGraph.ts +10 -3
- package/src/hooks/useBarChart.ts +40 -13
- package/src/hooks/{useHighlightedBars.js → useHighlightedBars.ts} +2 -1
- package/src/hooks/useIntersectionObserver.ts +37 -0
- package/src/hooks/useMinMax.ts +11 -8
- package/src/hooks/useReduceData.ts +1 -1
- package/src/hooks/useScales.ts +10 -0
- package/src/hooks/useTooltip.tsx +21 -2
- package/src/index.jsx +1 -0
- package/src/scss/DataTable.scss +0 -5
- package/src/scss/main.scss +31 -116
- package/src/store/chart.actions.ts +40 -0
- package/src/store/chart.reducer.ts +83 -0
- package/src/types/ChartConfig.ts +6 -3
- package/src/types/ChartContext.ts +1 -3
- package/src/helpers/getQuartiles.ts +0 -27
- package/src/hooks/useColorScale.ts +0 -50
- package/src/hooks/useIntersectionObserver.jsx +0 -29
- package/src/hooks/useTopAxis.js +0 -6
|
@@ -0,0 +1,1113 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useRef, useId, useContext, useReducer } from 'react'
|
|
2
|
+
|
|
3
|
+
// IE11
|
|
4
|
+
import ResizeObserver from 'resize-observer-polyfill'
|
|
5
|
+
import 'whatwg-fetch'
|
|
6
|
+
// Core components
|
|
7
|
+
import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
|
|
8
|
+
import Layout from '@cdc/core/components/Layout'
|
|
9
|
+
import Confirm from '@cdc/core/components/elements/Confirm'
|
|
10
|
+
import Error from '@cdc/core/components/elements/Error'
|
|
11
|
+
import SkipTo from '@cdc/core/components/elements/SkipTo'
|
|
12
|
+
import Title from '@cdc/core/components/ui/Title'
|
|
13
|
+
import DataTable from '@cdc/core/components/DataTable'
|
|
14
|
+
// Local Components
|
|
15
|
+
import LegendWrapper from './components/LegendWrapper'
|
|
16
|
+
//types
|
|
17
|
+
import { type DashboardConfig } from '@cdc/dashboard/src/types/DashboardConfig'
|
|
18
|
+
import type { TableConfig } from '@cdc/core/components/DataTable/types/TableConfig'
|
|
19
|
+
import { AllChartsConfig, ChartConfig } from './types/ChartConfig'
|
|
20
|
+
import { Pivot } from '@cdc/core/types/Table'
|
|
21
|
+
import { Runtime } from '@cdc/core/types/Runtime'
|
|
22
|
+
import { Label } from './types/Label'
|
|
23
|
+
// External Libraries
|
|
24
|
+
import ParentSize from '@visx/responsive/lib/components/ParentSize'
|
|
25
|
+
import { timeParse, timeFormat } from 'd3-time-format'
|
|
26
|
+
import parse from 'html-react-parser'
|
|
27
|
+
import 'react-tooltip/dist/react-tooltip.css'
|
|
28
|
+
import _ from 'lodash'
|
|
29
|
+
// Primary Components
|
|
30
|
+
import ConfigContext, { ChartDispatchContext } from './ConfigContext'
|
|
31
|
+
import PieChart from './components/PieChart'
|
|
32
|
+
import SankeyChart from './components/Sankey'
|
|
33
|
+
import LinearChart from './components/LinearChart'
|
|
34
|
+
import { isDateScale } from '@cdc/core/helpers/cove/date'
|
|
35
|
+
|
|
36
|
+
import { colorPalettesChart as colorPalettes, twoColorPalette } from '@cdc/core/data/colorPalettes'
|
|
37
|
+
|
|
38
|
+
import SparkLine from './components/Sparkline'
|
|
39
|
+
import Legend from './components/Legend'
|
|
40
|
+
import defaults from './data/initial-state'
|
|
41
|
+
import EditorPanel from './components/EditorPanel'
|
|
42
|
+
import { abbreviateNumber } from './helpers/abbreviateNumber'
|
|
43
|
+
import { handleChartTabbing } from './helpers/handleChartTabbing'
|
|
44
|
+
|
|
45
|
+
import { handleChartAriaLabels } from './helpers/handleChartAriaLabels'
|
|
46
|
+
import { lineOptions } from './helpers/lineOptions'
|
|
47
|
+
import { handleLineType } from './helpers/handleLineType'
|
|
48
|
+
import { handleRankByValue } from './helpers/handleRankByValue'
|
|
49
|
+
import { generateColorsArray } from './helpers/generateColorsArray'
|
|
50
|
+
import Loading from '@cdc/core/components/Loading'
|
|
51
|
+
import Filters from '@cdc/core/components/Filters'
|
|
52
|
+
import MediaControls from '@cdc/core/components/MediaControls'
|
|
53
|
+
import Annotation from './components/Annotations'
|
|
54
|
+
// Core Helpers
|
|
55
|
+
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
56
|
+
import { isLegendWrapViewport } from '@cdc/core/helpers/viewports'
|
|
57
|
+
import { missingRequiredSections } from '@cdc/core/helpers/missingRequiredSections'
|
|
58
|
+
import { filterVizData } from '@cdc/core/helpers/filterVizData'
|
|
59
|
+
import { addValuesToFilters } from '@cdc/core/helpers/addValuesToFilters'
|
|
60
|
+
import { publish, subscribe, unsubscribe } from '@cdc/core/helpers/events'
|
|
61
|
+
import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
|
|
62
|
+
import numberFromString from '@cdc/core/helpers/numberFromString'
|
|
63
|
+
import getViewport from '@cdc/core/helpers/getViewport'
|
|
64
|
+
import isNumber from '@cdc/core/helpers/isNumber'
|
|
65
|
+
import coveUpdateWorker from '@cdc/core/helpers/coveUpdateWorker'
|
|
66
|
+
import EditorContext from '../../editor/src/ConfigContext'
|
|
67
|
+
// Local helpers
|
|
68
|
+
import { isConvertLineToBarGraph } from './helpers/isConvertLineToBarGraph'
|
|
69
|
+
import { getBoxPlotConfig } from './helpers/getBoxPlotConfig'
|
|
70
|
+
import { getComboChartConfig } from './helpers/getComboChartConfig'
|
|
71
|
+
import { getExcludedData } from './helpers/getExcludedData'
|
|
72
|
+
import { getColorScale } from './helpers/getColorScale'
|
|
73
|
+
// styles
|
|
74
|
+
import './scss/main.scss'
|
|
75
|
+
import { getInitialState, reducer } from './store/chart.reducer'
|
|
76
|
+
|
|
77
|
+
interface CdcChartProps {
|
|
78
|
+
config?: ChartConfig
|
|
79
|
+
isEditor?: boolean
|
|
80
|
+
isDebug?: boolean
|
|
81
|
+
isDashboard?: boolean
|
|
82
|
+
setConfig?: (config: ChartConfig) => void
|
|
83
|
+
setEditing?: (editing: boolean) => void
|
|
84
|
+
hostname?: string
|
|
85
|
+
link?: string
|
|
86
|
+
setSharedFilter?: (filter: any) => void
|
|
87
|
+
setSharedFilterValue?: (value: any) => void
|
|
88
|
+
dashboardConfig?: DashboardConfig
|
|
89
|
+
}
|
|
90
|
+
const CdcChart: React.FC<CdcChartProps> = ({
|
|
91
|
+
config: configObj,
|
|
92
|
+
isEditor = false,
|
|
93
|
+
isDebug = false,
|
|
94
|
+
isDashboard = false,
|
|
95
|
+
setConfig: setParentConfig,
|
|
96
|
+
setEditing,
|
|
97
|
+
link,
|
|
98
|
+
setSharedFilter,
|
|
99
|
+
setSharedFilterValue,
|
|
100
|
+
dashboardConfig
|
|
101
|
+
}) => {
|
|
102
|
+
const transform = new DataTransform()
|
|
103
|
+
const initialState = getInitialState(configObj)
|
|
104
|
+
const [state, dispatch] = useReducer(reducer, initialState)
|
|
105
|
+
const {
|
|
106
|
+
config,
|
|
107
|
+
stateData,
|
|
108
|
+
excludedData,
|
|
109
|
+
filteredData,
|
|
110
|
+
currentViewport,
|
|
111
|
+
isLoading,
|
|
112
|
+
dimensions,
|
|
113
|
+
container,
|
|
114
|
+
coveLoadedEventRan,
|
|
115
|
+
imageId,
|
|
116
|
+
seriesHighlight,
|
|
117
|
+
colorScale,
|
|
118
|
+
brushConfig
|
|
119
|
+
} = state
|
|
120
|
+
const { description, visualizationType } = config
|
|
121
|
+
const svgRef = useRef(null)
|
|
122
|
+
const editorContext = useContext(EditorContext)
|
|
123
|
+
const [externalFilters, setExternalFilters] = useState<any[]>()
|
|
124
|
+
|
|
125
|
+
const setConfig = (newConfig: ChartConfig): void => {
|
|
126
|
+
dispatch({ type: 'SET_CONFIG', payload: newConfig })
|
|
127
|
+
if (isEditor && !isDashboard) {
|
|
128
|
+
editorContext.setTempConfig(newConfig)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const setFiltersData = (filteredData: object[]): void => {
|
|
133
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: filteredData })
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const legendRef = useRef(null)
|
|
137
|
+
const parentRef = useRef(null)
|
|
138
|
+
|
|
139
|
+
const handleDragStateChange = isDragging => {
|
|
140
|
+
dispatch({ type: 'SET_DRAG_ANNOTATIONS', payload: isDragging })
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (isDebug) console.log('Chart config, isEditor', config, isEditor)
|
|
144
|
+
|
|
145
|
+
// Destructure items from config for more readable JSX
|
|
146
|
+
let { legend, title } = config
|
|
147
|
+
|
|
148
|
+
// set defaults on titles if blank AND only in editor
|
|
149
|
+
if (isEditor) {
|
|
150
|
+
if (!title || title === '') title = 'Chart Title'
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (config.table && (!config.table?.label || config.table?.label === '')) config.table.label = 'Data Table'
|
|
154
|
+
|
|
155
|
+
const { lineDatapointClass, contentClasses, sparkLineStyles } = useDataVizClasses(config)
|
|
156
|
+
const legendId = useId()
|
|
157
|
+
|
|
158
|
+
const hasDateAxis =
|
|
159
|
+
(config.xAxis || config.yAxis) && ['date-time', 'date'].includes((config.xAxis || config.yAxis).type)
|
|
160
|
+
const dataTableDefaultSortBy = hasDateAxis && config.xAxis.dataKey
|
|
161
|
+
|
|
162
|
+
const convertLineToBarGraph = isConvertLineToBarGraph(config, filteredData)
|
|
163
|
+
|
|
164
|
+
const prepareConfig = async (loadedConfig: ChartConfig) => {
|
|
165
|
+
let newConfig = _.defaultsDeep(loadedConfig, defaults)
|
|
166
|
+
_.defaultsDeep(newConfig, {
|
|
167
|
+
table: { showVertical: false }
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
_.set(newConfig, 'table.show', _.get(newConfig, 'table.show', !isDashboard))
|
|
171
|
+
|
|
172
|
+
_.forEach(newConfig.series, series => {
|
|
173
|
+
_.defaults(series, {
|
|
174
|
+
tooltip: true,
|
|
175
|
+
axis: 'Left'
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
if (newConfig.visualizationType === 'Bump Chart') {
|
|
180
|
+
newConfig.xAxis.type === 'date-time'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { ...coveUpdateWorker(newConfig) }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const updateConfig = (_config: AllChartsConfig, dataOverride?: any[]) => {
|
|
187
|
+
const newConfig = _.cloneDeep(_config)
|
|
188
|
+
let data = dataOverride || stateData
|
|
189
|
+
|
|
190
|
+
data = handleRankByValue(data, newConfig)
|
|
191
|
+
|
|
192
|
+
// Deeper copy
|
|
193
|
+
Object.keys(defaults).forEach(key => {
|
|
194
|
+
if (newConfig[key] && 'object' === typeof newConfig[key] && !Array.isArray(newConfig[key])) {
|
|
195
|
+
newConfig[key] = { ...defaults[key], ...newConfig[key] }
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const newExcludedData: any[] = getExcludedData(newConfig, dataOverride || stateData)
|
|
200
|
+
dispatch({ type: 'SET_EXCLUDED_DATA', payload: newExcludedData })
|
|
201
|
+
|
|
202
|
+
// After data is grabbed, loop through and generate filter column values if there are any
|
|
203
|
+
let currentData: any[] = []
|
|
204
|
+
if (newConfig.filters) {
|
|
205
|
+
const filtersWithValues = addValuesToFilters(newConfig.filters, newExcludedData)
|
|
206
|
+
currentData = filterVizData(filtersWithValues, newExcludedData)
|
|
207
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: currentData })
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (newConfig.xAxis.type === 'date-time' && config.orientation === 'horizontal') {
|
|
211
|
+
newConfig.xAxis.type = 'date'
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
//Enforce default values that need to be calculated at runtime
|
|
215
|
+
newConfig.runtime = {} as Runtime
|
|
216
|
+
newConfig.runtime.series = _.cloneDeep(newConfig.series)
|
|
217
|
+
newConfig.runtime.seriesLabels = {}
|
|
218
|
+
newConfig.runtime.seriesLabelsAll = []
|
|
219
|
+
newConfig.runtime.originalXAxis = newConfig.xAxis
|
|
220
|
+
|
|
221
|
+
if (newConfig.visualizationType === 'Pie') {
|
|
222
|
+
newConfig.runtime.seriesKeys = (dataOverride || data).map(d => d[newConfig.xAxis.dataKey])
|
|
223
|
+
newConfig.runtime.seriesLabelsAll = newConfig.runtime.seriesKeys
|
|
224
|
+
} else {
|
|
225
|
+
const finalData = dataOverride || newConfig.formattedData || newConfig.data
|
|
226
|
+
newConfig.runtime.seriesKeys = (newConfig.runtime.series || []).flatMap(series => {
|
|
227
|
+
if (series.dynamicCategory) {
|
|
228
|
+
_.remove(newConfig.runtime.seriesLabelsAll, label => label === series.dataKey)
|
|
229
|
+
_.remove(newConfig.runtime.series, s => s.dataKey === series.dataKey)
|
|
230
|
+
// grab the dynamic series keys from the data
|
|
231
|
+
const seriesKeys: string[] = _.uniq(finalData.map(d => d[series.dynamicCategory]))
|
|
232
|
+
// for each of those keys perform side effects
|
|
233
|
+
seriesKeys.forEach(dataKey => {
|
|
234
|
+
newConfig.runtime.seriesLabels[dataKey] = dataKey
|
|
235
|
+
newConfig.runtime.seriesLabelsAll.push(dataKey)
|
|
236
|
+
newConfig.runtime.series.push({
|
|
237
|
+
dataKey,
|
|
238
|
+
type: series.type,
|
|
239
|
+
lineType: series.lineType,
|
|
240
|
+
originalDataKey: series.dataKey,
|
|
241
|
+
dynamicCategory: series.dynamicCategory,
|
|
242
|
+
tooltip: true
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
// return the series keys
|
|
246
|
+
return seriesKeys
|
|
247
|
+
} else {
|
|
248
|
+
newConfig.runtime.seriesLabels[series.dataKey] = series.name || series.label || series.dataKey
|
|
249
|
+
newConfig.runtime.seriesLabelsAll.push(series.name || series.dataKey)
|
|
250
|
+
// return the series keys
|
|
251
|
+
return [series.dataKey]
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (newConfig.visualizationType === 'Box Plot' && newConfig.series) {
|
|
257
|
+
const [plots, categories] = getBoxPlotConfig(newConfig, stateData)
|
|
258
|
+
newConfig.boxplot['categories'] = categories
|
|
259
|
+
newConfig.boxplot.plots = plots
|
|
260
|
+
}
|
|
261
|
+
if (newConfig.visualizationType === 'Combo' && newConfig.series) {
|
|
262
|
+
newConfig.runtime = getComboChartConfig(newConfig)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (newConfig.visualizationType === 'Forecasting' && newConfig.series) {
|
|
266
|
+
newConfig.runtime.forecastingSeriesKeys = []
|
|
267
|
+
|
|
268
|
+
newConfig.series.forEach(series => {
|
|
269
|
+
if (series.type === 'Forecasting') {
|
|
270
|
+
newConfig.runtime.forecastingSeriesKeys.push(series)
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (newConfig.visualizationType === 'Area Chart' && newConfig.series) {
|
|
276
|
+
newConfig.runtime.areaSeriesKeys = []
|
|
277
|
+
|
|
278
|
+
newConfig.series.forEach(series => {
|
|
279
|
+
newConfig.runtime.areaSeriesKeys.push({ ...series, type: 'Area Chart' })
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (
|
|
284
|
+
(newConfig.visualizationType === 'Bar' && newConfig.orientation === 'horizontal') ||
|
|
285
|
+
['Deviation Bar', 'Paired Bar', 'Forest Plot'].includes(newConfig.visualizationType)
|
|
286
|
+
) {
|
|
287
|
+
newConfig.runtime.xAxis = newConfig.yAxis['yAxis'] ? newConfig.yAxis['yAxis'] : newConfig.yAxis
|
|
288
|
+
newConfig.runtime.yAxis = newConfig.xAxis['xAxis'] ? newConfig.xAxis['xAxis'] : newConfig.xAxis
|
|
289
|
+
newConfig.runtime.yAxis.labelOffset *= -1
|
|
290
|
+
|
|
291
|
+
newConfig.runtime.horizontal = false
|
|
292
|
+
newConfig.orientation = 'horizontal'
|
|
293
|
+
// remove after COVE supports categorical axis on horizonatal bars
|
|
294
|
+
newConfig.yAxis.type = newConfig.yAxis.type === 'categorical' ? 'linear' : newConfig.yAxis.type
|
|
295
|
+
} else if (
|
|
296
|
+
['Box Plot', 'Scatter Plot', 'Area Chart', 'Line', 'Forecasting'].includes(newConfig.visualizationType) &&
|
|
297
|
+
!convertLineToBarGraph
|
|
298
|
+
) {
|
|
299
|
+
newConfig.runtime.xAxis = newConfig.xAxis
|
|
300
|
+
newConfig.runtime.yAxis = newConfig.yAxis
|
|
301
|
+
newConfig.runtime.horizontal = false
|
|
302
|
+
newConfig.orientation = 'vertical'
|
|
303
|
+
} else {
|
|
304
|
+
newConfig.runtime.xAxis = newConfig.xAxis
|
|
305
|
+
newConfig.runtime.yAxis = newConfig.yAxis
|
|
306
|
+
newConfig.runtime.horizontal = false
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
newConfig.runtime.uniqueId = Date.now()
|
|
310
|
+
newConfig.runtime.editorErrorMessage =
|
|
311
|
+
newConfig.visualizationType === 'Pie' && !newConfig.yAxis.dataKey
|
|
312
|
+
? 'Data Key property in Y Axis section must be set for pie charts.'
|
|
313
|
+
: ''
|
|
314
|
+
|
|
315
|
+
// Sankey Description box error message
|
|
316
|
+
newConfig.runtime.editorErrorMessage = ''
|
|
317
|
+
|
|
318
|
+
if (newConfig.legend.seriesHighlight?.length) {
|
|
319
|
+
dispatch({ type: 'SET_SERIES_HIGHLIGHT', payload: newConfig.legend?.seriesHighlight })
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
setConfig(newConfig)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Sorts data series for horizontal bar charts
|
|
326
|
+
const sortData = (a, b) => {
|
|
327
|
+
let sortKey =
|
|
328
|
+
config.visualizationType === 'Bar' && config.visualizationSubType === 'horizontal'
|
|
329
|
+
? config.xAxis.dataKey
|
|
330
|
+
: config.yAxis.sortKey
|
|
331
|
+
let aData = parseFloat(a[sortKey])
|
|
332
|
+
let bData = parseFloat(b[sortKey])
|
|
333
|
+
|
|
334
|
+
if (aData < bData) {
|
|
335
|
+
return config.sortData === 'ascending' ? 1 : -1
|
|
336
|
+
} else if (aData > bData) {
|
|
337
|
+
return config.sortData === 'ascending' ? -1 : 1
|
|
338
|
+
} else {
|
|
339
|
+
return 0
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Observes changes to outermost container and changes viewport size in state
|
|
344
|
+
const resizeObserver = new ResizeObserver(entries => {
|
|
345
|
+
for (let entry of entries) {
|
|
346
|
+
let { width, height } = entry.contentRect
|
|
347
|
+
const svgMarginWidth = 15
|
|
348
|
+
const editorWidth = 350
|
|
349
|
+
|
|
350
|
+
width = isEditor ? width - editorWidth : width
|
|
351
|
+
|
|
352
|
+
const newViewport = getViewport(width)
|
|
353
|
+
|
|
354
|
+
dispatch({ type: 'SET_VIEWPORT', payload: newViewport })
|
|
355
|
+
|
|
356
|
+
if (entry.target.dataset.lollipop === 'true') {
|
|
357
|
+
width = width - 2.5
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
width = width - svgMarginWidth
|
|
361
|
+
dispatch({ type: 'SET_DIMENSIONS', payload: [width, height] })
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
const outerContainerRef = useCallback(node => {
|
|
366
|
+
if (node !== null) {
|
|
367
|
+
resizeObserver.observe(node)
|
|
368
|
+
}
|
|
369
|
+
dispatch({ type: 'SET_CONTAINER', payload: node })
|
|
370
|
+
}, []) // eslint-disable-line
|
|
371
|
+
|
|
372
|
+
const prepareData = async newConfig => {
|
|
373
|
+
try {
|
|
374
|
+
const urlFilters = newConfig.filters
|
|
375
|
+
? newConfig.filters.filter(filter => filter.type === 'url').length > 0
|
|
376
|
+
? true
|
|
377
|
+
: false
|
|
378
|
+
: false
|
|
379
|
+
|
|
380
|
+
if (newConfig.dataUrl && !urlFilters) {
|
|
381
|
+
// handle urls with spaces in the name.
|
|
382
|
+
if (newConfig.dataUrl) newConfig.dataUrl = `${newConfig.dataUrl}`
|
|
383
|
+
let newData = await fetchRemoteData(newConfig.dataUrl, 'Chart')
|
|
384
|
+
|
|
385
|
+
if (newData && newConfig.dataDescription) {
|
|
386
|
+
newData = transform.autoStandardize(newData)
|
|
387
|
+
newData = transform.developerStandardize(newData, newConfig.dataDescription)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (newData) {
|
|
391
|
+
newConfig.data = newData
|
|
392
|
+
}
|
|
393
|
+
} else if (newConfig.formattedData) {
|
|
394
|
+
newConfig.data = newConfig.formattedData
|
|
395
|
+
} else if (newConfig.dataDescription) {
|
|
396
|
+
newConfig.data = transform.autoStandardize(newConfig.data)
|
|
397
|
+
newConfig.data = transform.developerStandardize(newConfig.data, newConfig.dataDescription)
|
|
398
|
+
}
|
|
399
|
+
} catch (err) {
|
|
400
|
+
console.log('Error on prepareData function ', err)
|
|
401
|
+
}
|
|
402
|
+
return newConfig
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
useEffect(() => {
|
|
406
|
+
const load = async () => {
|
|
407
|
+
try {
|
|
408
|
+
if (configObj) {
|
|
409
|
+
const preparedConfig = await prepareConfig(configObj)
|
|
410
|
+
let preppedData = await prepareData(preparedConfig)
|
|
411
|
+
dispatch({ type: 'SET_STATE_DATA', payload: preppedData.data })
|
|
412
|
+
dispatch({ type: 'SET_EXCLUDED_DATA', payload: preppedData.data })
|
|
413
|
+
updateConfig(preparedConfig, preppedData.data)
|
|
414
|
+
}
|
|
415
|
+
} catch (err) {
|
|
416
|
+
console.error('Could not Load!')
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
load()
|
|
421
|
+
}, [configObj?.data?.length ? configObj.data : null])
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* When cove has a config and container ref publish the cove_loaded event.
|
|
425
|
+
*/
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
if (container && !isLoading && !_.isEmpty(config) && !coveLoadedEventRan) {
|
|
428
|
+
publish('cove_loaded', { config: config })
|
|
429
|
+
dispatch({ type: 'SET_LOADED_EVENT', payload: true })
|
|
430
|
+
}
|
|
431
|
+
}, [container, config, isLoading]) // eslint-disable-line
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Handles filter change events outside of COVE
|
|
435
|
+
* Updates externalFilters state
|
|
436
|
+
* Another useEffect listens to externalFilterChanges and updates the config.
|
|
437
|
+
*/
|
|
438
|
+
useEffect(() => {
|
|
439
|
+
const handleFilterData = e => {
|
|
440
|
+
let tmp: any[] = []
|
|
441
|
+
tmp.push(e.detail)
|
|
442
|
+
setExternalFilters(tmp)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
subscribe('cove_filterData', e => handleFilterData(e))
|
|
446
|
+
|
|
447
|
+
return () => {
|
|
448
|
+
unsubscribe('cove_filterData', handleFilterData)
|
|
449
|
+
}
|
|
450
|
+
}, [config])
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Handles changes to externalFilters
|
|
454
|
+
* For some reason e.detail is returning [order: "asc"] even though
|
|
455
|
+
* we're not passing that in. The code here checks for an active prop instead of an empty array.
|
|
456
|
+
*/
|
|
457
|
+
useEffect(() => {
|
|
458
|
+
if (externalFilters && externalFilters[0]) {
|
|
459
|
+
const hasActiveProperty = externalFilters[0].hasOwnProperty('active')
|
|
460
|
+
|
|
461
|
+
if (!hasActiveProperty) {
|
|
462
|
+
let configCopy = { ...config }
|
|
463
|
+
delete configCopy['filters']
|
|
464
|
+
setConfig(configCopy)
|
|
465
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: filterVizData(externalFilters, excludedData) })
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (
|
|
470
|
+
externalFilters &&
|
|
471
|
+
externalFilters.length > 0 &&
|
|
472
|
+
externalFilters.length > 0 &&
|
|
473
|
+
externalFilters[0].hasOwnProperty('active')
|
|
474
|
+
) {
|
|
475
|
+
let newConfigHere = { ...config, filters: externalFilters }
|
|
476
|
+
setConfig(newConfigHere)
|
|
477
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: filterVizData(externalFilters, excludedData) })
|
|
478
|
+
}
|
|
479
|
+
}, [externalFilters]) // eslint-disable-line
|
|
480
|
+
|
|
481
|
+
// Generates color palette to pass to child chart component
|
|
482
|
+
useEffect(() => {
|
|
483
|
+
if (stateData && config.xAxis && config.runtime?.seriesKeys) {
|
|
484
|
+
const newColorScale = getColorScale(config)
|
|
485
|
+
dispatch({ type: 'SET_COLOR_SCALE', payload: newColorScale })
|
|
486
|
+
// setColorScale(newColorScale)
|
|
487
|
+
dispatch({ type: 'SET_LOADING', payload: false })
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (config && stateData && config.sortData) {
|
|
491
|
+
stateData.sort(sortData)
|
|
492
|
+
}
|
|
493
|
+
}, [config, stateData]) // eslint-disable-line
|
|
494
|
+
|
|
495
|
+
// Called on legend click, highlights/unhighlights the data series with the given label
|
|
496
|
+
const highlight = (label: Label): void => {
|
|
497
|
+
if (seriesHighlight.length + 1 === config.runtime.seriesKeys.length && config.visualizationType !== 'Forecasting') {
|
|
498
|
+
return handleShowAll()
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const newHighlight = _.findKey(config.runtime.seriesLabels, v => v === label.datum) || label.datum
|
|
502
|
+
|
|
503
|
+
const newSeriesHighlight = _.xor(seriesHighlight, [newHighlight])
|
|
504
|
+
dispatch({ type: 'SET_SERIES_HIGHLIGHT', payload: newSeriesHighlight })
|
|
505
|
+
}
|
|
506
|
+
// Called on reset button click, unhighlights all data series
|
|
507
|
+
const handleShowAll = () => {
|
|
508
|
+
try {
|
|
509
|
+
const legend = legendRef.current
|
|
510
|
+
if (!legend) throw new Error('No legend available to set previous focus on.')
|
|
511
|
+
legend.focus()
|
|
512
|
+
} catch (e) {
|
|
513
|
+
console.error('COVE:', e.message)
|
|
514
|
+
}
|
|
515
|
+
dispatch({ type: 'SET_SERIES_HIGHLIGHT', payload: [] })
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const section = config.orientation === 'horizontal' ? 'yAxis' : 'xAxis'
|
|
519
|
+
|
|
520
|
+
const parseDate = (dateString, showError = true) => {
|
|
521
|
+
let date = timeParse(config.runtime[section].dateParseFormat)(dateString)
|
|
522
|
+
if (!date) {
|
|
523
|
+
if (showError) {
|
|
524
|
+
config.runtime.editorErrorMessage = `Error parsing date "${dateString}". Try reviewing your data and date parse settings in the X Axis section.`
|
|
525
|
+
}
|
|
526
|
+
return new Date()
|
|
527
|
+
} else {
|
|
528
|
+
return date
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const formatDate = (date, i, ticks) => {
|
|
533
|
+
let formattedDate = timeFormat(config.runtime[section].dateDisplayFormat)(date)
|
|
534
|
+
// Handle the case where all months work with '%b.' except for May
|
|
535
|
+
if (config.runtime[section].dateDisplayFormat?.includes('%b.') && formattedDate.includes('May.')) {
|
|
536
|
+
formattedDate = formattedDate.replace(/May\./g, 'May')
|
|
537
|
+
}
|
|
538
|
+
// Show years only once
|
|
539
|
+
if (config.xAxis.showYearsOnce && config.runtime[section].dateDisplayFormat?.includes('%Y') && ticks) {
|
|
540
|
+
const prevDate = ticks[i - 1] ? ticks[i - 1].value : null
|
|
541
|
+
const prevFormattedDate = timeFormat(config.runtime[section].dateDisplayFormat)(prevDate)
|
|
542
|
+
const year = formattedDate.match(/\d{4}/)
|
|
543
|
+
const prevYear = prevFormattedDate.match(/\d{4}/)
|
|
544
|
+
if (year && prevYear && year[0] === prevYear[0]) {
|
|
545
|
+
formattedDate = formattedDate.replace(year, '')
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return formattedDate
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const formatTooltipsDate = date => {
|
|
552
|
+
return timeFormat(config.tooltips.dateDisplayFormat)(date)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Format numeric data based on settings in config OR from passed in settings for Additional Columns
|
|
556
|
+
// - use only for old horizontal data - newer formatNumber is in helper/formatNumber
|
|
557
|
+
// TODO: we should combine various formatNumber functions across this project.
|
|
558
|
+
// TODO suggestion: pass all options as object key/values to allow for more flexibility
|
|
559
|
+
const formatNumber = (
|
|
560
|
+
num,
|
|
561
|
+
axis,
|
|
562
|
+
shouldAbbreviate = false,
|
|
563
|
+
addColPrefix,
|
|
564
|
+
addColSuffix,
|
|
565
|
+
addColRoundTo,
|
|
566
|
+
{ index, length } = { index: null, length: null }
|
|
567
|
+
) => {
|
|
568
|
+
// if num is NaN return num
|
|
569
|
+
if (isNaN(num) || !num) return num
|
|
570
|
+
// Check if the input number is negative
|
|
571
|
+
const isNegative = num < 0
|
|
572
|
+
|
|
573
|
+
if (axis === undefined || !axis) axis = 'left'
|
|
574
|
+
|
|
575
|
+
// If the input number is negative, take the absolute value
|
|
576
|
+
if (isNegative) {
|
|
577
|
+
num = Math.abs(num)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// destructure dataFormat values
|
|
581
|
+
let {
|
|
582
|
+
dataFormat: {
|
|
583
|
+
commas,
|
|
584
|
+
abbreviated,
|
|
585
|
+
roundTo,
|
|
586
|
+
prefix,
|
|
587
|
+
suffix,
|
|
588
|
+
rightRoundTo,
|
|
589
|
+
bottomRoundTo,
|
|
590
|
+
rightPrefix,
|
|
591
|
+
rightSuffix,
|
|
592
|
+
bottomPrefix,
|
|
593
|
+
bottomSuffix,
|
|
594
|
+
bottomAbbreviated,
|
|
595
|
+
onlyShowTopPrefixSuffix
|
|
596
|
+
}
|
|
597
|
+
} = config
|
|
598
|
+
|
|
599
|
+
// check if value contains comma and remove it. later will add comma below.
|
|
600
|
+
if (String(num).indexOf(',') !== -1) num = num.replaceAll(',', '')
|
|
601
|
+
|
|
602
|
+
let original = num
|
|
603
|
+
let stringFormattingOptions: any = {
|
|
604
|
+
useGrouping: commas ? true : false // for old chart data table to work right cant just leave this to undefined
|
|
605
|
+
}
|
|
606
|
+
if (axis === 'left' || axis === undefined) {
|
|
607
|
+
let roundToPlace
|
|
608
|
+
if (addColRoundTo !== undefined) {
|
|
609
|
+
// if its an Additional Column
|
|
610
|
+
roundToPlace = addColRoundTo ? Number(addColRoundTo) : 0
|
|
611
|
+
} else {
|
|
612
|
+
roundToPlace = roundTo ? Number(roundTo) : 0
|
|
613
|
+
}
|
|
614
|
+
stringFormattingOptions = {
|
|
615
|
+
useGrouping: addColRoundTo ? true : config.dataFormat.commas ? true : false,
|
|
616
|
+
minimumFractionDigits: roundToPlace,
|
|
617
|
+
maximumFractionDigits: roundToPlace
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (axis === 'right') {
|
|
622
|
+
stringFormattingOptions = {
|
|
623
|
+
useGrouping: config.dataFormat.rightCommas ? true : false,
|
|
624
|
+
minimumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0,
|
|
625
|
+
maximumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const resolveBottomTickRounding = () => {
|
|
630
|
+
if (config.forestPlot.type === 'Logarithmic' && !bottomRoundTo) return 2
|
|
631
|
+
if (Number(bottomRoundTo)) return Number(bottomRoundTo)
|
|
632
|
+
return 0
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (axis === 'bottom') {
|
|
636
|
+
stringFormattingOptions = {
|
|
637
|
+
useGrouping: config.dataFormat.bottomCommas ? true : false,
|
|
638
|
+
minimumFractionDigits: resolveBottomTickRounding(),
|
|
639
|
+
maximumFractionDigits: resolveBottomTickRounding()
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
num = numberFromString(num)
|
|
644
|
+
|
|
645
|
+
if (isNaN(num)) {
|
|
646
|
+
config.runtime.editorErrorMessage = `Unable to parse number from data ${original}. Try reviewing your data and selections in the Data Series section.`
|
|
647
|
+
return original
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (!config.dataFormat) return num
|
|
651
|
+
if (config.dataCutoff) {
|
|
652
|
+
let cutoff = numberFromString(config.dataCutoff)
|
|
653
|
+
|
|
654
|
+
if (num < cutoff) {
|
|
655
|
+
num = cutoff
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// When we're formatting the left axis
|
|
660
|
+
// Use commas also updates bars and the data table
|
|
661
|
+
// We can't use commas when we're formatting the dataFormatted number
|
|
662
|
+
// Example: commas -> 12,000; abbreviated -> 12k (correct); abbreviated & commas -> 12 (incorrect)
|
|
663
|
+
//
|
|
664
|
+
// Edge case for small numbers with decimals
|
|
665
|
+
// - if roundTo undefined which means it is blank, then do not round
|
|
666
|
+
|
|
667
|
+
if (
|
|
668
|
+
(axis === 'left' && commas && abbreviated && shouldAbbreviate) ||
|
|
669
|
+
(axis === 'bottom' && commas && abbreviated && shouldAbbreviate)
|
|
670
|
+
) {
|
|
671
|
+
num = num // eslint-disable-line
|
|
672
|
+
} else {
|
|
673
|
+
num = num.toLocaleString('en-US', stringFormattingOptions)
|
|
674
|
+
}
|
|
675
|
+
let result = ''
|
|
676
|
+
|
|
677
|
+
if (abbreviated && axis === 'left' && shouldAbbreviate) {
|
|
678
|
+
num = abbreviateNumber(parseFloat(num))
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (bottomAbbreviated && axis === 'bottom' && shouldAbbreviate) {
|
|
682
|
+
num = abbreviateNumber(parseFloat(num))
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (addColPrefix && axis === 'left') {
|
|
686
|
+
result = addColPrefix + result
|
|
687
|
+
} else {
|
|
688
|
+
// if onlyShowTopPrefixSuffix only show top prefix
|
|
689
|
+
const suppressAllButLast = onlyShowTopPrefixSuffix && length - 1 !== index
|
|
690
|
+
if (prefix && axis === 'left' && !suppressAllButLast) {
|
|
691
|
+
result += prefix
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (rightPrefix && axis === 'right') {
|
|
696
|
+
result += rightPrefix
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (bottomPrefix && axis === 'bottom') {
|
|
700
|
+
result += bottomPrefix
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// combine prefix and num
|
|
704
|
+
result += num
|
|
705
|
+
|
|
706
|
+
if (addColSuffix && axis === 'left') {
|
|
707
|
+
result += addColSuffix
|
|
708
|
+
} else {
|
|
709
|
+
if (suffix && axis === 'left' && !onlyShowTopPrefixSuffix) {
|
|
710
|
+
result += suffix
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (rightSuffix && axis === 'right') {
|
|
715
|
+
result += rightSuffix
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (bottomSuffix && axis === 'bottom') {
|
|
719
|
+
result += bottomSuffix
|
|
720
|
+
}
|
|
721
|
+
if (isNegative) {
|
|
722
|
+
result = '-' + result
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return String(result)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// this is passed DOWN into the various components
|
|
729
|
+
// then they do a lookup based on the bin number as index into here (TT)
|
|
730
|
+
const applyLegendToRow = rowObj => {
|
|
731
|
+
try {
|
|
732
|
+
if (!rowObj) throw new Error('COVE: No rowObj in applyLegendToRow')
|
|
733
|
+
// Navigation map
|
|
734
|
+
if ('navigation' === config.type) {
|
|
735
|
+
let mapColorPalette = colorPalettes[config.color] || colorPalettes['bluegreenreverse']
|
|
736
|
+
return generateColorsArray(mapColorPalette[3])
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Fail state
|
|
740
|
+
return generateColorsArray()
|
|
741
|
+
} catch (e) {
|
|
742
|
+
console.error('COVE: ', e) // eslint-disable-line
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// TODO: should be part of the DataTransform class.
|
|
747
|
+
const clean = data => {
|
|
748
|
+
// cleaning is deleting data we need in forecasting charts.
|
|
749
|
+
if (!Array.isArray(data)) return []
|
|
750
|
+
if (config.visualizationType === 'Forecasting') return data
|
|
751
|
+
// specify keys that needs to be cleaned to render chart and skip rest
|
|
752
|
+
const CIkeys: string[] = Object.values(config.confidenceKeys) as string[]
|
|
753
|
+
const seriesKeys: string[] = config.series.map(s => s.dataKey)
|
|
754
|
+
const keysToClean: string[] = seriesKeys.concat(CIkeys)
|
|
755
|
+
// key that does not need to be cleaned
|
|
756
|
+
const excludedKey = config.xAxis.dataKey
|
|
757
|
+
return config?.xAxis?.dataKey ? transform.cleanData(data, excludedKey, keysToClean) : data
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const getTableRuntimeData = () => {
|
|
761
|
+
if (visualizationType === 'Sankey') return config?.data?.[0]?.tableData
|
|
762
|
+
const data = filteredData || excludedData
|
|
763
|
+
const dynamicSeries = config.series.find(series => !!series.dynamicCategory)
|
|
764
|
+
if (!dynamicSeries) return data
|
|
765
|
+
const usedColumns = Object.values(config.columns)
|
|
766
|
+
.filter(col => col.dataTable)
|
|
767
|
+
.map(col => col.name)
|
|
768
|
+
.concat([dynamicSeries.dynamicCategory, dynamicSeries.dataKey])
|
|
769
|
+
if (config.xAxis?.dataKey) usedColumns.push(config.xAxis.dataKey)
|
|
770
|
+
return data.map(d => _.pick(d, usedColumns))
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const pivotDynamicSeries = (config: ChartConfig): TableConfig => {
|
|
774
|
+
const tableConfig: TableConfig = _.cloneDeep(config)
|
|
775
|
+
const dynamicSeries = tableConfig.series.find(series => !!series.dynamicCategory)
|
|
776
|
+
if (dynamicSeries) {
|
|
777
|
+
const pivot: Pivot = { columnName: dynamicSeries.dynamicCategory, valueColumns: [dynamicSeries.dataKey] }
|
|
778
|
+
tableConfig.table.pivot = pivot
|
|
779
|
+
}
|
|
780
|
+
return tableConfig
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Prevent render if loading
|
|
784
|
+
let body = <Loading />
|
|
785
|
+
|
|
786
|
+
const makeClassName = string => {
|
|
787
|
+
if (!_.isString(string)) return undefined
|
|
788
|
+
|
|
789
|
+
return _.kebabCase(string)
|
|
790
|
+
}
|
|
791
|
+
const getChartWrapperClasses = () => {
|
|
792
|
+
const isLegendOnBottom = legend?.position === 'bottom' || isLegendWrapViewport(currentViewport)
|
|
793
|
+
const classes = ['chart-container', 'p-relative']
|
|
794
|
+
if (legend?.position) {
|
|
795
|
+
if (isLegendWrapViewport(currentViewport) && legend?.position !== 'top') {
|
|
796
|
+
classes.push('legend-bottom')
|
|
797
|
+
} else {
|
|
798
|
+
classes.push(`legend-${legend.position}`)
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (legend?.hide) classes.push('legend-hidden')
|
|
802
|
+
if (lineDatapointClass) classes.push(lineDatapointClass)
|
|
803
|
+
if (!config.barHasBorder) classes.push('chart-bar--no-border')
|
|
804
|
+
if (config.brush?.active && dashboardConfig?.type === 'dashboard' && (!isLegendOnBottom || legend.hide))
|
|
805
|
+
classes.push('dashboard-brush')
|
|
806
|
+
classes.push(...contentClasses)
|
|
807
|
+
return classes
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const getChartSubTextClasses = () => {
|
|
811
|
+
const classes = ['subtext mt-4']
|
|
812
|
+
const isLegendOnBottom = legend?.position === 'bottom' || isLegendWrapViewport(currentViewport)
|
|
813
|
+
|
|
814
|
+
if (config.isResponsiveTicks) classes.push('subtext--responsive-ticks ')
|
|
815
|
+
if (config.brush?.active && !isLegendOnBottom) classes.push('subtext--brush-active ')
|
|
816
|
+
if (config.brush?.active && config.legend.hide) classes.push('subtext--brush-active ')
|
|
817
|
+
return classes
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (!isLoading) {
|
|
821
|
+
const tableLink = (
|
|
822
|
+
<a href={`#data-table-${config.dataKey}`} className='margin-left-href'>
|
|
823
|
+
{config.dataKey} (Go to Table)
|
|
824
|
+
</a>
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
body = (
|
|
828
|
+
<>
|
|
829
|
+
{isEditor && <EditorPanel />}
|
|
830
|
+
<Layout.Responsive isEditor={isEditor}>
|
|
831
|
+
{config.newViz && <Confirm updateConfig={updateConfig} config={config} />}
|
|
832
|
+
{undefined === config.newViz && isEditor && config.runtime && config.runtime?.editorErrorMessage && (
|
|
833
|
+
<Error errorMessage={config.runtime.editorErrorMessage} />
|
|
834
|
+
)}
|
|
835
|
+
{!missingRequiredSections(config) && !config.newViz && (
|
|
836
|
+
<div
|
|
837
|
+
className={`cdc-chart-inner-container cove-component__content type-${makeClassName(
|
|
838
|
+
config.visualizationType
|
|
839
|
+
)}`}
|
|
840
|
+
aria-label={handleChartAriaLabels(config)}
|
|
841
|
+
tabIndex={0}
|
|
842
|
+
>
|
|
843
|
+
<Title
|
|
844
|
+
showTitle={config.showTitle}
|
|
845
|
+
isDashboard={isDashboard}
|
|
846
|
+
title={title}
|
|
847
|
+
superTitle={config.superTitle}
|
|
848
|
+
classes={['chart-title', `${config.theme}`, 'cove-component__header', 'mb-3']}
|
|
849
|
+
style={undefined}
|
|
850
|
+
/>
|
|
851
|
+
|
|
852
|
+
{/* Visualization Wrapper */}
|
|
853
|
+
<div className={getChartWrapperClasses().join(' ')}>
|
|
854
|
+
{/* Intro Text/Message */}
|
|
855
|
+
{config?.introText && config.visualizationType !== 'Spark Line' && (
|
|
856
|
+
<section className={`introText mb-4`}>{parse(config.introText)}</section>
|
|
857
|
+
)}
|
|
858
|
+
|
|
859
|
+
{/* Filters */}
|
|
860
|
+
{config.filters && !externalFilters && config.visualizationType !== 'Spark Line' && (
|
|
861
|
+
<Filters
|
|
862
|
+
config={config}
|
|
863
|
+
setConfig={setConfig}
|
|
864
|
+
setFilteredData={setFiltersData}
|
|
865
|
+
filteredData={filteredData}
|
|
866
|
+
excludedData={excludedData}
|
|
867
|
+
filterData={filterVizData}
|
|
868
|
+
dimensions={dimensions}
|
|
869
|
+
/>
|
|
870
|
+
)}
|
|
871
|
+
<SkipTo skipId={handleChartTabbing(config, legendId)} skipMessage='Skip Over Chart Container' />
|
|
872
|
+
{config.annotations?.length > 0 && (
|
|
873
|
+
<SkipTo
|
|
874
|
+
skipId={handleChartTabbing(config, legendId)}
|
|
875
|
+
skipMessage={`Skip over annotations`}
|
|
876
|
+
key={`skip-annotations`}
|
|
877
|
+
/>
|
|
878
|
+
)}
|
|
879
|
+
<LegendWrapper>
|
|
880
|
+
<div
|
|
881
|
+
className={
|
|
882
|
+
legend.hide || isLegendWrapViewport(currentViewport)
|
|
883
|
+
? 'w-100'
|
|
884
|
+
: legend.position === 'bottom' || legend.position === 'top' || visualizationType === 'Sankey'
|
|
885
|
+
? 'w-100'
|
|
886
|
+
: 'w-75'
|
|
887
|
+
}
|
|
888
|
+
>
|
|
889
|
+
{/* All charts with LinearChart */}
|
|
890
|
+
{!['Spark Line', 'Line', 'Sankey', 'Pie', 'Sankey'].includes(config.visualizationType) && (
|
|
891
|
+
<div ref={parentRef} style={{ width: `100%` }}>
|
|
892
|
+
<ParentSize>
|
|
893
|
+
{parent => (
|
|
894
|
+
<LinearChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
|
|
895
|
+
)}
|
|
896
|
+
</ParentSize>
|
|
897
|
+
</div>
|
|
898
|
+
)}
|
|
899
|
+
|
|
900
|
+
{config.visualizationType === 'Pie' && (
|
|
901
|
+
<ParentSize className='justify-content-center d-flex' style={{ width: `100%` }}>
|
|
902
|
+
{parent => <PieChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />}
|
|
903
|
+
</ParentSize>
|
|
904
|
+
)}
|
|
905
|
+
{/* Line Chart */}
|
|
906
|
+
{config.visualizationType === 'Line' &&
|
|
907
|
+
(convertLineToBarGraph ? (
|
|
908
|
+
<div ref={parentRef} style={{ width: `100%` }}>
|
|
909
|
+
<ParentSize>
|
|
910
|
+
{parent => (
|
|
911
|
+
<LinearChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
|
|
912
|
+
)}
|
|
913
|
+
</ParentSize>
|
|
914
|
+
</div>
|
|
915
|
+
) : (
|
|
916
|
+
<div ref={parentRef} style={{ width: `100%` }}>
|
|
917
|
+
<ParentSize>
|
|
918
|
+
{parent => (
|
|
919
|
+
<LinearChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
|
|
920
|
+
)}
|
|
921
|
+
</ParentSize>
|
|
922
|
+
</div>
|
|
923
|
+
))}
|
|
924
|
+
{/* Sparkline */}
|
|
925
|
+
{config.visualizationType === 'Spark Line' && (
|
|
926
|
+
<>
|
|
927
|
+
<Filters
|
|
928
|
+
config={config}
|
|
929
|
+
setConfig={setConfig}
|
|
930
|
+
setFilteredData={setFiltersData}
|
|
931
|
+
filteredData={filteredData}
|
|
932
|
+
excludedData={excludedData}
|
|
933
|
+
filterData={filterVizData}
|
|
934
|
+
dimensions={dimensions}
|
|
935
|
+
/>
|
|
936
|
+
{config?.introText && (
|
|
937
|
+
<section className='introText mb-4' style={{ padding: '0px 0 35px' }}>
|
|
938
|
+
{parse(config.introText)}
|
|
939
|
+
</section>
|
|
940
|
+
)}
|
|
941
|
+
<div style={{ height: `100px`, width: `100%`, ...sparkLineStyles }}>
|
|
942
|
+
<ParentSize>{parent => <SparkLine width={parent.width} height={parent.height} />}</ParentSize>
|
|
943
|
+
</div>
|
|
944
|
+
{description && (
|
|
945
|
+
<div className='subtext' style={{ padding: '35px 0 15px' }}>
|
|
946
|
+
{parse(description)}
|
|
947
|
+
</div>
|
|
948
|
+
)}
|
|
949
|
+
</>
|
|
950
|
+
)}
|
|
951
|
+
{/* Sankey */}
|
|
952
|
+
{config.visualizationType === 'Sankey' && (
|
|
953
|
+
<ParentSize aria-hidden='true'>
|
|
954
|
+
{parent => <SankeyChart runtime={config.runtime} width={parent.width} height={parent.height} />}
|
|
955
|
+
</ParentSize>
|
|
956
|
+
)}
|
|
957
|
+
</div>
|
|
958
|
+
{/* Legend */}
|
|
959
|
+
{!config.legend.hide &&
|
|
960
|
+
config.visualizationType !== 'Spark Line' &&
|
|
961
|
+
config.visualizationType !== 'Sankey' && (
|
|
962
|
+
<Legend ref={legendRef} skipId={handleChartTabbing(config, legendId)} />
|
|
963
|
+
)}
|
|
964
|
+
</LegendWrapper>
|
|
965
|
+
{/* Link */}
|
|
966
|
+
{isDashboard && config.table && config.table.show && config.table.showDataTableLink
|
|
967
|
+
? tableLink
|
|
968
|
+
: link && link}
|
|
969
|
+
{/* Description */}
|
|
970
|
+
|
|
971
|
+
{config.description && config.visualizationType !== 'Spark Line' && (
|
|
972
|
+
<div className={getChartSubTextClasses().join(' ')}>{parse(config.description)}</div>
|
|
973
|
+
)}
|
|
974
|
+
|
|
975
|
+
{/* buttons */}
|
|
976
|
+
<MediaControls.Section classes={['download-buttons']}>
|
|
977
|
+
{config.table.showDownloadImgButton && (
|
|
978
|
+
<MediaControls.Button
|
|
979
|
+
text='Download Image'
|
|
980
|
+
title='Download Chart as Image'
|
|
981
|
+
type='image'
|
|
982
|
+
state={config}
|
|
983
|
+
elementToCapture={imageId}
|
|
984
|
+
/>
|
|
985
|
+
)}
|
|
986
|
+
{config.table.showDownloadPdfButton && (
|
|
987
|
+
<MediaControls.Button
|
|
988
|
+
text='Download PDF'
|
|
989
|
+
title='Download Chart as PDF'
|
|
990
|
+
type='pdf'
|
|
991
|
+
state={config}
|
|
992
|
+
elementToCapture={imageId}
|
|
993
|
+
/>
|
|
994
|
+
)}
|
|
995
|
+
</MediaControls.Section>
|
|
996
|
+
{/* Data Table */}
|
|
997
|
+
{((config.xAxis.dataKey &&
|
|
998
|
+
config.table.show &&
|
|
999
|
+
config.visualizationType !== 'Spark Line' &&
|
|
1000
|
+
config.visualizationType !== 'Sankey') ||
|
|
1001
|
+
(config.visualizationType === 'Sankey' && config.table.show)) && (
|
|
1002
|
+
<DataTable
|
|
1003
|
+
/* changing the "key" will force the table to re-render
|
|
1004
|
+
when the default sort changes while editing */
|
|
1005
|
+
key={dataTableDefaultSortBy}
|
|
1006
|
+
config={pivotDynamicSeries(config)}
|
|
1007
|
+
rawData={
|
|
1008
|
+
config.visualizationType === 'Sankey'
|
|
1009
|
+
? config?.data?.[0]?.tableData
|
|
1010
|
+
: config.table.customTableConfig
|
|
1011
|
+
? filterVizData(config.filters, config.data)
|
|
1012
|
+
: config.data
|
|
1013
|
+
}
|
|
1014
|
+
runtimeData={getTableRuntimeData()}
|
|
1015
|
+
expandDataTable={config.table.expanded}
|
|
1016
|
+
columns={config.columns}
|
|
1017
|
+
defaultSortBy={dataTableDefaultSortBy}
|
|
1018
|
+
displayGeoName={name => name}
|
|
1019
|
+
applyLegendToRow={applyLegendToRow}
|
|
1020
|
+
tableTitle={config.table.label}
|
|
1021
|
+
indexTitle={config.table.indexLabel}
|
|
1022
|
+
vizTitle={title}
|
|
1023
|
+
viewport={currentViewport}
|
|
1024
|
+
tabbingId={handleChartTabbing(config, legendId)}
|
|
1025
|
+
colorScale={colorScale}
|
|
1026
|
+
/>
|
|
1027
|
+
)}
|
|
1028
|
+
{config?.annotations?.length > 0 && <Annotation.Dropdown />}
|
|
1029
|
+
{/* show pdf or image button */}
|
|
1030
|
+
{config?.footnotes && <section className='footnotes pt-2 mt-4'>{parse(config.footnotes)}</section>}
|
|
1031
|
+
</div>
|
|
1032
|
+
</div>
|
|
1033
|
+
)}
|
|
1034
|
+
</Layout.Responsive>
|
|
1035
|
+
</>
|
|
1036
|
+
)
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const getXAxisData = d =>
|
|
1040
|
+
isDateScale(config.runtime.xAxis)
|
|
1041
|
+
? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime()
|
|
1042
|
+
: d[config.runtime.originalXAxis.dataKey]
|
|
1043
|
+
const getYAxisData = (d, seriesKey) => d[seriesKey]
|
|
1044
|
+
|
|
1045
|
+
const capitalize = str => {
|
|
1046
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const contextValues = {
|
|
1050
|
+
...state,
|
|
1051
|
+
brushConfig,
|
|
1052
|
+
capitalize,
|
|
1053
|
+
convertLineToBarGraph,
|
|
1054
|
+
clean,
|
|
1055
|
+
colorPalettes,
|
|
1056
|
+
dashboardConfig,
|
|
1057
|
+
debugSvg: isDebug,
|
|
1058
|
+
formatDate,
|
|
1059
|
+
formatNumber,
|
|
1060
|
+
formatTooltipsDate,
|
|
1061
|
+
getXAxisData,
|
|
1062
|
+
getYAxisData,
|
|
1063
|
+
handleChartAriaLabels,
|
|
1064
|
+
handleLineType,
|
|
1065
|
+
handleChartTabbing,
|
|
1066
|
+
highlight,
|
|
1067
|
+
handleShowAll,
|
|
1068
|
+
isDashboard,
|
|
1069
|
+
isDebug,
|
|
1070
|
+
handleDragStateChange,
|
|
1071
|
+
isEditor,
|
|
1072
|
+
isNumber,
|
|
1073
|
+
legend,
|
|
1074
|
+
legendId,
|
|
1075
|
+
legendRef,
|
|
1076
|
+
lineOptions,
|
|
1077
|
+
missingRequiredSections,
|
|
1078
|
+
outerContainerRef,
|
|
1079
|
+
parentRef,
|
|
1080
|
+
parseDate,
|
|
1081
|
+
rawData: _.cloneDeep(stateData) ?? {},
|
|
1082
|
+
setConfig,
|
|
1083
|
+
setEditing,
|
|
1084
|
+
setParentConfig,
|
|
1085
|
+
setSharedFilter,
|
|
1086
|
+
setSharedFilterValue,
|
|
1087
|
+
svgRef,
|
|
1088
|
+
tableData: filteredData || excludedData,
|
|
1089
|
+
transformedData: clean(filteredData || excludedData),
|
|
1090
|
+
twoColorPalette,
|
|
1091
|
+
unfilteredData: _.cloneDeep(stateData),
|
|
1092
|
+
updateConfig
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
return (
|
|
1096
|
+
<ConfigContext.Provider value={contextValues}>
|
|
1097
|
+
<ChartDispatchContext.Provider value={dispatch}>
|
|
1098
|
+
<Layout.VisualizationWrapper
|
|
1099
|
+
config={config}
|
|
1100
|
+
isEditor={isEditor}
|
|
1101
|
+
currentViewport={currentViewport}
|
|
1102
|
+
ref={outerContainerRef}
|
|
1103
|
+
imageId={imageId}
|
|
1104
|
+
showEditorPanel={config?.showEditorPanel}
|
|
1105
|
+
>
|
|
1106
|
+
{body}
|
|
1107
|
+
</Layout.VisualizationWrapper>
|
|
1108
|
+
</ChartDispatchContext.Provider>
|
|
1109
|
+
</ConfigContext.Provider>
|
|
1110
|
+
)
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
export default CdcChart
|