@cdc/core 4.23.3 → 4.23.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.
@@ -0,0 +1,421 @@
1
+ import React, { useState, useEffect, useRef } from 'react'
2
+ import { useId } from 'react'
3
+
4
+ // CDC
5
+ import Button from '@cdc/core/components/elements/Button'
6
+
7
+ // Third Party
8
+ import PropTypes from 'prop-types'
9
+
10
+ export const useFilters = props => {
11
+ const [showApplyButton, setShowApplyButton] = useState(false)
12
+
13
+ // Desconstructing: notice, adding more descriptive visualizationConfig name over config
14
+ // visualizationConfig feels more robust for all vis types so that its not confused with config/state/etc.
15
+ const { config: visualizationConfig, setConfig, filteredData, setFilteredData, excludedData, filterData } = props
16
+ const { type, filterBehavior, filters } = visualizationConfig
17
+
18
+ const filterStyleOptions = ['dropdown', 'pill', 'tab', 'tab bar']
19
+
20
+ const filterOrderOptions = [
21
+ {
22
+ label: 'Ascending Alphanumeric',
23
+ value: 'asc'
24
+ },
25
+ {
26
+ label: 'Descending Alphanumeric',
27
+ value: 'desc'
28
+ },
29
+ {
30
+ label: 'Custom',
31
+ value: 'cust'
32
+ }
33
+ ]
34
+
35
+ /**
36
+ * Re-orders a filter based on two indices and updates the runtime filters array and filters state
37
+ * @param {number} idx1 - The index of the original position of the filter value.
38
+ * @param {number} idx2 - The index of the new position for the filter value.
39
+ * @param {number} filterIndex - The index of the filter item within the array of filter items.
40
+ * @param {object} filter - The filter item itself, which contains an array of filter values.
41
+ * @return {void} None. This function only updates the state of the component.
42
+ *
43
+ * @modifies {object} - The filter object passed in as a parameter
44
+ * @modifies {array} - The filteredData state if visualizationConfig.type equals 'map'
45
+ * @modifies {object} - The visualizationConfig state
46
+ */
47
+ const handleFilterOrder = (idx1, idx2, filterIndex, filter) => {
48
+ // Create a shallow copy of the filter values array & update position of the values
49
+ const updatedValues = [...filter.values]
50
+ const [movedItem] = updatedValues.splice(idx1, 1)
51
+ updatedValues.splice(idx2, 0, movedItem)
52
+
53
+ const filtersCopy = visualizationConfig.type === 'chart' ? [...visualizationConfig.filters] : [...filteredData]
54
+ const filterItem = { ...filtersCopy[filterIndex] }
55
+
56
+ // Overwrite filterItem.values since thats what we map through in the editor panel
57
+ filterItem.values = updatedValues
58
+ filterItem.orderedValues = updatedValues
59
+ filterItem.active = updatedValues[0]
60
+ filterItem.order = 'cust'
61
+
62
+ // Update the filters
63
+ filtersCopy[filterIndex] = filterItem
64
+
65
+ if (visualizationConfig.type === 'map') {
66
+ setFilteredData(filtersCopy)
67
+ }
68
+
69
+ setConfig({ ...visualizationConfig, filters: filtersCopy })
70
+ }
71
+
72
+ const announceChange = text => {}
73
+
74
+ const changeFilterActive = (index, value) => {
75
+ let newFilters = visualizationConfig.type === 'map' ? [...filteredData] : [...visualizationConfig.filters]
76
+
77
+ newFilters[index].active = value
78
+ setConfig({ ...visualizationConfig })
79
+
80
+ // If this is a button filter type show the button.
81
+ if (visualizationConfig.filterBehavior === 'Apply Button') {
82
+ setShowApplyButton(true)
83
+ }
84
+
85
+ // If we're not using the apply button we can set the filters right away.
86
+ if (visualizationConfig.filterBehavior !== 'Apply Button') {
87
+ setConfig({
88
+ ...visualizationConfig,
89
+ filters: newFilters
90
+ })
91
+ }
92
+
93
+ // Used for setting active filter, fromHash breaks the filteredData functionality.
94
+ if (visualizationConfig.type === 'map' && visualizationConfig.filterBehavior === 'Filter Change') {
95
+ setFilteredData(newFilters)
96
+ }
97
+
98
+ // If we're on a chart and not using the apply button
99
+ if (visualizationConfig.type === 'chart' && visualizationConfig.filterBehavior === 'Filter Change') {
100
+ setFilteredData(filterData(newFilters, excludedData))
101
+ }
102
+ }
103
+
104
+ const handleApplyButton = newFilters => {
105
+ setConfig({ ...visualizationConfig, filters: newFilters })
106
+
107
+ if (type === 'map') {
108
+ setFilteredData(newFilters, excludedData)
109
+ }
110
+
111
+ if (type === 'chart') {
112
+ setFilteredData(filterData(newFilters, excludedData))
113
+ }
114
+
115
+ setShowApplyButton(false)
116
+ }
117
+
118
+ const handleReset = e => {
119
+ let newFilters = [...visualizationConfig.filters]
120
+ e.preventDefault()
121
+
122
+ // reset to first item in values array.
123
+ newFilters.map(filter => {
124
+ filter = handleSorting(filter)
125
+ filter.active = filter.values[0]
126
+ return filter
127
+ })
128
+
129
+ if (type === 'map') {
130
+ setFilteredData(newFilters, excludedData)
131
+ } else {
132
+ setFilteredData(filterData(newFilters, excludedData))
133
+ }
134
+
135
+ setConfig({ ...visualizationConfig, filters: newFilters })
136
+ }
137
+
138
+ const filterConstants = {
139
+ buttonText: 'Apply Filters',
140
+ resetText: 'Reset All',
141
+ introText: `Make a selection from the filters to change the visualization information.`,
142
+ applyText: 'Select the apply button to update the visualization information.'
143
+ }
144
+
145
+ const handleSorting = singleFilter => {
146
+ const { order } = singleFilter
147
+
148
+ const sortAsc = (a, b) => {
149
+ return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
150
+ }
151
+
152
+ const sortDesc = (a, b) => {
153
+ return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
154
+ }
155
+
156
+ if (!order || order === '') {
157
+ singleFilter.order = 'asc'
158
+ }
159
+
160
+ if (order === 'desc') {
161
+ singleFilter.values = singleFilter.values.sort(sortDesc)
162
+ }
163
+
164
+ if (order === 'asc') {
165
+ singleFilter.values = singleFilter.values.sort(sortAsc)
166
+ }
167
+ return singleFilter
168
+ }
169
+
170
+ // prettier-ignore
171
+ return {
172
+ handleApplyButton,
173
+ changeFilterActive,
174
+ announceChange,
175
+ showApplyButton,
176
+ handleReset,
177
+ filterConstants,
178
+ filterStyleOptions,
179
+ filterOrderOptions,
180
+ handleFilterOrder,
181
+ handleSorting
182
+ }
183
+ }
184
+
185
+ const Filters = props => {
186
+ const { config: visualizationConfig, filteredData, dimensions } = props
187
+ const { filters, type, general, theme, filterBehavior } = visualizationConfig
188
+ const [mobileFilterStyle, setMobileFilterStyle] = useState(false)
189
+ const [selectedFilter, setSelectedFilter] = useState('')
190
+ const id = useId()
191
+
192
+ // useFilters hook provides data and logic for handling various filter functions
193
+ // prettier-ignore
194
+ const {
195
+ handleApplyButton,
196
+ changeFilterActive,
197
+ announceChange,
198
+ showApplyButton,
199
+ handleReset,
200
+ filterConstants,
201
+ handleSorting
202
+ } = useFilters(props)
203
+
204
+ useEffect(() => {
205
+ if (!dimensions) return
206
+ if (dimensions[0] < 768 && filters?.length > 0) {
207
+ setMobileFilterStyle(true)
208
+ } else {
209
+ setMobileFilterStyle(false)
210
+ }
211
+ }, [dimensions])
212
+
213
+ useEffect(() => {
214
+ if (selectedFilter) {
215
+ let el = document.getElementById(selectedFilter.id)
216
+ if (el) el.focus()
217
+ }
218
+ }, [changeFilterActive, selectedFilter])
219
+
220
+ const Filters = props => props.children
221
+
222
+ const filterSectionClassList = ['filters-section', type === 'map' ? general.headerColor : theme]
223
+
224
+ // Exterior Section Wrapper
225
+ Filters.Section = props => {
226
+ return (
227
+ <section className={filterSectionClassList.join(' ')}>
228
+ <p className='filters-section__intro-text'>
229
+ {filterConstants.introText} {visualizationConfig.filterBehavior === 'Apply Button' && filterConstants.applyText}
230
+ </p>
231
+ <div className='filters-section__wrapper'>{props.children}</div>
232
+ </section>
233
+ )
234
+ }
235
+
236
+ // Apply/Reset Buttons
237
+ Filters.ApplyBehavior = props => {
238
+ if (filterBehavior !== 'Apply Button') return
239
+ const applyButtonClasses = [general?.headerColor ? general.headerColor : theme, 'apply']
240
+ return (
241
+ <div className='filters-section__buttons'>
242
+ <Button onClick={() => handleApplyButton(filters)} disabled={!showApplyButton} className={applyButtonClasses.join(' ')}>
243
+ {filterConstants.buttonText}
244
+ </Button>
245
+ <a href='#!' role='button' onClick={handleReset}>
246
+ {filterConstants.resetText}
247
+ </a>
248
+ </div>
249
+ )
250
+ }
251
+
252
+ Filters.TabBar = props => {
253
+ const { filter: singleFilter, index: outerIndex } = props
254
+ return (
255
+ <section className='single-filters__tab-bar'>
256
+ {singleFilter.values.map((filter, index) => {
257
+ const buttonClassList = ['button__tab-bar', singleFilter.active === filter ? 'button__tab-bar--active' : '']
258
+ return (
259
+ <button
260
+ id={`${filter}-${outerIndex}-${index}-${id}`}
261
+ className={buttonClassList.join(' ')}
262
+ key={filter}
263
+ onClick={e => {
264
+ changeFilterActive(outerIndex, filter)
265
+ setSelectedFilter(e.target)
266
+ }}
267
+ onKeyDown={e => {
268
+ if (e.keyCode === 13) {
269
+ changeFilterActive(outerIndex, filter)
270
+ setSelectedFilter(e.target)
271
+ }
272
+ }}
273
+ >
274
+ {filter}
275
+ </button>
276
+ )
277
+ })}
278
+ </section>
279
+ )
280
+ }
281
+
282
+ Filters.Pills = props => props.pills
283
+
284
+ Filters.Tabs = props => props.tabs
285
+
286
+ Filters.Dropdown = props => {
287
+ const { index: outerIndex, label, active, filters } = props
288
+ return (
289
+ <select
290
+ id={`filter-${outerIndex}`}
291
+ name={label}
292
+ className='filter-select'
293
+ data-index='0'
294
+ value={active}
295
+ onChange={e => {
296
+ changeFilterActive(outerIndex, e.target.value)
297
+ announceChange(`Filter ${label} value has been changed to ${e.target.value}, please reference the data table to see updated values.`)
298
+ }}
299
+ >
300
+ {filters}
301
+ </select>
302
+ )
303
+ }
304
+
305
+ // Resolve Filter Styles
306
+ Filters.Style = () => {
307
+ if (filters || filteredData) {
308
+ // Here charts is using config.filters where maps is using a runtime value
309
+ let filtersToLoop = type === 'map' ? filteredData : filters
310
+
311
+ // Remove fromHash if it exists on filters to loop so we can loop nicely
312
+ delete filtersToLoop.fromHash
313
+
314
+ return filtersToLoop.map((singleFilter, outerIndex) => {
315
+ const values = []
316
+ const pillValues = []
317
+ const tabValues = []
318
+ const tabBarValues = []
319
+
320
+ const { active, label, filterStyle } = singleFilter
321
+
322
+ handleSorting(singleFilter)
323
+
324
+ singleFilter.values.forEach((filterOption, index) => {
325
+ const pillClassList = ['pill', active === filterOption ? 'pill--active' : null, theme && theme]
326
+ const tabClassList = ['tab', active === filterOption && 'tab--active', theme && theme]
327
+
328
+ pillValues.push(
329
+ <div className='pill__wrapper' key={`pill-${index}`}>
330
+ <button
331
+ id={`${filterOption}-${outerIndex}-${index}-${id}`}
332
+ className={pillClassList.join(' ')}
333
+ onKeyDown={e => {
334
+ if (e.keyCode === 13) {
335
+ changeFilterActive(outerIndex, filterOption)
336
+ setSelectedFilter(e.target)
337
+ }
338
+ }}
339
+ onClick={e => {
340
+ changeFilterActive(outerIndex, filterOption)
341
+ setSelectedFilter(e.target)
342
+ }}
343
+ name={label}
344
+ >
345
+ {filterOption}
346
+ </button>
347
+ </div>
348
+ )
349
+
350
+ values.push(
351
+ <option key={index} value={filterOption}>
352
+ {singleFilter.labels && singleFilter.labels[filterOption] ? singleFilter.labels[filterOption] : filterOption}
353
+ </option>
354
+ )
355
+
356
+ tabValues.push(
357
+ <button
358
+ id={`${filterOption}-${outerIndex}-${index}-${id}`}
359
+ className={tabClassList.join(' ')}
360
+ onClick={e => {
361
+ changeFilterActive(outerIndex, filterOption)
362
+ setSelectedFilter(e.target)
363
+ }}
364
+ onKeyDown={e => {
365
+ if (e.keyCode === 13) {
366
+ changeFilterActive(outerIndex, filterOption)
367
+ setSelectedFilter(e.target)
368
+ }
369
+ }}
370
+ >
371
+ {filterOption}
372
+ </button>
373
+ )
374
+
375
+ tabBarValues.push(filterOption)
376
+ })
377
+
378
+ const classList = ['single-filters', mobileFilterStyle ? 'single-filters--dropdown' : `single-filters--${filterStyle}`]
379
+
380
+ return (
381
+ <div className={classList.join(' ')} key={outerIndex}>
382
+ <>
383
+ {label && <label htmlFor={label}>{label}</label>}
384
+ {filterStyle === 'tab' && !mobileFilterStyle && <Filters.Tabs tabs={tabValues} />}
385
+ {filterStyle === 'pill' && !mobileFilterStyle && <Filters.Pills pills={pillValues} />}
386
+ {filterStyle === 'tab bar' && !mobileFilterStyle && <Filters.TabBar filter={singleFilter} index={outerIndex} />}
387
+ {(filterStyle === 'dropdown' || mobileFilterStyle) && <Filters.Dropdown index={outerIndex} label={label} active={active} filters={values} />}
388
+ </>
389
+ </div>
390
+ )
391
+ })
392
+ }
393
+ }
394
+
395
+ if (visualizationConfig?.filters?.length === 0 || props?.filteredData?.length === 0) return
396
+ return (
397
+ <Filters>
398
+ <Filters.Section>
399
+ <Filters.Style />
400
+ <Filters.ApplyBehavior />
401
+ </Filters.Section>
402
+ </Filters>
403
+ )
404
+ }
405
+
406
+ Filters.propTypes = {
407
+ // runtimeFilters in place
408
+ filteredData: PropTypes.array,
409
+ // function for updating the runtime filters
410
+ setFilteredData: PropTypes.func,
411
+ // the full apps config
412
+ config: PropTypes.object,
413
+ // updating function for setting fitlerBehavior
414
+ setConfig: PropTypes.func,
415
+ // exclusions
416
+ excludedData: PropTypes.array,
417
+ // function for filtering the data
418
+ filterData: PropTypes.func
419
+ }
420
+
421
+ export default Filters
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
 
3
- export default function LegendCircle({ fill }) {
3
+ export default function LegendCircle({ fill, borderColor }) {
4
4
  const styles = {
5
5
  marginRight: '5px',
6
6
  borderRadius: '300px',
@@ -8,7 +8,7 @@ export default function LegendCircle({ fill }) {
8
8
  display: 'inline-block',
9
9
  height: '1em',
10
10
  width: '1em',
11
- border: 'rgba(0,0,0,.3) 1px solid',
11
+ border: borderColor ? `${borderColor} 1px solid` : 'rgba(0,0,0,.3) 1px solid',
12
12
  backgroundColor: fill
13
13
  }
14
14
 
@@ -74,7 +74,8 @@ const Icon = ({ display = null, base, alt = '', size, color, style, ...attribute
74
74
  const styles = {
75
75
  ...style,
76
76
  color: color ? color : null,
77
- width: size ? size + 'px' : null
77
+ width: size ? size + 'px' : null,
78
+ cursor: display === 'move' ? 'move' : 'default'
78
79
  }
79
80
 
80
81
  return (
@@ -51,16 +51,16 @@ export const colorPalettes3 = {
51
51
  'monochrome-4': ['#C2C0FC', '#6a3d9a'],
52
52
  'monochrome-5': ['#fedab8', '#bf5b17'],
53
53
  'cool-1': ['#b2df8a', '#1f78b4'],
54
- 'cool-2': ['#a6cee3', '#33A02C'],
54
+ 'cool-2': ['#a6cee3', '#72D66B'],
55
55
  'cool-3': ['#C2C0FC', '#386cb0'],
56
- 'cool-4': ['#33A02c', '#6a3d9a'],
56
+ 'cool-4': ['#72D66B', '#6a3d9a'],
57
57
  'cool-5': ['#a6cee3', '#6a3d9a'],
58
58
  'warm-1': ['#e31a1c', '#fedab8'],
59
59
  'complementary-1': ['#1f78b4', '#e6ab02'],
60
60
  'complementary-2': ['#1f78b4', '#ff7f00'],
61
61
  'complementary-3': ['#6a3d9a', '#ff7f00'],
62
62
  'complementary-4': ['#6a3d9a', '#e6ab02'],
63
- 'complementary-5': ['#e31a90', '#1b9e77']
63
+ 'complementary-5': ['#df168c', '#1EB386']
64
64
  }
65
65
 
66
66
  export const colorPalettesChart = updatePaletteNames(colorPalettes2)
@@ -196,6 +196,60 @@ export class DataTransform {
196
196
 
197
197
  return data;
198
198
  }
199
+
200
+ /**
201
+ * cleanData
202
+ *
203
+ // This cleans a data set by:
204
+ // - removing commas and $ signs from any numbers to try to plot the point
205
+ // - removing any data points that are NOT composed of of all digits (but allow a decimal point)
206
+ // Without this the charts "break" and do not render
207
+ *
208
+ * Inputs: data as array, excludeKey indicates which key to use to NOT clean
209
+ * Example: "Date" should not be cleaned if part of the data
210
+ *
211
+ * Output: returns the cleanedData
212
+ *
213
+ * Set testing = true if you need to see before and after data
214
+ *
215
+ */
216
+ cleanData (data, excludeKey, testing = false) {
217
+ let cleanedupData = []
218
+ if (testing) console.log('## Data to clean=', data)
219
+ if (excludeKey === undefined) {
220
+ console.log('COVE: cleanData excludeKey undefined')
221
+ return data // because no excludeKey
222
+ }
223
+ data.forEach(function (d, i) {
224
+ if (testing) console.log("clean", i, " d", d);
225
+ let cleaned = {}
226
+ Object.keys(d).forEach(function (key) {
227
+ if (key === excludeKey) {
228
+ // pass thru
229
+ cleaned[key] = d[key]
230
+ } else {
231
+ // remove comma and dollar signs
232
+ if (testing) console.log("typeof d[key] is ", typeof d[key]);
233
+ let tmp = "";
234
+ if (typeof d[key] === 'string') {
235
+ tmp = d[key] !== null && d[key] !== '' ? d[key].replace(/[,\$]/g, '') : ''
236
+ } else {
237
+ tmp = d[key] !== null && d[key] !== '' ? d[key] : ''
238
+ }
239
+ if ((tmp !== '' && tmp !== null && !isNaN(tmp)) || (tmp !== '' && tmp !== null && /\d+\.?\d*/.test(tmp))) {
240
+ cleaned[key] = tmp
241
+ } else { cleaned[key] = '' }
242
+ // if you get here, then return nothing to skip bad data point
243
+ }
244
+ })
245
+ if (testing) console.log("cleaned=", cleaned)
246
+ cleanedupData.push(cleaned)
247
+ })
248
+ if (testing) console.log('## cleanedData =', cleanedupData)
249
+ return cleanedupData
250
+ }
251
+
252
+
199
253
  }
200
254
 
201
255
  export default DataTransform
@@ -1,7 +1,7 @@
1
1
  export default function isNumberLog(value = '', state = null) {
2
2
  // if you need to check data to see if there is junk in there that can't be handled
3
3
  // you can run the points through this and see values on the console
4
- console.log("entering isNumber valuetype is:",typeof value);
4
+ console.log("entering isNumberLog value, valuetype:",value,typeof value);
5
5
  var test;
6
6
  if (typeof value === 'number') {
7
7
  test = !Number.isNaN(value)
@@ -10,9 +10,9 @@ export default function isNumberLog(value = '', state = null) {
10
10
  test = value !== null && value !== '' && /\d+\.?\d*/.test(value)
11
11
  }
12
12
  if (test === false) {
13
- console.log('# isNumber FALSE on value, result', value, test)
13
+ console.log('# isNumberLog FALSE on value, result', value, test)
14
14
  } else {
15
- console.log('# isNumber TRUE on value, result', value, test)
15
+ console.log('# isNumberLog TRUE on value, result', value, test)
16
16
  }
17
17
  return test
18
18
  }
@@ -28,7 +28,7 @@ export default function validateFipsCodeLength(stateOrData) {
28
28
  }
29
29
 
30
30
  // Only includes data - get column name from somewhere else
31
- if (Array.isArray(stateOrData)) {
31
+ if (Array.isArray(stateOrData) && stateOrData.length > 0) {
32
32
  let columns = Object.keys(stateOrData[0])
33
33
 
34
34
  let potentialColumnNames = ['fips', 'FIPS', 'fips codes', 'FIPS CODES', 'Fips Codes', 'fips Codes', 'Fips codes', 'FIPS Codes']
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cdc/core",
3
- "version": "4.23.3",
3
+ "version": "4.23.5",
4
4
  "description": "Core components, styles, hooks, and helpers, for the CDC Open Visualization project",
5
5
  "moduleName": "CdcCore",
6
6
  "main": "dist/cdccore",
@@ -30,5 +30,5 @@
30
30
  "react": "^18.2.0",
31
31
  "react-dom": "^18.2.0"
32
32
  },
33
- "gitHead": "6fa3b11db159d38538f18023fe70b67a29e7d327"
33
+ "gitHead": "34add3436994ca3cf13e51f313add4d70377f53e"
34
34
  }
@@ -39,6 +39,7 @@ table.data-table {
39
39
  border-collapse: collapse;
40
40
  overflow: auto;
41
41
  appearance: none;
42
+ table-layout: fixed;
42
43
  * {
43
44
  box-sizing: border-box;
44
45
  }
@@ -172,6 +173,15 @@ table.data-table {
172
173
  margin-left: 5px;
173
174
  }
174
175
  }
176
+
177
+ .boxplot-td {
178
+ //display: inline-block;
179
+ //box-sizing: border-box;
180
+ table-layout: fixed;
181
+ width: 200;
182
+ //min-width: 150px;
183
+ //max-width: 400px;
184
+ }
175
185
  }
176
186
 
177
187
  .no-data {
@@ -189,3 +189,8 @@ section.footnotes {
189
189
  .cdc-chart-inner-container .subtext {
190
190
  padding: 15px;
191
191
  }
192
+
193
+ .margin-left-href {
194
+ margin-left: 15px;
195
+ }
196
+