@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
package/src/CdcChart.tsx
CHANGED
|
@@ -1,167 +1,47 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
import '
|
|
6
|
-
import * as d3 from 'd3-array'
|
|
7
|
-
import Layout from '@cdc/core/components/Layout'
|
|
8
|
-
import Button from '@cdc/core/components/elements/Button'
|
|
9
|
-
|
|
10
|
-
//types
|
|
11
|
-
import { DimensionsType } from '@cdc/core/types/Dimensions'
|
|
12
|
-
import { type DashboardConfig } from '@cdc/dashboard/src/types/DashboardConfig'
|
|
13
|
-
|
|
14
|
-
// External Libraries
|
|
15
|
-
import { scaleOrdinal } from '@visx/scale'
|
|
16
|
-
import ParentSize from '@visx/responsive/lib/components/ParentSize'
|
|
17
|
-
import { timeParse, timeFormat } from 'd3-time-format'
|
|
1
|
+
import React, { useEffect, useState, useCallback, useRef, useContext } from 'react'
|
|
2
|
+
import CdcChart from './CdcChartComponent'
|
|
3
|
+
import { ChartConfig } from './types/ChartConfig'
|
|
4
|
+
import { getFileExtension } from '@cdc/core/helpers/getFileExtension'
|
|
5
|
+
import { isSolrCsv, isSolrJson } from '@cdc/core/helpers/isSolr'
|
|
18
6
|
import Papa from 'papaparse'
|
|
19
|
-
import parse from 'html-react-parser'
|
|
20
7
|
import 'react-tooltip/dist/react-tooltip.css'
|
|
21
|
-
|
|
22
|
-
// Primary Components
|
|
23
|
-
import ConfigContext from './ConfigContext'
|
|
24
|
-
import PieChart from './components/PieChart'
|
|
25
|
-
import SankeyChart from './components/Sankey'
|
|
26
|
-
import LinearChart from './components/LinearChart'
|
|
27
|
-
import { isDateScale } from '@cdc/core/helpers/cove/date'
|
|
28
|
-
|
|
29
|
-
import { colorPalettesChart as colorPalettes, twoColorPalette } from '@cdc/core/data/colorPalettes'
|
|
30
|
-
|
|
31
|
-
import SparkLine from './components/Sparkline'
|
|
32
|
-
import Legend from './components/Legend'
|
|
33
|
-
import defaults from './data/initial-state'
|
|
34
|
-
import EditorPanel from './components/EditorPanel'
|
|
35
|
-
import { abbreviateNumber } from './helpers/abbreviateNumber'
|
|
36
|
-
import { handleChartTabbing } from './helpers/handleChartTabbing'
|
|
37
|
-
import { getQuartiles } from './helpers/getQuartiles'
|
|
38
|
-
import { handleChartAriaLabels } from './helpers/handleChartAriaLabels'
|
|
39
|
-
import { lineOptions } from './helpers/lineOptions'
|
|
40
|
-
import { handleLineType } from './helpers/handleLineType'
|
|
41
|
-
import { handleRankByValue } from './helpers/handleRankByValue'
|
|
42
|
-
import { generateColorsArray } from './helpers/generateColorsArray'
|
|
43
|
-
import Loading from '@cdc/core/components/Loading'
|
|
44
|
-
import Filters from '@cdc/core/components/Filters'
|
|
45
|
-
import MediaControls from '@cdc/core/components/MediaControls'
|
|
46
|
-
import Annotation from './components/Annotations'
|
|
47
|
-
|
|
48
|
-
// Helpers
|
|
49
|
-
import { publish, subscribe, unsubscribe } from '@cdc/core/helpers/events'
|
|
50
|
-
import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
|
|
51
|
-
import numberFromString from '@cdc/core/helpers/numberFromString'
|
|
52
|
-
import getViewport from '@cdc/core/helpers/getViewport'
|
|
53
|
-
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
54
8
|
import cacheBustingString from '@cdc/core/helpers/cacheBustingString'
|
|
55
|
-
import
|
|
56
|
-
import coveUpdateWorker from '@cdc/core/helpers/coveUpdateWorker'
|
|
57
|
-
import { isConvertLineToBarGraph } from './helpers/isConvertLineToBarGraph'
|
|
58
|
-
import { isLegendWrapViewport } from '@cdc/core/helpers/viewports'
|
|
59
|
-
|
|
60
|
-
import './scss/main.scss'
|
|
61
|
-
// load both then config below determines which to use
|
|
62
|
-
import DataTable from '@cdc/core/components/DataTable'
|
|
63
|
-
import type { TableConfig } from '@cdc/core/components/DataTable/types/TableConfig'
|
|
64
|
-
import { getFileExtension } from '@cdc/core/helpers/getFileExtension'
|
|
65
|
-
import Title from '@cdc/core/components/ui/Title'
|
|
66
|
-
import { AllChartsConfig, ChartConfig } from './types/ChartConfig'
|
|
67
|
-
import { Label } from './types/Label'
|
|
68
|
-
import { type ViewportSize } from './types/ChartConfig'
|
|
69
|
-
import { isSolrCsv, isSolrJson } from '@cdc/core/helpers/isSolr'
|
|
70
|
-
import SkipTo from '@cdc/core/components/elements/SkipTo'
|
|
71
|
-
import { filterVizData } from '@cdc/core/helpers/filterVizData'
|
|
72
|
-
import LegendWrapper from './components/LegendWrapper'
|
|
9
|
+
import Loading from '@cdc/core/components/Loading'
|
|
73
10
|
import _ from 'lodash'
|
|
74
|
-
import
|
|
75
|
-
import { Runtime } from '@cdc/core/types/Runtime'
|
|
76
|
-
import { Pivot } from '@cdc/core/types/Table'
|
|
77
|
-
|
|
11
|
+
import EditorContext from '../../editor/src/ConfigContext'
|
|
78
12
|
interface CdcChartProps {
|
|
79
13
|
configUrl?: string
|
|
80
|
-
config?: ChartConfig
|
|
81
14
|
isEditor?: boolean
|
|
82
15
|
isDebug?: boolean
|
|
83
|
-
|
|
84
|
-
setConfig?: (config: ChartConfig) => void
|
|
85
|
-
setEditing?: (editing: boolean) => void
|
|
86
|
-
hostname?: string
|
|
87
|
-
link?: string
|
|
88
|
-
setSharedFilter?: (filter: any) => void
|
|
89
|
-
setSharedFilterValue?: (value: any) => void
|
|
90
|
-
dashboardConfig?: DashboardConfig
|
|
16
|
+
config?: ChartConfig
|
|
91
17
|
}
|
|
92
|
-
const CdcChart = ({
|
|
93
|
-
configUrl,
|
|
94
|
-
config: configObj,
|
|
95
|
-
isEditor = false,
|
|
96
|
-
isDebug = false,
|
|
97
|
-
isDashboard = false,
|
|
98
|
-
setConfig: setParentConfig,
|
|
99
|
-
setEditing,
|
|
100
|
-
hostname,
|
|
101
|
-
link,
|
|
102
|
-
setSharedFilter,
|
|
103
|
-
setSharedFilterValue,
|
|
104
|
-
dashboardConfig
|
|
105
|
-
}: CdcChartProps) => {
|
|
106
|
-
const transform = new DataTransform()
|
|
107
|
-
const [loading, setLoading] = useState(true)
|
|
108
|
-
const svgRef = useRef(null)
|
|
109
|
-
const [colorScale, setColorScale] = useState(null)
|
|
110
|
-
const [config, setConfig] = useState<ChartConfig>({} as ChartConfig)
|
|
111
|
-
const [stateData, setStateData] = useState(_.cloneDeep(configObj?.data) || [])
|
|
112
|
-
const [excludedData, setExcludedData] = useState<Record<string, number>[] | undefined>(undefined)
|
|
113
|
-
const [filteredData, setFilteredData] = useState<Record<string, any>[] | undefined>(undefined)
|
|
114
|
-
const [seriesHighlight, setSeriesHighlight] = useState<string[]>(
|
|
115
|
-
configObj && configObj?.legend?.seriesHighlight?.length ? [...configObj?.legend?.seriesHighlight] : []
|
|
116
|
-
)
|
|
117
|
-
const [currentViewport, setCurrentViewport] = useState<ViewportSize>('lg')
|
|
118
|
-
const [dimensions, setDimensions] = useState<DimensionsType>([0, 0])
|
|
119
|
-
const [externalFilters, setExternalFilters] = useState<any[]>()
|
|
120
|
-
const [container, setContainer] = useState()
|
|
121
|
-
const [coveLoadedEventRan, setCoveLoadedEventRan] = useState(false)
|
|
122
|
-
const [isDraggingAnnotation, setIsDraggingAnnotation] = useState(false)
|
|
123
|
-
const [dynamicLegendItems, setDynamicLegendItems] = useState<any[]>([])
|
|
124
|
-
const [imageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`)
|
|
125
|
-
const [brushConfig, setBrushConfig] = useState({
|
|
126
|
-
data: [],
|
|
127
|
-
isActive: false,
|
|
128
|
-
isBrushing: false
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
const { description, visualizationType } = config
|
|
132
|
-
|
|
133
|
-
const legendRef = useRef(null)
|
|
134
|
-
const parentRef = useRef(null)
|
|
135
|
-
|
|
136
|
-
const handleDragStateChange = isDragging => {
|
|
137
|
-
setIsDraggingAnnotation(isDragging)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (isDebug) console.log('Chart config, isEditor', config, isEditor)
|
|
141
|
-
|
|
142
|
-
// Destructure items from config for more readable JSX
|
|
143
|
-
let { legend, title } = config
|
|
144
18
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
19
|
+
const CdcChartWrapper: React.FC<CdcChartProps> = ({ configUrl, isEditor, isDebug, config: editorsConfig }) => {
|
|
20
|
+
const editorContext = useContext(EditorContext)
|
|
21
|
+
const [config, _setConfig] = useState<ChartConfig>({} as ChartConfig)
|
|
22
|
+
const setConfig = newConfig => {
|
|
23
|
+
_setConfig(newConfig)
|
|
24
|
+
if (isEditor) {
|
|
25
|
+
editorContext.setTempConfig(newConfig)
|
|
26
|
+
}
|
|
148
27
|
}
|
|
28
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
29
|
+
const prevFiltersRef = useRef(config.filters)
|
|
149
30
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
31
|
+
const loadConfig = useCallback(
|
|
32
|
+
async (url: string) => {
|
|
33
|
+
const response = editorsConfig || (await (await fetch(url)).json())
|
|
34
|
+
return response
|
|
35
|
+
},
|
|
36
|
+
[editorsConfig]
|
|
37
|
+
)
|
|
158
38
|
|
|
159
|
-
const
|
|
39
|
+
const reloadFilteredData = useCallback(async () => {
|
|
160
40
|
if (config.dataUrl) {
|
|
161
41
|
const dataUrl = new URL(config.runtimeDataUrl || config.dataUrl, window.location.origin)
|
|
162
|
-
|
|
163
|
-
|
|
42
|
+
const qsParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
|
|
164
43
|
let isUpdateNeeded = false
|
|
44
|
+
|
|
165
45
|
config.filters?.forEach(filter => {
|
|
166
46
|
if (filter.type === 'url' && qsParams[filter.queryParameter] !== decodeURIComponent(filter.active)) {
|
|
167
47
|
qsParams[filter.queryParameter] = filter.active
|
|
@@ -169,1394 +49,94 @@ const CdcChart = ({
|
|
|
169
49
|
}
|
|
170
50
|
})
|
|
171
51
|
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
let dataUrlFinal = `${dataUrl.origin}${dataUrl.pathname}${Object.keys(qsParams)
|
|
175
|
-
.map((param, i) => {
|
|
176
|
-
let qs = i === 0 ? '?' : '&'
|
|
177
|
-
qs += param + '='
|
|
178
|
-
qs += qsParams[param]
|
|
179
|
-
return qs
|
|
180
|
-
})
|
|
181
|
-
.join('')}`
|
|
182
|
-
|
|
183
|
-
let data: any[] = []
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
const ext = getFileExtension(dataUrl.href)
|
|
187
|
-
if ('csv' === ext || isSolrCsv(dataUrlFinal)) {
|
|
188
|
-
data = await fetch(dataUrlFinal)
|
|
189
|
-
.then(response => response.text())
|
|
190
|
-
.then(responseText => {
|
|
191
|
-
const parsedCsv = Papa.parse(responseText, {
|
|
192
|
-
header: true,
|
|
193
|
-
dynamicTyping: true,
|
|
194
|
-
skipEmptyLines: true
|
|
195
|
-
})
|
|
196
|
-
return parsedCsv.data
|
|
197
|
-
})
|
|
198
|
-
} else if ('json' === ext || isSolrJson(dataUrlFinal)) {
|
|
199
|
-
data = await fetch(dataUrlFinal).then(response => response.json())
|
|
200
|
-
} else {
|
|
201
|
-
data = []
|
|
202
|
-
}
|
|
203
|
-
} catch {
|
|
204
|
-
console.error(`Cannot parse URL: ${dataUrlFinal}`)
|
|
205
|
-
data = []
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (config.dataDescription) {
|
|
209
|
-
data = transform.autoStandardize(data)
|
|
210
|
-
data = transform.developerStandardize(data, config.dataDescription)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
Object.assign(data, { urlFiltered: true })
|
|
214
|
-
|
|
215
|
-
data = handleRankByValue(data, config)
|
|
216
|
-
|
|
217
|
-
updateConfig({ ...config, runtimeDataUrl: dataUrlFinal, data, formattedData: data })
|
|
218
|
-
|
|
219
|
-
if (data) {
|
|
220
|
-
setStateData(data)
|
|
221
|
-
setExcludedData(data)
|
|
222
|
-
setFilteredData(filterVizData(config.filters, data))
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const loadConfig = async () => {
|
|
228
|
-
const response = _.cloneDeep(configObj) || (await (await fetch(configUrl)).json())
|
|
229
|
-
|
|
230
|
-
// If data is included through a URL, fetch that and store
|
|
231
|
-
let data: any[] = response.data || []
|
|
232
|
-
|
|
233
|
-
const urlFilters = response.filters
|
|
234
|
-
? response.filters.filter(filter => filter.type === 'url').length > 0
|
|
235
|
-
? true
|
|
236
|
-
: false
|
|
237
|
-
: false
|
|
238
|
-
|
|
239
|
-
if (response.dataUrl && !urlFilters) {
|
|
240
|
-
try {
|
|
241
|
-
const ext = getFileExtension(response.dataUrl)
|
|
242
|
-
if ('csv' === ext || isSolrCsv(response.dataUrl)) {
|
|
243
|
-
data = await fetch(response.dataUrl + `?v=${cacheBustingString()}`)
|
|
244
|
-
.then(response => response.text())
|
|
245
|
-
.then(responseText => {
|
|
246
|
-
// for every comma NOT inside quotes, replace with a pipe delimiter
|
|
247
|
-
// - this will let commas inside the quotes not be parsed as a new column
|
|
248
|
-
// - Limitation: if a delimiter other than comma is used in the csv this will break
|
|
249
|
-
// Examples of other delimiters that would break: tab
|
|
250
|
-
responseText = responseText.replace(/(".*?")|,/g, (...m) => m[1] || '|')
|
|
251
|
-
// now strip the double quotes
|
|
252
|
-
responseText = responseText.replace(/["]+/g, '')
|
|
253
|
-
const parsedCsv = Papa.parse(responseText, {
|
|
254
|
-
//quotes: "true", // dont need these
|
|
255
|
-
//quoteChar: "'", // has no effect that I can tell
|
|
256
|
-
header: true,
|
|
257
|
-
dynamicTyping: true,
|
|
258
|
-
skipEmptyLines: true,
|
|
259
|
-
delimiter: '|' // we are using pipe symbol as delimiter so setting this explicitly for Papa.parse
|
|
260
|
-
})
|
|
261
|
-
return parsedCsv.data
|
|
262
|
-
})
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if ('json' === ext || isSolrJson(response.dataUrl)) {
|
|
266
|
-
data = await fetch(response.dataUrl + `?v=${cacheBustingString()}`).then(response => response.json())
|
|
267
|
-
}
|
|
268
|
-
} catch {
|
|
269
|
-
console.error(`COVE: Cannot parse URL: ${response.dataUrl}`) // eslint-disable-line
|
|
270
|
-
data = []
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (response.dataDescription) {
|
|
275
|
-
data = transform.autoStandardize(data)
|
|
276
|
-
data = transform.developerStandardize(data, response.dataDescription)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
data = handleRankByValue(data, response)
|
|
280
|
-
|
|
281
|
-
if (data) {
|
|
282
|
-
setStateData(data)
|
|
283
|
-
setExcludedData(data)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// force showVertical for data tables false if it does not exist
|
|
287
|
-
if (response !== undefined && response.table !== undefined) {
|
|
288
|
-
if (!response.table || !response.table.showVertical) {
|
|
289
|
-
response.table = response.table || {}
|
|
290
|
-
response.table.showVertical = false
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
let newConfig = { ...defaults, ...response }
|
|
294
|
-
|
|
295
|
-
if (undefined === newConfig.table.show) newConfig.table.show = !isDashboard
|
|
296
|
-
|
|
297
|
-
newConfig.series.forEach(series => {
|
|
298
|
-
if (series.tooltip === undefined || series.tooltip === null) {
|
|
299
|
-
series.tooltip = true
|
|
300
|
-
}
|
|
301
|
-
if (!series.axis) series.axis = 'Left'
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
if (data) {
|
|
305
|
-
newConfig.data = data
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const processedConfig = { ...coveUpdateWorker(newConfig) }
|
|
309
|
-
|
|
310
|
-
updateConfig(processedConfig, data)
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const updateConfig = (_config: AllChartsConfig, dataOverride?: any[]) => {
|
|
314
|
-
const newConfig = _.cloneDeep(_config)
|
|
315
|
-
let data = dataOverride || stateData
|
|
316
|
-
|
|
317
|
-
data = handleRankByValue(data, newConfig)
|
|
318
|
-
|
|
319
|
-
// Deeper copy
|
|
320
|
-
Object.keys(defaults).forEach(key => {
|
|
321
|
-
if (newConfig[key] && 'object' === typeof newConfig[key] && !Array.isArray(newConfig[key])) {
|
|
322
|
-
newConfig[key] = { ...defaults[key], ...newConfig[key] }
|
|
323
|
-
}
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
let newExcludedData: any[] = []
|
|
327
|
-
|
|
328
|
-
if (newConfig.exclusions && newConfig.exclusions.active) {
|
|
329
|
-
if (newConfig.xAxis.type === 'categorical' && newConfig.exclusions.keys?.length > 0) {
|
|
330
|
-
newExcludedData = data.filter(e => !newConfig.exclusions.keys.includes(e[newConfig.xAxis.dataKey]))
|
|
331
|
-
} else if (
|
|
332
|
-
isDateScale(newConfig.xAxis) &&
|
|
333
|
-
(newConfig.exclusions.dateStart || newConfig.exclusions.dateEnd) &&
|
|
334
|
-
newConfig.xAxis.dateParseFormat
|
|
335
|
-
) {
|
|
336
|
-
// Filter dates
|
|
337
|
-
const timestamp = e => new Date(e).getTime()
|
|
338
|
-
|
|
339
|
-
let startDate = timestamp(newConfig.exclusions.dateStart)
|
|
340
|
-
let endDate = timestamp(newConfig.exclusions.dateEnd) + 86399999 //Increase by 24h in ms (86400000ms - 1ms) to include selected end date for .getTime() comparative
|
|
341
|
-
|
|
342
|
-
let startDateValid = undefined !== typeof startDate && false === isNaN(startDate)
|
|
343
|
-
let endDateValid = undefined !== typeof endDate && false === isNaN(endDate)
|
|
344
|
-
|
|
345
|
-
if (startDateValid && endDateValid) {
|
|
346
|
-
newExcludedData = data.filter(
|
|
347
|
-
e => timestamp(e[newConfig.xAxis.dataKey]) >= startDate && timestamp(e[newConfig.xAxis.dataKey]) <= endDate
|
|
348
|
-
)
|
|
349
|
-
} else if (startDateValid) {
|
|
350
|
-
newExcludedData = data.filter(e => timestamp(e[newConfig.xAxis.dataKey]) >= startDate)
|
|
351
|
-
} else if (endDateValid) {
|
|
352
|
-
newExcludedData = data.filter(e => timestamp(e[newConfig.xAxis.dataKey]) <= endDate)
|
|
353
|
-
}
|
|
354
|
-
} else {
|
|
355
|
-
newExcludedData = dataOverride || stateData
|
|
356
|
-
}
|
|
357
|
-
} else {
|
|
358
|
-
newExcludedData = dataOverride || stateData
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
setExcludedData(newExcludedData)
|
|
362
|
-
|
|
363
|
-
// After data is grabbed, loop through and generate filter column values if there are any
|
|
364
|
-
let currentData: any[] = []
|
|
365
|
-
if (newConfig.filters) {
|
|
366
|
-
const filtersWithValues = addValuesToFilters(newConfig.filters, newExcludedData)
|
|
367
|
-
currentData = filterVizData(filtersWithValues, newExcludedData)
|
|
368
|
-
setFilteredData(currentData)
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (newConfig.xAxis.type === 'date-time' && config.orientation === 'horizontal') {
|
|
372
|
-
newConfig.xAxis.type = 'date'
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
//Enforce default values that need to be calculated at runtime
|
|
376
|
-
newConfig.runtime = {} as Runtime
|
|
377
|
-
newConfig.runtime.series = newConfig.dynamicSeries ? [] : _.cloneDeep(newConfig.series)
|
|
378
|
-
newConfig.runtime.seriesLabels = {}
|
|
379
|
-
newConfig.runtime.seriesLabelsAll = []
|
|
380
|
-
newConfig.runtime.originalXAxis = newConfig.xAxis
|
|
381
|
-
|
|
382
|
-
if (newConfig.dynamicSeries) {
|
|
383
|
-
let finalData = dataOverride || newConfig.formattedData || newConfig.data
|
|
384
|
-
if (finalData?.length) {
|
|
385
|
-
Object.keys(finalData[0]).forEach(seriesKey => {
|
|
386
|
-
if (
|
|
387
|
-
seriesKey !== newConfig.xAxis.dataKey &&
|
|
388
|
-
(!newConfig.filters || newConfig.filters.filter(filter => filter.columnName === seriesKey).length === 0) &&
|
|
389
|
-
(!newConfig.columns || Object.keys(newConfig.columns).indexOf(seriesKey) === -1)
|
|
390
|
-
) {
|
|
391
|
-
newConfig.runtime.series.push({
|
|
392
|
-
dataKey: seriesKey,
|
|
393
|
-
type: newConfig.dynamicSeriesType,
|
|
394
|
-
lineType: newConfig.dynamicSeriesLineType,
|
|
395
|
-
tooltip: true
|
|
396
|
-
})
|
|
397
|
-
}
|
|
398
|
-
})
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
if (newConfig.visualizationType === 'Pie') {
|
|
403
|
-
newConfig.runtime.seriesKeys = (dataOverride || data).map(d => d[newConfig.xAxis.dataKey])
|
|
404
|
-
newConfig.runtime.seriesLabelsAll = newConfig.runtime.seriesKeys
|
|
405
|
-
} else {
|
|
406
|
-
const finalData = dataOverride || newConfig.formattedData || newConfig.data
|
|
407
|
-
newConfig.runtime.seriesKeys = (newConfig.runtime.series || []).flatMap(series => {
|
|
408
|
-
if (series.dynamicCategory) {
|
|
409
|
-
_.remove(newConfig.runtime.seriesLabelsAll, label => label === series.dataKey)
|
|
410
|
-
_.remove(newConfig.runtime.series, s => s.dataKey === series.dataKey)
|
|
411
|
-
// grab the dynamic series keys from the data
|
|
412
|
-
const seriesKeys: string[] = _.uniq(finalData.map(d => d[series.dynamicCategory]))
|
|
413
|
-
// for each of those keys perform side effects
|
|
414
|
-
seriesKeys.forEach(dataKey => {
|
|
415
|
-
newConfig.runtime.seriesLabels[dataKey] = dataKey
|
|
416
|
-
newConfig.runtime.seriesLabelsAll.push(dataKey)
|
|
417
|
-
newConfig.runtime.series.push({
|
|
418
|
-
dataKey,
|
|
419
|
-
type: series.type,
|
|
420
|
-
lineType: series.lineType,
|
|
421
|
-
originalDataKey: series.dataKey,
|
|
422
|
-
dynamicCategory: series.dynamicCategory,
|
|
423
|
-
tooltip: true
|
|
424
|
-
})
|
|
425
|
-
})
|
|
426
|
-
// return the series keys
|
|
427
|
-
return seriesKeys
|
|
428
|
-
} else {
|
|
429
|
-
newConfig.runtime.seriesLabels[series.dataKey] = series.name || series.label || series.dataKey
|
|
430
|
-
newConfig.runtime.seriesLabelsAll.push(series.name || series.dataKey)
|
|
431
|
-
// return the series keys
|
|
432
|
-
return [series.dataKey]
|
|
433
|
-
}
|
|
434
|
-
})
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (newConfig.visualizationType === 'Box Plot' && newConfig.series) {
|
|
438
|
-
const combinedData = filteredData || data
|
|
439
|
-
const groups = _.uniq(_.map(combinedData, newConfig.xAxis.dataKey))
|
|
440
|
-
const seriesKeys = _.map(newConfig.series, 'dataKey')
|
|
441
|
-
const plots: any[] = []
|
|
442
|
-
|
|
443
|
-
groups.forEach(g => {
|
|
444
|
-
seriesKeys.forEach(seriesKey => {
|
|
445
|
-
try {
|
|
446
|
-
if (!g) throw new Error('No groups resolved in box plots')
|
|
447
|
-
|
|
448
|
-
// Start handle operations on combinedData
|
|
449
|
-
const { count, sortedData } = _.chain(combinedData)
|
|
450
|
-
// Filter by xAxis data key
|
|
451
|
-
.filter(item => item[newConfig.xAxis.dataKey] === g)
|
|
452
|
-
// perform multiple operations on the filtered data
|
|
453
|
-
.thru(filteredData => ({
|
|
454
|
-
count: filteredData.length,
|
|
455
|
-
sortedData: _.map(filteredData, item => Number(item[seriesKey])).sort()
|
|
456
|
-
}))
|
|
457
|
-
// get the results from the chain
|
|
458
|
-
.value()
|
|
459
|
-
|
|
460
|
-
// ! - Notice d3.quantile doesn't work here, and we had to take a custom route.
|
|
461
|
-
const quartiles = getQuartiles(sortedData)
|
|
462
|
-
|
|
463
|
-
if (!sortedData) throw new Error('boxplots dont have data yet')
|
|
464
|
-
if (!plots) throw new Error('boxplots dont have plots yet')
|
|
465
|
-
|
|
466
|
-
const q1 = quartiles.q1
|
|
467
|
-
const q3 = quartiles.q3
|
|
468
|
-
|
|
469
|
-
const iqr = q3 - q1
|
|
470
|
-
const lowerBounds = q1 - 1.5 * iqr
|
|
471
|
-
const upperBounds = q3 + 1.5 * iqr
|
|
472
|
-
const filteredData = sortedData.filter(d => d <= upperBounds)
|
|
473
|
-
const max = d3.max(filteredData)
|
|
474
|
-
plots.push({
|
|
475
|
-
columnCategory: g,
|
|
476
|
-
columnMax: max,
|
|
477
|
-
columnThirdQuartile: _.round(q3, newConfig.dataFormat.roundTo),
|
|
478
|
-
columnMedian: Number(d3.median(sortedData)).toFixed(newConfig.dataFormat.roundTo),
|
|
479
|
-
columnFirstQuartile: _.round(q1, newConfig.dataFormat.roundTo),
|
|
480
|
-
columnMin: _.min(sortedData),
|
|
481
|
-
columnCount: count,
|
|
482
|
-
columnSd: Number(d3.deviation(sortedData)).toFixed(newConfig.dataFormat.roundTo),
|
|
483
|
-
columnMean: Number(d3.mean(sortedData)).toFixed(newConfig.dataFormat.roundTo),
|
|
484
|
-
columnIqr: _.round(iqr, newConfig.dataFormat.roundTo),
|
|
485
|
-
values: sortedData,
|
|
486
|
-
columnLowerBounds: lowerBounds,
|
|
487
|
-
columnUpperBounds: upperBounds,
|
|
488
|
-
columnOutliers: _.filter(sortedData, value => value < lowerBounds || value > upperBounds),
|
|
489
|
-
columnNonOutliers: _.filter(sortedData, value => value >= lowerBounds && value <= upperBounds)
|
|
490
|
-
})
|
|
491
|
-
} catch (e) {
|
|
492
|
-
console.error('COVE: ', e.message) // eslint-disable-line
|
|
493
|
-
}
|
|
494
|
-
})
|
|
495
|
-
})
|
|
496
|
-
// Generate a flat list of categories based on seriesKeys and groups
|
|
497
|
-
const categories =
|
|
498
|
-
seriesKeys.length > 1
|
|
499
|
-
? _.flatMap(groups, value => _.map(seriesKeys, key => `${_.capitalize(key)} - ${_.capitalize(value)}`))
|
|
500
|
-
: groups
|
|
501
|
-
|
|
502
|
-
newConfig.boxplot['categories'] = categories
|
|
503
|
-
newConfig.boxplot.plots = plots
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
if (newConfig.visualizationType === 'Combo' && newConfig.series) {
|
|
507
|
-
newConfig.runtime.barSeriesKeys = []
|
|
508
|
-
newConfig.runtime.lineSeriesKeys = []
|
|
509
|
-
newConfig.runtime.areaSeriesKeys = []
|
|
510
|
-
newConfig.runtime.forecastingSeriesKeys = []
|
|
511
|
-
|
|
512
|
-
newConfig.series.forEach(series => {
|
|
513
|
-
if (series.type === 'Area Chart') {
|
|
514
|
-
newConfig.runtime.areaSeriesKeys.push(series)
|
|
515
|
-
}
|
|
516
|
-
if (series.type === 'Forecasting') {
|
|
517
|
-
newConfig.runtime.forecastingSeriesKeys.push(series)
|
|
518
|
-
}
|
|
519
|
-
if (series.type === 'Bar' || series.type === 'Combo') {
|
|
520
|
-
newConfig.runtime.barSeriesKeys.push(series.dataKey)
|
|
521
|
-
}
|
|
522
|
-
if (
|
|
523
|
-
series.type === 'Line' ||
|
|
524
|
-
series.type === 'dashed-sm' ||
|
|
525
|
-
series.type === 'dashed-md' ||
|
|
526
|
-
series.type === 'dashed-lg'
|
|
527
|
-
) {
|
|
528
|
-
newConfig.runtime.lineSeriesKeys.push(series.dataKey)
|
|
529
|
-
}
|
|
530
|
-
if (series.type === 'Combo') {
|
|
531
|
-
series.type = 'Bar'
|
|
532
|
-
}
|
|
533
|
-
})
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
if (newConfig.visualizationType === 'Forecasting' && newConfig.series) {
|
|
537
|
-
newConfig.runtime.forecastingSeriesKeys = []
|
|
538
|
-
|
|
539
|
-
newConfig.series.forEach(series => {
|
|
540
|
-
if (series.type === 'Forecasting') {
|
|
541
|
-
newConfig.runtime.forecastingSeriesKeys.push(series)
|
|
542
|
-
}
|
|
543
|
-
})
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
if (newConfig.visualizationType === 'Area Chart' && newConfig.series) {
|
|
547
|
-
newConfig.runtime.areaSeriesKeys = []
|
|
548
|
-
|
|
549
|
-
newConfig.series.forEach(series => {
|
|
550
|
-
newConfig.runtime.areaSeriesKeys.push({ ...series, type: 'Area Chart' })
|
|
551
|
-
})
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
if (
|
|
555
|
-
(newConfig.visualizationType === 'Bar' && newConfig.orientation === 'horizontal') ||
|
|
556
|
-
['Deviation Bar', 'Paired Bar', 'Forest Plot'].includes(newConfig.visualizationType)
|
|
557
|
-
) {
|
|
558
|
-
newConfig.runtime.xAxis = newConfig.yAxis['yAxis'] ? newConfig.yAxis['yAxis'] : newConfig.yAxis
|
|
559
|
-
newConfig.runtime.yAxis = newConfig.xAxis['xAxis'] ? newConfig.xAxis['xAxis'] : newConfig.xAxis
|
|
560
|
-
newConfig.runtime.yAxis.labelOffset *= -1
|
|
561
|
-
|
|
562
|
-
newConfig.runtime.horizontal = false
|
|
563
|
-
newConfig.orientation = 'horizontal'
|
|
564
|
-
// remove after COVE supports categorical axis on horizonatal bars
|
|
565
|
-
newConfig.yAxis.type = newConfig.yAxis.type === 'categorical' ? 'linear' : newConfig.yAxis.type
|
|
566
|
-
} else if (
|
|
567
|
-
['Box Plot', 'Scatter Plot', 'Area Chart', 'Line', 'Forecasting'].includes(newConfig.visualizationType) &&
|
|
568
|
-
!checkLineToBarGraph()
|
|
569
|
-
) {
|
|
570
|
-
newConfig.runtime.xAxis = newConfig.xAxis
|
|
571
|
-
newConfig.runtime.yAxis = newConfig.yAxis
|
|
572
|
-
newConfig.runtime.horizontal = false
|
|
573
|
-
newConfig.orientation = 'vertical'
|
|
574
|
-
} else {
|
|
575
|
-
newConfig.runtime.xAxis = newConfig.xAxis
|
|
576
|
-
newConfig.runtime.yAxis = newConfig.yAxis
|
|
577
|
-
newConfig.runtime.horizontal = false
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
newConfig.runtime.uniqueId = Date.now()
|
|
581
|
-
newConfig.runtime.editorErrorMessage =
|
|
582
|
-
newConfig.visualizationType === 'Pie' && !newConfig.yAxis.dataKey
|
|
583
|
-
? 'Data Key property in Y Axis section must be set for pie charts.'
|
|
584
|
-
: ''
|
|
585
|
-
|
|
586
|
-
// Sankey Description box error message
|
|
587
|
-
newConfig.runtime.editorErrorMessage = ''
|
|
588
|
-
|
|
589
|
-
if (newConfig.legend.seriesHighlight?.length) {
|
|
590
|
-
setSeriesHighlight(newConfig.legend?.seriesHighlight)
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
setConfig(newConfig)
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// Sorts data series for horizontal bar charts
|
|
597
|
-
const sortData = (a, b) => {
|
|
598
|
-
let sortKey =
|
|
599
|
-
config.visualizationType === 'Bar' && config.visualizationSubType === 'horizontal'
|
|
600
|
-
? config.xAxis.dataKey
|
|
601
|
-
: config.yAxis.sortKey
|
|
602
|
-
let aData = parseFloat(a[sortKey])
|
|
603
|
-
let bData = parseFloat(b[sortKey])
|
|
604
|
-
|
|
605
|
-
if (aData < bData) {
|
|
606
|
-
return config.sortData === 'ascending' ? 1 : -1
|
|
607
|
-
} else if (aData > bData) {
|
|
608
|
-
return config.sortData === 'ascending' ? -1 : 1
|
|
609
|
-
} else {
|
|
610
|
-
return 0
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// Observes changes to outermost container and changes viewport size in state
|
|
615
|
-
const resizeObserver = new ResizeObserver(entries => {
|
|
616
|
-
for (let entry of entries) {
|
|
617
|
-
let { width, height } = entry.contentRect
|
|
618
|
-
const svgMarginWidth = 15
|
|
619
|
-
const editorWidth = 350
|
|
620
|
-
|
|
621
|
-
width = isEditor ? width - editorWidth : width
|
|
622
|
-
|
|
623
|
-
const newViewport = getViewport(width)
|
|
624
|
-
|
|
625
|
-
setCurrentViewport(newViewport)
|
|
626
|
-
|
|
627
|
-
if (entry.target.dataset.lollipop === 'true') {
|
|
628
|
-
width = width - 2.5
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
width = width - svgMarginWidth
|
|
632
|
-
|
|
633
|
-
setDimensions([width, height])
|
|
634
|
-
}
|
|
635
|
-
})
|
|
636
|
-
|
|
637
|
-
const outerContainerRef = useCallback(node => {
|
|
638
|
-
if (node !== null) {
|
|
639
|
-
resizeObserver.observe(node)
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
setContainer(node)
|
|
643
|
-
}, []) // eslint-disable-line
|
|
644
|
-
|
|
645
|
-
const isEmpty = obj => {
|
|
646
|
-
return Object.keys(obj).length === 0
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// Load data when component first mounts
|
|
650
|
-
useEffect(() => {
|
|
651
|
-
loadConfig()
|
|
652
|
-
}, [configObj?.data?.length ? configObj.data : null]) // eslint-disable-line
|
|
653
|
-
|
|
654
|
-
useEffect(() => {
|
|
655
|
-
reloadURLData()
|
|
656
|
-
}, [JSON.stringify(config.filters)])
|
|
657
|
-
|
|
658
|
-
/**
|
|
659
|
-
* When cove has a config and container ref publish the cove_loaded event.
|
|
660
|
-
*/
|
|
661
|
-
useEffect(() => {
|
|
662
|
-
if (container && !isEmpty(config) && !coveLoadedEventRan) {
|
|
663
|
-
publish('cove_loaded', { config: config })
|
|
664
|
-
setCoveLoadedEventRan(true)
|
|
665
|
-
}
|
|
666
|
-
}, [container, config]) // eslint-disable-line
|
|
667
|
-
|
|
668
|
-
/**
|
|
669
|
-
* Handles filter change events outside of COVE
|
|
670
|
-
* Updates externalFilters state
|
|
671
|
-
* Another useEffect listens to externalFilterChanges and updates the config.
|
|
672
|
-
*/
|
|
673
|
-
useEffect(() => {
|
|
674
|
-
const handleFilterData = e => {
|
|
675
|
-
let tmp: any[] = []
|
|
676
|
-
tmp.push(e.detail)
|
|
677
|
-
setExternalFilters(tmp)
|
|
678
|
-
}
|
|
52
|
+
if (!isUpdateNeeded) return
|
|
679
53
|
|
|
680
|
-
|
|
54
|
+
const finalUrl = `${dataUrl.origin}${dataUrl.pathname}${new URLSearchParams(qsParams)}`
|
|
55
|
+
const ext = getFileExtension(finalUrl)
|
|
56
|
+
const data = await fetchAndParseData(finalUrl, ext)
|
|
681
57
|
|
|
682
|
-
|
|
683
|
-
|
|
58
|
+
setConfig(prev => ({
|
|
59
|
+
...prev,
|
|
60
|
+
data: { ...data, urlFiltered: true },
|
|
61
|
+
runtimeDataUrl: finalUrl,
|
|
62
|
+
formattedData: { ...data, urlFiltered: true }
|
|
63
|
+
}))
|
|
684
64
|
}
|
|
685
65
|
}, [config])
|
|
686
66
|
|
|
687
|
-
/**
|
|
688
|
-
* Handles changes to externalFilters
|
|
689
|
-
* For some reason e.detail is returning [order: "asc"] even though
|
|
690
|
-
* we're not passing that in. The code here checks for an active prop instead of an empty array.
|
|
691
|
-
*/
|
|
692
67
|
useEffect(() => {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
delete configCopy['filters']
|
|
699
|
-
setConfig(configCopy)
|
|
700
|
-
setFilteredData(filterVizData(externalFilters, excludedData))
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
if (
|
|
705
|
-
externalFilters &&
|
|
706
|
-
externalFilters.length > 0 &&
|
|
707
|
-
externalFilters.length > 0 &&
|
|
708
|
-
externalFilters[0].hasOwnProperty('active')
|
|
709
|
-
) {
|
|
710
|
-
let newConfigHere = { ...config, filters: externalFilters }
|
|
711
|
-
setConfig(newConfigHere)
|
|
712
|
-
setFilteredData(filterVizData(externalFilters, excludedData))
|
|
713
|
-
}
|
|
714
|
-
}, [externalFilters]) // eslint-disable-line
|
|
715
|
-
|
|
716
|
-
// This will set the bump chart's default scaling type to date-time
|
|
717
|
-
useEffect(() => {
|
|
718
|
-
if (['Bump Chart'].includes(config.visualizationType)) {
|
|
719
|
-
setConfig({
|
|
720
|
-
...config,
|
|
721
|
-
xAxis: {
|
|
722
|
-
...config.xAxis,
|
|
723
|
-
type: 'date-time'
|
|
724
|
-
}
|
|
725
|
-
})
|
|
726
|
-
}
|
|
727
|
-
}, [config.visualizationType])
|
|
728
|
-
|
|
729
|
-
// Generates color palette to pass to child chart component
|
|
730
|
-
useEffect(() => {
|
|
731
|
-
if (stateData && config.xAxis && config.runtime?.seriesKeys) {
|
|
732
|
-
const configPalette = ['Paired Bar', 'Deviation Bar'].includes(config.visualizationType)
|
|
733
|
-
? config.twoColor.palette
|
|
734
|
-
: config.palette
|
|
735
|
-
const allPalettes: Record<string, string[]> = { ...colorPalettes, ...twoColorPalette }
|
|
736
|
-
let palette = config.customColors || allPalettes[configPalette]
|
|
737
|
-
let numberOfKeys = config.runtime.seriesKeys.length
|
|
738
|
-
let newColorScale
|
|
739
|
-
|
|
740
|
-
while (numberOfKeys > palette.length) {
|
|
741
|
-
palette = palette.concat(palette)
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
palette = palette.slice(0, numberOfKeys)
|
|
68
|
+
const load = async () => {
|
|
69
|
+
setIsLoading(true)
|
|
70
|
+
try {
|
|
71
|
+
const loadedConfig = await loadConfig(configUrl)
|
|
72
|
+
const data = await loadDataFromConfig(loadedConfig)
|
|
745
73
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
range: palette,
|
|
750
|
-
unknown: null
|
|
74
|
+
setConfig({
|
|
75
|
+
...loadedConfig,
|
|
76
|
+
data
|
|
751
77
|
})
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
if (config && stateData && config.sortData) {
|
|
758
|
-
stateData.sort(sortData)
|
|
759
|
-
}
|
|
760
|
-
}, [config, stateData]) // eslint-disable-line
|
|
761
|
-
|
|
762
|
-
// Called on legend click, highlights/unhighlights the data series with the given label
|
|
763
|
-
const highlight = (label: Label) => {
|
|
764
|
-
// If we're highlighting all the series, reset them
|
|
765
|
-
if (seriesHighlight.length + 1 === config.runtime.seriesKeys.length && config.visualizationType !== 'Forecasting') {
|
|
766
|
-
highlightReset()
|
|
767
|
-
return
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
const newSeriesHighlight = [...seriesHighlight]
|
|
771
|
-
|
|
772
|
-
let newHighlight = label.datum
|
|
773
|
-
if (config.runtime.seriesLabels) {
|
|
774
|
-
config.runtime.seriesKeys.forEach(key => {
|
|
775
|
-
if (config.runtime.seriesLabels[key] === label.datum) {
|
|
776
|
-
newHighlight = key
|
|
777
|
-
}
|
|
778
|
-
})
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
if (newSeriesHighlight.indexOf(newHighlight) !== -1) {
|
|
782
|
-
newSeriesHighlight.splice(newSeriesHighlight.indexOf(newHighlight), 1)
|
|
783
|
-
} else {
|
|
784
|
-
newSeriesHighlight.push(newHighlight)
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
/**
|
|
788
|
-
* pushDataKeyBySeriesName
|
|
789
|
-
* - pushes series.dataKey into the series highlight based on the found series.name
|
|
790
|
-
* @param {String} value
|
|
791
|
-
*/
|
|
792
|
-
// const pushDataKeyBySeriesName = value => {
|
|
793
|
-
// let matchingSeries = config.series.filter(series => series.name === value.text)
|
|
794
|
-
// if (matchingSeries?.length > 0) {
|
|
795
|
-
// newSeriesHighlight.push(matchingSeries[0].dataKey)
|
|
796
|
-
// }
|
|
797
|
-
// }
|
|
798
|
-
|
|
799
|
-
// pushDataKeyBySeriesName(label)
|
|
800
|
-
|
|
801
|
-
setSeriesHighlight(newSeriesHighlight)
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
// Called on reset button click, unhighlights all data series
|
|
805
|
-
const highlightReset = () => {
|
|
806
|
-
try {
|
|
807
|
-
const legend = legendRef.current
|
|
808
|
-
if (!legend) throw new Error('No legend available to set previous focus on.')
|
|
809
|
-
legend.focus()
|
|
810
|
-
} catch (e) {
|
|
811
|
-
console.error('COVE:', e.message)
|
|
812
|
-
}
|
|
813
|
-
setSeriesHighlight([])
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
const section = config.orientation === 'horizontal' ? 'yAxis' : 'xAxis'
|
|
817
|
-
|
|
818
|
-
const parseDate = (dateString, showError = true) => {
|
|
819
|
-
let date = timeParse(config.runtime[section].dateParseFormat)(dateString)
|
|
820
|
-
if (!date) {
|
|
821
|
-
if (showError) {
|
|
822
|
-
config.runtime.editorErrorMessage = `Error parsing date "${dateString}". Try reviewing your data and date parse settings in the X Axis section.`
|
|
823
|
-
}
|
|
824
|
-
return new Date()
|
|
825
|
-
} else {
|
|
826
|
-
return date
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
const formatDate = (date, i, ticks) => {
|
|
831
|
-
let formattedDate = timeFormat(config.runtime[section].dateDisplayFormat)(date)
|
|
832
|
-
// Handle the case where all months work with '%b.' except for May
|
|
833
|
-
if (config.runtime[section].dateDisplayFormat?.includes('%b.') && formattedDate.includes('May.')) {
|
|
834
|
-
formattedDate = formattedDate.replace(/May\./g, 'May')
|
|
835
|
-
}
|
|
836
|
-
// Show years only once
|
|
837
|
-
if (config.xAxis.showYearsOnce && config.runtime[section].dateDisplayFormat?.includes('%Y') && ticks) {
|
|
838
|
-
const prevDate = ticks[i - 1] ? ticks[i - 1].value : null
|
|
839
|
-
const prevFormattedDate = timeFormat(config.runtime[section].dateDisplayFormat)(prevDate)
|
|
840
|
-
const year = formattedDate.match(/\d{4}/)
|
|
841
|
-
const prevYear = prevFormattedDate.match(/\d{4}/)
|
|
842
|
-
if (year && prevYear && year[0] === prevYear[0]) {
|
|
843
|
-
formattedDate = formattedDate.replace(year, '')
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
return formattedDate
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
const formatTooltipsDate = date => {
|
|
850
|
-
return timeFormat(config.tooltips.dateDisplayFormat)(date)
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// Format numeric data based on settings in config OR from passed in settings for Additional Columns
|
|
854
|
-
// - use only for old horizontal data - newer formatNumber is in helper/formatNumber
|
|
855
|
-
// TODO: we should combine various formatNumber functions across this project.
|
|
856
|
-
// TODO suggestion: pass all options as object key/values to allow for more flexibility
|
|
857
|
-
const formatNumber = (
|
|
858
|
-
num,
|
|
859
|
-
axis,
|
|
860
|
-
shouldAbbreviate = false,
|
|
861
|
-
addColPrefix,
|
|
862
|
-
addColSuffix,
|
|
863
|
-
addColRoundTo,
|
|
864
|
-
{ index, length } = { index: null, length: null }
|
|
865
|
-
) => {
|
|
866
|
-
// if num is NaN return num
|
|
867
|
-
if (isNaN(num) || !num) return num
|
|
868
|
-
// Check if the input number is negative
|
|
869
|
-
const isNegative = num < 0
|
|
870
|
-
|
|
871
|
-
if (axis === undefined || !axis) axis = 'left'
|
|
872
|
-
|
|
873
|
-
// If the input number is negative, take the absolute value
|
|
874
|
-
if (isNegative) {
|
|
875
|
-
num = Math.abs(num)
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
// destructure dataFormat values
|
|
879
|
-
let {
|
|
880
|
-
dataFormat: {
|
|
881
|
-
commas,
|
|
882
|
-
abbreviated,
|
|
883
|
-
roundTo,
|
|
884
|
-
prefix,
|
|
885
|
-
suffix,
|
|
886
|
-
rightRoundTo,
|
|
887
|
-
bottomRoundTo,
|
|
888
|
-
rightPrefix,
|
|
889
|
-
rightSuffix,
|
|
890
|
-
bottomPrefix,
|
|
891
|
-
bottomSuffix,
|
|
892
|
-
bottomAbbreviated,
|
|
893
|
-
onlyShowTopPrefixSuffix
|
|
894
|
-
}
|
|
895
|
-
} = config
|
|
896
|
-
|
|
897
|
-
// check if value contains comma and remove it. later will add comma below.
|
|
898
|
-
if (String(num).indexOf(',') !== -1) num = num.replaceAll(',', '')
|
|
899
|
-
|
|
900
|
-
let original = num
|
|
901
|
-
let stringFormattingOptions: any = {
|
|
902
|
-
useGrouping: commas ? true : false // for old chart data table to work right cant just leave this to undefined
|
|
903
|
-
}
|
|
904
|
-
if (axis === 'left' || axis === undefined) {
|
|
905
|
-
let roundToPlace
|
|
906
|
-
if (addColRoundTo !== undefined) {
|
|
907
|
-
// if its an Additional Column
|
|
908
|
-
roundToPlace = addColRoundTo ? Number(addColRoundTo) : 0
|
|
909
|
-
} else {
|
|
910
|
-
roundToPlace = roundTo ? Number(roundTo) : 0
|
|
911
|
-
}
|
|
912
|
-
stringFormattingOptions = {
|
|
913
|
-
useGrouping: addColRoundTo ? true : config.dataFormat.commas ? true : false,
|
|
914
|
-
minimumFractionDigits: roundToPlace,
|
|
915
|
-
maximumFractionDigits: roundToPlace
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
if (axis === 'right') {
|
|
920
|
-
stringFormattingOptions = {
|
|
921
|
-
useGrouping: config.dataFormat.rightCommas ? true : false,
|
|
922
|
-
minimumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0,
|
|
923
|
-
maximumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
const resolveBottomTickRounding = () => {
|
|
928
|
-
if (config.forestPlot.type === 'Logarithmic' && !bottomRoundTo) return 2
|
|
929
|
-
if (Number(bottomRoundTo)) return Number(bottomRoundTo)
|
|
930
|
-
return 0
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
if (axis === 'bottom') {
|
|
934
|
-
stringFormattingOptions = {
|
|
935
|
-
useGrouping: config.dataFormat.bottomCommas ? true : false,
|
|
936
|
-
minimumFractionDigits: resolveBottomTickRounding(),
|
|
937
|
-
maximumFractionDigits: resolveBottomTickRounding()
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
num = numberFromString(num)
|
|
942
|
-
|
|
943
|
-
if (isNaN(num)) {
|
|
944
|
-
config.runtime.editorErrorMessage = `Unable to parse number from data ${original}. Try reviewing your data and selections in the Data Series section.`
|
|
945
|
-
return original
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
if (!config.dataFormat) return num
|
|
949
|
-
if (config.dataCutoff) {
|
|
950
|
-
let cutoff = numberFromString(config.dataCutoff)
|
|
951
|
-
|
|
952
|
-
if (num < cutoff) {
|
|
953
|
-
num = cutoff
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// When we're formatting the left axis
|
|
958
|
-
// Use commas also updates bars and the data table
|
|
959
|
-
// We can't use commas when we're formatting the dataFormatted number
|
|
960
|
-
// Example: commas -> 12,000; abbreviated -> 12k (correct); abbreviated & commas -> 12 (incorrect)
|
|
961
|
-
//
|
|
962
|
-
// Edge case for small numbers with decimals
|
|
963
|
-
// - if roundTo undefined which means it is blank, then do not round
|
|
964
|
-
|
|
965
|
-
if (
|
|
966
|
-
(axis === 'left' && commas && abbreviated && shouldAbbreviate) ||
|
|
967
|
-
(axis === 'bottom' && commas && abbreviated && shouldAbbreviate)
|
|
968
|
-
) {
|
|
969
|
-
num = num // eslint-disable-line
|
|
970
|
-
} else {
|
|
971
|
-
num = num.toLocaleString('en-US', stringFormattingOptions)
|
|
972
|
-
}
|
|
973
|
-
let result = ''
|
|
974
|
-
|
|
975
|
-
if (abbreviated && axis === 'left' && shouldAbbreviate) {
|
|
976
|
-
num = abbreviateNumber(parseFloat(num))
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
if (bottomAbbreviated && axis === 'bottom' && shouldAbbreviate) {
|
|
980
|
-
num = abbreviateNumber(parseFloat(num))
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
if (addColPrefix && axis === 'left') {
|
|
984
|
-
result = addColPrefix + result
|
|
985
|
-
} else {
|
|
986
|
-
// if onlyShowTopPrefixSuffix only show top prefix
|
|
987
|
-
const suppressAllButLast = onlyShowTopPrefixSuffix && length - 1 !== index
|
|
988
|
-
if (prefix && axis === 'left' && !suppressAllButLast) {
|
|
989
|
-
result += prefix
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
if (rightPrefix && axis === 'right') {
|
|
994
|
-
result += rightPrefix
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
if (bottomPrefix && axis === 'bottom') {
|
|
998
|
-
result += bottomPrefix
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
// combine prefix and num
|
|
1002
|
-
result += num
|
|
1003
|
-
|
|
1004
|
-
if (addColSuffix && axis === 'left') {
|
|
1005
|
-
result += addColSuffix
|
|
1006
|
-
} else {
|
|
1007
|
-
if (suffix && axis === 'left' && !onlyShowTopPrefixSuffix) {
|
|
1008
|
-
result += suffix
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
if (rightSuffix && axis === 'right') {
|
|
1013
|
-
result += rightSuffix
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
if (bottomSuffix && axis === 'bottom') {
|
|
1017
|
-
result += bottomSuffix
|
|
1018
|
-
}
|
|
1019
|
-
if (isNegative) {
|
|
1020
|
-
result = '-' + result
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
return String(result)
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
const missingRequiredSections = () => {
|
|
1027
|
-
if (config.visualizationType === 'Sankey') return false // skip checks for now
|
|
1028
|
-
if (config.visualizationType === 'Forecasting') return false // skip required checks for now.
|
|
1029
|
-
if (config.visualizationType === 'Forest Plot') return false // skip required checks for now.
|
|
1030
|
-
if (config.visualizationType === 'Pie') {
|
|
1031
|
-
if (undefined === config?.yAxis.dataKey) {
|
|
1032
|
-
return true
|
|
1033
|
-
}
|
|
1034
|
-
} else {
|
|
1035
|
-
if ((undefined === config?.series || false === config?.series.length > 0) && !config?.dynamicSeries) {
|
|
1036
|
-
return true
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
if (!config.xAxis.dataKey) {
|
|
1041
|
-
return true
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
return false
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// used for Additional Column
|
|
1048
|
-
const displayDataAsText = (value, columnName) => {
|
|
1049
|
-
if (value === null || value === '' || value === undefined) {
|
|
1050
|
-
return ''
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
if (typeof value === 'string' && value.length > 0 && config.legend.type === 'equalnumber') {
|
|
1054
|
-
return value
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
let formattedValue = value
|
|
1058
|
-
|
|
1059
|
-
let columnObj //= config.columns[columnName]
|
|
1060
|
-
// config.columns not an array but a hash of objects
|
|
1061
|
-
if (Object.keys(config.columns).length > 0) {
|
|
1062
|
-
Object.keys(config.columns).forEach(function (key) {
|
|
1063
|
-
var column = config.columns[key]
|
|
1064
|
-
// add if not the index AND it is enabled to be added to data table
|
|
1065
|
-
if (column.name === columnName) {
|
|
1066
|
-
columnObj = column
|
|
1067
|
-
}
|
|
1068
|
-
})
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
if (columnObj === undefined) {
|
|
1072
|
-
// then use left axis config
|
|
1073
|
-
columnObj = config.type === 'chart' ? config.dataFormat : config.primary
|
|
1074
|
-
// NOTE: Left Value Axis uses different names
|
|
1075
|
-
// so map them below so the code below works
|
|
1076
|
-
// - copy commas to useCommas to work below
|
|
1077
|
-
columnObj['useCommas'] = columnObj.commas
|
|
1078
|
-
// - copy roundTo to roundToPlace to work below
|
|
1079
|
-
columnObj['roundToPlace'] = columnObj.roundTo ? columnObj.roundTo : ''
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
if (columnObj) {
|
|
1083
|
-
// If value is a number, apply specific formattings
|
|
1084
|
-
let hasDecimal = false
|
|
1085
|
-
let decimalPoint = 0
|
|
1086
|
-
if (Number(value)) {
|
|
1087
|
-
if (columnObj.roundToPlace >= 0) {
|
|
1088
|
-
hasDecimal = columnObj.roundToPlace ? columnObj.roundToPlace !== '' || columnObj.roundToPlace !== null : false
|
|
1089
|
-
decimalPoint = columnObj.roundToPlace ? Number(columnObj.roundToPlace) : 0
|
|
1090
|
-
|
|
1091
|
-
// Rounding
|
|
1092
|
-
if (columnObj.hasOwnProperty('roundToPlace') && hasDecimal) {
|
|
1093
|
-
formattedValue = Number(value).toFixed(decimalPoint)
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
if (columnObj.hasOwnProperty('useCommas') && columnObj.useCommas === true) {
|
|
1098
|
-
// Formats number to string with commas - allows up to 5 decimal places, if rounding is not defined.
|
|
1099
|
-
// Otherwise, uses the rounding value set at 'columnObj.roundToPlace'.
|
|
1100
|
-
formattedValue = Number(value).toLocaleString('en-US', {
|
|
1101
|
-
style: 'decimal',
|
|
1102
|
-
minimumFractionDigits: hasDecimal ? decimalPoint : 0,
|
|
1103
|
-
maximumFractionDigits: hasDecimal ? decimalPoint : 5
|
|
1104
|
-
})
|
|
1105
|
-
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('Failed to load configuration or data', error)
|
|
80
|
+
} finally {
|
|
81
|
+
setIsLoading(false)
|
|
1106
82
|
}
|
|
1107
|
-
|
|
1108
|
-
// add prefix and suffix if set
|
|
1109
|
-
formattedValue = (columnObj.prefix || '') + formattedValue + (columnObj.suffix || '')
|
|
1110
83
|
}
|
|
1111
84
|
|
|
1112
|
-
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
const Confirm = () => {
|
|
1116
|
-
const confirmDone = e => {
|
|
1117
|
-
if (e) {
|
|
1118
|
-
e.preventDefault()
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
let newConfig = { ...config }
|
|
1122
|
-
delete newConfig.newViz
|
|
85
|
+
load()
|
|
86
|
+
}, [configUrl, loadConfig])
|
|
1123
87
|
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
position: 'relative',
|
|
1129
|
-
height: '100vh',
|
|
1130
|
-
width: '100%',
|
|
1131
|
-
display: 'flex',
|
|
1132
|
-
justifyContent: 'center',
|
|
1133
|
-
alignItems: 'center',
|
|
1134
|
-
gridArea: 'content'
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
return (
|
|
1138
|
-
<section className='waiting' style={styles}>
|
|
1139
|
-
<section className='waiting-container'>
|
|
1140
|
-
<h3>Finish Configuring</h3>
|
|
1141
|
-
<p>Set all required options to the left and confirm below to display a preview of the chart.</p>
|
|
1142
|
-
<Button
|
|
1143
|
-
className='btn btn-primary'
|
|
1144
|
-
style={{ margin: '1em auto' }}
|
|
1145
|
-
disabled={missingRequiredSections()}
|
|
1146
|
-
onClick={e => confirmDone(e)}
|
|
1147
|
-
>
|
|
1148
|
-
I'm Done
|
|
1149
|
-
</Button>
|
|
1150
|
-
</section>
|
|
1151
|
-
</section>
|
|
1152
|
-
)
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
const Error = () => {
|
|
1156
|
-
const styles = {
|
|
1157
|
-
position: 'absolute',
|
|
1158
|
-
background: 'white',
|
|
1159
|
-
zIndex: '999',
|
|
1160
|
-
height: '100vh',
|
|
1161
|
-
width: '100%',
|
|
1162
|
-
display: 'flex',
|
|
1163
|
-
justifyContent: 'center',
|
|
1164
|
-
alignItems: 'center',
|
|
1165
|
-
gridArea: 'content'
|
|
1166
|
-
}
|
|
1167
|
-
return (
|
|
1168
|
-
<section className='waiting' style={styles}>
|
|
1169
|
-
<section className='waiting-container'>
|
|
1170
|
-
<h3>Error With Configuration</h3>
|
|
1171
|
-
<p>{config.runtime.editorErrorMessage}</p>
|
|
1172
|
-
</section>
|
|
1173
|
-
</section>
|
|
1174
|
-
)
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
// this is passed DOWN into the various components
|
|
1178
|
-
// then they do a lookup based on the bin number as index into here (TT)
|
|
1179
|
-
const applyLegendToRow = rowObj => {
|
|
1180
|
-
try {
|
|
1181
|
-
if (!rowObj) throw new Error('COVE: No rowObj in applyLegendToRow')
|
|
1182
|
-
// Navigation map
|
|
1183
|
-
if ('navigation' === config.type) {
|
|
1184
|
-
let mapColorPalette = colorPalettes[config.color] || colorPalettes['bluegreenreverse']
|
|
1185
|
-
return generateColorsArray(mapColorPalette[3])
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
// Fail state
|
|
1189
|
-
return generateColorsArray()
|
|
1190
|
-
} catch (e) {
|
|
1191
|
-
console.error('COVE: ', e) // eslint-disable-line
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (!_.isEqual(prevFiltersRef.current, config.filters)) {
|
|
90
|
+
prevFiltersRef.current = config.filters
|
|
91
|
+
reloadFilteredData()
|
|
1192
92
|
}
|
|
1193
|
-
}
|
|
93
|
+
}, [config.filters, reloadFilteredData])
|
|
1194
94
|
|
|
1195
|
-
|
|
1196
|
-
const clean = data => {
|
|
1197
|
-
// cleaning is deleting data we need in forecasting charts.
|
|
1198
|
-
if (!Array.isArray(data)) return []
|
|
1199
|
-
if (config.visualizationType === 'Forecasting') return data
|
|
1200
|
-
if (config.series?.some(series => !!series.dynamicCategory)) return data
|
|
1201
|
-
return config?.xAxis?.dataKey ? transform.cleanData(data, config.xAxis.dataKey) : data
|
|
1202
|
-
}
|
|
95
|
+
if (isLoading) return <Loading />
|
|
1203
96
|
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
const data = filteredData || excludedData
|
|
1207
|
-
const dynamicSeries = config.series.find(series => !!series.dynamicCategory)
|
|
1208
|
-
if (!dynamicSeries) return data
|
|
1209
|
-
const usedColumns = Object.values(config.columns)
|
|
1210
|
-
.filter(col => col.dataTable)
|
|
1211
|
-
.map(col => col.name)
|
|
1212
|
-
.concat([dynamicSeries.dynamicCategory, dynamicSeries.dataKey])
|
|
1213
|
-
if (config.xAxis?.dataKey) usedColumns.push(config.xAxis.dataKey)
|
|
1214
|
-
return data.map(d => _.pick(d, usedColumns))
|
|
1215
|
-
}
|
|
97
|
+
return <CdcChart config={config} isEditor={isEditor} isDebug={isDebug} />
|
|
98
|
+
}
|
|
1216
99
|
|
|
1217
|
-
|
|
1218
|
-
const tableConfig: TableConfig = _.cloneDeep(config)
|
|
1219
|
-
const dynamicSeries = tableConfig.series.find(series => !!series.dynamicCategory)
|
|
1220
|
-
if (dynamicSeries) {
|
|
1221
|
-
const pivot: Pivot = { columnName: dynamicSeries.dynamicCategory, valueColumns: [dynamicSeries.dataKey] }
|
|
1222
|
-
tableConfig.table.pivot = pivot
|
|
1223
|
-
}
|
|
1224
|
-
return tableConfig
|
|
1225
|
-
}
|
|
100
|
+
export default CdcChartWrapper
|
|
1226
101
|
|
|
1227
|
-
|
|
1228
|
-
|
|
102
|
+
const parseCsv = (responseText: string, delimiter = '|') => {
|
|
103
|
+
const sanitizedText = responseText.replace(/(".*?")|,/g, (...m) => m[1] || delimiter).replace(/["]+/g, '')
|
|
1229
104
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
105
|
+
return Papa.parse(sanitizedText, {
|
|
106
|
+
header: true,
|
|
107
|
+
dynamicTyping: true,
|
|
108
|
+
skipEmptyLines: true,
|
|
109
|
+
delimiter
|
|
110
|
+
}).data
|
|
111
|
+
}
|
|
1234
112
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
const
|
|
1238
|
-
if (
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
}
|
|
113
|
+
const fetchAndParseData = async (url: string, ext: string) => {
|
|
114
|
+
try {
|
|
115
|
+
const response = await fetch(url)
|
|
116
|
+
if (ext === 'csv' || isSolrCsv(url)) {
|
|
117
|
+
const responseText = await response.text()
|
|
118
|
+
return parseCsv(responseText)
|
|
119
|
+
} else if (ext === 'json' || isSolrJson(url)) {
|
|
120
|
+
return await response.json()
|
|
1244
121
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
if (config.brush?.active && dashboardConfig?.type === 'dashboard' && (!isLegendOnBottom || legend.hide))
|
|
1249
|
-
classes.push('dashboard-brush')
|
|
1250
|
-
classes.push(...contentClasses)
|
|
1251
|
-
return classes
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(`Error parsing URL: ${url}`, error)
|
|
124
|
+
return []
|
|
1252
125
|
}
|
|
126
|
+
}
|
|
1253
127
|
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
if (config.isResponsiveTicks) classes.push('subtext--responsive-ticks ')
|
|
1259
|
-
if (config.brush?.active && !isLegendOnBottom) classes.push('subtext--brush-active ')
|
|
1260
|
-
if (config.brush?.active && config.legend.hide) classes.push('subtext--brush-active ')
|
|
1261
|
-
return classes
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
if (!loading) {
|
|
1265
|
-
const tableLink = (
|
|
1266
|
-
<a href={`#data-table-${config.dataKey}`} className='margin-left-href'>
|
|
1267
|
-
{config.dataKey} (Go to Table)
|
|
1268
|
-
</a>
|
|
1269
|
-
)
|
|
1270
|
-
body = (
|
|
1271
|
-
<>
|
|
1272
|
-
{isEditor && <EditorPanel />}
|
|
1273
|
-
<Layout.Responsive isEditor={isEditor}>
|
|
1274
|
-
{config.newViz && <Confirm />}
|
|
1275
|
-
{undefined === config.newViz && isEditor && config.runtime && config.runtime?.editorErrorMessage && <Error />}
|
|
1276
|
-
{!missingRequiredSections() && !config.newViz && (
|
|
1277
|
-
<div
|
|
1278
|
-
className={`cdc-chart-inner-container cove-component__content type-${makeClassName(
|
|
1279
|
-
config.visualizationType
|
|
1280
|
-
)}`}
|
|
1281
|
-
aria-label={handleChartAriaLabels(config)}
|
|
1282
|
-
tabIndex={0}
|
|
1283
|
-
>
|
|
1284
|
-
<Title
|
|
1285
|
-
showTitle={config.showTitle}
|
|
1286
|
-
isDashboard={isDashboard}
|
|
1287
|
-
title={title}
|
|
1288
|
-
superTitle={config.superTitle}
|
|
1289
|
-
classes={['chart-title', `${config.theme}`, 'cove-component__header']}
|
|
1290
|
-
style={undefined}
|
|
1291
|
-
/>
|
|
1292
|
-
|
|
1293
|
-
{/* Visualization Wrapper */}
|
|
1294
|
-
<div className={getChartWrapperClasses().join(' ')}>
|
|
1295
|
-
{/* Intro Text/Message */}
|
|
1296
|
-
{config?.introText && config.visualizationType !== 'Spark Line' && (
|
|
1297
|
-
<section className={`introText `}>{parse(config.introText)}</section>
|
|
1298
|
-
)}
|
|
1299
|
-
|
|
1300
|
-
{/* Filters */}
|
|
1301
|
-
{config.filters && !externalFilters && config.visualizationType !== 'Spark Line' && (
|
|
1302
|
-
<Filters
|
|
1303
|
-
config={config}
|
|
1304
|
-
setConfig={setConfig}
|
|
1305
|
-
setFilteredData={setFilteredData}
|
|
1306
|
-
filteredData={filteredData}
|
|
1307
|
-
excludedData={excludedData}
|
|
1308
|
-
filterData={filterVizData}
|
|
1309
|
-
dimensions={dimensions}
|
|
1310
|
-
/>
|
|
1311
|
-
)}
|
|
1312
|
-
<SkipTo skipId={handleChartTabbing(config, legendId)} skipMessage='Skip Over Chart Container' />
|
|
1313
|
-
{config.annotations?.length > 0 && (
|
|
1314
|
-
<SkipTo
|
|
1315
|
-
skipId={handleChartTabbing(config, legendId)}
|
|
1316
|
-
skipMessage={`Skip over annotations`}
|
|
1317
|
-
key={`skip-annotations`}
|
|
1318
|
-
/>
|
|
1319
|
-
)}
|
|
1320
|
-
<LegendWrapper>
|
|
1321
|
-
<div
|
|
1322
|
-
className={
|
|
1323
|
-
legend.hide || isLegendWrapViewport(currentViewport)
|
|
1324
|
-
? 'w-100'
|
|
1325
|
-
: legend.position === 'bottom' || legend.position === 'top' || visualizationType === 'Sankey'
|
|
1326
|
-
? 'w-100'
|
|
1327
|
-
: 'w-75'
|
|
1328
|
-
}
|
|
1329
|
-
>
|
|
1330
|
-
{/* All charts with LinearChart */}
|
|
1331
|
-
{!['Spark Line', 'Line', 'Sankey', 'Pie', 'Sankey'].includes(config.visualizationType) && (
|
|
1332
|
-
<div ref={parentRef} style={{ width: `100%` }}>
|
|
1333
|
-
<ParentSize>
|
|
1334
|
-
{parent => (
|
|
1335
|
-
<LinearChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
|
|
1336
|
-
)}
|
|
1337
|
-
</ParentSize>
|
|
1338
|
-
</div>
|
|
1339
|
-
)}
|
|
1340
|
-
|
|
1341
|
-
{config.visualizationType === 'Pie' && (
|
|
1342
|
-
<ParentSize className='justify-content-center d-flex' style={{ width: `100%` }}>
|
|
1343
|
-
{parent => <PieChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />}
|
|
1344
|
-
</ParentSize>
|
|
1345
|
-
)}
|
|
1346
|
-
{/* Line Chart */}
|
|
1347
|
-
{config.visualizationType === 'Line' &&
|
|
1348
|
-
(checkLineToBarGraph() ? (
|
|
1349
|
-
<div ref={parentRef} style={{ width: `100%` }}>
|
|
1350
|
-
<ParentSize>
|
|
1351
|
-
{parent => (
|
|
1352
|
-
<LinearChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
|
|
1353
|
-
)}
|
|
1354
|
-
</ParentSize>
|
|
1355
|
-
</div>
|
|
1356
|
-
) : (
|
|
1357
|
-
<div ref={parentRef} style={{ width: `100%` }}>
|
|
1358
|
-
<ParentSize>
|
|
1359
|
-
{parent => (
|
|
1360
|
-
<LinearChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
|
|
1361
|
-
)}
|
|
1362
|
-
</ParentSize>
|
|
1363
|
-
</div>
|
|
1364
|
-
))}
|
|
1365
|
-
{/* Sparkline */}
|
|
1366
|
-
{config.visualizationType === 'Spark Line' && (
|
|
1367
|
-
<>
|
|
1368
|
-
<Filters
|
|
1369
|
-
config={config}
|
|
1370
|
-
setConfig={setConfig}
|
|
1371
|
-
setFilteredData={setFilteredData}
|
|
1372
|
-
filteredData={filteredData}
|
|
1373
|
-
excludedData={excludedData}
|
|
1374
|
-
filterData={filterVizData}
|
|
1375
|
-
dimensions={dimensions}
|
|
1376
|
-
/>
|
|
1377
|
-
{config?.introText && (
|
|
1378
|
-
<section className='introText' style={{ padding: '0px 0 35px' }}>
|
|
1379
|
-
{parse(config.introText)}
|
|
1380
|
-
</section>
|
|
1381
|
-
)}
|
|
1382
|
-
<div style={{ height: `100px`, width: `100%`, ...sparkLineStyles }}>
|
|
1383
|
-
<ParentSize>{parent => <SparkLine width={parent.width} height={parent.height} />}</ParentSize>
|
|
1384
|
-
</div>
|
|
1385
|
-
{description && (
|
|
1386
|
-
<div className='subtext' style={{ padding: '35px 0 15px' }}>
|
|
1387
|
-
{parse(description)}
|
|
1388
|
-
</div>
|
|
1389
|
-
)}
|
|
1390
|
-
</>
|
|
1391
|
-
)}
|
|
1392
|
-
{/* Sankey */}
|
|
1393
|
-
{config.visualizationType === 'Sankey' && (
|
|
1394
|
-
<ParentSize aria-hidden='true'>
|
|
1395
|
-
{parent => <SankeyChart runtime={config.runtime} width={parent.width} height={parent.height} />}
|
|
1396
|
-
</ParentSize>
|
|
1397
|
-
)}
|
|
1398
|
-
</div>
|
|
1399
|
-
{/* Legend */}
|
|
1400
|
-
{!config.legend.hide &&
|
|
1401
|
-
config.visualizationType !== 'Spark Line' &&
|
|
1402
|
-
config.visualizationType !== 'Sankey' && (
|
|
1403
|
-
<Legend ref={legendRef} skipId={handleChartTabbing(config, legendId)} />
|
|
1404
|
-
)}
|
|
1405
|
-
</LegendWrapper>
|
|
1406
|
-
{/* Link */}
|
|
1407
|
-
{isDashboard && config.table && config.table.show && config.table.showDataTableLink
|
|
1408
|
-
? tableLink
|
|
1409
|
-
: link && link}
|
|
1410
|
-
{/* Description */}
|
|
1411
|
-
|
|
1412
|
-
{description && config.visualizationType !== 'Spark Line' && (
|
|
1413
|
-
<div className={getChartSubTextClasses().join('')}>{parse(description)}</div>
|
|
1414
|
-
)}
|
|
1415
|
-
|
|
1416
|
-
{/* buttons */}
|
|
1417
|
-
<MediaControls.Section classes={['download-buttons']}>
|
|
1418
|
-
{config.table.showDownloadImgButton && (
|
|
1419
|
-
<MediaControls.Button
|
|
1420
|
-
text='Download Image'
|
|
1421
|
-
title='Download Chart as Image'
|
|
1422
|
-
type='image'
|
|
1423
|
-
state={config}
|
|
1424
|
-
elementToCapture={imageId}
|
|
1425
|
-
/>
|
|
1426
|
-
)}
|
|
1427
|
-
{config.table.showDownloadPdfButton && (
|
|
1428
|
-
<MediaControls.Button
|
|
1429
|
-
text='Download PDF'
|
|
1430
|
-
title='Download Chart as PDF'
|
|
1431
|
-
type='pdf'
|
|
1432
|
-
state={config}
|
|
1433
|
-
elementToCapture={imageId}
|
|
1434
|
-
/>
|
|
1435
|
-
)}
|
|
1436
|
-
</MediaControls.Section>
|
|
1437
|
-
{/* Data Table */}
|
|
1438
|
-
{((config.xAxis.dataKey &&
|
|
1439
|
-
config.table.show &&
|
|
1440
|
-
config.visualizationType !== 'Spark Line' &&
|
|
1441
|
-
config.visualizationType !== 'Sankey') ||
|
|
1442
|
-
(config.visualizationType === 'Sankey' && config.table.show)) && (
|
|
1443
|
-
<DataTable
|
|
1444
|
-
config={pivotDynamicSeries(config)}
|
|
1445
|
-
rawData={
|
|
1446
|
-
config.visualizationType === 'Sankey'
|
|
1447
|
-
? config?.data?.[0]?.tableData
|
|
1448
|
-
: config.table.customTableConfig
|
|
1449
|
-
? filterVizData(config.filters, config.data)
|
|
1450
|
-
: config.data
|
|
1451
|
-
}
|
|
1452
|
-
runtimeData={getTableRuntimeData()}
|
|
1453
|
-
expandDataTable={config.table.expanded}
|
|
1454
|
-
columns={config.columns}
|
|
1455
|
-
displayDataAsText={displayDataAsText}
|
|
1456
|
-
displayGeoName={name => name}
|
|
1457
|
-
applyLegendToRow={applyLegendToRow}
|
|
1458
|
-
tableTitle={config.table.label}
|
|
1459
|
-
indexTitle={config.table.indexLabel}
|
|
1460
|
-
vizTitle={title}
|
|
1461
|
-
viewport={currentViewport}
|
|
1462
|
-
tabbingId={handleChartTabbing(config, legendId)}
|
|
1463
|
-
colorScale={colorScale}
|
|
1464
|
-
/>
|
|
1465
|
-
)}
|
|
1466
|
-
{config?.annotations?.length > 0 && <Annotation.Dropdown />}
|
|
1467
|
-
{/* show pdf or image button */}
|
|
1468
|
-
</div>
|
|
1469
|
-
{config?.footnotes && <section className='footnotes'>{parse(config.footnotes)}</section>}
|
|
1470
|
-
</div>
|
|
1471
|
-
)}
|
|
1472
|
-
</Layout.Responsive>
|
|
1473
|
-
</>
|
|
1474
|
-
)
|
|
128
|
+
const loadDataFromConfig = async (response: any) => {
|
|
129
|
+
if (!response.dataUrl || _.some(_.get(response, 'filters', []), { type: 'url' })) {
|
|
130
|
+
return response.data || []
|
|
1475
131
|
}
|
|
1476
132
|
|
|
1477
|
-
const
|
|
1478
|
-
|
|
1479
|
-
? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime()
|
|
1480
|
-
: d[config.runtime.originalXAxis.dataKey]
|
|
1481
|
-
const getYAxisData = (d, seriesKey) => d[seriesKey]
|
|
133
|
+
const ext = getFileExtension(response.dataUrl)
|
|
134
|
+
const urlWithCacheBuster = `${response.dataUrl}`
|
|
1482
135
|
|
|
1483
|
-
|
|
1484
|
-
return
|
|
136
|
+
try {
|
|
137
|
+
return await fetchAndParseData(urlWithCacheBuster, ext)
|
|
138
|
+
} catch {
|
|
139
|
+
console.error(`Cannot parse URL: ${response.dataUrl}`)
|
|
140
|
+
return []
|
|
1485
141
|
}
|
|
1486
|
-
|
|
1487
|
-
const contextValues = {
|
|
1488
|
-
brushConfig,
|
|
1489
|
-
capitalize,
|
|
1490
|
-
clean,
|
|
1491
|
-
colorPalettes,
|
|
1492
|
-
colorScale,
|
|
1493
|
-
config,
|
|
1494
|
-
currentViewport,
|
|
1495
|
-
dashboardConfig,
|
|
1496
|
-
debugSvg: isDebug,
|
|
1497
|
-
dimensions,
|
|
1498
|
-
dynamicLegendItems,
|
|
1499
|
-
excludedData,
|
|
1500
|
-
formatDate,
|
|
1501
|
-
formatNumber,
|
|
1502
|
-
formatTooltipsDate,
|
|
1503
|
-
getXAxisData,
|
|
1504
|
-
getYAxisData,
|
|
1505
|
-
handleChartAriaLabels,
|
|
1506
|
-
handleLineType,
|
|
1507
|
-
handleChartTabbing,
|
|
1508
|
-
highlight,
|
|
1509
|
-
highlightReset,
|
|
1510
|
-
imageId,
|
|
1511
|
-
isDashboard,
|
|
1512
|
-
isLegendBottom: legend?.position === 'bottom' || isLegendWrapViewport(currentViewport),
|
|
1513
|
-
isDebug,
|
|
1514
|
-
isDraggingAnnotation,
|
|
1515
|
-
handleDragStateChange,
|
|
1516
|
-
isEditor,
|
|
1517
|
-
isNumber,
|
|
1518
|
-
legend,
|
|
1519
|
-
legendId,
|
|
1520
|
-
legendRef,
|
|
1521
|
-
lineOptions,
|
|
1522
|
-
loading,
|
|
1523
|
-
missingRequiredSections,
|
|
1524
|
-
outerContainerRef,
|
|
1525
|
-
parentRef,
|
|
1526
|
-
parseDate,
|
|
1527
|
-
rawData: _.cloneDeep(stateData) ?? {},
|
|
1528
|
-
seriesHighlight,
|
|
1529
|
-
setBrushConfig,
|
|
1530
|
-
setConfig,
|
|
1531
|
-
setDynamicLegendItems,
|
|
1532
|
-
setEditing,
|
|
1533
|
-
setFilteredData,
|
|
1534
|
-
setParentConfig,
|
|
1535
|
-
setSeriesHighlight,
|
|
1536
|
-
setSharedFilter,
|
|
1537
|
-
setSharedFilterValue,
|
|
1538
|
-
svgRef,
|
|
1539
|
-
tableData: filteredData || excludedData, // do not clean table data
|
|
1540
|
-
transformedData: clean(filteredData || excludedData), // do this right before passing to components
|
|
1541
|
-
twoColorPalette,
|
|
1542
|
-
unfilteredData: _.cloneDeep(stateData),
|
|
1543
|
-
updateConfig
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
return (
|
|
1547
|
-
<ConfigContext.Provider value={contextValues}>
|
|
1548
|
-
<Layout.VisualizationWrapper
|
|
1549
|
-
config={config}
|
|
1550
|
-
isEditor={isEditor}
|
|
1551
|
-
currentViewport={currentViewport}
|
|
1552
|
-
ref={outerContainerRef}
|
|
1553
|
-
imageId={imageId}
|
|
1554
|
-
showEditorPanel={config?.showEditorPanel}
|
|
1555
|
-
>
|
|
1556
|
-
{body}
|
|
1557
|
-
</Layout.VisualizationWrapper>
|
|
1558
|
-
</ConfigContext.Provider>
|
|
1559
|
-
)
|
|
1560
142
|
}
|
|
1561
|
-
|
|
1562
|
-
export default CdcChart
|