@cdc/map 2.6.2 → 2.6.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.
Files changed (37) hide show
  1. package/dist/cdcmap.js +27 -27
  2. package/examples/default-county.json +105 -0
  3. package/examples/default-single-state.json +109 -0
  4. package/examples/default-usa.json +744 -603
  5. package/examples/example-city-state.json +474 -0
  6. package/examples/example-world-map.json +1596 -0
  7. package/examples/gender-rate-map.json +1 -0
  8. package/package.json +50 -47
  9. package/src/CdcMap.js +422 -159
  10. package/src/components/CityList.js +3 -2
  11. package/src/components/CountyMap.js +556 -0
  12. package/src/components/DataTable.js +73 -19
  13. package/src/components/EditorPanel.js +2088 -1230
  14. package/src/components/Sidebar.js +5 -5
  15. package/src/components/SingleStateMap.js +326 -0
  16. package/src/components/UsaMap.js +20 -3
  17. package/src/data/abbreviations.js +57 -0
  18. package/src/data/color-palettes.js +10 -1
  19. package/src/data/county-map-halfquality.json +58453 -0
  20. package/src/data/county-map-quarterquality.json +1 -0
  21. package/src/data/county-topo.json +1 -0
  22. package/src/data/dfc-map.json +1 -0
  23. package/src/data/initial-state.js +2 -2
  24. package/src/data/newtest.json +1 -0
  25. package/src/data/state-abbreviations.js +60 -0
  26. package/src/data/supported-geos.js +3504 -151
  27. package/src/data/test.json +1 -0
  28. package/src/hooks/useActiveElement.js +19 -0
  29. package/src/index.html +27 -20
  30. package/src/index.js +8 -4
  31. package/src/scss/datatable.scss +2 -1
  32. package/src/scss/main.scss +10 -1
  33. package/src/scss/map.scss +153 -123
  34. package/src/scss/sidebar.scss +0 -1
  35. package/uploads/upload-example-city-state.json +392 -0
  36. package/uploads/upload-example-world-data.json +1490 -0
  37. package/LICENSE +0 -201
@@ -1,1253 +1,2111 @@
1
- import React, { useState, useEffect, useCallback } from 'react'
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
2
  import {
3
- Accordion,
4
- AccordionItem,
5
- AccordionItemHeading,
6
- AccordionItemPanel,
7
- AccordionItemButton,
3
+ Accordion,
4
+ AccordionItem,
5
+ AccordionItemHeading,
6
+ AccordionItemPanel,
7
+ AccordionItemButton,
8
8
  } from 'react-accessible-accordion';
9
- import ReactTooltip from 'react-tooltip'
9
+ import ReactTooltip from 'react-tooltip';
10
10
  import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
11
11
  import { useDebounce } from 'use-debounce';
12
12
 
13
- import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
14
- import Waiting from '@cdc/core/components/Waiting'
13
+ import ErrorBoundary from '@cdc/core/components/ErrorBoundary';
14
+ import Waiting from '@cdc/core/components/Waiting';
15
15
 
16
16
  import MapIcon from '../images/map-folded.svg';
17
17
  import UsaGraphic from '@cdc/core/assets/usa-graphic.svg';
18
18
  import WorldGraphic from '@cdc/core/assets/world-graphic.svg';
19
+ import AlabamaGraphic from '@cdc/core/assets/alabama-graphic.svg';
19
20
  import colorPalettes from '../data/color-palettes';
20
21
  import worldDefaultConfig from '../../examples/default-world.json';
21
22
  import usaDefaultConfig from '../../examples/default-usa.json';
23
+ import countyDefaultConfig from '../../examples/default-county.json';
22
24
  import QuestionIcon from '@cdc/core/assets/question-circle.svg';
23
25
 
24
- const ReactTags = require('react-tag-autocomplete'); // Future: Lazy
25
-
26
- const Helper = ({text}) => {
27
- return (
28
- <span className='tooltip helper' data-tip={text}>
29
- <QuestionIcon />
30
- </span>
31
- )
32
- }
33
-
34
- const TextField = ({label, section = null, subsection = null, fieldName, updateField, value: stateValue, type = "input", helper = null, ...attributes}) => {
35
- const [ value, setValue ] = useState(stateValue);
36
-
37
- const [ debouncedValue ] = useDebounce(value, 500);
38
-
39
- useEffect(() => {
40
- if('string' === typeof debouncedValue && stateValue !== debouncedValue ) {
41
- updateField(section, subsection, fieldName, debouncedValue)
42
- }
43
- }, [debouncedValue])
26
+ import { supportedStatesFipsCodes } from '../data/supported-geos';
44
27
 
45
- let name = subsection ? `${section}-${subsection}-${fieldName}` : `${section}-${subsection}-${fieldName}`;
46
-
47
- const onChange = (e) => setValue(e.target.value);
48
-
49
- let formElement = <input type="text" name={name} onChange={onChange} {...attributes} value={value} />
50
-
51
- if('textarea' === type) {
52
- formElement = (
53
- <textarea name={name} onChange={onChange} {...attributes} value={value}></textarea>
54
- )
55
- }
56
-
57
- if('number' === type) {
58
- formElement = <input type="number" name={name} onChange={onChange} {...attributes} value={value} />
59
- }
28
+ const ReactTags = require('react-tag-autocomplete'); // Future: Lazy
60
29
 
61
- return (
62
- <label>
63
- <span className="edit-label column-heading">{label} {helper && <Helper text={helper} />}</span>
64
- {formElement}
65
- </label>
66
- )
67
- }
30
+ const Helper = ({ text }) => {
31
+ return (
32
+ <span className='tooltip helper' data-tip={text}>
33
+ <QuestionIcon />
34
+ </span>
35
+ );
36
+ };
37
+
38
+ const TextField = ({
39
+ label,
40
+ section = null,
41
+ subsection = null,
42
+ fieldName,
43
+ updateField,
44
+ value: stateValue,
45
+ type = 'input',
46
+ helper = null,
47
+ ...attributes
48
+ }) => {
49
+ const [value, setValue] = useState(stateValue);
50
+
51
+ const [debouncedValue] = useDebounce(value, 500);
52
+
53
+ useEffect(() => {
54
+ if ('string' === typeof debouncedValue && stateValue !== debouncedValue) {
55
+ updateField(section, subsection, fieldName, debouncedValue);
56
+ }
57
+ }, [debouncedValue]);
58
+
59
+ let name = subsection ? `${section}-${subsection}-${fieldName}` : `${section}-${subsection}-${fieldName}`;
60
+
61
+ const onChange = (e) => setValue(e.target.value);
62
+
63
+ let formElement = <input type='text' name={name} onChange={onChange} {...attributes} value={value} />;
64
+
65
+ if ('textarea' === type) {
66
+ formElement = <textarea name={name} onChange={onChange} {...attributes} value={value}></textarea>;
67
+ }
68
+
69
+ if ('number' === type) {
70
+ formElement = <input type='number' name={name} onChange={onChange} {...attributes} value={value} />;
71
+ }
72
+
73
+ return (
74
+ <label>
75
+ <span className='edit-label column-heading'>
76
+ {label} {helper && <Helper text={helper} />}
77
+ </span>
78
+ {formElement}
79
+ </label>
80
+ );
81
+ };
68
82
 
69
83
  const EditorPanel = (props) => {
70
- const {
71
- state,
72
- columnsInData = [],
73
- loadConfig,
74
- setState,
75
- isDashboard,
76
- setParentConfig,
77
- runtimeFilters,
78
- runtimeLegend,
79
- } = props
80
-
81
- const { general, columns, legend, dataTable, tooltips} = state
82
-
83
- const [ requiredColumns, setRequiredColumns ] = useState(null) // Simple state so we know if we need more information before parsing the map
84
-
85
- const [ configTextboxValue, setConfigTextbox ] = useState({})
86
-
87
- const [ loadedDefault, setLoadedDefault ] = useState(false)
88
-
89
- const [ displayPanel, setDisplayPanel ] = useState(true)
90
-
91
- const [ advancedToggle, setAdvancedToggle ] = useState(false)
92
-
93
- const [ activeFilterValueForDescription, setActiveFilterValueForDescription ] = useState([0,0])
94
-
95
- const [ editorCatOrder, setEditorCatOrder ] = useState(state.legend.categoryValuesOrder || [])
96
-
97
- const headerColors = ['theme-blue','theme-purple','theme-brown','theme-teal','theme-pink','theme-orange','theme-slate','theme-indigo','theme-cyan','theme-green','theme-amber']
98
-
99
- const categoryMove = (idx1, idx2) => {
100
- let categoryValuesOrder = [...editorCatOrder]
101
-
102
- let [movedItem] = categoryValuesOrder.splice(idx1, 1)
103
-
104
- categoryValuesOrder.splice(idx2, 0, movedItem)
105
-
106
- setEditorCatOrder(categoryValuesOrder)
107
-
108
- setState({
109
- ...state,
110
- legend: {
111
- ...state.legend,
112
- categoryValuesOrder
113
- }
114
- })
115
- }
116
-
117
- const DynamicDesc = ({label, fieldName, value: stateValue, type = "input", helper = null, ...attributes}) => {
118
- const [ value, setValue ] = useState(stateValue);
119
-
120
- const [ debouncedValue ] = useDebounce(value, 500);
121
-
122
- useEffect(() => {
123
- if('string' === typeof debouncedValue && stateValue !== debouncedValue ) {
124
- handleEditorChanges("changeLegendDescription", [String(activeFilterValueForDescription), debouncedValue])
125
- }
126
- }, [debouncedValue])
127
-
128
- const onChange = (e) => setValue(e.target.value);
129
-
130
- return (
131
- <textarea onChange={onChange} {...attributes} value={value}></textarea>
132
- )
133
- }
134
-
135
- const handleEditorChanges = async (property, value) => {
136
- switch (property) {
137
- case 'showTitle':
138
- setState({
139
- ...state,
140
- general: {
141
- ...state.general,
142
- showTitle: value
143
- }
144
- })
145
- break;
146
- case 'showSidebar':
147
- setState({
148
- ...state,
149
- general: {
150
- ...state.general,
151
- showSidebar: value
152
- }
153
- })
154
- break;
155
- case 'fullBorder':
156
- setState({
157
- ...state,
158
- general: {
159
- ...state.general,
160
- fullBorder: value
161
- }
162
- })
163
- break;
164
- case 'expandDataTable':
165
- setState({
166
- ...state,
167
- general: {
168
- ...state.general,
169
- expandDataTable: value
170
- }
171
- })
172
- break;
173
- case 'color':
174
- setState({
175
- ...state,
176
- color: value
177
- })
178
- break;
179
- case 'sidebarPosition':
180
- setState({
181
- ...state,
182
- legend: {
183
- ...state.legend,
184
- position: value
185
- }
186
- })
187
- break;
188
- case 'geoBorderColor':
189
- setState({
190
- ...state,
191
- general: {
192
- ...state.general,
193
- geoBorderColor: value
194
- }
195
- })
196
- break;
197
- case 'headerColor':
198
- setState({
199
- ...state,
200
- general: {
201
- ...state.general,
202
- headerColor: value
203
- }
204
- })
205
- break;
206
- case 'navigateColumn':
207
- setState({
208
- ...state,
209
- columns: {
210
- ...state.columns,
211
- navigate: {
212
- ...state.columns.navigate,
213
- name: value
214
- }
215
- }
216
- })
217
- break;
218
- case 'legendDescription':
219
- setState({
220
- ...state,
221
- legend: {
222
- ...state.legend,
223
- description: value
224
- }
225
- })
226
- break;
227
- case 'legendType':
228
- setState({
229
- ...state,
230
- legend: {
231
- ...state.legend,
232
- type: value
233
- }
234
- })
235
- break;
236
- case 'legendNumber':
237
- setState({
238
- ...state,
239
- legend: {
240
- ...state.legend,
241
- numberOfItems: parseInt(value)
242
- }
243
- })
244
- break;
245
- case 'changeActiveFilterValue':
246
- const arrVal = value.split(',')
247
-
248
- setActiveFilterValueForDescription(arrVal)
249
- break;
250
- case 'unifiedLegend':
251
- setState({
252
- ...state,
253
- legend: {
254
- ...state.legend,
255
- unified: value
256
- }
257
- })
258
- break;
259
- case 'separateZero':
260
- setState({
261
- ...state,
262
- legend: {
263
- ...state.legend,
264
- separateZero: value
265
- }
266
- })
267
- break;
268
- case 'toggleDownloadButton':
269
- setState({
270
- ...state,
271
- general: {
272
- ...state.general,
273
- showDownloadButton: !state.general.showDownloadButton
274
- }
275
- })
276
- break;
277
- case 'toggleDownloadMediaButton':
278
- setState({
279
- ...state,
280
- general: {
281
- ...state.general,
282
- showDownloadMediaButton: !state.general.showDownloadMediaButton
283
- }
284
- })
285
- break;
286
- case 'displayAsHex':
287
- setState({
288
- ...state,
289
- general: {
290
- ...state.general,
291
- displayAsHex: value
292
- }
293
- })
294
- break;
295
- case 'editorMapType':
296
- switch(value) {
297
- case 'data':
298
- setState({
299
- ...state,
300
- general: {
301
- ...state.general,
302
- showSidebar: true,
303
- type: "data"
304
- }
305
- })
306
- break;
307
- case 'navigation':
308
- setState({
309
- ...state,
310
- general: {
311
- ...state.general,
312
- showSidebar: false,
313
- type: "navigation"
314
- },
315
- tooltips: {
316
- ...state.tooltips,
317
- appearanceType: "hover"
318
- }
319
- })
320
- break;
321
- default:
322
- console.warn("Map type not set")
323
- break;
324
- }
325
- break;
326
- case 'geoType':
327
- // If we're still working with default data, switch to the world default to show it as an example
328
- if(true === loadedDefault && 'world' === value) {
329
- loadConfig(worldDefaultConfig)
330
- ReactTooltip.rebuild()
331
- break;
332
- }
333
-
334
- if(true === loadedDefault && 'us' === value) {
335
- loadConfig(usaDefaultConfig)
336
- ReactTooltip.rebuild()
337
- break;
338
- }
339
-
340
- switch(value) {
341
- case 'us':
342
- setState({
343
- ...state,
344
- general: {
345
- ...state.general,
346
- geoType: "us"
347
- }
348
- })
349
- break;
350
- case 'world':
351
- setState({
352
- ...state,
353
- general: {
354
- ...state.general,
355
- geoType: "world"
356
- }
357
- })
358
- break;
359
- default:
360
- console.warn("Map type not set.")
361
- break;
362
- }
363
-
364
- ReactTooltip.rebuild()
365
- break;
366
- case 'singleColumnLegend':
367
- setState({
368
- ...state,
369
- legend: {
370
- ...state.legend,
371
- singleColumn: !state.legend.singleColumn
372
- }
373
- })
374
- break;
375
- case 'dynamicDescription':
376
- setState({
377
- ...state,
378
- editor: {
379
- ...state.editor,
380
- activeFilterValueForDescription: value
381
- },
382
- legend: {
383
- ...state.legend,
384
- dynamicDescription: !state.legend.dynamicDescription
385
- }
386
- })
387
- break;
388
- case 'changeLegendDescription':
389
- const [filterValKey, filterValDesc] = value
390
- setState({
391
- ...state,
392
- legend: {
393
- ...state.legend,
394
- descriptions: {
395
- ...state.legend.descriptions,
396
- [filterValKey]: [filterValDesc]
397
- }
398
- }
399
- })
400
- break;
401
- case 'appearanceType':
402
- setState({
403
- ...state,
404
- tooltips: {
405
- ...state.tooltips,
406
- appearanceType: value
407
- }
408
- })
409
- break;
410
- case 'linkLabel':
411
- setState({
412
- ...state,
413
- tooltips: {
414
- ...state.tooltips,
415
- linkLabel: value
416
- }
417
- })
418
- break;
419
- case 'displayStateLabels':
420
- setState({
421
- ...state,
422
- general: {
423
- ...state.general,
424
- displayStateLabels: !state.general.displayStateLabels
425
- }
426
- })
427
- break;
428
- case 'capitalizeLabels':
429
- setState({
430
- ...state,
431
- tooltips: {
432
- ...state.tooltips,
433
- capitalizeLabels: value
434
- }
435
- })
436
- break;
437
- case 'showDataTable':
438
- setState({
439
- ...state,
440
- dataTable: {
441
- ...state.dataTable,
442
- forceDisplay: value
443
- }
444
- })
445
- break;
446
- default:
447
- console.warn(`Did not recognize editor property.`)
448
- break;
449
- }
450
- }
451
-
452
- const columnsRequiredChecker = useCallback(() => {
453
- let columnList = []
454
-
455
- // Geo is always required
456
- if('' === state.columns.geo.name) {
457
- columnList.push('Geography')
458
- }
459
-
460
- // Primary is required if we're on a data map or a point map
461
- if('navigation' !== state.general.type && '' === state.columns.primary.name) {
462
- columnList.push('Primary')
463
- }
464
-
465
- // Navigate is required for navigation maps
466
- if('navigation' === state.general.type && ('' === state.columns.navigate.name || undefined === state.columns.navigate) ) {
467
- columnList.push('Navigation')
468
- }
469
-
470
- if(columnList.length === 0) columnList = null
471
-
472
- setRequiredColumns(columnList)
473
- }, [state.columns, state.general.type])
474
-
475
- const editColumn = async (columnName, editTarget, value) => {
476
- switch (editTarget) {
477
- case 'specialClassDelete':
478
- const updatedSpecialClasses = Array.from(legend.specialClasses)
479
-
480
- updatedSpecialClasses.splice(value, 1)
481
-
482
- setState({
483
- ...state,
484
- legend: {
485
- ...state.legend,
486
- specialClasses: updatedSpecialClasses
487
- }
488
- })
489
- break;
490
- case 'specialClassAdd':
491
- let newSpecialClasses = legend.specialClasses
492
-
493
- newSpecialClasses.push(value.name)
494
-
495
- setState({
496
- ...state,
497
- legend: {
498
- ...state.legend,
499
- specialClasses: newSpecialClasses
500
- }
501
- })
502
- break;
503
- case 'name':
504
- setState({
505
- ...state,
506
- columns: {
507
- ...state.columns,
508
- [columnName]: {
509
- ...state.columns[columnName],
510
- [editTarget]: value
511
- }
512
- }
513
- })
514
-
515
- break;
516
- default:
517
- setState({
518
- ...state,
519
- columns: {
520
- ...state.columns,
521
- [columnName]: {
522
- ...state.columns[columnName],
523
- [editTarget]: value
524
- }
525
- }
526
- })
527
- break;
528
- }
529
- }
530
-
531
- const changeFilter = async (idx, target, value) => {
532
- let newFilters = [...state.filters]
533
-
534
- switch (target) {
535
- case 'addNew':
536
- newFilters.push({
537
- label: '',
538
- values:[]
539
- })
540
- break;
541
- case 'remove':
542
- newFilters.splice(idx, 1)
543
- break;
544
- case 'columnName':
545
- newFilters[idx] = {...newFilters[idx]}
546
- newFilters[idx].columnName = value
547
- break;
548
- default:
549
- newFilters[idx][target] = value
550
- break;
551
- }
552
-
553
- setState({
554
- ...state,
555
- filters: newFilters
556
- })
557
- }
558
-
559
- const addAdditionalColumn = (number) => {
560
- const columnKey = `additionalColumn${number}`
561
-
562
- setState({
563
- ...state,
564
- columns: {
565
- ...state.columns,
566
- [columnKey]: {
567
- label: "New Column",
568
- dataTable: false,
569
- tooltips: false,
570
- prefix: "",
571
- suffix: ""
572
- }
573
- }
574
- })
575
- }
576
-
577
- const removeAdditionalColumn = (columnName) => {
578
- const newColumns = state.columns
579
-
580
- delete newColumns[columnName]
581
-
582
- setState({
583
- ...state,
584
- columns: newColumns
585
- })
586
- }
587
-
588
- const displayFilterLegendValue = (arr) => {
589
- const filterName = state.filters[ arr[0] ].label || `Unlabeled Legend`
590
-
591
- const filterValue = runtimeFilters[ arr[0] ]
592
-
593
- if(filterValue) {
594
- return filterName + ' - ' + filterValue.values[ arr[1] ]
595
- }
596
- }
597
-
598
- const sortableItemStyles = {
599
- display:"block",
600
- boxSizing:"border-box",
601
- border:"1px solid #D1D1D1",
602
- borderRadius:"2px",
603
- background:"#F1F1F1",
604
- padding:".4em .6em",
605
- fontSize:".8em",
606
- marginRight:".3em",
607
- marginBottom:".3em",
608
- cursor:"move",
609
- zIndex:"999"
610
- }
611
-
612
- const convertStateToConfig = () => {
613
- let strippedState = JSON.parse(JSON.stringify(state)) // Deep copy
614
-
615
- // Strip ref
616
- delete strippedState[""]
617
-
618
- delete strippedState.newViz
619
-
620
- // Remove the legend
621
- let strippedLegend = JSON.parse(JSON.stringify(state.legend))
622
-
623
- delete strippedLegend.disabledAmt
624
-
625
- strippedState.legend = strippedLegend
626
-
627
- // Remove default data marker if the user started this map from default data
628
- delete strippedState.defaultData
629
-
630
- // Remove tooltips if they're active in the editor
631
- let strippedGeneral = JSON.parse(JSON.stringify(state.general))
632
-
633
- strippedState.general = strippedGeneral
634
-
635
- // Add columns property back to data if it's there
636
- if(state.data.columns) {
637
- strippedState.data.columns = state.data.columns
638
- }
639
-
640
- return strippedState
641
- }
642
-
643
- useEffect(() => {
644
- setLoadedDefault(state.defaultData)
645
-
646
- columnsRequiredChecker()
647
- }, [state])
648
-
649
- useEffect(() => {
650
- if('category' === state.legend.type && editorCatOrder.length === 0) {
651
- let arr = runtimeLegend.filter(item => !item.special).map(({value}) => value)
652
-
653
- setEditorCatOrder(arr)
654
- }
655
- }, [runtimeLegend])
656
-
657
- const columnsOptions = [<option value="" key={"Select Option"}>- Select Option -</option>]
658
-
659
- columnsInData.map(colName => {
660
- columnsOptions.push(<option value={colName} key={colName}>{colName}</option>)
661
- })
662
-
663
- const specialClasses = []
664
-
665
- if("" !== legend.specialClasses[0]) {
666
- legend.specialClasses.forEach( (specialClass, index) => {
667
- specialClasses.push({id: index, name: specialClass})
668
- })
669
- }
670
-
671
- const additionalColumns = Object.keys(state.columns).filter( (value) => {
672
- const defaultCols = [
673
- 'geo',
674
- 'navigate',
675
- 'primary'
676
- ]
677
-
678
- if( true === defaultCols.includes(value) ) {
679
- return false
680
- }
681
- return true
682
- })
683
-
684
- const updateField = (section, subsection, fieldName, newValue) => {
685
- const isArray = Array.isArray(state[section]);
686
- let sectionValue = isArray ? [...state[section], newValue] : {...state[section], [fieldName]: newValue};
687
-
688
- if(null !== subsection) {
689
- if(isArray) {
690
- sectionValue = [...state[section]]
691
- sectionValue[subsection] = {...sectionValue[subsection], [fieldName]: newValue}
692
- } else {
693
- sectionValue = {...state[section], [subsection]: { ...state[section][subsection], [fieldName]: newValue}}
694
- }
695
- }
696
-
697
- let updatedState = {
698
- ...state,
699
- [section]: sectionValue
700
- }
701
-
702
- setState(updatedState)
703
- }
704
-
705
- const onBackClick = () => {
706
- setDisplayPanel(!displayPanel);
707
- }
708
-
709
- const usedFilterColumns = {}
710
-
711
- const filtersJSX = state.filters.map( (filter, index) => {
712
- if(filter.columnName) {
713
- usedFilterColumns[filter.columnName] = true
714
- }
715
-
716
- return (
717
- <fieldset className="edit-block" key={`filter-${index}`}>
718
- <button className="remove-column" onClick={() => { changeFilter(index, "remove")}}>Remove</button>
719
- <TextField value={state.filters[index].label} section="filters" subsection={index} fieldName="label" label="Label" updateField={updateField} />
720
- <label>
721
- <span className="edit-label column-heading">Filter Column <Helper text="Selecting a column will add a dropdown menu below the map legend and allow users to filter based on the values in this column." /></span>
722
- <select value={filter.columnName} onChange={(event) => { changeFilter(index, "columnName", event.target.value) }}>
723
- {columnsOptions.filter(({key}) => undefined === usedFilterColumns[key] || filter.columnName === key)}
724
- </select>
725
- </label>
726
- </fieldset>
727
- )
728
- })
729
-
730
- const filterValueOptionList = []
731
-
732
- if(runtimeFilters.length > 0) {
733
- runtimeFilters.forEach( (filter, index) => {
734
- runtimeFilters[index].values.forEach( (value, valueNum) => {
735
- filterValueOptionList.push([index, valueNum])
736
- })
737
- })
738
- }
739
-
740
- useEffect(() => {
741
- const parsedData = convertStateToConfig()
742
-
743
- const formattedData = JSON.stringify(parsedData, undefined, 2);
744
-
745
- setConfigTextbox(formattedData)
746
-
747
- // Pass up to Editor if needed
748
- if(setParentConfig) {
749
- const newConfig = convertStateToConfig()
750
- setParentConfig(newConfig)
751
- }
752
-
753
- // eslint-disable-next-line react-hooks/exhaustive-deps
754
- }, [state])
755
-
756
- let numberOfItemsLimit = 8
757
-
758
- const getItemStyle = (isDragging, draggableStyle) => ({
759
- ...draggableStyle
760
- });
761
-
762
- const CategoryList = () => {
763
- return editorCatOrder.map((value, index) => (
764
- <Draggable key={value} draggableId={`item-${value}`} index={index}>
765
- {(provided, snapshot) => (
766
- <li
767
- style={{position: 'relative'}}
768
- >
769
- <div
770
- className={snapshot.isDragging ? 'currently-dragging': ''}
771
- style={getItemStyle(
772
- snapshot.isDragging,
773
- provided.draggableProps.style,
774
- sortableItemStyles
775
- )}
776
- ref={provided.innerRef}
777
- {...provided.draggableProps}
778
- {...provided.dragHandleProps}
779
- >{value}</div>
780
- </li>
781
- )}
782
- </Draggable>
783
- ))
784
- }
785
-
786
- return (
787
- <ErrorBoundary component="EditorPanel">
788
- {requiredColumns && <Waiting requiredColumns={requiredColumns} className={displayPanel ? `waiting` : `waiting collapsed`} />}
789
- <button className={displayPanel ? `editor-toggle` : `editor-toggle collapsed`} title={displayPanel ? `Collapse Editor` : `Expand Editor`} onClick={(onBackClick)} data-html2canvas-ignore></button>
790
- <section className={displayPanel ? 'editor-panel' : 'hidden editor-panel'} data-html2canvas-ignore>
791
- <ReactTooltip
792
- html={true}
793
- multiline={true}
794
- />
795
- <span className="base-label">Configure Map</span>
796
- <section className="form-container">
797
- <form>
798
- <Accordion allowZeroExpanded={true}>
799
- <AccordionItem> {/* Type */}
800
- <AccordionItemHeading>
801
- <AccordionItemButton>
802
- Type
803
- </AccordionItemButton>
804
- </AccordionItemHeading>
805
- <AccordionItemPanel>
806
- {/* Geography */}
807
- <label>
808
- <span className="edit-label column-heading"><span>Geography</span></span>
809
- <ul className="geo-buttons">
810
- <li className={state.general.geoType === 'us' ? 'active' : ''} onClick={() => handleEditorChanges("geoType", "us")}>
811
- <UsaGraphic />
812
- <span>United States</span>
813
- </li>
814
- <li className={state.general.geoType === 'world' ? 'active' : ''} onClick={() => handleEditorChanges("geoType", "world")}>
815
- <WorldGraphic />
816
- <span>World</span>
817
- </li>
818
- </ul>
819
- </label>
820
- <label>
821
- {/* Type */}
822
- <span className="edit-label column-heading">Map Type</span>
823
- <select value={state.general.type} onChange={(event) => { handleEditorChanges("editorMapType", event.target.value) }}>
824
- <option value="data">Data</option>
825
- <option value="navigation">Navigation</option>
826
- </select>
827
- </label>
828
- {/* SubType */}
829
- {'us' === state.general.geoType && 'data' === state.general.type &&
830
- <label className="checkbox mt-4">
831
- <input type="checkbox" checked={ state.general.displayAsHex } onChange={(event) => { handleEditorChanges("displayAsHex", event.target.checked) }} />
832
- <span className="edit-label">Display As Hex Map</span>
833
- </label>
834
- }
835
- {'us' === state.general.geoType && 'data' === state.general.type && false === state.general.displayAsHex &&
836
- <label className="checkbox">
837
- <input type="checkbox" checked={ state.general.displayStateLabels } onChange={(event) => { handleEditorChanges("displayStateLabels", event.target.checked) }} />
838
- <span className="edit-label">Display state labels</span>
839
- </label>
840
- }
841
- </AccordionItemPanel>
842
- </AccordionItem>
843
- <AccordionItem> {/* General */}
844
- <AccordionItemHeading>
845
- <AccordionItemButton>
846
- General
847
- </AccordionItemButton>
848
- </AccordionItemHeading>
849
- <AccordionItemPanel>
850
- <TextField value={state.general.title} updateField={updateField} section="general" fieldName="title" label="Title" placeholder="Map Title" helper="For accessibility reasons, you should enter a title even if you are not planning on displaying it." />
851
- <TextField type="textarea" value={general.subtext} updateField={updateField} section="general" fieldName="subtext" label="Subtext" />
852
- {'us' === state.general.geoType &&
853
- <TextField value={general.territoriesLabel} updateField={updateField} section="general" fieldName="territoriesLabel" label="Territories Label" placeholder="Territories" />
854
- }
855
- {/* <label className="checkbox mt-4">
84
+ const {
85
+ state,
86
+ columnsInData = [],
87
+ loadConfig,
88
+ setState,
89
+ isDashboard,
90
+ setParentConfig,
91
+ setRuntimeFilters,
92
+ runtimeFilters,
93
+ runtimeLegend,
94
+ } = props;
95
+
96
+ const { general, columns, legend, dataTable, tooltips } = state;
97
+
98
+ const [requiredColumns, setRequiredColumns] = useState(null); // Simple state so we know if we need more information before parsing the map
99
+
100
+ const [configTextboxValue, setConfigTextbox] = useState({});
101
+
102
+ const [loadedDefault, setLoadedDefault] = useState(false);
103
+
104
+ const [displayPanel, setDisplayPanel] = useState(true);
105
+
106
+ const [advancedToggle, setAdvancedToggle] = useState(false);
107
+
108
+ const [activeFilterValueForDescription, setActiveFilterValueForDescription] = useState([0, 0]);
109
+
110
+ const [editorCatOrder, setEditorCatOrder] = useState(state.legend.categoryValuesOrder || []);
111
+
112
+ const headerColors = [
113
+ 'theme-blue',
114
+ 'theme-purple',
115
+ 'theme-brown',
116
+ 'theme-teal',
117
+ 'theme-pink',
118
+ 'theme-orange',
119
+ 'theme-slate',
120
+ 'theme-indigo',
121
+ 'theme-cyan',
122
+ 'theme-green',
123
+ 'theme-amber',
124
+ ];
125
+
126
+ const categoryMove = (idx1, idx2) => {
127
+ let categoryValuesOrder = [...editorCatOrder];
128
+
129
+ let [movedItem] = categoryValuesOrder.splice(idx1, 1);
130
+
131
+ categoryValuesOrder.splice(idx2, 0, movedItem);
132
+
133
+ setEditorCatOrder(categoryValuesOrder);
134
+
135
+ setState({
136
+ ...state,
137
+ legend: {
138
+ ...state.legend,
139
+ categoryValuesOrder,
140
+ },
141
+ });
142
+ };
143
+
144
+
145
+ const handleFilterOrder = (idx1, idx2, filterIndex, filter) => {
146
+
147
+ let filterOrder = filter.values;
148
+ let [movedItem] = filterOrder.splice(idx1, 1);
149
+ filterOrder.splice(idx2, 0, movedItem);
150
+ let filters = [...runtimeFilters]
151
+ let filterItem= { ...runtimeFilters[filterIndex] };
152
+ filterItem.active = filter.values[0]
153
+ filterItem.values = filterOrder;
154
+ filterItem.order = 'cust'
155
+ filters[filterIndex] = filterItem
156
+
157
+ setState({
158
+ ...state,
159
+ filters
160
+ });
161
+
162
+ };
163
+
164
+
165
+ const DynamicDesc = ({ label, fieldName, value: stateValue, type = 'input', helper = null, ...attributes }) => {
166
+ const [value, setValue] = useState(stateValue);
167
+
168
+ const [debouncedValue] = useDebounce(value, 500);
169
+
170
+ useEffect(() => {
171
+ if ('string' === typeof debouncedValue && stateValue !== debouncedValue) {
172
+ handleEditorChanges('changeLegendDescription', [
173
+ String(activeFilterValueForDescription),
174
+ debouncedValue,
175
+ ]);
176
+ }
177
+ }, [debouncedValue]);
178
+
179
+ const onChange = (e) => setValue(e.target.value);
180
+
181
+ return <textarea onChange={onChange} {...attributes} value={value}></textarea>;
182
+ };
183
+
184
+ const handleEditorChanges = async (property, value) => {
185
+ switch (property) {
186
+ case 'showTitle':
187
+ setState({
188
+ ...state,
189
+ general: {
190
+ ...state.general,
191
+ showTitle: value,
192
+ },
193
+ });
194
+ break;
195
+ case 'showSidebar':
196
+ setState({
197
+ ...state,
198
+ general: {
199
+ ...state.general,
200
+ showSidebar: value,
201
+ },
202
+ });
203
+ break;
204
+ case 'fullBorder':
205
+ setState({
206
+ ...state,
207
+ general: {
208
+ ...state.general,
209
+ fullBorder: value,
210
+ },
211
+ });
212
+ break;
213
+ case 'expandDataTable':
214
+ setState({
215
+ ...state,
216
+ general: {
217
+ ...state.general,
218
+ expandDataTable: value,
219
+ },
220
+ });
221
+ break;
222
+ case 'color':
223
+ setState({
224
+ ...state,
225
+ color: value,
226
+ });
227
+ break;
228
+ case 'sidebarPosition':
229
+ setState({
230
+ ...state,
231
+ legend: {
232
+ ...state.legend,
233
+ position: value,
234
+ },
235
+ });
236
+ break;
237
+ case 'geoBorderColor':
238
+ setState({
239
+ ...state,
240
+ general: {
241
+ ...state.general,
242
+ geoBorderColor: value,
243
+ },
244
+ });
245
+ break;
246
+ case 'headerColor':
247
+ setState({
248
+ ...state,
249
+ general: {
250
+ ...state.general,
251
+ headerColor: value,
252
+ },
253
+ });
254
+ break;
255
+ case 'navigateColumn':
256
+ setState({
257
+ ...state,
258
+ columns: {
259
+ ...state.columns,
260
+ navigate: {
261
+ ...state.columns.navigate,
262
+ name: value,
263
+ },
264
+ },
265
+ });
266
+ break;
267
+ case 'legendDescription':
268
+ setState({
269
+ ...state,
270
+ legend: {
271
+ ...state.legend,
272
+ description: value,
273
+ },
274
+ });
275
+ break;
276
+ case 'legendType':
277
+ setState({
278
+ ...state,
279
+ legend: {
280
+ ...state.legend,
281
+ type: value,
282
+ },
283
+ });
284
+ break;
285
+ case 'legendNumber':
286
+ setState({
287
+ ...state,
288
+ legend: {
289
+ ...state.legend,
290
+ numberOfItems: parseInt(value),
291
+ },
292
+ });
293
+ break;
294
+ case 'changeActiveFilterValue':
295
+ const arrVal = value.split(',');
296
+
297
+ setActiveFilterValueForDescription(arrVal);
298
+ break;
299
+ case 'unifiedLegend':
300
+ setState({
301
+ ...state,
302
+ legend: {
303
+ ...state.legend,
304
+ unified: value,
305
+ },
306
+ });
307
+ break;
308
+ case 'separateZero':
309
+ setState({
310
+ ...state,
311
+ legend: {
312
+ ...state.legend,
313
+ separateZero: value,
314
+ },
315
+ });
316
+ break;
317
+ case 'toggleDownloadButton':
318
+ setState({
319
+ ...state,
320
+ general: {
321
+ ...state.general,
322
+ showDownloadButton: !state.general.showDownloadButton,
323
+ },
324
+ });
325
+ break;
326
+ case 'toggleDownloadMediaButton':
327
+ setState({
328
+ ...state,
329
+ general: {
330
+ ...state.general,
331
+ showDownloadMediaButton: !state.general.showDownloadMediaButton,
332
+ },
333
+ });
334
+ break;
335
+ case 'displayAsHex':
336
+ setState({
337
+ ...state,
338
+ general: {
339
+ ...state.general,
340
+ displayAsHex: value,
341
+ },
342
+ });
343
+ break;
344
+ case 'editorMapType':
345
+ switch (value) {
346
+ case 'data':
347
+ setState({
348
+ ...state,
349
+ general: {
350
+ ...state.general,
351
+ showSidebar: true,
352
+ type: 'data',
353
+ },
354
+ });
355
+ break;
356
+ case 'navigation':
357
+ setState({
358
+ ...state,
359
+ general: {
360
+ ...state.general,
361
+ showSidebar: false,
362
+ type: 'navigation',
363
+ },
364
+ tooltips: {
365
+ ...state.tooltips,
366
+ appearanceType: 'hover',
367
+ },
368
+ });
369
+ break;
370
+ default:
371
+ console.warn('Map type not set');
372
+ break;
373
+ }
374
+ break;
375
+ case 'geoType':
376
+ // If we're still working with default data, switch to the world default to show it as an example
377
+ if (true === loadedDefault && 'world' === value) {
378
+ loadConfig(worldDefaultConfig);
379
+ ReactTooltip.rebuild();
380
+ break;
381
+ }
382
+
383
+ if (true === loadedDefault && 'us' === value) {
384
+ loadConfig(usaDefaultConfig);
385
+ ReactTooltip.rebuild();
386
+ break;
387
+ }
388
+
389
+ if (true === loadedDefault && 'us-county' === value) {
390
+ loadConfig(countyDefaultConfig);
391
+ ReactTooltip.rebuild();
392
+ break;
393
+ }
394
+
395
+ switch (value) {
396
+ case 'us':
397
+ setState({
398
+ ...state,
399
+ general: {
400
+ ...state.general,
401
+ geoType: 'us',
402
+ },
403
+ dataTable: {
404
+ ...state.dataTable,
405
+ forceDisplay: true,
406
+ },
407
+ });
408
+ ReactTooltip.rebuild();
409
+ break;
410
+ case 'world':
411
+ setState({
412
+ ...state,
413
+ general: {
414
+ ...state.general,
415
+ geoType: 'world',
416
+ },
417
+ dataTable: {
418
+ ...state.dataTable,
419
+ forceDisplay: true,
420
+ },
421
+ });
422
+ break;
423
+ case 'us-county':
424
+ setState({
425
+ ...state,
426
+ general: {
427
+ ...state.general,
428
+ geoType: 'us-county',
429
+ expandDataTable: false,
430
+ },
431
+ dataTable: {
432
+ ...state.dataTable,
433
+ forceDisplay: true,
434
+ },
435
+ });
436
+ break;
437
+ case 'single-state':
438
+ setState({
439
+ ...state,
440
+ general: {
441
+ ...state.general,
442
+ geoType: 'single-state',
443
+ expandDataTable: false,
444
+ },
445
+ dataTable: {
446
+ ...state.dataTable,
447
+ forceDisplay: true,
448
+ },
449
+ });
450
+ break;
451
+ default:
452
+ break;
453
+ }
454
+
455
+ ReactTooltip.rebuild();
456
+ break;
457
+ case 'singleColumnLegend':
458
+ setState({
459
+ ...state,
460
+ legend: {
461
+ ...state.legend,
462
+ singleColumn: !state.legend.singleColumn,
463
+ },
464
+ });
465
+ break;
466
+ case 'dynamicDescription':
467
+ setState({
468
+ ...state,
469
+ editor: {
470
+ ...state.editor,
471
+ activeFilterValueForDescription: value,
472
+ },
473
+ legend: {
474
+ ...state.legend,
475
+ dynamicDescription: !state.legend.dynamicDescription,
476
+ },
477
+ });
478
+ break;
479
+ case 'changeLegendDescription':
480
+ const [filterValKey, filterValDesc] = value;
481
+ setState({
482
+ ...state,
483
+ legend: {
484
+ ...state.legend,
485
+ descriptions: {
486
+ ...state.legend.descriptions,
487
+ [filterValKey]: [filterValDesc],
488
+ },
489
+ },
490
+ });
491
+ break;
492
+ case 'appearanceType':
493
+ setState({
494
+ ...state,
495
+ tooltips: {
496
+ ...state.tooltips,
497
+ appearanceType: value,
498
+ },
499
+ });
500
+ break;
501
+ case 'linkLabel':
502
+ setState({
503
+ ...state,
504
+ tooltips: {
505
+ ...state.tooltips,
506
+ linkLabel: value,
507
+ },
508
+ });
509
+ break;
510
+ case 'displayStateLabels':
511
+ setState({
512
+ ...state,
513
+ general: {
514
+ ...state.general,
515
+ displayStateLabels: !state.general.displayStateLabels,
516
+ },
517
+ });
518
+ break;
519
+ case 'capitalizeLabels':
520
+ setState({
521
+ ...state,
522
+ tooltips: {
523
+ ...state.tooltips,
524
+ capitalizeLabels: value,
525
+ },
526
+ });
527
+ break;
528
+ case 'showDataTable':
529
+ setState({
530
+ ...state,
531
+ dataTable: {
532
+ ...state.dataTable,
533
+ forceDisplay: value,
534
+ },
535
+ });
536
+ break;
537
+ case 'limitDataTableHeight':
538
+ setState({
539
+ ...state,
540
+ dataTable: {
541
+ ...state.dataTable,
542
+ limitHeight: value
543
+ }
544
+ });
545
+ break;
546
+ case 'chooseState':
547
+ let fipsCode = Object.keys(supportedStatesFipsCodes).find(
548
+ (key) => supportedStatesFipsCodes[key] === value
549
+ );
550
+ let stateName = value;
551
+ let stateData = { fipsCode, stateName };
552
+
553
+ setState({
554
+ ...state,
555
+ general: {
556
+ ...state.general,
557
+ statePicked: stateData,
558
+ },
559
+ });
560
+ break;
561
+ default:
562
+ console.warn(`Did not recognize editor property.`);
563
+ break;
564
+ }
565
+ };
566
+
567
+ const columnsRequiredChecker = useCallback(() => {
568
+ let columnList = [];
569
+
570
+ // Geo is always required
571
+ if ('' === state.columns.geo.name) {
572
+ columnList.push('Geography');
573
+ }
574
+
575
+ // Primary is required if we're on a data map or a point map
576
+ if ('navigation' !== state.general.type && '' === state.columns.primary.name) {
577
+ columnList.push('Primary');
578
+ }
579
+
580
+ // Navigate is required for navigation maps
581
+ if (
582
+ 'navigation' === state.general.type &&
583
+ ('' === state.columns.navigate.name || undefined === state.columns.navigate)
584
+ ) {
585
+ columnList.push('Navigation');
586
+ }
587
+
588
+ if (columnList.length === 0) columnList = null;
589
+
590
+ setRequiredColumns(columnList);
591
+ }, [state.columns, state.general.type]);
592
+
593
+ const editColumn = async (columnName, editTarget, value) => {
594
+ let newSpecialClasses;
595
+
596
+ switch (editTarget) {
597
+ case 'specialClassEdit':
598
+ newSpecialClasses = Array.from(legend.specialClasses);
599
+
600
+ newSpecialClasses[value.index][value.prop] = value.value;
601
+
602
+ setState({
603
+ ...state,
604
+ legend: {
605
+ ...state.legend,
606
+ specialClasses: newSpecialClasses,
607
+ },
608
+ });
609
+ break;
610
+ case 'specialClassDelete':
611
+ newSpecialClasses = Array.from(legend.specialClasses);
612
+
613
+ newSpecialClasses.splice(value, 1);
614
+
615
+ setState({
616
+ ...state,
617
+ legend: {
618
+ ...state.legend,
619
+ specialClasses: newSpecialClasses,
620
+ },
621
+ });
622
+ break;
623
+ case 'specialClassAdd':
624
+ newSpecialClasses = legend.specialClasses;
625
+
626
+ newSpecialClasses.push(value);
627
+
628
+ setState({
629
+ ...state,
630
+ legend: {
631
+ ...state.legend,
632
+ specialClasses: newSpecialClasses,
633
+ },
634
+ });
635
+ break;
636
+ case 'name':
637
+ setState({
638
+ ...state,
639
+ columns: {
640
+ ...state.columns,
641
+ [columnName]: {
642
+ ...state.columns[columnName],
643
+ [editTarget]: value,
644
+ },
645
+ },
646
+ });
647
+
648
+ break;
649
+ default:
650
+ setState({
651
+ ...state,
652
+ columns: {
653
+ ...state.columns,
654
+ [columnName]: {
655
+ ...state.columns[columnName],
656
+ [editTarget]: value,
657
+ },
658
+ },
659
+ });
660
+ break;
661
+ }
662
+ };
663
+
664
+ const changeFilter = async (idx, target, value) => {
665
+
666
+ let newFilters = [...state.filters];
667
+
668
+ switch (target) {
669
+ case 'addNew':
670
+ newFilters.push({
671
+ label: '',
672
+ values: [],
673
+ });
674
+ break;
675
+ case 'remove':
676
+
677
+ if(newFilters.length === 1) {
678
+ newFilters = []
679
+ } else {
680
+ newFilters.splice(idx, 1);
681
+ }
682
+ break;
683
+ case 'columnName':
684
+ newFilters[idx] = { ...newFilters[idx] };
685
+ newFilters[idx].columnName = value;
686
+ newFilters[idx].values = [] // when a column name changes knock the previous values out
687
+ break;
688
+ case 'filterOrder':
689
+ if(value === 'desc') {
690
+ newFilters[idx] = { ...runtimeFilters[idx]}
691
+ delete newFilters[idx].active;
692
+ newFilters[idx].order = 'desc';
693
+ }
694
+ if(value === 'asc') {
695
+ newFilters[idx] = { ...runtimeFilters[idx] }
696
+ delete newFilters[idx].active;
697
+ newFilters[idx].order = 'asc'
698
+ }
699
+ if(value === 'cust') {
700
+ newFilters[idx] = { ...runtimeFilters[idx] }
701
+ newFilters[idx].order = 'cust'
702
+ }
703
+ break;
704
+ default:
705
+ newFilters[idx][target] = value;
706
+ break;
707
+ }
708
+
709
+ setState({
710
+ ...state,
711
+ filters: newFilters,
712
+ });
713
+
714
+ };
715
+
716
+ const addAdditionalColumn = (number) => {
717
+ const columnKey = `additionalColumn${number}`;
718
+
719
+ setState({
720
+ ...state,
721
+ columns: {
722
+ ...state.columns,
723
+ [columnKey]: {
724
+ label: 'New Column',
725
+ dataTable: false,
726
+ tooltips: false,
727
+ prefix: '',
728
+ suffix: '',
729
+ },
730
+ },
731
+ });
732
+ };
733
+
734
+ const removeAdditionalColumn = (columnName) => {
735
+ const newColumns = state.columns;
736
+
737
+ delete newColumns[columnName];
738
+
739
+ setState({
740
+ ...state,
741
+ columns: newColumns,
742
+ });
743
+ };
744
+
745
+ const displayFilterLegendValue = (arr) => {
746
+ const filterName = state.filters[arr[0]].label || `Unlabeled Legend`;
747
+
748
+ const filterValue = runtimeFilters[arr[0]];
749
+
750
+ if (filterValue) {
751
+ return filterName + ' - ' + filterValue.values[arr[1]];
752
+ }
753
+ };
754
+
755
+ const sortableItemStyles = {
756
+ display: 'block',
757
+ boxSizing: 'border-box',
758
+ border: '1px solid #D1D1D1',
759
+ borderRadius: '2px',
760
+ background: '#F1F1F1',
761
+ padding: '.4em .6em',
762
+ fontSize: '.8em',
763
+ marginRight: '.3em',
764
+ marginBottom: '.3em',
765
+ cursor: 'move',
766
+ zIndex: '999',
767
+ };
768
+
769
+ const convertStateToConfig = () => {
770
+ let strippedState = JSON.parse(JSON.stringify(state)); // Deep copy
771
+
772
+ // Strip ref
773
+ delete strippedState[''];
774
+
775
+ delete strippedState.newViz;
776
+
777
+ // Remove the legend
778
+ let strippedLegend = JSON.parse(JSON.stringify(state.legend));
779
+
780
+ delete strippedLegend.disabledAmt;
781
+
782
+ strippedState.legend = strippedLegend;
783
+
784
+ // Remove default data marker if the user started this map from default data
785
+ delete strippedState.defaultData;
786
+
787
+ // Remove tooltips if they're active in the editor
788
+ let strippedGeneral = JSON.parse(JSON.stringify(state.general));
789
+
790
+ strippedState.general = strippedGeneral;
791
+
792
+ // Add columns property back to data if it's there
793
+ if (state.data.columns) {
794
+ strippedState.data.columns = state.data.columns;
795
+ }
796
+
797
+ return strippedState;
798
+ };
799
+
800
+ useEffect(() => {
801
+ setLoadedDefault(state.defaultData);
802
+
803
+ columnsRequiredChecker();
804
+ }, [state]);
805
+
806
+ useEffect(() => {
807
+ if ('category' === state.legend.type && editorCatOrder.length === 0) {
808
+ let arr = runtimeLegend.filter((item) => !item.special).map(({ value }) => value);
809
+
810
+ setEditorCatOrder(arr);
811
+ }
812
+ }, [runtimeLegend]);
813
+
814
+
815
+ // if no state choice by default show alabama
816
+ useEffect(() => {
817
+ if (!state.general.statePicked) {
818
+ setState({
819
+ ...state,
820
+ general: {
821
+ ...general,
822
+ statePicked: {
823
+ fipsCode: '01',
824
+ stateName: 'Alabama',
825
+ },
826
+ },
827
+ });
828
+ }
829
+ }, []);
830
+
831
+ const columnsOptions = [
832
+ <option value='' key={'Select Option'}>
833
+ - Select Option -
834
+ </option>,
835
+ ];
836
+
837
+ columnsInData.map((colName) => {
838
+ columnsOptions.push(
839
+ <option value={colName} key={colName}>
840
+ {colName}
841
+ </option>
842
+ );
843
+ });
844
+
845
+ let columnsByKey = {};
846
+ state.data.forEach(datum => {
847
+ Object.keys(datum).forEach(key => {
848
+ columnsByKey[key] = columnsByKey[key] || [];
849
+ const value = typeof datum[key] === 'number' ? datum[key].toString() : datum[key];
850
+
851
+ if(columnsByKey[key].indexOf(value) === -1){
852
+ columnsByKey[key].push(value);
853
+ }
854
+ });
855
+ });
856
+
857
+ let specialClasses = [];
858
+ if(legend.specialClasses && legend.specialClasses.length && typeof legend.specialClasses[0] === 'string'){
859
+ legend.specialClasses.forEach(specialClass => {
860
+ specialClasses.push({
861
+ key: state.columns.primary && state.columns.primary.name ? state.columns.primary.name : columnsInData[0],
862
+ value: specialClass,
863
+ label: specialClass
864
+ });
865
+ });
866
+ } else {
867
+ specialClasses = legend.specialClasses || [];
868
+ }
869
+
870
+ const additionalColumns = Object.keys(state.columns).filter((value) => {
871
+ const defaultCols = ['geo', 'navigate', 'primary'];
872
+
873
+ if (true === defaultCols.includes(value)) {
874
+ return false;
875
+ }
876
+ return true;
877
+ });
878
+
879
+ const updateField = (section, subsection, fieldName, newValue) => {
880
+ const isArray = Array.isArray(state[section]);
881
+ let sectionValue = isArray ? [...state[section], newValue] : { ...state[section], [fieldName]: newValue };
882
+
883
+ if (null !== subsection) {
884
+ if (isArray) {
885
+ sectionValue = [...state[section]];
886
+ sectionValue[subsection] = { ...sectionValue[subsection], [fieldName]: newValue };
887
+ } else {
888
+ sectionValue = {
889
+ ...state[section],
890
+ [subsection]: { ...state[section][subsection], [fieldName]: newValue },
891
+ };
892
+ }
893
+ }
894
+
895
+ let updatedState = {
896
+ ...state,
897
+ [section]: sectionValue,
898
+ };
899
+
900
+ setState(updatedState);
901
+ };
902
+
903
+ const onBackClick = () => {
904
+ setDisplayPanel(!displayPanel);
905
+ };
906
+
907
+ const usedFilterColumns = {};
908
+
909
+ const filtersJSX = state.filters.map((filter, index) => {
910
+ if (filter.columnName) {
911
+ usedFilterColumns[filter.columnName] = true;
912
+ }
913
+
914
+ const filterOptions = [
915
+ {
916
+ label: 'Ascending Alphanumeric',
917
+ value: 'asc'
918
+ },
919
+ {
920
+ label: 'Descending Alphanumeric',
921
+ value: 'desc'
922
+ },
923
+ {
924
+ label: 'Custom',
925
+ value: 'cust'
926
+ }
927
+ ]
928
+
929
+ return (
930
+ <fieldset className='edit-block' key={`filter-${index}`}>
931
+ <button
932
+ className='remove-column'
933
+ onClick={(e) => {
934
+ e.preventDefault();
935
+ changeFilter(index, 'remove');
936
+ }}
937
+ >
938
+ Remove
939
+ </button>
940
+ <TextField
941
+ value={state.filters[index].label}
942
+ section='filters'
943
+ subsection={index}
944
+ fieldName='label'
945
+ label='Label'
946
+ updateField={updateField}
947
+ />
948
+ <label>
949
+ <span className='edit-label column-heading'>
950
+ Filter Column{' '}
951
+ <Helper text='Selecting a column will add a dropdown menu below the map legend and allow users to filter based on the values in this column.' />
952
+ </span>
953
+ <select
954
+ value={filter.columnName}
955
+ onChange={(event) => {
956
+ changeFilter(index, 'columnName', event.target.value);
957
+ }}
958
+ >
959
+ {columnsOptions.filter(
960
+ ({ key }) => undefined === usedFilterColumns[key] || filter.columnName === key
961
+ )}
962
+ </select>
963
+ </label>
964
+
965
+ <label>
966
+ <span className="edit-filterOrder column-heading">Filter Order</span>
967
+ <select value={filter.order} onChange={ (e) => {
968
+ changeFilter(index, 'filterOrder', e.target.value)
969
+ }}>
970
+ {filterOptions.map( (option, index) => {
971
+ return <option value={option.value} key={`filter-${index}`}>{option.label}</option>
972
+ })}
973
+ </select>
974
+ </label>
975
+
976
+ {filter.order === 'cust' &&
977
+ <DragDropContext
978
+ onDragEnd={({ source, destination }) =>
979
+ handleFilterOrder(source.index, destination.index, index, runtimeFilters[index])
980
+ }>
981
+ <Droppable droppableId='filter_order'>
982
+ {(provided) => (
983
+ <ul
984
+ {...provided.droppableProps}
985
+ className='sort-list'
986
+ ref={provided.innerRef}
987
+ style={{ marginTop: '1em' }}
988
+ >
989
+ {runtimeFilters[index]?.values.map( (value, index) => {
990
+ return (
991
+ <Draggable key={value} draggableId={`draggableFilter-${value}`} index={index}>
992
+ {(provided, snapshot) => (
993
+ <li>
994
+ <div className={snapshot.isDragging ? 'currently-dragging' : ''}
995
+ style={getItemStyle(snapshot.isDragging, provided.draggableProps.style, sortableItemStyles)}
996
+ ref={provided.innerRef}
997
+ {...provided.draggableProps}
998
+ {...provided.dragHandleProps}>
999
+ {value}
1000
+ </div>
1001
+ </li>
1002
+ ) }
1003
+ </Draggable>
1004
+ )
1005
+ })}
1006
+ {provided.placeholder}
1007
+ </ul>
1008
+ )}
1009
+ </Droppable>
1010
+ </DragDropContext>
1011
+ }
1012
+
1013
+ </fieldset>
1014
+ );
1015
+ });
1016
+
1017
+ const StateOptionList = () => {
1018
+ const arrOfArrays = Object.entries(supportedStatesFipsCodes);
1019
+
1020
+ let sorted = arrOfArrays.sort((a, b) => {
1021
+ return a[0].localeCompare(b[0]);
1022
+ });
1023
+
1024
+ let options = [];
1025
+ sorted.forEach((state) => {
1026
+ options.push(
1027
+ <option key={state[0]} value={state[1]}>
1028
+ {state[1]}
1029
+ </option>
1030
+ );
1031
+ });
1032
+
1033
+ return options;
1034
+ };
1035
+
1036
+ const filterValueOptionList = [];
1037
+
1038
+ if (runtimeFilters.length > 0) {
1039
+ runtimeFilters.forEach((filter, index) => {
1040
+ runtimeFilters[index].values.forEach((value, valueNum) => {
1041
+ filterValueOptionList.push([index, valueNum]);
1042
+ });
1043
+ });
1044
+ }
1045
+
1046
+ useEffect(() => {
1047
+ const parsedData = convertStateToConfig();
1048
+ const formattedData = JSON.stringify(parsedData, undefined, 2);
1049
+
1050
+ setConfigTextbox(formattedData);
1051
+
1052
+ // Pass up to Editor if needed
1053
+ if (setParentConfig) {
1054
+ const newConfig = convertStateToConfig();
1055
+ setParentConfig(newConfig);
1056
+ }
1057
+
1058
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1059
+ }, [state]);
1060
+
1061
+ let numberOfItemsLimit = 8;
1062
+
1063
+ const getItemStyle = (isDragging, draggableStyle) => ({
1064
+ ...draggableStyle,
1065
+ });
1066
+
1067
+ const CategoryList = () => {
1068
+ return editorCatOrder.map((value, index) => (
1069
+ <Draggable key={value} draggableId={`item-${value}`} index={index}>
1070
+ {(provided, snapshot) => (
1071
+ <li style={{ position: 'relative' }}>
1072
+ <div
1073
+ className={snapshot.isDragging ? 'currently-dragging' : ''}
1074
+ style={getItemStyle(snapshot.isDragging, provided.draggableProps.style, sortableItemStyles)}
1075
+ ref={provided.innerRef}
1076
+ {...provided.draggableProps}
1077
+ {...provided.dragHandleProps}
1078
+ >
1079
+ {value}
1080
+ </div>
1081
+ </li>
1082
+ )}
1083
+ </Draggable>
1084
+ ));
1085
+ };
1086
+
1087
+ return (
1088
+ <ErrorBoundary component='EditorPanel'>
1089
+ {requiredColumns && (
1090
+ <Waiting requiredColumns={requiredColumns} className={displayPanel ? `waiting` : `waiting collapsed`} />
1091
+ )}
1092
+ <button
1093
+ className={displayPanel ? `editor-toggle` : `editor-toggle collapsed`}
1094
+ title={displayPanel ? `Collapse Editor` : `Expand Editor`}
1095
+ onClick={onBackClick}
1096
+ data-html2canvas-ignore
1097
+ ></button>
1098
+ <section className={displayPanel ? 'editor-panel' : 'hidden editor-panel'} data-html2canvas-ignore>
1099
+ <ReactTooltip html={true} multiline={true} />
1100
+ <span className='base-label'>Configure Map</span>
1101
+ <section className='form-container'>
1102
+ <form>
1103
+ <Accordion allowZeroExpanded={true}>
1104
+ <AccordionItem>
1105
+ {' '}
1106
+ {/* Type */}
1107
+ <AccordionItemHeading>
1108
+ <AccordionItemButton>Type</AccordionItemButton>
1109
+ </AccordionItemHeading>
1110
+ <AccordionItemPanel>
1111
+ {/* Geography */}
1112
+ <label>
1113
+ <span className='edit-label column-heading'>
1114
+ <span>Geography</span>
1115
+ </span>
1116
+ <ul className='geo-buttons'>
1117
+ <li
1118
+ className={
1119
+ state.general.geoType === 'us' ||
1120
+ state.general.geoType === 'us-county'
1121
+ ? 'active'
1122
+ : ''
1123
+ }
1124
+ onClick={() => handleEditorChanges('geoType', 'us')}
1125
+ >
1126
+ <UsaGraphic />
1127
+ <span>United States</span>
1128
+ </li>
1129
+ <li
1130
+ className={state.general.geoType === 'world' ? 'active' : ''}
1131
+ onClick={() => handleEditorChanges('geoType', 'world')}
1132
+ >
1133
+ <WorldGraphic />
1134
+ <span>World</span>
1135
+ </li>
1136
+ <li
1137
+ className={state.general.geoType === 'single-state' ? 'active' : ''}
1138
+ onClick={() => handleEditorChanges('geoType', 'single-state')}
1139
+ >
1140
+ <AlabamaGraphic />
1141
+ <span>U.S. State</span>
1142
+ </li>
1143
+ </ul>
1144
+ </label>
1145
+ {/* Select > State or County Map */}
1146
+ {(state.general.geoType === 'us' || state.general.geoType === 'us-county') && (
1147
+ <label>
1148
+ <span className='edit-label column-heading'>Map Type</span>
1149
+ <select
1150
+ value={state.general.geoType}
1151
+ onChange={(event) => {
1152
+ handleEditorChanges('geoType', event.target.value);
1153
+ }}
1154
+ >
1155
+ <option value='us'>US State-Level</option>
1156
+ <option value='us-county'>US County-Level</option>
1157
+ </select>
1158
+ </label>
1159
+ )}
1160
+ {/* Type */}
1161
+ {/* Select > Filter a state */}
1162
+ {state.general.geoType === 'single-state' && (
1163
+ <label>
1164
+ <span className='edit-label column-heading'>State Selector</span>
1165
+ <select
1166
+ value={
1167
+ state.general.hasOwnProperty('statePicked')
1168
+ ? state.general.statePicked.stateName
1169
+ : { fipsCode: '04', stateName: 'Alabama' }
1170
+ }
1171
+ onChange={(event) => {
1172
+ handleEditorChanges('chooseState', event.target.value);
1173
+ }}
1174
+ >
1175
+ <StateOptionList />
1176
+ </select>
1177
+ </label>
1178
+ )}
1179
+ {/* Type */}
1180
+ <label>
1181
+ <span className='edit-label column-heading'>Map Type</span>
1182
+ <select
1183
+ value={state.general.type}
1184
+ onChange={(event) => {
1185
+ handleEditorChanges('editorMapType', event.target.value);
1186
+ }}
1187
+ >
1188
+ <option value='data'>Data</option>
1189
+ <option value='navigation'>Navigation</option>
1190
+ </select>
1191
+ </label>
1192
+ {/* SubType */}
1193
+ {'us' === state.general.geoType && 'data' === state.general.type && (
1194
+ <label className='checkbox mt-4'>
1195
+ <input
1196
+ type='checkbox'
1197
+ checked={state.general.displayAsHex}
1198
+ onChange={(event) => {
1199
+ handleEditorChanges('displayAsHex', event.target.checked);
1200
+ }}
1201
+ />
1202
+ <span className='edit-label'>Display As Hex Map</span>
1203
+ </label>
1204
+ )}
1205
+ {'us' === state.general.geoType &&
1206
+ 'data' === state.general.type &&
1207
+ false === state.general.displayAsHex && (
1208
+ <label className='checkbox'>
1209
+ <input
1210
+ type='checkbox'
1211
+ checked={state.general.displayStateLabels}
1212
+ onChange={(event) => {
1213
+ handleEditorChanges('displayStateLabels', event.target.checked);
1214
+ }}
1215
+ />
1216
+ <span className='edit-label'>Display state labels</span>
1217
+ </label>
1218
+ )}
1219
+ </AccordionItemPanel>
1220
+ </AccordionItem>
1221
+ <AccordionItem>
1222
+ {' '}
1223
+ {/* General */}
1224
+ <AccordionItemHeading>
1225
+ <AccordionItemButton>General</AccordionItemButton>
1226
+ </AccordionItemHeading>
1227
+ <AccordionItemPanel>
1228
+ <TextField
1229
+ value={state.general.title}
1230
+ updateField={updateField}
1231
+ section='general'
1232
+ fieldName='title'
1233
+ label='Title'
1234
+ placeholder='Map Title'
1235
+ helper='For accessibility reasons, you should enter a title even if you are not planning on displaying it.'
1236
+ />
1237
+ <TextField
1238
+ type='textarea'
1239
+ value={general.subtext}
1240
+ updateField={updateField}
1241
+ section='general'
1242
+ fieldName='subtext'
1243
+ label='Subtext'
1244
+ />
1245
+ {'us' === state.general.geoType && (
1246
+ <TextField
1247
+ value={general.territoriesLabel}
1248
+ updateField={updateField}
1249
+ section='general'
1250
+ fieldName='territoriesLabel'
1251
+ label='Territories Label'
1252
+ placeholder='Territories'
1253
+ />
1254
+ )}
1255
+ {/* <label className="checkbox mt-4">
856
1256
  <input type="checkbox" checked={ state.general.showDownloadMediaButton } onChange={(event) => { handleEditorChanges("toggleDownloadMediaButton", event.target.checked) }} />
857
1257
  <span className="edit-label">Enable Media Download</span>
858
1258
  </label> */}
859
- </AccordionItemPanel>
860
- </AccordionItem>
861
- <AccordionItem> {/* Columns */}
862
- <AccordionItemHeading>
863
- <AccordionItemButton>
864
- Columns
865
- </AccordionItemButton>
866
- </AccordionItemHeading>
867
- <AccordionItemPanel>
868
- <label className="edit-block geo">
869
- <span className="edit-label column-heading">Geography</span>
870
- <select value={state.columns.geo ? state.columns.geo.name : columnsOptions[0] } onChange={(event) => { editColumn("geo", "name", event.target.value) }}>
871
- {columnsOptions}
872
- </select>
873
- </label>
874
- {"navigation" !== state.general.type &&
875
- <fieldset className="primary-fieldset edit-block">
876
- <label>
877
- <span className="edit-label column-heading">Primary</span>
878
- <select value={state.columns.primary ? state.columns.primary.name : columnsOptions[0] } onChange={(event) => { editColumn("primary", "name", event.target.value) }}>
879
- {columnsOptions}
880
- </select>
881
- </label>
882
- <TextField value={columns.primary.label} section="columns" subsection="primary" fieldName="label" label="Label" updateField={updateField} />
883
- <ul className="column-edit">
884
- <li className="three-col">
885
- <TextField value={columns.primary.prefix} section="columns" subsection="primary" fieldName="prefix" label="Prefix" updateField={updateField} />
886
- <TextField value={columns.primary.suffix} section="columns" subsection="primary" fieldName="suffix" label="Suffix" updateField={updateField} />
887
- <TextField type="number" value={columns.primary.roundToPlace} section="columns" subsection="primary" fieldName="roundToPlace" label="Round" updateField={updateField} />
888
- </li>
889
- <li>
890
- <label className="checkbox">
891
- <input type="checkbox" checked={ state.columns.primary.useCommas } onChange={(event) => { editColumn("primary", "useCommas", event.target.checked) }} />
892
- <span className="edit-label">Add Commas to Numbers</span>
893
- </label>
894
- </li>
895
- <li>
896
- <label className="checkbox">
897
- <input type="checkbox" checked={ state.columns.primary.dataTable || false} onChange={(event) => { editColumn("primary", "dataTable", event.target.checked) }} />
898
- <span className="edit-label">Display in Data Table</span>
899
- </label>
900
- </li>
901
- <li>
902
- <label className="checkbox">
903
- <input type="checkbox" checked={ state.columns.primary.tooltip || false} onChange={(event) => { editColumn("primary", "tooltip", event.target.checked) }} />
904
- <span className="edit-label">Display in Tooltips</span>
905
- </label>
906
- </li>
907
- <li>
908
- <label>
909
- <span className="edit-label">Special Classes</span>
910
- </label>
911
- <ReactTags
912
- placeholder="Separate by comma"
913
- delimiters={[' ',',','Enter']}
914
- allowNew={true}
915
- minQueryLength={1}
916
- tags={specialClasses}
917
- onDelete={(event) => { editColumn("primary", "specialClassDelete", event) }}
918
- onAddition={(value) => { editColumn("primary", "specialClassAdd", value) }}
919
- />
920
- </li>
921
- </ul>
922
- </fieldset>}
923
- <label className="edit-block navigate column-heading">
924
- <span className="edit-label column-heading">Navigation</span>
925
- <select value={state.columns.navigate ? state.columns.navigate.name : columnsOptions[0] } onChange={(event) => { editColumn("navigate", "name", event.target.value) }}>
926
- {columnsOptions}
927
- </select>
928
- </label>
929
- {"navigation" !== state.general.type && additionalColumns.map((val) => (
930
- <fieldset className="edit-block" key={val}>
931
- <button className="remove-column" onClick={(event) => { event.preventDefault(); removeAdditionalColumn(val)}}>Remove</button>
932
- <label>
933
- <span className="edit-label column-heading">Column</span>
934
- <select value={state.columns[val] ? state.columns[val].name : columnsOptions[0] } onChange={(event) => { editColumn(val, "name", event.target.value) }}>
935
- {columnsOptions}
936
- </select>
937
- </label>
938
- <TextField value={columns[val].label} section="columns" subsection={val} fieldName="label" label="Label" updateField={updateField} />
939
- <ul className="column-edit">
940
- <li className="three-col">
941
- <TextField value={columns[val].prefix} section="columns" subsection={val} fieldName="prefix" label="Prefix" updateField={updateField} />
942
- <TextField value={columns[val].suffix} section="columns" subsection={val} fieldName="suffix" label="Suffix" updateField={updateField} />
943
- <TextField type="number" value={columns[val].roundToPlace} section="columns" subsection={val} fieldName="roundToPlace" label="Round" updateField={updateField} />
944
- </li>
945
- <li>
946
- <label className="checkbox">
947
- <input type="checkbox" checked={ state.columns[val].useCommas } onChange={(event) => { editColumn(val, "useCommas", event.target.checked) }} />
948
- <span className="edit-label">Add Commas to Numbers</span>
949
- </label>
950
- </li>
951
- <li>
952
- <label className="checkbox">
953
- <input type="checkbox" checked={ state.columns[val].dataTable } onChange={(event) => { editColumn(val, "dataTable", event.target.checked) }} />
954
- <span className="edit-label">Display in Data Table</span>
955
- </label>
956
- </li>
957
- <li>
958
- <label className="checkbox">
959
- <input type="checkbox" checked={ state.columns[val].tooltip } onChange={(event) => { editColumn(val, "tooltip", event.target.checked) }} />
960
- <span className="edit-label">Display in Tooltips</span>
961
- </label>
962
- </li>
963
- </ul>
964
- </fieldset>
965
- ))}
966
- {"navigation" !== state.general.type && <button className={"btn full-width"} onClick={(event) => {event.preventDefault(); addAdditionalColumn(additionalColumns.length + 1)}}>Add Column</button>}
967
- </AccordionItemPanel>
968
- </AccordionItem> {/* Columns */}
969
- {"navigation" !== state.general.type && <AccordionItem> {/* Legend */}
970
- <AccordionItemHeading>
971
- <AccordionItemButton>
972
- Legend
973
- </AccordionItemButton>
974
- </AccordionItemHeading>
975
- <AccordionItemPanel>
976
- <label>
977
- <span className="edit-label">Legend Type</span>
978
- <select value={legend.type} onChange={(event) => { handleEditorChanges("legendType", event.target.value) }}>
979
- <option value="equalnumber">Equal Number</option>
980
- <option value="equalinterval">Equal Interval</option>
981
- <option value="category">Categorical</option>
982
- </select>
983
- </label>
984
- {"category" !== legend.type && (
985
- <label className="checkbox">
986
- <input type="checkbox"
987
- checked={legend.separateZero || false}
988
- onChange={(event) => handleEditorChanges("separateZero", event.target.checked)}
989
- />
990
- <span className="edit-label">Separate Zero</span>
991
- </label>)}
992
- {"category" !== legend.type &&
993
- <label>
994
- <span className="edit-label">Number of Items</span>
995
- <select value={legend.numberOfItems} onChange={(event) => { handleEditorChanges("legendNumber", event.target.value) }}>
996
- {[...Array(numberOfItemsLimit).keys()].map( (num) => {
997
- return (<option value={num + 1} key={num + 1}>{num + 1}</option>)
998
- })}
999
- </select>
1000
- </label>
1001
- }
1002
- {"category" === legend.type &&
1003
- <React.Fragment>
1004
- <label>
1005
- <span className="edit-label">Category Order</span>
1006
- </label>
1007
- {/* TODO: Swap out this drag and drop library back to something simpler. I had to remove the old one because it hadn't been updated and wouldn't work with Webpack 5. This is overkill for our needs. */}
1008
- <DragDropContext onDragEnd={({source, destination}) => categoryMove(source.index, destination.index)}>
1009
- <Droppable droppableId="category_order">
1010
- {(provided) => (
1011
- <ul
1012
- {...provided.droppableProps}
1013
- className="sort-list"
1014
- ref={provided.innerRef}
1015
- >
1016
- <CategoryList />
1017
- {provided.placeholder}
1018
- </ul>
1019
- )}
1020
- </Droppable>
1021
- </DragDropContext>
1022
- {editorCatOrder.length >= 9 && <section className="error-box my-2"><div><strong className="pt-1">Warning</strong><p>The maximum number of categorical legend items is 9. If your data has more than 9 categories your map will not display properly.</p></div></section>}
1023
- </React.Fragment>
1024
- }
1025
- <TextField value={legend.title} updateField={updateField} section="legend" fieldName="title" label="Legend Title" placeholder="Legend Title" />
1026
- {false === legend.dynamicDescription && (
1027
- <TextField type="textarea" value={legend.description} updateField={updateField} section="legend" fieldName="description" label="Legend Description" />
1028
- )}
1029
- {true === legend.dynamicDescription && (
1030
- <React.Fragment>
1031
- <label>
1032
- <span>Legend Description</span>
1033
- <span className="subtext">For {displayFilterLegendValue( activeFilterValueForDescription )}</span>
1034
- <DynamicDesc value={legend.descriptions[String(activeFilterValueForDescription)]} />
1035
- </label>
1036
- <label>
1037
- <select value={String(activeFilterValueForDescription)} onChange={(event) => { handleEditorChanges("changeActiveFilterValue", event.target.value) }}>
1038
- {filterValueOptionList.map( (arr, i) => {
1039
- return (<option value={arr} key={i}>{displayFilterLegendValue(arr)}</option>)
1040
- })}
1041
- </select>
1042
- </label>
1043
- </React.Fragment>)}
1044
- {filtersJSX.length > 0 && (
1045
- <label className="checkbox">
1046
- <input type="checkbox" checked={ legend.dynamicDescription} onChange={() => { handleEditorChanges("dynamicDescription", filterValueOptionList[0]) }} />
1047
- <span className="edit-label">Dynamic Legend Description</span>
1048
- </label>)}
1049
- {filtersJSX.length > 0 &&
1050
- <label className="checkbox">
1051
- <input type="checkbox"
1052
- checked={legend.unified}
1053
- onChange={(event) => handleEditorChanges("unifiedLegend", event.target.checked)}
1054
- />
1055
- <span className="edit-label">Unified Legend</span>
1056
- </label>
1057
- }
1058
- </AccordionItemPanel>
1059
- </AccordionItem>}
1060
- {"navigation" !== state.general.type && <AccordionItem> {/* Filters */}
1061
- <AccordionItemHeading>
1062
- <AccordionItemButton>
1063
- Filters
1064
- </AccordionItemButton>
1065
- </AccordionItemHeading>
1066
- <AccordionItemPanel>
1067
- {filtersJSX.length > 0 ? filtersJSX : (<p style={{textAlign: "center"}}>There are currently no filters.</p>) }
1068
- <button className={"btn full-width"} onClick={(event) => {event.preventDefault(); changeFilter(null, "addNew")}}>Add Filter</button>
1069
- </AccordionItemPanel>
1070
- </AccordionItem>}
1071
- {"navigation" !== state.general.type && (
1072
- <AccordionItem> {/* Data Table */}
1073
- <AccordionItemHeading>
1074
- <AccordionItemButton>
1075
- Data Table
1076
- </AccordionItemButton>
1077
- </AccordionItemHeading>
1078
- <AccordionItemPanel>
1079
- <TextField value={dataTable.title} updateField={updateField} section="dataTable" fieldName="title" label="Data Table Title" placeholder="Data Table" />
1080
- <TextField value={dataTable.indexTitle} updateField={updateField} section="dataTable" fieldName="indexTitle" label="Index Column Title" placeholder="Location" />
1081
- <label className="checkbox">
1082
- <input type="checkbox" checked={ state.dataTable.forceDisplay !== undefined ? state.dataTable.forceDisplay : !isDashboard } onChange={(event) => { handleEditorChanges("showDataTable", event.target.checked) }} />
1083
- <span className="edit-label">Show Table</span>
1084
- <Helper text="Data tables are required for 508 compliance. When choosing to hide this data table, replace with your own version." />
1085
- </label>
1086
- <label className="checkbox">
1087
- <input type="checkbox" checked={ state.general.expandDataTable || false } onChange={(event) => { handleEditorChanges("expandDataTable", event.target.checked) }} />
1088
- <span className="edit-label">Map loads with data table expanded</span>
1089
- </label>
1090
- <label className="checkbox">
1091
- <input type="checkbox" checked={ state.general.showDownloadButton } onChange={(event) => { handleEditorChanges("toggleDownloadButton", event.target.checked) }} />
1092
- <span className="edit-label">Enable Download CSV Button</span>
1093
- </label>
1094
- </AccordionItemPanel>
1095
- </AccordionItem>)}
1096
- <AccordionItem> {/* Tooltips */}
1097
- <AccordionItemHeading>
1098
- <AccordionItemButton>
1099
- Interactivity
1100
- </AccordionItemButton>
1101
- </AccordionItemHeading>
1102
- <AccordionItemPanel>
1103
- <label>
1104
- <span className="edit-label">Detail displays on <Helper text="At mobile sizes, information always appears in a popover modal when a user taps on an item." /></span>
1105
- <select value={state.tooltips.appearanceType } onChange={(event) => { handleEditorChanges("appearanceType", event.target.value) }}>
1106
- <option value="hover">Hover - Tooltip</option>
1107
- <option value="click">Click - Popover Modal</option>
1108
- </select>
1109
- </label>
1110
- {'click' === state.tooltips.appearanceType &&
1111
- <TextField value={tooltips.linkLabel} section="tooltips" fieldName="linkLabel" label="Tooltips Link Label" updateField={updateField} />
1112
- }
1113
- <label className="checkbox">
1114
- <input type="checkbox" checked={state.tooltips.capitalizeLabels} onChange={(event) => { handleEditorChanges("capitalizeLabels", event.target.checked) }} />
1115
- <span className="edit-label">Capitalize text inside tooltip</span>
1116
- </label>
1117
- </AccordionItemPanel>
1118
- </AccordionItem>
1119
- <AccordionItem> {/* Visual */}
1120
- <AccordionItemHeading>
1121
- <AccordionItemButton>
1122
- Visual
1123
- </AccordionItemButton>
1124
- </AccordionItemHeading>
1125
- <AccordionItemPanel>
1126
- <label className="header">
1127
- <span className="edit-label">Header Theme</span>
1128
- <ul className="color-palette">
1129
- {headerColors.map( (palette) => {
1130
-
1131
- return (
1132
- <li title={ palette } key={ palette } onClick={ () => { handleEditorChanges("headerColor", palette) }} className={ state.general.headerColor === palette ? "selected " + palette : palette}>
1133
- </li>
1134
- )
1135
- })}
1136
- </ul>
1137
- </label>
1138
- <label className="checkbox">
1139
- <input type="checkbox" checked={ state.general.showTitle || false } onChange={(event) => { handleEditorChanges("showTitle", event.target.checked) }} />
1140
- <span className="edit-label">Show Title</span>
1141
- </label>
1142
- {"navigation" !== state.general.type && (
1143
- <label className="checkbox">
1144
- <input type="checkbox" checked={ state.general.showSidebar || false } onChange={(event) => { handleEditorChanges("showSidebar", event.target.checked) }} />
1145
- <span className="edit-label">Show Legend</span>
1146
- </label>)}
1147
- {"navigation" !== state.general.type && (
1148
- <label>
1149
- <span className="edit-label">Legend Position</span>
1150
- <select value={legend.position || false } onChange={(event) => { handleEditorChanges("sidebarPosition", event.target.value) }}>
1151
- <option value="side">Side</option>
1152
- <option value="bottom">Bottom</option>
1153
- </select>
1154
- </label>)}
1155
- {"side" === legend.position && (
1156
- <label className="checkbox">
1157
- <input type="checkbox" checked={ legend.singleColumn} onChange={(event) => { handleEditorChanges("singleColumnLegend", event.target.checked) }} />
1158
- <span className="edit-label">Single Column Legend</span>
1159
- </label>)}
1160
- {"navigation" === state.general.type && (
1161
- <label className="checkbox">
1162
- <input type="checkbox" checked={ state.general.fullBorder || false } onChange={(event) => { handleEditorChanges("fullBorder", event.target.checked) }} />
1163
- <span className="edit-label">Add border around map</span>
1164
- </label>)}
1165
- <label>
1166
- <span className="edit-label">Geo Border Color</span>
1167
- <select value={state.general.geoBorderColor || false } onChange={(event) => { handleEditorChanges("geoBorderColor", event.target.value) }}>
1168
- <option value="darkGray">Dark Gray (Default)</option>
1169
- <option value="sameAsBackground">White</option>
1170
- </select>
1171
- </label>
1172
- <label>
1173
- <span className="edit-label">Map Color Palette</span>
1174
- </label>
1175
- <span className="h5">Quantitative</span>
1176
- <ul className="color-palette">
1177
- {Object.keys(colorPalettes).filter((name) => !name.includes('qualitative')).map( (palette) => {
1178
-
1179
- const colorOne = {
1180
- backgroundColor: colorPalettes[palette][2]
1181
- }
1182
-
1183
- const colorTwo = {
1184
- backgroundColor: colorPalettes[palette][4]
1185
- }
1186
-
1187
- const colorThree = {
1188
- backgroundColor: colorPalettes[palette][6]
1189
- }
1190
-
1191
- return (
1192
- <li title={ palette } key={ palette } onClick={ () => { handleEditorChanges("color", palette) }} className={ state.color === palette ? "selected" : ""}>
1193
- <span style={colorOne}></span>
1194
- <span style={colorTwo}></span>
1195
- <span style={colorThree}></span>
1196
- </li>
1197
- )
1198
- })}
1199
- </ul>
1200
- <span className="h5">Qualitative</span>
1201
- <ul className="color-palette">
1202
- {Object.keys(colorPalettes).filter((name) => name.includes('qualitative')).map( (palette) => {
1203
-
1204
- const colorOne = {
1205
- backgroundColor: colorPalettes[palette][2]
1206
- }
1207
-
1208
- const colorTwo = {
1209
- backgroundColor: colorPalettes[palette][4]
1210
- }
1211
-
1212
- const colorThree = {
1213
- backgroundColor: colorPalettes[palette][6]
1214
- }
1215
-
1216
- return (
1217
- <li title={ palette } key={ palette } onClick={ () => { handleEditorChanges("color", palette) }} className={ state.color === palette ? "selected" : ""}>
1218
- <span style={colorOne}></span>
1219
- <span style={colorTwo}></span>
1220
- <span style={colorThree}></span>
1221
- </li>
1222
- )
1223
- })}
1224
- </ul>
1225
- </AccordionItemPanel>
1226
- </AccordionItem>
1227
- </Accordion>
1228
- </form>
1229
- <a href="https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/data-map.html" target="_blank" rel="noopener noreferrer" className="guidance-link">
1230
- <MapIcon />
1231
- <div>
1232
- <span className="heading-3">Get Maps Help</span>
1233
- <p>Examples and documentation</p>
1234
- </div>
1235
- </a>
1236
- <div className="advanced">
1237
- <span className="advanced-toggle-link" onClick={() => setAdvancedToggle(!advancedToggle)}><span>{advancedToggle ? `— ` : `+ `}</span>Advanced Options</span>
1238
- {advancedToggle && (
1239
- <React.Fragment>
1240
- <section className="error-box py-2 px-3 my-2"><div><strong className="pt-1">Warning</strong><p>This can cause serious errors in your map.</p></div></section>
1241
- <p className="pb-2">This tool displays the actual map configuration <acronym title="JavaScript Object Notation">JSON</acronym> that is generated by this editor and allows you to edit properties directly and apply them.</p>
1242
- <textarea value={ configTextboxValue } onChange={(event) => setConfigTextbox(event.target.value)} />
1243
- <button className="btn full-width" onClick={() => loadConfig( JSON.parse( configTextboxValue ) )}>Apply</button>
1244
- </React.Fragment>
1245
- )}
1246
- </div>
1247
- </section>
1248
- </section>
1249
- </ErrorBoundary>
1250
- )
1251
- }
1259
+ </AccordionItemPanel>
1260
+ </AccordionItem>
1261
+ <AccordionItem>
1262
+ {' '}
1263
+ {/* Columns */}
1264
+ <AccordionItemHeading>
1265
+ <AccordionItemButton>Columns</AccordionItemButton>
1266
+ </AccordionItemHeading>
1267
+ <AccordionItemPanel>
1268
+ <label className='edit-block geo'>
1269
+ <span className='edit-label column-heading'>Geography</span>
1270
+ <select
1271
+ value={state.columns.geo ? state.columns.geo.name : columnsOptions[0]}
1272
+ onChange={(event) => {
1273
+ editColumn('geo', 'name', event.target.value);
1274
+ }}
1275
+ >
1276
+ {columnsOptions}
1277
+ </select>
1278
+ </label>
1279
+ {'navigation' !== state.general.type && (
1280
+ <fieldset className='primary-fieldset edit-block'>
1281
+ <label>
1282
+ <span className='edit-label column-heading'>Primary</span>
1283
+ <select
1284
+ value={
1285
+ state.columns.primary
1286
+ ? state.columns.primary.name
1287
+ : columnsOptions[0]
1288
+ }
1289
+ onChange={(event) => {
1290
+ editColumn('primary', 'name', event.target.value);
1291
+ }}
1292
+ >
1293
+ {columnsOptions}
1294
+ </select>
1295
+ </label>
1296
+ <TextField
1297
+ value={columns.primary.label}
1298
+ section='columns'
1299
+ subsection='primary'
1300
+ fieldName='label'
1301
+ label='Label'
1302
+ updateField={updateField}
1303
+ />
1304
+ <ul className='column-edit'>
1305
+ <li className='three-col'>
1306
+ <TextField
1307
+ value={columns.primary.prefix}
1308
+ section='columns'
1309
+ subsection='primary'
1310
+ fieldName='prefix'
1311
+ label='Prefix'
1312
+ updateField={updateField}
1313
+ />
1314
+ <TextField
1315
+ value={columns.primary.suffix}
1316
+ section='columns'
1317
+ subsection='primary'
1318
+ fieldName='suffix'
1319
+ label='Suffix'
1320
+ updateField={updateField}
1321
+ />
1322
+ <TextField
1323
+ type='number'
1324
+ value={columns.primary.roundToPlace}
1325
+ section='columns'
1326
+ subsection='primary'
1327
+ fieldName='roundToPlace'
1328
+ label='Round'
1329
+ updateField={updateField}
1330
+ />
1331
+ </li>
1332
+ <li>
1333
+ <label className='checkbox'>
1334
+ <input
1335
+ type='checkbox'
1336
+ checked={state.columns.primary.useCommas}
1337
+ onChange={(event) => {
1338
+ editColumn(
1339
+ 'primary',
1340
+ 'useCommas',
1341
+ event.target.checked
1342
+ );
1343
+ }}
1344
+ />
1345
+ <span className='edit-label'>Add Commas to Numbers</span>
1346
+ </label>
1347
+ </li>
1348
+ <li>
1349
+ <label className='checkbox'>
1350
+ <input
1351
+ type='checkbox'
1352
+ checked={state.columns.primary.dataTable || false}
1353
+ onChange={(event) => {
1354
+ editColumn(
1355
+ 'primary',
1356
+ 'dataTable',
1357
+ event.target.checked
1358
+ );
1359
+ }}
1360
+ />
1361
+ <span className='edit-label'>Display in Data Table</span>
1362
+ </label>
1363
+ </li>
1364
+ <li>
1365
+ <label className='checkbox'>
1366
+ <input
1367
+ type='checkbox'
1368
+ checked={state.columns.primary.tooltip || false}
1369
+ onChange={(event) => {
1370
+ editColumn('primary', 'tooltip', event.target.checked);
1371
+ }}
1372
+ />
1373
+ <span className='edit-label'>Display in Tooltips</span>
1374
+ </label>
1375
+ </li>
1376
+ <li>
1377
+ <label>
1378
+ <span className='edit-label'>Special Classes</span>
1379
+ </label>
1380
+ {specialClasses.map((specialClass, i) => (
1381
+ <div className="edit-block" key={`special-class-${i}`}>
1382
+ <button className="remove-column"
1383
+ onClick={(e) => {
1384
+ e.preventDefault();
1385
+ editColumn('primary', 'specialClassDelete', i);
1386
+ }}
1387
+ >Remove</button>
1388
+ <p>Special Class {i + 1}</p>
1389
+ <label>
1390
+ <span className="edit-label column-heading">Data Key</span>
1391
+ <select value={specialClass.key} onChange={(e) => {
1392
+ editColumn('primary', 'specialClassEdit', {prop: 'key', index: i, value: e.target.value});
1393
+ }}>
1394
+ {columnsOptions}
1395
+ </select>
1396
+ </label>
1397
+ <label>
1398
+ <span className="edit-label column-heading">Value</span>
1399
+ <select value={specialClass.value} onChange={(e) => {
1400
+ editColumn('primary', 'specialClassEdit', {prop: 'value', index: i, value: e.target.value});
1401
+ }}>
1402
+ <option value="">- Select Value -</option>
1403
+ {columnsByKey[specialClass.key] && columnsByKey[specialClass.key].sort().map(option => (
1404
+ <option key={`special-class-value-option-${i}-${option}`}>{option}</option>
1405
+ ))}
1406
+ </select>
1407
+ </label>
1408
+ <label>
1409
+ <span className="edit-label column-heading">Label</span>
1410
+ <input type="text" value={specialClass.label} onChange={(e) => {
1411
+ editColumn('primary', 'specialClassEdit', {prop: 'label', index: i, value: e.target.value});
1412
+ }} />
1413
+ </label>
1414
+ </div>
1415
+ ))}
1416
+ <button className="btn full-width"
1417
+ onClick={(e) => {
1418
+ e.preventDefault();
1419
+ editColumn('primary', 'specialClassAdd', {});
1420
+ }}
1421
+ >Add SpecialClass</button>
1422
+ </li>
1423
+ </ul>
1424
+ </fieldset>
1425
+ )}
1426
+ <label className='edit-block navigate column-heading'>
1427
+ <span className='edit-label column-heading'>Navigation</span>
1428
+ <select
1429
+ value={
1430
+ state.columns.navigate ? state.columns.navigate.name : columnsOptions[0]
1431
+ }
1432
+ onChange={(event) => {
1433
+ editColumn('navigate', 'name', event.target.value);
1434
+ }}
1435
+ >
1436
+ {columnsOptions}
1437
+ </select>
1438
+ </label>
1439
+ {'navigation' !== state.general.type &&
1440
+ additionalColumns.map((val) => (
1441
+ <fieldset className='edit-block' key={val}>
1442
+ <button
1443
+ className='remove-column'
1444
+ onClick={(event) => {
1445
+ event.preventDefault();
1446
+ removeAdditionalColumn(val);
1447
+ }}
1448
+ >
1449
+ Remove
1450
+ </button>
1451
+ <label>
1452
+ <span className='edit-label column-heading'>Column</span>
1453
+ <select
1454
+ value={
1455
+ state.columns[val]
1456
+ ? state.columns[val].name
1457
+ : columnsOptions[0]
1458
+ }
1459
+ onChange={(event) => {
1460
+ editColumn(val, 'name', event.target.value);
1461
+ }}
1462
+ >
1463
+ {columnsOptions}
1464
+ </select>
1465
+ </label>
1466
+ <TextField
1467
+ value={columns[val].label}
1468
+ section='columns'
1469
+ subsection={val}
1470
+ fieldName='label'
1471
+ label='Label'
1472
+ updateField={updateField}
1473
+ />
1474
+ <ul className='column-edit'>
1475
+ <li className='three-col'>
1476
+ <TextField
1477
+ value={columns[val].prefix}
1478
+ section='columns'
1479
+ subsection={val}
1480
+ fieldName='prefix'
1481
+ label='Prefix'
1482
+ updateField={updateField}
1483
+ />
1484
+ <TextField
1485
+ value={columns[val].suffix}
1486
+ section='columns'
1487
+ subsection={val}
1488
+ fieldName='suffix'
1489
+ label='Suffix'
1490
+ updateField={updateField}
1491
+ />
1492
+ <TextField
1493
+ type='number'
1494
+ value={columns[val].roundToPlace}
1495
+ section='columns'
1496
+ subsection={val}
1497
+ fieldName='roundToPlace'
1498
+ label='Round'
1499
+ updateField={updateField}
1500
+ />
1501
+ </li>
1502
+ <li>
1503
+ <label className='checkbox'>
1504
+ <input
1505
+ type='checkbox'
1506
+ checked={state.columns[val].useCommas}
1507
+ onChange={(event) => {
1508
+ editColumn(val, 'useCommas', event.target.checked);
1509
+ }}
1510
+ />
1511
+ <span className='edit-label'>Add Commas to Numbers</span>
1512
+ </label>
1513
+ </li>
1514
+ <li>
1515
+ <label className='checkbox'>
1516
+ <input
1517
+ type='checkbox'
1518
+ checked={state.columns[val].dataTable}
1519
+ onChange={(event) => {
1520
+ editColumn(val, 'dataTable', event.target.checked);
1521
+ }}
1522
+ />
1523
+ <span className='edit-label'>Display in Data Table</span>
1524
+ </label>
1525
+ </li>
1526
+ <li>
1527
+ <label className='checkbox'>
1528
+ <input
1529
+ type='checkbox'
1530
+ checked={state.columns[val].tooltip}
1531
+ onChange={(event) => {
1532
+ editColumn(val, 'tooltip', event.target.checked);
1533
+ }}
1534
+ />
1535
+ <span className='edit-label'>Display in Tooltips</span>
1536
+ </label>
1537
+ </li>
1538
+ </ul>
1539
+ </fieldset>
1540
+ ))}
1541
+ {'navigation' !== state.general.type && (
1542
+ <button
1543
+ className={'btn full-width'}
1544
+ onClick={(event) => {
1545
+ event.preventDefault();
1546
+ addAdditionalColumn(additionalColumns.length + 1);
1547
+ }}
1548
+ >
1549
+ Add Column
1550
+ </button>
1551
+ )}
1552
+ </AccordionItemPanel>
1553
+ </AccordionItem>{' '}
1554
+ {/* Columns */}
1555
+ {'navigation' !== state.general.type && (
1556
+ <AccordionItem>
1557
+ {' '}
1558
+ {/* Legend */}
1559
+ <AccordionItemHeading>
1560
+ <AccordionItemButton>Legend</AccordionItemButton>
1561
+ </AccordionItemHeading>
1562
+ <AccordionItemPanel>
1563
+ <label>
1564
+ <span className='edit-label'>Legend Type</span>
1565
+ <select
1566
+ value={legend.type}
1567
+ onChange={(event) => {
1568
+ handleEditorChanges('legendType', event.target.value);
1569
+ }}
1570
+ >
1571
+ <option value='equalnumber'>Equal Number</option>
1572
+ <option value='equalinterval'>Equal Interval</option>
1573
+ <option value='category'>Categorical</option>
1574
+ </select>
1575
+ </label>
1576
+ {'category' !== legend.type && (
1577
+ <label className='checkbox'>
1578
+ <input
1579
+ type='checkbox'
1580
+ checked={legend.separateZero || false}
1581
+ onChange={(event) =>
1582
+ handleEditorChanges('separateZero', event.target.checked)
1583
+ }
1584
+ />
1585
+ <span className='edit-label'>Separate Zero</span>
1586
+ </label>
1587
+ )}
1588
+ {'category' !== legend.type && (
1589
+ <label>
1590
+ <span className='edit-label'>Number of Items</span>
1591
+ <select
1592
+ value={legend.numberOfItems}
1593
+ onChange={(event) => {
1594
+ handleEditorChanges('legendNumber', event.target.value);
1595
+ }}
1596
+ >
1597
+ {[...Array(numberOfItemsLimit).keys()].map((num) => {
1598
+ return (
1599
+ <option value={num + 1} key={num + 1}>
1600
+ {num + 1}
1601
+ </option>
1602
+ );
1603
+ })}
1604
+ </select>
1605
+ </label>
1606
+ )}
1607
+ {'category' === legend.type && (
1608
+ <React.Fragment>
1609
+ <label>
1610
+ <span className='edit-label'>Category Order</span>
1611
+ </label>
1612
+ {/* TODO: Swap out this drag and drop library back to something simpler. I had to remove the old one because it hadn't been updated and wouldn't work with Webpack 5. This is overkill for our needs. */}
1613
+ <DragDropContext
1614
+ onDragEnd={({ source, destination }) =>
1615
+ categoryMove(source.index, destination.index)
1616
+ }
1617
+ >
1618
+ <Droppable droppableId='category_order'>
1619
+ {(provided) => (
1620
+ <ul
1621
+ {...provided.droppableProps}
1622
+ className='sort-list'
1623
+ ref={provided.innerRef}
1624
+ >
1625
+ <CategoryList />
1626
+ {provided.placeholder}
1627
+ </ul>
1628
+ )}
1629
+ </Droppable>
1630
+ </DragDropContext>
1631
+ {editorCatOrder.length >= 9 && (
1632
+ <section className='error-box my-2'>
1633
+ <div>
1634
+ <strong className='pt-1'>Warning</strong>
1635
+ <p>
1636
+ The maximum number of categorical legend items is 9. If
1637
+ your data has more than 9 categories your map will not
1638
+ display properly.
1639
+ </p>
1640
+ </div>
1641
+ </section>
1642
+ )}
1643
+ </React.Fragment>
1644
+ )}
1645
+ <TextField
1646
+ value={legend.title}
1647
+ updateField={updateField}
1648
+ section='legend'
1649
+ fieldName='title'
1650
+ label='Legend Title'
1651
+ placeholder='Legend Title'
1652
+ />
1653
+ {false === legend.dynamicDescription && (
1654
+ <TextField
1655
+ type='textarea'
1656
+ value={legend.description}
1657
+ updateField={updateField}
1658
+ section='legend'
1659
+ fieldName='description'
1660
+ label='Legend Description'
1661
+ />
1662
+ )}
1663
+ {true === legend.dynamicDescription && (
1664
+ <React.Fragment>
1665
+ <label>
1666
+ <span>Legend Description</span>
1667
+ <span className='subtext'>
1668
+ For {displayFilterLegendValue(activeFilterValueForDescription)}
1669
+ </span>
1670
+ <DynamicDesc
1671
+ value={
1672
+ legend.descriptions[String(activeFilterValueForDescription)]
1673
+ }
1674
+ />
1675
+ </label>
1676
+ <label>
1677
+ <select
1678
+ value={String(activeFilterValueForDescription)}
1679
+ onChange={(event) => {
1680
+ handleEditorChanges(
1681
+ 'changeActiveFilterValue',
1682
+ event.target.value
1683
+ );
1684
+ }}
1685
+ >
1686
+ {filterValueOptionList.map((arr, i) => {
1687
+ return (
1688
+ <option value={arr} key={i}>
1689
+ {displayFilterLegendValue(arr)}
1690
+ </option>
1691
+ );
1692
+ })}
1693
+ </select>
1694
+ </label>
1695
+ </React.Fragment>
1696
+ )}
1697
+ {filtersJSX.length > 0 && (
1698
+ <label className='checkbox'>
1699
+ <input
1700
+ type='checkbox'
1701
+ checked={legend.dynamicDescription}
1702
+ onChange={() => {
1703
+ handleEditorChanges(
1704
+ 'dynamicDescription',
1705
+ filterValueOptionList[0]
1706
+ );
1707
+ }}
1708
+ />
1709
+ <span className='edit-label'>Dynamic Legend Description</span>
1710
+ </label>
1711
+ )}
1712
+ {filtersJSX.length > 0 && (
1713
+ <label className='checkbox'>
1714
+ <input
1715
+ type='checkbox'
1716
+ checked={legend.unified}
1717
+ onChange={(event) =>
1718
+ handleEditorChanges('unifiedLegend', event.target.checked)
1719
+ }
1720
+ />
1721
+ <span className='edit-label'>Unified Legend</span>
1722
+ </label>
1723
+ )}
1724
+ </AccordionItemPanel>
1725
+ </AccordionItem>
1726
+ )}
1727
+ {'navigation' !== state.general.type && (
1728
+ <AccordionItem>
1729
+ {' '}
1730
+ {/* Filters */}
1731
+ <AccordionItemHeading>
1732
+ <AccordionItemButton>Filters</AccordionItemButton>
1733
+ </AccordionItemHeading>
1734
+ <AccordionItemPanel>
1735
+ {filtersJSX.length > 0 ? (
1736
+ filtersJSX
1737
+ ) : (
1738
+ <p style={{ textAlign: 'center' }}>There are currently no filters.</p>
1739
+ )}
1740
+ <button
1741
+ className={'btn full-width'}
1742
+ onClick={(event) => {
1743
+ event.preventDefault();
1744
+ changeFilter(null, 'addNew');
1745
+ }}
1746
+ >
1747
+ Add Filter
1748
+ </button>
1749
+ </AccordionItemPanel>
1750
+ </AccordionItem>
1751
+ )}
1752
+ {'navigation' !== state.general.type && (
1753
+ <AccordionItem>
1754
+ {' '}
1755
+ {/* Data Table */}
1756
+ <AccordionItemHeading>
1757
+ <AccordionItemButton>Data Table</AccordionItemButton>
1758
+ </AccordionItemHeading>
1759
+ <AccordionItemPanel>
1760
+ <TextField
1761
+ value={dataTable.title}
1762
+ updateField={updateField}
1763
+ section='dataTable'
1764
+ fieldName='title'
1765
+ label='Data Table Title'
1766
+ placeholder='Data Table'
1767
+ />
1768
+ <TextField
1769
+ value={dataTable.indexTitle}
1770
+ updateField={updateField}
1771
+ section='dataTable'
1772
+ fieldName='indexTitle'
1773
+ label='Index Column Title'
1774
+ placeholder='Location'
1775
+ />
1776
+ <TextField
1777
+ value={dataTable.caption}
1778
+ updateField={updateField}
1779
+ section='dataTable'
1780
+ fieldName='caption'
1781
+ label='Data Table Caption'
1782
+ placeholder='Data Table'
1783
+ helper='Text that describes the data table for screen'
1784
+ type="textarea"
1785
+ />
1786
+ <label className='checkbox'>
1787
+ <input
1788
+ type='checkbox'
1789
+ checked={
1790
+ state.dataTable.forceDisplay !== undefined
1791
+ ? state.dataTable.forceDisplay
1792
+ : !isDashboard
1793
+ }
1794
+ onChange={(event) => {
1795
+ handleEditorChanges('showDataTable', event.target.checked);
1796
+ }}
1797
+ />
1798
+ <span className='edit-label'>Show Table</span>
1799
+ <Helper text='Data tables are required for 508 compliance. When choosing to hide this data table, replace with your own version.' />
1800
+ </label>
1801
+ <label className='checkbox'>
1802
+ <input
1803
+ type='checkbox'
1804
+ checked={ state.dataTable.limitHeight }
1805
+ onChange={(event) => {
1806
+ handleEditorChanges('limitDataTableHeight', event.target.checked);
1807
+ }}
1808
+ />
1809
+ <span className='edit-label'>Limit Table Height</span>
1810
+ </label>
1811
+ {state.dataTable.limitHeight &&
1812
+ <TextField
1813
+ value={dataTable.height}
1814
+ updateField={updateField}
1815
+ section='dataTable'
1816
+ fieldName='height'
1817
+ label='Data Table Height'
1818
+ placeholder='Height(px)'
1819
+ type="number"
1820
+ min="0"
1821
+ max="500"
1822
+ />
1823
+ }
1824
+ <label className='checkbox'>
1825
+ <input
1826
+ type='checkbox'
1827
+ checked={state.general.expandDataTable || false}
1828
+ onChange={(event) => {
1829
+ handleEditorChanges('expandDataTable', event.target.checked);
1830
+ }}
1831
+ />
1832
+ <span className='edit-label'>Map loads with data table expanded</span>
1833
+ </label>
1834
+ <label className='checkbox'>
1835
+ <input
1836
+ type='checkbox'
1837
+ checked={state.general.showDownloadButton}
1838
+ onChange={(event) => {
1839
+ handleEditorChanges('toggleDownloadButton', event.target.checked);
1840
+ }}
1841
+ />
1842
+ <span className='edit-label'>Enable Download CSV Button</span>
1843
+ </label>
1844
+ </AccordionItemPanel>
1845
+ </AccordionItem>
1846
+ )}
1847
+ <AccordionItem>
1848
+ {' '}
1849
+ {/* Tooltips */}
1850
+ <AccordionItemHeading>
1851
+ <AccordionItemButton>Interactivity</AccordionItemButton>
1852
+ </AccordionItemHeading>
1853
+ <AccordionItemPanel>
1854
+ <label>
1855
+ <span className='edit-label'>
1856
+ Detail displays on{' '}
1857
+ <Helper text='At mobile sizes, information always appears in a popover modal when a user taps on an item.' />
1858
+ </span>
1859
+ <select
1860
+ value={state.tooltips.appearanceType}
1861
+ onChange={(event) => {
1862
+ handleEditorChanges('appearanceType', event.target.value);
1863
+ }}
1864
+ >
1865
+ <option value='hover'>Hover - Tooltip</option>
1866
+ <option value='click'>Click - Popover Modal</option>
1867
+ </select>
1868
+ </label>
1869
+ {'click' === state.tooltips.appearanceType && (
1870
+ <TextField
1871
+ value={tooltips.linkLabel}
1872
+ section='tooltips'
1873
+ fieldName='linkLabel'
1874
+ label='Tooltips Link Label'
1875
+ updateField={updateField}
1876
+ />
1877
+ )}
1878
+ <label className='checkbox'>
1879
+ <input
1880
+ type='checkbox'
1881
+ checked={state.tooltips.capitalizeLabels}
1882
+ onChange={(event) => {
1883
+ handleEditorChanges('capitalizeLabels', event.target.checked);
1884
+ }}
1885
+ />
1886
+ <span className='edit-label'>Capitalize text inside tooltip</span>
1887
+ </label>
1888
+ </AccordionItemPanel>
1889
+ </AccordionItem>
1890
+ <AccordionItem>
1891
+ {' '}
1892
+ {/* Visual */}
1893
+ <AccordionItemHeading>
1894
+ <AccordionItemButton>Visual</AccordionItemButton>
1895
+ </AccordionItemHeading>
1896
+ <AccordionItemPanel>
1897
+ <label className='header'>
1898
+ <span className='edit-label'>Header Theme</span>
1899
+ <ul className='color-palette'>
1900
+ {headerColors.map((palette) => {
1901
+ return (
1902
+ <li
1903
+ title={palette}
1904
+ key={palette}
1905
+ onClick={() => {
1906
+ handleEditorChanges('headerColor', palette);
1907
+ }}
1908
+ className={
1909
+ state.general.headerColor === palette
1910
+ ? 'selected ' + palette
1911
+ : palette
1912
+ }
1913
+ ></li>
1914
+ );
1915
+ })}
1916
+ </ul>
1917
+ </label>
1918
+ <label className='checkbox'>
1919
+ <input
1920
+ type='checkbox'
1921
+ checked={state.general.showTitle || false}
1922
+ onChange={(event) => {
1923
+ handleEditorChanges('showTitle', event.target.checked);
1924
+ }}
1925
+ />
1926
+ <span className='edit-label'>Show Title</span>
1927
+ </label>
1928
+ {'navigation' !== state.general.type && (
1929
+ <label className='checkbox'>
1930
+ <input
1931
+ type='checkbox'
1932
+ checked={state.general.showSidebar || false}
1933
+ onChange={(event) => {
1934
+ handleEditorChanges('showSidebar', event.target.checked);
1935
+ }}
1936
+ />
1937
+ <span className='edit-label'>Show Legend</span>
1938
+ </label>
1939
+ )}
1940
+ {'navigation' !== state.general.type && (
1941
+ <label>
1942
+ <span className='edit-label'>Legend Position</span>
1943
+ <select
1944
+ value={legend.position || false}
1945
+ onChange={(event) => {
1946
+ handleEditorChanges('sidebarPosition', event.target.value);
1947
+ }}
1948
+ >
1949
+ <option value='side'>Side</option>
1950
+ <option value='bottom'>Bottom</option>
1951
+ </select>
1952
+ </label>
1953
+ )}
1954
+ {'side' === legend.position && (
1955
+ <label className='checkbox'>
1956
+ <input
1957
+ type='checkbox'
1958
+ checked={legend.singleColumn}
1959
+ onChange={(event) => {
1960
+ handleEditorChanges('singleColumnLegend', event.target.checked);
1961
+ }}
1962
+ />
1963
+ <span className='edit-label'>Single Column Legend</span>
1964
+ </label>
1965
+ )}
1966
+ {'navigation' === state.general.type && (
1967
+ <label className='checkbox'>
1968
+ <input
1969
+ type='checkbox'
1970
+ checked={state.general.fullBorder || false}
1971
+ onChange={(event) => {
1972
+ handleEditorChanges('fullBorder', event.target.checked);
1973
+ }}
1974
+ />
1975
+ <span className='edit-label'>Add border around map</span>
1976
+ </label>
1977
+ )}
1978
+ <label>
1979
+ <span className='edit-label'>Geo Border Color</span>
1980
+ <select
1981
+ value={state.general.geoBorderColor || false}
1982
+ onChange={(event) => {
1983
+ handleEditorChanges('geoBorderColor', event.target.value);
1984
+ }}
1985
+ >
1986
+ <option value='darkGray'>Dark Gray (Default)</option>
1987
+ <option value='sameAsBackground'>White</option>
1988
+ </select>
1989
+ </label>
1990
+ <label>
1991
+ <span className='edit-label'>Map Color Palette</span>
1992
+ </label>
1993
+ <span className='h5'>Sequential</span>
1994
+ <ul className='color-palette'>
1995
+ {Object.keys(colorPalettes)
1996
+ .filter((name) => !name.includes('qualitative'))
1997
+ .map((palette) => {
1998
+ const colorOne = {
1999
+ backgroundColor: colorPalettes[palette][2],
2000
+ };
2001
+
2002
+ const colorTwo = {
2003
+ backgroundColor: colorPalettes[palette][4],
2004
+ };
2005
+
2006
+ const colorThree = {
2007
+ backgroundColor: colorPalettes[palette][6],
2008
+ };
2009
+
2010
+ return (
2011
+ <li
2012
+ title={palette}
2013
+ key={palette}
2014
+ onClick={() => {
2015
+ handleEditorChanges('color', palette);
2016
+ }}
2017
+ className={state.color === palette ? 'selected' : ''}
2018
+ >
2019
+ <span style={colorOne}></span>
2020
+ <span style={colorTwo}></span>
2021
+ <span style={colorThree}></span>
2022
+ </li>
2023
+ );
2024
+ })}
2025
+ </ul>
2026
+ <span className='h5'>Non-Sequential</span>
2027
+ <ul className='color-palette'>
2028
+ {Object.keys(colorPalettes)
2029
+ .filter((name) => name.includes('qualitative'))
2030
+ .map((palette) => {
2031
+ const colorOne = {
2032
+ backgroundColor: colorPalettes[palette][2],
2033
+ };
2034
+
2035
+ const colorTwo = {
2036
+ backgroundColor: colorPalettes[palette][4],
2037
+ };
2038
+
2039
+ const colorThree = {
2040
+ backgroundColor: colorPalettes[palette][6],
2041
+ };
2042
+
2043
+ return (
2044
+ <li
2045
+ title={palette}
2046
+ key={palette}
2047
+ onClick={() => {
2048
+ handleEditorChanges('color', palette);
2049
+ }}
2050
+ className={state.color === palette ? 'selected' : ''}
2051
+ >
2052
+ <span style={colorOne}></span>
2053
+ <span style={colorTwo}></span>
2054
+ <span style={colorThree}></span>
2055
+ </li>
2056
+ );
2057
+ })}
2058
+ </ul>
2059
+ </AccordionItemPanel>
2060
+ </AccordionItem>
2061
+ </Accordion>
2062
+ </form>
2063
+ <a
2064
+ href='https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/data-map.html'
2065
+ target='_blank'
2066
+ rel='noopener noreferrer'
2067
+ className='guidance-link'
2068
+ >
2069
+ <MapIcon />
2070
+ <div>
2071
+ <span className='heading-3'>Get Maps Help</span>
2072
+ <p>Examples and documentation</p>
2073
+ </div>
2074
+ </a>
2075
+ <div className='advanced'>
2076
+ <span className='advanced-toggle-link' onClick={() => setAdvancedToggle(!advancedToggle)}>
2077
+ <span>{advancedToggle ? `— ` : `+ `}</span>Advanced Options
2078
+ </span>
2079
+ {advancedToggle && (
2080
+ <React.Fragment>
2081
+ <section className='error-box py-2 px-3 my-2'>
2082
+ <div>
2083
+ <strong className='pt-1'>Warning</strong>
2084
+ <p>This can cause serious errors in your map.</p>
2085
+ </div>
2086
+ </section>
2087
+ <p className='pb-2'>
2088
+ This tool displays the actual map configuration{' '}
2089
+ <acronym title='JavaScript Object Notation'>JSON</acronym> that is generated by this
2090
+ editor and allows you to edit properties directly and apply them.
2091
+ </p>
2092
+ <textarea
2093
+ value={configTextboxValue}
2094
+ onChange={(event) => setConfigTextbox(event.target.value)}
2095
+ />
2096
+ <button
2097
+ className='btn full-width'
2098
+ onClick={() => loadConfig(JSON.parse(configTextboxValue))}
2099
+ >
2100
+ Apply
2101
+ </button>
2102
+ </React.Fragment>
2103
+ )}
2104
+ </div>
2105
+ </section>
2106
+ </section>
2107
+ </ErrorBoundary>
2108
+ );
2109
+ };
1252
2110
 
1253
2111
  export default EditorPanel;