@cdc/dashboard 1.1.2 → 9.22.9

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