@cdc/data-bite 1.1.2 → 1.1.4
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/cdcdatabite.js +8 -2
- package/examples/example-config.json +6 -2
- package/examples/private/WCMSRD-12345.json +1027 -0
- package/package.json +11 -10
- package/src/CdcDataBite.tsx +323 -76
- package/src/components/CircleCallout.js +3 -3
- package/src/components/EditorPanel.js +265 -42
- package/src/data/initial-state.js +17 -4
- package/src/scss/bite.scss +27 -2
- package/src/scss/editor-panel.scss +50 -0
- package/src/scss/main.scss +6 -2
- package/LICENSE +0 -201
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdc/data-bite",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "React component for displaying a single piece of data in a card module",
|
|
5
5
|
"main": "dist/cdcdatabite",
|
|
6
6
|
"scripts": {
|
|
@@ -19,10 +19,12 @@
|
|
|
19
19
|
},
|
|
20
20
|
"license": "Apache-2.0",
|
|
21
21
|
"homepage": "https://github.com/CDCgov/cdc-open-viz#readme",
|
|
22
|
-
"
|
|
23
|
-
"@cdc/core": "^1.1.
|
|
24
|
-
"
|
|
25
|
-
"
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@cdc/core": "^1.1.4",
|
|
24
|
+
"chroma": "0.0.1",
|
|
25
|
+
"chroma-js": "^2.1.0",
|
|
26
|
+
"html-react-parser": "1.4.9",
|
|
27
|
+
"papaparse": "^5.3.0",
|
|
26
28
|
"react-accessible-accordion": "^3.3.4",
|
|
27
29
|
"react-beautiful-dnd": "^13.0.0",
|
|
28
30
|
"react-select": "^3.0.8",
|
|
@@ -30,13 +32,12 @@
|
|
|
30
32
|
"use-debounce": "^6.0.1",
|
|
31
33
|
"whatwg-fetch": "^3.6.2"
|
|
32
34
|
},
|
|
33
|
-
"dependencies": {
|
|
34
|
-
"chroma": "0.0.1",
|
|
35
|
-
"chroma-js": "^2.1.0"
|
|
36
|
-
},
|
|
37
35
|
"peerDependencies": {
|
|
38
36
|
"react": "^17.0.2",
|
|
39
37
|
"react-dom": ">=16"
|
|
40
38
|
},
|
|
41
|
-
"
|
|
39
|
+
"resolutions": {
|
|
40
|
+
"@types/react": "17.x"
|
|
41
|
+
},
|
|
42
|
+
"gitHead": "ff89a7aea74c533413c62ef8859cc011e6b3cbfa"
|
|
42
43
|
}
|
package/src/CdcDataBite.tsx
CHANGED
|
@@ -1,51 +1,108 @@
|
|
|
1
|
-
import React, { useEffect, useState, useCallback } from 'react';
|
|
1
|
+
import React, { useEffect, useState, useCallback,FC } from 'react';
|
|
2
2
|
import EditorPanel from './components/EditorPanel';
|
|
3
3
|
import defaults from './data/initial-state';
|
|
4
4
|
import Loading from '@cdc/core/components/Loading';
|
|
5
5
|
import getViewport from '@cdc/core/helpers/getViewport';
|
|
6
|
-
import numberFromString from '@cdc/core/helpers/numberFromString'
|
|
7
6
|
import ResizeObserver from 'resize-observer-polyfill';
|
|
7
|
+
import Papa from 'papaparse';
|
|
8
|
+
import parse from 'html-react-parser';
|
|
9
|
+
|
|
8
10
|
import Context from './context';
|
|
9
11
|
// @ts-ignore
|
|
12
|
+
import DataTransform from '@cdc/core/components/DataTransform';
|
|
10
13
|
import CircleCallout from './components/CircleCallout';
|
|
11
14
|
import './scss/main.scss';
|
|
12
|
-
import
|
|
15
|
+
import numberFromString from '@cdc/core/helpers/numberFromString';
|
|
16
|
+
import { Fragment } from 'react';
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
{ configUrl, config: configObj, isDashboard = false, isEditor = false, setConfig: setParentConfig } :
|
|
16
|
-
{ configUrl?: string, config?: any, isDashboard?: boolean, isEditor?: boolean, setConfig? }
|
|
17
|
-
) => {
|
|
18
|
+
import { publish } from '@cdc/core/helpers/events'
|
|
18
19
|
|
|
19
|
-
interface keyable {
|
|
20
|
-
[key: string]: any
|
|
21
|
-
}
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
type DefaultsType = typeof defaults
|
|
22
|
+
interface Props{
|
|
23
|
+
configUrl?: string,
|
|
24
|
+
config?: any
|
|
25
|
+
isDashboard?: boolean
|
|
26
|
+
isEditor?: boolean
|
|
27
|
+
setConfig?:any
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const CdcDataBite:FC<Props> = (props) => {
|
|
31
|
+
const { configUrl, config: configObj, isDashboard = false, isEditor = false, setConfig: setParentConfig } = props
|
|
32
|
+
|
|
33
|
+
const [config, setConfig] = useState<DefaultsType>({...defaults});
|
|
24
34
|
const [loading, setLoading] = useState<Boolean>(true);
|
|
25
35
|
|
|
26
36
|
const {
|
|
27
37
|
title,
|
|
28
38
|
dataColumn,
|
|
29
39
|
dataFunction,
|
|
30
|
-
|
|
40
|
+
imageData,
|
|
31
41
|
biteBody,
|
|
32
42
|
biteFontSize,
|
|
43
|
+
dataFormat,
|
|
33
44
|
biteStyle,
|
|
34
45
|
filters,
|
|
35
|
-
subtext
|
|
46
|
+
subtext,
|
|
47
|
+
general: { isCompactStyle }
|
|
36
48
|
} = config;
|
|
37
49
|
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
const transform = new DataTransform()
|
|
53
|
+
|
|
38
54
|
const [currentViewport, setCurrentViewport] = useState<String>('lg');
|
|
39
55
|
|
|
56
|
+
const [ coveLoadedHasRan, setCoveLoadedHasRan ] = useState(false)
|
|
57
|
+
|
|
58
|
+
const [ container, setContainer ] = useState()
|
|
59
|
+
|
|
40
60
|
//Observes changes to outermost container and changes viewport size in state
|
|
41
61
|
const resizeObserver = new ResizeObserver(entries => {
|
|
42
62
|
for (let entry of entries) {
|
|
43
63
|
let newViewport = getViewport(entry.contentRect.width * 2) // Data bite is usually presented as small, so we scale it up for responsive calculations
|
|
44
|
-
|
|
45
64
|
setCurrentViewport(newViewport)
|
|
46
65
|
}
|
|
47
66
|
});
|
|
48
67
|
|
|
68
|
+
const fetchRemoteData = async (url) => {
|
|
69
|
+
try {
|
|
70
|
+
const urlObj = new URL(url);
|
|
71
|
+
const regex = /(?:\.([^.]+))?$/
|
|
72
|
+
|
|
73
|
+
let data = []
|
|
74
|
+
|
|
75
|
+
const ext = (regex.exec(urlObj.pathname)[1])
|
|
76
|
+
if ('csv' === ext) {
|
|
77
|
+
data = await fetch(url)
|
|
78
|
+
.then(response => response.text())
|
|
79
|
+
.then(responseText => {
|
|
80
|
+
const parsedCsv = Papa.parse(responseText, {
|
|
81
|
+
header: true,
|
|
82
|
+
dynamicTyping: true,
|
|
83
|
+
skipEmptyLines: true
|
|
84
|
+
})
|
|
85
|
+
return parsedCsv.data
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if ('json' === ext) {
|
|
90
|
+
data = await fetch(url)
|
|
91
|
+
.then(response => response.json())
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return data;
|
|
95
|
+
} catch {
|
|
96
|
+
// If we can't parse it, still attempt to fetch it
|
|
97
|
+
try {
|
|
98
|
+
let response = await (await fetch(configUrl)).json()
|
|
99
|
+
return response
|
|
100
|
+
} catch {
|
|
101
|
+
console.error(`Cannot parse URL: ${url}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
49
106
|
const updateConfig = (newConfig) => {
|
|
50
107
|
// Deeper copy
|
|
51
108
|
Object.keys(defaults).forEach(key => {
|
|
@@ -66,38 +123,116 @@ const CdcDataBite = (
|
|
|
66
123
|
const loadConfig = async () => {
|
|
67
124
|
let response = configObj || await (await fetch(configUrl)).json();
|
|
68
125
|
|
|
126
|
+
const round = 1000 * 60 * 15;
|
|
127
|
+
const date = new Date();
|
|
128
|
+
let cacheBustingString = new Date(date.getTime() - (date.getTime() % round)).toISOString();
|
|
129
|
+
|
|
69
130
|
// If data is included through a URL, fetch that and store
|
|
70
131
|
let responseData = response.data ?? {}
|
|
71
132
|
|
|
72
|
-
if(response.dataUrl) {
|
|
73
|
-
|
|
74
|
-
|
|
133
|
+
if (response.dataUrl) {
|
|
134
|
+
response.dataUrl = `${response.dataUrl}?${cacheBustingString}`;
|
|
135
|
+
let newData = await fetchRemoteData(response.dataUrl)
|
|
136
|
+
|
|
137
|
+
if (newData && response.dataDescription) {
|
|
138
|
+
newData = transform.autoStandardize(newData);
|
|
139
|
+
newData = transform.developerStandardize(newData, response.dataDescription);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if(newData) {
|
|
143
|
+
responseData = newData;
|
|
144
|
+
}
|
|
75
145
|
}
|
|
76
146
|
|
|
77
147
|
response.data = responseData;
|
|
78
148
|
|
|
79
149
|
updateConfig({ ...defaults, ...response });
|
|
150
|
+
|
|
80
151
|
setLoading(false);
|
|
81
152
|
}
|
|
82
153
|
|
|
83
|
-
const calculateDataBite = () => {
|
|
84
|
-
|
|
154
|
+
const calculateDataBite = ():string|number => {
|
|
155
|
+
|
|
85
156
|
//If either the column or function aren't set, do not calculate
|
|
86
157
|
if (!dataColumn || !dataFunction) {
|
|
87
|
-
return '';
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
const applyPrecision =(value:number|string):string => {
|
|
163
|
+
// first validation
|
|
164
|
+
if(value === undefined || value===null){
|
|
165
|
+
console.error('Enter correct value to "applyPrecision()" function ')
|
|
166
|
+
return ;
|
|
167
|
+
}
|
|
168
|
+
// second validation
|
|
169
|
+
if(Number.isNaN(value)){
|
|
170
|
+
console.error(' Argunment isNaN, "applyPrecision()" function ')
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
let result:number|string = value
|
|
174
|
+
let roundToPlace = Number(config.dataFormat.roundToPlace) // default equals to 0
|
|
175
|
+
// ROUND FIELD going -1,-2,-3 numbers
|
|
176
|
+
if(roundToPlace<0) {
|
|
177
|
+
console.error(' ROUND field is below "0", "applyPrecision()" function ')
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if(typeof roundToPlace ==='number' && roundToPlace > -1 ){
|
|
181
|
+
result = Number(result).toFixed(roundToPlace); // returns STRING
|
|
182
|
+
}
|
|
183
|
+
return String(result)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// filter null and 0 out from count data
|
|
187
|
+
const getColumnCount = (arr:(string|number)[]) => {
|
|
188
|
+
if(config.dataFormat.ignoreZeros) {
|
|
189
|
+
numericalData = numericalData.filter( item => item && item)
|
|
190
|
+
return numericalData.length
|
|
191
|
+
} else {
|
|
192
|
+
return numericalData.length
|
|
193
|
+
}
|
|
88
194
|
}
|
|
89
195
|
|
|
90
|
-
const getColumnSum = (arr) => {
|
|
91
|
-
|
|
196
|
+
const getColumnSum = (arr:(string|number)[]) => {
|
|
197
|
+
// first validation
|
|
198
|
+
if(arr===undefined || arr===null){
|
|
199
|
+
console.error('Enter valid value for getColumnSum function ')
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// second validation
|
|
203
|
+
if(arr.length === 0 || !Array.isArray(arr)){
|
|
204
|
+
console.error('Arguments are not valid getColumnSum function ')
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
let sum:number = 0
|
|
208
|
+
if(arr.length > 1){
|
|
209
|
+
/// first convert each element to number then add using reduce method to escape string concatination.
|
|
210
|
+
sum = arr.map(el=>Number(el)).reduce((sum:number, x:number) => sum + x);
|
|
211
|
+
}else {
|
|
212
|
+
sum = Number(arr[0])
|
|
213
|
+
}
|
|
92
214
|
return applyPrecision(sum);
|
|
93
215
|
}
|
|
94
216
|
|
|
95
|
-
const getColumnMean
|
|
96
|
-
|
|
217
|
+
const getColumnMean=(arr:(string|number)[]) => { // add default params to escape errors on runtime
|
|
218
|
+
// first validation
|
|
219
|
+
if(arr===undefined || arr===null ||!Array.isArray(arr)){
|
|
220
|
+
console.error('Enter valid parameter getColumnMean function')
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let mean:number = 0
|
|
225
|
+
if(arr.length > 1){
|
|
226
|
+
/// first convert each element to number then add using reduce method to escape string concatination.
|
|
227
|
+
mean = arr.map(el=>Number(el)).reduce((a, b) => a + b) / arr.length
|
|
228
|
+
}else {
|
|
229
|
+
mean = Number(arr[0])
|
|
230
|
+
}
|
|
97
231
|
return applyPrecision(mean);
|
|
98
232
|
}
|
|
99
233
|
|
|
100
|
-
const getMode = (arr) => {
|
|
234
|
+
const getMode = (arr:any[]=[]):string[] => { // add default params to escape errors on runtime
|
|
235
|
+
// this function accepts any array and returns array of strings
|
|
101
236
|
let freq = {}
|
|
102
237
|
let max = -Infinity
|
|
103
238
|
|
|
@@ -123,27 +258,45 @@ const CdcDataBite = (
|
|
|
123
258
|
}
|
|
124
259
|
|
|
125
260
|
const getMedian = arr => {
|
|
261
|
+
if(!arr.length) return ;
|
|
126
262
|
const mid = Math.floor(arr.length / 2),
|
|
127
263
|
nums = [...arr].sort((a, b) => a - b);
|
|
128
264
|
const value = arr.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2;
|
|
129
265
|
return applyPrecision(value);
|
|
130
266
|
};
|
|
131
267
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
value
|
|
268
|
+
const applyLocaleString = (value:string):string=>{
|
|
269
|
+
if(value===undefined || value===null) return ;
|
|
270
|
+
if(Number.isNaN(value)|| typeof value ==='number') {
|
|
271
|
+
value = String(value)
|
|
272
|
+
|
|
135
273
|
}
|
|
136
|
-
|
|
274
|
+
const language = 'en-US'
|
|
275
|
+
let formattedValue = parseFloat(value).toLocaleString(language, {
|
|
276
|
+
useGrouping: true,
|
|
277
|
+
maximumFractionDigits: 6
|
|
278
|
+
})
|
|
279
|
+
// Add back missing .0 in e.g. 12.0
|
|
280
|
+
const match = value.match(/\.\d*?(0*)$/)
|
|
281
|
+
|
|
282
|
+
if (match){
|
|
283
|
+
formattedValue += (/[1-9]/).test(match[0]) ? match[1] : match[0]
|
|
284
|
+
}
|
|
285
|
+
return formattedValue
|
|
137
286
|
}
|
|
287
|
+
|
|
288
|
+
|
|
138
289
|
|
|
139
|
-
|
|
290
|
+
|
|
140
291
|
|
|
292
|
+
let dataBite:string|number = '';
|
|
293
|
+
|
|
141
294
|
//Optionally filter the data based on the user's filter
|
|
142
295
|
let filteredData = config.data;
|
|
143
296
|
|
|
144
297
|
filters.map((filter) => {
|
|
145
298
|
if ( filter.columnName && filter.columnValue ) {
|
|
146
|
-
|
|
299
|
+
return filteredData = filteredData.filter(function (e) {
|
|
147
300
|
return e[filter.columnName] === filter.columnValue;
|
|
148
301
|
});
|
|
149
302
|
} else {
|
|
@@ -151,17 +304,22 @@ const CdcDataBite = (
|
|
|
151
304
|
}
|
|
152
305
|
});
|
|
153
306
|
|
|
154
|
-
let numericalData = []
|
|
307
|
+
let numericalData:any[] = []
|
|
155
308
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
309
|
+
|
|
310
|
+
// Get the column's data
|
|
311
|
+
if(filteredData.length){
|
|
312
|
+
filteredData.forEach(row => {
|
|
313
|
+
let value = numberFromString(row[dataColumn])
|
|
314
|
+
if(typeof value === 'number') numericalData.push(value)
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
161
319
|
|
|
162
320
|
switch (dataFunction) {
|
|
163
321
|
case DATA_FUNCTION_COUNT:
|
|
164
|
-
dataBite = numericalData
|
|
322
|
+
dataBite = getColumnCount(numericalData);
|
|
165
323
|
break;
|
|
166
324
|
case DATA_FUNCTION_SUM:
|
|
167
325
|
dataBite = getColumnSum(numericalData);
|
|
@@ -176,22 +334,21 @@ const CdcDataBite = (
|
|
|
176
334
|
dataBite = Math.max(...numericalData);
|
|
177
335
|
break;
|
|
178
336
|
case DATA_FUNCTION_MIN:
|
|
179
|
-
dataBite =
|
|
337
|
+
dataBite =Math.min(...numericalData);
|
|
180
338
|
break;
|
|
181
339
|
case DATA_FUNCTION_MODE:
|
|
182
|
-
dataBite = getMode(numericalData).join('
|
|
340
|
+
dataBite = getMode(numericalData).join('');
|
|
183
341
|
break;
|
|
184
342
|
case DATA_FUNCTION_RANGE:
|
|
185
|
-
|
|
186
|
-
let
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
rangeMin =
|
|
191
|
-
rangeMax =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
dataBite = config.dataFormat.prefix + applyPrecision(rangeMin) + config.dataFormat.suffix + ' - ' + config.dataFormat.prefix + applyPrecision(rangeMax) + config.dataFormat.suffix;
|
|
343
|
+
let rangeMin :number|string = Math.min(...numericalData)
|
|
344
|
+
let rangeMax :number|string = Math.max(...numericalData)
|
|
345
|
+
rangeMin = applyPrecision(rangeMin)
|
|
346
|
+
rangeMax = applyPrecision(rangeMax)
|
|
347
|
+
if (config.dataFormat.commas) {
|
|
348
|
+
rangeMin = applyLocaleString(rangeMin)
|
|
349
|
+
rangeMax = applyLocaleString(rangeMax)
|
|
350
|
+
}
|
|
351
|
+
dataBite = config.dataFormat.prefix + rangeMin + config.dataFormat.suffix + ' - ' + config.dataFormat.prefix + rangeMax+config.dataFormat.suffix;
|
|
195
352
|
break;
|
|
196
353
|
default:
|
|
197
354
|
console.warn('Data bite function not recognized: ' + dataFunction);
|
|
@@ -200,36 +357,111 @@ const CdcDataBite = (
|
|
|
200
357
|
// If not the range, then round and format here
|
|
201
358
|
if (dataFunction !== DATA_FUNCTION_RANGE) {
|
|
202
359
|
dataBite = applyPrecision(dataBite);
|
|
203
|
-
|
|
360
|
+
|
|
204
361
|
if (config.dataFormat.commas) {
|
|
205
|
-
|
|
362
|
+
dataBite = applyLocaleString(dataBite)
|
|
206
363
|
}
|
|
364
|
+
// Optional
|
|
365
|
+
// return config.dataFormat.prefix + dataBite + config.dataFormat.suffix;
|
|
366
|
+
|
|
367
|
+
return dataFormat.prefix + dataBite + dataFormat.suffix
|
|
368
|
+
} else {
|
|
369
|
+
//Rounding and formatting for ranges happens earlier.
|
|
207
370
|
|
|
208
|
-
return
|
|
209
|
-
} else { //Rounding and formatting for ranges happens earlier.
|
|
210
|
-
return dataBite;
|
|
371
|
+
return dataFormat.prefix + dataBite + dataFormat.suffix
|
|
211
372
|
}
|
|
212
373
|
}
|
|
213
374
|
|
|
375
|
+
let innerContainerClasses = ['cove-component__inner']
|
|
376
|
+
config.title && innerContainerClasses.push('component--has-title')
|
|
377
|
+
config.subtext && innerContainerClasses.push('component--has-subtext')
|
|
378
|
+
config.biteStyle && innerContainerClasses.push(`bite__style--${config.biteStyle}`)
|
|
379
|
+
config.general?.isCompactStyle && innerContainerClasses.push(`component--isCompactStyle`)
|
|
380
|
+
|
|
381
|
+
let contentClasses = ['cove-component__content'];
|
|
382
|
+
!config.visual?.border && contentClasses.push('no-borders');
|
|
383
|
+
config.visual?.borderColorTheme && contentClasses.push('component--has-borderColorTheme');
|
|
384
|
+
config.visual?.accent && contentClasses.push('component--has-accent');
|
|
385
|
+
config.visual?.background && contentClasses.push('component--has-background');
|
|
386
|
+
config.visual?.hideBackgroundColor && contentClasses.push('component--hideBackgroundColor');
|
|
387
|
+
|
|
388
|
+
// ! these two will be retired.
|
|
389
|
+
config.shadow && innerContainerClasses.push('shadow')
|
|
390
|
+
config?.visual?.roundedBorders && innerContainerClasses.push('bite--has-rounded-borders')
|
|
391
|
+
|
|
214
392
|
// Load data when component first mounts
|
|
215
393
|
const outerContainerRef = useCallback(node => {
|
|
216
394
|
if (node !== null) {
|
|
217
395
|
resizeObserver.observe(node);
|
|
218
396
|
}
|
|
397
|
+
setContainer(node)
|
|
219
398
|
},[]);
|
|
220
399
|
|
|
400
|
+
// Initial load
|
|
221
401
|
useEffect(() => {
|
|
222
|
-
loadConfig()
|
|
402
|
+
loadConfig()
|
|
403
|
+
publish('cove_loaded', { loadConfigHasRun: true })
|
|
223
404
|
}, [])
|
|
224
405
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
406
|
+
|
|
407
|
+
useEffect(() => {
|
|
408
|
+
if (config && !coveLoadedHasRan && container) {
|
|
409
|
+
publish('cove_loaded', { config: config })
|
|
410
|
+
setCoveLoadedHasRan(true)
|
|
411
|
+
}
|
|
412
|
+
}, [config, container]);
|
|
413
|
+
|
|
414
|
+
if(configObj && config && configObj.data !== config.data){
|
|
415
|
+
loadConfig();
|
|
229
416
|
}
|
|
230
417
|
|
|
231
418
|
let body = (<Loading />)
|
|
232
419
|
|
|
420
|
+
const DataImage = useCallback(() => {
|
|
421
|
+
let operators = {
|
|
422
|
+
'<': (a, b) => { return a < b },
|
|
423
|
+
'>': (a, b) => { return a > b },
|
|
424
|
+
'<=': (a, b) => { return a <= b },
|
|
425
|
+
'>=': (a, b) => { return a >= b },
|
|
426
|
+
'≠': (a, b) => { return a !== b },
|
|
427
|
+
'=': (a, b) => { return a === b }
|
|
428
|
+
}
|
|
429
|
+
let imageSource = imageData.url
|
|
430
|
+
let imageAlt = imageData.alt
|
|
431
|
+
|
|
432
|
+
if ('dynamic' === imageData.display && imageData.options && imageData.options?.length > 0) {
|
|
433
|
+
let targetVal = Number(calculateDataBite())
|
|
434
|
+
let argumentActive = false
|
|
435
|
+
|
|
436
|
+
imageData.options.forEach((option, index) => {
|
|
437
|
+
let argumentArr = option.arguments
|
|
438
|
+
let { source, alt } = option
|
|
439
|
+
|
|
440
|
+
if (false === argumentActive && argumentArr.length > 0) {
|
|
441
|
+
if (argumentArr[0].operator.length > 0 && argumentArr[0].threshold.length > 0) {
|
|
442
|
+
if (operators[argumentArr[0].operator](targetVal, argumentArr[0].threshold)) {
|
|
443
|
+
if (undefined !== argumentArr[1]) {
|
|
444
|
+
if (argumentArr[1].operator?.length > 0 && argumentArr[1].threshold?.length > 0) {
|
|
445
|
+
if (operators[argumentArr[1].operator](targetVal, argumentArr[1].threshold)) {
|
|
446
|
+
imageSource = source
|
|
447
|
+
if (alt !== '' && alt !== undefined) { imageAlt = alt }
|
|
448
|
+
argumentActive = true
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
} else {
|
|
452
|
+
imageSource = source
|
|
453
|
+
if (alt !== '' && alt !== undefined) { imageAlt = alt }
|
|
454
|
+
argumentActive = true
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return (imageSource.length > 0 && 'graphic' !== biteStyle && 'none' !== imageData.display ? <img alt={imageAlt} src={imageSource} className="bite-image callout" /> : null)
|
|
463
|
+
}, [ imageData])
|
|
464
|
+
|
|
233
465
|
if(false === loading) {
|
|
234
466
|
let biteClasses = [];
|
|
235
467
|
|
|
@@ -258,30 +490,29 @@ const CdcDataBite = (
|
|
|
258
490
|
if(config.shadow) biteClasses.push('shadow')
|
|
259
491
|
|
|
260
492
|
const showBite = undefined !== dataColumn && undefined !== dataFunction;
|
|
493
|
+
|
|
261
494
|
body = (
|
|
262
495
|
<>
|
|
263
496
|
{isEditor && <EditorPanel />}
|
|
264
497
|
<div className={isEditor ? 'spacing-wrapper' : ''}>
|
|
265
498
|
<div className="cdc-data-bite-inner-container">
|
|
266
|
-
{title && <div className=
|
|
267
|
-
<div className={`bite ${biteClasses.join(' ')}`}>
|
|
268
|
-
<div className=
|
|
269
|
-
{showBite && 'graphic' === biteStyle && isTop && <CircleCallout theme={config.theme} text={calculateDataBite()} biteFontSize={biteFontSize} /> }
|
|
270
|
-
{
|
|
271
|
-
<div className=
|
|
272
|
-
{showBite && 'title' === biteStyle && <div className="bite-value" style={{fontSize: biteFontSize + 'px'}}>{calculateDataBite()}</div>}
|
|
273
|
-
|
|
274
|
-
<>
|
|
499
|
+
{title && <div className={`bite-header component__header ${config.theme}`}>{parse(title)}</div>}
|
|
500
|
+
<div className={`bite ${biteClasses.join(' ')} ${contentClasses.join(' ')}`}>
|
|
501
|
+
<div className={`bite-content-container ${innerContainerClasses.join(' ')}`}>
|
|
502
|
+
{showBite && 'graphic' === biteStyle && isTop && <CircleCallout theme={config.theme} text={calculateDataBite()} biteFontSize={biteFontSize} dataFormat={dataFormat} /> }
|
|
503
|
+
{isTop && <DataImage />}
|
|
504
|
+
<div className={`bite-content`}>
|
|
505
|
+
{showBite && 'title' === biteStyle && <div className="bite-value cove-component__header" style={{fontSize: biteFontSize + 'px'}}>{calculateDataBite()}</div>}
|
|
506
|
+
<Fragment>
|
|
275
507
|
<p className="bite-text">
|
|
276
508
|
{showBite && 'body' === biteStyle && <span className="bite-value data-bite-body" style={{fontSize: biteFontSize + 'px'}}>{calculateDataBite()}</span>}
|
|
277
509
|
{parse(biteBody)}
|
|
278
510
|
</p>
|
|
279
|
-
{subtext && <p className="bite-subtext">{parse(subtext)}</p>}
|
|
280
|
-
|
|
281
|
-
}
|
|
511
|
+
{subtext && !isCompactStyle && <p className="bite-subtext">{parse(subtext)}</p>}
|
|
512
|
+
</Fragment>
|
|
282
513
|
</div>
|
|
283
|
-
{
|
|
284
|
-
{showBite && 'graphic' === biteStyle && !isTop && <CircleCallout theme={config.theme} text={calculateDataBite()} biteFontSize={biteFontSize} /> }
|
|
514
|
+
{isBottom && <DataImage />}
|
|
515
|
+
{showBite && 'graphic' === biteStyle && !isTop && <CircleCallout theme={config.theme} text={calculateDataBite()} biteFontSize={biteFontSize} dataFormat={dataFormat} /> }
|
|
285
516
|
</div>
|
|
286
517
|
</div>
|
|
287
518
|
</div>
|
|
@@ -337,8 +568,8 @@ export const BITE_LOCATION_BODY = 'body';
|
|
|
337
568
|
export const BITE_LOCATION_GRAPHIC = 'graphic';
|
|
338
569
|
export const BITE_LOCATIONS = {
|
|
339
570
|
'graphic': 'Graphic',
|
|
340
|
-
'title': '
|
|
341
|
-
'body': '
|
|
571
|
+
'title': 'Value above Message',
|
|
572
|
+
'body': 'Value before Message'
|
|
342
573
|
};
|
|
343
574
|
|
|
344
575
|
export const IMAGE_POSITION_LEFT = 'Left';
|
|
@@ -351,3 +582,19 @@ export const IMAGE_POSITIONS = [
|
|
|
351
582
|
IMAGE_POSITION_TOP,
|
|
352
583
|
IMAGE_POSITION_BOTTOM,
|
|
353
584
|
];
|
|
585
|
+
|
|
586
|
+
export const DATA_OPERATOR_LESS = '<'
|
|
587
|
+
export const DATA_OPERATOR_GREATER = '>'
|
|
588
|
+
export const DATA_OPERATOR_LESSEQUAL = '<='
|
|
589
|
+
export const DATA_OPERATOR_GREATEREQUAL = '>='
|
|
590
|
+
export const DATA_OPERATOR_EQUAL = '='
|
|
591
|
+
export const DATA_OPERATOR_NOTEQUAL = '≠'
|
|
592
|
+
|
|
593
|
+
export const DATA_OPERATORS = [
|
|
594
|
+
DATA_OPERATOR_LESS,
|
|
595
|
+
DATA_OPERATOR_GREATER,
|
|
596
|
+
DATA_OPERATOR_LESSEQUAL,
|
|
597
|
+
DATA_OPERATOR_GREATEREQUAL,
|
|
598
|
+
DATA_OPERATOR_EQUAL,
|
|
599
|
+
DATA_OPERATOR_NOTEQUAL
|
|
600
|
+
]
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react'
|
|
2
2
|
import themes from '@cdc/core/data/themes';
|
|
3
3
|
import chroma from 'chroma-js';
|
|
4
4
|
|
|
5
|
-
const CircleCallout = ({text, theme = 'theme-blue', biteFontSize}) => {
|
|
5
|
+
const CircleCallout = ({text, theme = 'theme-blue', dataFormat, biteFontSize}) => {
|
|
6
6
|
const styles = {
|
|
7
7
|
outerRing: {
|
|
8
8
|
fill: themes[theme].primary
|
|
@@ -23,4 +23,4 @@ const CircleCallout = ({text, theme = 'theme-blue', biteFontSize}) => {
|
|
|
23
23
|
)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export default CircleCallout
|
|
26
|
+
export default CircleCallout
|