@cdc/waffle-chart 1.0.0
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/README.md +33 -0
- package/dist/cdcwafflechart.js +6 -0
- package/examples/example-config.json +10 -0
- package/examples/example-data-2.json +32 -0
- package/examples/example-data.json +90 -0
- package/package.json +43 -0
- package/src/CdcWaffleChart.tsx +502 -0
- package/src/components/EditorPanel.js +510 -0
- package/src/context.js +5 -0
- package/src/data/initial-state.js +27 -0
- package/src/index.html +11 -0
- package/src/index.js +16 -0
- package/src/scss/editor-panel.scss +710 -0
- package/src/scss/main.scss +52 -0
- package/src/scss/responsive.scss +1 -0
- package/src/scss/variables.scss +29 -0
- package/src/scss/waffle-chart.scss +127 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"Insured Rate": "43",
|
|
4
|
+
"Coverage Status": "Insured",
|
|
5
|
+
"state": "Alabama",
|
|
6
|
+
"Year (Good filter option)": "2010",
|
|
7
|
+
"link": "",
|
|
8
|
+
"Verified": true
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"Insured Rate": "0",
|
|
12
|
+
"Coverage Status": "Uninsured",
|
|
13
|
+
"state": "Alaska",
|
|
14
|
+
"Year (Good filter option)": "2006",
|
|
15
|
+
"link": "",
|
|
16
|
+
"Verified": false
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"Insured Rate": "72.7",
|
|
20
|
+
"Coverage Status": "Insured",
|
|
21
|
+
"state": "Arizona",
|
|
22
|
+
"Year (Good filter option)": "2008",
|
|
23
|
+
"link": "#lorem",
|
|
24
|
+
"Verified": true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"Insured Rate": "78.7",
|
|
28
|
+
"Coverage Status": "Insured",
|
|
29
|
+
"state": "Arkansas",
|
|
30
|
+
"Year (Good filter option)": "2010",
|
|
31
|
+
"link": "",
|
|
32
|
+
"Verified": true
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"Insured Rate": "37.2",
|
|
36
|
+
"Coverage Status": "Insured",
|
|
37
|
+
"state": "California",
|
|
38
|
+
"Year (Good filter option)": "2018",
|
|
39
|
+
"link": "https://search.cdc.gov/search/?query=California&utf8=%E2%9C%93&affiliate=cdc-main",
|
|
40
|
+
"Verified": true
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"Insured Rate": "50.6",
|
|
44
|
+
"Coverage Status": "Insured",
|
|
45
|
+
"state": "Colorado",
|
|
46
|
+
"Year (Good filter option)": "2014",
|
|
47
|
+
"link": "",
|
|
48
|
+
"Verified": false
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"Insured Rate": "83.2",
|
|
52
|
+
"Coverage Status": "Insured",
|
|
53
|
+
"state": "Connecticut",
|
|
54
|
+
"Year (Good filter option)": "2019",
|
|
55
|
+
"link": "",
|
|
56
|
+
"Verified": true
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"Insured Rate": "90",
|
|
60
|
+
"Coverage Status": "Insured",
|
|
61
|
+
"state": "Delaware",
|
|
62
|
+
"Year (Good filter option)": "2020",
|
|
63
|
+
"link": "",
|
|
64
|
+
"Verified": true
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"Insured Rate": "77.1",
|
|
68
|
+
"Coverage Status": "Insured",
|
|
69
|
+
"state": "District of Columbia",
|
|
70
|
+
"Year (Good filter option)": "2019",
|
|
71
|
+
"link": "https://search.cdc.gov/search/index.html?query=Washington+D.C.&sitelimit=&utf8=%E2%9C%93&affiliate=cdc-main",
|
|
72
|
+
"Verified": false
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"Insured Rate": "83",
|
|
76
|
+
"Coverage Status": "Insured",
|
|
77
|
+
"state": "Florida",
|
|
78
|
+
"Year (Good filter option)": "2016",
|
|
79
|
+
"link": "",
|
|
80
|
+
"Verified": true
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"Insured Rate": "67.4",
|
|
84
|
+
"Coverage Status": "Insured",
|
|
85
|
+
"state": "Georgia",
|
|
86
|
+
"Year (Good filter option)": "2013",
|
|
87
|
+
"link": "",
|
|
88
|
+
"Verified": false
|
|
89
|
+
}
|
|
90
|
+
]
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cdc/waffle-chart",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React component for displaying a single piece of data in a card module",
|
|
5
|
+
"main": "dist/cdcwafflechart",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "npx webpack serve --mode development -c ../../webpack.config.js",
|
|
8
|
+
"build": "npx webpack --mode production --env packageName=CdcWaffleChart --env folderName=waffle-chart -c ../../webpack.config.js",
|
|
9
|
+
"prepublishOnly": "lerna run --scope @cdc/waffle-chart build"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/CDCgov/cdc-open-viz",
|
|
14
|
+
"directory": "packages/waffle-chart"
|
|
15
|
+
},
|
|
16
|
+
"author": "rshelnutt <qyu6@cdc.gov>",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/CDCgov/cdc-open-viz/issues"
|
|
19
|
+
},
|
|
20
|
+
"license": "Apache-2.0",
|
|
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
|
+
"dependencies": {
|
|
34
|
+
"@visx/shape": "^2.1.1",
|
|
35
|
+
"chroma": "0.0.1",
|
|
36
|
+
"chroma-js": "^2.1.0",
|
|
37
|
+
"link": "^0.1.5"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"react": "^17.0.2",
|
|
41
|
+
"react-dom": ">=16"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react'
|
|
2
|
+
import parse from 'html-react-parser'
|
|
3
|
+
import { Group } from '@visx/group'
|
|
4
|
+
import { Circle, Bar } from '@visx/shape'
|
|
5
|
+
import ResizeObserver from 'resize-observer-polyfill'
|
|
6
|
+
import getViewport from '@cdc/core/helpers/getViewport'
|
|
7
|
+
|
|
8
|
+
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
9
|
+
import Loading from '@cdc/core/components/Loading'
|
|
10
|
+
|
|
11
|
+
import EditorPanel from './components/EditorPanel'
|
|
12
|
+
import defaults from './data/initial-state'
|
|
13
|
+
import Context from './context'
|
|
14
|
+
import './scss/main.scss'
|
|
15
|
+
|
|
16
|
+
const themeColor = {
|
|
17
|
+
'theme-blue': '#005eaa',
|
|
18
|
+
'theme-purple': '#712177',
|
|
19
|
+
'theme-brown': '#705043',
|
|
20
|
+
'theme-teal': '#00695c',
|
|
21
|
+
'theme-pink': '#af4448',
|
|
22
|
+
'theme-orange': '#bb4d00',
|
|
23
|
+
'theme-slate': '#29434e',
|
|
24
|
+
'theme-indigo': '#26418f',
|
|
25
|
+
'theme-cyan': '#006778',
|
|
26
|
+
'theme-green': '#4b830d',
|
|
27
|
+
'theme-amber': '#fbab18',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const WaffleChart = ({ config, isEditor }) => {
|
|
31
|
+
let {
|
|
32
|
+
title,
|
|
33
|
+
theme,
|
|
34
|
+
shape,
|
|
35
|
+
nodeWidth,
|
|
36
|
+
nodeSpacer,
|
|
37
|
+
prefix,
|
|
38
|
+
suffix,
|
|
39
|
+
subtext,
|
|
40
|
+
content,
|
|
41
|
+
orientation,
|
|
42
|
+
filters,
|
|
43
|
+
dataColumn,
|
|
44
|
+
dataFunction,
|
|
45
|
+
dataConditionalColumn,
|
|
46
|
+
dataConditionalOperator,
|
|
47
|
+
dataConditionalComparate,
|
|
48
|
+
customDenom,
|
|
49
|
+
dataDenom,
|
|
50
|
+
dataDenomColumn,
|
|
51
|
+
dataDenomFunction,
|
|
52
|
+
roundToPlace
|
|
53
|
+
} = config
|
|
54
|
+
|
|
55
|
+
const calculateData = useCallback(() => {
|
|
56
|
+
|
|
57
|
+
//If either the column or function aren't set, do not calculate
|
|
58
|
+
if (!dataColumn || !dataFunction) {
|
|
59
|
+
return ''
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const getColumnSum = (arr) => {
|
|
63
|
+
if (Array.isArray(arr) && arr.length > 0) {
|
|
64
|
+
const sum = arr.reduce((sum, x) => sum + x)
|
|
65
|
+
return applyPrecision(sum)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const getColumnMean = (arr) => {
|
|
70
|
+
const mean = arr.length > 1 ? arr.reduce((a, b) => a + b) / arr.length : arr[0]
|
|
71
|
+
return applyPrecision(mean)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const getMode = (arr) => {
|
|
75
|
+
let freq = {}
|
|
76
|
+
let max = -Infinity
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < arr.length; i++) {
|
|
79
|
+
if (freq[arr[i]]) {
|
|
80
|
+
freq[arr[i]] += 1
|
|
81
|
+
} else {
|
|
82
|
+
freq[arr[i]] = 1
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (freq[arr[i]] > max) {
|
|
86
|
+
max = freq[arr[i]]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let res = []
|
|
91
|
+
|
|
92
|
+
for (let key in freq) {
|
|
93
|
+
if (freq[key] === max) res.push(key)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return res
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const getMedian = arr => {
|
|
100
|
+
const mid = Math.floor(arr.length / 2),
|
|
101
|
+
nums = [ ...arr ].sort((a, b) => a - b)
|
|
102
|
+
const value = arr.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2
|
|
103
|
+
return applyPrecision(value)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const applyPrecision = (value) => {
|
|
107
|
+
if ('' !== roundToPlace && !isNaN(roundToPlace) && Number(roundToPlace) > -1) {
|
|
108
|
+
value = Number(value).toFixed(Number(roundToPlace))
|
|
109
|
+
}
|
|
110
|
+
return value
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
//Optionally filter the data based on the user's filter
|
|
114
|
+
let filteredData = config.data
|
|
115
|
+
|
|
116
|
+
filters.map((filter) => {
|
|
117
|
+
if (filter.columnName && filter.columnValue) {
|
|
118
|
+
filteredData = filteredData.filter(function (e) {
|
|
119
|
+
return e[filter.columnName] === filter.columnValue
|
|
120
|
+
})
|
|
121
|
+
} else {
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
let conditionalData = []
|
|
127
|
+
|
|
128
|
+
if (dataConditionalColumn !== '' && dataConditionalOperator !== '' && dataConditionalComparate !== '') {
|
|
129
|
+
switch (dataConditionalOperator) {
|
|
130
|
+
case ('<'):
|
|
131
|
+
conditionalData = filteredData.filter(e => e[dataConditionalColumn] < dataConditionalComparate)
|
|
132
|
+
break
|
|
133
|
+
case ('>'):
|
|
134
|
+
conditionalData = filteredData.filter(e => e[dataConditionalColumn] > dataConditionalComparate)
|
|
135
|
+
break
|
|
136
|
+
case ('<='):
|
|
137
|
+
conditionalData = filteredData.filter(e => e[dataConditionalColumn] <= dataConditionalComparate)
|
|
138
|
+
break
|
|
139
|
+
case ('>='):
|
|
140
|
+
conditionalData = filteredData.filter(e => e[dataConditionalColumn] >= dataConditionalComparate)
|
|
141
|
+
break
|
|
142
|
+
case ('='):
|
|
143
|
+
if (isNaN(Number(dataConditionalComparate))) {
|
|
144
|
+
conditionalData = filteredData.filter(e => String(e[dataConditionalColumn]) === dataConditionalComparate)
|
|
145
|
+
} else {
|
|
146
|
+
conditionalData = filteredData.filter(e => e[dataConditionalColumn] === dataConditionalComparate)
|
|
147
|
+
}
|
|
148
|
+
break
|
|
149
|
+
case ('≠'):
|
|
150
|
+
if (isNaN(Number(dataConditionalComparate))) {
|
|
151
|
+
conditionalData = filteredData.filter(e => String(e[dataConditionalColumn]) !== dataConditionalComparate)
|
|
152
|
+
} else {
|
|
153
|
+
conditionalData = filteredData.filter(e => e[dataConditionalColumn] !== dataConditionalComparate)
|
|
154
|
+
}
|
|
155
|
+
break
|
|
156
|
+
default:
|
|
157
|
+
conditionalData = []
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
//Get the column's data
|
|
162
|
+
const columnData = conditionalData.length > 0 ? conditionalData.map(a => a[dataColumn]) : filteredData.map(a => a[dataColumn])
|
|
163
|
+
const denomColumnData = filteredData.map(a => a[dataDenomColumn])
|
|
164
|
+
|
|
165
|
+
//Filter the column's data for numerical values only
|
|
166
|
+
let numericalData = columnData.filter((value) => {
|
|
167
|
+
let include = false
|
|
168
|
+
if (Number(value) || Number.isFinite(Number(value))) {
|
|
169
|
+
include = true
|
|
170
|
+
}
|
|
171
|
+
return include
|
|
172
|
+
}).map(Number)
|
|
173
|
+
|
|
174
|
+
let numericalDenomData = denomColumnData.filter((value) => {
|
|
175
|
+
let include = false
|
|
176
|
+
if (Number(value) || Number.isFinite(Number(value))) {
|
|
177
|
+
include = true
|
|
178
|
+
}
|
|
179
|
+
return include
|
|
180
|
+
}).map(Number)
|
|
181
|
+
|
|
182
|
+
let waffleNumerator = ''
|
|
183
|
+
|
|
184
|
+
switch (dataFunction) {
|
|
185
|
+
case DATA_FUNCTION_COUNT:
|
|
186
|
+
waffleNumerator = String(numericalData.length)
|
|
187
|
+
break
|
|
188
|
+
case DATA_FUNCTION_SUM:
|
|
189
|
+
waffleNumerator = String(getColumnSum(numericalData))
|
|
190
|
+
break
|
|
191
|
+
case DATA_FUNCTION_MEAN:
|
|
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)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let waffleDenominator = null
|
|
211
|
+
|
|
212
|
+
if (customDenom && dataDenomColumn && dataDenomFunction) {
|
|
213
|
+
switch (dataDenomFunction) {
|
|
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
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
waffleDenominator = dataDenom > 0 ? dataDenom : 100
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// @ts-ignore
|
|
243
|
+
return applyPrecision((waffleNumerator / waffleDenominator) * 100)
|
|
244
|
+
}, [
|
|
245
|
+
dataColumn,
|
|
246
|
+
dataFunction,
|
|
247
|
+
config.data,
|
|
248
|
+
filters,
|
|
249
|
+
dataConditionalColumn,
|
|
250
|
+
dataConditionalOperator,
|
|
251
|
+
dataConditionalComparate,
|
|
252
|
+
customDenom,
|
|
253
|
+
dataDenomColumn,
|
|
254
|
+
dataDenomFunction,
|
|
255
|
+
roundToPlace,
|
|
256
|
+
dataDenom
|
|
257
|
+
])
|
|
258
|
+
|
|
259
|
+
const dataPercentage = calculateData()
|
|
260
|
+
|
|
261
|
+
const buildWaffle = useCallback(() => {
|
|
262
|
+
let waffleData = []
|
|
263
|
+
let nodeWidthNum = parseInt(nodeWidth, 10)
|
|
264
|
+
let nodeSpacerNum = parseInt(nodeSpacer, 10)
|
|
265
|
+
|
|
266
|
+
const calculatePos = (shape, axis, index, width, spacer) => {
|
|
267
|
+
let mod = axis === 'x' ? index % 10 : axis === 'y' ? Math.floor(index / 10) : null
|
|
268
|
+
return shape === 'circle' ? (mod * (width + spacer)) + (width / 2) : mod * (width + spacer)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
for (let i = 0; i < 100; i++) {
|
|
272
|
+
let newNode = {
|
|
273
|
+
shape: shape,
|
|
274
|
+
x: calculatePos(shape, 'x', i, nodeWidthNum, nodeSpacerNum),
|
|
275
|
+
y: calculatePos(shape, 'y', i, nodeWidthNum, nodeSpacerNum),
|
|
276
|
+
color: themeColor[theme],
|
|
277
|
+
opacity: i + 1 > (100 - Math.round(dataPercentage)) ? 1 : 0.35
|
|
278
|
+
}
|
|
279
|
+
waffleData.push(newNode)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return waffleData.map((node, key) => (
|
|
283
|
+
node.shape === 'square'
|
|
284
|
+
? <Bar className="cdc-waffle-chart__node" style={{ transitionDelay: `${0.1 * key}ms` }} x={node.x} y={node.y}
|
|
285
|
+
width={nodeWidthNum} height={nodeWidthNum} fill={node.color} fillOpacity={node.opacity} key={key}/>
|
|
286
|
+
: node.shape === 'person' ?
|
|
287
|
+
<path
|
|
288
|
+
style={{ transform: `translateX(${node.x + nodeWidthNum / 4}px) translateY(${node.y}px) scale(${nodeWidthNum / 20})` }}
|
|
289
|
+
fill={node.color} fillOpacity={node.opacity} key={key}
|
|
290
|
+
d="M3.75,0a2.5,2.5,0,1,1-2.5,2.5A2.5,2.5,0,0,1,3.75,0M5.625,5.625H5.18125a3.433,3.433,0,0,1-2.8625,0H1.875A1.875,1.875,
|
|
291
|
+
0,0,0,0,7.5v5.3125a.9375.9375,0,0,0,.9375.9375h.625v5.3125A.9375.9375,0,0,0,2.5,20H5a.9375.9375,0,0,0,
|
|
292
|
+
.9375-.9375V13.75h.625A.9375.9375,0,0,0,7.5,12.8125V7.5A1.875,1.875,0,0,0,5.625,5.625Z">
|
|
293
|
+
</path>
|
|
294
|
+
:
|
|
295
|
+
<Circle className="cdc-waffle-chart__node" style={{ transitionDelay: `${0.1 * key}ms` }} cx={node.x} cy={node.y}
|
|
296
|
+
r={nodeWidthNum / 2} fill={node.color} fillOpacity={node.opacity} key={key}/>
|
|
297
|
+
))
|
|
298
|
+
}, [ theme, dataPercentage, shape, nodeWidth, nodeSpacer ])
|
|
299
|
+
|
|
300
|
+
const setRatio = useCallback(() => {
|
|
301
|
+
return (nodeWidth * 10) + (nodeSpacer * 9)
|
|
302
|
+
}, [ nodeWidth, nodeSpacer ])
|
|
303
|
+
|
|
304
|
+
let dataFontSize = config.fontSize ? {fontSize: config.fontSize + 'px'} : null
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<div className={isEditor ? 'spacing-wrapper' : ''}>
|
|
308
|
+
<section className={`cdc-waffle-chart ${theme}${config.overallFontSize ? ' font-' + config.overallFontSize : ''}`}>
|
|
309
|
+
<div className="cdc-waffle-chart__container">
|
|
310
|
+
{title &&
|
|
311
|
+
<header aria-hidden="true">
|
|
312
|
+
<div className="cdc-waffle-chart__header">{parse(title)}</div>
|
|
313
|
+
</header>
|
|
314
|
+
}
|
|
315
|
+
<div className={`cdc-waffle-chart__inner-container${orientation === 'vertical' ? ' cdc-waffle-chart--verical' : ''}`}>
|
|
316
|
+
<div className="cdc-waffle-chart__chart" style={{width: setRatio()}}>
|
|
317
|
+
<svg width={setRatio()} height={setRatio()}>
|
|
318
|
+
<Group>
|
|
319
|
+
{buildWaffle()}
|
|
320
|
+
</Group>
|
|
321
|
+
</svg>
|
|
322
|
+
</div>
|
|
323
|
+
{ (dataPercentage || content) &&
|
|
324
|
+
<div className="cdc-waffle-chart__data">
|
|
325
|
+
{dataPercentage &&
|
|
326
|
+
<div className="cdc-waffle-chart__data--primary" style={dataFontSize}>
|
|
327
|
+
{prefix ? prefix : null}{dataPercentage}{suffix ? suffix : null}
|
|
328
|
+
</div>
|
|
329
|
+
}
|
|
330
|
+
<div className="cdc-waffle-chart__data--text">{parse(content)}</div>
|
|
331
|
+
</div>
|
|
332
|
+
}
|
|
333
|
+
</div>
|
|
334
|
+
{subtext &&
|
|
335
|
+
<div className="cdc-waffle-chart__subtext">
|
|
336
|
+
{parse(subtext)}
|
|
337
|
+
</div>
|
|
338
|
+
}
|
|
339
|
+
</div>
|
|
340
|
+
</section>
|
|
341
|
+
</div>
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const CdcWaffleChart = (
|
|
346
|
+
{
|
|
347
|
+
configUrl,
|
|
348
|
+
config: configObj,
|
|
349
|
+
isDashboard = false,
|
|
350
|
+
isEditor = false,
|
|
351
|
+
setConfig: setParentConfig
|
|
352
|
+
}
|
|
353
|
+
) => {
|
|
354
|
+
const [ config, setConfig ] = useState({ ...defaults })
|
|
355
|
+
const [ loading, setLoading ] = useState(true)
|
|
356
|
+
|
|
357
|
+
const [ currentViewport, setCurrentViewport ] = useState<String>('lg')
|
|
358
|
+
|
|
359
|
+
//Observes changes to outermost container and changes viewport size in state
|
|
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
|
+
|
|
368
|
+
const updateConfig = (newConfig) => {
|
|
369
|
+
|
|
370
|
+
// Deeper copy
|
|
371
|
+
Object.keys(defaults).forEach(key => {
|
|
372
|
+
if (newConfig[key] && 'object' === typeof newConfig[key] && !Array.isArray(newConfig[key])) {
|
|
373
|
+
newConfig[key] = { ...defaults[key], ...newConfig[key] }
|
|
374
|
+
}
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
//Enforce default values that need to be calculated at runtime
|
|
378
|
+
newConfig.runtime = {}
|
|
379
|
+
newConfig.runtime.uniqueId = Date.now()
|
|
380
|
+
|
|
381
|
+
//Check things that are needed and set error messages if needed
|
|
382
|
+
newConfig.runtime.editorErrorMessage = ''
|
|
383
|
+
setConfig(newConfig)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const loadConfig = async () => {
|
|
387
|
+
let response = configObj || await (await fetch(configUrl)).json()
|
|
388
|
+
|
|
389
|
+
// If data is included through a URL, fetch that and store
|
|
390
|
+
let responseData = response.data ?? {}
|
|
391
|
+
|
|
392
|
+
if (response.dataUrl) {
|
|
393
|
+
const dataString = await fetch(response.dataUrl)
|
|
394
|
+
responseData = await dataString.json()
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
response.data = responseData
|
|
398
|
+
|
|
399
|
+
updateConfig({ ...defaults, ...response })
|
|
400
|
+
setLoading(false)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Load data when component first mounts
|
|
404
|
+
const outerContainerRef = useCallback(node => {
|
|
405
|
+
if (node !== null) {
|
|
406
|
+
resizeObserver.observe(node)
|
|
407
|
+
}
|
|
408
|
+
}, [])
|
|
409
|
+
|
|
410
|
+
useEffect(() => {
|
|
411
|
+
console.log('Running empty useEFfect')
|
|
412
|
+
loadConfig()
|
|
413
|
+
}, [])
|
|
414
|
+
|
|
415
|
+
if (configObj) {
|
|
416
|
+
useEffect(() => {
|
|
417
|
+
console.log('Running last useEFfect')
|
|
418
|
+
loadConfig()
|
|
419
|
+
}, [ configObj.data ])
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
useEffect(() => {
|
|
423
|
+
loadConfig()
|
|
424
|
+
}, [])
|
|
425
|
+
|
|
426
|
+
if (configObj) {
|
|
427
|
+
useEffect(() => {
|
|
428
|
+
loadConfig()
|
|
429
|
+
}, [ configObj.data ])
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
let body = (<Loading/>)
|
|
433
|
+
|
|
434
|
+
if (loading === false) {
|
|
435
|
+
let classNames = [
|
|
436
|
+
'cdc-open-viz-module',
|
|
437
|
+
'type-waffle-chart',
|
|
438
|
+
currentViewport,
|
|
439
|
+
config.theme,
|
|
440
|
+
'font-' + config.overallFontSize
|
|
441
|
+
]
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
if (isEditor) {
|
|
445
|
+
classNames.push('is-editor')
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
body = (
|
|
449
|
+
<>
|
|
450
|
+
<div className={classNames.join(' ')} ref={outerContainerRef}>
|
|
451
|
+
{isEditor && <EditorPanel/>}
|
|
452
|
+
<WaffleChart config={config} isEditor={isEditor}/>
|
|
453
|
+
</div>
|
|
454
|
+
</>
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return (
|
|
459
|
+
<ErrorBoundary component="WaffleChart">
|
|
460
|
+
<Context.Provider
|
|
461
|
+
value={{ config, updateConfig, loading, data: config.data, setParentConfig, isDashboard, outerContainerRef }}>
|
|
462
|
+
{body}
|
|
463
|
+
</Context.Provider>
|
|
464
|
+
</ErrorBoundary>
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export default CdcWaffleChart
|
|
469
|
+
|
|
470
|
+
export const DATA_FUNCTION_MAX = 'Max'
|
|
471
|
+
export const DATA_FUNCTION_COUNT = 'Count'
|
|
472
|
+
export const DATA_FUNCTION_MEAN = 'Mean (Average)'
|
|
473
|
+
export const DATA_FUNCTION_MEDIAN = 'Median'
|
|
474
|
+
export const DATA_FUNCTION_MIN = 'Min'
|
|
475
|
+
export const DATA_FUNCTION_MODE = 'Mode'
|
|
476
|
+
export const DATA_FUNCTION_SUM = 'Sum'
|
|
477
|
+
|
|
478
|
+
export const DATA_FUNCTIONS = [
|
|
479
|
+
DATA_FUNCTION_COUNT,
|
|
480
|
+
DATA_FUNCTION_MAX,
|
|
481
|
+
DATA_FUNCTION_MEAN,
|
|
482
|
+
DATA_FUNCTION_MEDIAN,
|
|
483
|
+
DATA_FUNCTION_MIN,
|
|
484
|
+
DATA_FUNCTION_MODE,
|
|
485
|
+
DATA_FUNCTION_SUM
|
|
486
|
+
]
|
|
487
|
+
|
|
488
|
+
export const DATA_OPERATOR_LESS = '<'
|
|
489
|
+
export const DATA_OPERATOR_GREATER = '>'
|
|
490
|
+
export const DATA_OPERATOR_LESSEQUAL = '<='
|
|
491
|
+
export const DATA_OPERATOR_GREATEREQUAL = '>='
|
|
492
|
+
export const DATA_OPERATOR_EQUAL = '='
|
|
493
|
+
export const DATA_OPERATOR_NOTEQUAL = '≠'
|
|
494
|
+
|
|
495
|
+
export const DATA_OPERATORS = [
|
|
496
|
+
DATA_OPERATOR_LESS,
|
|
497
|
+
DATA_OPERATOR_GREATER,
|
|
498
|
+
DATA_OPERATOR_LESSEQUAL,
|
|
499
|
+
DATA_OPERATOR_GREATEREQUAL,
|
|
500
|
+
DATA_OPERATOR_EQUAL,
|
|
501
|
+
DATA_OPERATOR_NOTEQUAL
|
|
502
|
+
]
|