@cdc/waffle-chart 4.25.10 → 4.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdcwafflechart.js +9025 -8034
- package/examples/tp5-style-white.json +72 -0
- package/examples/tp5-style.json +72 -0
- package/examples/tp5-waffle.json +123 -0
- package/index.html +3 -6
- package/package.json +5 -5
- package/src/CdcWaffleChart.tsx +165 -119
- package/src/_stories/WaffleChart.Editor.stories.tsx +45 -54
- package/src/_stories/WaffleChart.stories.tsx +46 -1
- package/src/components/EditorPanel.jsx +179 -356
- package/src/images/callout-flag.svg +7 -0
- package/src/scss/main.scss +2 -12
- package/src/scss/waffle-chart.scss +91 -23
- package/src/store/chart.reducer.ts +1 -1
- package/src/test/CdcWaffleChart.test.jsx +1 -1
- package/src/types/Config.ts +4 -2
- package/LICENSE +0 -201
package/src/CdcWaffleChart.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useReducer, useState } from 'react'
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'
|
|
2
2
|
|
|
3
3
|
// visx
|
|
4
4
|
import { Circle, Bar } from '@visx/shape'
|
|
@@ -13,6 +13,7 @@ import ResizeObserver from 'resize-observer-polyfill'
|
|
|
13
13
|
import { Config } from './types/Config'
|
|
14
14
|
import getViewport from '@cdc/core/helpers/getViewport'
|
|
15
15
|
import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
|
|
16
|
+
import { DATA_OPERATORS } from '@cdc/core/helpers/constants'
|
|
16
17
|
|
|
17
18
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
18
19
|
import Loading from '@cdc/core/components/Loading'
|
|
@@ -29,6 +30,13 @@ import './scss/main.scss'
|
|
|
29
30
|
import Title from '@cdc/core/components/ui/Title'
|
|
30
31
|
import Layout from '@cdc/core/components/Layout'
|
|
31
32
|
|
|
33
|
+
// images
|
|
34
|
+
import CalloutFlag from './images/callout-flag.svg?url'
|
|
35
|
+
|
|
36
|
+
// TP5 Style Constants
|
|
37
|
+
const TP5_NODE_WIDTH = 13
|
|
38
|
+
const TP5_NODE_SPACER = 3
|
|
39
|
+
|
|
32
40
|
type CdcWaffleChartProps = {
|
|
33
41
|
configUrl?: string
|
|
34
42
|
config?: Config
|
|
@@ -67,7 +75,7 @@ const WaffleChart = ({ config, isEditor, link = '', showConfigConfirm, updateCon
|
|
|
67
75
|
const gaugeColor = config.visual.colors[config.theme]
|
|
68
76
|
let dataFontSize = config.fontSize ? { fontSize: config.fontSize + 'px' } : null
|
|
69
77
|
|
|
70
|
-
const
|
|
78
|
+
const [dataPercentage, waffleDenominator, waffleNumerator] = useMemo(() => {
|
|
71
79
|
//If either the column or function aren't set, do not calculate
|
|
72
80
|
if (!dataColumn || !dataFunction) {
|
|
73
81
|
return ''
|
|
@@ -239,26 +247,26 @@ const WaffleChart = ({ config, isEditor, link = '', showConfigConfirm, updateCon
|
|
|
239
247
|
applyPrecision(waffleNumerator)
|
|
240
248
|
]
|
|
241
249
|
}, [
|
|
242
|
-
dataColumn,
|
|
243
|
-
dataFunction,
|
|
244
250
|
config.data,
|
|
245
251
|
filters,
|
|
252
|
+
dataColumn,
|
|
253
|
+
dataFunction,
|
|
246
254
|
dataConditionalColumn,
|
|
247
255
|
dataConditionalOperator,
|
|
248
256
|
dataConditionalComparate,
|
|
249
257
|
customDenom,
|
|
258
|
+
dataDenom,
|
|
250
259
|
dataDenomColumn,
|
|
251
260
|
dataDenomFunction,
|
|
252
|
-
roundToPlace
|
|
253
|
-
dataDenom
|
|
261
|
+
roundToPlace
|
|
254
262
|
])
|
|
255
263
|
|
|
256
|
-
const [dataPercentage, waffleDenominator, waffleNumerator] = calculateData()
|
|
257
|
-
|
|
258
264
|
const buildWaffle = useCallback(() => {
|
|
259
265
|
let waffleData = []
|
|
260
|
-
|
|
261
|
-
|
|
266
|
+
// Use standardized values for TP5 style
|
|
267
|
+
const isTP5 = config.visualizationType === 'TP5 Waffle'
|
|
268
|
+
let nodeWidthNum = isTP5 ? TP5_NODE_WIDTH : parseInt(nodeWidth, 10)
|
|
269
|
+
let nodeSpacerNum = isTP5 ? TP5_NODE_SPACER : parseInt(nodeSpacer, 10)
|
|
262
270
|
|
|
263
271
|
const calculatePos = (shape, axis, index, width, spacer) => {
|
|
264
272
|
let mod = axis === 'x' ? index % 10 : axis === 'y' ? Math.floor(index / 10) : null
|
|
@@ -271,7 +279,8 @@ const WaffleChart = ({ config, isEditor, link = '', showConfigConfirm, updateCon
|
|
|
271
279
|
x: calculatePos(shape, 'x', i, nodeWidthNum, nodeSpacerNum),
|
|
272
280
|
y: calculatePos(shape, 'y', i, nodeWidthNum, nodeSpacerNum),
|
|
273
281
|
color: config.visual.colors[theme],
|
|
274
|
-
opacity: i + 1 > 100 - Math.round(dataPercentage) ? 1 : 0.
|
|
282
|
+
opacity: i + 1 > 100 - Math.round(dataPercentage) ? 1 : 0.2,
|
|
283
|
+
isFilled: i + 1 > 100 - Math.round(dataPercentage)
|
|
275
284
|
}
|
|
276
285
|
waffleData.push(newNode)
|
|
277
286
|
}
|
|
@@ -281,25 +290,37 @@ const WaffleChart = ({ config, isEditor, link = '', showConfigConfirm, updateCon
|
|
|
281
290
|
<Bar
|
|
282
291
|
className='cdc-waffle-chart__node'
|
|
283
292
|
style={{ transitionDelay: `${0.1 * key}ms` }}
|
|
284
|
-
x={node.x}
|
|
285
|
-
y={node.y}
|
|
286
|
-
width={nodeWidthNum}
|
|
287
|
-
height={nodeWidthNum}
|
|
288
|
-
fill={node.color}
|
|
289
|
-
fillOpacity={node.opacity}
|
|
293
|
+
x={isTP5 && !node.isFilled ? node.x + 1 : node.x}
|
|
294
|
+
y={isTP5 && !node.isFilled ? node.y + 1 : node.y}
|
|
295
|
+
width={isTP5 && !node.isFilled ? nodeWidthNum - 2 : nodeWidthNum}
|
|
296
|
+
height={isTP5 && !node.isFilled ? nodeWidthNum - 2 : nodeWidthNum}
|
|
297
|
+
fill={isTP5 ? (node.isFilled ? '#009EC1' : '#DFF2F6') : node.color}
|
|
298
|
+
fillOpacity={isTP5 ? 1 : node.opacity}
|
|
299
|
+
stroke={isTP5 ? (!node.isFilled ? '#009EC1' : undefined) : undefined}
|
|
300
|
+
strokeWidth={isTP5 ? (!node.isFilled ? 1 : 0) : 0}
|
|
290
301
|
key={key}
|
|
291
302
|
/>
|
|
292
303
|
) : node.shape === 'person' ? (
|
|
293
304
|
<path
|
|
305
|
+
className='cdc-waffle-chart__node'
|
|
294
306
|
style={{
|
|
295
|
-
transform:
|
|
307
|
+
transform: isTP5
|
|
308
|
+
? `translateX(${node.x}px) translateY(${node.y + nodeWidthNum * 0.1}px) scale(${
|
|
309
|
+
(nodeWidthNum * 0.8) / 448
|
|
310
|
+
})`
|
|
311
|
+
: `translateX(${node.x + nodeWidthNum / 4}px) translateY(${node.y}px) scale(${nodeWidthNum / 20})`,
|
|
312
|
+
transitionDelay: `${0.1 * key}ms`
|
|
296
313
|
}}
|
|
297
|
-
fill={node.color}
|
|
298
|
-
fillOpacity={node.opacity}
|
|
314
|
+
fill={isTP5 ? (node.isFilled ? '#009EC1' : 'transparent') : node.color}
|
|
315
|
+
fillOpacity={isTP5 ? 1 : node.opacity}
|
|
316
|
+
stroke={isTP5 ? (!node.isFilled ? '#009EC1' : undefined) : undefined}
|
|
317
|
+
strokeWidth={isTP5 ? (!node.isFilled ? 448 / nodeWidthNum : 0) : 0}
|
|
299
318
|
key={key}
|
|
300
|
-
d=
|
|
301
|
-
|
|
302
|
-
|
|
319
|
+
d={
|
|
320
|
+
isTP5
|
|
321
|
+
? 'M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512l388.6 0c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304l-91.4 0z'
|
|
322
|
+
: '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,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,.9375-.9375V13.75h.625A.9375.9375,0,0,0,7.5,12.8125V7.5A1.875,1.875,0,0,0,5.625,5.625Z'
|
|
323
|
+
}
|
|
303
324
|
></path>
|
|
304
325
|
) : (
|
|
305
326
|
<Circle
|
|
@@ -307,18 +328,28 @@ const WaffleChart = ({ config, isEditor, link = '', showConfigConfirm, updateCon
|
|
|
307
328
|
style={{ transitionDelay: `${0.1 * key}ms` }}
|
|
308
329
|
cx={node.x}
|
|
309
330
|
cy={node.y}
|
|
310
|
-
r={nodeWidthNum / 2}
|
|
311
|
-
fill={node.color}
|
|
312
|
-
fillOpacity={node.opacity}
|
|
331
|
+
r={isTP5 && !node.isFilled ? nodeWidthNum / 2 - 1 : nodeWidthNum / 2}
|
|
332
|
+
fill={isTP5 ? (node.isFilled ? '#009EC1' : '#DFF2F6') : node.color}
|
|
333
|
+
fillOpacity={isTP5 ? 1 : node.opacity}
|
|
334
|
+
stroke={isTP5 ? (!node.isFilled ? '#009EC1' : undefined) : undefined}
|
|
335
|
+
strokeWidth={isTP5 ? (!node.isFilled ? 1 : 0) : 0}
|
|
313
336
|
key={key}
|
|
314
337
|
/>
|
|
315
338
|
)
|
|
316
339
|
)
|
|
317
|
-
}, [theme, dataPercentage, shape, nodeWidth, nodeSpacer])
|
|
340
|
+
}, [theme, dataPercentage, shape, nodeWidth, nodeSpacer, config.visualizationType, config.visual?.whiteBackground])
|
|
318
341
|
|
|
319
342
|
const setRatio = useCallback(() => {
|
|
320
|
-
|
|
321
|
-
|
|
343
|
+
const isTP5 = config.visualizationType === 'TP5 Waffle'
|
|
344
|
+
const width = isTP5 ? TP5_NODE_WIDTH : nodeWidth
|
|
345
|
+
const spacer = isTP5 ? TP5_NODE_SPACER : nodeSpacer
|
|
346
|
+
return width * 10 + spacer * 9
|
|
347
|
+
}, [nodeWidth, nodeSpacer, config.visualizationType])
|
|
348
|
+
|
|
349
|
+
const setSvgSize = useCallback(() => {
|
|
350
|
+
// Add 2px padding to account for strokes on edges
|
|
351
|
+
return setRatio() + 2
|
|
352
|
+
}, [nodeWidth, nodeSpacer, config.visualizationType])
|
|
322
353
|
|
|
323
354
|
const { innerContainerClasses, contentClasses } = useDataVizClasses(config)
|
|
324
355
|
|
|
@@ -359,76 +390,108 @@ const WaffleChart = ({ config, isEditor, link = '', showConfigConfirm, updateCon
|
|
|
359
390
|
)
|
|
360
391
|
}
|
|
361
392
|
|
|
393
|
+
// Render waffle chart content (without title)
|
|
394
|
+
const renderChartContent = () => (
|
|
395
|
+
<>
|
|
396
|
+
{!config.newViz && config.runtime && config.runtime.editorErrorMessage && (
|
|
397
|
+
<Error updateConfig={updateConfig} config={config} />
|
|
398
|
+
)}
|
|
399
|
+
{config.newViz && showConfigConfirm && <Confirm updateConfig={updateConfig} config={config} />}
|
|
400
|
+
<div className='cove-component__content-wrap p-0'>
|
|
401
|
+
{config.visualizationType === 'Gauge' && (
|
|
402
|
+
<div className={`cove-gauge-chart${config.overallFontSize ? ' font-' + config.overallFontSize : ''}`}>
|
|
403
|
+
<div className='cove-gauge-chart__chart'>
|
|
404
|
+
<div className='cove-waffle-chart__data--primary' style={dataFontSize}>
|
|
405
|
+
{prefix ? prefix : ' '}
|
|
406
|
+
{config.showPercent ? dataPercentage : waffleNumerator}
|
|
407
|
+
{suffix ? suffix + ' ' : ' '} {config.valueDescription}{' '}
|
|
408
|
+
{config.showDenominator && waffleDenominator ? waffleDenominator : ' '}
|
|
409
|
+
</div>
|
|
410
|
+
<div className='cove-waffle-chart__data--text'>{parse(content)}</div>
|
|
411
|
+
<svg height={config.gauge.height + 4} width={'100%'} style={{ overflow: 'visible' }}>
|
|
412
|
+
<Group top={2} left={2}>
|
|
413
|
+
<foreignObject
|
|
414
|
+
style={{ border: '1px solid black' }}
|
|
415
|
+
x={0}
|
|
416
|
+
y={0}
|
|
417
|
+
width={config.gauge.width}
|
|
418
|
+
height={config.gauge.height}
|
|
419
|
+
fill='#fff'
|
|
420
|
+
/>
|
|
421
|
+
<Bar x={0} y={0} width={xScale(waffleNumerator)} height={config.gauge.height} fill={gaugeColor} />
|
|
422
|
+
</Group>
|
|
423
|
+
</svg>
|
|
424
|
+
<div className={'cove-waffle-chart__subtext subtext'}>{parse(subtext)}</div>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
)}
|
|
428
|
+
{config.visualizationType !== 'Gauge' && (
|
|
429
|
+
<div
|
|
430
|
+
className={`cove-waffle-chart${orientation === 'vertical' ? ' cove-waffle-chart--verical' : ''}${
|
|
431
|
+
config.overallFontSize ? ' font-' + config.overallFontSize : ''
|
|
432
|
+
}`}
|
|
433
|
+
>
|
|
434
|
+
<div className='cove-waffle-chart__chart' style={{ width: setRatio() }}>
|
|
435
|
+
<svg width={setSvgSize()} height={setSvgSize()} style={{ display: 'block' }}>
|
|
436
|
+
<Group top={1} left={1}>
|
|
437
|
+
{buildWaffle()}
|
|
438
|
+
</Group>
|
|
439
|
+
</svg>
|
|
440
|
+
</div>
|
|
441
|
+
{(dataPercentage || content) && (
|
|
442
|
+
<div className='cove-waffle-chart__data'>
|
|
443
|
+
{dataPercentage && (
|
|
444
|
+
<div className='cove-waffle-chart__data--primary' style={dataFontSize}>
|
|
445
|
+
{prefix ? prefix : null}
|
|
446
|
+
{dataPercentage}
|
|
447
|
+
{suffix ? suffix : null}
|
|
448
|
+
</div>
|
|
449
|
+
)}
|
|
450
|
+
<div className='cove-waffle-chart__data--text'>{parse(content)}</div>
|
|
451
|
+
|
|
452
|
+
{subtext && <div className='cove-waffle-chart__subtext subtext fst-italic'>{parse(subtext)}</div>}
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
</div>
|
|
456
|
+
)}
|
|
457
|
+
</div>
|
|
458
|
+
</>
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
// TP5 Style: render with callout wrapper inside cove-component__content
|
|
462
|
+
if (config.visualizationType === 'TP5 Waffle') {
|
|
463
|
+
const calloutClasses = ['cdc-callout', 'd-flex', 'flex-column']
|
|
464
|
+
if (!config.visual?.whiteBackground) {
|
|
465
|
+
calloutClasses.push('dfe-block', 'cdc-callout--data')
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return (
|
|
469
|
+
<div className='cove-component__content p-0 border-0'>
|
|
470
|
+
<div className={calloutClasses.join(' ')}>
|
|
471
|
+
{!config.visual?.whiteBackground && (
|
|
472
|
+
<img src={CalloutFlag} alt='' className='cdc-callout__flag' aria-hidden='true' />
|
|
473
|
+
)}
|
|
474
|
+
{config.showTitle && title && title.trim() && (
|
|
475
|
+
<h3 className='cdc-callout__heading fw-bold flex-shrink-0'>{parse(title)}</h3>
|
|
476
|
+
)}
|
|
477
|
+
<div className='w-100 mw-100 overflow-hidden'>{renderChartContent()}</div>
|
|
478
|
+
</div>
|
|
479
|
+
{link && link}
|
|
480
|
+
</div>
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Original Style: Regular title and content
|
|
362
485
|
return (
|
|
363
486
|
<div className='cove-component__content'>
|
|
364
487
|
<Title
|
|
365
488
|
showTitle={config.showTitle}
|
|
366
489
|
title={title}
|
|
490
|
+
titleStyle='legacy'
|
|
367
491
|
config={config}
|
|
368
492
|
classes={['chart-title', `${config.theme}`, 'mb-0']}
|
|
369
493
|
/>
|
|
370
|
-
<div className={contentClasses.join(' ')}>
|
|
371
|
-
{!config.newViz && config.runtime && config.runtime.editorErrorMessage && (
|
|
372
|
-
<Error updateConfig={updateConfig} config={config} />
|
|
373
|
-
)}
|
|
374
|
-
{config.newViz && showConfigConfirm && <Confirm updateConfig={updateConfig} config={config} />}
|
|
375
|
-
<div className='cove-component__content-wrap'>
|
|
376
|
-
{config.visualizationType === 'Gauge' && (
|
|
377
|
-
<div className={`cove-gauge-chart${config.overallFontSize ? ' font-' + config.overallFontSize : ''}`}>
|
|
378
|
-
<div className='cove-gauge-chart__chart'>
|
|
379
|
-
<div className='cove-waffle-chart__data--primary' style={dataFontSize}>
|
|
380
|
-
{prefix ? prefix : ' '}
|
|
381
|
-
{config.showPercent ? dataPercentage : waffleNumerator}
|
|
382
|
-
{suffix ? suffix + ' ' : ' '} {config.valueDescription}{' '}
|
|
383
|
-
{config.showDenominator && waffleDenominator ? waffleDenominator : ' '}
|
|
384
|
-
</div>
|
|
385
|
-
<div className='cove-waffle-chart__data--text'>{parse(content)}</div>
|
|
386
|
-
<svg height={config.gauge.height} width={'100%'}>
|
|
387
|
-
<Group>
|
|
388
|
-
<foreignObject
|
|
389
|
-
style={{ border: '1px solid black' }}
|
|
390
|
-
x={0}
|
|
391
|
-
y={0}
|
|
392
|
-
width={config.gauge.width}
|
|
393
|
-
height={config.gauge.height}
|
|
394
|
-
fill='#fff'
|
|
395
|
-
/>
|
|
396
|
-
<Bar x={0} y={0} width={xScale(waffleNumerator)} height={config.gauge.height} fill={gaugeColor} />
|
|
397
|
-
</Group>
|
|
398
|
-
</svg>
|
|
399
|
-
<div className={'cove-waffle-chart__subtext subtext'}>{parse(subtext)}</div>
|
|
400
|
-
</div>
|
|
401
|
-
</div>
|
|
402
|
-
)}
|
|
403
|
-
{config.visualizationType !== 'Gauge' && (
|
|
404
|
-
<div
|
|
405
|
-
className={`cove-waffle-chart${orientation === 'vertical' ? ' cove-waffle-chart--verical' : ''}${
|
|
406
|
-
config.overallFontSize ? ' font-' + config.overallFontSize : ''
|
|
407
|
-
}`}
|
|
408
|
-
>
|
|
409
|
-
<div className='cove-waffle-chart__chart' style={{ width: setRatio() }}>
|
|
410
|
-
<svg width={setRatio()} height={setRatio()}>
|
|
411
|
-
<Group>{buildWaffle()}</Group>
|
|
412
|
-
</svg>
|
|
413
|
-
</div>
|
|
414
|
-
{(dataPercentage || content) && (
|
|
415
|
-
<div className='cove-waffle-chart__data'>
|
|
416
|
-
{dataPercentage && (
|
|
417
|
-
<div className='cove-waffle-chart__data--primary' style={dataFontSize}>
|
|
418
|
-
{prefix ? prefix : null}
|
|
419
|
-
{dataPercentage}
|
|
420
|
-
{suffix ? suffix : null}
|
|
421
|
-
</div>
|
|
422
|
-
)}
|
|
423
|
-
<div className='cove-waffle-chart__data--text'>{parse(content)}</div>
|
|
424
|
-
|
|
425
|
-
{subtext && <div className='cove-waffle-chart__subtext subtext'>{parse(subtext)}</div>}
|
|
426
|
-
</div>
|
|
427
|
-
)}
|
|
428
|
-
</div>
|
|
429
|
-
)}
|
|
430
|
-
</div>
|
|
431
|
-
</div>
|
|
494
|
+
<div className={contentClasses.join(' ')}>{renderChartContent()}</div>
|
|
432
495
|
{link && link}
|
|
433
496
|
</div>
|
|
434
497
|
)
|
|
@@ -522,21 +585,17 @@ const CdcWaffleChart = ({
|
|
|
522
585
|
let content = <Loading />
|
|
523
586
|
|
|
524
587
|
if (loading === false) {
|
|
525
|
-
let body = (
|
|
526
|
-
<Layout.Responsive isEditor={isEditor}>
|
|
527
|
-
<WaffleChart
|
|
528
|
-
config={config}
|
|
529
|
-
isEditor={isEditor}
|
|
530
|
-
showConfigConfirm={showConfigConfirm}
|
|
531
|
-
updateConfig={updateConfig}
|
|
532
|
-
/>
|
|
533
|
-
</Layout.Responsive>
|
|
534
|
-
)
|
|
535
|
-
|
|
536
588
|
content = (
|
|
537
589
|
<>
|
|
538
|
-
{isEditor && <EditorPanel showConfigConfirm={showConfigConfirm}
|
|
539
|
-
{
|
|
590
|
+
{isEditor && <EditorPanel showConfigConfirm={showConfigConfirm} />}
|
|
591
|
+
<Layout.Responsive isEditor={isEditor}>
|
|
592
|
+
<WaffleChart
|
|
593
|
+
config={config}
|
|
594
|
+
isEditor={isEditor}
|
|
595
|
+
showConfigConfirm={showConfigConfirm}
|
|
596
|
+
updateConfig={updateConfig}
|
|
597
|
+
/>
|
|
598
|
+
</Layout.Responsive>
|
|
540
599
|
</>
|
|
541
600
|
)
|
|
542
601
|
}
|
|
@@ -579,18 +638,5 @@ export const DATA_FUNCTIONS = [
|
|
|
579
638
|
DATA_FUNCTION_SUM
|
|
580
639
|
]
|
|
581
640
|
|
|
582
|
-
export
|
|
583
|
-
export
|
|
584
|
-
export const DATA_OPERATOR_LESSEQUAL = '<='
|
|
585
|
-
export const DATA_OPERATOR_GREATEREQUAL = '>='
|
|
586
|
-
export const DATA_OPERATOR_EQUAL = '='
|
|
587
|
-
export const DATA_OPERATOR_NOTEQUAL = '≠'
|
|
588
|
-
|
|
589
|
-
export const DATA_OPERATORS = [
|
|
590
|
-
DATA_OPERATOR_LESS,
|
|
591
|
-
DATA_OPERATOR_GREATER,
|
|
592
|
-
DATA_OPERATOR_LESSEQUAL,
|
|
593
|
-
DATA_OPERATOR_GREATEREQUAL,
|
|
594
|
-
DATA_OPERATOR_EQUAL,
|
|
595
|
-
DATA_OPERATOR_NOTEQUAL
|
|
596
|
-
]
|
|
641
|
+
// Re-export DATA_OPERATORS for backward compatibility
|
|
642
|
+
export { DATA_OPERATORS }
|
|
@@ -100,8 +100,12 @@ export const GeneralSectionTests: Story = {
|
|
|
100
100
|
// TEST 3: Show Title Toggle
|
|
101
101
|
// Expectation: Header region appears / disappears (DOM visibility change).
|
|
102
102
|
// ============================================================================
|
|
103
|
-
|
|
104
|
-
const
|
|
103
|
+
// Find show title checkbox by label text
|
|
104
|
+
const showTitleCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]')).find(input => {
|
|
105
|
+
const label = input.closest('label')
|
|
106
|
+
return label?.textContent?.includes('show title')
|
|
107
|
+
}) as HTMLInputElement
|
|
108
|
+
const checkboxWrapper = showTitleCheckbox?.closest('label.checkbox')
|
|
105
109
|
expect(showTitleCheckbox).toBeTruthy()
|
|
106
110
|
expect(checkboxWrapper).toBeTruthy()
|
|
107
111
|
|
|
@@ -110,7 +114,7 @@ export const GeneralSectionTests: Story = {
|
|
|
110
114
|
'Title Toggle',
|
|
111
115
|
() => showTitleCheckbox.checked,
|
|
112
116
|
async () => {
|
|
113
|
-
await userEvent.click(
|
|
117
|
+
await userEvent.click(showTitleCheckbox)
|
|
114
118
|
},
|
|
115
119
|
(before, after) => after === !wasChecked
|
|
116
120
|
)
|
|
@@ -128,7 +132,7 @@ export const GeneralSectionTests: Story = {
|
|
|
128
132
|
'Title Toggle Reset',
|
|
129
133
|
() => showTitleCheckbox.checked,
|
|
130
134
|
async () => {
|
|
131
|
-
await userEvent.click(
|
|
135
|
+
await userEvent.click(showTitleCheckbox)
|
|
132
136
|
},
|
|
133
137
|
(before, after) => after === wasChecked
|
|
134
138
|
)
|
|
@@ -253,7 +257,15 @@ export const DataSectionTests: Story = {
|
|
|
253
257
|
'Clear Conditional Value',
|
|
254
258
|
getValueText,
|
|
255
259
|
async () => {
|
|
260
|
+
// Clear the conditional column to fully reset the filter
|
|
261
|
+
const conditionalColumnSelect = canvasElement.querySelector(
|
|
262
|
+
'select[name="dataConditionalColumn"]'
|
|
263
|
+
) as HTMLSelectElement
|
|
264
|
+
await userEvent.selectOptions(conditionalColumnSelect, '')
|
|
256
265
|
await userEvent.clear(conditionalValueInput)
|
|
266
|
+
conditionalValueInput.blur() // Trigger change event
|
|
267
|
+
// Wait for debounced input processing (TextField uses 500ms debounce)
|
|
268
|
+
await new Promise(resolve => setTimeout(resolve, 600))
|
|
257
269
|
},
|
|
258
270
|
(before, after) => after !== before
|
|
259
271
|
)
|
|
@@ -312,7 +324,7 @@ export const DataSectionTests: Story = {
|
|
|
312
324
|
'Custom Denominator Toggle',
|
|
313
325
|
getValueText,
|
|
314
326
|
async () => {
|
|
315
|
-
await userEvent.click(
|
|
327
|
+
await userEvent.click(customDenomCheckbox)
|
|
316
328
|
},
|
|
317
329
|
(before, after) => after !== before
|
|
318
330
|
)
|
|
@@ -404,38 +416,12 @@ export const DataSectionTests: Story = {
|
|
|
404
416
|
},
|
|
405
417
|
(before, after) => after !== before && after.endsWith('deaths')
|
|
406
418
|
)
|
|
407
|
-
|
|
408
|
-
// ============================================================================
|
|
409
|
-
// TEST 16: Add Filter (state = Alaska)
|
|
410
|
-
// Expectation: Primary value text changes after filter applied.
|
|
411
|
-
// ============================================================================
|
|
412
|
-
const addFilterButton = Array.from(canvasElement.querySelectorAll('button')).find(
|
|
413
|
-
b => (b as HTMLButtonElement).textContent?.trim() === 'Add Filter'
|
|
414
|
-
) as HTMLButtonElement
|
|
415
|
-
await performAndAssert(
|
|
416
|
-
'Add Filter',
|
|
417
|
-
getValueText,
|
|
418
|
-
async () => {
|
|
419
|
-
await userEvent.click(addFilterButton)
|
|
420
|
-
|
|
421
|
-
await waitForPresence('.filters-list .edit-block:last-of-type', canvasElement)
|
|
422
|
-
|
|
423
|
-
const newFilter = canvasElement.querySelector('.filters-list .edit-block:last-of-type') as HTMLElement
|
|
424
|
-
const [colSelect, valSelect] = Array.from(newFilter.querySelectorAll('select')) as HTMLSelectElement[]
|
|
425
|
-
await userEvent.selectOptions(colSelect, 'state')
|
|
426
|
-
|
|
427
|
-
await waitForOptionsToPopulate(valSelect)
|
|
428
|
-
|
|
429
|
-
await userEvent.selectOptions(valSelect, 'Alaska')
|
|
430
|
-
},
|
|
431
|
-
(before, after) => after !== before
|
|
432
|
-
)
|
|
433
419
|
}
|
|
434
420
|
}
|
|
435
421
|
|
|
436
422
|
/**
|
|
437
|
-
* VISUAL SECTION TESTS
|
|
438
|
-
* Tests
|
|
423
|
+
* CHART SETTINGS AND VISUAL SECTION TESTS
|
|
424
|
+
* Tests functionality within the Chart Settings and Visual accordions
|
|
439
425
|
*/
|
|
440
426
|
export const VisualSectionTests: Story = {
|
|
441
427
|
args: {
|
|
@@ -445,7 +431,8 @@ export const VisualSectionTests: Story = {
|
|
|
445
431
|
play: async ({ canvasElement }) => {
|
|
446
432
|
const canvas = within(canvasElement)
|
|
447
433
|
await waitForEditor(canvas)
|
|
448
|
-
|
|
434
|
+
// Open Chart Settings accordion first for tests 1-5
|
|
435
|
+
await openAccordion(canvas, 'Chart Settings')
|
|
449
436
|
// Core helper functions used throughout the visual tests
|
|
450
437
|
const waffleRoot = () => canvasElement.querySelector('.cove-waffle-chart') as HTMLElement
|
|
451
438
|
const contentContainer = () => canvasElement.querySelector('.cove-component__content > div') as HTMLElement
|
|
@@ -583,6 +570,9 @@ export const VisualSectionTests: Story = {
|
|
|
583
570
|
// TEST 6: Overall Font Size Change
|
|
584
571
|
// Expectation: font-small|font-medium|font-large class changes on waffle root.
|
|
585
572
|
// ============================================================================
|
|
573
|
+
// Open Visual accordion for tests 6+
|
|
574
|
+
await openAccordion(canvas, 'Visual')
|
|
575
|
+
|
|
586
576
|
const overallFontClass = () => {
|
|
587
577
|
const root = waffleRoot()
|
|
588
578
|
if (!root) return ''
|
|
@@ -618,7 +608,7 @@ export const VisualSectionTests: Story = {
|
|
|
618
608
|
return node?.getAttribute('fill') || ''
|
|
619
609
|
}
|
|
620
610
|
|
|
621
|
-
const themeButtons = Array.from(canvasElement.querySelectorAll('.color-palette
|
|
611
|
+
const themeButtons = Array.from(canvasElement.querySelectorAll('.color-palette button')) as HTMLElement[]
|
|
622
612
|
expect(themeButtons.length).toBeGreaterThan(1)
|
|
623
613
|
await performAndAssert(
|
|
624
614
|
'Theme Change',
|
|
@@ -638,9 +628,10 @@ export const VisualSectionTests: Story = {
|
|
|
638
628
|
// ============================================================================
|
|
639
629
|
const contentClassSig = () => Array.from(contentContainer().classList).sort().join(' ')
|
|
640
630
|
|
|
641
|
-
|
|
631
|
+
// Find border checkbox by exact label text
|
|
632
|
+
const borderCheckbox = canvas.getByLabelText('Display Border') as HTMLInputElement
|
|
642
633
|
expect(borderCheckbox).toBeTruthy()
|
|
643
|
-
const borderWrapper = borderCheckbox.closest('.
|
|
634
|
+
const borderWrapper = borderCheckbox.closest('label.checkbox') as HTMLElement
|
|
644
635
|
expect(borderWrapper).toBeTruthy()
|
|
645
636
|
const borderStyleSig = () => {
|
|
646
637
|
const el = contentContainer()
|
|
@@ -656,7 +647,7 @@ export const VisualSectionTests: Story = {
|
|
|
656
647
|
'Border Toggle',
|
|
657
648
|
borderStyleSig,
|
|
658
649
|
async () => {
|
|
659
|
-
await userEvent.click(
|
|
650
|
+
await userEvent.click(borderCheckbox)
|
|
660
651
|
},
|
|
661
652
|
(before, after) =>
|
|
662
653
|
before.classes !== after.classes ||
|
|
@@ -669,17 +660,16 @@ export const VisualSectionTests: Story = {
|
|
|
669
660
|
// TEST 9: Theme Border Color Toggle
|
|
670
661
|
// Expectation: Class 'component--has-borderColorTheme' toggles.
|
|
671
662
|
// ============================================================================
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
) as HTMLInputElement
|
|
663
|
+
// Find border color theme checkbox by exact label text
|
|
664
|
+
const borderColorThemeCheckbox = canvas.getByLabelText('Use Border Color Theme') as HTMLInputElement
|
|
675
665
|
expect(borderColorThemeCheckbox).toBeTruthy()
|
|
676
|
-
const borderColorThemeWrapper = borderColorThemeCheckbox.closest('.
|
|
666
|
+
const borderColorThemeWrapper = borderColorThemeCheckbox.closest('label.checkbox') as HTMLElement
|
|
677
667
|
expect(borderColorThemeWrapper).toBeTruthy()
|
|
678
668
|
await performAndAssert(
|
|
679
669
|
'Border Color Theme Toggle',
|
|
680
670
|
contentClassSig,
|
|
681
671
|
async () => {
|
|
682
|
-
await userEvent.click(
|
|
672
|
+
await userEvent.click(borderColorThemeCheckbox)
|
|
683
673
|
},
|
|
684
674
|
(before, after) => before !== after && (after.includes('borderColorTheme') || before.includes('borderColorTheme'))
|
|
685
675
|
)
|
|
@@ -688,15 +678,16 @@ export const VisualSectionTests: Story = {
|
|
|
688
678
|
// TEST 10: Accent Style Toggle
|
|
689
679
|
// Expectation: Class 'component--has-accent' toggles.
|
|
690
680
|
// ============================================================================
|
|
691
|
-
|
|
681
|
+
// Find accent checkbox by exact label text
|
|
682
|
+
const accentCheckbox = canvas.getByLabelText('Use Accent Style') as HTMLInputElement
|
|
692
683
|
expect(accentCheckbox).toBeTruthy()
|
|
693
|
-
const accentWrapper = accentCheckbox.closest('.
|
|
684
|
+
const accentWrapper = accentCheckbox.closest('label.checkbox') as HTMLElement
|
|
694
685
|
expect(accentWrapper).toBeTruthy()
|
|
695
686
|
await performAndAssert(
|
|
696
687
|
'Accent Toggle',
|
|
697
688
|
contentClassSig,
|
|
698
689
|
async () => {
|
|
699
|
-
await userEvent.click(
|
|
690
|
+
await userEvent.click(accentCheckbox)
|
|
700
691
|
},
|
|
701
692
|
(before, after) => before !== after
|
|
702
693
|
)
|
|
@@ -705,15 +696,16 @@ export const VisualSectionTests: Story = {
|
|
|
705
696
|
// TEST 11: Theme Background Color Toggle
|
|
706
697
|
// Expectation: Class 'component--has-background' toggles.
|
|
707
698
|
// ============================================================================
|
|
708
|
-
|
|
699
|
+
// Find background checkbox by exact label text
|
|
700
|
+
const backgroundCheckbox = canvas.getByLabelText('Use Theme Background Color') as HTMLInputElement
|
|
709
701
|
expect(backgroundCheckbox).toBeTruthy()
|
|
710
|
-
const backgroundWrapper = backgroundCheckbox.closest('.
|
|
702
|
+
const backgroundWrapper = backgroundCheckbox.closest('label.checkbox') as HTMLElement
|
|
711
703
|
expect(backgroundWrapper).toBeTruthy()
|
|
712
704
|
await performAndAssert(
|
|
713
705
|
'Background Toggle',
|
|
714
706
|
contentClassSig,
|
|
715
707
|
async () => {
|
|
716
|
-
await userEvent.click(
|
|
708
|
+
await userEvent.click(backgroundCheckbox)
|
|
717
709
|
},
|
|
718
710
|
(before, after) => before !== after
|
|
719
711
|
)
|
|
@@ -722,17 +714,16 @@ export const VisualSectionTests: Story = {
|
|
|
722
714
|
// TEST 12: Hide Background Color Toggle
|
|
723
715
|
// Expectation: Class 'component--hideBackgroundColor' toggles.
|
|
724
716
|
// ============================================================================
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
) as HTMLInputElement
|
|
717
|
+
// Find hide background checkbox by exact label text
|
|
718
|
+
const hideBackgroundCheckbox = canvas.getByLabelText('Hide Background Color') as HTMLInputElement
|
|
728
719
|
expect(hideBackgroundCheckbox).toBeTruthy()
|
|
729
|
-
const hideBackgroundWrapper = hideBackgroundCheckbox.closest('.
|
|
720
|
+
const hideBackgroundWrapper = hideBackgroundCheckbox.closest('label.checkbox') as HTMLElement
|
|
730
721
|
expect(hideBackgroundWrapper).toBeTruthy()
|
|
731
722
|
await performAndAssert(
|
|
732
723
|
'Hide Background Toggle',
|
|
733
724
|
contentClassSig,
|
|
734
725
|
async () => {
|
|
735
|
-
await userEvent.click(
|
|
726
|
+
await userEvent.click(hideBackgroundCheckbox)
|
|
736
727
|
},
|
|
737
728
|
(before, after) => before !== after
|
|
738
729
|
)
|