@cdc/data-bite 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.
@@ -1,719 +0,0 @@
1
- import React, { memo, useContext, useEffect, useState } from 'react'
2
- import cloneConfig from '@cdc/core/helpers/cloneConfig'
3
- import _ from 'lodash'
4
- import {
5
- Accordion,
6
- AccordionItem,
7
- AccordionItemButton,
8
- AccordionItemHeading,
9
- AccordionItemPanel
10
- } from 'react-accessible-accordion'
11
-
12
- import Context from '../context'
13
- import WarningImage from '@cdc/core/assets/icon-warning-circle.svg'
14
- import Tooltip from '@cdc/core/components/ui/Tooltip'
15
- import Icon from '@cdc/core/components/ui/Icon'
16
- import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
17
- import { updateFieldFactory } from '@cdc/core/helpers/updateFieldFactory'
18
- import { BITE_LOCATIONS, DATA_FUNCTIONS, IMAGE_POSITIONS, DATA_OPERATORS, HEADER_COLORS } from './../constants'
19
- import Layout from '@cdc/core/components/Layout'
20
- import { Select, TextField, CheckBox } from '@cdc/core/components/EditorPanel/Inputs'
21
- import Button from '@cdc/core/components/elements/Button'
22
- import PanelMarkup from '@cdc/core/components/EditorPanel/components/PanelMarkup'
23
-
24
- const EditorPanel = memo(() => {
25
- const { config, updateConfig, loading, data, setParentConfig, isDashboard, isEditor } = useContext(Context)
26
-
27
- const [displayPanel, setDisplayPanel] = useState(true)
28
-
29
- const updateField = updateFieldFactory(config, updateConfig, true)
30
-
31
- const missingRequiredSections = () => {
32
- //Whether to show error message if something is required to show a data-bite and isn't filled in
33
- return false
34
- }
35
-
36
- useEffect(() => {
37
- // Pass up to Editor if needed
38
- if (setParentConfig) {
39
- const newConfig = convertStateToConfig()
40
- delete newConfig.newViz
41
-
42
- setParentConfig(newConfig)
43
- }
44
- // eslint-disable-next-line react-hooks/exhaustive-deps
45
- }, [config])
46
-
47
- const onBackClick = () => {
48
- setDisplayPanel(!displayPanel)
49
- updateConfig({
50
- ...config,
51
- showEditorPanel: !displayPanel
52
- })
53
- }
54
-
55
- const convertStateToConfig = () => {
56
- let strippedState = cloneConfig(config)
57
- //if(false === missingRequiredSections()) {
58
- //strippedState.newViz
59
- //}
60
- delete strippedState.runtime
61
-
62
- return strippedState
63
- }
64
-
65
- // Filters -----------------------------------------------
66
- const removeFilter = index => {
67
- let filters = [...config.filters]
68
-
69
- filters.splice(index, 1)
70
-
71
- updateConfig({ ...config, filters })
72
- }
73
-
74
- const updateFilterProp = (name, index, value) => {
75
- let filters = [...config.filters]
76
-
77
- filters[index][name] = value
78
-
79
- updateConfig({ ...config, filters })
80
- }
81
-
82
- const addNewFilter = () => {
83
- let filters = config.filters ? [...config.filters] : []
84
-
85
- filters.push({ values: [] })
86
-
87
- updateConfig({ ...config, filters })
88
- }
89
-
90
- const getColumns = (filter = true) => {
91
- let columns = {}
92
- if (data.length) {
93
- data.map(row => {
94
- return Object.keys(row).forEach(columnName => (columns[columnName] = true))
95
- })
96
- }
97
-
98
- return Object.keys(columns)
99
- }
100
-
101
- const getFilterColumnValues = index => {
102
- let filterDataOptions = []
103
- const filterColumnName = config.filters[index].columnName
104
- if (data && filterColumnName) {
105
- data.forEach(function (row) {
106
- if (undefined !== row[filterColumnName] && -1 === filterDataOptions.indexOf(row[filterColumnName])) {
107
- filterDataOptions.push(row[filterColumnName])
108
- }
109
- })
110
- filterDataOptions.sort()
111
- }
112
- return filterDataOptions
113
- }
114
-
115
- // Dynamic Images ----------------------------------------
116
- const updateDynamicImage = (name, index, subindex = null, value) => {
117
- let imageOptions = [...config.imageData.options]
118
- null === subindex ? (imageOptions[index][name] = value) : (imageOptions[index].arguments[subindex][name] = value)
119
-
120
- let payload = { ...config.imageData, options: imageOptions }
121
- updateConfig({ ...config, imageData: payload })
122
- }
123
-
124
- const setDynamicArgument = (optionIndex, name, value) => {
125
- let imageArguments = [...config.imageData.options[optionIndex].arguments]
126
- imageArguments[1] = { ...imageArguments[1], [name]: value }
127
- let argumentsPayload = { ...config.imageData.options[optionIndex], arguments: imageArguments }
128
- let optionsPayload = [...config.imageData.options]
129
- optionsPayload[optionIndex] = argumentsPayload
130
- let payload = { ...config.imageData, options: optionsPayload }
131
- updateConfig({ ...config, imageData: payload })
132
- }
133
-
134
- const removeDynamicArgument = optionIndex => {
135
- if (config.imageData.options[optionIndex].arguments.length > 1) {
136
- let imageArguments = [...config.imageData.options[optionIndex].arguments]
137
- imageArguments.pop()
138
- let argumentsPayload = { ...config.imageData.options[optionIndex], arguments: imageArguments }
139
- let optionsPayload = [...config.imageData.options]
140
- optionsPayload[optionIndex] = argumentsPayload
141
- let payload = { ...config.imageData, options: optionsPayload }
142
- updateConfig({ ...config, imageData: payload })
143
- }
144
- }
145
-
146
- const addDynamicImage = () => {
147
- let imageOptions = config.imageData.options ? [...config.imageData.options] : []
148
- imageOptions.push({ source: '', arguments: [{ operator: '', threshold: '' }], alt: '', secondArgument: false })
149
-
150
- let payload = { ...config.imageData, options: imageOptions }
151
- updateConfig({ ...config, imageData: payload })
152
- }
153
-
154
- const removeDynamicImage = index => {
155
- let imageOptions = [...config.imageData.options]
156
- imageOptions.splice(index, 1)
157
-
158
- let payload = { ...config.imageData, options: imageOptions }
159
- updateConfig({ ...config, imageData: payload })
160
- }
161
-
162
- // General -----------------------------------------------
163
- if (loading) {
164
- return null
165
- }
166
-
167
- return (
168
- <ErrorBoundary component='EditorPanel'>
169
- <Layout.Sidebar
170
- isEditor={true}
171
- config={config}
172
- title='Configure Data Bites'
173
- onBackClick={onBackClick}
174
- displayPanel={displayPanel}
175
- >
176
- <section className='form-container'>
177
- <form>
178
- <Accordion allowZeroExpanded={true}>
179
- <AccordionItem>
180
- {' '}
181
- {/* General */}
182
- <AccordionItemHeading>
183
- <AccordionItemButton>General</AccordionItemButton>
184
- </AccordionItemHeading>
185
- <AccordionItemPanel>
186
- <Select
187
- value={config.biteStyle}
188
- fieldName='biteStyle'
189
- label='Data Bite Style'
190
- updateField={updateField}
191
- options={Object.entries(BITE_LOCATIONS).map(([key, value]) => ({ value: key, label: value }))}
192
- initial='Select'
193
- />
194
- <TextField
195
- value={config.title}
196
- fieldName='title'
197
- label='Title'
198
- placeholder='Data Bite Title'
199
- updateField={updateField}
200
- />
201
- <CheckBox
202
- value={config.visual?.showTitle}
203
- section='visual'
204
- checked={config.visual?.showTitle}
205
- fieldName='showTitle'
206
- label='Show Title'
207
- updateField={updateField}
208
- />
209
-
210
- <TextField
211
- type='textarea'
212
- value={config.biteBody}
213
- fieldName='biteBody'
214
- label='Message'
215
- updateField={updateField}
216
- tooltip={
217
- <Tooltip style={{ textTransform: 'none' }}>
218
- <Tooltip.Target>
219
- <Icon display='question' style={{ marginLeft: '0.5rem' }} />
220
- </Tooltip.Target>
221
- <Tooltip.Content>
222
- <p>
223
- Enter the message text for the visualization. The following HTML tags are supported: strong,
224
- em, sup, and sub.
225
- </p>
226
- </Tooltip.Content>
227
- </Tooltip>
228
- }
229
- />
230
- <TextField
231
- value={config.subtext}
232
- fieldName='subtext'
233
- label='Subtext/Citation'
234
- placeholder='Data Bite Subtext or Citation'
235
- updateField={updateField}
236
- tooltip={
237
- <Tooltip style={{ textTransform: 'none' }}>
238
- <Tooltip.Target>
239
- <Icon display='question' style={{ marginLeft: '0.5rem' }} />
240
- </Tooltip.Target>
241
- <Tooltip.Content>
242
- <p>
243
- Enter supporting text to display below the data visualization, if applicable. The following
244
- HTML tags are supported: strong, em, sup, and sub. You can also use markup variables like{' '}
245
- {'{{variable-name}}'} to display dynamic data.
246
- </p>
247
- </Tooltip.Content>
248
- </Tooltip>
249
- }
250
- />
251
- </AccordionItemPanel>
252
- </AccordionItem>
253
-
254
- <AccordionItem>
255
- {' '}
256
- {/*Data*/}
257
- <AccordionItemHeading>
258
- <AccordionItemButton>
259
- Data{' '}
260
- {(!config.dataColumn || !config.dataFunction) && (
261
- <WarningImage width='25' className='warning-icon' />
262
- )}
263
- </AccordionItemButton>
264
- </AccordionItemHeading>
265
- <AccordionItemPanel>
266
- <ul className='column-edit'>
267
- <li className='two-col'>
268
- <Select
269
- value={config.dataColumn || ''}
270
- fieldName='dataColumn'
271
- label='Data Column'
272
- updateField={updateField}
273
- initial='Select'
274
- required={true}
275
- options={getColumns() ? Array.from(getColumns()) : []}
276
- />
277
- <Select
278
- value={config.dataFunction || ''}
279
- fieldName='dataFunction'
280
- label='Data Function'
281
- updateField={updateField}
282
- initial='Select'
283
- required={true}
284
- options={
285
- Array.isArray(DATA_FUNCTIONS)
286
- ? DATA_FUNCTIONS
287
- : DATA_FUNCTIONS
288
- ? Object.values(DATA_FUNCTIONS)
289
- : []
290
- }
291
- />
292
- </li>
293
- </ul>
294
- <span className='divider-heading'>Number Formatting</span>
295
- <ul className='column-edit'>
296
- <li className='three-col'>
297
- <TextField
298
- value={config.dataFormat.prefix}
299
- section='dataFormat'
300
- fieldName='prefix'
301
- label='Prefix'
302
- updateField={updateField}
303
- />
304
- <TextField
305
- value={config.dataFormat.suffix}
306
- section='dataFormat'
307
- fieldName='suffix'
308
- label='Suffix'
309
- updateField={updateField}
310
- />
311
- <TextField
312
- type='number'
313
- value={config.dataFormat.roundToPlace}
314
- section='dataFormat'
315
- fieldName='roundToPlace'
316
- label='Round'
317
- updateField={updateField}
318
- min='0'
319
- max='99'
320
- />
321
- </li>
322
- </ul>
323
- <CheckBox
324
- value={config.dataFormat.commas}
325
- section='dataFormat'
326
- fieldName='commas'
327
- label='Add commas'
328
- updateField={updateField}
329
- />
330
- <CheckBox
331
- value={config.dataFormat.ignoreZeros}
332
- section='dataFormat'
333
- fieldName='ignoreZeros'
334
- label='Ignore Zeros'
335
- updateField={updateField}
336
- />
337
- <hr className='accordion__divider' />
338
-
339
- <label style={{ marginBottom: '1rem' }}>
340
- <span className='edit-label'>
341
- Data Point Filters
342
- <Tooltip style={{ textTransform: 'none' }}>
343
- <Tooltip.Target>
344
- <Icon display='question' style={{ marginLeft: '0.5rem' }} />
345
- </Tooltip.Target>
346
- <Tooltip.Content>
347
- <p>
348
- To refine the highlighted data point, specify one or more filters (e.g., "Male" and "Female"
349
- for a column called "Sex").
350
- </p>
351
- </Tooltip.Content>
352
- </Tooltip>
353
- </span>
354
- </label>
355
- {config.filters && (
356
- <ul className='filters-list'>
357
- {config.filters.map((filter, index) => (
358
- <fieldset className='edit-block' key={index}>
359
- <button
360
- type='button'
361
- className='btn btn-danger'
362
- onClick={() => {
363
- removeFilter(index)
364
- }}
365
- >
366
- Remove
367
- </button>
368
- <Select
369
- value={filter.columnName ? filter.columnName : ''}
370
- fieldName='columnName'
371
- label={'Column Name'}
372
- updateField={(section, subsection, fieldName, value) =>
373
- updateFilterProp(fieldName, index, value)
374
- }
375
- options={getColumns()}
376
- initial='- Select Option -'
377
- />
378
- <Select
379
- value={filter.columnValue || ''}
380
- fieldName='columnValue'
381
- label='Column Value'
382
- updateField={(section, subsection, fieldName, value) =>
383
- updateFilterProp(fieldName, index, value)
384
- }
385
- options={Array.isArray(getFilterColumnValues(index)) ? getFilterColumnValues(index) : []}
386
- initial='- Select Option -'
387
- />
388
- </fieldset>
389
- ))}
390
- </ul>
391
- )}
392
- {(!config.filters || config.filters.length === 0) && (
393
- <div>
394
- <fieldset className='edit-block'>
395
- <p style={{ textAlign: 'center' }}>There are currently no filters.</p>
396
- </fieldset>
397
- </div>
398
- )}
399
- <Button type='button' onClick={addNewFilter} className='btn btn-primary full-width mt-3'>
400
- Add Filter
401
- </Button>
402
- </AccordionItemPanel>
403
- </AccordionItem>
404
-
405
- <AccordionItem>
406
- {' '}
407
- {/*Visual*/}
408
- <AccordionItemHeading>
409
- <AccordionItemButton>Visual</AccordionItemButton>
410
- </AccordionItemHeading>
411
- <AccordionItemPanel>
412
- <TextField
413
- type='number'
414
- value={config.biteFontSize}
415
- fieldName='biteFontSize'
416
- label='Bite Font Size'
417
- updateField={updateField}
418
- min='17'
419
- max='65'
420
- />
421
- <Select
422
- value={config.fontSize}
423
- fieldName='fontSize'
424
- label='Overall Font Size'
425
- updateField={updateField}
426
- options={['small', 'medium', 'large']}
427
- />
428
- <div className='checkbox-group'>
429
- <CheckBox
430
- value={config.visual?.border}
431
- section='visual'
432
- fieldName='border'
433
- label='Display Border'
434
- updateField={updateField}
435
- />
436
- <CheckBox
437
- value={config.visual?.borderColorTheme}
438
- section='visual'
439
- fieldName='borderColorTheme'
440
- label='Use Border Color Theme'
441
- updateField={updateField}
442
- />
443
- <CheckBox
444
- value={config.visual?.accent}
445
- section='visual'
446
- fieldName='accent'
447
- label='Use Accent Style'
448
- updateField={updateField}
449
- />
450
- <CheckBox
451
- value={config.visual?.background}
452
- section='visual'
453
- fieldName='background'
454
- label='Use Theme Background Color'
455
- updateField={updateField}
456
- />
457
- <CheckBox
458
- value={config.visual?.hideBackgroundColor}
459
- section='visual'
460
- fieldName='hideBackgroundColor'
461
- label='Hide Background Color'
462
- updateField={updateField}
463
- />
464
- </div>
465
- <label>
466
- <span className='edit-label'>Theme</span>
467
- <ul className='color-palette'>
468
- {HEADER_COLORS.map(palette => (
469
- <button
470
- title={palette}
471
- key={palette}
472
- onClick={e => {
473
- e.preventDefault()
474
- updateConfig({ ...config, theme: palette })
475
- }}
476
- className={config.theme === palette ? 'selected ' + palette : palette}
477
- />
478
- ))}
479
- </ul>
480
- </label>
481
- </AccordionItemPanel>
482
- </AccordionItem>
483
-
484
- {['title', 'body', 'graphic'].includes(config.biteStyle) && (
485
- <AccordionItem>
486
- {' '}
487
- {/*Image & Dynamic Images*/}
488
- <AccordionItemHeading>
489
- <AccordionItemButton>
490
- Image
491
- {['dynamic'].includes(config.imageData.display) && 's'}
492
- </AccordionItemButton>
493
- </AccordionItemHeading>
494
- <AccordionItemPanel>
495
- <Select
496
- value={config.imageData.display || ''}
497
- section='imageData'
498
- fieldName='display'
499
- label='Image Display Type'
500
- updateField={updateField}
501
- options={['none', 'static', 'dynamic']}
502
- />
503
- <Select
504
- value={config.bitePosition || ''}
505
- fieldName='bitePosition'
506
- label='Image/Graphic Position'
507
- updateField={updateField}
508
- initial='Select'
509
- options={IMAGE_POSITIONS}
510
- />
511
- {['static'].includes(config.imageData.display) && (
512
- <>
513
- <TextField
514
- value={config.imageData.url}
515
- section='imageData'
516
- fieldName='url'
517
- label='Image URL'
518
- updateField={updateField}
519
- />
520
- <TextField
521
- value={config.imageData.alt}
522
- section='imageData'
523
- fieldName='alt'
524
- label='Alt Text'
525
- updateField={updateField}
526
- />
527
- </>
528
- )}
529
-
530
- {['dynamic'].includes(config.imageData.display) && (
531
- <>
532
- <TextField
533
- value={config.imageData.url || ''}
534
- section='imageData'
535
- fieldName='url'
536
- label='Image URL (default)'
537
- updateField={updateField}
538
- />
539
- <TextField
540
- value={config.imageData.alt}
541
- section='imageData'
542
- fieldName='alt'
543
- label='Alt Text (default)'
544
- updateField={updateField}
545
- />
546
-
547
- <hr className='accordion__divider' />
548
-
549
- {(!config.imageData.options || config.imageData.options.length === 0) && (
550
- <p style={{ textAlign: 'center' }}>There are currently no dynamic images.</p>
551
- )}
552
- {config.imageData.options && config.imageData.options.length > 0 && (
553
- <>
554
- <ul>
555
- {config.imageData.options.map((option, index) => (
556
- <fieldset className='edit-block' key={index}>
557
- <button
558
- type='button'
559
- className='remove-column'
560
- onClick={() => {
561
- removeDynamicImage(index)
562
- }}
563
- >
564
- Remove
565
- </button>
566
- <label>
567
- <span className='edit-label column-heading'>
568
- <strong>{'Image #' + (index + 1)}</strong>
569
- </span>
570
-
571
- <div className='accordion__panel-row align-center'>
572
- <div className='accordion__panel-col flex-auto'>If Value</div>
573
- <div className='accordion__panel-col flex-auto'>
574
- <select
575
- value={option.arguments[0]?.operator || ''}
576
- onChange={e => {
577
- updateDynamicImage('operator', index, 0, e.target.value)
578
- }}
579
- >
580
- <option value='' disabled />
581
- {DATA_OPERATORS.map((operator, index) => (
582
- <option value={operator} key={index}>
583
- {operator}
584
- </option>
585
- ))}
586
- </select>
587
- </div>
588
- <div className='accordion__panel-col flex-grow flex-shrink'>
589
- <input
590
- type='number'
591
- value={option.arguments[0]?.threshold || ''}
592
- onChange={e => {
593
- updateDynamicImage('threshold', index, 0, e.target.value)
594
- }}
595
- />
596
- </div>
597
- </div>
598
-
599
- <div className='accordion__panel-row mb-2 align-center'>
600
- <div className='accordion__panel-col flex-grow'>
601
- <select
602
- className='border-dashed text-center'
603
- value={option.secondArgument ? 'and' : 'then'}
604
- onChange={e => {
605
- if ('then' === e.target.value) {
606
- updateDynamicImage('secondArgument', index, null, false)
607
- removeDynamicArgument(index)
608
- }
609
- if ('and' === e.target.value) {
610
- updateDynamicImage('secondArgument', index, null, true)
611
- }
612
- }}
613
- >
614
- <option value={'then'}>Then</option>
615
- <option value={'and'}>And</option>
616
- </select>
617
- </div>
618
- </div>
619
-
620
- {option.secondArgument && true === option.secondArgument && (
621
- <>
622
- <div className='accordion__panel-row align-center'>
623
- <div className='accordion__panel-col flex-auto'>If Value</div>
624
- <div className='accordion__panel-col flex-auto'>
625
- <select
626
- value={option.arguments[1]?.operator || ''}
627
- onChange={e => {
628
- setDynamicArgument(index, 'operator', e.target.value)
629
- }}
630
- >
631
- <option value='' disabled />
632
- {DATA_OPERATORS.map((operator, index) => (
633
- <option value={operator} key={index}>
634
- {operator}
635
- </option>
636
- ))}
637
- </select>
638
- </div>
639
- <div className='accordion__panel-col flex-grow flex-shrink'>
640
- <input
641
- type='number'
642
- value={option.arguments[1]?.threshold || ''}
643
- onChange={e => {
644
- setDynamicArgument(index, 'threshold', e.target.value)
645
- }}
646
- />
647
- </div>
648
- </div>
649
- <div className='accordion__panel-row mb-2 align-center text-center text-capitalize'>
650
- <div className='accordion__panel-col flex-grow'>Then</div>
651
- </div>
652
- </>
653
- )}
654
-
655
- <div className='accordion__panel-row mb-2 align-center'>
656
- <div className='accordion__panel-col flex-auto'>Show</div>
657
- <div className='accordion__panel-col flex-grow'>
658
- <input
659
- type='text'
660
- value={option.source || ''}
661
- onChange={e => {
662
- updateDynamicImage('source', index, null, e.target.value)
663
- }}
664
- />
665
- </div>
666
- </div>
667
-
668
- <div className='accordion__panel-row mb-2 align-center'>
669
- <div className='accordion__panel-col flex-auto'>Alt Text</div>
670
- <div className='accordion__panel-col flex-grow'>
671
- <input
672
- type='text'
673
- value={option.alt || ''}
674
- onChange={e => {
675
- updateDynamicImage('alt', index, null, e.target.value)
676
- }}
677
- />
678
- </div>
679
- </div>
680
- </label>
681
- </fieldset>
682
- ))}
683
- </ul>
684
- </>
685
- )}
686
- <button type='button' onClick={addDynamicImage} className='btn btn-primary full-width'>
687
- Add Dynamic Image
688
- </button>
689
- </>
690
- )}
691
- </AccordionItemPanel>
692
- </AccordionItem>
693
- )}
694
-
695
- <AccordionItem>
696
- <AccordionItemHeading>
697
- <AccordionItemButton>Markup Variables</AccordionItemButton>
698
- </AccordionItemHeading>
699
- <AccordionItemPanel>
700
- <PanelMarkup
701
- name='Markup Variables'
702
- markupVariables={config.markupVariables || []}
703
- data={data}
704
- enableMarkupVariables={config.enableMarkupVariables || false}
705
- onMarkupVariablesChange={variables => updateField(null, null, 'markupVariables', variables)}
706
- onToggleEnable={enabled => updateField(null, null, 'enableMarkupVariables', enabled)}
707
- withAccordion={false}
708
- />
709
- </AccordionItemPanel>
710
- </AccordionItem>
711
- </Accordion>
712
- </form>
713
- </section>
714
- </Layout.Sidebar>
715
- </ErrorBoundary>
716
- )
717
- })
718
-
719
- export default EditorPanel