@cdc/chart 4.24.12 → 4.25.2-25

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 (84) hide show
  1. package/dist/cdcchart.js +79900 -78999
  2. package/examples/feature/boxplot/boxplot.json +2 -157
  3. package/examples/feature/boxplot/testing.csv +23 -38
  4. package/examples/feature/tests-non-numerics/example-combo-bar-nonnumeric.json +579 -49
  5. package/examples/private/ehdi.json +29939 -0
  6. package/examples/private/line-issue.json +497 -0
  7. package/examples/private/not-loading.json +360 -0
  8. package/index.html +11 -15
  9. package/package.json +2 -2
  10. package/src/CdcChart.tsx +92 -1512
  11. package/src/CdcChartComponent.tsx +1113 -0
  12. package/src/ConfigContext.tsx +6 -1
  13. package/src/_stories/Chart.Anchors.stories.tsx +1 -1
  14. package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
  15. package/src/_stories/Chart.DynamicSeries.stories.tsx +17 -2
  16. package/src/_stories/Chart.Filters.stories.tsx +19 -0
  17. package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
  18. package/src/_stories/Chart.ScatterPlot.stories.tsx +19 -0
  19. package/src/_stories/Chart.tooltip.stories.tsx +1 -2
  20. package/src/_stories/ChartAnnotation.stories.tsx +1 -1
  21. package/src/_stories/ChartAxisLabels.stories.tsx +1 -1
  22. package/src/_stories/ChartAxisTitles.stories.tsx +1 -1
  23. package/src/_stories/ChartEditor.stories.tsx +1 -1
  24. package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
  25. package/src/_stories/ChartLine.Symbols.stories.tsx +18 -0
  26. package/src/_stories/ChartPrefixSuffix.stories.tsx +1 -1
  27. package/src/_stories/_mock/line_chart_symbols.json +437 -0
  28. package/src/_stories/_mock/scatterplot-image-download.json +1244 -0
  29. package/src/components/Annotations/components/AnnotationDraggable.tsx +3 -11
  30. package/src/components/Annotations/components/AnnotationDropdown.tsx +3 -3
  31. package/src/components/Axis/Categorical.Axis.tsx +3 -4
  32. package/src/components/BarChart/components/BarChart.Horizontal.tsx +14 -5
  33. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +10 -4
  34. package/src/components/BarChart/components/BarChart.Vertical.tsx +5 -7
  35. package/src/components/BarChart/components/BarChart.jsx +24 -4
  36. package/src/components/BarChart/components/context.tsx +1 -0
  37. package/src/components/BoxPlot/BoxPlot.tsx +34 -32
  38. package/src/components/BoxPlot/helpers/index.ts +108 -18
  39. package/src/components/BrushChart.tsx +44 -24
  40. package/src/components/DeviationBar.jsx +2 -6
  41. package/src/components/EditorPanel/EditorPanel.tsx +64 -8
  42. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +4 -0
  43. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +3 -1
  44. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +44 -7
  45. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +6 -1
  46. package/src/components/ForestPlot/ForestPlot.tsx +176 -26
  47. package/src/components/Legend/Legend.Component.tsx +29 -38
  48. package/src/components/Legend/Legend.Suppression.tsx +3 -5
  49. package/src/components/Legend/Legend.tsx +2 -2
  50. package/src/components/Legend/LegendLine.Shape.tsx +51 -0
  51. package/src/components/Legend/helpers/createFormatLabels.tsx +29 -26
  52. package/src/components/Legend/helpers/getLegendClasses.ts +20 -38
  53. package/src/components/Legend/helpers/index.ts +22 -9
  54. package/src/components/Legend/tests/getLegendClasses.test.ts +3 -20
  55. package/src/components/LineChart/components/LineChart.Circle.tsx +104 -94
  56. package/src/components/LineChart/index.tsx +6 -2
  57. package/src/components/LinearChart.tsx +77 -43
  58. package/src/components/PairedBarChart.jsx +2 -9
  59. package/src/components/ZoomBrush.tsx +5 -7
  60. package/src/data/initial-state.js +6 -3
  61. package/src/helpers/getBoxPlotConfig.ts +68 -0
  62. package/src/helpers/getColorScale.ts +24 -0
  63. package/src/helpers/getComboChartConfig.ts +42 -0
  64. package/src/helpers/getExcludedData.ts +37 -0
  65. package/src/helpers/getTopAxis.ts +7 -0
  66. package/src/helpers/isConvertLineToBarGraph.ts +10 -3
  67. package/src/hooks/useBarChart.ts +40 -13
  68. package/src/hooks/{useHighlightedBars.js → useHighlightedBars.ts} +2 -1
  69. package/src/hooks/useIntersectionObserver.ts +37 -0
  70. package/src/hooks/useMinMax.ts +11 -8
  71. package/src/hooks/useReduceData.ts +1 -1
  72. package/src/hooks/useScales.ts +10 -0
  73. package/src/hooks/useTooltip.tsx +21 -2
  74. package/src/index.jsx +1 -0
  75. package/src/scss/DataTable.scss +0 -5
  76. package/src/scss/main.scss +31 -116
  77. package/src/store/chart.actions.ts +40 -0
  78. package/src/store/chart.reducer.ts +83 -0
  79. package/src/types/ChartConfig.ts +6 -3
  80. package/src/types/ChartContext.ts +1 -3
  81. package/src/helpers/getQuartiles.ts +0 -27
  82. package/src/hooks/useColorScale.ts +0 -50
  83. package/src/hooks/useIntersectionObserver.jsx +0 -29
  84. package/src/hooks/useTopAxis.js +0 -6
@@ -1,6 +1,6 @@
1
1
  import { Group } from '@visx/group'
2
2
  import { useContext, useEffect, useRef, useState } from 'react'
3
- import ConfigContext from '../ConfigContext'
3
+ import ConfigContext, { ChartDispatchContext } from '../ConfigContext'
4
4
  import * as d3 from 'd3'
5
5
  import { Text } from '@visx/text'
6
6
  import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
@@ -12,7 +12,8 @@ interface BrushChartProps {
12
12
  }
13
13
 
14
14
  const BrushChart = ({ xMax, yMax }: BrushChartProps) => {
15
- const { tableData, config, setBrushConfig, dashboardConfig, formatDate, parseDate } = useContext(ConfigContext)
15
+ const { tableData, config, dashboardConfig, formatDate, parseDate } = useContext(ConfigContext)
16
+ const dispatch = useContext(ChartDispatchContext)
16
17
  const [brushState, setBrushState] = useState({ isBrushing: false, selection: [] })
17
18
  const [brushKey, setBrushKey] = useState(0)
18
19
  const sharedFilters = dashboardConfig?.dashboard?.sharedFilters ?? []
@@ -54,6 +55,9 @@ const BrushChart = ({ xMax, yMax }: BrushChartProps) => {
54
55
 
55
56
  const brushHandle = (g, selection, firstDate, lastDate) => {
56
57
  const textWidth = getTextWidth(firstDate, `normal ${16 / 1.1}px sans-serif`)
58
+ const textPositionLeft = selection[0] < textWidth ? 0 : -textWidth
59
+ const textPositionRight = xMax - selection[1] < textWidth ? -textWidth : 0
60
+
57
61
  return g
58
62
  .selectAll('.handle--custom')
59
63
  .data([{ side: 'left' }, { side: 'right' }])
@@ -61,7 +65,7 @@ const BrushChart = ({ xMax, yMax }: BrushChartProps) => {
61
65
  const handleGroup = enter.append('g').attr('class', 'handle--custom')
62
66
  handleGroup
63
67
  .append('text')
64
- .attr('x', d => (d.side === 'left' ? 0 : -textWidth))
68
+ .attr('x', d => (d.side === 'left' ? textPositionLeft : textPositionRight))
65
69
  .attr('y', 30)
66
70
  .text(d => (d.side === 'left' ? firstDate : lastDate))
67
71
  .attr('font-size', '13px')
@@ -102,22 +106,25 @@ const BrushChart = ({ xMax, yMax }: BrushChartProps) => {
102
106
  const sortedData = _.sortBy(newFilteredData, item => new Date(item[config.xAxis.dataKey]))
103
107
 
104
108
  // If ascending is false, reverse the sorted array
105
- const finalData = !sortByRecentDate ? sortedData : sortedData.reverse()
109
+ const finalData: object[] = !sortByRecentDate ? sortedData : sortedData.reverse()
106
110
 
107
111
  // Retrieve the start and end dates based on the sorted data array
108
- const startDate = _.get(_.first(finalData), config.xAxis.dataKey, '')
109
- const endDate = _.get(_.last(finalData), config.xAxis.dataKey, '')
112
+ const startDate: string = _.get(_.first(finalData), config.xAxis.dataKey, '')
113
+ const endDate: string = _.get(_.last(finalData), config.xAxis.dataKey, '')
110
114
  // add custom blue colored handlers to each corners of brush
111
115
  svg.selectAll('.handle--custom').remove()
112
- // append handler
113
- const [formattedStartDate, formattedEndDate] = [startDate, endDate].map(date => formatDate(parseDate(date)))
116
+ // Parse and format the dates, setting them to an empty string if undefined
117
+ const parseAndFormatDate = date => (date ? formatDate(parseDate(date)) : '')
118
+ const formattedStartDate = parseAndFormatDate(startDate)
119
+ const formattedEndDate = parseAndFormatDate(endDate)
114
120
  svg.call(brushHandle, selection, formattedStartDate, formattedEndDate)
115
121
 
116
- setBrushConfig({
122
+ const payload = {
117
123
  active: config.brush.active,
118
124
  isBrushing: isUserBrushing,
119
125
  data: finalData
120
- })
126
+ }
127
+ dispatch({ type: 'SET_BRUSH_CONFIG', payload: payload })
121
128
  setBrushState({
122
129
  isBrushing: true,
123
130
  selection
@@ -150,22 +157,35 @@ const BrushChart = ({ xMax, yMax }: BrushChartProps) => {
150
157
 
151
158
  if ((isFiltersActive || isExclusionsActive || isDashboardFilters) && config.brush?.active) {
152
159
  setBrushKey(prevKey => prevKey + 1)
153
- setBrushConfig(prev => {
154
- return {
155
- ...prev,
156
- data: tableData
157
- }
158
- })
159
160
  }
160
- return () =>
161
- setBrushConfig(prev => {
162
- return {
163
- ...prev,
164
- data: []
165
- }
166
- })
161
+ dispatch({ type: 'SET_BRUSH_CONFIG', payload: { ...config.brush, data: tableData } })
162
+
163
+ return () => dispatch({ type: 'SET_BRUSH_CONFIG', payload: { ...config.brush, data: [] } })
167
164
  }, [config.filters, config.exclusions, config.brush?.active, isDashboardFilters])
168
- // Initialize brush when component is first rendered
165
+
166
+ // this effect handles where brush chart is missing on production. it helpes re render
167
+ useEffect(() => {
168
+ let timeoutId = null
169
+
170
+ const checkAndInitializeBrush = () => {
171
+ if (xMax > 0) {
172
+ initializeBrush()
173
+ } else {
174
+ // Clear the existing timeout and set a new one
175
+ clearTimeout(timeoutId)
176
+ timeoutId = setTimeout(checkAndInitializeBrush, 500)
177
+ }
178
+ }
179
+
180
+ checkAndInitializeBrush()
181
+
182
+ // Cleanup function to clear timeout
183
+ return () => {
184
+ if (timeoutId) {
185
+ clearTimeout(timeoutId)
186
+ }
187
+ }
188
+ }, [xMax])
169
189
 
170
190
  // reset brush on keychange
171
191
  useEffect(() => {
@@ -30,7 +30,6 @@ export default function DeviationBar({ height, xScale }) {
30
30
  : roundingStyle === 'finger'
31
31
  ? '15px'
32
32
  : '0px'
33
- const fontSize = { small: 16, medium: 18, large: 20 }
34
33
  const isRounded = config.barStyle === 'rounded'
35
34
  const target = Number(config.xAxis.target)
36
35
  const seriesKey = config.series[0].dataKey
@@ -65,7 +64,7 @@ export default function DeviationBar({ height, xScale }) {
65
64
  const firstBarValue = data[0][seriesKey]
66
65
  const barPosition = firstBarValue < target ? 'left' : 'right'
67
66
  const label = `${config.xAxis.targetLabel} ${formatNumber(config.xAxis.target || 0, 'left')}`
68
- const labelWidth = getTextWidth(label, `bold ${fontSize[config.fontSize]}px sans-serif`)
67
+ const labelWidth = getTextWidth(label, `bold ${20}px sans-serif`)
69
68
  let labelY = config.isLollipopChart ? lollipopBarHeight / 2 : Number(config.barHeight) / 2
70
69
  let paddingX = 0
71
70
  let labelX = 0
@@ -165,10 +164,7 @@ export default function DeviationBar({ height, xScale }) {
165
164
  config.heights.horizontal = totalheight
166
165
 
167
166
  // text,labels postiions
168
- const textWidth = getTextWidth(
169
- formatNumber(barValue, 'left'),
170
- `normal ${fontSize[config.fontSize]}px sans-serif`
171
- )
167
+ const textWidth = getTextWidth(formatNumber(barValue, 'left'), `normal ${16}px sans-serif`)
172
168
  const textFits = textWidth < barWidth - 6
173
169
  const textX = barBaseX
174
170
  const textY = barY + barHeight / 2
@@ -596,7 +596,7 @@ const EditorPanel = () => {
596
596
  updateConfig,
597
597
  tableData,
598
598
  transformedData: data,
599
- loading,
599
+ isLoading,
600
600
  colorScale,
601
601
  colorPalettes,
602
602
  twoColorPalette,
@@ -610,7 +610,7 @@ const EditorPanel = () => {
610
610
  lineOptions,
611
611
  rawData,
612
612
  highlight,
613
- highlightReset,
613
+ handleShowAll,
614
614
  dimensions
615
615
  } = useContext<ChartContext>(ConfigContext)
616
616
 
@@ -813,7 +813,7 @@ const EditorPanel = () => {
813
813
  const [displayPanel, setDisplayPanel] = useState(true)
814
814
  const [displayViewportOverrides, setDisplayViewportOverrides] = useState(false)
815
815
 
816
- if (loading) {
816
+ if (isLoading) {
817
817
  return null
818
818
  }
819
819
 
@@ -906,7 +906,7 @@ const EditorPanel = () => {
906
906
  return Object.keys(columns)
907
907
  }
908
908
 
909
- const getLegendStyleOptions = (option: 'style' | 'subStyle'): string[] => {
909
+ const getLegendStyleOptions = (option: 'style' | 'subStyle' | 'shapes'): string[] => {
910
910
  const options: string[] = []
911
911
 
912
912
  switch (option) {
@@ -963,7 +963,7 @@ const EditorPanel = () => {
963
963
 
964
964
  const convertStateToConfig = () => {
965
965
  let strippedState = JSON.parse(JSON.stringify(config))
966
- if (false === missingRequiredSections()) {
966
+ if (false === missingRequiredSections(config)) {
967
967
  delete strippedState.newViz
968
968
  }
969
969
  delete strippedState.runtime
@@ -1631,8 +1631,19 @@ const EditorPanel = () => {
1631
1631
  value={config.yAxis.label}
1632
1632
  section='yAxis'
1633
1633
  fieldName='label'
1634
- label='Label '
1634
+ label='Label'
1635
1635
  updateField={updateField}
1636
+ maxLength={35}
1637
+ tooltip={
1638
+ <Tooltip style={{ textTransform: 'none' }}>
1639
+ <Tooltip.Target>
1640
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
1641
+ </Tooltip.Target>
1642
+ <Tooltip.Content>
1643
+ <p>35 character limit</p>
1644
+ </Tooltip.Content>
1645
+ </Tooltip>
1646
+ }
1636
1647
  />
1637
1648
  {config.runtime.seriesKeys &&
1638
1649
  config.runtime.seriesKeys.length === 1 &&
@@ -2306,6 +2317,17 @@ const EditorPanel = () => {
2306
2317
  fieldName='rightLabel'
2307
2318
  label='Label'
2308
2319
  updateField={updateField}
2320
+ maxLength={35}
2321
+ tooltip={
2322
+ <Tooltip style={{ textTransform: 'none' }}>
2323
+ <Tooltip.Target>
2324
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
2325
+ </Tooltip.Target>
2326
+ <Tooltip.Content>
2327
+ <p>35 character limit</p>
2328
+ </Tooltip.Content>
2329
+ </Tooltip>
2330
+ }
2309
2331
  />
2310
2332
  <TextField
2311
2333
  value={config.yAxis.rightNumTicks}
@@ -2596,6 +2618,17 @@ const EditorPanel = () => {
2596
2618
  fieldName='label'
2597
2619
  label='Label'
2598
2620
  updateField={updateField}
2621
+ maxLength={35}
2622
+ tooltip={
2623
+ <Tooltip style={{ textTransform: 'none' }}>
2624
+ <Tooltip.Target>
2625
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
2626
+ </Tooltip.Target>
2627
+ <Tooltip.Content>
2628
+ <p>35 character limit</p>
2629
+ </Tooltip.Content>
2630
+ </Tooltip>
2631
+ }
2599
2632
  />
2600
2633
 
2601
2634
  {config.xAxis.type === 'continuous' && (
@@ -3658,6 +3691,27 @@ const EditorPanel = () => {
3658
3691
  updateField={updateField}
3659
3692
  options={getLegendStyleOptions('style')}
3660
3693
  />
3694
+ <CheckBox
3695
+ tooltip={
3696
+ <Tooltip style={{ textTransform: 'none' }}>
3697
+ <Tooltip.Target>
3698
+ <Icon
3699
+ display='question'
3700
+ style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }}
3701
+ />
3702
+ </Tooltip.Target>
3703
+ <Tooltip.Content>
3704
+ <p>Choose option Shapes in Line Datapoint Symbols to display.</p>
3705
+ </Tooltip.Content>
3706
+ </Tooltip>
3707
+ }
3708
+ display={!config.legend.hide && config.legend.style === 'lines'}
3709
+ value={config.legend.hasShape}
3710
+ section='legend'
3711
+ fieldName='hasShape'
3712
+ label='Shapes'
3713
+ updateField={updateField}
3714
+ />
3661
3715
 
3662
3716
  <Select
3663
3717
  display={!config.legend.hide && config.legend.style === 'gradient'}
@@ -3797,7 +3851,7 @@ const EditorPanel = () => {
3797
3851
  updatedSeriesHighlight.splice(i, 1)
3798
3852
  updateField('legend', null, 'seriesHighlight', updatedSeriesHighlight)
3799
3853
  if (!updatedSeriesHighlight.length) {
3800
- highlightReset()
3854
+ handleShowAll()
3801
3855
  }
3802
3856
  }}
3803
3857
  >
@@ -3887,7 +3941,9 @@ const EditorPanel = () => {
3887
3941
  display={
3888
3942
  ['bottom', 'top'].includes(config.legend.position) &&
3889
3943
  !config.legend.hide &&
3890
- config.legend.style !== 'gradient'
3944
+ config.legend.style !== 'gradient' &&
3945
+ !config.legend.singleRow &&
3946
+ !config.legend.singleRow
3891
3947
  }
3892
3948
  value={config.legend.verticalSorted}
3893
3949
  section='legend'
@@ -209,6 +209,10 @@ const PanelGeneral: FC<PanelProps> = props => {
209
209
  <Icon display='question' style={{ marginLeft: '0.5rem' }} />
210
210
  </Tooltip.Target>
211
211
  <Tooltip.Content>
212
+ <p>
213
+ Recommended set to display for Section 508 compliance.
214
+ </p>
215
+ <hr/>
212
216
  <p>
213
217
  Selecting this option will <i> not </i> hide the display of "zero value", "suppressed data", or
214
218
  "missing data" indicators on the chart (if applicable).
@@ -500,6 +500,8 @@ const SeriesInputName = props => {
500
500
 
501
501
  updateConfig(newConfig)
502
502
  }
503
+ // if series name is emty show default data value.
504
+ const value = series.name !== undefined && series.name !== series.dataKey ? series.name : series.dataKey
503
505
 
504
506
  return (
505
507
  <>
@@ -507,7 +509,7 @@ const SeriesInputName = props => {
507
509
  <input
508
510
  type='text'
509
511
  key={`series-name-${i}`}
510
- value={series.name ? series.name : ''}
512
+ value={value}
511
513
  onChange={event => {
512
514
  changeSeriesName(i, event.target.value)
513
515
  }}
@@ -136,15 +136,23 @@ const PanelVisual: FC<PanelProps> = props => {
136
136
  />
137
137
  </fieldset>
138
138
  )}
139
- <Select
140
- value={config.fontSize}
141
- fieldName='fontSize'
142
- label='Font Size'
143
- updateField={updateField}
144
- options={['small', 'medium', 'large']}
145
- />
146
139
  {visHasBarBorders() && (
147
140
  <Select
141
+ tooltip={
142
+ <Tooltip style={{ textTransform: 'none' }}>
143
+ <Tooltip.Target>
144
+ <Icon
145
+ display='question'
146
+ style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }}
147
+ />
148
+ </Tooltip.Target>
149
+ <Tooltip.Content>
150
+ <p>
151
+ Recommended set to display for Section 508 compliance.
152
+ </p>
153
+ </Tooltip.Content>
154
+ </Tooltip>
155
+ }
148
156
  value={config.barHasBorder}
149
157
  fieldName='barHasBorder'
150
158
  label='Bar Borders'
@@ -171,6 +179,35 @@ const PanelVisual: FC<PanelProps> = props => {
171
179
  config.visualizationType === 'Combo') ||
172
180
  config.visualizationType === 'Line') && (
173
181
  <>
182
+ <Select
183
+ tooltip={
184
+ <Tooltip style={{ textTransform: 'none' }}>
185
+ <Tooltip.Target>
186
+ <Icon
187
+ display='question'
188
+ style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }}
189
+ />
190
+ </Tooltip.Target>
191
+ <Tooltip.Content>
192
+ <p>
193
+ Shapes will appear in the following order: circle, square, triangle, diamond, and inverted
194
+ triangle. Use with a maximum of 5 data points.
195
+ </p>
196
+ </Tooltip.Content>
197
+ </Tooltip>
198
+ }
199
+ value={config.visual.lineDatapointSymbol}
200
+ section='visual'
201
+ fieldName='lineDatapointSymbol'
202
+ label='Line Datapoint Symbols'
203
+ updateField={updateField}
204
+ options={['none', 'standard']}
205
+ />
206
+ {config.series.length > config.visual.maximumShapeAmount &&
207
+ config.visual.lineDatapointSymbol === 'standard' && (
208
+ <small className='text-danger'>Standard only supports up to 7 data points</small>
209
+ )}
210
+
174
211
  <Select
175
212
  value={config.lineDatapointStyle}
176
213
  fieldName='lineDatapointStyle'
@@ -30,7 +30,12 @@ export const updateFieldRankByValue = (
30
30
  newConfig.rankByValue = newValue
31
31
 
32
32
  if (config.rankByValue && !newValue) {
33
- const cleanData = config?.xAxis?.dataKey ? transform.cleanData(config.data, config.xAxis.dataKey) : config.data
33
+ const CIkeys: string[] = Object.values(config.confidenceKeys) as string[]
34
+ const seriesKeys: string[] = config.series.map(s => s.dataKey)
35
+ const keysToClean: string[] = seriesKeys.concat(CIkeys)
36
+ const cleanData = config?.xAxis?.dataKey
37
+ ? transform.cleanData(config.data, config.xAxis.dataKey, keysToClean)
38
+ : config.data
34
39
  const newData = preTransformedData.sort((a, b) => {
35
40
  const aIndex = indexOfObj(cleanData, a)
36
41
  const bIndex = indexOfObj(cleanData, b)