@cdc/dashboard 1.1.2 → 4.22.10

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 (35) hide show
  1. package/dist/cdcdashboard.js +159 -48
  2. package/examples/default-filter-control.json +175 -0
  3. package/examples/default-multi-dataset.json +498 -0
  4. package/examples/default.json +36 -348
  5. package/examples/private/chart-issue.json +3467 -0
  6. package/examples/private/no-issue.json +3467 -0
  7. package/examples/private/totals-two.json +104 -0
  8. package/examples/private/totals.json +103 -0
  9. package/examples/temp-example-data.json +130 -0
  10. package/examples/test-example.json +1 -0
  11. package/package.json +14 -8
  12. package/src/CdcDashboard.js +279 -156
  13. package/src/CdcDashboard.jsx +697 -0
  14. package/src/{context.tsx → ConfigContext.js} +0 -0
  15. package/src/components/{Column.js → Column.jsx} +9 -7
  16. package/src/components/DataTable.tsx +57 -55
  17. package/src/components/EditorPanel.js +207 -45
  18. package/src/components/{Grid.js → Grid.jsx} +5 -4
  19. package/src/components/Header.jsx +246 -0
  20. package/src/components/Row.js +1 -0
  21. package/src/components/Row.jsx +181 -0
  22. package/src/components/Row.jsx~HEAD +212 -0
  23. package/src/components/Widget.js +24 -5
  24. package/src/components/Widget.jsx +206 -0
  25. package/src/index.html +31 -25
  26. package/src/scss/editor-panel.scss +53 -49
  27. package/src/scss/grid.scss +60 -14
  28. package/src/scss/main.scss +77 -9
  29. package/src/components/Header.js +0 -15
  30. package/src/images/icon-close.svg +0 -1
  31. package/src/images/icon-down.svg +0 -1
  32. package/src/images/icon-edit.svg +0 -1
  33. package/src/images/icon-move.svg +0 -8
  34. package/src/images/icon-up.svg +0 -1
  35. package/src/images/warning.svg +0 -1
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useCallback } from 'react';
1
+ import React, { useState, useEffect, useCallback } from 'react'
2
2
 
3
3
  // IE11
4
4
  import 'core-js/stable'
@@ -8,44 +8,61 @@ import ResizeObserver from 'resize-observer-polyfill'
8
8
  import { DndProvider } from 'react-dnd'
9
9
  import { HTML5Backend } from 'react-dnd-html5-backend'
10
10
 
11
- import parse from 'html-react-parser';
11
+ import parse from 'html-react-parser'
12
12
 
13
- import Loading from '@cdc/core/components/Loading';
14
- import DataTransform from '@cdc/core/components/DataTransform';
15
- import getViewport from '@cdc/core/helpers/getViewport';
13
+ import Loading from '@cdc/core/components/Loading'
14
+ import DataTransform from '@cdc/core/components/DataTransform'
15
+ import getViewport from '@cdc/core/helpers/getViewport'
16
16
 
17
- import CdcMap from '@cdc/map';
18
- import CdcChart from '@cdc/chart';
19
- import CdcDataBite from '@cdc/data-bite';
17
+ import CdcMap from '@cdc/map'
18
+ import CdcChart from '@cdc/chart'
19
+ import CdcDataBite from '@cdc/data-bite'
20
+ import CdcWaffleChart from '@cdc/waffle-chart'
21
+ import CdcMarkupInclude from '@cdc/markup-include'
22
+ import FilteredText from '@cdc/filtered-text';
20
23
 
21
- import EditorPanel from './components/EditorPanel';
22
- import Grid from './components/Grid';
23
- import Header from './components/Header';
24
- import Context from './context';
25
- import defaults from './data/initial-state';
26
- import Widget from './components/Widget';
27
- import DataTable from './components/DataTable';
24
+ import EditorPanel from './components/EditorPanel'
25
+ import Grid from './components/Grid'
26
+ import Header from './components/Header'
27
+ import Context from './context'
28
+ import defaults from './data/initial-state'
29
+ import Widget from './components/Widget'
30
+ import DataTable from './components/DataTable'
28
31
 
29
- import './scss/main.scss';
32
+ import Papa from 'papaparse'
33
+
34
+ import './scss/main.scss'
35
+
36
+ import { publish } from '@cdc/core/helpers/events'
37
+ import cacheBustingString from '@cdc/core/helpers/cacheBustingString'
30
38
 
31
39
  const addVisualization = (type, subType) => {
32
40
  let newVisualizationConfig = {
33
41
  newViz: true,
34
42
  uid: type + Date.now(),
35
43
  type
36
- };
44
+ }
37
45
 
38
- switch(type) {
46
+ switch (type) {
39
47
  case 'chart':
40
- newVisualizationConfig.visualizationType = subType;
41
- break;
48
+ newVisualizationConfig.visualizationType = subType
49
+ break
42
50
  case 'map':
43
- newVisualizationConfig.general = {};
44
- newVisualizationConfig.general.geoType = subType;
45
- break;
51
+ newVisualizationConfig.general = {}
52
+ newVisualizationConfig.general.geoType = subType
53
+ break
46
54
  case 'data-bite':
47
- newVisualizationConfig.visualizationType = type;
48
- break;
55
+ newVisualizationConfig.visualizationType = type
56
+ break
57
+ case 'waffle-chart':
58
+ newVisualizationConfig.visualizationType = type
59
+ break
60
+ case 'markup-include':
61
+ newVisualizationConfig.visualizationType = type
62
+ break
63
+ case 'filtered-text':
64
+ newVisualizationConfig.visualizationType = type
65
+ break
49
66
  }
50
67
 
51
68
  return newVisualizationConfig
@@ -53,176 +70,252 @@ const addVisualization = (type, subType) => {
53
70
 
54
71
  const VisualizationsPanel = () => (
55
72
  <div className="visualizations-panel">
56
- <p style={{fontSize: '14px'}}>Click and drag an item onto the grid to add it to your dashboard.</p>
73
+ <p style={{ fontSize: '14px' }}>Click and drag an item onto the grid to add it to your dashboard.</p>
57
74
  <span className="subheading-3">Chart</span>
58
75
  <div className="drag-grid">
59
- <Widget addVisualization={() => addVisualization('chart', 'Bar')} type="Bar" />
60
- <Widget addVisualization={() => addVisualization('chart', 'Line')} type="Line" />
61
- <Widget addVisualization={() => addVisualization('chart', 'Pie')} type="Pie" />
76
+ <Widget addVisualization={() => addVisualization('chart', 'Bar')} type="Bar"/>
77
+ <Widget addVisualization={() => addVisualization('chart', 'Line')} type="Line"/>
78
+ <Widget addVisualization={() => addVisualization('chart', 'Pie')} type="Pie"/>
62
79
  </div>
63
80
  <span className="subheading-3">Map</span>
64
81
  <div className="drag-grid">
65
- <Widget addVisualization={() => addVisualization('map', 'us')} type="us" />
66
- <Widget addVisualization={() => addVisualization('map', 'world')} type="world" />
82
+ <Widget addVisualization={() => addVisualization('map', 'us')} type="us"/>
83
+ <Widget addVisualization={() => addVisualization('map', 'world')} type="world"/>
84
+ <Widget addVisualization={() => addVisualization('map', 'single-state')} type="single-state"/>
67
85
  </div>
68
86
  <span className="subheading-3">Misc.</span>
69
87
  <div className="drag-grid">
70
- <Widget addVisualization={() => addVisualization('data-bite', '')} type="data-bite" />
88
+ <Widget addVisualization={() => addVisualization('data-bite', '')} type="data-bite"/>
89
+ <Widget addVisualization={() => addVisualization('waffle-chart', '')} type="waffle-chart"/>
90
+ <Widget addVisualization={() => addVisualization('markup-include', '')} type="markup-include"/>
91
+ <Widget addVisualization={() => addVisualization('filtered-text', '')} type="filtered-text"/>
71
92
  </div>
72
93
  </div>
73
94
  )
74
95
 
75
96
  export default function CdcDashboard(
76
- { configUrl = '', config: configObj = undefined, isEditor = false, setConfig: setParentConfig }
97
+ { configUrl = '', config: configObj = undefined, isEditor = false, setConfig: setParentConfig, hostname }
77
98
  ) {
78
99
 
79
- const transform = new DataTransform();
100
+ const transform = new DataTransform()
80
101
 
81
- const [config, setConfig] = useState(configObj ?? {});
102
+ const [ config, setConfig ] = useState(configObj)
82
103
 
83
- const [data, setData] = useState([]);
104
+ const [ data, setData ] = useState([])
84
105
 
85
- const [filteredData, setFilteredData] = useState();
106
+ const [ filteredData, setFilteredData ] = useState()
86
107
 
87
- const [loading, setLoading] = useState(true);
108
+ const [ loading, setLoading ] = useState(true)
88
109
 
89
- const [preview, setPreview] = useState(false);
110
+ const [ preview, setPreview ] = useState(false)
90
111
 
91
- const [currentViewport, setCurrentViewport] = useState('lg')
112
+ const [ currentViewport, setCurrentViewport ] = useState('lg')
92
113
 
93
- const { title, description } = config.dashboard || config;
114
+ const [ coveLoadedHasRan, setCoveLoadedHasRan ] = useState(false)
94
115
 
95
- const loadConfig = async () => {
96
- let response = configObj || await (await fetch(configUrl)).json();
116
+ const [ container, setContainer ] = useState()
97
117
 
98
- // If data is included through a URL, fetch that and store
99
- let data = response.formattedData || response.data || {}
118
+ const { title, description } = config ? (config.dashboard || config) : {}
119
+
120
+ // Supports JSON or CSV
121
+ const fetchRemoteData = async (url) => {
122
+ try {
123
+ const urlObj = new URL(url)
124
+ const regex = /(?:\.([^.]+))?$/
100
125
 
101
- if(response.dataUrl) {
102
- const dataString = await fetch(response.dataUrl);
126
+ let data = []
103
127
 
104
- data = await dataString.json();
128
+ const ext = (regex.exec(urlObj.pathname)[1])
129
+ if ('csv' === ext) {
130
+ data = await fetch(url)
131
+ .then(response => response.text())
132
+ .then(responseText => {
133
+ const parsedCsv = Papa.parse(responseText, {
134
+ header: true,
135
+ dynamicTyping: true,
136
+ skipEmptyLines: true
137
+ })
138
+ return parsedCsv.data
139
+ })
140
+ }
141
+
142
+ if ('json' === ext) {
143
+ data = await fetch(url)
144
+ .then(response => response.json())
145
+ }
105
146
 
147
+ return data
148
+ } catch {
149
+ // If we can't parse it, still attempt to fetch it
106
150
  try {
107
- data = transform.autoStandardize(data);
108
- data = transform.developerStandardize(data, response.dataDescription);
109
- } catch(e) {
110
- //Data not able to be standardized, leave as is
151
+ let response = await (await fetch(configUrl)).json()
152
+ return response
153
+ } catch {
154
+ console.error(`Cannot parse URL: ${url}`)
155
+ }
156
+ }
157
+ }
158
+
159
+
160
+
161
+ const loadConfig = async (configObj) => {
162
+ // Set loading flag
163
+ if (!loading) setLoading(true)
164
+
165
+ let newState = configObj || await (await fetch(configUrl)).json()
166
+
167
+ // If a dataUrl property exists, always pull from that.
168
+ if (newState.dataUrl) {
169
+
170
+ if (newState.dataUrl[0] === '/') {
171
+ newState.dataUrl = 'https://' + hostname + newState.dataUrl
172
+ }
173
+
174
+ let newData = await fetchRemoteData(newState.dataUrl + `?v=${cacheBustingString()}`)
175
+
176
+ if (newData && newState.dataDescription) {
177
+ newData = transform.autoStandardize(newData)
178
+ newData = transform.developerStandardize(newData, newState.dataDescription)
179
+ }
180
+
181
+ if (newData) {
182
+ newState.data = newData
111
183
  }
112
184
  }
113
185
 
114
- setData(data);
186
+ // If data is included through a URL, fetch that and store
187
+ let data = newState.formattedData || newState.data || {}
188
+
189
+ setData(data)
115
190
 
116
- let newConfig = {...defaults, ...response}
191
+ let newConfig = { ...defaults, ...newState }
117
192
 
118
- updateConfig(newConfig, data);
193
+ updateConfig(newConfig, data)
119
194
 
120
- setLoading(false);
195
+ setLoading(false)
121
196
  }
122
197
 
123
198
  const filterData = (filters, data) => {
124
- let filteredData = [];
199
+ let filteredData = []
125
200
 
126
201
  data.forEach((row) => {
127
- let add = true;
202
+ let add = true
128
203
 
129
204
  filters.forEach((filter) => {
130
- if(row[filter.columnName] !== filter.active) {
131
- add = false;
205
+ if (row[filter.columnName] !== filter.active) {
206
+ add = false
132
207
  }
133
- });
208
+ })
134
209
 
135
- if(add) filteredData.push(row);
136
- });
210
+ if (add) filteredData.push(row)
211
+ })
137
212
 
138
- return filteredData;
213
+ return filteredData
139
214
  }
140
215
 
141
216
  // Gets filer values from dataset
142
217
  const generateValuesForFilter = (columnName, data = this.state.data) => {
143
- const values = [];
218
+ const values = []
144
219
 
145
- data.forEach( (row) => {
146
- const value = row[columnName]
147
- if(value && false === values.includes(value)) {
148
- values.push(value)
149
- }
150
- });
220
+ data.forEach((row) => {
221
+ const value = row[columnName]
222
+ if (value && false === values.includes(value)) {
223
+ values.push(value)
224
+ }
225
+ })
151
226
 
152
- return values;
227
+ return values
153
228
  }
154
229
 
155
- const updateConfig = (newConfig, dataOverride = null) => {
230
+ function isEmpty(obj) {
231
+ return Object.keys(obj).length === 0;
232
+ }
233
+
234
+ const updateConfig = (newConfig, dataOverride = null) => {
156
235
  // After data is grabbed, loop through and generate filter column values if there are any
157
236
  if (newConfig.dashboard.filters) {
158
- const filterList = [];
237
+ const filterList = []
159
238
 
160
239
  newConfig.dashboard.filters.forEach((filter) => {
161
- filterList.push(filter.columnName);
162
- });
240
+ filterList.push(filter.columnName)
241
+ })
163
242
 
164
243
  filterList.forEach((filter, index) => {
165
- const filterValues = generateValuesForFilter(filter, (dataOverride || data));
244
+ const filterValues = generateValuesForFilter(filter, (dataOverride || data))
245
+
246
+ if (newConfig.dashboard.filters[index].order === 'asc') {
247
+ filterValues.sort()
248
+ }
249
+ if (newConfig.dashboard.filters[index].order === 'desc') {
250
+ filterValues.sort().reverse()
251
+ }
166
252
 
167
- newConfig.dashboard.filters[index].values = filterValues;
253
+ newConfig.dashboard.filters[index].values = filterValues
168
254
 
169
- // Initial filter should be active
170
- newConfig.dashboard.filters[index].active = filterValues[0];
171
- });
255
+ // Initial filter should be active
256
+ newConfig.dashboard.filters[index].active = filterValues[0]
257
+ })
172
258
 
173
- setFilteredData(filterData(newConfig.dashboard.filters, (dataOverride || data)));
259
+ setFilteredData(filterData(newConfig.dashboard.filters, (dataOverride || data)))
174
260
  }
175
261
 
176
262
  //Enforce default values that need to be calculated at runtime
177
- newConfig.runtime = {};
263
+ newConfig.runtime = {}
178
264
 
179
- setConfig(newConfig);
180
- };
265
+ setConfig(newConfig)
266
+ }
181
267
 
182
268
  // Load data when component first mounts
183
269
  useEffect(() => {
184
- loadConfig();
185
- }, []);
270
+ loadConfig(config)
271
+ }, [])
186
272
 
187
273
  // Pass up to <CdcEditor /> if it exists when config state changes
188
274
  useEffect(() => {
189
- if(setParentConfig && isEditor) {
190
- setParentConfig(config);
275
+ if (setParentConfig && isEditor) {
276
+ setParentConfig(config)
191
277
  }
192
- }, [config])
278
+ }, [ config ])
279
+
280
+ useEffect(() => {
281
+ if (config && !coveLoadedHasRan && container) {
282
+ publish('cove_loaded', { config: config })
283
+ setCoveLoadedHasRan(true)
284
+ }
285
+ }, [config, container]);
193
286
 
194
287
  const updateChildConfig = (visualizationKey, newConfig) => {
195
- let updatedConfig = {...config}
288
+ let updatedConfig = { ...config }
196
289
 
197
- updatedConfig.visualizations[visualizationKey] = newConfig;
198
- setConfig(updatedConfig);
199
- };
290
+ updatedConfig.visualizations[visualizationKey] = newConfig
291
+ setConfig(updatedConfig)
292
+ }
200
293
 
201
294
  const Filters = () => {
202
295
  const changeFilterActive = (index, value) => {
203
- let dashboardConfig = {...config.dashboard};
296
+ let dashboardConfig = { ...config.dashboard }
204
297
 
205
- dashboardConfig.filters[index].active = value;
298
+ dashboardConfig.filters[index].active = value
206
299
 
207
- setConfig({...config, dashboard: dashboardConfig});
300
+ setConfig({ ...config, dashboard: dashboardConfig })
208
301
 
209
- setFilteredData(filterData(dashboardConfig.filters, data));
210
- };
302
+ setFilteredData(filterData(dashboardConfig.filters, data))
303
+ }
211
304
 
212
305
  const announceChange = (text) => {
213
306
 
214
- };
307
+ }
215
308
 
216
309
  return config.dashboard.filters.map((singleFilter, index) => {
217
- const values = [];
310
+ const values = []
218
311
 
219
312
  singleFilter.values.forEach((filterOption, index) => {
220
313
  values.push(<option
221
314
  key={index}
222
315
  value={filterOption}
223
316
  >{filterOption}
224
- </option>);
225
- });
317
+ </option>)
318
+ })
226
319
 
227
320
  return (
228
321
  <section className="dashboard-filters-section" key={index}>
@@ -233,50 +326,52 @@ export default function CdcDashboard(
233
326
  data-index="0"
234
327
  value={singleFilter.active}
235
328
  onChange={(val) => {
236
- changeFilterActive(index, val.target.value);
237
- announceChange(`Filter ${singleFilter.label} value has been changed to ${val.target.value}, please reference the data table to see updated values.`);
329
+ changeFilterActive(index, val.target.value)
330
+ announceChange(`Filter ${singleFilter.label} value has been changed to ${val.target.value}, please reference the data table to see updated values.`)
238
331
  }}
239
332
  >
240
333
  {values}
241
334
  </select>
242
335
  </section>
243
- );
244
- });;
336
+ )
337
+ })
338
+
245
339
  }
246
340
 
247
341
  const resizeObserver = new ResizeObserver(entries => {
248
342
  for (let entry of entries) {
249
- let newViewport = getViewport(entry.contentRect.width)
343
+ let newViewport = getViewport(entry.contentRect.width)
250
344
 
251
- setCurrentViewport(newViewport)
345
+ setCurrentViewport(newViewport)
252
346
  }
253
- });
347
+ })
254
348
 
255
349
  const outerContainerRef = useCallback(node => {
256
350
  if (node !== null) {
257
- resizeObserver.observe(node);
351
+ resizeObserver.observe(node)
258
352
  }
259
- },[]);
353
+ setContainer(node)
354
+ }, [])
260
355
 
261
356
  // Prevent render if loading
262
- if(loading) return <Loading />
357
+ if (loading) return <Loading/>
263
358
 
264
359
  let body = null
265
360
 
266
361
  // Editor mode
267
- if(isEditor && !preview) {
268
- let subVisualizationEditing = false;
362
+ if (isEditor && !preview) {
363
+ let subVisualizationEditing = false
269
364
 
270
365
  Object.keys(config.visualizations).forEach(visualizationKey => {
271
- let visualizationConfig = config.visualizations[visualizationKey];
366
+ let visualizationConfig = config.visualizations[visualizationKey]
272
367
 
273
- visualizationConfig.data = filteredData || data;
368
+ visualizationConfig.data = filteredData || data
274
369
 
275
- if(visualizationConfig.editing) {
276
- subVisualizationEditing = true;
370
+ if (visualizationConfig.editing) {
371
+ subVisualizationEditing = true
277
372
 
278
373
  const back = () => {
279
- const newConfig = {...config}
374
+ const newConfig = { ...config }
280
375
 
281
376
  delete newConfig.visualizations[visualizationKey].editing
282
377
 
@@ -285,28 +380,37 @@ export default function CdcDashboard(
285
380
 
286
381
  const updateConfig = (newConfig) => updateChildConfig(visualizationKey, newConfig)
287
382
 
288
- switch(visualizationConfig.type){
383
+ switch (visualizationConfig.type) {
289
384
  case 'chart':
290
- body = <><Header back={back} subEditor="Chart" /><CdcChart key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={updateConfig} isDashboard={true} /></>;
291
- break;
292
- case 'map':
293
- body = <><Header back={back} subEditor="Map" /><CdcMap key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={updateConfig} isDashboard={true} /></>;
294
- break;
385
+ body = <><Header back={back} subEditor="Chart"/><CdcChart key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={updateConfig} isDashboard={true}/></>
386
+ break
387
+ case 'map':
388
+ body = <><Header back={back} subEditor="Map"/><CdcMap key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={updateConfig} isDashboard={true}/></>
389
+ break
295
390
  case 'data-bite':
296
- visualizationConfig = {...visualizationConfig, newViz: true}
297
- body = <><Header back={back} subEditor="Data Bite" /><CdcDataBite key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={updateConfig} isDashboard={true} /></>
298
- break;
391
+ visualizationConfig = { ...visualizationConfig, newViz: true }
392
+ body = <><Header back={back} subEditor="Data Bite"/><CdcDataBite key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={updateConfig} isDashboard={true}/></>
393
+ break
394
+ case 'waffle-chart':
395
+ body = <><Header back={back} subEditor="Waffle Chart"/><CdcWaffleChart key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={updateConfig} isDashboard={true}/></>
396
+ break
397
+ case 'markup-include':
398
+ body = <><Header back={back} subEditor="Markup Include"/><CdcMarkupInclude key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={updateConfig} isDashboard={true}/></>
399
+ break
400
+ case 'filtered-text':
401
+ body = <><Header back={back} subEditor="Filtered Text"/><FilteredText key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={updateConfig} isDashboard={true}/></>
402
+ break
299
403
  }
300
404
  }
301
- });
405
+ })
302
406
 
303
- if(!subVisualizationEditing){
407
+ if (!subVisualizationEditing) {
304
408
  body = (
305
409
  <DndProvider backend={HTML5Backend}>
306
- <Header preview={preview} setPreview={setPreview} />
410
+ <Header preview={preview} setPreview={setPreview}/>
307
411
  <div className="layout-container">
308
- <VisualizationsPanel />
309
- <Grid />
412
+ <VisualizationsPanel/>
413
+ <Grid/>
310
414
  </div>
311
415
  </DndProvider>
312
416
  )
@@ -314,42 +418,61 @@ export default function CdcDashboard(
314
418
  } else {
315
419
  body = (
316
420
  <>
317
- {isEditor && <Header preview={preview} setPreview={setPreview} />}
318
- {isEditor && <EditorPanel />}
421
+ {isEditor && <Header preview={preview} setPreview={setPreview}/>}
422
+ {isEditor && <EditorPanel/>}
319
423
  <div className="cdc-dashboard-inner-container">
320
424
  {/* Title */}
321
425
  {title && <div role="heading" className={`dashboard-title ${config.dashboard.theme ?? 'theme-blue'}`}>{title}</div>}
322
426
 
323
427
  {/* Filters */}
324
- {config.dashboard.filters && <Filters />}
428
+ {config.dashboard.filters &&
429
+ <div className="cove-dashboard-filters">
430
+ <Filters />
431
+ </div>
432
+ }
325
433
 
326
434
  {/* Visualizations */}
327
- {config.rows && config.rows.map(row => {
435
+ {config.rows && config.rows.map((row, index) => {
328
436
  // Empty check
329
- if(row.filter(col => col.widget).length === 0) return null
437
+ if (row.filter(col => col.widget).length === 0) return null
330
438
 
331
439
  return (
332
- <div className="dashboard-row">
333
- {row.map(col => {
334
- if(col.width) {
335
- if(!col.widget) return <div className={`dashboard-col dashboard-col-${col.width}`}></div>
336
-
337
- let visualizationConfig = config.visualizations[col.widget];
338
-
339
- visualizationConfig.data = filteredData || data;
340
-
341
- return <div className={`dashboard-col dashboard-col-${col.width}`}>
342
- {visualizationConfig.type === 'chart' && <CdcChart key={col.widget} config={visualizationConfig} isEditor={false} setConfig={(newConfig) => {updateChildConfig(col.widget, newConfig)}} isDashboard={true} />}
343
- {visualizationConfig.type === 'map' && <CdcMap key={col.widget} config={visualizationConfig} isEditor={false} setConfig={(newConfig) => {updateChildConfig(col.widget, newConfig)}} isDashboard={true} />}
344
- {visualizationConfig.type === 'data-bite' && <CdcDataBite key={col.widget} config={visualizationConfig} isEditor={false} setConfig={(newConfig) => {updateChildConfig(col.widget, newConfig)}} isDashboard={true} />}
440
+ <div className="dashboard-row" key={`row__${index}`}>
441
+ {row.map((col, index) => {
442
+ if (col.width) {
443
+ if (!col.widget) return <div className={`dashboard-col dashboard-col-${col.width}`}></div>
444
+
445
+ let visualizationConfig = config.visualizations[col.widget]
446
+
447
+ visualizationConfig.data = filteredData || data
448
+
449
+ return <div className={`dashboard-col dashboard-col-${col.width}`} key={`vis__${index}`}>
450
+ {visualizationConfig.type === 'chart' && <CdcChart key={col.widget} config={visualizationConfig} isEditor={false} setConfig={(newConfig) => {
451
+ updateChildConfig(col.widget, newConfig)
452
+ }} isDashboard={true}/>}
453
+ {visualizationConfig.type === 'map' && <CdcMap key={col.widget} config={visualizationConfig} isEditor={false} setConfig={(newConfig) => {
454
+ updateChildConfig(col.widget, newConfig)
455
+ }} isDashboard={true}/>}
456
+ {visualizationConfig.type === 'data-bite' && <CdcDataBite key={col.widget} config={visualizationConfig} isEditor={false} setConfig={(newConfig) => {
457
+ updateChildConfig(col.widget, newConfig)
458
+ }} isDashboard={true}/>}
459
+ {visualizationConfig.type === 'waffle-chart' && <CdcWaffleChart key={col.widget} config={visualizationConfig} isEditor={false} setConfig={(newConfig) => {
460
+ updateChildConfig(col.widget, newConfig)
461
+ }} isDashboard={true}/>}
462
+ {visualizationConfig.type === 'markup-include' && <CdcMarkupInclude key={col.widget} config={visualizationConfig} isEditor={false} setConfig={(newConfig) => {
463
+ updateChildConfig(col.widget, newConfig)
464
+ }} isDashboard={true}/>}
465
+ {visualizationConfig.type === 'filtered-text' && <FilteredText key={col.widget} config={visualizationConfig} isEditor={false} setConfig={(newConfig) => {
466
+ updateChildConfig(col.widget, newConfig)
467
+ }} isDashboard={true}/>}
345
468
  </div>
346
469
  }
347
470
  })}
348
- </div>);
471
+ </div>)
349
472
  })}
350
473
 
351
474
  {/* Data Table */}
352
- {config.table.show && <DataTable />}
475
+ {config.table.show && <DataTable/>}
353
476
 
354
477
  {/* Description */}
355
478
  {description && <div className="subtext">{parse(description)}</div>}
@@ -369,12 +492,12 @@ export default function CdcDashboard(
369
492
  setParentConfig,
370
493
  setPreview
371
494
  }
372
-
495
+
373
496
  return (
374
497
  <Context.Provider value={contextValues}>
375
498
  <div className={`cdc-open-viz-module type-dashboard ${currentViewport}`} ref={outerContainerRef}>
376
499
  {body}
377
500
  </div>
378
501
  </Context.Provider>
379
- );
502
+ )
380
503
  }