@cdc/chart 4.23.9 → 4.23.10
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 +44099 -44436
- 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/filters/bar-filter.json +5027 -0
- package/examples/feature/legend-highlights/highlights.json +567 -0
- package/index.html +9 -6
- package/package.json +3 -2
- package/src/{CdcChart.jsx → CdcChart.tsx} +77 -71
- 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/Legend.jsx +6 -5
- package/src/components/LineChart.Circle.tsx +108 -0
- package/src/components/{LineChart.jsx → LineChart.tsx} +10 -42
- 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/useEditorPermissions.js +87 -24
- package/src/hooks/useReduceData.js +5 -0
- package/src/hooks/useScales.js +1 -1
- 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
|
|
|
@@ -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
|
}
|
|
@@ -773,10 +768,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
773
768
|
|
|
774
769
|
// Called on legend click, highlights/unhighlights the data series with the given label
|
|
775
770
|
const highlight = label => {
|
|
776
|
-
const newSeriesHighlight = []
|
|
771
|
+
const newSeriesHighlight: any[] = []
|
|
777
772
|
|
|
778
773
|
// If we're highlighting all the series, reset them
|
|
779
|
-
if (seriesHighlight.length + 1 === config.runtime.seriesKeys.length &&
|
|
774
|
+
if (seriesHighlight.length + 1 === config.runtime.seriesKeys.length && config.visualizationType !== 'Forecasting') {
|
|
780
775
|
highlightReset()
|
|
781
776
|
return
|
|
782
777
|
}
|
|
@@ -806,25 +801,28 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
806
801
|
* - pushes series.dataKey into the series highlight based on the found series.name
|
|
807
802
|
* @param {String} value
|
|
808
803
|
*/
|
|
809
|
-
const pushDataKeyBySeriesName = value => {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
}
|
|
804
|
+
// const pushDataKeyBySeriesName = value => {
|
|
805
|
+
// let matchingSeries = config.series.filter(series => series.name === value.text)
|
|
806
|
+
// if (matchingSeries?.length > 0) {
|
|
807
|
+
// newSeriesHighlight.push(matchingSeries[0].dataKey)
|
|
808
|
+
// }
|
|
809
|
+
// }
|
|
815
810
|
|
|
816
|
-
pushDataKeyBySeriesName(label)
|
|
811
|
+
// pushDataKeyBySeriesName(label)
|
|
817
812
|
|
|
818
813
|
setSeriesHighlight(newSeriesHighlight)
|
|
819
814
|
}
|
|
820
815
|
|
|
821
816
|
// Called on reset button click, unhighlights all data series
|
|
822
817
|
const highlightReset = () => {
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
818
|
+
try {
|
|
819
|
+
const legend = document.getElementById('legend')
|
|
820
|
+
if (!legend) throw new Error('No legend available to set previous focus on.')
|
|
821
|
+
legend.focus()
|
|
822
|
+
} catch (e) {
|
|
823
|
+
console.error('COVE:', e.message)
|
|
827
824
|
}
|
|
825
|
+
setSeriesHighlight([])
|
|
828
826
|
}
|
|
829
827
|
|
|
830
828
|
const section = config.orientation === 'horizontal' ? 'yAxis' : 'xAxis'
|
|
@@ -849,7 +847,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
849
847
|
function getTextWidth(text, font) {
|
|
850
848
|
const canvas = document.createElement('canvas')
|
|
851
849
|
const context = canvas.getContext('2d')
|
|
852
|
-
|
|
850
|
+
if (!context) {
|
|
851
|
+
console.error('2d context not found')
|
|
852
|
+
return
|
|
853
|
+
}
|
|
853
854
|
context.font = font || getComputedStyle(document.body).font
|
|
854
855
|
|
|
855
856
|
return Math.ceil(context.measureText(text).width)
|
|
@@ -875,6 +876,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
875
876
|
|
|
876
877
|
// Format numeric data based on settings in config OR from passed in settings for Additional Columns
|
|
877
878
|
// - use only for old horizontal data - newer formatNumber is in helper/formatNumber
|
|
879
|
+
// TODO: we should combine various formatNumber functions across this project.
|
|
878
880
|
const formatNumber = (num, axis, shouldAbbreviate = false, addColPrefix, addColSuffix, addColRoundTo) => {
|
|
879
881
|
// if num is NaN return num
|
|
880
882
|
if (isNaN(num) || !num) return num
|
|
@@ -897,7 +899,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
897
899
|
if (String(num).indexOf(',') !== -1) num = num.replaceAll(',', '')
|
|
898
900
|
|
|
899
901
|
let original = num
|
|
900
|
-
let stringFormattingOptions = {
|
|
902
|
+
let stringFormattingOptions: any = {
|
|
901
903
|
useGrouping: commas ? true : false // for old chart data table to work right cant just leave this to undefined
|
|
902
904
|
}
|
|
903
905
|
if (axis === 'left' || axis === undefined) {
|
|
@@ -1144,8 +1146,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
1144
1146
|
}
|
|
1145
1147
|
}
|
|
1146
1148
|
|
|
1149
|
+
// TODO: should be part of the DataTransform class.
|
|
1147
1150
|
const clean = data => {
|
|
1148
1151
|
// cleaning is deleting data we need in forecasting charts.
|
|
1152
|
+
if (!Array.isArray(data)) return []
|
|
1149
1153
|
if (config.visualizationType === 'Forecasting') return data
|
|
1150
1154
|
return config?.xAxis?.dataKey ? transform.cleanData(data, config.xAxis.dataKey) : data
|
|
1151
1155
|
}
|
|
@@ -1180,9 +1184,9 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
1180
1184
|
Skip Over Chart Container
|
|
1181
1185
|
</a>
|
|
1182
1186
|
{/* Filters */}
|
|
1183
|
-
{config.filters && !externalFilters && <Filters config={config} setConfig={setConfig} setFilteredData={setFilteredData} filteredData={filteredData} excludedData={excludedData} filterData={filterData}
|
|
1187
|
+
{config.filters && !externalFilters && <Filters config={config} setConfig={setConfig} setFilteredData={setFilteredData} filteredData={filteredData} excludedData={excludedData} filterData={filterData} dimensions={dimensions} />}
|
|
1184
1188
|
{/* Visualization */}
|
|
1185
|
-
{config?.introText && <section className='introText'>{parse(config.introText)}</section>}
|
|
1189
|
+
{config?.introText && config.visualizationType !== 'Spark Line' && <section className='introText'>{parse(config.introText)}</section>}
|
|
1186
1190
|
<div
|
|
1187
1191
|
style={{ marginBottom: config.legend.position !== 'bottom' && config.orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px' }}
|
|
1188
1192
|
className={`chart-container p-relative ${config.legend.position === 'bottom' ? 'bottom' : ''}${config.legend.hide ? ' legend-hidden' : ''}${lineDatapointClass}${barBorderClass} ${contentClasses.join(' ')}`}
|
|
@@ -1193,16 +1197,19 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
1193
1197
|
{/* Sparkline */}
|
|
1194
1198
|
{config.visualizationType === 'Spark Line' && (
|
|
1195
1199
|
<>
|
|
1196
|
-
{
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
)}
|
|
1204
|
-
</ParentSize>
|
|
1200
|
+
{config?.introText && (
|
|
1201
|
+
<section className='introText' style={{ padding: '0px 0 35px' }}>
|
|
1202
|
+
{parse(config.introText)}
|
|
1203
|
+
</section>
|
|
1204
|
+
)}
|
|
1205
|
+
<div style={{ height: `100px`, width: `100%`, ...sparkLineStyles }}>
|
|
1206
|
+
<ParentSize>{parent => <SparkLine width={parent.width} height={parent.height} />}</ParentSize>
|
|
1205
1207
|
</div>
|
|
1208
|
+
{description && (
|
|
1209
|
+
<div className='subtext' style={{ padding: '35px 0 15px' }}>
|
|
1210
|
+
{parse(description)}
|
|
1211
|
+
</div>
|
|
1212
|
+
)}
|
|
1206
1213
|
</>
|
|
1207
1214
|
)}
|
|
1208
1215
|
{!config.legend.hide && config.visualizationType !== 'Spark Line' && <Legend />}
|
|
@@ -1225,9 +1232,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
1225
1232
|
config={config}
|
|
1226
1233
|
rawData={config.data}
|
|
1227
1234
|
runtimeData={filteredData || excludedData}
|
|
1228
|
-
//navigationHandler={navigationHandler} // do we need this? What does it do?
|
|
1229
1235
|
expandDataTable={config.table.expanded}
|
|
1230
|
-
//headerColor={general.headerColor} // have this in map but not chart
|
|
1231
1236
|
columns={config.columns}
|
|
1232
1237
|
showDownloadButton={config.general.showDownloadButton}
|
|
1233
1238
|
runtimeLegend={dynamicLegendItems}
|
|
@@ -1247,6 +1252,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
1247
1252
|
innerContainerRef={innerContainerRef}
|
|
1248
1253
|
outerContainerRef={outerContainerRef}
|
|
1249
1254
|
imageRef={imageId}
|
|
1255
|
+
colorScale={colorScale}
|
|
1250
1256
|
isDebug={isDebug}
|
|
1251
1257
|
isEditor={isEditor}
|
|
1252
1258
|
/>
|
|
@@ -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)
|
|
@@ -10,10 +10,8 @@ import { AreaClosed, LinePath, Bar } from '@visx/shape'
|
|
|
10
10
|
import { Group } from '@visx/group'
|
|
11
11
|
import { bisector } from 'd3-array'
|
|
12
12
|
|
|
13
|
-
const AreaChart =
|
|
14
|
-
|
|
15
|
-
const DEBUG = isDebug
|
|
16
|
-
|
|
13
|
+
const AreaChart = props => {
|
|
14
|
+
const { xScale, yScale, yMax, xMax, handleTooltipMouseOver, handleTooltipMouseOff, isDebug, isBrush, brushData, children } = props
|
|
17
15
|
// import data from context
|
|
18
16
|
let { transformedData: data, config, handleLineType, parseDate, formatDate, formatNumber, seriesHighlight, colorScale, rawData } = useContext(ConfigContext)
|
|
19
17
|
|
|
@@ -23,7 +21,7 @@ const AreaChart = ({ xScale, yScale, yMax, xMax, getXAxisData, getYAxisData, cha
|
|
|
23
21
|
if (isBrush && isDebug) console.log('###AREAchart BRUSH data, xScale, yScale, yMax, xMax', data, xScale, yScale, yMax, xMax)
|
|
24
22
|
|
|
25
23
|
// Draw transparent bars over the chart to get tooltip data
|
|
26
|
-
// Turn
|
|
24
|
+
// Turn isDebug on for additional context.
|
|
27
25
|
if (!data) return
|
|
28
26
|
|
|
29
27
|
// Tooltip helper for getting data to the closest date/category hovered.
|
|
@@ -98,29 +96,29 @@ const AreaChart = ({ xScale, yScale, yMax, xMax, getXAxisData, getYAxisData, cha
|
|
|
98
96
|
<React.Fragment key={index}>
|
|
99
97
|
{/* prettier-ignore */}
|
|
100
98
|
<LinePath
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
99
|
+
data={seriesData}
|
|
100
|
+
x={d => handleX(d)}
|
|
101
|
+
y={d => handleY(d, index, s)}
|
|
102
|
+
stroke={displayArea ? colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[s.dataKey] : s.dataKey) : '#000' : 'transparent'}
|
|
103
|
+
strokeWidth={2}
|
|
104
|
+
strokeOpacity={1}
|
|
105
|
+
shapeRendering='geometricPrecision'
|
|
106
|
+
curve={curveType}
|
|
107
|
+
strokeDasharray={s.type ? handleLineType(s.type) : 0}
|
|
108
|
+
/>
|
|
111
109
|
|
|
112
110
|
{/* prettier-ignore */}
|
|
113
111
|
<AreaClosed
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
112
|
+
key={'area-chart'}
|
|
113
|
+
fill={displayArea ? colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[s.dataKey] : s.dataKey) : '#000' : 'transparent'}
|
|
114
|
+
fillOpacity={transparentArea ? 0.25 : 0.5}
|
|
115
|
+
data={seriesData}
|
|
116
|
+
x={d => handleX(d)}
|
|
117
|
+
y={d => handleY(d, index, s)}
|
|
118
|
+
yScale={yScale}
|
|
119
|
+
curve={curveType}
|
|
120
|
+
strokeDasharray={s.type ? handleLineType(s.type) : 0}
|
|
121
|
+
/>
|
|
124
122
|
{getFirstBrushHandleOnly(children, index)}
|
|
125
123
|
</React.Fragment>
|
|
126
124
|
)
|
|
@@ -128,7 +126,7 @@ const AreaChart = ({ xScale, yScale, yMax, xMax, getXAxisData, getYAxisData, cha
|
|
|
128
126
|
|
|
129
127
|
{/* Transparent bar for tooltips - disable if AreaChart is a brush */}
|
|
130
128
|
{/* prettier-ignore */}
|
|
131
|
-
{!isBrush && <Bar width={Number(xMax)} height={Number(yMax)} fill={
|
|
129
|
+
{!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} />}
|
|
132
130
|
</Group>
|
|
133
131
|
</ErrorBoundary>
|
|
134
132
|
</svg>
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { Line } from '@visx/shape'
|
|
2
2
|
import { Group } from '@visx/group'
|
|
3
|
-
import { useContext, useEffect } from 'react'
|
|
3
|
+
import { useContext, useEffect, useRef, useState } from 'react'
|
|
4
4
|
import ConfigContext from '../ConfigContext'
|
|
5
5
|
import { Text } from '@visx/text'
|
|
6
6
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
7
7
|
import chroma from 'chroma-js'
|
|
8
|
+
import useIntersectionObserver from '../hooks/useIntersectionObserver'
|
|
8
9
|
|
|
9
10
|
export default function DeviationBar({ height, xScale }) {
|
|
10
|
-
const { transformedData: data, config, formatNumber, twoColorPalette, getTextWidth, updateConfig, parseDate, formatDate } = useContext(ConfigContext)
|
|
11
|
-
|
|
12
|
-
if (!config || config?.series?.length !== 1 || config.orientation !== 'horizontal') return
|
|
13
|
-
|
|
11
|
+
const { transformedData: data, config, formatNumber, twoColorPalette, getTextWidth, updateConfig, parseDate, formatDate, currentViewport } = useContext(ConfigContext)
|
|
14
12
|
const { barStyle, tipRounding, roundingStyle, twoColor } = config
|
|
15
|
-
|
|
13
|
+
const barRefs = useRef([])
|
|
14
|
+
const [windowWidth, setWindowWidth] = useState(window.innerWidth)
|
|
16
15
|
const radius = roundingStyle === 'standard' ? '8px' : roundingStyle === 'shallow' ? '5px' : roundingStyle === 'finger' ? '15px' : '0px'
|
|
17
16
|
const fontSize = { small: 16, medium: 18, large: 20 }
|
|
18
17
|
const isRounded = config.barStyle === 'rounded'
|
|
@@ -76,14 +75,57 @@ export default function DeviationBar({ height, xScale }) {
|
|
|
76
75
|
}
|
|
77
76
|
targetLabel.calculate()
|
|
78
77
|
|
|
78
|
+
const targetRef = useRef(null)
|
|
79
|
+
|
|
80
|
+
const entry = useIntersectionObserver(targetRef, {})
|
|
81
|
+
|
|
79
82
|
useEffect(() => {
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
const handleResize = () => {
|
|
84
|
+
setWindowWidth(window.innerWidth)
|
|
85
|
+
barRefs.current.forEach(bar => {
|
|
86
|
+
bar.style.transition = 'none'
|
|
87
|
+
bar.style.transform = 'translate(0) scale(1)'
|
|
88
|
+
})
|
|
82
89
|
}
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
window.addEventListener('resize', handleResize)
|
|
91
|
+
|
|
92
|
+
return () => {
|
|
93
|
+
window.removeEventListener('resize', handleResize)
|
|
85
94
|
}
|
|
86
|
-
}, [
|
|
95
|
+
}, [])
|
|
96
|
+
const [animatedChart, setAnimatedChart] = useState(false)
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (entry?.isIntersecting) {
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
setAnimatedChart(true)
|
|
102
|
+
}, 100)
|
|
103
|
+
}
|
|
104
|
+
}, [entry?.isIntersecting, config.animate]) // eslint-disable-line
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
barRefs.current.forEach((bar, i) => {
|
|
108
|
+
if (config.animate) {
|
|
109
|
+
const normalizedTarget = (target / maxVal) * 100
|
|
110
|
+
bar.style.opacity = '0'
|
|
111
|
+
bar.style.transform = `translate(${normalizedTarget / 1.07}%) scale(0, 1)`
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
bar.style.opacity = '1'
|
|
114
|
+
bar.style.transform = 'translate(0) scale(1)'
|
|
115
|
+
bar.style.transition = 'transform 0.5s ease'
|
|
116
|
+
}, 100)
|
|
117
|
+
} else {
|
|
118
|
+
bar.style.transition = 'none'
|
|
119
|
+
bar.style.opacity = '0'
|
|
120
|
+
}
|
|
121
|
+
if (!config.animate) {
|
|
122
|
+
bar.style.transition = 'none'
|
|
123
|
+
bar.style.opacity = '1'
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
}, [config.animate, config, animatedChart])
|
|
127
|
+
|
|
128
|
+
if (!config || config?.series?.length !== 1) return <></>
|
|
87
129
|
|
|
88
130
|
return (
|
|
89
131
|
<ErrorBoundary component='Deviation Bar'>
|
|
@@ -122,7 +164,6 @@ export default function DeviationBar({ height, xScale }) {
|
|
|
122
164
|
const fill = isBarColorDark ? '#FFFFFF' : '#000000'
|
|
123
165
|
|
|
124
166
|
let textProps = getTextProps(config.isLollipopChart, textFits, lollipopShapeSize, fill)
|
|
125
|
-
|
|
126
167
|
// tooltips
|
|
127
168
|
const xAxisValue = formatNumber(barValue, 'left')
|
|
128
169
|
const yAxisValue = config.runtime.yAxis.type === 'date' ? formatDate(parseDate(data[index][config.runtime.originalXAxis.dataKey])) : data[index][config.runtime.originalXAxis.dataKey]
|
|
@@ -135,7 +176,19 @@ export default function DeviationBar({ height, xScale }) {
|
|
|
135
176
|
|
|
136
177
|
return (
|
|
137
178
|
<Group key={`deviation-bar-${config.orientation}-${seriesKey}-${index}`}>
|
|
138
|
-
<foreignObject
|
|
179
|
+
<foreignObject
|
|
180
|
+
ref={el => {
|
|
181
|
+
// targetRef.current = el
|
|
182
|
+
barRefs.current[index] = el
|
|
183
|
+
}}
|
|
184
|
+
x={barX}
|
|
185
|
+
y={barY}
|
|
186
|
+
width={barWidth}
|
|
187
|
+
height={barHeight}
|
|
188
|
+
style={{ border: `${borderWidth}px solid #333`, backgroundColor: barColor[barPosition], ...borderRadius }}
|
|
189
|
+
data-tooltip-html={tooltip}
|
|
190
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
191
|
+
/>
|
|
139
192
|
{config.yAxis.displayNumbersOnBar && (
|
|
140
193
|
<Text verticalAnchor='middle' x={textX} y={textY} {...textProps[barPosition]}>
|
|
141
194
|
{formatNumber(d[seriesKey], 'left')}
|
|
@@ -155,6 +208,7 @@ export default function DeviationBar({ height, xScale }) {
|
|
|
155
208
|
|
|
156
209
|
{shouldShowTargetLine && <Line from={{ x: targetX, y: 0 }} to={{ x: targetX, y: height }} stroke='#333' strokeWidth={2} />}
|
|
157
210
|
</Group>
|
|
211
|
+
<foreignObject y={height / 2} ref={targetRef}></foreignObject>
|
|
158
212
|
</ErrorBoundary>
|
|
159
213
|
)
|
|
160
214
|
}
|