@cdc/core 4.24.5 → 4.24.9-1
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.
- package/assets/icon-gear-multi.svg +23 -0
- package/components/AdvancedEditor/AdvancedEditor.tsx +93 -0
- package/components/AdvancedEditor/advanced-editor-styles.css +3 -0
- package/components/AdvancedEditor/index.ts +1 -0
- package/components/Alert/components/Alert.styles.css +15 -0
- package/components/Alert/components/Alert.tsx +39 -0
- package/components/Alert/index.tsx +3 -0
- package/components/DataTable/DataTable.tsx +127 -32
- package/components/DataTable/DataTableStandAlone.tsx +4 -25
- package/components/DataTable/components/DataTableEditorPanel.tsx +4 -4
- package/components/DataTable/components/ExpandCollapse.tsx +1 -1
- package/components/DataTable/helpers/chartCellMatrix.tsx +6 -12
- package/components/DataTable/helpers/getChartCellValue.ts +9 -5
- package/components/DataTable/helpers/getDataSeriesColumns.ts +10 -7
- package/components/DataTable/helpers/getRowType.ts +6 -0
- package/components/DataTable/helpers/mapCellMatrix.tsx +3 -3
- package/components/DataTable/types/TableConfig.ts +2 -1
- package/components/EditorPanel/ColumnsEditor.tsx +3 -30
- package/components/EditorPanel/DataTableEditor.tsx +66 -22
- package/components/EditorPanel/FieldSetWrapper.tsx +51 -0
- package/components/EditorPanel/FootnotesEditor.tsx +77 -0
- package/components/EditorPanel/Inputs.tsx +13 -4
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +268 -0
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +306 -0
- package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +40 -0
- package/components/EditorPanel/VizFilterEditor/index.ts +1 -0
- package/components/EditorWrapper/EditorWrapper.tsx +3 -4
- package/components/EditorWrapper/index.ts +1 -0
- package/components/Filters.tsx +520 -0
- package/components/Footnotes/Footnotes.tsx +25 -0
- package/components/Footnotes/FootnotesStandAlone.tsx +45 -0
- package/components/Footnotes/footnotes.css +5 -0
- package/components/Footnotes/index.ts +1 -0
- package/components/Layout/components/Responsive.tsx +14 -4
- package/components/Layout/components/Sidebar/components/Sidebar.tsx +14 -5
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +23 -20
- package/components/Layout/components/Visualization/index.tsx +19 -6
- package/components/Layout/components/Visualization/visualizations.scss +32 -26
- package/components/Layout/styles/editor.scss +0 -8
- package/components/Legend/Legend.Gradient.tsx +133 -0
- package/components/LegendShape.tsx +28 -0
- package/components/MultiSelect/MultiSelect.tsx +41 -11
- package/components/MultiSelect/multiselect.styles.css +0 -3
- package/components/NestedDropdown/NestedDropdown.tsx +47 -52
- package/components/NestedDropdown/nesteddropdown.styles.css +19 -25
- package/components/Table/Table.tsx +8 -5
- package/components/Table/components/Cell.tsx +2 -2
- package/components/Table/components/Row.tsx +25 -7
- package/components/_stories/Footnotes.stories.tsx +17 -0
- package/components/_stories/Layout.Debug.stories.tsx +91 -0
- package/components/_stories/_mocks/bar-chart-suppressed.json +474 -0
- package/components/_stories/styles.scss +14 -1
- package/components/createBarElement.jsx +4 -4
- package/components/inputs/InputSelect.tsx +17 -6
- package/components/ui/Icon.tsx +22 -16
- package/components/ui/Title/Title.scss +0 -8
- package/helpers/DataTransform.ts +2 -2
- package/helpers/addValuesToFilters.ts +135 -0
- package/helpers/cove/accessibility.ts +17 -4
- package/helpers/cove/fontSettings.ts +2 -0
- package/helpers/coveUpdateWorker.ts +30 -9
- package/helpers/filterVizData.ts +49 -0
- package/helpers/formatConfigBeforeSave.ts +110 -0
- package/helpers/gatherQueryParams.ts +24 -7
- package/helpers/getGradientLegendWidth.ts +15 -0
- package/helpers/getTextWidth.ts +18 -0
- package/helpers/lineChartHelpers.js +2 -1
- package/helpers/pivotData.ts +18 -0
- package/helpers/queryStringUtils.ts +29 -0
- package/helpers/scaling.ts +7 -0
- package/helpers/tests/addValuesToFilters.test.ts +55 -0
- package/helpers/tests/filterVizData.test.ts +31 -0
- package/helpers/tests/gatherQueryParams.test.ts +22 -0
- package/helpers/tests/invertValue.test.ts +35 -0
- package/helpers/tests/updateFieldFactory.test.ts +1 -0
- package/helpers/updateFieldFactory.ts +1 -1
- package/helpers/updatePaletteNames.ts +19 -0
- package/helpers/{useDataVizClasses.js → useDataVizClasses.ts} +3 -2
- package/helpers/ver/4.24.5.ts +3 -3
- package/helpers/ver/4.24.7.ts +123 -0
- package/helpers/ver/4.24.9.ts +63 -0
- package/helpers/ver/tests/4.24.9.test.ts +22 -0
- package/helpers/ver/versionNeedsUpdate.ts +9 -0
- package/package.json +6 -4
- package/styles/_button-section.scss +7 -2
- package/styles/_data-table.scss +0 -1
- package/styles/_global.scss +6 -2
- package/styles/base.scss +4 -0
- package/styles/filters.scss +4 -0
- package/styles/v2/themes/_color-definitions.scss +1 -0
- package/types/Annotation.ts +46 -0
- package/types/Axis.ts +3 -2
- package/types/ConfigureData.ts +1 -1
- package/types/Dimensions.ts +1 -0
- package/types/Footnotes.ts +17 -0
- package/types/General.ts +5 -0
- package/types/Runtime.ts +2 -7
- package/types/Table.ts +6 -0
- package/types/Visualization.ts +31 -9
- package/types/VizFilter.ts +39 -7
- package/components/AdvancedEditor.jsx +0 -74
- package/components/EditorPanel/VizFilterEditor.tsx +0 -234
- package/components/Filters.jsx +0 -461
- package/components/LegendCircle.jsx +0 -17
- package/helpers/queryStringUtils.js +0 -26
- package/helpers/updatePaletteNames.js +0 -16
- package/types/BaseVisualizationType.ts +0 -1
- /package/components/{Waiting.jsx → Waiting.tsx} +0 -0
- /package/helpers/ver/{4.23.4.ts → 4.24.4.ts} +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400">
|
|
3
|
+
<defs>
|
|
4
|
+
<style>
|
|
5
|
+
.cls-1 {
|
|
6
|
+
fill: #231f20;
|
|
7
|
+
stroke-width: 0px;
|
|
8
|
+
}
|
|
9
|
+
</style>
|
|
10
|
+
</defs>
|
|
11
|
+
<g>
|
|
12
|
+
<path class="cls-1" d="M218.85,262.45c-.39-37.16-30.83-66.97-68.01-66.58-37.16.41-66.97,30.86-66.58,68.03.4,37.15,30.86,66.94,68.02,66.57,37.18-.41,66.98-30.86,66.57-68.02ZM151.88,292.62c-4.07.04-7.95-.74-11.49-2.19-10.62-4.36-18.15-14.75-18.28-26.94-.17-16.27,12.86-29.59,29.13-29.76,4.06-.04,7.95.74,11.48,2.19,10.62,4.35,18.15,14.74,18.28,26.94.18,16.25-12.87,29.59-29.12,29.76Z"/>
|
|
13
|
+
<path class="cls-1" d="M250.22,280.09c.99-5.85,1.5-11.87,1.44-18.01-.05-3.92-.34-7.82-.82-11.64l23.73-17.16c-1.72-7.65-4.12-15.04-7.14-22.1l-30.29.03c-4.82-7.93-10.7-15.12-17.46-21.39l7.59-30.08c-6.16-4.7-12.74-8.82-19.71-12.35l-24.62,20.71c-7.91-2.64-16.26-4.28-24.91-4.81l-4.17-8.61-10.1-20.88c-7.89.37-15.6,1.46-23.03,3.17l-5.77,33c-7.53,2.96-14.62,6.81-21.11,11.39l-30.76-13.8c-5.69,5.27-10.89,11.08-15.55,17.29l16.85,29.12c-3.92,6.93-7.04,14.4-9.23,22.24l-32.12,9.11c-.72,5.75-1.1,11.59-1.03,17.54.02,1.92.1,3.82.2,5.73l30.61,11.08c1.43,8.61,3.98,16.85,7.44,24.55l-17.93,26.42c4.21,6.54,9,12.7,14.3,18.34l28.94-10.58c6.98,6.15,14.83,11.31,23.32,15.33l3.06,29.9c7.33,2.28,14.93,3.88,22.74,4.81l14.52-25.22c1.14.03,2.29.07,3.45.04,9.06-.08,17.81-1.4,26.14-3.74l20.52,19.93c7.2-3.04,14.06-6.7,20.53-10.93l-4.95-27.85c7.91-6.47,14.82-14.1,20.44-22.65l28.22,2.05c3.53-6.85,6.48-14.04,8.74-21.52l-22.06-18.46ZM152.4,341.41c-43.21.47-78.62-34.19-79.08-77.4-.46-43.2,34.19-78.61,77.4-79.08,43.21-.46,78.6,34.21,79.06,77.41.47,43.2-34.18,78.6-77.39,79.07Z"/>
|
|
14
|
+
</g>
|
|
15
|
+
<g>
|
|
16
|
+
<path class="cls-1" d="M345.31,139.47c-7.44-19.18-29.04-28.71-48.23-21.26-19.18,7.45-28.7,29.05-21.25,48.24,7.45,19.18,29.04,28.69,48.24,21.25,19.19-7.46,28.71-29.05,21.25-48.24ZM316.47,168.16c-2.1.82-4.26,1.17-6.38,1.1-6.35-.19-12.28-4.11-14.73-10.4-3.26-8.4.9-17.85,9.3-21.11,2.1-.82,4.26-1.17,6.38-1.1,6.35.19,12.28,4.1,14.73,10.4,3.26,8.39-.9,17.85-9.29,21.1Z"/>
|
|
17
|
+
<path class="cls-1" d="M365.01,142.5c-.63-3.23-1.54-6.45-2.76-9.62-.79-2.02-1.7-3.99-2.69-5.87l8.96-13.52c-2.38-3.63-5.07-7-8.01-10.06l-15.7,5.92c-4.05-3.17-8.5-5.75-13.22-7.69l-1.93-17.07c-4.11-1.24-8.33-2.09-12.62-2.56l-8.73,15.54c-4.61.17-9.26.95-13.85,2.36l-3.84-3.65-9.31-8.86c-4.02,1.73-7.8,3.79-11.32,6.13l3.44,18.23c-3.33,3-6.25,6.38-8.72,10.02l-18.64-1.16c-1.93,3.84-3.49,7.87-4.69,11.99l14.41,11.81c-.68,4.36-.85,8.84-.45,13.33l-14.87,10.98c.75,3.12,1.69,6.22,2.88,9.29.38.99.8,1.96,1.22,2.93l18.03-.22c2.42,4.19,5.35,7.96,8.64,11.28l-4.15,17.19c3.46,2.57,7.14,4.83,10.99,6.72l12.94-11.13c4.82,1.83,9.89,2.98,15.07,3.4l7.42,14.9c4.24-.25,8.5-.9,12.73-1.94l2.61-15.9c.6-.21,1.2-.41,1.8-.65,4.68-1.81,8.96-4.2,12.82-7.04l14.52,6.33c3.14-2.98,5.99-6.21,8.51-9.67l-7.99-13.47c2.84-4.9,4.94-10.2,6.18-15.73l15.03-4.44c.49-4.24.62-8.54.34-12.86l-15.03-5.27ZM326.25,193.35c-22.31,8.66-47.42-2.4-56.08-24.71-8.66-22.3,2.4-47.42,24.71-56.08,22.31-8.66,47.41,2.41,56.07,24.72,8.66,22.3-2.4,47.41-24.7,56.07Z"/>
|
|
18
|
+
</g>
|
|
19
|
+
<g>
|
|
20
|
+
<path class="cls-1" d="M219.71,100.11c13.58-15.46,12.07-39.01-3.4-52.6-15.47-13.58-39.02-12.06-52.61,3.41-13.58,15.46-12.05,39.01,3.41,52.6,15.47,13.58,39.02,12.06,52.6-3.41ZM180.95,87.77c-1.69-1.49-3-3.24-3.92-5.15-2.76-5.72-2.02-12.79,2.43-17.87,5.95-6.77,16.25-7.44,23.02-1.49,1.69,1.49,3,3.24,3.92,5.15,2.77,5.72,2.03,12.79-2.43,17.87-5.94,6.76-16.25,7.44-23.01,1.49Z"/>
|
|
21
|
+
<path class="cls-1" d="M226.11,118.99c2.57-2.05,5.01-4.34,7.26-6.89,1.43-1.64,2.75-3.35,3.97-5.1l16.13,1.71c2.12-3.79,3.87-7.72,5.23-11.75l-12.5-11.19c.94-5.05,1.18-10.19.72-15.28l14.26-9.59c-.8-4.22-1.99-8.35-3.55-12.38l-17.81-.57c-2.28-4.01-5.12-7.78-8.49-11.2l1.47-5.09,3.56-12.34c-3.39-2.77-6.97-5.17-10.67-7.21l-14.59,11.47c-4.2-1.57-8.54-2.6-12.92-3.11l-7.57-17.07c-4.3.06-8.59.54-12.81,1.37l-3.83,18.24c-4.18,1.41-8.23,3.33-12.03,5.75l-16.61-8.13c-2.42,2.1-4.74,4.37-6.91,6.85-.7.8-1.37,1.61-2.04,2.44l8.52,15.89c-2.6,4.08-4.59,8.42-6.02,12.87l-17.16,4.25c-.68,4.26-.99,8.56-.89,12.85l15.84,6.34c.6,5.12,1.92,10.15,3.94,14.94l-9.8,13.46c2.18,3.65,4.72,7.12,7.59,10.4l15.31-5.02c.46.44.92.87,1.41,1.29,3.76,3.32,7.86,6.01,12.16,8.13l1.09,15.81c4.09,1.41,8.27,2.44,12.5,3.09l8.26-13.31c5.65.26,11.32-.33,16.8-1.78l10.87,11.29c3.99-1.52,7.86-3.39,11.57-5.64l-2.26-15.77ZM163.11,108.07c-17.98-15.79-19.75-43.18-3.96-61.15,15.79-17.98,43.17-19.75,61.16-3.96,17.98,15.8,19.74,43.18,3.95,61.15-15.79,17.98-43.16,19.75-61.15,3.96Z"/>
|
|
22
|
+
</g>
|
|
23
|
+
</svg>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import MapIcon from '../../assets/map-folded.svg'
|
|
3
|
+
import ChartIcon from '../../assets/icon-chart-bar.svg'
|
|
4
|
+
import MarkupIncludeIcon from '../../assets/icon-code.svg'
|
|
5
|
+
import { FilterFunction, JsonEditor, UpdateFunction } from 'json-edit-react'
|
|
6
|
+
import { formatConfigBeforeSave as stripConfig } from '../../helpers/formatConfigBeforeSave'
|
|
7
|
+
import './advanced-editor-styles.css'
|
|
8
|
+
import _ from 'lodash'
|
|
9
|
+
|
|
10
|
+
export const AdvancedEditor = ({ loadConfig, config, convertStateToConfig, onExpandCollapse = () => {} }) => {
|
|
11
|
+
const [advancedToggle, _setAdvancedToggle] = useState(false)
|
|
12
|
+
const [configTextboxValue, setConfigTextbox] = useState<Record<string, any>>({})
|
|
13
|
+
const setAdvancedToggle = val => {
|
|
14
|
+
_setAdvancedToggle(val)
|
|
15
|
+
onExpandCollapse()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const collapseFields: FilterFunction = input => {
|
|
19
|
+
if (['datasets', 'data', 'originalFormattedData', 'formattedData'].includes(String(input.key))) return true
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const onUpdate: UpdateFunction = val => {
|
|
24
|
+
setConfigTextbox(val.newData)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
let parsedConfig = stripConfig(config)
|
|
29
|
+
if (config.type !== 'dashboard') {
|
|
30
|
+
parsedConfig = convertStateToConfig()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setConfigTextbox(parsedConfig)
|
|
34
|
+
}, [config])
|
|
35
|
+
|
|
36
|
+
const typeLookup = {
|
|
37
|
+
chart: ['Charts', 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/bar-chart.html', <ChartIcon />],
|
|
38
|
+
dashboard: ['Dashboard', 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/bar-chart.html', <ChartIcon />],
|
|
39
|
+
map: ['Maps', 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/data-map.html', <MapIcon />],
|
|
40
|
+
'markup-include': ['Markup Include', 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/Markup-Include.html', <MarkupIncludeIcon />]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!config.type) return <></>
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
<a href={typeLookup[config.type][1]} target='_blank' rel='noopener noreferrer' className='guidance-link'>
|
|
47
|
+
{typeLookup[config.type][2]}
|
|
48
|
+
<div>
|
|
49
|
+
<span className='heading-3'>Get Help with {typeLookup[config.type][0]}</span>
|
|
50
|
+
<p>Examples and documentation</p>
|
|
51
|
+
</div>
|
|
52
|
+
</a>
|
|
53
|
+
<div className='advanced'>
|
|
54
|
+
<span className='advanced-toggle-link' onClick={() => setAdvancedToggle(!advancedToggle)}>
|
|
55
|
+
<span>{advancedToggle ? `— ` : `+ `}</span>Advanced Options
|
|
56
|
+
</span>
|
|
57
|
+
{advancedToggle && (
|
|
58
|
+
<React.Fragment>
|
|
59
|
+
<section className='error-box py-2 px-3 my-2'>
|
|
60
|
+
<div>
|
|
61
|
+
<strong className='pt-1'>Warning</strong>
|
|
62
|
+
<p>This can cause serious errors in your visualization.</p>
|
|
63
|
+
</div>
|
|
64
|
+
</section>
|
|
65
|
+
<p className='pb-2'>
|
|
66
|
+
This tool displays the actual <acronym title='JavaScript Object Notation'>JSON</acronym> configuration that is generated by this editor and allows you to edit properties directly and apply them.
|
|
67
|
+
</p>
|
|
68
|
+
<button
|
|
69
|
+
className='btn '
|
|
70
|
+
onClick={() => {
|
|
71
|
+
navigator.clipboard.writeText(JSON.stringify(configTextboxValue))
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
Copy to Clipboard
|
|
75
|
+
</button>
|
|
76
|
+
<JsonEditor className='advanced-json-editor' data={configTextboxValue} onUpdate={onUpdate} rootName='' collapse={collapseFields} />
|
|
77
|
+
<button
|
|
78
|
+
className='btn full-width'
|
|
79
|
+
onClick={() => {
|
|
80
|
+
loadConfig(configTextboxValue)
|
|
81
|
+
setAdvancedToggle(!advancedToggle)
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
Apply
|
|
85
|
+
</button>
|
|
86
|
+
</React.Fragment>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
</>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default AdvancedEditor
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './AdvancedEditor'
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Icon from '../../ui/Icon'
|
|
2
|
+
|
|
3
|
+
import DOMPurify from 'dompurify'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
|
|
6
|
+
import './Alert.styles.css'
|
|
7
|
+
|
|
8
|
+
type AlertProps = {
|
|
9
|
+
// type of alert for styling the alert box
|
|
10
|
+
type?: 'success' | 'danger' | 'info'
|
|
11
|
+
// message to display in the alert box
|
|
12
|
+
message?: string
|
|
13
|
+
// size of the icon in the alert box
|
|
14
|
+
iconSize?: number
|
|
15
|
+
// heading for the alert box
|
|
16
|
+
heading?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const Alert: React.FC<AlertProps> = ({ type = 'info', message = '', iconSize = 21, heading }) => {
|
|
20
|
+
// sanitize the text for setting dangerouslySetInnerHTML
|
|
21
|
+
const sanitizedData = () => ({
|
|
22
|
+
__html: DOMPurify.sanitize(message)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// reset styles to avoid conflicts in wcms
|
|
26
|
+
const styleResets = { width: 'unset', height: 'unset', paddingRight: '5px' }
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className={`alert alert-${type} p-1`} role='alert'>
|
|
30
|
+
{heading && <h4 className='alert-heading'>{heading}</h4>}
|
|
31
|
+
{type === 'success' && <Icon display='check' size={iconSize} />}
|
|
32
|
+
{type === 'danger' && <Icon display='warningCircle' size={iconSize} />}
|
|
33
|
+
{type === 'info' && <Icon display='info' size={iconSize} />}
|
|
34
|
+
<span dangerouslySetInnerHTML={sanitizedData()} />
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default Alert
|
|
@@ -19,6 +19,7 @@ import boxplotCellMatrix from './helpers/boxplotCellMatrix'
|
|
|
19
19
|
import removeNullColumns from './helpers/removeNullColumns'
|
|
20
20
|
import { TableConfig } from './types/TableConfig'
|
|
21
21
|
import { Column } from '../../types/Column'
|
|
22
|
+
import { pivotData } from '../../helpers/pivotData'
|
|
22
23
|
|
|
23
24
|
export type DataTableProps = {
|
|
24
25
|
applyLegendToRow?: Function
|
|
@@ -52,11 +53,39 @@ export type DataTableProps = {
|
|
|
52
53
|
|
|
53
54
|
/* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
|
|
54
55
|
const DataTable = (props: DataTableProps) => {
|
|
55
|
-
const {
|
|
56
|
-
|
|
56
|
+
const {
|
|
57
|
+
config,
|
|
58
|
+
dataConfig,
|
|
59
|
+
tableTitle,
|
|
60
|
+
vizTitle,
|
|
61
|
+
rawData,
|
|
62
|
+
runtimeData: parentRuntimeData,
|
|
63
|
+
headerColor,
|
|
64
|
+
expandDataTable,
|
|
65
|
+
columns,
|
|
66
|
+
viewport,
|
|
67
|
+
formatLegendLocation,
|
|
68
|
+
tabbingId,
|
|
69
|
+
wrapColumns
|
|
70
|
+
} = props
|
|
71
|
+
const runtimeData = useMemo(() => {
|
|
72
|
+
const data = removeNullColumns(parentRuntimeData)
|
|
73
|
+
if (config.table.pivot) {
|
|
74
|
+
const { columnName, valueColumn } = config.table.pivot
|
|
75
|
+
if (columnName && valueColumn) {
|
|
76
|
+
return pivotData(data, columnName, valueColumn)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return data
|
|
80
|
+
}, [parentRuntimeData, config.table.pivot?.columnName, config.table.pivot?.valueColumn])
|
|
81
|
+
|
|
57
82
|
const [expanded, setExpanded] = useState(expandDataTable)
|
|
58
83
|
|
|
59
|
-
const [sortBy, setSortBy] = useState<any>({
|
|
84
|
+
const [sortBy, setSortBy] = useState<any>({
|
|
85
|
+
column: config.type === 'map' ? 'geo' : 'date',
|
|
86
|
+
asc: false,
|
|
87
|
+
colIndex: null
|
|
88
|
+
})
|
|
60
89
|
|
|
61
90
|
const [accessibilityLabel, setAccessibilityLabel] = useState('')
|
|
62
91
|
|
|
@@ -75,7 +104,8 @@ const DataTable = (props: DataTableProps) => {
|
|
|
75
104
|
// Change accessibility label depending on expanded status
|
|
76
105
|
useEffect(() => {
|
|
77
106
|
const expandedLabel = 'Accessible data table.'
|
|
78
|
-
const collapsedLabel =
|
|
107
|
+
const collapsedLabel =
|
|
108
|
+
'Accessible data table. This table is currently collapsed visually but can still be read using a screen reader.'
|
|
79
109
|
|
|
80
110
|
if (expanded === true && accessibilityLabel !== expandedLabel) {
|
|
81
111
|
setAccessibilityLabel(expandedLabel)
|
|
@@ -130,7 +160,9 @@ const DataTable = (props: DataTableProps) => {
|
|
|
130
160
|
|
|
131
161
|
const caption = useMemo(() => {
|
|
132
162
|
if (config.type === 'map') {
|
|
133
|
-
return config.table.caption
|
|
163
|
+
return config.table.caption
|
|
164
|
+
? config.table.caption
|
|
165
|
+
: `Data table showing data for the ${mapLookup[config.general.geoType]} figure.`
|
|
134
166
|
} else {
|
|
135
167
|
return config.table.caption ? config.table.caption : `Data table showing data for the ${config.type} figure.`
|
|
136
168
|
}
|
|
@@ -176,48 +208,102 @@ const DataTable = (props: DataTableProps) => {
|
|
|
176
208
|
}
|
|
177
209
|
}
|
|
178
210
|
|
|
211
|
+
const getMediaControlsClasses = () => {
|
|
212
|
+
const classes = ['download-links']
|
|
213
|
+
const isLegendOnBottom = config?.legend?.position === 'bottom' || ['sm', 'xs', 'xxs'].includes(viewport)
|
|
214
|
+
if (config.brush?.active && !isLegendOnBottom) classes.push('brush-active')
|
|
215
|
+
if (config.brush?.active && config.legend.hide) classes.push('brush-active')
|
|
216
|
+
return classes
|
|
217
|
+
}
|
|
218
|
+
|
|
179
219
|
return (
|
|
180
220
|
<ErrorBoundary component='DataTable'>
|
|
181
|
-
<MediaControls.Section classes={
|
|
221
|
+
<MediaControls.Section classes={getMediaControlsClasses()}>
|
|
182
222
|
<MediaControls.Link config={config} dashboardDataConfig={dataConfig} />
|
|
183
|
-
{(config.table.download || config.general?.showDownloadButton) &&
|
|
223
|
+
{(config.table.download || config.general?.showDownloadButton) && (
|
|
224
|
+
<DownloadButton
|
|
225
|
+
rawData={getDownloadData()}
|
|
226
|
+
fileName={`${vizTitle || 'data-table'}.csv`}
|
|
227
|
+
headerColor={headerColor}
|
|
228
|
+
/>
|
|
229
|
+
)}
|
|
184
230
|
</MediaControls.Section>
|
|
185
|
-
<section
|
|
231
|
+
<section
|
|
232
|
+
id={tabbingId.replace('#', '')}
|
|
233
|
+
className={`data-table-container ${viewport} w-100`}
|
|
234
|
+
aria-label={accessibilityLabel}
|
|
235
|
+
>
|
|
186
236
|
<SkipTo skipId={skipId} skipMessage='Skip Data Table' />
|
|
187
|
-
{config.table.collapsible !== false &&
|
|
237
|
+
{config.table.collapsible !== false && (
|
|
238
|
+
<ExpandCollapse
|
|
239
|
+
expanded={expanded}
|
|
240
|
+
setExpanded={setExpanded}
|
|
241
|
+
fontSize={config.fontSize}
|
|
242
|
+
tableTitle={tableTitle}
|
|
243
|
+
viewport={viewport}
|
|
244
|
+
/>
|
|
245
|
+
)}
|
|
188
246
|
<div className='table-container' style={limitHeight}>
|
|
189
247
|
<Table
|
|
248
|
+
preliminaryData={config.preliminaryData}
|
|
190
249
|
viewport={viewport}
|
|
191
250
|
wrapColumns={wrapColumns}
|
|
192
|
-
childrenMatrix={
|
|
251
|
+
childrenMatrix={
|
|
252
|
+
config.type === 'map'
|
|
253
|
+
? mapCellMatrix({ rows, wrapColumns, ...props, runtimeData, viewport })
|
|
254
|
+
: chartCellMatrix({ rows, ...props, runtimeData, isVertical, sortBy, hasRowType, viewport })
|
|
255
|
+
}
|
|
193
256
|
tableName={config.type}
|
|
194
257
|
caption={caption}
|
|
195
258
|
stickyHeader
|
|
196
259
|
hasRowType={hasRowType}
|
|
197
|
-
headContent={
|
|
198
|
-
|
|
260
|
+
headContent={
|
|
261
|
+
config.type === 'map' ? (
|
|
262
|
+
<MapHeader columns={columns} {...props} sortBy={sortBy} setSortBy={setSortBy} />
|
|
263
|
+
) : (
|
|
264
|
+
<ChartHeader
|
|
265
|
+
data={runtimeData}
|
|
266
|
+
{...props}
|
|
267
|
+
hasRowType={hasRowType}
|
|
268
|
+
isVertical={isVertical}
|
|
269
|
+
sortBy={sortBy}
|
|
270
|
+
setSortBy={setSortBy}
|
|
271
|
+
/>
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
tableOptions={{
|
|
275
|
+
className: `${expanded ? 'data-table' : 'data-table cdcdataviz-sr-only'}${
|
|
276
|
+
isVertical ? '' : ' horizontal'
|
|
277
|
+
}`,
|
|
278
|
+
'aria-live': 'assertive',
|
|
279
|
+
'aria-rowcount': config?.data?.length ? config.data.length : -1,
|
|
280
|
+
hidden: !expanded
|
|
281
|
+
}}
|
|
199
282
|
fontSize={config.fontSize}
|
|
200
283
|
/>
|
|
201
284
|
|
|
202
285
|
{/* REGION Data Table */}
|
|
203
|
-
{noRelativeRegions &&
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
286
|
+
{noRelativeRegions &&
|
|
287
|
+
config.regions &&
|
|
288
|
+
config.regions.length > 0 &&
|
|
289
|
+
config.visualizationType !== 'Box Plot' && (
|
|
290
|
+
<Table
|
|
291
|
+
viewport={viewport}
|
|
292
|
+
wrapColumns={wrapColumns}
|
|
293
|
+
childrenMatrix={regionCellMatrix({ config })}
|
|
294
|
+
tableName={config.visualizationType}
|
|
295
|
+
caption='Table of the highlighted regions in the visualization'
|
|
296
|
+
headContent={
|
|
297
|
+
<tr>
|
|
298
|
+
<th>Region Name</th>
|
|
299
|
+
<th>Start Date</th>
|
|
300
|
+
<th>End Date</th>
|
|
301
|
+
</tr>
|
|
302
|
+
}
|
|
303
|
+
tableOptions={{ className: 'region-table data-table' }}
|
|
304
|
+
fontSize={config.fontSize}
|
|
305
|
+
/>
|
|
306
|
+
)}
|
|
221
307
|
</div>
|
|
222
308
|
</section>
|
|
223
309
|
<div id={skipId} className='cdcdataviz-sr-only'>
|
|
@@ -229,7 +315,11 @@ const DataTable = (props: DataTableProps) => {
|
|
|
229
315
|
// Render Data Table for Box Plots
|
|
230
316
|
return (
|
|
231
317
|
<ErrorBoundary component='DataTable'>
|
|
232
|
-
<section
|
|
318
|
+
<section
|
|
319
|
+
id={tabbingId.replace('#', '')}
|
|
320
|
+
className={`data-table-container ${viewport} w-100`}
|
|
321
|
+
aria-label={accessibilityLabel}
|
|
322
|
+
>
|
|
233
323
|
<SkipTo skipId={skipId} skipMessage='Skip Data Table' />
|
|
234
324
|
<ExpandCollapse expanded={expanded} setExpanded={setExpanded} tableTitle={tableTitle} />
|
|
235
325
|
<div className='table-container' style={limitHeight}>
|
|
@@ -241,7 +331,12 @@ const DataTable = (props: DataTableProps) => {
|
|
|
241
331
|
caption={caption}
|
|
242
332
|
stickyHeader
|
|
243
333
|
headContent={<BoxplotHeader categories={config.boxplot.categories} />}
|
|
244
|
-
tableOptions={{
|
|
334
|
+
tableOptions={{
|
|
335
|
+
className: `${expanded ? 'data-table' : 'data-table cdcdataviz-sr-only'}`,
|
|
336
|
+
'aria-live': 'assertive',
|
|
337
|
+
'aria-rowcount': 11,
|
|
338
|
+
hidden: !expanded
|
|
339
|
+
}}
|
|
245
340
|
fontSize={config.fontSize}
|
|
246
341
|
/>
|
|
247
342
|
</div>
|
|
@@ -5,6 +5,7 @@ import DataTable from './DataTable'
|
|
|
5
5
|
import DataTableEditorPanel from './components/DataTableEditorPanel'
|
|
6
6
|
import Filters from '../Filters'
|
|
7
7
|
import { TableConfig } from './types/TableConfig'
|
|
8
|
+
import { filterVizData } from '../../helpers/filterVizData'
|
|
8
9
|
|
|
9
10
|
type StandAloneProps = {
|
|
10
11
|
visualizationKey: string
|
|
@@ -14,34 +15,12 @@ type StandAloneProps = {
|
|
|
14
15
|
updateConfig?: (Visualization) => void
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
// filterData is copied from ./packages/chart/src/helpers/filterData.ts
|
|
18
|
-
// consider moving this to a shared location
|
|
19
|
-
const filterData = (filters, data) => {
|
|
20
|
-
if (!filters) return data
|
|
21
|
-
const filteredData: any[] = []
|
|
22
|
-
|
|
23
|
-
data.forEach(row => {
|
|
24
|
-
let add = true
|
|
25
|
-
filters
|
|
26
|
-
.filter(filter => filter.type !== 'url')
|
|
27
|
-
.forEach(filter => {
|
|
28
|
-
if (row[filter.columnName] != filter.active) {
|
|
29
|
-
add = false
|
|
30
|
-
}
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
if (add) filteredData.push(row)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
return filteredData
|
|
37
|
-
}
|
|
38
|
-
|
|
39
18
|
const DataTableStandAlone: React.FC<StandAloneProps> = ({ visualizationKey, config, updateConfig, viewport, isEditor }) => {
|
|
40
|
-
const [filteredData, setFilteredData] = useState<Record<string, any>[]>(
|
|
19
|
+
const [filteredData, setFilteredData] = useState<Record<string, any>[]>(filterVizData(config.filters, config.formattedData))
|
|
41
20
|
|
|
42
21
|
useEffect(() => {
|
|
43
22
|
// when using editor changes to filter should update the data
|
|
44
|
-
setFilteredData(
|
|
23
|
+
setFilteredData(filterVizData(config.filters, config?.formattedData?.length > 0 ? config.formattedData : config.data))
|
|
45
24
|
}, [config.filters])
|
|
46
25
|
|
|
47
26
|
if (isEditor)
|
|
@@ -53,7 +32,7 @@ const DataTableStandAlone: React.FC<StandAloneProps> = ({ visualizationKey, conf
|
|
|
53
32
|
|
|
54
33
|
return (
|
|
55
34
|
<>
|
|
56
|
-
<Filters config={config} setConfig={updateConfig} setFilteredData={setFilteredData}
|
|
35
|
+
<Filters config={config} setConfig={updateConfig} setFilteredData={setFilteredData} filteredData={filteredData} excludedData={config.formattedData} />
|
|
57
36
|
<DataTable expandDataTable={true} config={config} rawData={config.data} runtimeData={filteredData} tabbingId={visualizationKey} tableTitle={config.table.label} viewport={viewport || 'lg'} />
|
|
58
37
|
</>
|
|
59
38
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AccordionItem, AccordionItemButton, AccordionItemHeading, AccordionItemPanel } from 'react-accessible-accordion'
|
|
1
|
+
import { Accordion, AccordionItem, AccordionItemButton, AccordionItemHeading, AccordionItemPanel } from 'react-accessible-accordion'
|
|
2
2
|
import DataTableEditor from '../../EditorPanel/DataTableEditor'
|
|
3
3
|
import { Visualization } from '@cdc/core/types/Visualization'
|
|
4
4
|
import { updateFieldFactory } from '@cdc/core/helpers/updateFieldFactory'
|
|
@@ -25,9 +25,9 @@ const DataTableEditorPanel: React.FC<DataTableEditorProps> = ({ config, updateCo
|
|
|
25
25
|
})
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const columns = Object.keys(config.originalFormattedData[0] || {})
|
|
28
|
+
const columns = Object.keys(config.originalFormattedData?.[0] || {})
|
|
29
29
|
return (
|
|
30
|
-
|
|
30
|
+
<Accordion allowZeroExpanded={true}>
|
|
31
31
|
<AccordionItem>
|
|
32
32
|
<AccordionItemHeading>
|
|
33
33
|
<AccordionItemButton>Filters</AccordionItemButton>
|
|
@@ -52,7 +52,7 @@ const DataTableEditorPanel: React.FC<DataTableEditorProps> = ({ config, updateCo
|
|
|
52
52
|
<DataTableEditor config={config} columns={columns} updateField={updateField} isDashboard={true} />
|
|
53
53
|
</AccordionItemPanel>
|
|
54
54
|
</AccordionItem>
|
|
55
|
-
|
|
55
|
+
</Accordion>
|
|
56
56
|
)
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Icon from '../../ui/Icon'
|
|
2
|
+
import { fontSizes } from '../../../helpers/cove/fontSettings'
|
|
2
3
|
|
|
3
4
|
const ExpandCollapse = ({ expanded, setExpanded, tableTitle, fontSize, viewport }) => {
|
|
4
|
-
const fontSizes = { small: 16, medium: 18, large: 20 }
|
|
5
5
|
const titleFontSize = ['sm', 'xs', 'xxs'].includes(viewport) ? '13px' : `${fontSizes[fontSize]}px`
|
|
6
6
|
return (
|
|
7
7
|
<div
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import LegendShape from '@cdc/core/components/LegendShape'
|
|
2
2
|
import { customSort } from './customSort'
|
|
3
3
|
import { getSeriesName } from './getSeriesName'
|
|
4
4
|
import { DataTableProps } from '../DataTable'
|
|
@@ -6,6 +6,7 @@ import { getChartCellValue } from './getChartCellValue'
|
|
|
6
6
|
import { getDataSeriesColumns } from './getDataSeriesColumns'
|
|
7
7
|
import { ReactNode } from 'react'
|
|
8
8
|
import { CellMatrix, GroupCellMatrix } from '../../Table/types/CellMatrix'
|
|
9
|
+
import { getRowType } from './getRowType'
|
|
9
10
|
|
|
10
11
|
type ChartRowsProps = DataTableProps & {
|
|
11
12
|
rows: string[]
|
|
@@ -15,7 +16,7 @@ type ChartRowsProps = DataTableProps & {
|
|
|
15
16
|
hasRowType?: boolean
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
const chartCellArray = ({ rows, runtimeData, config, isVertical, sortBy, colorScale, hasRowType
|
|
19
|
+
const chartCellArray = ({ rows, runtimeData, config, isVertical, sortBy, colorScale, hasRowType }: ChartRowsProps): CellMatrix | GroupCellMatrix => {
|
|
19
20
|
const groupBy = config.table?.groupBy
|
|
20
21
|
const dataSeriesColumns = getDataSeriesColumns(config, isVertical, runtimeData)
|
|
21
22
|
|
|
@@ -58,15 +59,8 @@ const chartCellArray = ({ rows, runtimeData, config, isVertical, sortBy, colorSc
|
|
|
58
59
|
} else {
|
|
59
60
|
return rows.map(row => {
|
|
60
61
|
if (hasRowType) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
dataSeriesColumns.forEach((column, j) => {
|
|
64
|
-
if (column.match(/row[_-]?type/i)) {
|
|
65
|
-
rowType = getChartCellValue(row, column, config, runtimeData)
|
|
66
|
-
} else {
|
|
67
|
-
rowValues.push(getChartCellValue(row, column, config, runtimeData))
|
|
68
|
-
}
|
|
69
|
-
})
|
|
62
|
+
const rowType = getRowType(runtimeData[row])
|
|
63
|
+
const rowValues = dataSeriesColumns.map(column => getChartCellValue(row, column, config, runtimeData))
|
|
70
64
|
return [rowType, ...rowValues]
|
|
71
65
|
} else {
|
|
72
66
|
return dataSeriesColumns.map((column, j) => getChartCellValue(row, column, config, runtimeData))
|
|
@@ -80,7 +74,7 @@ const chartCellArray = ({ rows, runtimeData, config, isVertical, sortBy, colorSc
|
|
|
80
74
|
config.visualizationType !== 'Pie'
|
|
81
75
|
? [
|
|
82
76
|
<>
|
|
83
|
-
{colorScale && colorScale(seriesName) && <
|
|
77
|
+
{colorScale && colorScale(seriesName) && <LegendShape fill={colorScale(seriesName)} />}
|
|
84
78
|
{seriesName}
|
|
85
79
|
</>
|
|
86
80
|
]
|
|
@@ -60,19 +60,23 @@ export const getChartCellValue = (row: string, column: string, config: TableConf
|
|
|
60
60
|
const isValueMatch = String(pd.value) === String(labelValue)
|
|
61
61
|
// check entered suppression column against table key
|
|
62
62
|
const isColumnMatch = !pd.column || pd.column === column
|
|
63
|
-
|
|
63
|
+
const barSeriesExist = config.runtime?.barSeriesKeys?.includes(column)
|
|
64
|
+
const lineSeriesExist = config.runtime?.lineSeriesKeys?.includes(column)
|
|
65
|
+
const showSymbol = config.general.showSuppressedSymbol
|
|
66
|
+
if (isValueMatch && isColumnMatch && pd.displayTable && pd.type === 'suppression' && config.visualizationSubType !== 'stacked') {
|
|
64
67
|
switch (config.visualizationType) {
|
|
65
68
|
case 'Combo':
|
|
66
|
-
cellValue =
|
|
69
|
+
cellValue = barSeriesExist && showSymbol ? pd.iconCode : lineSeriesExist && showSymbol ? pd.lineCode : ''
|
|
67
70
|
break
|
|
68
71
|
case 'Bar':
|
|
69
|
-
cellValue = pd.iconCode
|
|
72
|
+
cellValue = !showSymbol ? '' : pd.iconCode
|
|
70
73
|
break
|
|
71
74
|
case 'Line':
|
|
72
|
-
cellValue = pd.lineCode
|
|
75
|
+
cellValue = !showSymbol ? '' : pd.lineCode
|
|
73
76
|
break
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
})
|
|
77
|
-
|
|
80
|
+
const shoMissingDataCellValue = config.general?.showMissingDataLabel && (!labelValue || labelValue === 'null')
|
|
81
|
+
return shoMissingDataCellValue ? 'N/A' : cellValue
|
|
78
82
|
}
|
|
@@ -3,15 +3,16 @@ import _ from 'lodash'
|
|
|
3
3
|
import { Column } from '../../../types/Column'
|
|
4
4
|
|
|
5
5
|
export const getDataSeriesColumns = (config: TableConfig, isVertical: boolean, runtimeData: Object[]): string[] => {
|
|
6
|
-
|
|
6
|
+
if (config.visualizationType === 'Sankey') return Object.keys(config?.data?.[0]?.tableData[0])
|
|
7
|
+
const configColumns = _.cloneDeep(config.columns) || ({} as Record<string, Column>)
|
|
7
8
|
const excludeColumns = Object.values(configColumns)
|
|
8
9
|
.filter(column => column.dataTable === false)
|
|
9
10
|
.map(column => column.name)
|
|
10
11
|
let tmpSeriesColumns: string[] = []
|
|
11
12
|
if (config.visualizationType !== 'Pie') {
|
|
12
13
|
tmpSeriesColumns = isVertical ? [config.xAxis?.dataKey] : [] //, ...config.runtime.seriesLabelsAll
|
|
13
|
-
if (config.series) {
|
|
14
|
-
config.series.forEach(element => {
|
|
14
|
+
if (config.runtime?.series) {
|
|
15
|
+
config.runtime?.series.forEach(element => {
|
|
15
16
|
tmpSeriesColumns.push(element.dataKey)
|
|
16
17
|
})
|
|
17
18
|
} else if (runtimeData && runtimeData.length > 0) {
|
|
@@ -21,12 +22,15 @@ export const getDataSeriesColumns = (config: TableConfig, isVertical: boolean, r
|
|
|
21
22
|
tmpSeriesColumns = isVertical ? [config.xAxis?.dataKey, config.yAxis?.dataKey] : [config.yAxis?.dataKey]
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
const dataColumns = Object.keys(runtimeData[0] || {})
|
|
26
|
+
|
|
24
27
|
// then add the additional Columns
|
|
25
|
-
Object.
|
|
26
|
-
|
|
28
|
+
Object.values(configColumns).forEach(function (value) {
|
|
29
|
+
if (!value.name) return
|
|
27
30
|
// add if not the index AND it is enabled to be added to data table
|
|
28
31
|
const alreadyAdded = tmpSeriesColumns.includes(value.name)
|
|
29
|
-
|
|
32
|
+
const columnIsInData = dataColumns.includes(value.name) // null columns are excluded from data
|
|
33
|
+
if (value.name !== config.xAxis?.dataKey && value.dataTable === true && !alreadyAdded && columnIsInData) {
|
|
30
34
|
tmpSeriesColumns.push(value.name)
|
|
31
35
|
}
|
|
32
36
|
})
|
|
@@ -53,6 +57,5 @@ export const getDataSeriesColumns = (config: TableConfig, isVertical: boolean, r
|
|
|
53
57
|
})
|
|
54
58
|
|
|
55
59
|
tmpSeriesColumns.sort((a, b) => columnOrderingHash[a] - columnOrderingHash[b])
|
|
56
|
-
|
|
57
60
|
return tmpSeriesColumns
|
|
58
61
|
}
|