@cdc/chart 4.23.9 → 4.23.10-alpha
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/LICENSE +201 -0
- package/dist/cdcchart.js +44124 -44458
- package/examples/feature/__data__/area-chart-date-apple.json +1 -5073
- package/examples/feature/area/area-chart-date-apple.json +73 -10316
- package/examples/feature/area/area-chart-date-city-temperature.json +204 -80
- package/examples/feature/area/area-chart-stacked.json +239 -0
- package/examples/feature/bar/lollipop.json +156 -0
- package/examples/feature/combo/planet-combo-example-config.json +99 -9
- package/examples/feature/filters/bar-filter.json +5027 -0
- package/examples/feature/legend-highlights/highlights.json +567 -0
- package/examples/private/TESTING.json +0 -0
- package/examples/private/forest-plot.json +356 -0
- package/examples/private/full.json +45324 -0
- package/examples/private/missing-color.json +333 -0
- package/index.html +11 -7
- package/package.json +3 -2
- package/src/{CdcChart.jsx → CdcChart.tsx} +81 -74
- package/src/_stories/Chart.stories.tsx +188 -0
- package/src/components/AreaChart.Stacked.jsx +73 -0
- package/src/components/AreaChart.jsx +24 -26
- package/src/components/DeviationBar.jsx +67 -13
- package/src/components/EditorPanel.jsx +483 -452
- package/src/components/Forecasting.jsx +5 -5
- package/src/components/ForestPlotSettings.jsx +5 -6
- package/src/components/Legend.jsx +7 -6
- package/src/components/LineChart.Circle.tsx +102 -0
- package/src/components/{LineChart.jsx → LineChart.tsx} +9 -48
- package/src/components/LinearChart.jsx +460 -443
- package/src/components/PieChart.jsx +54 -25
- package/src/components/Series.jsx +63 -17
- package/src/components/SparkLine.jsx +7 -19
- package/src/data/initial-state.js +6 -0
- package/src/hooks/useBarChart.js +1 -1
- package/src/hooks/useEditorPermissions.js +87 -24
- package/src/hooks/useReduceData.js +5 -0
- package/src/hooks/useScales.js +3 -3
- package/src/hooks/useTooltip.jsx +19 -6
- package/src/scss/main.scss +6 -12
- package/src/components/DataTable.jsx +0 -494
- /package/src/{components → hooks}/useIntersectionObserver.jsx +0 -0
|
@@ -41,8 +41,8 @@ import coveUpdateWorker from '@cdc/core/helpers/coveUpdateWorker'
|
|
|
41
41
|
|
|
42
42
|
import './scss/main.scss'
|
|
43
43
|
// load both then config below determines which to use
|
|
44
|
-
import
|
|
45
|
-
import
|
|
44
|
+
import DataTable from '@cdc/core/components/DataTable'
|
|
45
|
+
import { getFileExtension } from '@cdc/core/helpers/getFileExtension'
|
|
46
46
|
|
|
47
47
|
const generateColorsArray = (color = '#000000', special = false) => {
|
|
48
48
|
let colorObj = chroma(color)
|
|
@@ -78,17 +78,17 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
78
78
|
const transform = new DataTransform()
|
|
79
79
|
const [loading, setLoading] = useState(true)
|
|
80
80
|
const [colorScale, setColorScale] = useState(null)
|
|
81
|
-
const [config, setConfig] = useState({})
|
|
81
|
+
const [config, setConfig] = useState<any>({})
|
|
82
82
|
const [stateData, setStateData] = useState(config.data || [])
|
|
83
|
-
const [excludedData, setExcludedData] = useState()
|
|
84
|
-
const [filteredData, setFilteredData] = useState()
|
|
85
|
-
const [seriesHighlight, setSeriesHighlight] = useState([])
|
|
83
|
+
const [excludedData, setExcludedData] = useState<Record<string, number>[] | undefined>(undefined)
|
|
84
|
+
const [filteredData, setFilteredData] = useState<Record<string, any>[] | undefined>(undefined)
|
|
85
|
+
const [seriesHighlight, setSeriesHighlight] = useState<any[]>([])
|
|
86
86
|
const [currentViewport, setCurrentViewport] = useState('lg')
|
|
87
|
-
const [dimensions, setDimensions] = useState([])
|
|
88
|
-
const [externalFilters, setExternalFilters] = useState(
|
|
87
|
+
const [dimensions, setDimensions] = useState<[number?, number?]>([])
|
|
88
|
+
const [externalFilters, setExternalFilters] = useState<any[]>()
|
|
89
89
|
const [container, setContainer] = useState()
|
|
90
90
|
const [coveLoadedEventRan, setCoveLoadedEventRan] = useState(false)
|
|
91
|
-
const [dynamicLegendItems, setDynamicLegendItems] = useState([])
|
|
91
|
+
const [dynamicLegendItems, setDynamicLegendItems] = useState<any[]>([])
|
|
92
92
|
const [imageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`)
|
|
93
93
|
|
|
94
94
|
let legendMemo = useRef(new Map()) // map collection
|
|
@@ -96,8 +96,6 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
96
96
|
|
|
97
97
|
if (isDebug) console.log('Chart config, isEditor', config, isEditor)
|
|
98
98
|
|
|
99
|
-
const DataTable = config?.table?.showVertical ? DataTable_vert : DataTable_horiz
|
|
100
|
-
|
|
101
99
|
// Destructure items from config for more readable JSX
|
|
102
100
|
let { legend, title, description, visualizationType } = config
|
|
103
101
|
|
|
@@ -142,7 +140,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
142
140
|
|
|
143
141
|
const reloadURLData = async () => {
|
|
144
142
|
if (config.dataUrl) {
|
|
145
|
-
const dataUrl = new URL(config.runtimeDataUrl || config.dataUrl)
|
|
143
|
+
const dataUrl = new URL(config.runtimeDataUrl || config.dataUrl, window.location.origin)
|
|
146
144
|
let qsParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
|
|
147
145
|
|
|
148
146
|
let isUpdateNeeded = false
|
|
@@ -164,12 +162,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
164
162
|
})
|
|
165
163
|
.join('')}`
|
|
166
164
|
|
|
167
|
-
let data
|
|
165
|
+
let data: any[] = []
|
|
168
166
|
|
|
169
167
|
try {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
const ext = regex.exec(dataUrl.pathname)[1]
|
|
168
|
+
const ext = getFileExtension(dataUrl.pathname)
|
|
173
169
|
if ('csv' === ext) {
|
|
174
170
|
data = await fetch(dataUrlFinal)
|
|
175
171
|
.then(response => response.text())
|
|
@@ -250,15 +246,13 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
250
246
|
let response = configObj || (await (await fetch(configUrl)).json())
|
|
251
247
|
|
|
252
248
|
// If data is included through a URL, fetch that and store
|
|
253
|
-
let data = response.
|
|
249
|
+
let data: any[] = response.data || []
|
|
254
250
|
|
|
255
251
|
const urlFilters = response.filters ? (response.filters.filter(filter => filter.type === 'url').length > 0 ? true : false) : false
|
|
256
252
|
|
|
257
253
|
if (response.dataUrl && !urlFilters) {
|
|
258
254
|
try {
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
const ext = regex.exec(response.dataUrl)[1]
|
|
255
|
+
const ext = getFileExtension(response.dataUrl)
|
|
262
256
|
if ('csv' === ext) {
|
|
263
257
|
data = await fetch(response.dataUrl + `?v=${cacheBustingString()}`)
|
|
264
258
|
.then(response => response.text())
|
|
@@ -289,11 +283,11 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
289
283
|
console.error(`COVE: Cannot parse URL: ${response.dataUrl}`) // eslint-disable-line
|
|
290
284
|
data = []
|
|
291
285
|
}
|
|
286
|
+
}
|
|
292
287
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
288
|
+
if (response.dataDescription) {
|
|
289
|
+
data = transform.autoStandardize(data)
|
|
290
|
+
data = transform.developerStandardize(data, response.dataDescription)
|
|
297
291
|
}
|
|
298
292
|
|
|
299
293
|
if (data) {
|
|
@@ -323,7 +317,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
323
317
|
updateConfig(processedConfig, data)
|
|
324
318
|
}
|
|
325
319
|
|
|
326
|
-
const updateConfig = (newConfig, dataOverride
|
|
320
|
+
const updateConfig = (newConfig, dataOverride?: any[]) => {
|
|
327
321
|
let data = dataOverride || stateData
|
|
328
322
|
|
|
329
323
|
// Deeper copy
|
|
@@ -333,7 +327,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
333
327
|
}
|
|
334
328
|
})
|
|
335
329
|
|
|
336
|
-
let newExcludedData
|
|
330
|
+
let newExcludedData: any[] = []
|
|
337
331
|
|
|
338
332
|
if (newConfig.exclusions && newConfig.exclusions.active) {
|
|
339
333
|
if (newConfig.xAxis.type === 'categorical' && newConfig.exclusions.keys?.length > 0) {
|
|
@@ -365,7 +359,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
365
359
|
setExcludedData(newExcludedData)
|
|
366
360
|
|
|
367
361
|
// After data is grabbed, loop through and generate filter column values if there are any
|
|
368
|
-
let currentData
|
|
362
|
+
let currentData: any[] = []
|
|
369
363
|
if (newConfig.filters) {
|
|
370
364
|
newConfig.filters.forEach((filter, index) => {
|
|
371
365
|
let filterValues = []
|
|
@@ -394,8 +388,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
394
388
|
} else {
|
|
395
389
|
newConfig.runtime.seriesKeys = newConfig.series
|
|
396
390
|
? newConfig.series.map(series => {
|
|
397
|
-
newConfig.runtime.seriesLabels[series.dataKey] = series.label || series.dataKey
|
|
398
|
-
newConfig.runtime.seriesLabelsAll.push(series.name || series.
|
|
391
|
+
newConfig.runtime.seriesLabels[series.dataKey] = series.name || series.label || series.dataKey
|
|
392
|
+
newConfig.runtime.seriesLabelsAll.push(series.name || series.dataKey)
|
|
399
393
|
return series.dataKey
|
|
400
394
|
})
|
|
401
395
|
: []
|
|
@@ -412,8 +406,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
412
406
|
}
|
|
413
407
|
|
|
414
408
|
const groups = uniqueArray(allKeys)
|
|
415
|
-
let tableData = []
|
|
416
|
-
const plots = []
|
|
409
|
+
let tableData: any[] = []
|
|
410
|
+
const plots: any[] = []
|
|
417
411
|
|
|
418
412
|
/**
|
|
419
413
|
* Calculates the first quartile (q1) and third quartile (q3) from an array of integers or decimals.
|
|
@@ -449,13 +443,13 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
449
443
|
// group specific statistics
|
|
450
444
|
// prevent re-renders
|
|
451
445
|
if (!groups) return
|
|
452
|
-
groups.forEach(
|
|
446
|
+
groups.forEach(g => {
|
|
453
447
|
try {
|
|
454
448
|
if (!g) throw new Error('No groups resolved in box plots')
|
|
455
449
|
|
|
456
450
|
// filter data by group
|
|
457
451
|
let filteredData = newExcludedData ? newExcludedData.filter(item => item[newConfig.xAxis.dataKey] === g) : data.filter(item => item[newConfig.xAxis.dataKey] === g)
|
|
458
|
-
let filteredDataValues = filteredData.map(item => Number(item[newConfig?.series[0]?.dataKey]))
|
|
452
|
+
let filteredDataValues: number[] = filteredData.map(item => Number(item[newConfig?.series[0]?.dataKey]))
|
|
459
453
|
|
|
460
454
|
// Sort the data for upcoming functions.
|
|
461
455
|
let sortedData = filteredDataValues.sort((a, b) => a - b)
|
|
@@ -484,19 +478,20 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
484
478
|
let nonOutliers = filteredDataValues
|
|
485
479
|
|
|
486
480
|
nonOutliers = nonOutliers.filter(item => !outliers.includes(item))
|
|
487
|
-
|
|
481
|
+
const minValue: number = d3.min<number>(filteredDataValues) || 0
|
|
482
|
+
const _colMin = d3.max<number>([minValue, q1 - 1.5 * iqr])
|
|
488
483
|
plots.push({
|
|
489
484
|
columnCategory: g,
|
|
490
485
|
columnMax: d3.min([d3.max(filteredDataValues), q1 + 1.5 * iqr]),
|
|
491
486
|
columnThirdQuartile: Number(q3).toFixed(newConfig.dataFormat.roundTo),
|
|
492
487
|
columnMedian: Number(d3.median(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
|
|
493
488
|
columnFirstQuartile: q1.toFixed(newConfig.dataFormat.roundTo),
|
|
494
|
-
columnMin:
|
|
489
|
+
columnMin: _colMin,
|
|
495
490
|
columnTotal: filteredDataValues.reduce((partialSum, a) => partialSum + a, 0),
|
|
496
491
|
columnSd: Number(d3.deviation(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
|
|
497
492
|
columnMean: Number(d3.mean(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
|
|
498
493
|
columnIqr: Number(iqr).toFixed(newConfig.dataFormat.roundTo),
|
|
499
|
-
columnLowerBounds:
|
|
494
|
+
columnLowerBounds: _colMin,
|
|
500
495
|
columnUpperBounds: d3.min([d3.max(sortedData), q1 + 1.5 * iqr]),
|
|
501
496
|
columnOutliers: outliers,
|
|
502
497
|
values: filteredDataValues,
|
|
@@ -511,10 +506,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
511
506
|
// this appears to be the easiest option instead of running logic against the datatable cell...
|
|
512
507
|
tableData = JSON.parse(JSON.stringify(plots))
|
|
513
508
|
tableData.map(table => {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
509
|
+
table.columnIqr = undefined
|
|
510
|
+
table.nonOutlierValues = undefined
|
|
511
|
+
table.columnLowerBounds = undefined
|
|
512
|
+
table.columnUpperBounds = undefined
|
|
518
513
|
return null // resolve eslint
|
|
519
514
|
})
|
|
520
515
|
|
|
@@ -570,7 +565,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
570
565
|
newConfig.runtime.yAxis = newConfig.xAxis
|
|
571
566
|
newConfig.runtime.horizontal = true
|
|
572
567
|
newConfig.orientation = 'horizontal'
|
|
573
|
-
} else if (['Box Plot', 'Scatter Plot', 'Area Chart'].includes(newConfig.visualizationType)) {
|
|
568
|
+
} else if (['Box Plot', 'Scatter Plot', 'Area Chart', 'Line', 'Forecasting'].includes(newConfig.visualizationType)) {
|
|
574
569
|
newConfig.runtime.xAxis = newConfig.xAxis
|
|
575
570
|
newConfig.runtime.yAxis = newConfig.yAxis
|
|
576
571
|
newConfig.runtime.horizontal = false
|
|
@@ -587,7 +582,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
587
582
|
}
|
|
588
583
|
|
|
589
584
|
const filterData = (filters, data) => {
|
|
590
|
-
let filteredData = []
|
|
585
|
+
let filteredData: any[] = []
|
|
591
586
|
|
|
592
587
|
data.forEach(row => {
|
|
593
588
|
let add = true
|
|
@@ -607,7 +602,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
607
602
|
|
|
608
603
|
// Gets filter values from dataset
|
|
609
604
|
const generateValuesForFilter = (columnName, data = this.state.data) => {
|
|
610
|
-
const values = []
|
|
605
|
+
const values: any[] = []
|
|
611
606
|
|
|
612
607
|
data.forEach(row => {
|
|
613
608
|
const value = row[columnName]
|
|
@@ -697,7 +692,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
697
692
|
*/
|
|
698
693
|
useEffect(() => {
|
|
699
694
|
const handleFilterData = e => {
|
|
700
|
-
let tmp = []
|
|
695
|
+
let tmp: any[] = []
|
|
701
696
|
tmp.push(e.detail)
|
|
702
697
|
setExternalFilters(tmp)
|
|
703
698
|
}
|
|
@@ -744,7 +739,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
744
739
|
// Generates color palette to pass to child chart component
|
|
745
740
|
useEffect(() => {
|
|
746
741
|
if (stateData && config.xAxis && config.runtime.seriesKeys) {
|
|
747
|
-
const configPalette = config.visualizationType === 'Paired Bar' || config.visualizationType === 'Deviation Bar' ? config.twoColor.palette : config.palette
|
|
742
|
+
const configPalette = config.customColors ? config.customColors : config.visualizationType === 'Paired Bar' || config.visualizationType === 'Deviation Bar' ? config.twoColor.palette : config.palette
|
|
748
743
|
const allPalettes = { ...colorPalettes, ...twoColorPalette }
|
|
749
744
|
let palette = config.customColors || allPalettes[configPalette]
|
|
750
745
|
let numberOfKeys = config.runtime.seriesKeys.length
|
|
@@ -759,7 +754,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
759
754
|
newColorScale = () =>
|
|
760
755
|
scaleOrdinal({
|
|
761
756
|
domain: config.runtime.seriesLabelsAll,
|
|
762
|
-
range: palette
|
|
757
|
+
range: palette,
|
|
758
|
+
unknown: null
|
|
763
759
|
})
|
|
764
760
|
|
|
765
761
|
setColorScale(newColorScale)
|
|
@@ -773,10 +769,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
773
769
|
|
|
774
770
|
// Called on legend click, highlights/unhighlights the data series with the given label
|
|
775
771
|
const highlight = label => {
|
|
776
|
-
const newSeriesHighlight = []
|
|
772
|
+
const newSeriesHighlight: any[] = []
|
|
777
773
|
|
|
778
774
|
// If we're highlighting all the series, reset them
|
|
779
|
-
if (seriesHighlight.length + 1 === config.runtime.seriesKeys.length &&
|
|
775
|
+
if (seriesHighlight.length + 1 === config.runtime.seriesKeys.length && config.visualizationType !== 'Forecasting') {
|
|
780
776
|
highlightReset()
|
|
781
777
|
return
|
|
782
778
|
}
|
|
@@ -806,25 +802,28 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
806
802
|
* - pushes series.dataKey into the series highlight based on the found series.name
|
|
807
803
|
* @param {String} value
|
|
808
804
|
*/
|
|
809
|
-
const pushDataKeyBySeriesName = value => {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
}
|
|
805
|
+
// const pushDataKeyBySeriesName = value => {
|
|
806
|
+
// let matchingSeries = config.series.filter(series => series.name === value.text)
|
|
807
|
+
// if (matchingSeries?.length > 0) {
|
|
808
|
+
// newSeriesHighlight.push(matchingSeries[0].dataKey)
|
|
809
|
+
// }
|
|
810
|
+
// }
|
|
815
811
|
|
|
816
|
-
pushDataKeyBySeriesName(label)
|
|
812
|
+
// pushDataKeyBySeriesName(label)
|
|
817
813
|
|
|
818
814
|
setSeriesHighlight(newSeriesHighlight)
|
|
819
815
|
}
|
|
820
816
|
|
|
821
817
|
// Called on reset button click, unhighlights all data series
|
|
822
818
|
const highlightReset = () => {
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
819
|
+
try {
|
|
820
|
+
const legend = document.getElementById('legend')
|
|
821
|
+
if (!legend) throw new Error('No legend available to set previous focus on.')
|
|
822
|
+
legend.focus()
|
|
823
|
+
} catch (e) {
|
|
824
|
+
console.error('COVE:', e.message)
|
|
827
825
|
}
|
|
826
|
+
setSeriesHighlight([])
|
|
828
827
|
}
|
|
829
828
|
|
|
830
829
|
const section = config.orientation === 'horizontal' ? 'yAxis' : 'xAxis'
|
|
@@ -849,7 +848,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
849
848
|
function getTextWidth(text, font) {
|
|
850
849
|
const canvas = document.createElement('canvas')
|
|
851
850
|
const context = canvas.getContext('2d')
|
|
852
|
-
|
|
851
|
+
if (!context) {
|
|
852
|
+
console.error('2d context not found')
|
|
853
|
+
return
|
|
854
|
+
}
|
|
853
855
|
context.font = font || getComputedStyle(document.body).font
|
|
854
856
|
|
|
855
857
|
return Math.ceil(context.measureText(text).width)
|
|
@@ -875,6 +877,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
875
877
|
|
|
876
878
|
// Format numeric data based on settings in config OR from passed in settings for Additional Columns
|
|
877
879
|
// - use only for old horizontal data - newer formatNumber is in helper/formatNumber
|
|
880
|
+
// TODO: we should combine various formatNumber functions across this project.
|
|
878
881
|
const formatNumber = (num, axis, shouldAbbreviate = false, addColPrefix, addColSuffix, addColRoundTo) => {
|
|
879
882
|
// if num is NaN return num
|
|
880
883
|
if (isNaN(num) || !num) return num
|
|
@@ -897,7 +900,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
897
900
|
if (String(num).indexOf(',') !== -1) num = num.replaceAll(',', '')
|
|
898
901
|
|
|
899
902
|
let original = num
|
|
900
|
-
let stringFormattingOptions = {
|
|
903
|
+
let stringFormattingOptions: any = {
|
|
901
904
|
useGrouping: commas ? true : false // for old chart data table to work right cant just leave this to undefined
|
|
902
905
|
}
|
|
903
906
|
if (axis === 'left' || axis === undefined) {
|
|
@@ -1144,8 +1147,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
1144
1147
|
}
|
|
1145
1148
|
}
|
|
1146
1149
|
|
|
1150
|
+
// TODO: should be part of the DataTransform class.
|
|
1147
1151
|
const clean = data => {
|
|
1148
1152
|
// cleaning is deleting data we need in forecasting charts.
|
|
1153
|
+
if (!Array.isArray(data)) return []
|
|
1149
1154
|
if (config.visualizationType === 'Forecasting') return data
|
|
1150
1155
|
return config?.xAxis?.dataKey ? transform.cleanData(data, config.xAxis.dataKey) : data
|
|
1151
1156
|
}
|
|
@@ -1180,9 +1185,9 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
1180
1185
|
Skip Over Chart Container
|
|
1181
1186
|
</a>
|
|
1182
1187
|
{/* Filters */}
|
|
1183
|
-
{config.filters && !externalFilters && <Filters config={config} setConfig={setConfig} setFilteredData={setFilteredData} filteredData={filteredData} excludedData={excludedData} filterData={filterData}
|
|
1188
|
+
{config.filters && !externalFilters && <Filters config={config} setConfig={setConfig} setFilteredData={setFilteredData} filteredData={filteredData} excludedData={excludedData} filterData={filterData} dimensions={dimensions} />}
|
|
1184
1189
|
{/* Visualization */}
|
|
1185
|
-
{config?.introText && <section className='introText'>{parse(config.introText)}</section>}
|
|
1190
|
+
{config?.introText && config.visualizationType !== 'Spark Line' && <section className='introText'>{parse(config.introText)}</section>}
|
|
1186
1191
|
<div
|
|
1187
1192
|
style={{ marginBottom: config.legend.position !== 'bottom' && config.orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px' }}
|
|
1188
1193
|
className={`chart-container p-relative ${config.legend.position === 'bottom' ? 'bottom' : ''}${config.legend.hide ? ' legend-hidden' : ''}${lineDatapointClass}${barBorderClass} ${contentClasses.join(' ')}`}
|
|
@@ -1193,16 +1198,19 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
1193
1198
|
{/* Sparkline */}
|
|
1194
1199
|
{config.visualizationType === 'Spark Line' && (
|
|
1195
1200
|
<>
|
|
1196
|
-
{
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
)}
|
|
1204
|
-
</ParentSize>
|
|
1201
|
+
{config?.introText && (
|
|
1202
|
+
<section className='introText' style={{ padding: '0px 0 35px' }}>
|
|
1203
|
+
{parse(config.introText)}
|
|
1204
|
+
</section>
|
|
1205
|
+
)}
|
|
1206
|
+
<div style={{ height: `100px`, width: `100%`, ...sparkLineStyles }}>
|
|
1207
|
+
<ParentSize>{parent => <SparkLine width={parent.width} height={parent.height} />}</ParentSize>
|
|
1205
1208
|
</div>
|
|
1209
|
+
{description && (
|
|
1210
|
+
<div className='subtext' style={{ padding: '35px 0 15px' }}>
|
|
1211
|
+
{parse(description)}
|
|
1212
|
+
</div>
|
|
1213
|
+
)}
|
|
1206
1214
|
</>
|
|
1207
1215
|
)}
|
|
1208
1216
|
{!config.legend.hide && config.visualizationType !== 'Spark Line' && <Legend />}
|
|
@@ -1225,9 +1233,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
1225
1233
|
config={config}
|
|
1226
1234
|
rawData={config.data}
|
|
1227
1235
|
runtimeData={filteredData || excludedData}
|
|
1228
|
-
//navigationHandler={navigationHandler} // do we need this? What does it do?
|
|
1229
1236
|
expandDataTable={config.table.expanded}
|
|
1230
|
-
//headerColor={general.headerColor} // have this in map but not chart
|
|
1231
1237
|
columns={config.columns}
|
|
1232
1238
|
showDownloadButton={config.general.showDownloadButton}
|
|
1233
1239
|
runtimeLegend={dynamicLegendItems}
|
|
@@ -1247,6 +1253,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
1247
1253
|
innerContainerRef={innerContainerRef}
|
|
1248
1254
|
outerContainerRef={outerContainerRef}
|
|
1249
1255
|
imageRef={imageId}
|
|
1256
|
+
colorScale={colorScale}
|
|
1250
1257
|
isDebug={isDebug}
|
|
1251
1258
|
isEditor={isEditor}
|
|
1252
1259
|
/>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
|
|
3
|
+
import Chart from '../CdcChart'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Chart> = {
|
|
6
|
+
title: 'Components/Templates/Chart',
|
|
7
|
+
component: Chart
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type Story = StoryObj<typeof Chart>
|
|
11
|
+
|
|
12
|
+
export const Lollipop: Story = {
|
|
13
|
+
args: {
|
|
14
|
+
config: {
|
|
15
|
+
type: 'chart',
|
|
16
|
+
title: 'Lollipop Style Horizontal Bar Chart - Number of Spills Occurring in the Home',
|
|
17
|
+
showTitle: true,
|
|
18
|
+
showDownloadMediaButton: false,
|
|
19
|
+
theme: 'theme-blue',
|
|
20
|
+
animate: true,
|
|
21
|
+
fontSize: 'medium',
|
|
22
|
+
lineDatapointStyle: 'hover',
|
|
23
|
+
barHasBorder: 'false',
|
|
24
|
+
isLollipopChart: true,
|
|
25
|
+
lollipopShape: 'circle',
|
|
26
|
+
lollipopColorStyle: 'two-tone',
|
|
27
|
+
visualizationSubType: 'horizontal',
|
|
28
|
+
barStyle: '',
|
|
29
|
+
roundingStyle: 'standard',
|
|
30
|
+
tipRounding: 'top',
|
|
31
|
+
isResponsiveTicks: false,
|
|
32
|
+
general: { showDownloadButton: false },
|
|
33
|
+
padding: { left: 5, right: 5 },
|
|
34
|
+
yAxis: {
|
|
35
|
+
hideAxis: true,
|
|
36
|
+
displayNumbersOnBar: true,
|
|
37
|
+
hideLabel: false,
|
|
38
|
+
hideTicks: false,
|
|
39
|
+
size: '13',
|
|
40
|
+
gridLines: false,
|
|
41
|
+
enablePadding: false,
|
|
42
|
+
min: '',
|
|
43
|
+
max: '',
|
|
44
|
+
labelColor: '#333',
|
|
45
|
+
tickLabelColor: '#333',
|
|
46
|
+
tickColor: '#333',
|
|
47
|
+
rightHideAxis: true,
|
|
48
|
+
rightAxisSize: 50,
|
|
49
|
+
rightLabel: '',
|
|
50
|
+
rightLabelOffsetSize: 0,
|
|
51
|
+
rightAxisLabelColor: '#333',
|
|
52
|
+
rightAxisTickLabelColor: '#333',
|
|
53
|
+
rightAxisTickColor: '#333',
|
|
54
|
+
numTicks: '9',
|
|
55
|
+
axisPadding: 0,
|
|
56
|
+
tickRotation: 0,
|
|
57
|
+
anchors: [],
|
|
58
|
+
type: 'chart',
|
|
59
|
+
title: 'Lollipop Style Horizontal Bar Chart',
|
|
60
|
+
theme: 'theme-blue',
|
|
61
|
+
fontSize: 'medium',
|
|
62
|
+
lineDatapointStyle: 'hover',
|
|
63
|
+
barHasBorder: 'false',
|
|
64
|
+
isLollipopChart: false,
|
|
65
|
+
lollipopShape: 'circle',
|
|
66
|
+
lollipopColorStyle: 'two-tone',
|
|
67
|
+
visualizationSubType: 'horizontal',
|
|
68
|
+
padding: { left: 5, right: 5 },
|
|
69
|
+
yAxis: { size: 50, gridLines: false },
|
|
70
|
+
barThickness: 0.35,
|
|
71
|
+
height: 260,
|
|
72
|
+
xAxis: { type: 'categorical', size: 75, tickRotation: 0, dataKey: 'Vehicle' },
|
|
73
|
+
table: { label: 'Data Table', expanded: true, show: true },
|
|
74
|
+
legend: { behavior: 'isolate', position: 'right' },
|
|
75
|
+
exclusions: { active: false, keys: [] },
|
|
76
|
+
palette: 'qualitative-bold',
|
|
77
|
+
labels: false,
|
|
78
|
+
dataFormat: {},
|
|
79
|
+
confidenceKeys: {},
|
|
80
|
+
data: [
|
|
81
|
+
{ Group: 'Combined Total of Group A', Vehicle: '100', Home: '120', Work: '140', Office: '120' },
|
|
82
|
+
{ Group: 'Combined Total of Group B', Vehicle: '150', Home: '140', Work: '100', Office: '90' },
|
|
83
|
+
{ Group: 'Combined Total of Group C', Vehicle: '90', Home: '90', Work: '80', Office: '80' },
|
|
84
|
+
{ Group: 'Combined Total of Group D', Vehicle: '70', Home: '60', Work: '50', Office: '70' }
|
|
85
|
+
],
|
|
86
|
+
dataFileName: 'CSV_Source_Example_for_Horizontal_Bar_viz-cdcwp1619811744363.csv',
|
|
87
|
+
dataFileSourceType: 'file',
|
|
88
|
+
visualizationType: 'Bar',
|
|
89
|
+
runtime: {
|
|
90
|
+
seriesLabels: { Vehicle: 'Vehicle' },
|
|
91
|
+
seriesLabelsAll: ['Vehicle'],
|
|
92
|
+
originalXAxis: { type: 'categorical', size: 75, tickRotation: 0, dataKey: 'Vehicle' },
|
|
93
|
+
seriesKeys: ['Vehicle'],
|
|
94
|
+
xAxis: { size: 50, gridLines: false },
|
|
95
|
+
yAxis: { type: 'categorical', size: 75, tickRotation: 0, dataKey: 'Vehicle' },
|
|
96
|
+
horizontal: true,
|
|
97
|
+
uniqueId: 1651765968212,
|
|
98
|
+
editorErrorMessage: ''
|
|
99
|
+
},
|
|
100
|
+
description: 'Subtext can be added here for options like citing data sources or insight into reading the bar chart.',
|
|
101
|
+
series: [{ dataKey: 'Vehicle', type: 'Bar' }],
|
|
102
|
+
barHeight: 25,
|
|
103
|
+
barPadding: 40,
|
|
104
|
+
labelPlacement: 'Below Bar',
|
|
105
|
+
label: 'Number of Accidents'
|
|
106
|
+
},
|
|
107
|
+
boxplot: [],
|
|
108
|
+
topAxis: { hasLine: false },
|
|
109
|
+
isLegendValue: false,
|
|
110
|
+
barThickness: 0.35,
|
|
111
|
+
barHeight: 6,
|
|
112
|
+
barSpace: 15,
|
|
113
|
+
heights: { vertical: 300, horizontal: 170.39999999999998 },
|
|
114
|
+
xAxis: {
|
|
115
|
+
anchors: [],
|
|
116
|
+
type: 'categorical',
|
|
117
|
+
showTargetLabel: true,
|
|
118
|
+
targetLabel: 'Target',
|
|
119
|
+
hideAxis: true,
|
|
120
|
+
hideLabel: true,
|
|
121
|
+
hideTicks: true,
|
|
122
|
+
size: '16',
|
|
123
|
+
tickRotation: 0,
|
|
124
|
+
min: '',
|
|
125
|
+
max: '160',
|
|
126
|
+
labelColor: '#333',
|
|
127
|
+
tickLabelColor: '#333',
|
|
128
|
+
tickColor: '#333',
|
|
129
|
+
numTicks: '',
|
|
130
|
+
labelOffset: 65,
|
|
131
|
+
axisPadding: 0,
|
|
132
|
+
target: 0,
|
|
133
|
+
maxTickRotation: 0,
|
|
134
|
+
dataKey: 'Group'
|
|
135
|
+
},
|
|
136
|
+
table: { label: 'Data Table', expanded: false, limitHeight: false, height: '', caption: '', showDownloadUrl: false, showDataTableLink: true, indexLabel: 'Group', download: false, showVertical: true, show: true },
|
|
137
|
+
orientation: 'horizontal',
|
|
138
|
+
color: 'pinkpurple',
|
|
139
|
+
columns: {},
|
|
140
|
+
legend: {
|
|
141
|
+
behavior: 'isolate',
|
|
142
|
+
singleRow: false,
|
|
143
|
+
colorCode: '',
|
|
144
|
+
reverseLabelOrder: false,
|
|
145
|
+
description: '',
|
|
146
|
+
dynamicLegend: false,
|
|
147
|
+
dynamicLegendDefaultText: 'Show All',
|
|
148
|
+
dynamicLegendItemLimit: 5,
|
|
149
|
+
dynamicLegendItemLimitMessage: 'Dynamic Legend Item Limit Hit.',
|
|
150
|
+
dynamicLegendChartMessage: 'Select Options from the Legend',
|
|
151
|
+
position: 'right',
|
|
152
|
+
hide: true,
|
|
153
|
+
label: 'Accident Location'
|
|
154
|
+
},
|
|
155
|
+
exclusions: { active: false, keys: [] },
|
|
156
|
+
palette: 'qualitative-bold',
|
|
157
|
+
isPaletteReversed: false,
|
|
158
|
+
twoColor: { palette: 'monochrome-1', isPaletteReversed: false },
|
|
159
|
+
labels: false,
|
|
160
|
+
dataFormat: { commas: false, prefix: '', suffix: '', abbreviated: false, bottomSuffix: '', bottomPrefix: '', bottomAbbreviated: false },
|
|
161
|
+
confidenceKeys: {},
|
|
162
|
+
visual: { border: true, accent: true, background: true, verticalHoverLine: false, horizontalHoverLine: false },
|
|
163
|
+
useLogScale: false,
|
|
164
|
+
filterBehavior: 'Filter Change',
|
|
165
|
+
highlightedBarValues: [],
|
|
166
|
+
series: [{ dataKey: 'Home', type: 'Bar', tooltip: true }],
|
|
167
|
+
tooltips: { opacity: 90 },
|
|
168
|
+
height: 212,
|
|
169
|
+
data: [
|
|
170
|
+
{ Group: 'Combined Total of Group A', Vehicle: '100', Home: '120', Work: '140', Office: '120' },
|
|
171
|
+
{ Group: 'Combined Total of Group B', Vehicle: '150', Home: '140', Work: '100', Office: '90' },
|
|
172
|
+
{ Group: 'Combined Total of Group C', Vehicle: '90', Home: '90', Work: '80', Office: '80' },
|
|
173
|
+
{ Group: 'Combined Total of Group D', Vehicle: '70', Home: '60', Work: '50', Office: '70' }
|
|
174
|
+
],
|
|
175
|
+
dataFileName: 'CSV_Source_Example_for_Horizontal_Bar_viz-cdcwp1619811744363.csv',
|
|
176
|
+
dataFileSourceType: 'file',
|
|
177
|
+
visualizationType: 'Bar',
|
|
178
|
+
description: 'Subtext can be added here for options like citing data sources or insight into reading the bar chart.',
|
|
179
|
+
barPadding: 47,
|
|
180
|
+
filters: [],
|
|
181
|
+
lollipopSize: 'medium',
|
|
182
|
+
validated: 4.23,
|
|
183
|
+
dynamicMarginTop: 0
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export default meta
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React, { useContext, memo } from 'react'
|
|
2
|
+
|
|
3
|
+
// cdc
|
|
4
|
+
import ConfigContext from '../ConfigContext'
|
|
5
|
+
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
6
|
+
|
|
7
|
+
// visx & d3
|
|
8
|
+
import * as allCurves from '@visx/curve'
|
|
9
|
+
import { Bar, AreaStack } from '@visx/shape'
|
|
10
|
+
import { Group } from '@visx/group'
|
|
11
|
+
import { approvedCurveTypes } from '@cdc/core/helpers/lineChartHelpers'
|
|
12
|
+
|
|
13
|
+
const AreaChartStacked = ({ xScale, yScale, yMax, xMax, handleTooltipMouseOver, handleTooltipMouseOff, isDebug, isBrush }) => {
|
|
14
|
+
// import data from context
|
|
15
|
+
let { transformedData: data, config, seriesHighlight, colorScale, rawData } = useContext(ConfigContext)
|
|
16
|
+
|
|
17
|
+
// Draw transparent bars over the chart to get tooltip data
|
|
18
|
+
// Turn DEBUG on for additional context.
|
|
19
|
+
if (!data) return
|
|
20
|
+
|
|
21
|
+
const handleDateCategory = value => {
|
|
22
|
+
if (config.xAxis.type === 'categorical') return xScale(value)
|
|
23
|
+
if (config.xAxis.type === 'date') {
|
|
24
|
+
let date = new Date(value)
|
|
25
|
+
return xScale(date)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const strokeWidth = 2
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
data && (
|
|
33
|
+
<svg height={Number(yMax)}>
|
|
34
|
+
<ErrorBoundary component='AreaChartStacked'>
|
|
35
|
+
<Group className='area-chart' key='area-wrapper' left={Number(config.yAxis.size) + strokeWidth / 2} height={Number(yMax)} style={{ overflow: 'hidden' }}>
|
|
36
|
+
<AreaStack
|
|
37
|
+
data={data}
|
|
38
|
+
keys={config.runtime.areaSeriesKeys.map(s => s.dataKey) || config.series.map(s => s.dataKey)}
|
|
39
|
+
x0={d => handleDateCategory(d.data[config.xAxis.dataKey])}
|
|
40
|
+
y0={d => Number(yScale(d[0]))}
|
|
41
|
+
y1={d => Number(yScale(d[1]))}
|
|
42
|
+
curve={allCurves[approvedCurveTypes[config.stackedAreaChartLineType]]}
|
|
43
|
+
>
|
|
44
|
+
{({ stacks, path }) => {
|
|
45
|
+
return stacks.map((stack, stackIndex) => {
|
|
46
|
+
let transparentArea = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(stack.key) === -1
|
|
47
|
+
let displayArea = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(stack.key) !== -1
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
// prettier-ignore
|
|
51
|
+
<path
|
|
52
|
+
key={stack.key}
|
|
53
|
+
d={path(stack) || ''}
|
|
54
|
+
strokeWidth={2}
|
|
55
|
+
stroke={displayArea ? colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[stack.key] : stack.key) : '#000' : 'transparent'}
|
|
56
|
+
fillOpacity={transparentArea ? 0.25 : 0.5}
|
|
57
|
+
fill={displayArea ? colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[stack.key] : stack.key) : '#000' : 'transparent'}
|
|
58
|
+
/>
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
}}
|
|
62
|
+
</AreaStack>
|
|
63
|
+
|
|
64
|
+
{/* prettier-ignore */}
|
|
65
|
+
{!isBrush && <Bar width={Number(xMax)} height={Number(yMax)} fill={isDebug ? 'red' : 'transparent'} fillOpacity={0.05} style={isDebug ? { stroke: 'black', strokeWidth: 2 } : {}} onMouseMove={e => handleTooltipMouseOver(e, rawData)} onMouseLeave={handleTooltipMouseOff} />}
|
|
66
|
+
</Group>
|
|
67
|
+
</ErrorBoundary>
|
|
68
|
+
</svg>
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default memo(AreaChartStacked)
|