@cdc/data-bite 1.1.2 → 1.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cdc/data-bite",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
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
- "devDependencies": {
23
- "@cdc/core": "^1.1.2",
24
- "bootstrap": "^4.3.1",
25
- "html-react-parser": "^0.14.0",
22
+ "dependencies": {
23
+ "@cdc/core": "^1.1.3",
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
- "gitHead": "8790a2336ff2453d0d6f09ecec56b5e4beaa7377"
39
+ "resolutions": {
40
+ "@types/react": "17.x"
41
+ },
42
+ "gitHead": "ff89a7aea74c533413c62ef8859cc011e6b3cbfa"
42
43
  }
@@ -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 parse from 'html-react-parser'
15
+ import numberFromString from '@cdc/core/helpers/numberFromString';
16
+ import { Fragment } from 'react';
13
17
 
14
- const CdcDataBite = (
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
- const [config, setConfig] = useState<keyable>({...defaults});
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
- imageUrl,
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
- const dataString = await fetch(response.dataUrl);
74
- responseData = await dataString.json();
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
- const sum = arr.reduce((sum, x) => sum + x);
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 = (arr) => {
96
- const mean = arr.length > 1 ? arr.reduce((a, b) => a + b) / arr.length : arr[0];
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
- const applyPrecision = (value) => {
133
- if ('' !== config.dataFormat.roundToPlace && !isNaN(config.dataFormat.roundToPlace) && Number(config.dataFormat.roundToPlace)>-1) {
134
- value = Number(value).toFixed(Number(config.dataFormat.roundToPlace));
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
- return value;
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
- let dataBite = null;
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
- filteredData = filteredData.filter(function (e) {
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
- //Get the column's data
157
- filteredData.forEach(row => {
158
- let value = numberFromString(row[dataColumn])
159
- if(typeof value === 'number') numericalData.push(value)
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.length;
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 = Math.min(...numericalData);
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
- numericalData.sort((a, b) => a - b);
186
- let rangeMin = applyPrecision(numericalData[0]);
187
- let rangeMax = applyPrecision(numericalData[numericalData.length - 1]);
188
-
189
- if (config.dataFormat.commas) {
190
- rangeMin = Number(rangeMin).toLocaleString('en-US');
191
- rangeMax = Number(rangeMax).toLocaleString('en-US');
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
- dataBite = Number(dataBite).toLocaleString('en-US');
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 config.dataFormat.prefix + dataBite + config.dataFormat.suffix;
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
- if(configObj) {
226
- useEffect(() => {
227
- loadConfig();
228
- }, [configObj.data])
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="bite-header">{parse(title)}</div>}
267
- <div className={`bite ${biteClasses.join(' ')}`}>
268
- <div className="bite-content-container">
269
- {showBite && 'graphic' === biteStyle && isTop && <CircleCallout theme={config.theme} text={calculateDataBite()} biteFontSize={biteFontSize} /> }
270
- {imageUrl && 'graphic' !== biteStyle && isTop && <img src={imageUrl} className="bite-image callout" />}
271
- <div className="bite-content">
272
- {showBite && 'title' === biteStyle && <div className="bite-value" style={{fontSize: biteFontSize + 'px'}}>{calculateDataBite()}</div>}
273
- {biteBody &&
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
- {imageUrl && 'graphic' !== biteStyle && isBottom && <img src={imageUrl} className="bite-image callout" />}
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': 'Text above body text',
341
- 'body': 'Inline with body text'
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, { useState, useEffect, memo, useContext } from '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