@cdc/core 4.23.5 → 4.23.6
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/assets/icon-linear-gauge.svg +1 -0
- package/components/AdvancedEditor.jsx +7 -1
- package/components/DataTable.jsx +68 -41
- package/components/Filters.jsx +2 -0
- package/components/{CoveMediaControls.jsx → MediaControls.jsx} +6 -6
- package/components/RepeatableGroup.jsx +37 -0
- package/components/inputs/InputSelect.jsx +1 -1
- package/components/ui/Accordion.jsx +3 -0
- package/helpers/DataTransform.js +65 -65
- package/helpers/cove/date.js +9 -0
- package/helpers/cove/number.js +139 -0
- package/helpers/coveUpdateWorker.js +15 -0
- package/helpers/fetchRemoteData.js +12 -2
- package/helpers/ver/4.23.js +10 -0
- package/package.json +2 -2
- package/styles/v2/components/input/_input.scss +3 -3
- package/styles/v2/layout/_component.scss +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96.31 82.55"><path d="M18.29,39.57c0-1.03,.23-1.79,.68-2.28,.45-.49,1.06-.74,1.83-.74s1.42,.25,1.87,.74c.45,.49,.68,1.25,.68,2.29s-.23,1.79-.68,2.28c-.45,.5-1.06,.74-1.83,.74s-1.42-.25-1.87-.74c-.45-.49-.68-1.25-.68-2.29Zm1.71-.02c0,.75,.09,1.25,.26,1.5,.13,.19,.31,.28,.54,.28s.42-.09,.55-.28c.17-.25,.25-.75,.25-1.5s-.08-1.24-.25-1.49c-.13-.19-.31-.29-.55-.29s-.41,.09-.54,.28c-.17,.26-.26,.76-.26,1.5Zm2.47,9.08h-1.62l6.1-12.09h1.58l-6.05,12.09Zm3.55-3.01c0-1.03,.23-1.79,.68-2.28,.45-.49,1.07-.74,1.85-.74s1.4,.25,1.86,.74c.45,.5,.68,1.26,.68,2.28s-.23,1.79-.68,2.29c-.45,.49-1.06,.74-1.83,.74s-1.42-.25-1.87-.74c-.45-.49-.68-1.26-.68-2.29Zm1.71,0c0,.75,.09,1.24,.26,1.49,.13,.19,.31,.29,.54,.29s.41-.09,.54-.28c.17-.25,.26-.75,.26-1.5s-.08-1.24-.25-1.5c-.13-.19-.31-.28-.55-.28s-.41,.09-.54,.28c-.17,.26-.26,.76-.26,1.5Z"/><path d="M92.33,52.83v-20.42c0-1.61-1.31-2.93-2.93-2.93H6.65c-1.61,0-2.93,1.31-2.93,2.93v20.42c0,1.61,1.31,2.93,2.93,2.93H89.4c1.61,0,2.93-1.31,2.93-2.93Zm-52.34-2.93H9.58v-14.56h30.41v14.56Zm5.86-14.56h40.63v14.56H45.85v-14.56Z"/></svg>
|
|
@@ -8,7 +8,12 @@ export const AdvancedEditor = ({ loadConfig, state, convertStateToConfig }) => {
|
|
|
8
8
|
const [configTextboxValue, setConfigTextbox] = useState({})
|
|
9
9
|
|
|
10
10
|
useEffect(() => {
|
|
11
|
-
|
|
11
|
+
let parsedData = state
|
|
12
|
+
if (state.type !== 'dashboard') {
|
|
13
|
+
parsedData = convertStateToConfig()
|
|
14
|
+
} else {
|
|
15
|
+
parsedData = JSON.parse(JSON.stringify(parsedData))
|
|
16
|
+
}
|
|
12
17
|
const formattedData = JSON.stringify(parsedData, undefined, 2)
|
|
13
18
|
|
|
14
19
|
setConfigTextbox(formattedData)
|
|
@@ -16,6 +21,7 @@ export const AdvancedEditor = ({ loadConfig, state, convertStateToConfig }) => {
|
|
|
16
21
|
|
|
17
22
|
const typeLookup = {
|
|
18
23
|
chart: ['Charts', 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/bar-chart.html', <ChartIcon />],
|
|
24
|
+
dashboard: ['Dashboard', 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/bar-chart.html', <ChartIcon />],
|
|
19
25
|
map: ['Maps', 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/data-map.html', <MapIcon />],
|
|
20
26
|
'markup-include': ['Markup Include', 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/Markup-Include.html', <MarkupIncludeIcon />]
|
|
21
27
|
}
|
package/components/DataTable.jsx
CHANGED
|
@@ -1,22 +1,38 @@
|
|
|
1
1
|
import React, { useEffect, useState, memo, useMemo } from 'react'
|
|
2
2
|
|
|
3
3
|
import Papa from 'papaparse'
|
|
4
|
-
import ExternalIcon from '../assets/external-link.svg'
|
|
4
|
+
import ExternalIcon from '../assets/external-link.svg'
|
|
5
5
|
import Icon from '@cdc/core/components/ui/Icon'
|
|
6
6
|
|
|
7
7
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
8
8
|
import LegendCircle from '@cdc/core/components/LegendCircle'
|
|
9
|
-
import
|
|
9
|
+
import MediaControls from '@cdc/core/components/MediaControls'
|
|
10
|
+
|
|
11
|
+
import { parseDate, formatDate } from '@cdc/core/helpers/cove/date'
|
|
12
|
+
import { formatNumber } from '@cdc/core/helpers/cove/number'
|
|
10
13
|
|
|
11
14
|
import Loading from '@cdc/core/components/Loading'
|
|
12
15
|
|
|
16
|
+
// FILE REVIEW
|
|
17
|
+
// TODO: Remove eslint-disable jsx/a11y/non-interactive-tabindex and handle appropriately
|
|
18
|
+
// TODO: Move ExternalIcon to core Icon component
|
|
19
|
+
// TODO: use destructuring
|
|
20
|
+
// TODO: @tturnerswdev33 - It looks like there's an unused variable setFilteredCountryCode that was added
|
|
21
|
+
// TODO: @tturnerswdev33 - change function declarations to arrow functions
|
|
22
|
+
// TODO: @tturnerswdev33 - move caption so that useMemo is not rendered conditionally
|
|
23
|
+
|
|
13
24
|
/* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
|
|
14
25
|
const DataTable = props => {
|
|
15
|
-
const { config, tableTitle, indexTitle, vizTitle, rawData, runtimeData, headerColor, expandDataTable, columns, displayDataAsText,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (isDebug)
|
|
19
|
-
|
|
26
|
+
const { config, tableTitle, indexTitle, vizTitle, rawData, runtimeData, headerColor, expandDataTable, columns, displayDataAsText, applyLegendToRow, displayGeoName, navigationHandler, viewport, formatLegendLocation, tabbingId, isDebug } = props
|
|
27
|
+
|
|
28
|
+
/* eslint-disable no-console */
|
|
29
|
+
if (isDebug) {
|
|
30
|
+
console.log('core/DataTable: props=', props)
|
|
31
|
+
console.log('core/DataTable: runtimeData=', runtimeData)
|
|
32
|
+
console.log('core/DataTable: rawData=', rawData)
|
|
33
|
+
console.log('core/DataTable: config=', config)
|
|
34
|
+
}
|
|
35
|
+
/* eslint-enable no-console */
|
|
20
36
|
|
|
21
37
|
const [expanded, setExpanded] = useState(expandDataTable)
|
|
22
38
|
|
|
@@ -137,7 +153,7 @@ const DataTable = props => {
|
|
|
137
153
|
const DownloadButton = memo(() => {
|
|
138
154
|
if (rawData !== undefined) {
|
|
139
155
|
let csvData
|
|
140
|
-
if (config.type === 'chart' || config.general.type === 'bubble') {
|
|
156
|
+
if (config.type === 'chart' || config.general.type === 'bubble' || !config.table.showFullGeoNameInCSV) {
|
|
141
157
|
// Just Unparse
|
|
142
158
|
csvData = Papa.unparse(rawData)
|
|
143
159
|
} else if ((config.general.geoType !== 'single-state' && config.general.geoType !== 'us-county') || config.general.type === 'us-geocode') {
|
|
@@ -299,12 +315,17 @@ const DataTable = props => {
|
|
|
299
315
|
}
|
|
300
316
|
}
|
|
301
317
|
|
|
302
|
-
|
|
318
|
+
const genChartHeader = (columns, data) => {
|
|
303
319
|
return (
|
|
304
320
|
<tr>
|
|
305
321
|
{dataSeriesColumns().map(column => {
|
|
306
322
|
let custLabel = getLabel(column) ? getLabel(column) : column
|
|
307
323
|
let text = column === config.xAxis.dataKey ? config.table.indexLabel : custLabel
|
|
324
|
+
|
|
325
|
+
// If a user sets the name on a series use that.
|
|
326
|
+
let userUpdatedSeriesName = config.series.filter(series => series.dataKey === column)?.[0]?.name
|
|
327
|
+
if (userUpdatedSeriesName) text = userUpdatedSeriesName
|
|
328
|
+
|
|
308
329
|
return (
|
|
309
330
|
<th
|
|
310
331
|
key={`col-header-${column}`}
|
|
@@ -334,38 +355,44 @@ const DataTable = props => {
|
|
|
334
355
|
)
|
|
335
356
|
}
|
|
336
357
|
|
|
337
|
-
|
|
338
|
-
const
|
|
358
|
+
const genChartRows = rows => {
|
|
359
|
+
const allRows = rows.map(row => {
|
|
339
360
|
return (
|
|
340
361
|
<tr role='row'>
|
|
341
|
-
{dataSeriesColumns()
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
362
|
+
{dataSeriesColumns().map(column => {
|
|
363
|
+
const rowObj = runtimeData[row]
|
|
364
|
+
let cellValue // placeholder for formatting below
|
|
365
|
+
let labelValue = rowObj[column] // just raw X axis string
|
|
366
|
+
if (column === config.xAxis.dataKey) {
|
|
367
|
+
// not the prettiest, but helper functions work nicely here.
|
|
368
|
+
cellValue = <>{config.xAxis.type === 'date' ? formatDate(config.xAxis.dateDisplayFormat, parseDate(config.xAxis.dateParseFormat, labelValue)) : labelValue}</>
|
|
369
|
+
} else {
|
|
370
|
+
let resolvedAxis = ''
|
|
371
|
+
let leftAxisItems = config.series.filter(item => item?.axis === 'Left')
|
|
372
|
+
let rightAxisItems = config.series.filter(item => item?.axis === 'Right')
|
|
373
|
+
console.log('column', column)
|
|
374
|
+
|
|
375
|
+
leftAxisItems.map(leftSeriesItem => {
|
|
376
|
+
if (leftSeriesItem.dataKey === column) resolvedAxis = 'left'
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
rightAxisItems.map(rightSeriesItem => {
|
|
380
|
+
if (rightSeriesItem.dataKey === column) resolvedAxis = 'right'
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
cellValue = formatNumber(runtimeData[row][column], resolvedAxis, true, config)
|
|
384
|
+
}
|
|
356
385
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
)
|
|
364
|
-
})}
|
|
386
|
+
return (
|
|
387
|
+
<td tabIndex='0' role='gridcell' id={`${runtimeData[config.runtime.originalXAxis.dataKey]}--${row}`}>
|
|
388
|
+
{cellValue}
|
|
389
|
+
</td>
|
|
390
|
+
)
|
|
391
|
+
})}
|
|
365
392
|
</tr>
|
|
366
393
|
)
|
|
367
394
|
})
|
|
368
|
-
return
|
|
395
|
+
return allRows
|
|
369
396
|
}
|
|
370
397
|
|
|
371
398
|
const limitHeight = {
|
|
@@ -437,10 +464,10 @@ const DataTable = props => {
|
|
|
437
464
|
|
|
438
465
|
return (
|
|
439
466
|
<ErrorBoundary component='DataTable'>
|
|
440
|
-
<
|
|
441
|
-
<
|
|
467
|
+
<MediaControls.Section classes={['download-links']}>
|
|
468
|
+
<MediaControls.Link config={config} />
|
|
442
469
|
{(config.table.download || config.general.showDownloadButton) && <DownloadButton />}
|
|
443
|
-
</
|
|
470
|
+
</MediaControls.Section>
|
|
444
471
|
<section id={tabbingId.replace('#', '')} className={`data-table-container ${viewport}`} aria-label={accessibilityLabel}>
|
|
445
472
|
<a id='skip-nav' className='cdcdataviz-sr-only-focusable' href={`#${skipId}`}>
|
|
446
473
|
Skip Navigation or Skip to Content
|
|
@@ -590,10 +617,10 @@ const DataTable = props => {
|
|
|
590
617
|
return (
|
|
591
618
|
<ErrorBoundary component='DataTable'>
|
|
592
619
|
{/* cove media results in error so disabling for now (TT)
|
|
593
|
-
<
|
|
594
|
-
<
|
|
620
|
+
<MediaControls.Section classes={['download-links']}>
|
|
621
|
+
<MediaControls.Link config={config} />
|
|
595
622
|
{config.general.showDownloadButton && <DownloadButton />}
|
|
596
|
-
</
|
|
623
|
+
</MediaControls.Section>
|
|
597
624
|
*/}
|
|
598
625
|
<section id={tabbingId.replace('#', '')} className={`data-table-container ${viewport}`} aria-label={accessibilityLabel}>
|
|
599
626
|
<a id='skip-nav' className='cdcdataviz-sr-only-focusable' href={`#${skipId}`}>
|
package/components/Filters.jsx
CHANGED
|
@@ -312,6 +312,8 @@ const Filters = props => {
|
|
|
312
312
|
delete filtersToLoop.fromHash
|
|
313
313
|
|
|
314
314
|
return filtersToLoop.map((singleFilter, outerIndex) => {
|
|
315
|
+
if(singleFilter.showDropdown === false) return
|
|
316
|
+
|
|
315
317
|
const values = []
|
|
316
318
|
const pillValues = []
|
|
317
319
|
const tabValues = []
|
|
@@ -134,11 +134,11 @@ const Section = ({ children, classes }) => {
|
|
|
134
134
|
)
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
const
|
|
137
|
+
const MediaControls = () => null
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
MediaControls.Section = Section
|
|
140
|
+
MediaControls.Link = Link
|
|
141
|
+
MediaControls.Button = Button
|
|
142
|
+
MediaControls.generateMedia = generateMedia
|
|
143
143
|
|
|
144
|
-
export default
|
|
144
|
+
export default MediaControls
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Accordion, AccordionItem, AccordionItemHeading, AccordionItemPanel, AccordionItemButton } from 'react-accessible-accordion'
|
|
3
|
+
import { Draggable } from '@hello-pangea/dnd'
|
|
4
|
+
import Icon from '@cdc/core/components/ui/Icon'
|
|
5
|
+
|
|
6
|
+
const RepeatableGroupSection = props => {
|
|
7
|
+
return <div className='repeatable-group'>{props.children}</div>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const RepeatableGroupItem = props => {
|
|
11
|
+
const { item, index, title } = props
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Draggable key={item} draggableId={`draggable-repeatable-item-${item}`} index={index}>
|
|
15
|
+
{(provided, snapshot) => (
|
|
16
|
+
<div key={index} className={snapshot.isDragging ? 'currently-dragging' : ''} ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
|
|
17
|
+
<Accordion allowZeroExpanded>
|
|
18
|
+
<AccordionItem className='series-item series-item--chart'>
|
|
19
|
+
<AccordionItemHeading className='series-item__title'>
|
|
20
|
+
<AccordionItemButton className={''}>
|
|
21
|
+
<Icon display='move' size={15} style={{ cursor: 'default' }} />
|
|
22
|
+
Title Section
|
|
23
|
+
</AccordionItemButton>
|
|
24
|
+
</AccordionItemHeading>
|
|
25
|
+
<AccordionItemPanel>panel section to be built</AccordionItemPanel>
|
|
26
|
+
</AccordionItem>
|
|
27
|
+
</Accordion>
|
|
28
|
+
</div>
|
|
29
|
+
)}
|
|
30
|
+
</Draggable>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const RepeatableGroup = {
|
|
35
|
+
Section: RepeatableGroupSection,
|
|
36
|
+
Item: RepeatableGroupItem
|
|
37
|
+
}
|
|
@@ -33,7 +33,7 @@ const InputSelect = memo(({ label, value, options, fieldName, section = null, su
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
return (
|
|
36
|
-
<label>
|
|
36
|
+
<label style={{ width: '100%', display: 'block' }}>
|
|
37
37
|
{label && <span className='edit-label cove-input__label'>{label}</span>}
|
|
38
38
|
<select
|
|
39
39
|
className={required && !value ? 'warning' : ''}
|
|
@@ -21,6 +21,7 @@ const Accordion = ({ children }) => {
|
|
|
21
21
|
<AccordionItem className='cove-accordion__item' key={index}>
|
|
22
22
|
<AccordionItemHeading className='cove-accordion__heading'>
|
|
23
23
|
<AccordionItemButton className='cove-accordion__button'>
|
|
24
|
+
{section.props.icon}
|
|
24
25
|
{section.props.title}
|
|
25
26
|
{section.props.tooltipText ? (
|
|
26
27
|
<Tooltip position='bottom'>
|
|
@@ -43,6 +44,8 @@ const Accordion = ({ children }) => {
|
|
|
43
44
|
Accordion.Section = AccordionSection
|
|
44
45
|
|
|
45
46
|
Accordion.Section.propTypes = {
|
|
47
|
+
/* Icon for the accordion label */
|
|
48
|
+
icon: PropTypes.node,
|
|
46
49
|
/* Title for the accordion label*/
|
|
47
50
|
title: PropTypes.string,
|
|
48
51
|
/* Tooltip for the accordion label*/
|
package/helpers/DataTransform.js
CHANGED
|
@@ -11,109 +11,109 @@ export class DataTransform {
|
|
|
11
11
|
|
|
12
12
|
//Performs standardizations that can be completed automatically without use input
|
|
13
13
|
autoStandardize(data) {
|
|
14
|
-
const errorsFound = []
|
|
14
|
+
const errorsFound = []
|
|
15
15
|
|
|
16
16
|
// Empty data
|
|
17
17
|
if (0 === data.length) {
|
|
18
|
-
errorsFound.push(this.constants.errorMessageEmptyData)
|
|
18
|
+
errorsFound.push(this.constants.errorMessageEmptyData)
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
// Does it have the correct data structure?
|
|
22
22
|
if (!data.filter || data.filter(row => typeof row !== 'object').length > 0) {
|
|
23
|
-
errorsFound.push(this.constants.errorMessageFormat)
|
|
23
|
+
errorsFound.push(this.constants.errorMessageFormat)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
if (errorsFound.length > 0) {
|
|
27
|
-
console.error(errorsFound)
|
|
28
|
-
return undefined
|
|
27
|
+
console.error(errorsFound)
|
|
28
|
+
return undefined
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
//Convert array of arrays, to array of objects
|
|
32
32
|
if (data.filter(row => row.constructor !== Object).length > 0) {
|
|
33
|
-
let standardizedData = []
|
|
33
|
+
let standardizedData = []
|
|
34
34
|
for (let row = 1; row < data.length; row++) {
|
|
35
|
-
let standardizedRow = {}
|
|
35
|
+
let standardizedRow = {}
|
|
36
36
|
data[row].forEach((datum, col) => {
|
|
37
|
-
standardizedRow[data[0][col]] = datum
|
|
37
|
+
standardizedRow[data[0][col]] = datum
|
|
38
38
|
})
|
|
39
|
-
standardizedData.push(standardizedRow)
|
|
39
|
+
standardizedData.push(standardizedRow)
|
|
40
40
|
}
|
|
41
|
-
data = standardizedData
|
|
41
|
+
data = standardizedData
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
return data
|
|
44
|
+
return data
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
//Performs standardizations based on developer provided description of the data
|
|
48
48
|
developerStandardize(data, description) {
|
|
49
49
|
//Validate the description object
|
|
50
50
|
if (!description) {
|
|
51
|
-
return undefined
|
|
51
|
+
return undefined
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
if (description.horizontal === undefined || description.series === undefined) {
|
|
55
|
-
return undefined
|
|
55
|
+
return undefined
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
if (description.series === true && description.horizontal === false && description.singleRow === undefined) {
|
|
59
|
-
return undefined
|
|
59
|
+
return undefined
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
if (description.horizontal === true) {
|
|
63
63
|
if (description.series === true) {
|
|
64
64
|
if (!description.seriesKey) {
|
|
65
|
-
return undefined
|
|
65
|
+
return undefined
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
let standardizedMapped = {}
|
|
69
|
-
let standardized = []
|
|
68
|
+
let standardizedMapped = {}
|
|
69
|
+
let standardized = []
|
|
70
70
|
data.forEach(row => {
|
|
71
|
-
let nonNumericKeys = []
|
|
71
|
+
let nonNumericKeys = []
|
|
72
72
|
Object.keys(row).forEach(key => {
|
|
73
73
|
if (key !== description.seriesKey && isNaN(parseFloat(row[key]))) {
|
|
74
|
-
nonNumericKeys.push(key)
|
|
74
|
+
nonNumericKeys.push(key)
|
|
75
75
|
}
|
|
76
76
|
})
|
|
77
77
|
|
|
78
78
|
Object.keys(row).forEach(key => {
|
|
79
79
|
if (key !== description.seriesKey && nonNumericKeys.indexOf(key) === -1) {
|
|
80
|
-
let uniqueKey = key + '|' + nonNumericKeys.map(nonNumericKey => nonNumericKey + '=' + row[nonNumericKey])
|
|
80
|
+
let uniqueKey = key + '|' + nonNumericKeys.map(nonNumericKey => nonNumericKey + '=' + row[nonNumericKey])
|
|
81
81
|
if (!standardizedMapped[uniqueKey]) {
|
|
82
|
-
standardizedMapped[uniqueKey] = { [row[description.seriesKey]]: row[key], key }
|
|
82
|
+
standardizedMapped[uniqueKey] = { [row[description.seriesKey]]: row[key], key }
|
|
83
83
|
nonNumericKeys.forEach(nonNumericKey => {
|
|
84
|
-
standardizedMapped[uniqueKey][nonNumericKey] = row[nonNumericKey]
|
|
84
|
+
standardizedMapped[uniqueKey][nonNumericKey] = row[nonNumericKey]
|
|
85
85
|
})
|
|
86
86
|
}
|
|
87
|
-
standardizedMapped[uniqueKey][row[description.seriesKey]] = row[key]
|
|
87
|
+
standardizedMapped[uniqueKey][row[description.seriesKey]] = row[key]
|
|
88
88
|
}
|
|
89
89
|
})
|
|
90
90
|
})
|
|
91
91
|
|
|
92
92
|
Object.keys(standardizedMapped).forEach(key => {
|
|
93
|
-
standardized.push(standardizedMapped[key])
|
|
93
|
+
standardized.push(standardizedMapped[key])
|
|
94
94
|
})
|
|
95
95
|
|
|
96
|
-
return standardized
|
|
96
|
+
return standardized
|
|
97
97
|
} else {
|
|
98
|
-
let standardized = []
|
|
98
|
+
let standardized = []
|
|
99
99
|
|
|
100
100
|
data.forEach(row => {
|
|
101
|
-
let nonNumericKeys = []
|
|
101
|
+
let nonNumericKeys = []
|
|
102
102
|
Object.keys(row).forEach(key => {
|
|
103
103
|
if (isNaN(parseFloat(row[key]))) {
|
|
104
|
-
nonNumericKeys.push(key)
|
|
104
|
+
nonNumericKeys.push(key)
|
|
105
105
|
}
|
|
106
106
|
})
|
|
107
107
|
|
|
108
108
|
Object.keys(row).forEach(key => {
|
|
109
109
|
if (nonNumericKeys.indexOf(key) === -1) {
|
|
110
|
-
let newRow = { key, value: row[key] }
|
|
110
|
+
let newRow = { key, value: row[key] }
|
|
111
111
|
|
|
112
112
|
nonNumericKeys.forEach(nonNumericKey => {
|
|
113
|
-
newRow[nonNumericKey] = row[nonNumericKey]
|
|
113
|
+
newRow[nonNumericKey] = row[nonNumericKey]
|
|
114
114
|
})
|
|
115
115
|
|
|
116
|
-
standardized.push(newRow)
|
|
116
|
+
standardized.push(newRow)
|
|
117
117
|
}
|
|
118
118
|
})
|
|
119
119
|
})
|
|
@@ -122,43 +122,43 @@ export class DataTransform {
|
|
|
122
122
|
}
|
|
123
123
|
} else if (description.series === true && description.singleRow === false) {
|
|
124
124
|
if (description.seriesKey !== undefined && description.xKey !== undefined && (description.valueKey !== undefined || (description.valueKeys !== undefined && description.valueKeys.length > 0))) {
|
|
125
|
-
if(description.valueKeys !== undefined){
|
|
126
|
-
let standardizedMapped = {}
|
|
127
|
-
let standardized = []
|
|
128
|
-
let valueKeys = description.valueKeys
|
|
129
|
-
if(description.ignoredKeys && description.ignoredKeys.length > 0){
|
|
130
|
-
valueKeys = valueKeys.concat(description.ignoredKeys)
|
|
125
|
+
if (description.valueKeys !== undefined) {
|
|
126
|
+
let standardizedMapped = {}
|
|
127
|
+
let standardized = []
|
|
128
|
+
let valueKeys = description.valueKeys
|
|
129
|
+
if (description.ignoredKeys && description.ignoredKeys.length > 0) {
|
|
130
|
+
valueKeys = valueKeys.concat(description.ignoredKeys)
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
data.forEach(row => {
|
|
134
134
|
valueKeys.forEach(valueKey => {
|
|
135
|
-
let extraKeys = []
|
|
136
|
-
let uniqueKey = row[description.xKey] + '|' + valueKey
|
|
135
|
+
let extraKeys = []
|
|
136
|
+
let uniqueKey = row[description.xKey] + '|' + valueKey
|
|
137
137
|
Object.keys(row).forEach(key => {
|
|
138
138
|
if (key !== description.xKey && key !== description.seriesKey && valueKeys.indexOf(key) === -1) {
|
|
139
|
-
uniqueKey += '|' + key + '=' + row[key]
|
|
140
|
-
extraKeys.push(key)
|
|
139
|
+
uniqueKey += '|' + key + '=' + row[key]
|
|
140
|
+
extraKeys.push(key)
|
|
141
141
|
}
|
|
142
142
|
})
|
|
143
143
|
|
|
144
|
-
if(!standardizedMapped[uniqueKey]){
|
|
145
|
-
standardizedMapped[uniqueKey] = { [description.xKey]: row[description.xKey], '**Numeric Value Property**': valueKey }
|
|
144
|
+
if (!standardizedMapped[uniqueKey]) {
|
|
145
|
+
standardizedMapped[uniqueKey] = { [description.xKey]: row[description.xKey], '**Numeric Value Property**': valueKey }
|
|
146
146
|
extraKeys.forEach(key => {
|
|
147
|
-
standardizedMapped[uniqueKey][key] = row[key]
|
|
147
|
+
standardizedMapped[uniqueKey][key] = row[key]
|
|
148
148
|
})
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
standardizedMapped[uniqueKey][row[description.seriesKey]] = row[valueKey]
|
|
152
|
-
})
|
|
151
|
+
standardizedMapped[uniqueKey][row[description.seriesKey]] = row[valueKey]
|
|
152
|
+
})
|
|
153
153
|
})
|
|
154
154
|
|
|
155
155
|
Object.keys(standardizedMapped).forEach(key => {
|
|
156
|
-
if(!description.ignoredKeys || description.ignoredKeys.indexOf(standardizedMapped[key]['**Numeric Value Property**']) === -1){
|
|
157
|
-
standardized.push(standardizedMapped[key])
|
|
156
|
+
if (!description.ignoredKeys || description.ignoredKeys.indexOf(standardizedMapped[key]['**Numeric Value Property**']) === -1) {
|
|
157
|
+
standardized.push(standardizedMapped[key])
|
|
158
158
|
}
|
|
159
159
|
})
|
|
160
160
|
|
|
161
|
-
return standardized
|
|
161
|
+
return standardized
|
|
162
162
|
} else {
|
|
163
163
|
let standardizedMapped = {}
|
|
164
164
|
let standardized = []
|
|
@@ -190,14 +190,14 @@ export class DataTransform {
|
|
|
190
190
|
return standardized
|
|
191
191
|
}
|
|
192
192
|
} else {
|
|
193
|
-
return undefined
|
|
193
|
+
return undefined
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
return data
|
|
197
|
+
return data
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
|
|
200
|
+
/**
|
|
201
201
|
* cleanData
|
|
202
202
|
*
|
|
203
203
|
// This cleans a data set by:
|
|
@@ -207,21 +207,21 @@ export class DataTransform {
|
|
|
207
207
|
*
|
|
208
208
|
* Inputs: data as array, excludeKey indicates which key to use to NOT clean
|
|
209
209
|
* Example: "Date" should not be cleaned if part of the data
|
|
210
|
-
*
|
|
210
|
+
*
|
|
211
211
|
* Output: returns the cleanedData
|
|
212
|
-
*
|
|
212
|
+
*
|
|
213
213
|
* Set testing = true if you need to see before and after data
|
|
214
|
-
*
|
|
214
|
+
*
|
|
215
215
|
*/
|
|
216
|
-
cleanData
|
|
216
|
+
cleanData(data, excludeKey, testing = false) {
|
|
217
217
|
let cleanedupData = []
|
|
218
218
|
if (testing) console.log('## Data to clean=', data)
|
|
219
219
|
if (excludeKey === undefined) {
|
|
220
220
|
console.log('COVE: cleanData excludeKey undefined')
|
|
221
|
-
return data // because no excludeKey
|
|
221
|
+
return data // because no excludeKey
|
|
222
222
|
}
|
|
223
223
|
data.forEach(function (d, i) {
|
|
224
|
-
if (testing) console.log(
|
|
224
|
+
if (testing) console.log('clean', i, ' d', d)
|
|
225
225
|
let cleaned = {}
|
|
226
226
|
Object.keys(d).forEach(function (key) {
|
|
227
227
|
if (key === excludeKey) {
|
|
@@ -229,27 +229,27 @@ export class DataTransform {
|
|
|
229
229
|
cleaned[key] = d[key]
|
|
230
230
|
} else {
|
|
231
231
|
// remove comma and dollar signs
|
|
232
|
-
if (testing) console.log(
|
|
233
|
-
let tmp =
|
|
232
|
+
if (testing) console.log('typeof d[key] is ', typeof d[key])
|
|
233
|
+
let tmp = ''
|
|
234
234
|
if (typeof d[key] === 'string') {
|
|
235
235
|
tmp = d[key] !== null && d[key] !== '' ? d[key].replace(/[,\$]/g, '') : ''
|
|
236
236
|
} else {
|
|
237
|
-
tmp =
|
|
237
|
+
tmp = d[key] !== null && d[key] !== '' ? d[key] : ''
|
|
238
238
|
}
|
|
239
239
|
if ((tmp !== '' && tmp !== null && !isNaN(tmp)) || (tmp !== '' && tmp !== null && /\d+\.?\d*/.test(tmp))) {
|
|
240
240
|
cleaned[key] = tmp
|
|
241
|
-
} else {
|
|
241
|
+
} else {
|
|
242
|
+
cleaned[key] = ''
|
|
243
|
+
}
|
|
242
244
|
// if you get here, then return nothing to skip bad data point
|
|
243
245
|
}
|
|
244
246
|
})
|
|
245
|
-
if (testing) console.log(
|
|
247
|
+
if (testing) console.log('cleaned=', cleaned)
|
|
246
248
|
cleanedupData.push(cleaned)
|
|
247
249
|
})
|
|
248
250
|
if (testing) console.log('## cleanedData =', cleanedupData)
|
|
249
251
|
return cleanedupData
|
|
250
252
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
export default DataTransform
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { timeFormat, timeParse } from 'd3-time-format'
|
|
2
|
+
|
|
3
|
+
export function formatDate(format = undefined, date) {
|
|
4
|
+
return timeFormat(format)(date)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function parseDate(format = undefined, dateString) {
|
|
8
|
+
return timeParse(format)(dateString) || new Date()
|
|
9
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import numberFromString from '@cdc/core/helpers/numberFromString'
|
|
2
|
+
|
|
3
|
+
const abbreviateNumber = num => {
|
|
4
|
+
let unit = ''
|
|
5
|
+
let absNum = Math.abs(num)
|
|
6
|
+
|
|
7
|
+
if (absNum >= 1e9) {
|
|
8
|
+
unit = 'B'
|
|
9
|
+
num = num / 1e9
|
|
10
|
+
} else if (absNum >= 1e6) {
|
|
11
|
+
unit = 'M'
|
|
12
|
+
num = num / 1e6
|
|
13
|
+
} else if (absNum >= 1e3) {
|
|
14
|
+
unit = 'K'
|
|
15
|
+
num = num / 1e3
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return num + unit
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Format numeric data based on settings in config
|
|
22
|
+
const formatNumber = (num, axis, shouldAbbreviate = false, config = null) => {
|
|
23
|
+
if (!config) console.error('no config found in formatNumber')
|
|
24
|
+
// if num is NaN return num
|
|
25
|
+
if (isNaN(num) || !num) return num
|
|
26
|
+
// Check if the input number is negative
|
|
27
|
+
const isNegative = num < 0
|
|
28
|
+
|
|
29
|
+
// If the input number is negative, take the absolute value
|
|
30
|
+
if (isNegative) {
|
|
31
|
+
num = Math.abs(num)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// destructure dataFormat values
|
|
35
|
+
let {
|
|
36
|
+
dataFormat: { commas, abbreviated, roundTo, prefix, suffix, rightRoundTo, bottomRoundTo, rightPrefix, rightSuffix, bottomPrefix, bottomSuffix, bottomAbbreviated }
|
|
37
|
+
} = config
|
|
38
|
+
|
|
39
|
+
// check if value contains comma and remove it. later will add comma below.
|
|
40
|
+
if (String(num).indexOf(',') !== -1) num = num.replaceAll(',', '')
|
|
41
|
+
|
|
42
|
+
let original = num
|
|
43
|
+
let stringFormattingOptions
|
|
44
|
+
if (axis === 'left') {
|
|
45
|
+
stringFormattingOptions = {
|
|
46
|
+
useGrouping: config.dataFormat.commas ? true : false,
|
|
47
|
+
minimumFractionDigits: roundTo ? Number(roundTo) : 0,
|
|
48
|
+
maximumFractionDigits: roundTo ? Number(roundTo) : 0
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (axis === 'right') {
|
|
53
|
+
stringFormattingOptions = {
|
|
54
|
+
useGrouping: config.dataFormat.rightCommas ? true : false,
|
|
55
|
+
minimumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0,
|
|
56
|
+
maximumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (axis === 'bottom') {
|
|
61
|
+
stringFormattingOptions = {
|
|
62
|
+
useGrouping: config.dataFormat.bottomCommas ? true : false,
|
|
63
|
+
minimumFractionDigits: bottomRoundTo ? Number(bottomRoundTo) : 0,
|
|
64
|
+
maximumFractionDigits: bottomRoundTo ? Number(bottomRoundTo) : 0
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
num = numberFromString(num)
|
|
69
|
+
|
|
70
|
+
if (isNaN(num)) {
|
|
71
|
+
config.runtime.editorErrorMessage = `Unable to parse number from data ${original}. Try reviewing your data and selections in the Data Series section.`
|
|
72
|
+
return original
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!config.dataFormat) return num
|
|
76
|
+
if (config.dataCutoff) {
|
|
77
|
+
let cutoff = numberFromString(config.dataCutoff)
|
|
78
|
+
|
|
79
|
+
if (num < cutoff) {
|
|
80
|
+
num = cutoff
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// When we're formatting the left axis
|
|
85
|
+
// Use commas also updates bars and the data table
|
|
86
|
+
// We can't use commas when we're formatting the dataFormatted number
|
|
87
|
+
// Example: commas -> 12,000; abbreviated -> 12k (correct); abbreviated & commas -> 12 (incorrect)
|
|
88
|
+
//
|
|
89
|
+
// Edge case for small numbers with decimals
|
|
90
|
+
// - if roundTo undefined which means it is blank, then do not round
|
|
91
|
+
|
|
92
|
+
if ((axis === 'left' && commas && abbreviated && shouldAbbreviate) || (axis === 'bottom' && commas && abbreviated && shouldAbbreviate)) {
|
|
93
|
+
num = num // eslint-disable-line
|
|
94
|
+
} else {
|
|
95
|
+
num = num.toLocaleString('en-US', stringFormattingOptions)
|
|
96
|
+
}
|
|
97
|
+
let result = ''
|
|
98
|
+
|
|
99
|
+
if (abbreviated && axis === 'left' && shouldAbbreviate) {
|
|
100
|
+
num = abbreviateNumber(parseFloat(num))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (bottomAbbreviated && axis === 'bottom' && shouldAbbreviate) {
|
|
104
|
+
num = abbreviateNumber(parseFloat(num))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (prefix && axis === 'left') {
|
|
108
|
+
result += prefix
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (rightPrefix && axis === 'right') {
|
|
112
|
+
result += rightPrefix
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (bottomPrefix && axis === 'bottom') {
|
|
116
|
+
result += bottomPrefix
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
result += num
|
|
120
|
+
|
|
121
|
+
if (suffix && axis === 'left') {
|
|
122
|
+
result += suffix
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (rightSuffix && axis === 'right') {
|
|
126
|
+
result += rightSuffix
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (bottomSuffix && axis === 'bottom') {
|
|
130
|
+
result += bottomSuffix
|
|
131
|
+
}
|
|
132
|
+
if (isNegative) {
|
|
133
|
+
result = '-' + result
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return String(result)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export { formatNumber }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// If config key names or position in the config have been changed with a version change,
|
|
2
|
+
// process those config entries and format old values into new
|
|
3
|
+
import update_4_23 from './ver/4.23'
|
|
4
|
+
|
|
5
|
+
// 4.23.6 ------------------------------------------------------
|
|
6
|
+
const coveUpdateWorker = async config => {
|
|
7
|
+
let genConfig = config
|
|
8
|
+
|
|
9
|
+
// v4.23
|
|
10
|
+
genConfig = await update_4_23(genConfig)
|
|
11
|
+
|
|
12
|
+
return genConfig
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default coveUpdateWorker
|
|
@@ -14,10 +14,20 @@ export default async function (url, visualizationType = '') {
|
|
|
14
14
|
data = await fetch(url.href)
|
|
15
15
|
.then(response => response.text())
|
|
16
16
|
.then(responseText => {
|
|
17
|
+
// for every comma NOT inside quotes, replace with a pipe delimiter
|
|
18
|
+
// - this will let commas inside the quotes not be parsed as a new column
|
|
19
|
+
// - Limitation: if a delimiter other than comma is used in the csv this will break
|
|
20
|
+
// Examples of other delimiters that would break: tab
|
|
21
|
+
responseText = responseText.replace(/(".*?")|,/g, (...m) => m[1] || '|')
|
|
22
|
+
// now strip the double quotes
|
|
23
|
+
responseText = responseText.replace(/["]+/g, '')
|
|
17
24
|
const parsedCsv = Papa.parse(responseText, {
|
|
25
|
+
//quotes: "true", // dont need these
|
|
26
|
+
//quoteChar: "'", // has no effect that I can tell
|
|
18
27
|
header: true,
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
skipEmptyLines: true,
|
|
29
|
+
delimiter: '|', // we are using pipe symbol as delimiter so setting this explicitly for Papa.parse
|
|
30
|
+
dynamicTyping: false
|
|
21
31
|
})
|
|
22
32
|
return parsedCsv.data
|
|
23
33
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdc/core",
|
|
3
|
-
"version": "4.23.
|
|
3
|
+
"version": "4.23.6",
|
|
4
4
|
"description": "Core components, styles, hooks, and helpers, for the CDC Open Visualization project",
|
|
5
5
|
"moduleName": "CdcCore",
|
|
6
6
|
"main": "dist/cdccore",
|
|
@@ -30,5 +30,5 @@
|
|
|
30
30
|
"react": "^18.2.0",
|
|
31
31
|
"react-dom": "^18.2.0"
|
|
32
32
|
},
|
|
33
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "aaed0388b487adfeb3e7e278b4ce74df09cbaade"
|
|
34
34
|
}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
display: block;
|
|
3
3
|
position: relative;
|
|
4
4
|
padding: 0.5em 0.5em;
|
|
5
|
-
border-width: 1px;
|
|
6
|
-
border-style: solid;
|
|
7
|
-
border-color: #cbcbcb;
|
|
5
|
+
border-width: 1px !important; // intentional use of !important for CDC
|
|
6
|
+
border-style: solid !important; // intentional use of !important for CDC
|
|
7
|
+
border-color: #cbcbcb !important; // intentional use of !important for CDC
|
|
8
8
|
border-radius: 0.1875rem;
|
|
9
9
|
transition: border-color 200ms $transition-expo-out;
|
|
10
10
|
font-family: sans-serif;
|