@cdc/map 4.26.3 → 4.26.5

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 (105) hide show
  1. package/CONFIG.md +268 -0
  2. package/README.md +74 -24
  3. package/dist/cdcmap-CY9IcPSi.es.js +6 -0
  4. package/dist/cdcmap-DlpiY3fQ.es.js +4 -0
  5. package/dist/cdcmap.js +29168 -27482
  6. package/examples/{testing-layer-2.json → __data__/testing-layer-2.json} +1 -1
  7. package/examples/{testing-layer.json → __data__/testing-layer.json} +1 -1
  8. package/examples/county-hsa-toggle.json +51993 -0
  9. package/examples/custom-map-layers.json +2 -2
  10. package/examples/default-county.json +6 -3
  11. package/examples/minimal-example.json +73 -0
  12. package/examples/private/annotation-bug.json +2 -2
  13. package/examples/private/css-issue.json +314 -0
  14. package/examples/private/region-breaking.json +1639 -0
  15. package/examples/private/test1.json +27247 -0
  16. package/package.json +4 -4
  17. package/src/CdcMapComponent.tsx +107 -14
  18. package/src/_stories/CdcMap.AltText.stories.tsx +122 -0
  19. package/src/_stories/CdcMap.Editor.ColumnsSectionTests.stories.tsx +600 -0
  20. package/src/_stories/CdcMap.Editor.DataTableSectionTests.stories.tsx +404 -0
  21. package/src/_stories/CdcMap.Editor.FiltersSectionTests.stories.tsx +229 -0
  22. package/src/_stories/CdcMap.Editor.GeneralSectionTests.stories.tsx +262 -0
  23. package/src/_stories/CdcMap.Editor.LegendSectionTests.stories.tsx +541 -0
  24. package/src/_stories/CdcMap.Editor.MultiCountryWorldMapTests.stories.tsx +359 -0
  25. package/src/_stories/CdcMap.Editor.PatternSettingsSectionTests.stories.tsx +516 -0
  26. package/src/_stories/CdcMap.Editor.SmallMultiplesSectionTests.stories.tsx +165 -0
  27. package/src/_stories/CdcMap.Editor.TextAnnotationsSectionTests.stories.tsx +145 -0
  28. package/src/_stories/CdcMap.Editor.TypeSectionTests.stories.tsx +312 -0
  29. package/src/_stories/CdcMap.Editor.VisualSectionTests.stories.tsx +359 -0
  30. package/src/_stories/CdcMap.Editor.ZoomControlsTests.stories.tsx +88 -0
  31. package/src/_stories/CdcMap.FocusVisibility.stories.tsx +87 -0
  32. package/src/_stories/CdcMap.HiddenMount.stories.tsx +69 -0
  33. package/src/_stories/CdcMap.ResetBehavior.stories.tsx +32 -0
  34. package/src/_stories/CdcMap.Zoom.stories.tsx +111 -0
  35. package/src/_stories/{CdcMap.stories.tsx → CdcMap.smoke.stories.tsx} +60 -0
  36. package/src/_stories/_mock/alt_text_metadata.json +65 -0
  37. package/src/_stories/_mock/legends/legend-tests.json +3 -3
  38. package/src/_stories/_mock/world-bubble-reset.json +138 -0
  39. package/src/_stories/_mock/world-data-zoom-filters.json +166 -0
  40. package/src/components/Annotation/AnnotationList.tsx +1 -1
  41. package/src/components/BubbleList.tsx +13 -0
  42. package/src/components/EditorPanel/components/EditorPanel.tsx +637 -382
  43. package/src/components/EditorPanel/components/HexShapeSettings.tsx +1 -1
  44. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +112 -117
  45. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +26 -13
  46. package/src/components/EditorPanel/components/editorPanel.styles.css +22 -2
  47. package/src/components/FilterControls.tsx +21 -0
  48. package/src/components/Legend/components/Legend.tsx +3 -3
  49. package/src/components/Legend/components/LegendItem.Hex.tsx +4 -2
  50. package/src/components/SmallMultiples/SmallMultiples.tsx +2 -2
  51. package/src/components/SmallMultiples/SynchronizedTooltip.tsx +1 -1
  52. package/src/components/UsaMap/components/UsaMap.County.tsx +309 -108
  53. package/src/components/UsaMap/components/UsaMap.Region.tsx +5 -2
  54. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +33 -10
  55. package/src/components/UsaMap/components/UsaMap.State.tsx +10 -3
  56. package/src/components/UsaMap/data/cb_2019_us_county_20m.json +75817 -1
  57. package/src/components/UsaMap/data/hsa_fips_mapping.json +3144 -0
  58. package/src/components/WorldMap/WorldMap.tsx +37 -4
  59. package/src/components/WorldMap/data/world-topo.json +1 -1
  60. package/src/components/ZoomableGroup.tsx +23 -3
  61. package/src/components/filterControls.styles.css +6 -0
  62. package/src/data/initial-state.js +3 -0
  63. package/src/data/supported-counties.json +1 -1
  64. package/src/helpers/countyTerritories.ts +38 -0
  65. package/src/helpers/dataTableHelpers.ts +35 -6
  66. package/src/helpers/generateRuntimeFilters.ts +2 -1
  67. package/src/helpers/handleMapAriaLabels.ts +45 -30
  68. package/src/helpers/shouldAutoResetSingleStateZoom.ts +22 -0
  69. package/src/helpers/tests/countyTerritories.test.ts +87 -0
  70. package/src/helpers/tests/handleMapAriaLabels.test.ts +71 -0
  71. package/src/helpers/tests/shouldAutoResetSingleStateZoom.test.ts +71 -0
  72. package/src/hooks/useApplyTooltipsToGeo.tsx +7 -4
  73. package/src/hooks/useGeoClickHandler.ts +13 -1
  74. package/src/hooks/useMapLayers.tsx +1 -1
  75. package/src/hooks/useStateZoom.tsx +39 -20
  76. package/src/hooks/useTooltip.test.tsx +2 -16
  77. package/src/hooks/useTooltip.ts +18 -7
  78. package/src/index.jsx +5 -2
  79. package/src/scss/main.scss +6 -21
  80. package/src/scss/map.scss +20 -0
  81. package/src/store/map.actions.ts +5 -2
  82. package/src/store/map.reducer.ts +12 -3
  83. package/src/test/CdcMap.test.jsx +24 -0
  84. package/src/types/MapConfig.ts +11 -0
  85. package/src/types/MapContext.ts +6 -1
  86. package/topojson-updater/README.txt +1 -1
  87. package/dist/cdcmap-vr9HZwRt.es.js +0 -6
  88. package/examples/__data__/city-state-data.json +0 -668
  89. package/examples/city-state.json +0 -434
  90. package/examples/default-world-data.json +0 -1450
  91. package/examples/new-cities.json +0 -656
  92. package/src/_stories/CdcMap.Editor.stories.tsx +0 -3648
  93. package/topojson-updater/package-lock.json +0 -223
  94. /package/src/_stories/{CdcMap.ColumnWrap.stories.tsx → CdcMap.ColumnWrap.smoke.stories.tsx} +0 -0
  95. /package/src/_stories/{CdcMap.Defaults.stories.tsx → CdcMap.Defaults.smoke.stories.tsx} +0 -0
  96. /package/src/_stories/{CdcMap.DistrictOfColumbia.stories.tsx → CdcMap.DistrictOfColumbia.smoke.stories.tsx} +0 -0
  97. /package/src/_stories/{CdcMap.Filters.stories.tsx → CdcMap.Filters.smoke.stories.tsx} +0 -0
  98. /package/src/_stories/{CdcMap.Legend.Gradient.stories.tsx → CdcMap.Legend.Gradient.smoke.stories.tsx} +0 -0
  99. /package/src/_stories/{CdcMap.Legend.stories.tsx → CdcMap.Legend.smoke.stories.tsx} +0 -0
  100. /package/src/_stories/{CdcMap.Patterns.stories.tsx → CdcMap.Patterns.smoke.stories.tsx} +0 -0
  101. /package/src/_stories/{CdcMap.SmallMultiples.stories.tsx → CdcMap.SmallMultiples.smoke.stories.tsx} +0 -0
  102. /package/src/_stories/{CdcMap.Table.stories.tsx → CdcMap.Table.smoke.stories.tsx} +0 -0
  103. /package/src/_stories/{CdcMap.ZeroColor.stories.tsx → CdcMap.ZeroColor.smoke.stories.tsx} +0 -0
  104. /package/src/_stories/{GoogleMap.stories.tsx → GoogleMap.smoke.stories.tsx} +0 -0
  105. /package/src/_stories/{UsaMap.NoData.stories.tsx → UsaMap.NoData.smoke.stories.tsx} +0 -0
@@ -1,6 +1,7 @@
1
1
  import React, { useContext, useEffect, useState, useMemo, useRef } from 'react'
2
2
  import { filterColorPalettes } from '@cdc/core/helpers/filterColorPalettes'
3
3
  import { cloneConfig } from '@cdc/core/helpers/cloneConfig'
4
+ import { resolveAltTextDescription } from '@cdc/core/helpers/resolveAltTextDescription'
4
5
 
5
6
  // Third Party
6
7
  import {
@@ -29,6 +30,7 @@ import { EditorPanel as BaseEditorPanel } from '@cdc/core/components/EditorPanel
29
30
  import AdvancedEditor from '@cdc/core/components/AdvancedEditor'
30
31
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
31
32
  import Icon from '@cdc/core/components/ui/Icon'
33
+ import GroupedList from '@cdc/core/components/EditorPanel/GroupedList'
32
34
  import InputToggle from '@cdc/core/components/inputs/InputToggle'
33
35
  import Tooltip from '@cdc/core/components/ui/Tooltip'
34
36
  import VizFilterEditor from '@cdc/core/components/EditorPanel/VizFilterEditor'
@@ -50,6 +52,8 @@ import { MapContext } from '../../../types/MapContext.js'
50
52
  import Alert from '@cdc/core/components/Alert'
51
53
  import { updateFieldFactory } from '@cdc/core/helpers/updateFieldFactory'
52
54
  import { CheckBox, Select, TextField } from '@cdc/core/components/EditorPanel/Inputs'
55
+ import DownloadUrlControls from '@cdc/core/components/EditorPanel/DownloadUrlControls'
56
+ import Button from '@cdc/core/components/elements/Button'
53
57
  import StyleTreatmentSection from '@cdc/core/components/EditorPanel/sections/StyleTreatmentSection'
54
58
  import { HeaderThemeSelector } from '@cdc/core/components/HeaderThemeSelector'
55
59
  import useColumnsRequiredChecker from '../../../hooks/useColumnsRequiredChecker'
@@ -1243,7 +1247,11 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1243
1247
  ))
1244
1248
  }
1245
1249
 
1246
- const isLoadedFromUrl = config?.dataKey?.includes('http://') || config?.dataKey?.includes('https://')
1250
+ const isLoadedFromUrl =
1251
+ config?.dataFileSourceType === 'url' ||
1252
+ Boolean(config?.runtimeDataUrl || config?.dataUrl || config?.dataFileName) ||
1253
+ config?.dataKey?.includes('http://') ||
1254
+ config?.dataKey?.includes('https://')
1247
1255
 
1248
1256
  // Custom convertStateToConfig for map with map-specific logic
1249
1257
  const customConvertStateToConfig = () => {
@@ -1362,6 +1370,41 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1362
1370
  }}
1363
1371
  />
1364
1372
  )}
1373
+ {config.general.geoType === 'us-county' && (
1374
+ <>
1375
+ <CheckBox
1376
+ value={config.general.showNeighboringStates || false}
1377
+ fieldName='showNeighboringStates'
1378
+ label="Show Neighboring States' Data"
1379
+ updateField={updateField}
1380
+ section='general'
1381
+ />
1382
+ <CheckBox
1383
+ value={config.general.showHSABoundaries || false}
1384
+ fieldName='showHSABoundaries'
1385
+ label='Show HSA Boundaries'
1386
+ updateField={updateField}
1387
+ section='general'
1388
+ tooltip={
1389
+ <Tooltip style={{ textTransform: 'none' }}>
1390
+ <Tooltip.Target>
1391
+ <Icon
1392
+ display='question'
1393
+ style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }}
1394
+ />
1395
+ </Tooltip.Target>
1396
+ <Tooltip.Content>
1397
+ <p>
1398
+ Health Service Areas (HSAs) are a county or cluster of contiguous counties which are
1399
+ relatively self-contained with respect to hospital care. Set HSA description in
1400
+ tooltip under the Columns accordion.
1401
+ </p>
1402
+ </Tooltip.Content>
1403
+ </Tooltip>
1404
+ }
1405
+ />
1406
+ </>
1407
+ )}
1365
1408
  {(config.general.geoType === 'us-county' || config.general.geoType === 'single-state') && (
1366
1409
  <Select
1367
1410
  label='County Census Year'
@@ -1599,14 +1642,26 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1599
1642
  />
1600
1643
  )}
1601
1644
 
1602
- {'us' === config.general.geoType && (
1645
+ {['us', 'us-county'].includes(config.general.geoType) && (
1603
1646
  <CheckBox
1604
- value={general.territoriesAlwaysShow || false}
1647
+ value={general.territoriesAlwaysShow ?? true}
1605
1648
  section='general'
1606
1649
  subsection={null}
1607
1650
  fieldName='territoriesAlwaysShow'
1608
- label='Show All Territories'
1609
- updateField={updateField}
1651
+ label='Show Available Territories'
1652
+ updateField={() => {
1653
+ setConfig({
1654
+ ...config,
1655
+ general: {
1656
+ ...config.general,
1657
+ territoriesAlwaysShow: !(general.territoriesAlwaysShow ?? true)
1658
+ },
1659
+ migrations: {
1660
+ ...config.migrations,
1661
+ showPuertoRico: false
1662
+ }
1663
+ })
1664
+ }}
1610
1665
  />
1611
1666
  )}
1612
1667
  </AccordionItemPanel>
@@ -1777,6 +1832,105 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1777
1832
  </Tooltip>
1778
1833
  }
1779
1834
  />
1835
+
1836
+ {/* Accessible Alt Text Description */}
1837
+ {(() => {
1838
+ const metadataKeys = Object.keys(config.dataMetadata || {})
1839
+ const hasMetadata = metadataKeys.length > 0
1840
+ const descType = config.altText?.type || ''
1841
+ const resolvedDescription = resolveAltTextDescription(config.altText, config.dataMetadata)
1842
+ return (
1843
+ <>
1844
+ <Select
1845
+ value={descType}
1846
+ fieldName='altTextType'
1847
+ label='Alt Text Description'
1848
+ options={[
1849
+ { value: '', label: 'None' },
1850
+ { value: 'static', label: 'Static (manual text)' },
1851
+ { value: 'metadata', label: 'Data File Metadata' }
1852
+ ]}
1853
+ updateField={(_section, _subsection, _fieldName, value) => {
1854
+ if (value === '') {
1855
+ updateField(null, null, 'altText', undefined)
1856
+ } else {
1857
+ updateField(null, null, 'altText', { type: value as 'static' | 'metadata' })
1858
+ }
1859
+ }}
1860
+ tooltip={
1861
+ <Tooltip style={{ textTransform: 'none' }}>
1862
+ <Tooltip.Target>
1863
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
1864
+ </Tooltip.Target>
1865
+ <Tooltip.Content>
1866
+ <p>
1867
+ Add a longer description for screen readers. The map title is always auto-generated.
1868
+ Use "Static" for manually written text, or "Data File Metadata" to pull it from a
1869
+ key in your data file.
1870
+ </p>
1871
+ </Tooltip.Content>
1872
+ </Tooltip>
1873
+ }
1874
+ />
1875
+ {descType === 'static' && (
1876
+ <TextField
1877
+ value={config.altText?.value || ''}
1878
+ fieldName='altTextValue'
1879
+ type='textarea'
1880
+ label='Description Text'
1881
+ placeholder='Longer interpretive description of map insights...'
1882
+ updateField={(_section, _subsection, _fieldName, value) => {
1883
+ updateField(null, null, 'altText', { ...config.altText, value })
1884
+ }}
1885
+ />
1886
+ )}
1887
+ {descType === 'metadata' && (
1888
+ <>
1889
+ {hasMetadata ? (
1890
+ <Select
1891
+ value={config.altText?.metadataKey || ''}
1892
+ fieldName='altTextMetadataKey'
1893
+ label='Description Metadata Field'
1894
+ options={[
1895
+ { value: '', label: 'Select Metadata Field...' },
1896
+ ...metadataKeys.map(key => ({
1897
+ value: key,
1898
+ label: `${key}: ${config.dataMetadata[key]}`
1899
+ }))
1900
+ ]}
1901
+ updateField={(_section, _subsection, _fieldName, value) => {
1902
+ updateField(null, null, 'altText', { ...config.altText, metadataKey: value })
1903
+ }}
1904
+ />
1905
+ ) : (
1906
+ <span className='subtext'>
1907
+ No metadata fields are available. Your data file must be a JSON object with a{' '}
1908
+ <code>data</code> array and sibling key-value pairs, for example:{' '}
1909
+ <code>{`{ "altDescription": "...", "data": [...] }`}</code>
1910
+ </span>
1911
+ )}
1912
+ </>
1913
+ )}
1914
+ {resolvedDescription && (
1915
+ <div
1916
+ style={{
1917
+ marginTop: '1em',
1918
+ padding: '0.75em',
1919
+ background: '#f5f5f5',
1920
+ borderRadius: '4px',
1921
+ fontSize: '0.8em',
1922
+ textTransform: 'none'
1923
+ }}
1924
+ >
1925
+ <strong style={{ display: 'block', marginBottom: '0.25em' }}>Preview:</strong>
1926
+ <p data-testid='alt-text-desc-preview' style={{ margin: 0, fontStyle: 'italic' }}>
1927
+ {resolvedDescription}
1928
+ </p>
1929
+ </div>
1930
+ )}
1931
+ </>
1932
+ )
1933
+ })()}
1780
1934
  </AccordionItemPanel>
1781
1935
  </AccordionItem>
1782
1936
  <AccordionItem>
@@ -1923,6 +2077,26 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1923
2077
  />
1924
2078
  </label>
1925
2079
  </ColumnSection>
2080
+ {config.general.geoType === 'us-county' && config.general.showHSABoundaries && (
2081
+ <Select
2082
+ label='HSA Description Column'
2083
+ value={config.columns.hsa?.name}
2084
+ options={columnsOptions.map(c => c.key)}
2085
+ onChange={e => {
2086
+ editColumn('hsa', 'name', e.target.value)
2087
+ }}
2088
+ tooltip={
2089
+ <Tooltip style={{ textTransform: 'none' }}>
2090
+ <Tooltip.Target>
2091
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
2092
+ </Tooltip.Target>
2093
+ <Tooltip.Content>
2094
+ <p>Select the source column containing the HSA description.</p>
2095
+ </Tooltip.Content>
2096
+ </Tooltip>
2097
+ }
2098
+ />
2099
+ )}
1926
2100
  {'navigation' !== config.general.type && (
1927
2101
  <ColumnSection
1928
2102
  fieldKey='primary'
@@ -2101,105 +2275,6 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2101
2275
  </>
2102
2276
  }
2103
2277
 
2104
- {'navigation' !== config.general.type && (
2105
- <fieldset className='primary-fieldset edit-block'>
2106
- <label>
2107
- <span className='edit-label'>
2108
- Special Classes
2109
- <Tooltip style={{ textTransform: 'none' }}>
2110
- <Tooltip.Target>
2111
- <Icon display='question' style={{ marginLeft: '0.5rem' }} />
2112
- </Tooltip.Target>
2113
- <Tooltip.Content>
2114
- <p>
2115
- For secondary values such as "NA", the system can automatically color-code them in
2116
- shades of gray, one shade for each special class.
2117
- </p>
2118
- </Tooltip.Content>
2119
- </Tooltip>
2120
- </span>
2121
- </label>
2122
- {config.legend.specialClasses.length === 2 && (
2123
- <Alert
2124
- type='info'
2125
- message='If a third special class is needed you can apply a pattern to set it apart.'
2126
- showCloseButton={false}
2127
- />
2128
- )}
2129
- {specialClasses.map((specialClass, i) => (
2130
- <div className='edit-block' key={`special-class-${i}`}>
2131
- <button
2132
- className='remove-column'
2133
- onClick={e => {
2134
- e.preventDefault()
2135
- editColumn('primary', 'specialClassDelete', i)
2136
- }}
2137
- >
2138
- Remove
2139
- </button>
2140
- <p>Special Class {i + 1}</p>
2141
- <Select
2142
- label='Data Key'
2143
- value={specialClass.key}
2144
- options={columnsOptions.map(option => ({
2145
- value: option.key,
2146
- label: option.key
2147
- }))}
2148
- onChange={event => {
2149
- editColumn('primary', 'specialClassEdit', {
2150
- prop: 'key',
2151
- index: i,
2152
- value: event.target.value
2153
- })
2154
- }}
2155
- />
2156
- <Select
2157
- label='Value'
2158
- value={specialClass.value}
2159
- options={[
2160
- { value: '', label: '- Select Value -' },
2161
- ...(columnsByKey[specialClass.key] || [])
2162
- .sort()
2163
- .map(option => ({ value: option, label: option }))
2164
- ]}
2165
- onChange={event => {
2166
- editColumn('primary', 'specialClassEdit', {
2167
- prop: 'value',
2168
- index: i,
2169
- value: event.target.value
2170
- })
2171
- }}
2172
- />
2173
- <label>
2174
- <span className='edit-label column-heading'>Label</span>
2175
- <input
2176
- type='text'
2177
- value={specialClass.label}
2178
- onChange={e => {
2179
- editColumn('primary', 'specialClassEdit', {
2180
- prop: 'label',
2181
- index: i,
2182
- value: e.target.value
2183
- })
2184
- }}
2185
- />
2186
- </label>
2187
- </div>
2188
- ))}
2189
- {config.legend.specialClasses.length < 2 && (
2190
- <button
2191
- className='btn btn-primary full-width'
2192
- onClick={e => {
2193
- e.preventDefault()
2194
- editColumn('primary', 'specialClassAdd', {})
2195
- }}
2196
- >
2197
- Add Special Class
2198
- </button>
2199
- )}
2200
- </fieldset>
2201
- )}
2202
-
2203
2278
  <label className='edit-block navigate column-heading'>
2204
2279
  <span className='edit-label column-heading'>
2205
2280
  Navigation
@@ -2227,144 +2302,261 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2227
2302
  <fieldset className='primary-fieldset edit-block'>
2228
2303
  <label>
2229
2304
  <span className='edit-label'>
2230
- Additional Columns
2305
+ Special Classes
2231
2306
  <Tooltip style={{ textTransform: 'none' }}>
2232
2307
  <Tooltip.Target>
2233
2308
  <Icon display='question' style={{ marginLeft: '0.5rem' }} />
2234
2309
  </Tooltip.Target>
2235
2310
  <Tooltip.Content>
2236
2311
  <p>
2237
- You can specify additional columns to display in tooltips and / or the supporting data
2238
- table.
2312
+ For secondary values such as "NA", the system can automatically color-code them in
2313
+ shades of gray, one shade for each special class.
2239
2314
  </p>
2240
2315
  </Tooltip.Content>
2241
2316
  </Tooltip>
2242
2317
  </span>
2243
2318
  </label>
2244
- {additionalColumns.map(val => (
2245
- <fieldset className='edit-block' key={val}>
2246
- <button
2247
- className='remove-column'
2248
- onClick={event => {
2249
- event.preventDefault()
2250
- removeAdditionalColumn(val)
2251
- }}
2252
- >
2253
- Remove
2254
- </button>
2255
- <Select
2256
- label='Column'
2257
- value={config.columns[val] ? config.columns[val].name : ''}
2258
- options={columnsOptions
2259
- .filter(option => {
2260
- const optionValue = option.props.value
2261
- return (
2262
- optionValue === '' ||
2263
- optionValue !== config.columns.geo?.name ||
2264
- optionValue === config.columns[val]?.name
2265
- )
2266
- })
2267
- .map(option => ({
2268
- value: option.props.value,
2269
- label: option.props.children
2270
- }))}
2271
- onChange={event => {
2272
- editColumn(val, 'name', event.target.value)
2273
- }}
2274
- />
2275
- <TextField
2276
- value={columns[val].label}
2277
- section='columns'
2278
- subsection={val}
2279
- fieldName='label'
2280
- label='Label'
2281
- updateField={updateField}
2282
- />
2283
- <ul className='column-edit'>
2284
- <li className='three-col'>
2285
- <TextField
2286
- value={columns[val].prefix}
2287
- section='columns'
2288
- subsection={val}
2289
- fieldName='prefix'
2290
- label='Prefix'
2291
- updateField={updateField}
2292
- />
2293
- <TextField
2294
- value={columns[val].suffix}
2295
- section='columns'
2296
- subsection={val}
2297
- fieldName='suffix'
2298
- label='Suffix'
2299
- updateField={updateField}
2300
- />
2301
- <TextField
2302
- type='number'
2303
- value={columns[val].roundToPlace}
2304
- section='columns'
2305
- subsection={val}
2306
- fieldName='roundToPlace'
2307
- label='Round'
2308
- updateField={updateField}
2309
- />
2310
- </li>
2311
- <CheckBox
2312
- value={config.columns[val].useCommas}
2313
- section='columns'
2314
- subsection={val}
2315
- fieldName='useCommas'
2316
- label='Add Commas to Numbers'
2317
- updateField={updateField}
2318
- onChange={event => {
2319
- editColumn(val, 'useCommas', event.target.checked)
2320
- }}
2321
- />
2322
- <CheckBox
2323
- value={config.columns[val].dataTable}
2324
- section='columns'
2325
- subsection={val}
2326
- fieldName='dataTable'
2327
- label='Show in Data Table'
2328
- updateField={updateField}
2329
- onChange={event => {
2330
- editColumn(val, 'dataTable', event.target.checked)
2331
- }}
2332
- />
2333
- <CheckBox
2334
- value={config.columns[val].tooltip}
2335
- section='columns'
2336
- subsection={val}
2337
- fieldName='tooltip'
2338
- label='Show in Tooltips'
2339
- updateField={updateField}
2340
- onChange={event => {
2341
- editColumn(val, 'tooltip', event.target.checked)
2342
- }}
2343
- />
2344
- <label>
2345
- <span className='edit-label column-heading'>Order</span>
2346
- <input
2347
- onWheel={e => e.currentTarget.blur()}
2348
- type='number'
2349
- min='1'
2350
- value={config.columns[val]?.order ?? ''}
2351
- onChange={event => {
2352
- updateColumnOrder(val, event.target.value)
2353
- }}
2354
- />
2355
- </label>
2356
- </ul>
2357
- </fieldset>
2358
- ))}
2359
- <button
2360
- className={'btn btn-primary full-width'}
2361
- onClick={event => {
2362
- event.preventDefault()
2363
- addAdditionalColumn(additionalColumns.length + 1)
2364
- }}
2319
+ {config.legend.specialClasses.length === 2 && (
2320
+ <Alert
2321
+ type='info'
2322
+ message='If a third special class is needed you can apply a pattern to set it apart.'
2323
+ showCloseButton={false}
2324
+ />
2325
+ )}
2326
+ <GroupedList
2327
+ items={specialClasses}
2328
+ label='Special Classes'
2329
+ droppableId='map-special-classes'
2330
+ draggable={false}
2331
+ renderItem={(specialClass, i) => (
2332
+ <Accordion allowZeroExpanded key={`special-class-${i}`}>
2333
+ <AccordionItem className='series-item series-item--chart'>
2334
+ <AccordionItemHeading className='series-item__title'>
2335
+ <AccordionItemButton className='accordion__button'>
2336
+ {specialClass.label || specialClass.value || `Special Class ${i + 1}`}
2337
+ </AccordionItemButton>
2338
+ </AccordionItemHeading>
2339
+ <AccordionItemPanel>
2340
+ <div className='series-item__panel-actions'>
2341
+ <Button
2342
+ type='button'
2343
+ variant='danger'
2344
+ size='sm'
2345
+ className='grouped-list__remove'
2346
+ onClick={() => editColumn('primary', 'specialClassDelete', i)}
2347
+ >
2348
+ Remove
2349
+ </Button>
2350
+ </div>
2351
+ <Select
2352
+ label='Data Key'
2353
+ value={specialClass.key}
2354
+ options={columnsOptions.map(option => ({
2355
+ value: option.key,
2356
+ label: option.key
2357
+ }))}
2358
+ onChange={event => {
2359
+ editColumn('primary', 'specialClassEdit', {
2360
+ prop: 'key',
2361
+ index: i,
2362
+ value: event.target.value
2363
+ })
2364
+ }}
2365
+ />
2366
+ <Select
2367
+ label='Value'
2368
+ value={specialClass.value}
2369
+ options={[
2370
+ { value: '', label: '- Select Value -' },
2371
+ ...(columnsByKey[specialClass.key] || [])
2372
+ .sort()
2373
+ .map(option => ({ value: option, label: option }))
2374
+ ]}
2375
+ onChange={event => {
2376
+ editColumn('primary', 'specialClassEdit', {
2377
+ prop: 'value',
2378
+ index: i,
2379
+ value: event.target.value
2380
+ })
2381
+ }}
2382
+ />
2383
+ <label>
2384
+ <span className='edit-label column-heading'>Label</span>
2385
+ <input
2386
+ type='text'
2387
+ value={specialClass.label}
2388
+ onChange={e => {
2389
+ editColumn('primary', 'specialClassEdit', {
2390
+ prop: 'label',
2391
+ index: i,
2392
+ value: e.target.value
2393
+ })
2394
+ }}
2395
+ />
2396
+ </label>
2397
+ </AccordionItemPanel>
2398
+ </AccordionItem>
2399
+ </Accordion>
2400
+ )}
2401
+ />
2402
+ {config.legend.specialClasses.length < 2 && (
2403
+ <Button
2404
+ type='button'
2405
+ variant='editor-primary'
2406
+ onClick={() => editColumn('primary', 'specialClassAdd', {})}
2407
+ >
2408
+ Add Special Class
2409
+ </Button>
2410
+ )}
2411
+ </fieldset>
2412
+ )}
2413
+ {'navigation' !== config.general.type && (
2414
+ <fieldset className='primary-fieldset edit-block'>
2415
+ <GroupedList
2416
+ items={additionalColumns}
2417
+ label={
2418
+ <>
2419
+ Additional Columns
2420
+ <Tooltip style={{ textTransform: 'none' }}>
2421
+ <Tooltip.Target>
2422
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
2423
+ </Tooltip.Target>
2424
+ <Tooltip.Content>
2425
+ <p>
2426
+ You can specify additional columns to display in tooltips and / or the supporting
2427
+ data table.
2428
+ </p>
2429
+ </Tooltip.Content>
2430
+ </Tooltip>
2431
+ </>
2432
+ }
2433
+ droppableId='map-additional-columns'
2434
+ draggable={false}
2435
+ renderItem={val => (
2436
+ <Accordion allowZeroExpanded key={val}>
2437
+ <AccordionItem className='series-item series-item--chart'>
2438
+ <AccordionItemHeading className='series-item__title'>
2439
+ <AccordionItemButton className='accordion__button'>
2440
+ {columns[val]?.label || config.columns[val]?.name || 'New Column'}
2441
+ </AccordionItemButton>
2442
+ </AccordionItemHeading>
2443
+ <AccordionItemPanel>
2444
+ <div className='series-item__panel-actions'>
2445
+ <Button
2446
+ type='button'
2447
+ variant='danger'
2448
+ size='sm'
2449
+ className='grouped-list__remove'
2450
+ onClick={() => removeAdditionalColumn(val)}
2451
+ >
2452
+ Remove
2453
+ </Button>
2454
+ </div>
2455
+ <Select
2456
+ label='Column'
2457
+ value={config.columns[val] ? config.columns[val].name : ''}
2458
+ options={columnsOptions.map(option => ({
2459
+ value: option.props.value,
2460
+ label: option.props.children
2461
+ }))}
2462
+ onChange={event => {
2463
+ editColumn(val, 'name', event.target.value)
2464
+ }}
2465
+ />
2466
+ <TextField
2467
+ value={columns[val].label}
2468
+ section='columns'
2469
+ subsection={val}
2470
+ fieldName='label'
2471
+ label='Label'
2472
+ updateField={updateField}
2473
+ />
2474
+ <ul className='column-edit'>
2475
+ <li className='three-col'>
2476
+ <TextField
2477
+ value={columns[val].prefix}
2478
+ section='columns'
2479
+ subsection={val}
2480
+ fieldName='prefix'
2481
+ label='Prefix'
2482
+ updateField={updateField}
2483
+ />
2484
+ <TextField
2485
+ value={columns[val].suffix}
2486
+ section='columns'
2487
+ subsection={val}
2488
+ fieldName='suffix'
2489
+ label='Suffix'
2490
+ updateField={updateField}
2491
+ />
2492
+ <TextField
2493
+ type='number'
2494
+ value={columns[val].roundToPlace}
2495
+ section='columns'
2496
+ subsection={val}
2497
+ fieldName='roundToPlace'
2498
+ label='Round'
2499
+ updateField={updateField}
2500
+ />
2501
+ </li>
2502
+ <CheckBox
2503
+ value={config.columns[val].useCommas}
2504
+ section='columns'
2505
+ subsection={val}
2506
+ fieldName='useCommas'
2507
+ label='Add Commas to Numbers'
2508
+ updateField={updateField}
2509
+ onChange={event => {
2510
+ editColumn(val, 'useCommas', event.target.checked)
2511
+ }}
2512
+ />
2513
+ <CheckBox
2514
+ value={config.columns[val].dataTable}
2515
+ section='columns'
2516
+ subsection={val}
2517
+ fieldName='dataTable'
2518
+ label='Show in Data Table'
2519
+ updateField={updateField}
2520
+ onChange={event => {
2521
+ editColumn(val, 'dataTable', event.target.checked)
2522
+ }}
2523
+ />
2524
+ <CheckBox
2525
+ value={config.columns[val].tooltip}
2526
+ section='columns'
2527
+ subsection={val}
2528
+ fieldName='tooltip'
2529
+ label='Show in Tooltips'
2530
+ updateField={updateField}
2531
+ onChange={event => {
2532
+ editColumn(val, 'tooltip', event.target.checked)
2533
+ }}
2534
+ />
2535
+ <label>
2536
+ <span className='edit-label column-heading'>Order</span>
2537
+ <input
2538
+ onWheel={e => e.currentTarget.blur()}
2539
+ type='number'
2540
+ min='1'
2541
+ value={config.columns[val]?.order ?? ''}
2542
+ onChange={event => {
2543
+ updateColumnOrder(val, event.target.value)
2544
+ }}
2545
+ />
2546
+ </label>
2547
+ </ul>
2548
+ </AccordionItemPanel>
2549
+ </AccordionItem>
2550
+ </Accordion>
2551
+ )}
2552
+ />
2553
+ <Button
2554
+ type='button'
2555
+ variant='editor-primary'
2556
+ onClick={() => addAdditionalColumn(additionalColumns.length + 1)}
2365
2557
  >
2366
2558
  Add Column
2367
- </button>
2559
+ </Button>
2368
2560
  </fieldset>
2369
2561
  )}
2370
2562
  {'category' === config.legend.type && (
@@ -2412,8 +2604,8 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2412
2604
  </label>
2413
2605
  </fieldset>
2414
2606
  ))}
2415
- <button
2416
- className={'btn btn-primary full-width'}
2607
+ <Button
2608
+ className={'btn btn-primary full-width editor-panel-action-button'}
2417
2609
  onClick={event => {
2418
2610
  event.preventDefault()
2419
2611
  const updatedAdditionaCategories = [...(config.legend.additionalCategories || [])]
@@ -2422,7 +2614,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2422
2614
  }}
2423
2615
  >
2424
2616
  Add Category
2425
- </button>
2617
+ </Button>
2426
2618
  </fieldset>
2427
2619
  )}
2428
2620
  </AccordionItemPanel>
@@ -2920,6 +3112,15 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2920
3112
  <AccordionItemButton>Filters</AccordionItemButton>
2921
3113
  </AccordionItemHeading>
2922
3114
  <AccordionItemPanel>
3115
+ {config.general.geoType === 'us-county' && (
3116
+ <CheckBox
3117
+ value={config.general.showStateDropdown || false}
3118
+ fieldName='showStateDropdown'
3119
+ label='Show State Dropdown'
3120
+ updateField={updateField}
3121
+ section='general'
3122
+ />
3123
+ )}
2923
3124
  <VizFilterEditor
2924
3125
  config={config}
2925
3126
  updateField={updateField}
@@ -3119,6 +3320,26 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
3119
3320
  label='Show collapse below table'
3120
3321
  updateField={updateField}
3121
3322
  />
3323
+ <CheckBox
3324
+ value={config.table.search ?? false}
3325
+ section='table'
3326
+ subsection={null}
3327
+ fieldName='search'
3328
+ label='Enable Search'
3329
+ updateField={updateField}
3330
+ />
3331
+ {config.table.search && (
3332
+ <div className='ms-4 mt-2' style={{ maxWidth: 'calc(100% - 1.5rem)' }}>
3333
+ <TextField
3334
+ value={config.table.searchPlaceholder || ''}
3335
+ section='table'
3336
+ fieldName='searchPlaceholder'
3337
+ label='Search Placeholder Text'
3338
+ placeholder='Filter...'
3339
+ updateField={updateField}
3340
+ />
3341
+ </div>
3342
+ )}
3122
3343
  <Select
3123
3344
  value={config.table.defaultSort?.column || ''}
3124
3345
  fieldName='column'
@@ -3242,16 +3463,12 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
3242
3463
  updateField={updateField}
3243
3464
  />
3244
3465
  )}
3245
- {isLoadedFromUrl && (
3246
- <CheckBox
3247
- value={config.table.showDownloadUrl}
3248
- section='table'
3249
- subsection={null}
3250
- fieldName='showDownloadUrl'
3251
- label='Show URL to Automatically Updated Data'
3252
- updateField={updateField}
3253
- />
3254
- )}
3466
+ <DownloadUrlControls
3467
+ hasUrlBackedDataSource={Boolean(isLoadedFromUrl)}
3468
+ showDownloadUrl={config.table.showDownloadUrl}
3469
+ downloadUrlLabel={config.table.downloadUrlLabel}
3470
+ updateField={updateField}
3471
+ />
3255
3472
  <CheckBox
3256
3473
  value={config.table.showFullGeoNameInCSV}
3257
3474
  section='table'
@@ -3658,6 +3875,20 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
3658
3875
  <span className='edit-label'>Allow Map Zooming</span>
3659
3876
  </label>
3660
3877
  )}
3878
+ {config.general.geoType === 'us' && (
3879
+ <label className='checkbox'>
3880
+ <input
3881
+ type='checkbox'
3882
+ checked={config.general.showClearSelectionButton !== false}
3883
+ onChange={event => {
3884
+ const _newConfig = cloneConfig(config)
3885
+ _newConfig.general.showClearSelectionButton = event.target.checked
3886
+ setConfig(_newConfig)
3887
+ }}
3888
+ />
3889
+ <span className='edit-label'>Show Clear Selection Button</span>
3890
+ </label>
3891
+ )}
3661
3892
  {config.general.type === 'bubble' && (
3662
3893
  <label className='checkbox'>
3663
3894
  <input
@@ -3712,73 +3943,84 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
3712
3943
  )}
3713
3944
  {/* <AdditionalCityStyles /> */}
3714
3945
  <>
3715
- {config.visual.additionalCityStyles.length > 0 &&
3716
- config.visual.additionalCityStyles.map(({ label, column, value, shape }, i) => {
3717
- return (
3718
- <div className='edit-block' key={`additional-city-style-${i}`}>
3719
- <button
3720
- className='remove-column'
3721
- onClick={e => {
3722
- e.preventDefault()
3723
- editCityStyles('remove', i, '', '')
3724
- }}
3725
- >
3726
- Remove
3727
- </button>
3728
- <p>City Style {i + 1}</p>
3729
- <Select
3730
- label='Column with configuration value'
3731
- value={column}
3732
- options={columnsOptions.map(c => c.key)}
3733
- onChange={e => {
3734
- editCityStyles('update', i, 'column', e.target.value)
3735
- }}
3736
- />
3737
- <label>
3738
- <span className='edit-label column-heading'>Value to Trigger</span>
3739
- <input
3740
- type='text'
3741
- value={value}
3946
+ <GroupedList
3947
+ items={config.visual.additionalCityStyles}
3948
+ label='Additional City Styles'
3949
+ droppableId='map-city-styles'
3950
+ draggable={false}
3951
+ renderItem={({ label, column, value, shape }, i) => (
3952
+ <Accordion allowZeroExpanded key={`additional-city-style-${i}`}>
3953
+ <AccordionItem className='series-item series-item--chart'>
3954
+ <AccordionItemHeading className='series-item__title'>
3955
+ <AccordionItemButton className='accordion__button'>
3956
+ {label || column || `City Style ${i + 1}`}
3957
+ </AccordionItemButton>
3958
+ </AccordionItemHeading>
3959
+ <AccordionItemPanel>
3960
+ <div className='series-item__panel-actions'>
3961
+ <Button
3962
+ type='button'
3963
+ variant='danger'
3964
+ size='sm'
3965
+ className='grouped-list__remove'
3966
+ onClick={() => editCityStyles('remove', i, '', '')}
3967
+ >
3968
+ Remove
3969
+ </Button>
3970
+ </div>
3971
+ <Select
3972
+ label='Column with configuration value'
3973
+ value={column}
3974
+ options={columnsOptions.map(c => c.key)}
3742
3975
  onChange={e => {
3743
- editCityStyles('update', i, 'value', e.target.value)
3976
+ editCityStyles('update', i, 'column', e.target.value)
3744
3977
  }}
3745
- ></input>
3746
- </label>
3747
- <Select
3748
- label='Shape'
3749
- value={shape}
3750
- options={[
3751
- { value: '', label: '- Select Option -' },
3752
- ...['Circle', 'Square', 'Triangle', 'Diamond', 'Star', 'Pin']
3753
- .filter(val => String(config.visual.cityStyle).toLowerCase() !== val.toLowerCase())
3754
- .map(val => ({ value: val, label: val }))
3755
- ]}
3756
- onChange={e => {
3757
- editCityStyles('update', i, 'shape', e.target.value)
3758
- }}
3759
- />
3760
- <label>
3761
- <span className='edit-label column-heading'>Label</span>
3762
- <input
3763
- key={i}
3764
- type='text'
3765
- value={label}
3978
+ />
3979
+ <label>
3980
+ <span className='edit-label column-heading'>Value to Trigger</span>
3981
+ <input
3982
+ type='text'
3983
+ value={value}
3984
+ onChange={e => {
3985
+ editCityStyles('update', i, 'value', e.target.value)
3986
+ }}
3987
+ />
3988
+ </label>
3989
+ <Select
3990
+ label='Shape'
3991
+ value={shape}
3992
+ options={[
3993
+ { value: '', label: '- Select Option -' },
3994
+ ...['Circle', 'Square', 'Triangle', 'Diamond', 'Star', 'Pin']
3995
+ .filter(
3996
+ val => String(config.visual.cityStyle).toLowerCase() !== val.toLowerCase()
3997
+ )
3998
+ .map(val => ({ value: val, label: val }))
3999
+ ]}
3766
4000
  onChange={e => {
3767
- editCityStyles('update', i, 'label', e.target.value)
4001
+ editCityStyles('update', i, 'shape', e.target.value)
3768
4002
  }}
3769
4003
  />
3770
- </label>
3771
- </div>
3772
- )
3773
- })}
4004
+ <label>
4005
+ <span className='edit-label column-heading'>Label</span>
4006
+ <input
4007
+ key={i}
4008
+ type='text'
4009
+ value={label}
4010
+ onChange={e => {
4011
+ editCityStyles('update', i, 'label', e.target.value)
4012
+ }}
4013
+ />
4014
+ </label>
4015
+ </AccordionItemPanel>
4016
+ </AccordionItem>
4017
+ </Accordion>
4018
+ )}
4019
+ />
3774
4020
 
3775
- <button
3776
- type='button'
3777
- onClick={() => editCityStyles('add', 0, '', '')}
3778
- className='btn btn-primary full-width'
3779
- >
4021
+ <Button type='button' variant='editor-primary' onClick={() => editCityStyles('add', 0, '', '')}>
3780
4022
  Add city style
3781
- </button>
4023
+ </Button>
3782
4024
  </>
3783
4025
  <label htmlFor='opacity'>
3784
4026
  <TextField
@@ -3821,87 +4063,100 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
3821
4063
  </AccordionItemHeading>
3822
4064
  <AccordionItemPanel>
3823
4065
  {config.map.layers.length === 0 && <p>There are currently no layers.</p>}
3824
-
3825
- {config.map.layers.map((layer, index) => {
3826
- return (
3827
- <>
3828
- <Accordion allowZeroExpanded>
3829
- <AccordionItem className='series-item map-layers-list'>
3830
- <AccordionItemHeading className='series-item__title map-layers-list--title'>
3831
- <AccordionItemButton>{`Layer ${index + 1}: ${layer.name}`}</AccordionItemButton>
3832
- </AccordionItemHeading>
3833
- <AccordionItemPanel>
3834
- <div className='map-layers-panel'>
3835
- <label htmlFor='layerName'>Layer Name:</label>
3836
- <input
3837
- type='text'
3838
- name='layerName'
3839
- value={layer.name}
3840
- onChange={e => handleMapLayer(e, index, 'name')}
3841
- />
3842
- <label htmlFor='layerFilename'>File:</label>
3843
- <input
3844
- type='text'
3845
- name='layerFilename'
3846
- value={layer.url}
3847
- onChange={e => handleMapLayer(e, index, 'url')}
3848
- />
3849
- <label htmlFor='layerNamespace'>TOPOJSON Namespace:</label>
3850
- <input
3851
- type='text'
3852
- name='layerNamespace'
3853
- value={layer.namespace}
3854
- onChange={e => handleMapLayer(e, index, 'namespace')}
3855
- />
3856
- <label htmlFor='layerFill'>Fill Color:</label>
3857
- <input
3858
- type='text'
3859
- name='layerFill'
3860
- value={layer.fill}
3861
- onChange={e => handleMapLayer(e, index, 'fill')}
3862
- />
3863
- <label htmlFor='layerFill'>Fill Opacity (%):</label>
3864
- <input
3865
- type='number'
3866
- min={0}
3867
- max={100}
3868
- name='layerFill'
3869
- value={layer.fillOpacity ? layer.fillOpacity * 100 : ''}
3870
- onChange={e => handleMapLayer(e, index, 'fillOpacity')}
3871
- />
3872
- <label htmlFor='layerStroke'>Stroke Color:</label>
3873
- <input
3874
- type='text'
3875
- name='layerStroke'
3876
- value={layer.stroke}
3877
- onChange={e => handleMapLayer(e, index, 'stroke')}
3878
- />
3879
- <label htmlFor='layerStroke'>Stroke Width:</label>
3880
- <input
3881
- type='number'
3882
- min={0}
3883
- max={5}
3884
- name='layerStrokeWidth'
3885
- value={layer.strokeWidth}
3886
- onChange={e => handleMapLayer(e, index, 'strokeWidth')}
3887
- />
3888
- <label htmlFor='layerTooltip'>Tooltip:</label>
3889
- <textarea
3890
- name='layerTooltip'
3891
- value={layer.tooltip}
3892
- onChange={e => handleMapLayer(e, index, 'tooltip')}
3893
- ></textarea>
3894
- <button onClick={e => handleRemoveLayer(e, index)}>Remove Layer</button>
3895
- </div>
3896
- </AccordionItemPanel>
3897
- </AccordionItem>
3898
- </Accordion>
3899
- </>
3900
- )
3901
- })}
3902
- <button className={'btn btn-primary full-width'} onClick={handleAddLayer}>
4066
+ <GroupedList
4067
+ items={config.map.layers}
4068
+ label='Custom Map Layers'
4069
+ droppableId='map-custom-layers'
4070
+ draggable={false}
4071
+ renderItem={(layer, index) => (
4072
+ <Accordion allowZeroExpanded key={`map-layer-${index}`}>
4073
+ <AccordionItem className='series-item series-item--chart map-layers-list'>
4074
+ <AccordionItemHeading className='series-item__title map-layers-list--title'>
4075
+ <AccordionItemButton className='accordion__button'>
4076
+ {`Layer ${index + 1}: ${layer.name}`}
4077
+ </AccordionItemButton>
4078
+ </AccordionItemHeading>
4079
+ <AccordionItemPanel>
4080
+ <div className='series-item__panel-actions'>
4081
+ <Button
4082
+ type='button'
4083
+ variant='danger'
4084
+ size='sm'
4085
+ className='grouped-list__remove'
4086
+ onClick={e => handleRemoveLayer(e, index)}
4087
+ >
4088
+ Remove Layer
4089
+ </Button>
4090
+ </div>
4091
+ <div className='map-layers-panel'>
4092
+ <label htmlFor='layerName'>Layer Name:</label>
4093
+ <input
4094
+ type='text'
4095
+ name='layerName'
4096
+ value={layer.name}
4097
+ onChange={e => handleMapLayer(e, index, 'name')}
4098
+ />
4099
+ <label htmlFor='layerFilename'>File:</label>
4100
+ <input
4101
+ type='text'
4102
+ name='layerFilename'
4103
+ value={layer.url}
4104
+ onChange={e => handleMapLayer(e, index, 'url')}
4105
+ />
4106
+ <label htmlFor='layerNamespace'>TOPOJSON Namespace:</label>
4107
+ <input
4108
+ type='text'
4109
+ name='layerNamespace'
4110
+ value={layer.namespace}
4111
+ onChange={e => handleMapLayer(e, index, 'namespace')}
4112
+ />
4113
+ <label htmlFor='layerFill'>Fill Color:</label>
4114
+ <input
4115
+ type='text'
4116
+ name='layerFill'
4117
+ value={layer.fill}
4118
+ onChange={e => handleMapLayer(e, index, 'fill')}
4119
+ />
4120
+ <label htmlFor='layerFill'>Fill Opacity (%):</label>
4121
+ <input
4122
+ type='number'
4123
+ min={0}
4124
+ max={100}
4125
+ name='layerFill'
4126
+ value={layer.fillOpacity ? layer.fillOpacity * 100 : ''}
4127
+ onChange={e => handleMapLayer(e, index, 'fillOpacity')}
4128
+ />
4129
+ <label htmlFor='layerStroke'>Stroke Color:</label>
4130
+ <input
4131
+ type='text'
4132
+ name='layerStroke'
4133
+ value={layer.stroke}
4134
+ onChange={e => handleMapLayer(e, index, 'stroke')}
4135
+ />
4136
+ <label htmlFor='layerStroke'>Stroke Width:</label>
4137
+ <input
4138
+ type='number'
4139
+ min={0}
4140
+ max={5}
4141
+ name='layerStrokeWidth'
4142
+ value={layer.strokeWidth}
4143
+ onChange={e => handleMapLayer(e, index, 'strokeWidth')}
4144
+ />
4145
+ <label htmlFor='layerTooltip'>Tooltip:</label>
4146
+ <textarea
4147
+ name='layerTooltip'
4148
+ value={layer.tooltip}
4149
+ onChange={e => handleMapLayer(e, index, 'tooltip')}
4150
+ ></textarea>
4151
+ </div>
4152
+ </AccordionItemPanel>
4153
+ </AccordionItem>
4154
+ </Accordion>
4155
+ )}
4156
+ />
4157
+ <Button type='button' variant='editor-primary' onClick={handleAddLayer}>
3903
4158
  Add Map Layer
3904
- </button>
4159
+ </Button>
3905
4160
  <p className='layer-purpose-details'>
3906
4161
  Context should be added to your visualization or associated page to describe the significance of
3907
4162
  layers that are added to maps.