@cdc/data-bite 1.1.4 → 4.22.11
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 +4 -4
- package/dist/cdcdatabite.js +8 -8
- package/examples/example-config.json +24 -24
- package/examples/example-data.json +833 -833
- package/examples/gallery/calculated-average.json +3167 -0
- package/examples/gallery/calculated-with-pic.json +3173 -0
- package/examples/gallery/max-value.json +3167 -0
- package/examples/gallery/sum-of-data.json +3161 -0
- package/examples/private/WCMSRD-12345.json +1026 -1026
- package/examples/private/double.json +0 -0
- package/examples/private/totals.json +67 -0
- package/package.json +3 -3
- package/src/CdcDataBite.tsx +278 -329
- package/src/components/CircleCallout.js +8 -6
- package/src/components/EditorPanel.js +418 -300
- package/src/context.tsx +3 -3
- package/src/data/initial-state.js +37 -37
- package/src/index.html +22 -17
- package/src/index.tsx +9 -9
- package/src/scss/bite.scss +111 -44
- package/src/scss/editor-panel.scss +83 -71
- package/src/scss/main.scss +17 -9
- package/src/scss/variables.scss +1 -1
package/src/CdcDataBite.tsx
CHANGED
|
@@ -1,37 +1,38 @@
|
|
|
1
|
-
import React, { useEffect,
|
|
2
|
-
import EditorPanel from './components/EditorPanel'
|
|
3
|
-
import defaults from './data/initial-state'
|
|
4
|
-
import Loading from '@cdc/core/components/Loading'
|
|
5
|
-
import getViewport from '@cdc/core/helpers/getViewport'
|
|
6
|
-
import ResizeObserver from 'resize-observer-polyfill'
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import '
|
|
15
|
-
import
|
|
16
|
-
import { Fragment } from 'react';
|
|
1
|
+
import React, { useEffect, useState, useCallback, FC, memo } from 'react'
|
|
2
|
+
import EditorPanel from './components/EditorPanel'
|
|
3
|
+
import defaults from './data/initial-state'
|
|
4
|
+
import Loading from '@cdc/core/components/Loading'
|
|
5
|
+
import getViewport from '@cdc/core/helpers/getViewport'
|
|
6
|
+
import ResizeObserver from 'resize-observer-polyfill'
|
|
7
|
+
import parse from 'html-react-parser'
|
|
8
|
+
|
|
9
|
+
import Context from './context'
|
|
10
|
+
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
11
|
+
import CircleCallout from './components/CircleCallout'
|
|
12
|
+
import './scss/main.scss'
|
|
13
|
+
import numberFromString from '@cdc/core/helpers/numberFromString'
|
|
14
|
+
import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
|
|
15
|
+
import { Fragment } from 'react'
|
|
17
16
|
|
|
18
17
|
import { publish } from '@cdc/core/helpers/events'
|
|
19
|
-
|
|
18
|
+
import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
|
|
19
|
+
import cacheBustingString from '@cdc/core/helpers/cacheBustingString'
|
|
20
20
|
|
|
21
21
|
type DefaultsType = typeof defaults
|
|
22
|
-
interface Props{
|
|
23
|
-
configUrl?: string
|
|
22
|
+
interface Props {
|
|
23
|
+
configUrl?: string
|
|
24
24
|
config?: any
|
|
25
25
|
isDashboard?: boolean
|
|
26
26
|
isEditor?: boolean
|
|
27
|
-
setConfig?:any
|
|
27
|
+
setConfig?: any
|
|
28
|
+
link?: any
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const CdcDataBite:FC<Props> =
|
|
31
|
-
const { configUrl, config: configObj, isDashboard = false, isEditor = false, setConfig: setParentConfig } = props
|
|
31
|
+
const CdcDataBite: FC<Props> = props => {
|
|
32
|
+
const { configUrl, config: configObj, isDashboard = false, isEditor = false, setConfig: setParentConfig, link } = props
|
|
32
33
|
|
|
33
|
-
const [config, setConfig] = useState<DefaultsType>({...defaults})
|
|
34
|
-
const [loading, setLoading] = useState<Boolean>(true)
|
|
34
|
+
const [config, setConfig] = useState<DefaultsType>({ ...defaults })
|
|
35
|
+
const [loading, setLoading] = useState<Boolean>(true)
|
|
35
36
|
|
|
36
37
|
const {
|
|
37
38
|
title,
|
|
@@ -45,198 +46,161 @@ const { configUrl, config: configObj, isDashboard = false, isEditor = false, set
|
|
|
45
46
|
filters,
|
|
46
47
|
subtext,
|
|
47
48
|
general: { isCompactStyle }
|
|
48
|
-
} = config
|
|
49
|
-
|
|
49
|
+
} = config
|
|
50
50
|
|
|
51
|
+
const { innerContainerClasses, contentClasses } = useDataVizClasses(config)
|
|
51
52
|
|
|
52
53
|
const transform = new DataTransform()
|
|
53
54
|
|
|
54
|
-
const [currentViewport, setCurrentViewport] = useState<String>('lg')
|
|
55
|
+
const [currentViewport, setCurrentViewport] = useState<String>('lg')
|
|
55
56
|
|
|
56
|
-
const [
|
|
57
|
+
const [coveLoadedHasRan, setCoveLoadedHasRan] = useState(false)
|
|
57
58
|
|
|
58
|
-
const [
|
|
59
|
+
const [container, setContainer] = useState()
|
|
59
60
|
|
|
60
61
|
//Observes changes to outermost container and changes viewport size in state
|
|
61
62
|
const resizeObserver = new ResizeObserver(entries => {
|
|
62
63
|
for (let entry of entries) {
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
let newViewport = getViewport(entry.contentRect.width * 2) // Data bite is usually presented as small, so we scale it up for responsive calculations
|
|
65
|
+
setCurrentViewport(newViewport)
|
|
65
66
|
}
|
|
66
|
-
})
|
|
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
|
-
}
|
|
67
|
+
})
|
|
88
68
|
|
|
89
|
-
|
|
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
|
-
|
|
106
|
-
const updateConfig = (newConfig) => {
|
|
69
|
+
const updateConfig = newConfig => {
|
|
107
70
|
// Deeper copy
|
|
108
71
|
Object.keys(defaults).forEach(key => {
|
|
109
72
|
if (newConfig[key] && 'object' === typeof newConfig[key] && !Array.isArray(newConfig[key])) {
|
|
110
73
|
newConfig[key] = { ...defaults[key], ...newConfig[key] }
|
|
111
74
|
}
|
|
112
|
-
})
|
|
75
|
+
})
|
|
113
76
|
|
|
114
77
|
//Enforce default values that need to be calculated at runtime
|
|
115
|
-
newConfig.runtime = {}
|
|
116
|
-
newConfig.runtime.uniqueId = Date.now()
|
|
78
|
+
newConfig.runtime = {}
|
|
79
|
+
newConfig.runtime.uniqueId = Date.now()
|
|
117
80
|
|
|
118
81
|
//Check things that are needed and set error messages if needed
|
|
119
|
-
newConfig.runtime.editorErrorMessage = ''
|
|
120
|
-
setConfig(newConfig)
|
|
82
|
+
newConfig.runtime.editorErrorMessage = ''
|
|
83
|
+
setConfig(newConfig)
|
|
121
84
|
}
|
|
122
85
|
|
|
123
86
|
const loadConfig = async () => {
|
|
124
|
-
let response = configObj || await (await fetch(configUrl)).json()
|
|
125
|
-
|
|
126
|
-
const round = 1000 * 60 * 15;
|
|
127
|
-
const date = new Date();
|
|
128
|
-
let cacheBustingString = new Date(date.getTime() - (date.getTime() % round)).toISOString();
|
|
87
|
+
let response = configObj || (await (await fetch(configUrl)).json())
|
|
129
88
|
|
|
130
89
|
// If data is included through a URL, fetch that and store
|
|
131
90
|
let responseData = response.data ?? {}
|
|
132
91
|
|
|
133
92
|
if (response.dataUrl) {
|
|
134
|
-
response.dataUrl = `${response.dataUrl}?${cacheBustingString}
|
|
93
|
+
response.dataUrl = `${response.dataUrl}?${cacheBustingString()}`
|
|
135
94
|
let newData = await fetchRemoteData(response.dataUrl)
|
|
136
|
-
|
|
95
|
+
|
|
137
96
|
if (newData && response.dataDescription) {
|
|
138
|
-
|
|
139
|
-
|
|
97
|
+
newData = transform.autoStandardize(newData)
|
|
98
|
+
newData = transform.developerStandardize(newData, response.dataDescription)
|
|
140
99
|
}
|
|
141
100
|
|
|
142
|
-
if(newData) {
|
|
143
|
-
responseData = newData
|
|
101
|
+
if (newData) {
|
|
102
|
+
responseData = newData
|
|
144
103
|
}
|
|
145
104
|
}
|
|
146
105
|
|
|
147
|
-
response.data = responseData
|
|
106
|
+
response.data = responseData
|
|
148
107
|
|
|
149
|
-
updateConfig({ ...defaults, ...response })
|
|
108
|
+
updateConfig({ ...defaults, ...response })
|
|
150
109
|
|
|
151
|
-
setLoading(false)
|
|
110
|
+
setLoading(false)
|
|
152
111
|
}
|
|
153
112
|
|
|
154
|
-
const calculateDataBite = ():string|number => {
|
|
155
|
-
|
|
113
|
+
const calculateDataBite = (includePrefixSuffix: boolean = true): string | number => {
|
|
156
114
|
//If either the column or function aren't set, do not calculate
|
|
157
115
|
if (!dataColumn || !dataFunction) {
|
|
158
|
-
return ''
|
|
116
|
+
return ''
|
|
159
117
|
}
|
|
160
118
|
|
|
161
|
-
|
|
162
|
-
const applyPrecision =(value:number|string):string => {
|
|
119
|
+
const applyPrecision = (value: number | string): string => {
|
|
163
120
|
// first validation
|
|
164
|
-
if(value === undefined || value===null){
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
121
|
+
if (value === undefined || value === null) {
|
|
122
|
+
console.error('Enter correct value to "applyPrecision()" function ')
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
// second validation
|
|
126
|
+
if (Number.isNaN(value)) {
|
|
127
|
+
console.error(' Argunment isNaN, "applyPrecision()" function ')
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
let result: number | string = value
|
|
131
|
+
let roundToPlace = Number(config.dataFormat.roundToPlace) // default equals to 0
|
|
132
|
+
// ROUND FIELD going -1,-2,-3 numbers
|
|
133
|
+
if (roundToPlace < 0) {
|
|
134
|
+
console.error(' ROUND field is below "0", "applyPrecision()" function ')
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
if (typeof roundToPlace === 'number' && roundToPlace > -1) {
|
|
138
|
+
result = Number(result).toFixed(roundToPlace) // returns STRING
|
|
139
|
+
}
|
|
140
|
+
return String(result)
|
|
184
141
|
}
|
|
185
142
|
|
|
186
143
|
// filter null and 0 out from count data
|
|
187
|
-
const getColumnCount = (arr:(string|number)[]) => {
|
|
188
|
-
if(config.dataFormat.ignoreZeros) {
|
|
189
|
-
numericalData = numericalData.filter(
|
|
144
|
+
const getColumnCount = (arr: (string | number)[]) => {
|
|
145
|
+
if (config.dataFormat.ignoreZeros) {
|
|
146
|
+
numericalData = numericalData.filter(item => item && item)
|
|
190
147
|
return numericalData.length
|
|
191
148
|
} else {
|
|
192
149
|
return numericalData.length
|
|
193
150
|
}
|
|
194
151
|
}
|
|
195
152
|
|
|
196
|
-
const getColumnSum = (arr:(string|number)[]) => {
|
|
153
|
+
const getColumnSum = (arr: (string | number)[]) => {
|
|
197
154
|
// first validation
|
|
198
|
-
if(arr===undefined || arr===null){
|
|
155
|
+
if (arr === undefined || arr === null) {
|
|
199
156
|
console.error('Enter valid value for getColumnSum function ')
|
|
200
|
-
return
|
|
157
|
+
return
|
|
201
158
|
}
|
|
202
159
|
// second validation
|
|
203
|
-
|
|
160
|
+
console.log('arr', arr)
|
|
161
|
+
if (arr.length === 0 || !Array.isArray(arr)) {
|
|
204
162
|
console.error('Arguments are not valid getColumnSum function ')
|
|
205
|
-
return
|
|
163
|
+
return
|
|
206
164
|
}
|
|
207
|
-
let sum:number = 0
|
|
208
|
-
if(arr.length > 1){
|
|
165
|
+
let sum: number = 0
|
|
166
|
+
if (arr.length > 1) {
|
|
209
167
|
/// first convert each element to number then add using reduce method to escape string concatination.
|
|
210
|
-
|
|
211
|
-
}else {
|
|
168
|
+
sum = arr.map(el => Number(el)).reduce((sum: number, x: number) => sum + x)
|
|
169
|
+
} else {
|
|
212
170
|
sum = Number(arr[0])
|
|
213
171
|
}
|
|
214
|
-
return applyPrecision(sum)
|
|
172
|
+
return applyPrecision(sum)
|
|
215
173
|
}
|
|
216
174
|
|
|
217
|
-
const getColumnMean=(arr:(string|number)[]) => {
|
|
175
|
+
const getColumnMean = (arr: (string | number)[]) => {
|
|
176
|
+
// add default params to escape errors on runtime
|
|
218
177
|
// first validation
|
|
219
|
-
if(arr===undefined || arr===null
|
|
178
|
+
if (arr === undefined || arr === null || !Array.isArray(arr)) {
|
|
220
179
|
console.error('Enter valid parameter getColumnMean function')
|
|
221
|
-
return
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (config.dataFormat.ignoreZeros) {
|
|
184
|
+
arr = arr.filter(num => num !== 0)
|
|
222
185
|
}
|
|
223
|
-
|
|
224
|
-
let mean:number = 0
|
|
225
|
-
if(arr.length > 1){
|
|
226
|
-
|
|
227
|
-
mean = arr.map(el=>Number(el)).reduce((a, b) => a + b) / arr.length
|
|
228
|
-
}else {
|
|
186
|
+
|
|
187
|
+
let mean: number = 0
|
|
188
|
+
if (arr.length > 1) {
|
|
189
|
+
/// first convert each element to number then add using reduce method to escape string concatination.
|
|
190
|
+
mean = arr.map(el => Number(el)).reduce((a, b) => a + b) / arr.length
|
|
191
|
+
} else {
|
|
229
192
|
mean = Number(arr[0])
|
|
230
193
|
}
|
|
231
|
-
return applyPrecision(mean)
|
|
194
|
+
return applyPrecision(mean)
|
|
232
195
|
}
|
|
233
196
|
|
|
234
|
-
const getMode = (arr:any[]=[]):string[] => {
|
|
235
|
-
|
|
197
|
+
const getMode = (arr: any[] = []): string[] => {
|
|
198
|
+
// add default params to escape errors on runtime
|
|
199
|
+
// this function accepts any array and returns array of strings
|
|
236
200
|
let freq = {}
|
|
237
201
|
let max = -Infinity
|
|
238
202
|
|
|
239
|
-
for(let i = 0; i < arr.length; i++) {
|
|
203
|
+
for (let i = 0; i < arr.length; i++) {
|
|
240
204
|
if (freq[arr[i]]) {
|
|
241
205
|
freq[arr[i]] += 1
|
|
242
206
|
} else {
|
|
@@ -250,152 +214,128 @@ const { configUrl, config: configObj, isDashboard = false, isEditor = false, set
|
|
|
250
214
|
|
|
251
215
|
let res = []
|
|
252
216
|
|
|
253
|
-
for(let key in freq) {
|
|
254
|
-
if(freq[key] === max) res.push(key)
|
|
217
|
+
for (let key in freq) {
|
|
218
|
+
if (freq[key] === max) res.push(key)
|
|
255
219
|
}
|
|
256
220
|
|
|
257
221
|
return res
|
|
258
222
|
}
|
|
259
223
|
|
|
260
224
|
const getMedian = arr => {
|
|
261
|
-
if(!arr.length) return
|
|
225
|
+
if (!arr.length) return
|
|
262
226
|
const mid = Math.floor(arr.length / 2),
|
|
263
|
-
nums = [...arr].sort((a, b) => a - b)
|
|
264
|
-
const value = arr.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2
|
|
265
|
-
return applyPrecision(value)
|
|
266
|
-
}
|
|
267
|
-
|
|
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)
|
|
227
|
+
nums = [...arr].sort((a, b) => a - b)
|
|
228
|
+
const value = arr.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2
|
|
229
|
+
return applyPrecision(value)
|
|
230
|
+
}
|
|
272
231
|
|
|
232
|
+
const applyLocaleString = (value: string): string => {
|
|
233
|
+
if (value === undefined || value === null) return
|
|
234
|
+
if (Number.isNaN(value) || typeof value === 'number') {
|
|
235
|
+
value = String(value)
|
|
273
236
|
}
|
|
274
237
|
const language = 'en-US'
|
|
275
238
|
let formattedValue = parseFloat(value).toLocaleString(language, {
|
|
276
239
|
useGrouping: true,
|
|
277
240
|
maximumFractionDigits: 6
|
|
278
241
|
})
|
|
279
|
-
|
|
280
|
-
|
|
242
|
+
// Add back missing .0 in e.g. 12.0
|
|
243
|
+
const match = value.match(/\.\d*?(0*)$/)
|
|
281
244
|
|
|
282
|
-
if (match){
|
|
283
|
-
|
|
245
|
+
if (match) {
|
|
246
|
+
formattedValue += /[1-9]/.test(match[0]) ? match[1] : match[0]
|
|
284
247
|
}
|
|
285
248
|
return formattedValue
|
|
286
249
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
250
|
|
|
290
|
-
|
|
251
|
+
let dataBite: string | number = ''
|
|
291
252
|
|
|
292
|
-
let dataBite:string|number = '';
|
|
293
|
-
|
|
294
253
|
//Optionally filter the data based on the user's filter
|
|
295
|
-
let filteredData = config.data
|
|
254
|
+
let filteredData = config.data
|
|
296
255
|
|
|
297
|
-
filters.map(
|
|
298
|
-
if (
|
|
299
|
-
|
|
300
|
-
return e[filter.columnName] === filter.columnValue
|
|
301
|
-
})
|
|
256
|
+
filters.map(filter => {
|
|
257
|
+
if (filter.columnName && filter.columnValue) {
|
|
258
|
+
return (filteredData = filteredData.filter(function (e) {
|
|
259
|
+
return e[filter.columnName] === filter.columnValue
|
|
260
|
+
}))
|
|
302
261
|
} else {
|
|
303
|
-
return false
|
|
262
|
+
return false
|
|
304
263
|
}
|
|
305
|
-
})
|
|
264
|
+
})
|
|
306
265
|
|
|
307
|
-
let numericalData:any[] = []
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
// Get the column's data
|
|
311
|
-
if(filteredData.length){
|
|
266
|
+
let numericalData: any[] = []
|
|
267
|
+
// Get the column's data
|
|
268
|
+
if (filteredData.length) {
|
|
312
269
|
filteredData.forEach(row => {
|
|
313
270
|
let value = numberFromString(row[dataColumn])
|
|
314
|
-
if(typeof value === 'number') numericalData.push(value)
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
271
|
+
if (typeof value === 'number') numericalData.push(value)
|
|
272
|
+
})
|
|
273
|
+
} else {
|
|
274
|
+
numericalData = config.data.map(item => Number(item[config.dataColumn]))
|
|
275
|
+
}
|
|
319
276
|
|
|
320
277
|
switch (dataFunction) {
|
|
321
278
|
case DATA_FUNCTION_COUNT:
|
|
322
|
-
dataBite = getColumnCount(numericalData)
|
|
323
|
-
break
|
|
279
|
+
dataBite = getColumnCount(numericalData)
|
|
280
|
+
break
|
|
324
281
|
case DATA_FUNCTION_SUM:
|
|
325
|
-
dataBite = getColumnSum(numericalData)
|
|
326
|
-
break
|
|
282
|
+
dataBite = getColumnSum(numericalData)
|
|
283
|
+
break
|
|
327
284
|
case DATA_FUNCTION_MEAN:
|
|
328
|
-
dataBite = getColumnMean(numericalData)
|
|
329
|
-
break
|
|
285
|
+
dataBite = getColumnMean(numericalData)
|
|
286
|
+
break
|
|
330
287
|
case DATA_FUNCTION_MEDIAN:
|
|
331
|
-
dataBite = getMedian(numericalData)
|
|
332
|
-
break
|
|
288
|
+
dataBite = getMedian(numericalData)
|
|
289
|
+
break
|
|
333
290
|
case DATA_FUNCTION_MAX:
|
|
334
|
-
dataBite = Math.max(...numericalData)
|
|
335
|
-
break
|
|
291
|
+
dataBite = Math.max(...numericalData)
|
|
292
|
+
break
|
|
336
293
|
case DATA_FUNCTION_MIN:
|
|
337
|
-
dataBite =Math.min(...numericalData)
|
|
338
|
-
break
|
|
294
|
+
dataBite = Math.min(...numericalData)
|
|
295
|
+
break
|
|
339
296
|
case DATA_FUNCTION_MODE:
|
|
340
|
-
dataBite = getMode(numericalData).join('')
|
|
341
|
-
break
|
|
297
|
+
dataBite = getMode(numericalData).join('')
|
|
298
|
+
break
|
|
342
299
|
case DATA_FUNCTION_RANGE:
|
|
343
|
-
let rangeMin
|
|
344
|
-
let rangeMax
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
300
|
+
let rangeMin: number | string = Math.min(...numericalData)
|
|
301
|
+
let rangeMax: number | string = Math.max(...numericalData)
|
|
302
|
+
rangeMin = applyPrecision(rangeMin)
|
|
303
|
+
rangeMax = applyPrecision(rangeMax)
|
|
304
|
+
if (config.dataFormat.commas) {
|
|
348
305
|
rangeMin = applyLocaleString(rangeMin)
|
|
349
306
|
rangeMax = applyLocaleString(rangeMax)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
break
|
|
307
|
+
}
|
|
308
|
+
dataBite = config.dataFormat.prefix + rangeMin + config.dataFormat.suffix + ' - ' + config.dataFormat.prefix + rangeMax + config.dataFormat.suffix
|
|
309
|
+
break
|
|
353
310
|
default:
|
|
354
|
-
console.warn('Data bite function not recognized: ' + dataFunction)
|
|
311
|
+
console.warn('Data bite function not recognized: ' + dataFunction)
|
|
355
312
|
}
|
|
356
313
|
|
|
357
314
|
// If not the range, then round and format here
|
|
358
315
|
if (dataFunction !== DATA_FUNCTION_RANGE) {
|
|
359
|
-
dataBite = applyPrecision(dataBite)
|
|
360
|
-
|
|
316
|
+
dataBite = applyPrecision(dataBite)
|
|
317
|
+
|
|
361
318
|
if (config.dataFormat.commas) {
|
|
362
|
-
|
|
319
|
+
dataBite = applyLocaleString(dataBite)
|
|
363
320
|
}
|
|
364
|
-
|
|
321
|
+
// Optional
|
|
365
322
|
// return config.dataFormat.prefix + dataBite + config.dataFormat.suffix;
|
|
366
323
|
|
|
367
|
-
|
|
368
|
-
} else {
|
|
324
|
+
return includePrefixSuffix ? dataFormat.prefix + dataBite + dataFormat.suffix : dataBite
|
|
325
|
+
} else {
|
|
369
326
|
//Rounding and formatting for ranges happens earlier.
|
|
370
327
|
|
|
371
|
-
return dataFormat.prefix + dataBite + dataFormat.suffix
|
|
328
|
+
return includePrefixSuffix ? dataFormat.prefix + dataBite + dataFormat.suffix : dataBite
|
|
372
329
|
}
|
|
373
330
|
}
|
|
374
331
|
|
|
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
|
-
|
|
392
332
|
// Load data when component first mounts
|
|
393
333
|
const outerContainerRef = useCallback(node => {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
},[])
|
|
334
|
+
if (node !== null) {
|
|
335
|
+
resizeObserver.observe(node)
|
|
336
|
+
}
|
|
337
|
+
setContainer(node)
|
|
338
|
+
}, [])
|
|
399
339
|
|
|
400
340
|
// Initial load
|
|
401
341
|
useEffect(() => {
|
|
@@ -403,34 +343,45 @@ const { configUrl, config: configObj, isDashboard = false, isEditor = false, set
|
|
|
403
343
|
publish('cove_loaded', { loadConfigHasRun: true })
|
|
404
344
|
}, [])
|
|
405
345
|
|
|
406
|
-
|
|
407
346
|
useEffect(() => {
|
|
408
347
|
if (config && !coveLoadedHasRan && container) {
|
|
409
348
|
publish('cove_loaded', { config: config })
|
|
410
349
|
setCoveLoadedHasRan(true)
|
|
411
350
|
}
|
|
412
|
-
}, [config, container])
|
|
351
|
+
}, [config, container])
|
|
413
352
|
|
|
414
|
-
if(configObj && config && configObj.data !== config.data){
|
|
415
|
-
loadConfig()
|
|
353
|
+
if (configObj && config && JSON.stringify(configObj.data) !== JSON.stringify(config.data)) {
|
|
354
|
+
loadConfig()
|
|
416
355
|
}
|
|
417
356
|
|
|
418
|
-
let body =
|
|
357
|
+
let body = <Loading />
|
|
419
358
|
|
|
420
359
|
const DataImage = useCallback(() => {
|
|
421
360
|
let operators = {
|
|
422
|
-
'<': (a, b) => {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
'
|
|
426
|
-
|
|
427
|
-
|
|
361
|
+
'<': (a, b) => {
|
|
362
|
+
return a < b
|
|
363
|
+
},
|
|
364
|
+
'>': (a, b) => {
|
|
365
|
+
return a > b
|
|
366
|
+
},
|
|
367
|
+
'<=': (a, b) => {
|
|
368
|
+
return a <= b
|
|
369
|
+
},
|
|
370
|
+
'>=': (a, b) => {
|
|
371
|
+
return a >= b
|
|
372
|
+
},
|
|
373
|
+
'≠': (a, b) => {
|
|
374
|
+
return a !== b
|
|
375
|
+
},
|
|
376
|
+
'=': (a, b) => {
|
|
377
|
+
return a === b
|
|
378
|
+
}
|
|
428
379
|
}
|
|
429
380
|
let imageSource = imageData.url
|
|
430
381
|
let imageAlt = imageData.alt
|
|
431
382
|
|
|
432
383
|
if ('dynamic' === imageData.display && imageData.options && imageData.options?.length > 0) {
|
|
433
|
-
let targetVal = Number(calculateDataBite())
|
|
384
|
+
let targetVal = Number(calculateDataBite(false))
|
|
434
385
|
let argumentActive = false
|
|
435
386
|
|
|
436
387
|
imageData.options.forEach((option, index) => {
|
|
@@ -444,13 +395,17 @@ const { configUrl, config: configObj, isDashboard = false, isEditor = false, set
|
|
|
444
395
|
if (argumentArr[1].operator?.length > 0 && argumentArr[1].threshold?.length > 0) {
|
|
445
396
|
if (operators[argumentArr[1].operator](targetVal, argumentArr[1].threshold)) {
|
|
446
397
|
imageSource = source
|
|
447
|
-
if (alt !== '' && alt !== undefined) {
|
|
398
|
+
if (alt !== '' && alt !== undefined) {
|
|
399
|
+
imageAlt = alt
|
|
400
|
+
}
|
|
448
401
|
argumentActive = true
|
|
449
402
|
}
|
|
450
403
|
}
|
|
451
404
|
} else {
|
|
452
405
|
imageSource = source
|
|
453
|
-
if (alt !== '' && alt !== undefined) {
|
|
406
|
+
if (alt !== '' && alt !== undefined) {
|
|
407
|
+
imageAlt = alt
|
|
408
|
+
}
|
|
454
409
|
argumentActive = true
|
|
455
410
|
}
|
|
456
411
|
}
|
|
@@ -459,77 +414,90 @@ const { configUrl, config: configObj, isDashboard = false, isEditor = false, set
|
|
|
459
414
|
})
|
|
460
415
|
}
|
|
461
416
|
|
|
462
|
-
return
|
|
463
|
-
}, [
|
|
417
|
+
return imageSource.length > 0 && 'graphic' !== biteStyle && 'none' !== imageData.display ? <img alt={imageAlt} src={imageSource} className='bite-image callout' /> : null
|
|
418
|
+
}, [imageData])
|
|
464
419
|
|
|
465
|
-
if(false === loading) {
|
|
466
|
-
let biteClasses = []
|
|
420
|
+
if (false === loading) {
|
|
421
|
+
let biteClasses = []
|
|
467
422
|
|
|
468
423
|
let isTop = false
|
|
469
424
|
let isBottom = false
|
|
470
425
|
|
|
471
426
|
switch (config.bitePosition) {
|
|
472
427
|
case IMAGE_POSITION_LEFT:
|
|
473
|
-
biteClasses.push('bite-left')
|
|
428
|
+
biteClasses.push('bite-left')
|
|
474
429
|
isTop = true
|
|
475
|
-
break
|
|
430
|
+
break
|
|
476
431
|
case IMAGE_POSITION_RIGHT:
|
|
477
|
-
biteClasses.push('bite-right')
|
|
432
|
+
biteClasses.push('bite-right')
|
|
478
433
|
isTop = true
|
|
479
|
-
break
|
|
434
|
+
break
|
|
480
435
|
case IMAGE_POSITION_TOP:
|
|
481
|
-
biteClasses.push('bite-top')
|
|
436
|
+
biteClasses.push('bite-top')
|
|
482
437
|
isTop = true
|
|
483
|
-
break
|
|
438
|
+
break
|
|
484
439
|
case IMAGE_POSITION_BOTTOM:
|
|
485
|
-
biteClasses.push('bite-bottom')
|
|
440
|
+
biteClasses.push('bite-bottom')
|
|
486
441
|
isBottom = true
|
|
487
|
-
break
|
|
442
|
+
break
|
|
488
443
|
}
|
|
489
444
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const showBite = undefined !== dataColumn && undefined !== dataFunction;
|
|
445
|
+
const showBite = undefined !== dataColumn && undefined !== dataFunction
|
|
493
446
|
|
|
494
447
|
body = (
|
|
495
448
|
<>
|
|
496
449
|
{isEditor && <EditorPanel />}
|
|
497
450
|
<div className={isEditor ? 'spacing-wrapper' : ''}>
|
|
498
|
-
<div className=
|
|
499
|
-
{title && <div className={`bite-header component__header ${config.theme}`}>{parse(title)}</div>}
|
|
500
|
-
<div className={`bite ${biteClasses.join(' ')}
|
|
501
|
-
<div className={`bite-content-container ${
|
|
502
|
-
{showBite && 'graphic' === biteStyle && isTop && <CircleCallout theme={config.theme} text={calculateDataBite()} biteFontSize={biteFontSize} dataFormat={dataFormat} />
|
|
451
|
+
<div className={innerContainerClasses.join(' ')}>
|
|
452
|
+
{title && <div className={`bite-header cove-component__header component__header ${config.theme}`}>{parse(title)}</div>}
|
|
453
|
+
<div className={`bite ${biteClasses.join(' ')}`}>
|
|
454
|
+
<div className={`bite-content-container ${contentClasses.join(' ')}`}>
|
|
455
|
+
{showBite && 'graphic' === biteStyle && isTop && <CircleCallout theme={config.theme} text={calculateDataBite()} biteFontSize={biteFontSize} dataFormat={dataFormat} />}
|
|
503
456
|
{isTop && <DataImage />}
|
|
504
457
|
<div className={`bite-content`}>
|
|
505
|
-
{showBite && 'title' === biteStyle &&
|
|
506
|
-
<
|
|
507
|
-
|
|
508
|
-
|
|
458
|
+
{showBite && 'title' === biteStyle && (
|
|
459
|
+
<div className='bite-value' style={{ fontSize: biteFontSize + 'px' }}>
|
|
460
|
+
{calculateDataBite()}
|
|
461
|
+
</div>
|
|
462
|
+
)}
|
|
463
|
+
{showBite && 'split' === biteStyle && (
|
|
464
|
+
<div className='bite-value' style={{ fontSize: biteFontSize + 'px' }}>
|
|
465
|
+
{calculateDataBite()}
|
|
466
|
+
</div>
|
|
467
|
+
)}
|
|
468
|
+
<Fragment>
|
|
469
|
+
<div className='bite-content__text-wrap'>
|
|
470
|
+
<p className='bite-text'>
|
|
471
|
+
{showBite && 'body' === biteStyle && (
|
|
472
|
+
<span className='bite-value data-bite-body' style={{ fontSize: biteFontSize + 'px' }}>
|
|
473
|
+
{calculateDataBite()}
|
|
474
|
+
</span>
|
|
475
|
+
)}
|
|
509
476
|
{parse(biteBody)}
|
|
510
477
|
</p>
|
|
511
|
-
{
|
|
512
|
-
|
|
478
|
+
{showBite && 'end' === biteStyle && (
|
|
479
|
+
<span className='bite-value data-bite-body' style={{ fontSize: biteFontSize + 'px' }}>
|
|
480
|
+
{calculateDataBite()}
|
|
481
|
+
</span>
|
|
482
|
+
)}
|
|
483
|
+
{subtext && !isCompactStyle && <p className='bite-subtext'>{parse(subtext)}</p>}
|
|
484
|
+
</div>
|
|
485
|
+
</Fragment>
|
|
513
486
|
</div>
|
|
514
487
|
{isBottom && <DataImage />}
|
|
515
|
-
{showBite && 'graphic' === biteStyle && !isTop && <CircleCallout theme={config.theme} text={calculateDataBite()} biteFontSize={biteFontSize} dataFormat={dataFormat} />
|
|
488
|
+
{showBite && 'graphic' === biteStyle && !isTop && <CircleCallout theme={config.theme} text={calculateDataBite()} biteFontSize={biteFontSize} dataFormat={dataFormat} />}
|
|
516
489
|
</div>
|
|
517
490
|
</div>
|
|
518
491
|
</div>
|
|
492
|
+
{link && link}
|
|
519
493
|
</div>
|
|
520
494
|
</>
|
|
521
495
|
)
|
|
522
496
|
}
|
|
523
497
|
|
|
524
|
-
let classNames = [
|
|
525
|
-
'cdc-open-viz-module',
|
|
526
|
-
'type-data-bite',
|
|
527
|
-
currentViewport,
|
|
528
|
-
config.theme,
|
|
529
|
-
'font-' + config.fontSize
|
|
530
|
-
];
|
|
498
|
+
let classNames = ['cove', 'cdc-open-viz-module', 'type-data-bite', currentViewport, config.theme, 'font-' + config.fontSize]
|
|
531
499
|
if (isEditor) {
|
|
532
|
-
classNames.push('is-editor')
|
|
500
|
+
classNames.push('is-editor')
|
|
533
501
|
}
|
|
534
502
|
|
|
535
503
|
return (
|
|
@@ -538,50 +506,38 @@ const { configUrl, config: configObj, isDashboard = false, isEditor = false, set
|
|
|
538
506
|
{body}
|
|
539
507
|
</div>
|
|
540
508
|
</Context.Provider>
|
|
541
|
-
)
|
|
542
|
-
}
|
|
509
|
+
)
|
|
510
|
+
}
|
|
543
511
|
|
|
544
|
-
export default CdcDataBite
|
|
512
|
+
export default CdcDataBite
|
|
545
513
|
|
|
546
514
|
/* Constant */
|
|
547
|
-
export const DATA_FUNCTION_MAX = 'Max'
|
|
548
|
-
export const DATA_FUNCTION_COUNT = 'Count'
|
|
549
|
-
export const DATA_FUNCTION_MEAN = 'Mean (Average)'
|
|
550
|
-
export const DATA_FUNCTION_MEDIAN = 'Median'
|
|
551
|
-
export const DATA_FUNCTION_MIN = 'Min'
|
|
552
|
-
export const DATA_FUNCTION_MODE = 'Mode'
|
|
553
|
-
export const DATA_FUNCTION_RANGE = 'Range'
|
|
554
|
-
export const DATA_FUNCTION_SUM = 'Sum'
|
|
555
|
-
export const DATA_FUNCTIONS = [
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
DATA_FUNCTION_MIN,
|
|
561
|
-
DATA_FUNCTION_MODE,
|
|
562
|
-
DATA_FUNCTION_RANGE,
|
|
563
|
-
DATA_FUNCTION_SUM
|
|
564
|
-
];
|
|
565
|
-
|
|
566
|
-
export const BITE_LOCATION_TITLE = 'title';
|
|
567
|
-
export const BITE_LOCATION_BODY = 'body';
|
|
568
|
-
export const BITE_LOCATION_GRAPHIC = 'graphic';
|
|
515
|
+
export const DATA_FUNCTION_MAX = 'Max'
|
|
516
|
+
export const DATA_FUNCTION_COUNT = 'Count'
|
|
517
|
+
export const DATA_FUNCTION_MEAN = 'Mean (Average)'
|
|
518
|
+
export const DATA_FUNCTION_MEDIAN = 'Median'
|
|
519
|
+
export const DATA_FUNCTION_MIN = 'Min'
|
|
520
|
+
export const DATA_FUNCTION_MODE = 'Mode'
|
|
521
|
+
export const DATA_FUNCTION_RANGE = 'Range'
|
|
522
|
+
export const DATA_FUNCTION_SUM = 'Sum'
|
|
523
|
+
export const DATA_FUNCTIONS = [DATA_FUNCTION_COUNT, DATA_FUNCTION_MAX, DATA_FUNCTION_MEAN, DATA_FUNCTION_MEDIAN, DATA_FUNCTION_MIN, DATA_FUNCTION_MODE, DATA_FUNCTION_RANGE, DATA_FUNCTION_SUM]
|
|
524
|
+
|
|
525
|
+
export const BITE_LOCATION_TITLE = 'title'
|
|
526
|
+
export const BITE_LOCATION_BODY = 'body'
|
|
527
|
+
export const BITE_LOCATION_GRAPHIC = 'graphic'
|
|
569
528
|
export const BITE_LOCATIONS = {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
export const
|
|
578
|
-
export const
|
|
579
|
-
export const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
IMAGE_POSITION_TOP,
|
|
583
|
-
IMAGE_POSITION_BOTTOM,
|
|
584
|
-
];
|
|
529
|
+
graphic: 'Graphic',
|
|
530
|
+
split: 'Split Graphic and Message',
|
|
531
|
+
title: 'Value above Message',
|
|
532
|
+
body: 'Value before Message',
|
|
533
|
+
end: 'Value after Message'
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export const IMAGE_POSITION_LEFT = 'Left'
|
|
537
|
+
export const IMAGE_POSITION_RIGHT = 'Right'
|
|
538
|
+
export const IMAGE_POSITION_TOP = 'Top'
|
|
539
|
+
export const IMAGE_POSITION_BOTTOM = 'Bottom'
|
|
540
|
+
export const IMAGE_POSITIONS = [IMAGE_POSITION_LEFT, IMAGE_POSITION_RIGHT, IMAGE_POSITION_TOP, IMAGE_POSITION_BOTTOM]
|
|
585
541
|
|
|
586
542
|
export const DATA_OPERATOR_LESS = '<'
|
|
587
543
|
export const DATA_OPERATOR_GREATER = '>'
|
|
@@ -590,11 +546,4 @@ export const DATA_OPERATOR_GREATEREQUAL = '>='
|
|
|
590
546
|
export const DATA_OPERATOR_EQUAL = '='
|
|
591
547
|
export const DATA_OPERATOR_NOTEQUAL = '≠'
|
|
592
548
|
|
|
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
|
-
]
|
|
549
|
+
export const DATA_OPERATORS = [DATA_OPERATOR_LESS, DATA_OPERATOR_GREATER, DATA_OPERATOR_LESSEQUAL, DATA_OPERATOR_GREATEREQUAL, DATA_OPERATOR_EQUAL, DATA_OPERATOR_NOTEQUAL]
|