@cdc/data-bite 1.1.1 → 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.1",
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": {
@@ -8,9 +8,6 @@
8
8
  "build": "npx webpack --mode production --env packageName=CdcDataBite --env folderName=data-bite -c ../../webpack.config.js",
9
9
  "prepublishOnly": "lerna run --scope @cdc/data-bite build"
10
10
  },
11
- "files": [
12
- "./dist/"
13
- ],
14
11
  "repository": {
15
12
  "type": "git",
16
13
  "url": "git+https://github.com/CDCgov/cdc-open-viz",
@@ -22,10 +19,12 @@
22
19
  },
23
20
  "license": "Apache-2.0",
24
21
  "homepage": "https://github.com/CDCgov/cdc-open-viz#readme",
25
- "devDependencies": {
26
- "@cdc/core": "^1.1.1",
27
- "bootstrap": "^4.3.1",
28
- "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",
29
28
  "react-accessible-accordion": "^3.3.4",
30
29
  "react-beautiful-dnd": "^13.0.0",
31
30
  "react-select": "^3.0.8",
@@ -33,13 +32,12 @@
33
32
  "use-debounce": "^6.0.1",
34
33
  "whatwg-fetch": "^3.6.2"
35
34
  },
36
- "dependencies": {
37
- "chroma": "0.0.1",
38
- "chroma-js": "^2.1.0"
39
- },
40
35
  "peerDependencies": {
41
36
  "react": "^17.0.2",
42
37
  "react-dom": ">=16"
43
38
  },
44
- "gitHead": "170845062bcbb93f31270104688f69c4148935d8"
39
+ "resolutions": {
40
+ "@types/react": "17.x"
41
+ },
42
+ "gitHead": "ff89a7aea74c533413c62ef8859cc011e6b3cbfa"
45
43
  }
@@ -0,0 +1,600 @@
1
+ import React, { useEffect, useState, useCallback,FC } 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 Papa from 'papaparse';
8
+ import parse from 'html-react-parser';
9
+
10
+ import Context from './context';
11
+ // @ts-ignore
12
+ import DataTransform from '@cdc/core/components/DataTransform';
13
+ import CircleCallout from './components/CircleCallout';
14
+ import './scss/main.scss';
15
+ import numberFromString from '@cdc/core/helpers/numberFromString';
16
+ import { Fragment } from 'react';
17
+
18
+ import { publish } from '@cdc/core/helpers/events'
19
+
20
+
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});
34
+ const [loading, setLoading] = useState<Boolean>(true);
35
+
36
+ const {
37
+ title,
38
+ dataColumn,
39
+ dataFunction,
40
+ imageData,
41
+ biteBody,
42
+ biteFontSize,
43
+ dataFormat,
44
+ biteStyle,
45
+ filters,
46
+ subtext,
47
+ general: { isCompactStyle }
48
+ } = config;
49
+
50
+
51
+
52
+ const transform = new DataTransform()
53
+
54
+ const [currentViewport, setCurrentViewport] = useState<String>('lg');
55
+
56
+ const [ coveLoadedHasRan, setCoveLoadedHasRan ] = useState(false)
57
+
58
+ const [ container, setContainer ] = useState()
59
+
60
+ //Observes changes to outermost container and changes viewport size in state
61
+ const resizeObserver = new ResizeObserver(entries => {
62
+ for (let entry of entries) {
63
+ let newViewport = getViewport(entry.contentRect.width * 2) // Data bite is usually presented as small, so we scale it up for responsive calculations
64
+ setCurrentViewport(newViewport)
65
+ }
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
+ }
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
+
106
+ const updateConfig = (newConfig) => {
107
+ // Deeper copy
108
+ Object.keys(defaults).forEach(key => {
109
+ if (newConfig[key] && 'object' === typeof newConfig[key] && !Array.isArray(newConfig[key])) {
110
+ newConfig[key] = { ...defaults[key], ...newConfig[key] }
111
+ }
112
+ });
113
+
114
+ //Enforce default values that need to be calculated at runtime
115
+ newConfig.runtime = {};
116
+ newConfig.runtime.uniqueId = Date.now();
117
+
118
+ //Check things that are needed and set error messages if needed
119
+ newConfig.runtime.editorErrorMessage = '';
120
+ setConfig(newConfig);
121
+ }
122
+
123
+ 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();
129
+
130
+ // If data is included through a URL, fetch that and store
131
+ let responseData = response.data ?? {}
132
+
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
+ }
145
+ }
146
+
147
+ response.data = responseData;
148
+
149
+ updateConfig({ ...defaults, ...response });
150
+
151
+ setLoading(false);
152
+ }
153
+
154
+ const calculateDataBite = ():string|number => {
155
+
156
+ //If either the column or function aren't set, do not calculate
157
+ if (!dataColumn || !dataFunction) {
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
+ }
194
+ }
195
+
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
+ }
214
+ return applyPrecision(sum);
215
+ }
216
+
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
+ }
231
+ return applyPrecision(mean);
232
+ }
233
+
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
236
+ let freq = {}
237
+ let max = -Infinity
238
+
239
+ for(let i = 0; i < arr.length; i++) {
240
+ if (freq[arr[i]]) {
241
+ freq[arr[i]] += 1
242
+ } else {
243
+ freq[arr[i]] = 1
244
+ }
245
+
246
+ if (freq[arr[i]] > max) {
247
+ max = freq[arr[i]]
248
+ }
249
+ }
250
+
251
+ let res = []
252
+
253
+ for(let key in freq) {
254
+ if(freq[key] === max) res.push(key)
255
+ }
256
+
257
+ return res
258
+ }
259
+
260
+ const getMedian = arr => {
261
+ if(!arr.length) return ;
262
+ 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)
272
+
273
+ }
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
286
+ }
287
+
288
+
289
+
290
+
291
+
292
+ let dataBite:string|number = '';
293
+
294
+ //Optionally filter the data based on the user's filter
295
+ let filteredData = config.data;
296
+
297
+ filters.map((filter) => {
298
+ if ( filter.columnName && filter.columnValue ) {
299
+ return filteredData = filteredData.filter(function (e) {
300
+ return e[filter.columnName] === filter.columnValue;
301
+ });
302
+ } else {
303
+ return false;
304
+ }
305
+ });
306
+
307
+ let numericalData:any[] = []
308
+
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
+
319
+
320
+ switch (dataFunction) {
321
+ case DATA_FUNCTION_COUNT:
322
+ dataBite = getColumnCount(numericalData);
323
+ break;
324
+ case DATA_FUNCTION_SUM:
325
+ dataBite = getColumnSum(numericalData);
326
+ break;
327
+ case DATA_FUNCTION_MEAN:
328
+ dataBite = getColumnMean(numericalData);
329
+ break;
330
+ case DATA_FUNCTION_MEDIAN:
331
+ dataBite = getMedian(numericalData);
332
+ break;
333
+ case DATA_FUNCTION_MAX:
334
+ dataBite = Math.max(...numericalData);
335
+ break;
336
+ case DATA_FUNCTION_MIN:
337
+ dataBite =Math.min(...numericalData);
338
+ break;
339
+ case DATA_FUNCTION_MODE:
340
+ dataBite = getMode(numericalData).join('');
341
+ break;
342
+ case DATA_FUNCTION_RANGE:
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;
352
+ break;
353
+ default:
354
+ console.warn('Data bite function not recognized: ' + dataFunction);
355
+ }
356
+
357
+ // If not the range, then round and format here
358
+ if (dataFunction !== DATA_FUNCTION_RANGE) {
359
+ dataBite = applyPrecision(dataBite);
360
+
361
+ if (config.dataFormat.commas) {
362
+ dataBite = applyLocaleString(dataBite)
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.
370
+
371
+ return dataFormat.prefix + dataBite + dataFormat.suffix
372
+ }
373
+ }
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
+
392
+ // Load data when component first mounts
393
+ const outerContainerRef = useCallback(node => {
394
+ if (node !== null) {
395
+ resizeObserver.observe(node);
396
+ }
397
+ setContainer(node)
398
+ },[]);
399
+
400
+ // Initial load
401
+ useEffect(() => {
402
+ loadConfig()
403
+ publish('cove_loaded', { loadConfigHasRun: true })
404
+ }, [])
405
+
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();
416
+ }
417
+
418
+ let body = (<Loading />)
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
+
465
+ if(false === loading) {
466
+ let biteClasses = [];
467
+
468
+ let isTop = false
469
+ let isBottom = false
470
+
471
+ switch (config.bitePosition) {
472
+ case IMAGE_POSITION_LEFT:
473
+ biteClasses.push('bite-left');
474
+ isTop = true
475
+ break;
476
+ case IMAGE_POSITION_RIGHT:
477
+ biteClasses.push('bite-right');
478
+ isTop = true
479
+ break;
480
+ case IMAGE_POSITION_TOP:
481
+ biteClasses.push('bite-top');
482
+ isTop = true
483
+ break;
484
+ case IMAGE_POSITION_BOTTOM:
485
+ biteClasses.push('bite-bottom');
486
+ isBottom = true
487
+ break;
488
+ }
489
+
490
+ if(config.shadow) biteClasses.push('shadow')
491
+
492
+ const showBite = undefined !== dataColumn && undefined !== dataFunction;
493
+
494
+ body = (
495
+ <>
496
+ {isEditor && <EditorPanel />}
497
+ <div className={isEditor ? 'spacing-wrapper' : ''}>
498
+ <div className="cdc-data-bite-inner-container">
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>
507
+ <p className="bite-text">
508
+ {showBite && 'body' === biteStyle && <span className="bite-value data-bite-body" style={{fontSize: biteFontSize + 'px'}}>{calculateDataBite()}</span>}
509
+ {parse(biteBody)}
510
+ </p>
511
+ {subtext && !isCompactStyle && <p className="bite-subtext">{parse(subtext)}</p>}
512
+ </Fragment>
513
+ </div>
514
+ {isBottom && <DataImage />}
515
+ {showBite && 'graphic' === biteStyle && !isTop && <CircleCallout theme={config.theme} text={calculateDataBite()} biteFontSize={biteFontSize} dataFormat={dataFormat} /> }
516
+ </div>
517
+ </div>
518
+ </div>
519
+ </div>
520
+ </>
521
+ )
522
+ }
523
+
524
+ let classNames = [
525
+ 'cdc-open-viz-module',
526
+ 'type-data-bite',
527
+ currentViewport,
528
+ config.theme,
529
+ 'font-' + config.fontSize
530
+ ];
531
+ if (isEditor) {
532
+ classNames.push('is-editor');
533
+ }
534
+
535
+ return (
536
+ <Context.Provider value={{ config, updateConfig, loading, data: config.data, setParentConfig, isDashboard }}>
537
+ <div className={classNames.join(' ')} ref={outerContainerRef}>
538
+ {body}
539
+ </div>
540
+ </Context.Provider>
541
+ );
542
+ };
543
+
544
+ export default CdcDataBite;
545
+
546
+ /* 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
+ DATA_FUNCTION_COUNT,
557
+ DATA_FUNCTION_MAX,
558
+ DATA_FUNCTION_MEAN,
559
+ DATA_FUNCTION_MEDIAN,
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';
569
+ export const BITE_LOCATIONS = {
570
+ 'graphic': 'Graphic',
571
+ 'title': 'Value above Message',
572
+ 'body': 'Value before Message'
573
+ };
574
+
575
+ export const IMAGE_POSITION_LEFT = 'Left';
576
+ export const IMAGE_POSITION_RIGHT = 'Right';
577
+ export const IMAGE_POSITION_TOP = 'Top';
578
+ export const IMAGE_POSITION_BOTTOM = 'Bottom';
579
+ export const IMAGE_POSITIONS = [
580
+ IMAGE_POSITION_LEFT,
581
+ IMAGE_POSITION_RIGHT,
582
+ IMAGE_POSITION_TOP,
583
+ IMAGE_POSITION_BOTTOM,
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
+ ]
@@ -0,0 +1,26 @@
1
+ import React from 'react'
2
+ import themes from '@cdc/core/data/themes';
3
+ import chroma from 'chroma-js';
4
+
5
+ const CircleCallout = ({text, theme = 'theme-blue', dataFormat, biteFontSize}) => {
6
+ const styles = {
7
+ outerRing: {
8
+ fill: themes[theme].primary
9
+ },
10
+ innerRing: {
11
+ fill: chroma(themes[theme].primary).darken(0.5)
12
+ },
13
+ text: {
14
+ fill: '#FFF'
15
+ }
16
+ }
17
+
18
+ return (
19
+ <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" className="circle-callout callout">
20
+ <circle cx="50" cy="50" r="45" style={styles.innerRing} strokeWidth="10" stroke={styles.outerRing.fill} />
21
+ <text y="50%" x="50%" fontSize={biteFontSize} style={styles.text} textAnchor="middle" dominantBaseline="central">{text}</text>
22
+ </svg>
23
+ )
24
+ }
25
+
26
+ export default CircleCallout