@cdc/map 4.25.6-1 → 4.25.7

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.
@@ -1104,8 +1104,9 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1104
1104
  </span>
1105
1105
  <ul className='geo-buttons d-grid' style={{ gridTemplateColumns: 'repeat(2, 1fr)', gap: '8px' }}>
1106
1106
  <button
1107
- className={`${config.general.geoType === 'us' || config.general.geoType === 'us-county' ? 'active' : ''
1108
- } full-width`}
1107
+ className={`${
1108
+ config.general.geoType === 'us' || config.general.geoType === 'us-county' ? 'active' : ''
1109
+ } full-width`}
1109
1110
  onClick={e => {
1110
1111
  e.preventDefault()
1111
1112
  handleEditorChanges('geoType', 'us')
@@ -3110,19 +3111,19 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
3110
3111
  )}
3111
3112
  {(config.general.geoType === 'world' ||
3112
3113
  (config.general.geoType === 'us' && config.general.type === 'bubble')) && (
3113
- <label className='checkbox'>
3114
- <input
3115
- type='checkbox'
3116
- checked={config.visual.showBubbleZeros}
3117
- onChange={event => {
3118
- const _newConfig = _.cloneDeep(config)
3119
- _newConfig.visual.showBubbleZeros = event.target.checked
3120
- setConfig(_newConfig)
3121
- }}
3122
- />
3123
- <span className='edit-label'>Show Data with Zero's on Bubble Map</span>
3124
- </label>
3125
- )}
3114
+ <label className='checkbox'>
3115
+ <input
3116
+ type='checkbox'
3117
+ checked={config.visual.showBubbleZeros}
3118
+ onChange={event => {
3119
+ const _newConfig = _.cloneDeep(config)
3120
+ _newConfig.visual.showBubbleZeros = event.target.checked
3121
+ setConfig(_newConfig)
3122
+ }}
3123
+ />
3124
+ <span className='edit-label'>Show Data with Zero's on Bubble Map</span>
3125
+ </label>
3126
+ )}
3126
3127
  {(config.general.geoType === 'world' || config.general.geoType === 'single-state') && (
3127
3128
  <label className='checkbox'>
3128
3129
  <input
@@ -3156,42 +3157,42 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
3156
3157
  {(config.general.geoType === 'us' ||
3157
3158
  config.general.geoType === 'us-county' ||
3158
3159
  config.general.geoType === 'world') && (
3159
- <>
3160
- <label>
3161
- <span className='edit-label'>Default City Style</span>
3162
- <select
3163
- value={config.visual.cityStyle || false}
3164
- onChange={event => {
3165
- handleEditorChanges('handleCityStyle', event.target.value)
3166
- }}
3167
- >
3168
- <option value='circle'>Circle</option>
3169
- <option value='pin'>Pin</option>
3170
- <option value='square'>Square</option>
3171
- <option value='triangle'>Triangle</option>
3172
- <option value='diamond'>Diamond</option>
3173
- <option value='star'>Star</option>
3174
- </select>
3175
- </label>
3176
- <TextField
3177
- value={config.visual.cityStyleLabel}
3178
- section='visual'
3179
- fieldName='cityStyleLabel'
3180
- label='Label (Optional) '
3181
- updateField={updateField}
3182
- tooltip={
3183
- <Tooltip style={{ textTransform: 'none' }}>
3184
- <Tooltip.Target>
3185
- <Icon display='question' style={{ marginLeft: '0.5rem' }} />
3186
- </Tooltip.Target>
3187
- <Tooltip.Content>
3188
- <p>When a label is provided, the default city style will appear in the legend.</p>
3189
- </Tooltip.Content>
3190
- </Tooltip>
3191
- }
3192
- />
3193
- </>
3194
- )}
3160
+ <>
3161
+ <label>
3162
+ <span className='edit-label'>Default City Style</span>
3163
+ <select
3164
+ value={config.visual.cityStyle || false}
3165
+ onChange={event => {
3166
+ handleEditorChanges('handleCityStyle', event.target.value)
3167
+ }}
3168
+ >
3169
+ <option value='circle'>Circle</option>
3170
+ <option value='pin'>Pin</option>
3171
+ <option value='square'>Square</option>
3172
+ <option value='triangle'>Triangle</option>
3173
+ <option value='diamond'>Diamond</option>
3174
+ <option value='star'>Star</option>
3175
+ </select>
3176
+ </label>
3177
+ <TextField
3178
+ value={config.visual.cityStyleLabel}
3179
+ section='visual'
3180
+ fieldName='cityStyleLabel'
3181
+ label='Label (Optional) '
3182
+ updateField={updateField}
3183
+ tooltip={
3184
+ <Tooltip style={{ textTransform: 'none' }}>
3185
+ <Tooltip.Target>
3186
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
3187
+ </Tooltip.Target>
3188
+ <Tooltip.Content>
3189
+ <p>When a label is provided, the default city style will appear in the legend.</p>
3190
+ </Tooltip.Content>
3191
+ </Tooltip>
3192
+ }
3193
+ />
3194
+ </>
3195
+ )}
3195
3196
  {/* <AdditionalCityStyles /> */}
3196
3197
  <>
3197
3198
  {config.visual.additionalCityStyles.length > 0 &&
@@ -94,7 +94,8 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
94
94
  label: parse(legendLabel),
95
95
  disabled: entry.disabled,
96
96
  special: entry.hasOwnProperty('special'),
97
- value: [entry.min, entry.max]
97
+ value: legend.type === 'category' ? entry.value : [entry.min, entry.max],
98
+ categoryValue: legend.type === 'category' ? entry.value : undefined
98
99
  }
99
100
  })
100
101
  } catch (e) {
@@ -122,19 +123,48 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
122
123
  dispatch({ type: 'SET_ACCESSIBLE_STATUS', payload: message })
123
124
  }
124
125
 
126
+ // Find the correct runtime index for toggling
127
+ // This is needed because special classes may have been moved to the end
128
+ const findRuntimeIndex = () => {
129
+ if (!runtimeLegend.items) return idx
130
+
131
+ return runtimeLegend.items.findIndex(runtimeItem => {
132
+ if (item.special && runtimeItem.special) {
133
+ // For special classes, match by label (since formatted item label comes from runtime item)
134
+ const runtimeLabel = runtimeItem.label || runtimeItem.value
135
+ const itemLabel = typeof item.label === 'string' ? item.label : item.label?.props?.children || item.label
136
+ return runtimeLabel === itemLabel
137
+ } else if (!item.special && !runtimeItem.special) {
138
+ // For categorical/qualitative items, match by single value
139
+ if (config.legend.type === 'category' && item.categoryValue !== undefined) {
140
+ return runtimeItem.value === item.categoryValue
141
+ }
142
+ // For numeric items, match by min/max values
143
+ return runtimeItem.min === item.value?.[0] && runtimeItem.max === item.value?.[1]
144
+ }
145
+ return false
146
+ })
147
+ }
148
+
149
+ const runtimeIndex = findRuntimeIndex()
150
+ const safeRuntimeIndex = runtimeIndex >= 0 ? runtimeIndex : idx
151
+
125
152
  return (
126
153
  <li
127
154
  className={handleListItemClass()}
128
155
  key={idx}
129
156
  title={`Legend item ${item.label} - Click to disable`}
130
- onClick={() => toggleLegendActive(idx, item.label, runtimeLegend, setRuntimeLegend, setAccessibleStatus)}
157
+ onClick={() =>
158
+ toggleLegendActive(safeRuntimeIndex, item.label, runtimeLegend, setRuntimeLegend, setAccessibleStatus)
159
+ }
131
160
  onKeyDown={e => {
132
161
  if (e.key === 'Enter') {
133
162
  e.preventDefault()
134
- toggleLegendActive(idx, item.label, runtimeLegend, setRuntimeLegend, setAccessibleStatus)
163
+ toggleLegendActive(safeRuntimeIndex, item.label, runtimeLegend, setRuntimeLegend, setAccessibleStatus)
135
164
  }
136
165
  }}
137
166
  tabIndex={0}
167
+ role='button'
138
168
  >
139
169
  <LegendShape shape={config.legend.style === 'boxes' ? 'square' : 'circle'} fill={item.color} />
140
170
  <span>{item.label}</span>
@@ -7,49 +7,68 @@ type LegendItem = {
7
7
  special: boolean
8
8
  }
9
9
 
10
- /**
11
- * applyColorToLegend
12
- * @param legendIdx legend item index
13
- * @param config chart config
14
- * @param result hash of legend items
15
- * @returns string - the corresponding color for the legend item
16
- */
17
- export const applyColorToLegend = (legendIdx: number, config: MapConfig, result: LegendItem[] = []): string => {
10
+ // Applies color to a legend item based on its index and special classes.
11
+ export const applyColorToLegend = (legendItemIndex: number, config: MapConfig, result: LegendItem[] = []): string => {
18
12
  if (!config) throw new Error('Config is required')
19
13
 
20
14
  const { legend, customColors, general, color } = config
21
15
  const { geoType, palette } = general
22
16
  const specialClasses = legend?.specialClasses ?? []
23
- const mapColorPalette = customColors ?? colorPalettes[color] ?? colorPalettes['bluegreen']
17
+ const colorPalette = customColors ?? colorPalettes[color] ?? colorPalettes['bluegreen']
24
18
 
25
19
  // Handle Region Maps need for a 10th color
26
- if (geoType === 'us-region' && mapColorPalette.length < 10 && mapColorPalette.length > 8) {
27
- const newColor = chroma(mapColorPalette[palette.isReversed ? 0 : 8])
20
+ if (geoType === 'us-region' && colorPalette.length < 10 && colorPalette.length > 8) {
21
+ const darkenedColor = chroma(colorPalette[palette.isReversed ? 0 : 8])
28
22
  .darken(0.75)
29
23
  .hex()
30
- palette.isReversed ? mapColorPalette.unshift(newColor) : mapColorPalette.push(newColor)
24
+ palette.isReversed ? colorPalette.unshift(darkenedColor) : colorPalette.push(darkenedColor)
31
25
  }
32
26
 
33
- const colorIdx = legendIdx - specialClasses.length
27
+ // Check if there are actually any special classes in the current result
28
+ const actualSpecialClassesCount = result.filter(item => item.special).length
29
+
30
+ const regularItemColorIndex = legendItemIndex - actualSpecialClassesCount
34
31
 
35
32
  // Handle special classes coloring
36
- if (result[legendIdx]?.special) {
33
+ if (result[legendItemIndex]?.special) {
37
34
  const specialClassColors = chroma.scale(['#D4D4D4', '#939393']).colors(specialClasses.length)
38
- return specialClassColors[legendIdx]
35
+ return specialClassColors[legendItemIndex]
36
+ }
37
+
38
+ // For categorical maps with custom colors, use color distribution logic
39
+ if (config.legend?.type === 'category' && customColors) {
40
+ const amt = config.legend.additionalCategories?.length ?? 10
41
+ const distributionArray = colorDistributions[amt] ?? []
42
+
43
+ const specificColor = distributionArray[legendItemIndex - specialClasses.length] ?? colorPalette.at(-1)
44
+
45
+ // If specificColor is a number, use it as an index; otherwise return the color directly
46
+ return colorPalette[specificColor] ?? '#fff'
39
47
  }
40
48
 
41
49
  // Use qualitative color palettes directly
42
- if (color.includes('qualitative')) return mapColorPalette[colorIdx]
50
+ if (color.includes('qualitative')) {
51
+ const safeIndex = Math.max(0, Math.min(regularItemColorIndex, colorPalette.length - 1))
52
+ return colorPalette[safeIndex]
53
+ }
43
54
 
44
- // Determine color distribution
45
- const amt =
46
- Math.max(result.length - specialClasses.length, 1) < 10
47
- ? Math.max(result.length - specialClasses.length, 1)
55
+ // Determine color distribution - use actual special classes count for consistent coloring
56
+ const legendItemCount =
57
+ Math.max(result.length - actualSpecialClassesCount, 1) < 10
58
+ ? Math.max(result.length - actualSpecialClassesCount, 1)
48
59
  : Object.keys(colorDistributions).length
49
- const distributionArray = colorDistributions[amt] ?? []
50
60
 
51
- const specificColor =
52
- distributionArray[legendIdx - specialClasses.length] ?? mapColorPalette[colorIdx] ?? mapColorPalette.at(-1)
61
+ const colorDistributionArray = colorDistributions[legendItemCount] ?? []
62
+
63
+ const rowDistributionIndex = colorDistributionArray[legendItemIndex - actualSpecialClassesCount]
64
+
65
+ const colorValue = rowDistributionIndex ?? colorPalette[regularItemColorIndex] ?? colorPalette.at(-1)
66
+
67
+ // Check if specificColor is a string(e.g., a valid color code)
68
+ if (typeof colorValue === 'string' && config.legend?.type === 'category' && customColors) {
69
+ return colorValue
70
+ }
53
71
 
54
- return mapColorPalette[specificColor]
72
+ // Otherwise, use specificColor as an index for mapColorPalette
73
+ return colorPalette[colorValue]
55
74
  }
@@ -29,21 +29,23 @@ export const applyLegendToRow = (
29
29
  return generateColorsArray(mapColorPalette[3])
30
30
  }
31
31
 
32
- const hash = hashObj(rowObj)
32
+ const hash = String(hashObj(rowObj))
33
33
 
34
34
  if (!legendMemo.current.has(hash)) {
35
35
  return generateColorsArray()
36
36
  }
37
37
 
38
38
  const idx = legendMemo.current.get(hash)!
39
- const disabledIdx = showSpecialClassesLast ? legendSpecialClassLastMemo.current.get(hash) ?? idx : idx
40
39
 
41
- if (runtimeLegend.items?.[disabledIdx]?.disabled) {
40
+ if (runtimeLegend.items?.[idx]?.disabled) {
42
41
  return generateColorsArray()
43
42
  }
44
43
 
45
- const legendBinColor = runtimeLegend.items.find(o => o.bin === idx)?.color
46
- return generateColorsArray(legendBinColor, runtimeLegend.items[idx]?.special)
44
+ // Use the index from legendMemo which should already be updated for reordered items
45
+ const legendItem = runtimeLegend.items?.[idx]
46
+ const legendBinColor = legendItem?.color
47
+
48
+ return generateColorsArray(legendBinColor, legendItem?.special)
47
49
  } catch (e) {
48
50
  console.error('COVE: ', e)
49
51
  return null