@cdc/waffle-chart 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdcwafflechart.js +2 -2
- package/package.json +12 -14
- package/src/CdcWaffleChart.tsx +127 -125
- package/src/{context.js → ConfigContext.js} +0 -0
- package/src/components/EditorPanel.js +281 -350
- package/src/data/initial-state.js +8 -1
- package/src/scss/main.scss +1 -4
- package/src/scss/waffle-chart.scss +42 -69
- package/src/scss/editor-panel.scss +0 -710
- package/src/scss/responsive.scss +0 -1
- package/src/scss/variables.scss +0 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdc/waffle-chart",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "React component for displaying a single piece of data in a card module",
|
|
5
5
|
"main": "dist/cdcwafflechart",
|
|
6
6
|
"scripts": {
|
|
@@ -19,25 +19,23 @@
|
|
|
19
19
|
},
|
|
20
20
|
"license": "Apache-2.0",
|
|
21
21
|
"homepage": "https://github.com/CDCgov/cdc-open-viz#readme",
|
|
22
|
-
"devDependencies": {
|
|
23
|
-
"@cdc/core": "^1.1.2",
|
|
24
|
-
"bootstrap": "^4.3.1",
|
|
25
|
-
"html-react-parser": "^0.14.0",
|
|
26
|
-
"react-accessible-accordion": "^3.3.4",
|
|
27
|
-
"react-beautiful-dnd": "^13.0.0",
|
|
28
|
-
"react-select": "^3.0.8",
|
|
29
|
-
"react-tooltip": "4.2.8",
|
|
30
|
-
"use-debounce": "^6.0.1",
|
|
31
|
-
"whatwg-fetch": "^3.6.2"
|
|
32
|
-
},
|
|
33
22
|
"dependencies": {
|
|
23
|
+
"@cdc/core": "^1.1.3",
|
|
34
24
|
"@visx/shape": "^2.1.1",
|
|
35
25
|
"chroma": "0.0.1",
|
|
36
26
|
"chroma-js": "^2.1.0",
|
|
37
|
-
"
|
|
27
|
+
"html-react-parser": "1.4.9",
|
|
28
|
+
"react-accessible-accordion": "^3.3.4",
|
|
29
|
+
"react-tooltip": "4.2.8",
|
|
30
|
+
"use-debounce": "^6.0.1",
|
|
31
|
+
"whatwg-fetch": "^3.6.2"
|
|
38
32
|
},
|
|
39
33
|
"peerDependencies": {
|
|
40
34
|
"react": "^17.0.2",
|
|
41
35
|
"react-dom": ">=16"
|
|
42
|
-
}
|
|
36
|
+
},
|
|
37
|
+
"resolutions": {
|
|
38
|
+
"@types/react": "17.x"
|
|
39
|
+
},
|
|
40
|
+
"gitHead": "ff89a7aea74c533413c62ef8859cc011e6b3cbfa"
|
|
43
41
|
}
|
package/src/CdcWaffleChart.tsx
CHANGED
|
@@ -2,15 +2,19 @@ import React, { useCallback, useEffect, useState } from 'react'
|
|
|
2
2
|
import parse from 'html-react-parser'
|
|
3
3
|
import { Group } from '@visx/group'
|
|
4
4
|
import { Circle, Bar } from '@visx/shape'
|
|
5
|
+
|
|
5
6
|
import ResizeObserver from 'resize-observer-polyfill'
|
|
6
7
|
import getViewport from '@cdc/core/helpers/getViewport'
|
|
7
8
|
|
|
8
9
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
9
10
|
import Loading from '@cdc/core/components/Loading'
|
|
10
11
|
|
|
12
|
+
import ConfigContext from './ConfigContext'
|
|
11
13
|
import EditorPanel from './components/EditorPanel'
|
|
12
14
|
import defaults from './data/initial-state'
|
|
13
|
-
|
|
15
|
+
|
|
16
|
+
import { publish } from '@cdc/core/helpers/events';
|
|
17
|
+
|
|
14
18
|
import './scss/main.scss'
|
|
15
19
|
|
|
16
20
|
const themeColor = {
|
|
@@ -179,62 +183,36 @@ const WaffleChart = ({ config, isEditor }) => {
|
|
|
179
183
|
return include
|
|
180
184
|
}).map(Number)
|
|
181
185
|
|
|
186
|
+
// Calculate numerator ------------------
|
|
182
187
|
let waffleNumerator = ''
|
|
183
188
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
waffleNumerator = String(getColumnMean(numericalData))
|
|
193
|
-
break
|
|
194
|
-
case DATA_FUNCTION_MEDIAN:
|
|
195
|
-
waffleNumerator = getMedian(numericalData).toString()
|
|
196
|
-
break
|
|
197
|
-
case DATA_FUNCTION_MAX:
|
|
198
|
-
waffleNumerator = Math.max(...numericalData).toString()
|
|
199
|
-
break
|
|
200
|
-
case DATA_FUNCTION_MIN:
|
|
201
|
-
waffleNumerator = Math.min(...numericalData).toString()
|
|
202
|
-
break
|
|
203
|
-
case DATA_FUNCTION_MODE:
|
|
204
|
-
waffleNumerator = getMode(numericalData).join(', ')
|
|
205
|
-
break
|
|
206
|
-
default:
|
|
207
|
-
console.log('Function not recognized: ' + dataFunction)
|
|
189
|
+
const numerFunctionList = {
|
|
190
|
+
[DATA_FUNCTION_COUNT]: String(numericalData.length),
|
|
191
|
+
[DATA_FUNCTION_SUM]: String(getColumnSum(numericalData)),
|
|
192
|
+
[DATA_FUNCTION_MEAN]: String(getColumnMean(numericalData)),
|
|
193
|
+
[DATA_FUNCTION_MEDIAN]: getMedian(numericalData).toString(),
|
|
194
|
+
[DATA_FUNCTION_MAX]: Math.max(...numericalData).toString(),
|
|
195
|
+
[DATA_FUNCTION_MIN]: Math.min(...numericalData).toString(),
|
|
196
|
+
[DATA_FUNCTION_MODE]: getMode(numericalData).join(', ')
|
|
208
197
|
}
|
|
209
198
|
|
|
199
|
+
waffleNumerator = numerFunctionList[dataFunction]
|
|
200
|
+
|
|
201
|
+
// Calculate denominator ------------------
|
|
210
202
|
let waffleDenominator = null
|
|
211
203
|
|
|
204
|
+
const denomFunctionList = {
|
|
205
|
+
[DATA_FUNCTION_COUNT]: String(numericalDenomData.length),
|
|
206
|
+
[DATA_FUNCTION_SUM]: String(getColumnSum(numericalDenomData)),
|
|
207
|
+
[DATA_FUNCTION_MEAN]: String(getColumnMean(numericalDenomData)),
|
|
208
|
+
[DATA_FUNCTION_MEDIAN]: getMedian(numericalDenomData).toString(),
|
|
209
|
+
[DATA_FUNCTION_MAX]: Math.max(...numericalDenomData).toString(),
|
|
210
|
+
[DATA_FUNCTION_MIN]: Math.min(...numericalDenomData).toString(),
|
|
211
|
+
[DATA_FUNCTION_MODE]: getMode(numericalDenomData).join(', '),
|
|
212
|
+
}
|
|
213
|
+
|
|
212
214
|
if (customDenom && dataDenomColumn && dataDenomFunction) {
|
|
213
|
-
|
|
214
|
-
case DATA_FUNCTION_COUNT:
|
|
215
|
-
waffleDenominator = String(numericalDenomData.length)
|
|
216
|
-
break
|
|
217
|
-
case DATA_FUNCTION_SUM:
|
|
218
|
-
waffleDenominator = String(getColumnSum(numericalDenomData))
|
|
219
|
-
break
|
|
220
|
-
case DATA_FUNCTION_MEAN:
|
|
221
|
-
waffleDenominator = String(getColumnMean(numericalDenomData))
|
|
222
|
-
break
|
|
223
|
-
case DATA_FUNCTION_MEDIAN:
|
|
224
|
-
waffleDenominator = getMedian(numericalDenomData).toString()
|
|
225
|
-
break
|
|
226
|
-
case DATA_FUNCTION_MAX:
|
|
227
|
-
waffleDenominator = Math.max(...numericalDenomData).toString()
|
|
228
|
-
break
|
|
229
|
-
case DATA_FUNCTION_MIN:
|
|
230
|
-
waffleDenominator = Math.min(...numericalDenomData).toString()
|
|
231
|
-
break
|
|
232
|
-
case DATA_FUNCTION_MODE:
|
|
233
|
-
waffleDenominator = getMode(numericalDenomData).join(', ')
|
|
234
|
-
break
|
|
235
|
-
default:
|
|
236
|
-
console.log('Function not recognized: ' + dataFunction)
|
|
237
|
-
}
|
|
215
|
+
waffleDenominator = denomFunctionList[dataDenomFunction]
|
|
238
216
|
} else {
|
|
239
217
|
waffleDenominator = dataDenom > 0 ? dataDenom : 100
|
|
240
218
|
}
|
|
@@ -301,43 +279,64 @@ const WaffleChart = ({ config, isEditor }) => {
|
|
|
301
279
|
return (nodeWidth * 10) + (nodeSpacer * 9)
|
|
302
280
|
}, [ nodeWidth, nodeSpacer ])
|
|
303
281
|
|
|
304
|
-
let dataFontSize = config.fontSize ? {fontSize: config.fontSize + 'px'} : null
|
|
282
|
+
let dataFontSize = config.fontSize ? { fontSize: config.fontSize + 'px' } : null
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
let innerContainerClasses = ['cove-component__inner']
|
|
286
|
+
config.title && innerContainerClasses.push('component--has-title')
|
|
287
|
+
config.subtext && innerContainerClasses.push('component--has-subtext')
|
|
288
|
+
config.biteStyle && innerContainerClasses.push(`bite__style--${config.biteStyle}`)
|
|
289
|
+
config.general?.isCompactStyle && innerContainerClasses.push(`component--isCompactStyle`)
|
|
290
|
+
|
|
291
|
+
let contentClasses = ['cove-component__content'];
|
|
292
|
+
!config.visual?.border && contentClasses.push('no-borders');
|
|
293
|
+
config.visual?.borderColorTheme && contentClasses.push('component--has-borderColorTheme');
|
|
294
|
+
config.visual?.accent && contentClasses.push('component--has-accent');
|
|
295
|
+
config.visual?.background && contentClasses.push('component--has-background');
|
|
296
|
+
config.visual?.hideBackgroundColor && contentClasses.push('component--hideBackgroundColor');
|
|
297
|
+
|
|
298
|
+
// ! these two will be retired.
|
|
299
|
+
config.shadow && innerContainerClasses.push('shadow')
|
|
300
|
+
config?.visual?.roundedBorders && innerContainerClasses.push('bite--has-rounded-borders')
|
|
305
301
|
|
|
306
302
|
return (
|
|
307
|
-
<div className={
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
<div className=
|
|
316
|
-
<div
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
{dataPercentage &&
|
|
326
|
-
<div className="
|
|
327
|
-
{
|
|
303
|
+
<div className={innerContainerClasses.join(' ')}>
|
|
304
|
+
<>
|
|
305
|
+
{title &&
|
|
306
|
+
<div className={`cove-component__header chart-title ${config.theme}`} >
|
|
307
|
+
{parse(title)}
|
|
308
|
+
</div>
|
|
309
|
+
}
|
|
310
|
+
<div className={contentClasses.join(' ')}>
|
|
311
|
+
<div className="cove-component__content-wrap">
|
|
312
|
+
<div
|
|
313
|
+
className={`cove-waffle-chart${orientation === 'vertical' ? ' cove-waffle-chart--verical' : ''}${config.overallFontSize ? ' font-' + config.overallFontSize : ''}`}>
|
|
314
|
+
<div className="cove-waffle-chart__chart" style={{ width: setRatio() }}>
|
|
315
|
+
<svg width={setRatio()} height={setRatio()}>
|
|
316
|
+
<Group>
|
|
317
|
+
{buildWaffle()}
|
|
318
|
+
</Group>
|
|
319
|
+
</svg>
|
|
320
|
+
</div>
|
|
321
|
+
{(dataPercentage || content) &&
|
|
322
|
+
<div className="cove-waffle-chart__data">
|
|
323
|
+
{dataPercentage &&
|
|
324
|
+
<div className="cove-waffle-chart__data--primary" style={dataFontSize}>
|
|
325
|
+
{prefix ? prefix : null}{dataPercentage}{suffix ? suffix : null}
|
|
326
|
+
</div>
|
|
327
|
+
}
|
|
328
|
+
<div className="cove-waffle-chart__data--text" >{parse(content)}</div>
|
|
328
329
|
</div>
|
|
329
330
|
}
|
|
330
|
-
<div className="cdc-waffle-chart__data--text">{parse(content)}</div>
|
|
331
331
|
</div>
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
{subtext &&
|
|
335
|
-
<div className="cdc-waffle-chart__subtext">
|
|
332
|
+
{subtext &&
|
|
333
|
+
<div className="cove-waffle-chart__subtext">
|
|
336
334
|
{parse(subtext)}
|
|
337
335
|
</div>
|
|
338
|
-
|
|
336
|
+
}
|
|
337
|
+
</div>
|
|
339
338
|
</div>
|
|
340
|
-
|
|
339
|
+
</>
|
|
341
340
|
</div>
|
|
342
341
|
)
|
|
343
342
|
}
|
|
@@ -351,42 +350,32 @@ const CdcWaffleChart = (
|
|
|
351
350
|
setConfig: setParentConfig
|
|
352
351
|
}
|
|
353
352
|
) => {
|
|
353
|
+
|
|
354
|
+
// Default States
|
|
354
355
|
const [ config, setConfig ] = useState({ ...defaults })
|
|
355
356
|
const [ loading, setLoading ] = useState(true)
|
|
356
357
|
|
|
357
358
|
const [ currentViewport, setCurrentViewport ] = useState<String>('lg')
|
|
359
|
+
const [ coveLoadedHasRan, setCoveLoadedHasRan ] = useState(false)
|
|
360
|
+
const [ container, setContainer ] = useState()
|
|
358
361
|
|
|
359
|
-
//
|
|
360
|
-
const resizeObserver = new ResizeObserver(entries => {
|
|
361
|
-
for (let entry of entries) {
|
|
362
|
-
let newViewport = getViewport(entry.contentRect.width * 2) // Data bite is usually presented as small, so we scale it up for responsive calculations
|
|
363
|
-
|
|
364
|
-
setCurrentViewport(newViewport)
|
|
365
|
-
}
|
|
366
|
-
})
|
|
367
|
-
|
|
362
|
+
// Default Functions
|
|
368
363
|
const updateConfig = (newConfig) => {
|
|
369
|
-
|
|
370
|
-
// Deeper copy
|
|
371
364
|
Object.keys(defaults).forEach(key => {
|
|
372
365
|
if (newConfig[key] && 'object' === typeof newConfig[key] && !Array.isArray(newConfig[key])) {
|
|
373
366
|
newConfig[key] = { ...defaults[key], ...newConfig[key] }
|
|
374
367
|
}
|
|
375
368
|
})
|
|
376
369
|
|
|
377
|
-
//Enforce default values that need to be calculated at runtime
|
|
378
370
|
newConfig.runtime = {}
|
|
379
371
|
newConfig.runtime.uniqueId = Date.now()
|
|
380
372
|
|
|
381
|
-
//Check things that are needed and set error messages if needed
|
|
382
373
|
newConfig.runtime.editorErrorMessage = ''
|
|
383
374
|
setConfig(newConfig)
|
|
384
375
|
}
|
|
385
376
|
|
|
386
|
-
const loadConfig = async () => {
|
|
377
|
+
const loadConfig = useCallback(async () => {
|
|
387
378
|
let response = configObj || await (await fetch(configUrl)).json()
|
|
388
|
-
|
|
389
|
-
// If data is included through a URL, fetch that and store
|
|
390
379
|
let responseData = response.data ?? {}
|
|
391
380
|
|
|
392
381
|
if (response.dataUrl) {
|
|
@@ -398,38 +387,44 @@ const CdcWaffleChart = (
|
|
|
398
387
|
|
|
399
388
|
updateConfig({ ...defaults, ...response })
|
|
400
389
|
setLoading(false)
|
|
401
|
-
}
|
|
390
|
+
}, [])
|
|
391
|
+
|
|
392
|
+
// Custom Functions
|
|
393
|
+
|
|
394
|
+
// --Observes changes to outermost container and changes viewport size in state
|
|
395
|
+
const resizeObserver = new ResizeObserver(entries => {
|
|
396
|
+
for (let entry of entries) {
|
|
397
|
+
let newViewport = getViewport(entry.contentRect.width * 2) // Data bite is usually presented as small, so we scale it up for responsive calculations
|
|
398
|
+
|
|
399
|
+
setCurrentViewport(newViewport)
|
|
400
|
+
}
|
|
401
|
+
})
|
|
402
402
|
|
|
403
|
-
// Load data when component first mounts
|
|
404
403
|
const outerContainerRef = useCallback(node => {
|
|
405
404
|
if (node !== null) {
|
|
406
405
|
resizeObserver.observe(node)
|
|
407
406
|
}
|
|
407
|
+
setContainer(node)
|
|
408
408
|
}, [])
|
|
409
409
|
|
|
410
|
+
//Load initial config
|
|
410
411
|
useEffect(() => {
|
|
411
|
-
console.log(
|
|
412
|
-
loadConfig()
|
|
412
|
+
loadConfig().catch((err) => console.log(err))
|
|
413
413
|
}, [])
|
|
414
414
|
|
|
415
|
-
if (configObj) {
|
|
416
|
-
useEffect(() => {
|
|
417
|
-
console.log('Running last useEFfect')
|
|
418
|
-
loadConfig()
|
|
419
|
-
}, [ configObj.data ])
|
|
420
|
-
}
|
|
421
|
-
|
|
422
415
|
useEffect(() => {
|
|
423
|
-
|
|
424
|
-
|
|
416
|
+
if (config && !coveLoadedHasRan && container) {
|
|
417
|
+
publish('cove_loaded', { config: config })
|
|
418
|
+
setCoveLoadedHasRan(true)
|
|
419
|
+
}
|
|
420
|
+
}, [config, container]);
|
|
425
421
|
|
|
426
|
-
if
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
422
|
+
//Reload config if config object provided/updated
|
|
423
|
+
useEffect(() => {
|
|
424
|
+
loadConfig().catch((err) => console.log(err))
|
|
425
|
+
}, [ configObj?.data ])
|
|
431
426
|
|
|
432
|
-
let
|
|
427
|
+
let content = (<Loading/>)
|
|
433
428
|
|
|
434
429
|
if (loading === false) {
|
|
435
430
|
let classNames = [
|
|
@@ -440,27 +435,34 @@ const CdcWaffleChart = (
|
|
|
440
435
|
'font-' + config.overallFontSize
|
|
441
436
|
]
|
|
442
437
|
|
|
443
|
-
|
|
444
438
|
if (isEditor) {
|
|
445
439
|
classNames.push('is-editor')
|
|
446
440
|
}
|
|
447
441
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
442
|
+
let bodyClasses = ['cove-component', 'waffle-chart']
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
let body = (
|
|
447
|
+
<div className={`${bodyClasses.join(' ')}`} ref={outerContainerRef}>
|
|
448
|
+
<WaffleChart config={config} isEditor={isEditor}/>
|
|
449
|
+
</div>
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
content = (
|
|
453
|
+
<div className={`cove ${config.theme}`}>
|
|
454
|
+
{isEditor && <EditorPanel>{body}</EditorPanel>}
|
|
455
|
+
{!isEditor && body}
|
|
456
|
+
</div>
|
|
455
457
|
)
|
|
456
458
|
}
|
|
457
459
|
|
|
458
460
|
return (
|
|
459
461
|
<ErrorBoundary component="WaffleChart">
|
|
460
|
-
<
|
|
462
|
+
<ConfigContext.Provider
|
|
461
463
|
value={{ config, updateConfig, loading, data: config.data, setParentConfig, isDashboard, outerContainerRef }}>
|
|
462
|
-
{
|
|
463
|
-
</
|
|
464
|
+
{content}
|
|
465
|
+
</ConfigContext.Provider>
|
|
464
466
|
</ErrorBoundary>
|
|
465
467
|
)
|
|
466
468
|
}
|
|
File without changes
|