@cdc/waffle-chart 1.0.0

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