@cdc/core 4.24.5 → 4.24.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 (109) hide show
  1. package/assets/icon-gear-multi.svg +23 -0
  2. package/components/AdvancedEditor/AdvancedEditor.tsx +93 -0
  3. package/components/AdvancedEditor/advanced-editor-styles.css +3 -0
  4. package/components/AdvancedEditor/index.ts +1 -0
  5. package/components/Alert/components/Alert.styles.css +15 -0
  6. package/components/Alert/components/Alert.tsx +39 -0
  7. package/components/Alert/index.tsx +3 -0
  8. package/components/DataTable/DataTable.tsx +127 -32
  9. package/components/DataTable/DataTableStandAlone.tsx +4 -25
  10. package/components/DataTable/components/DataTableEditorPanel.tsx +4 -4
  11. package/components/DataTable/components/ExpandCollapse.tsx +1 -1
  12. package/components/DataTable/helpers/chartCellMatrix.tsx +6 -12
  13. package/components/DataTable/helpers/getChartCellValue.ts +9 -5
  14. package/components/DataTable/helpers/getDataSeriesColumns.ts +10 -7
  15. package/components/DataTable/helpers/getRowType.ts +6 -0
  16. package/components/DataTable/helpers/mapCellMatrix.tsx +3 -3
  17. package/components/DataTable/types/TableConfig.ts +2 -1
  18. package/components/EditorPanel/ColumnsEditor.tsx +3 -30
  19. package/components/EditorPanel/DataTableEditor.tsx +66 -22
  20. package/components/EditorPanel/FieldSetWrapper.tsx +51 -0
  21. package/components/EditorPanel/FootnotesEditor.tsx +77 -0
  22. package/components/EditorPanel/Inputs.tsx +13 -4
  23. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +268 -0
  24. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +306 -0
  25. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +40 -0
  26. package/components/EditorPanel/VizFilterEditor/index.ts +1 -0
  27. package/components/EditorWrapper/EditorWrapper.tsx +3 -4
  28. package/components/EditorWrapper/index.ts +1 -0
  29. package/components/Filters.tsx +520 -0
  30. package/components/Footnotes/Footnotes.tsx +25 -0
  31. package/components/Footnotes/FootnotesStandAlone.tsx +45 -0
  32. package/components/Footnotes/footnotes.css +5 -0
  33. package/components/Footnotes/index.ts +1 -0
  34. package/components/Layout/components/Responsive.tsx +14 -4
  35. package/components/Layout/components/Sidebar/components/Sidebar.tsx +14 -5
  36. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +23 -20
  37. package/components/Layout/components/Visualization/index.tsx +19 -6
  38. package/components/Layout/components/Visualization/visualizations.scss +32 -26
  39. package/components/Layout/styles/editor.scss +0 -8
  40. package/components/Legend/Legend.Gradient.tsx +133 -0
  41. package/components/LegendShape.tsx +28 -0
  42. package/components/MultiSelect/MultiSelect.tsx +41 -11
  43. package/components/MultiSelect/multiselect.styles.css +0 -3
  44. package/components/NestedDropdown/NestedDropdown.tsx +47 -52
  45. package/components/NestedDropdown/nesteddropdown.styles.css +19 -25
  46. package/components/Table/Table.tsx +8 -5
  47. package/components/Table/components/Cell.tsx +2 -2
  48. package/components/Table/components/Row.tsx +25 -7
  49. package/components/_stories/Footnotes.stories.tsx +17 -0
  50. package/components/_stories/Layout.Debug.stories.tsx +91 -0
  51. package/components/_stories/_mocks/bar-chart-suppressed.json +474 -0
  52. package/components/_stories/styles.scss +14 -1
  53. package/components/createBarElement.jsx +4 -4
  54. package/components/inputs/InputSelect.tsx +17 -6
  55. package/components/ui/Icon.tsx +22 -16
  56. package/components/ui/Title/Title.scss +0 -8
  57. package/helpers/DataTransform.ts +2 -2
  58. package/helpers/addValuesToFilters.ts +135 -0
  59. package/helpers/cove/accessibility.ts +17 -4
  60. package/helpers/cove/fontSettings.ts +2 -0
  61. package/helpers/coveUpdateWorker.ts +30 -9
  62. package/helpers/filterVizData.ts +49 -0
  63. package/helpers/formatConfigBeforeSave.ts +95 -0
  64. package/helpers/gatherQueryParams.ts +14 -7
  65. package/helpers/getGradientLegendWidth.ts +15 -0
  66. package/helpers/getTextWidth.ts +18 -0
  67. package/helpers/lineChartHelpers.js +2 -1
  68. package/helpers/pivotData.ts +18 -0
  69. package/helpers/queryStringUtils.ts +29 -0
  70. package/helpers/scaling.ts +7 -0
  71. package/helpers/tests/addValuesToFilters.test.ts +55 -0
  72. package/helpers/tests/filterVizData.test.ts +31 -0
  73. package/helpers/tests/invertValue.test.ts +35 -0
  74. package/helpers/tests/updateFieldFactory.test.ts +1 -0
  75. package/helpers/updateFieldFactory.ts +1 -1
  76. package/helpers/updatePaletteNames.ts +19 -0
  77. package/helpers/{useDataVizClasses.js → useDataVizClasses.ts} +3 -2
  78. package/helpers/ver/4.24.5.ts +3 -3
  79. package/helpers/ver/4.24.7.ts +123 -0
  80. package/helpers/ver/4.24.9.ts +63 -0
  81. package/helpers/ver/tests/4.24.9.test.ts +22 -0
  82. package/helpers/ver/versionNeedsUpdate.ts +9 -0
  83. package/package.json +6 -4
  84. package/styles/_button-section.scss +7 -2
  85. package/styles/_data-table.scss +0 -1
  86. package/styles/_global.scss +6 -2
  87. package/styles/base.scss +4 -0
  88. package/styles/filters.scss +4 -0
  89. package/styles/v2/themes/_color-definitions.scss +1 -0
  90. package/types/Annotation.ts +46 -0
  91. package/types/Axis.ts +3 -2
  92. package/types/ConfigureData.ts +1 -1
  93. package/types/Dimensions.ts +1 -0
  94. package/types/Footnotes.ts +17 -0
  95. package/types/General.ts +5 -0
  96. package/types/Runtime.ts +2 -7
  97. package/types/Table.ts +6 -0
  98. package/types/Visualization.ts +31 -9
  99. package/types/VizFilter.ts +39 -7
  100. package/LICENSE +0 -201
  101. package/components/AdvancedEditor.jsx +0 -74
  102. package/components/EditorPanel/VizFilterEditor.tsx +0 -234
  103. package/components/Filters.jsx +0 -461
  104. package/components/LegendCircle.jsx +0 -17
  105. package/helpers/queryStringUtils.js +0 -26
  106. package/helpers/updatePaletteNames.js +0 -16
  107. package/types/BaseVisualizationType.ts +0 -1
  108. /package/components/{Waiting.jsx → Waiting.tsx} +0 -0
  109. /package/helpers/ver/{4.23.4.ts → 4.24.4.ts} +0 -0
@@ -0,0 +1,135 @@
1
+ import _ from 'lodash'
2
+ import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
3
+ import { VizFilter } from '../types/VizFilter'
4
+
5
+ type Filter = {
6
+ columnName: string
7
+ values: (string | number)[]
8
+ filterStyle?: string
9
+ active?: string | number | (string | number)[]
10
+ parents?: (string | number)[]
11
+ }
12
+
13
+ /** MapData is an object */
14
+ type MapData = Record<string, any[]>
15
+
16
+ const cleanLookup = (lookup: Record<string, { values: string[]; orderedValues?: string[] }>) => {
17
+ // for nested-dropdown
18
+ // removes values from subGrouping.valuesLookup
19
+ // keeps orderedValues
20
+ return Object.fromEntries(
21
+ Object.entries(lookup || {}).map(([key, { orderedValues }]) => {
22
+ if (!orderedValues) return [key, { values: [] }]
23
+ return [key, { orderedValues, values: [] }]
24
+ })
25
+ )
26
+ }
27
+
28
+ // Gets filter values from dataset
29
+ const generateValuesForFilter = (filter: VizFilter, data: any[] | MapData) => {
30
+ const columnName = filter.columnName
31
+ const values: string[] = []
32
+ const subGroupingColumn = filter.subGrouping?.columnName
33
+ const subValues = cleanLookup(filter.subGrouping?.valuesLookup)
34
+ if (Array.isArray(data)) {
35
+ data.forEach(row => {
36
+ const value = row[columnName]
37
+ if (value !== undefined && !values.includes(value)) {
38
+ values.push(value)
39
+ }
40
+ if (subGroupingColumn) {
41
+ const dataValue = row[subGroupingColumn]
42
+ if (value === undefined) return
43
+ if (!subValues[value]) {
44
+ subValues[value] = { values: [] }
45
+ }
46
+ if (!subValues[value].values.includes(dataValue)) {
47
+ subValues[value].values.push(dataValue)
48
+ }
49
+ }
50
+ })
51
+ } else {
52
+ // data is a dataset this loops through ALL datasets to find matching values
53
+ // not sure if this is desired behavior
54
+ // Maps Only
55
+ if (!data) return values
56
+ Object.values(data).forEach((rows: any[]) => {
57
+ rows.forEach(row => {
58
+ const value = row[columnName]
59
+ if (value !== undefined && !values.includes(value)) {
60
+ values.push(value)
61
+ }
62
+ })
63
+ })
64
+ }
65
+ filter.values = values
66
+ if (subGroupingColumn) {
67
+ filter.subGrouping.valuesLookup = subValues
68
+ }
69
+ }
70
+
71
+ const handleVizParents = (filter: VizFilter, data: any[] | MapData, filtersLookup: Record<string, Filter>) => {
72
+ let filteredData = Array.isArray(data) ? data : Object.values(data).flat(1)
73
+ filter.parents.forEach(parentKey => {
74
+ const parent = filtersLookup[parentKey]
75
+ if (parent.filterStyle === 'nested-dropdown') {
76
+ const { subGrouping } = parent as VizFilter
77
+ if (subGrouping.active) {
78
+ filteredData = filteredData.filter(d => {
79
+ const matchingParentGroup = parent.active == d[parent.columnName]
80
+ const matchingSubGroup = subGrouping.active == d[subGrouping.columnName]
81
+ return matchingParentGroup && matchingSubGroup
82
+ })
83
+ }
84
+ } else if (parent?.active) {
85
+ filteredData = filteredData.filter(d => {
86
+ if (Array.isArray(parent.active)) {
87
+ return parent.active.includes(d[parent.columnName])
88
+ }
89
+ return parent.active == d[parent.columnName]
90
+ })
91
+ }
92
+ })
93
+ return filteredData
94
+ }
95
+
96
+ const includes = (arr: any[], val: any): boolean => {
97
+ return arr.map(val => String(val)).includes(String(val))
98
+ }
99
+
100
+ export const addValuesToFilters = (filters: VizFilter[], data: any[] | MapData): Array<VizFilter> => {
101
+ const filtersLookup = _.keyBy(filters, 'id')
102
+ return filters?.map(filter => {
103
+ const filterCopy = _.cloneDeep(filter)
104
+ let filteredData = data
105
+ const isMapData = !Array.isArray(data)
106
+ if (filter.parents?.length && !isMapData) {
107
+ filteredData = handleVizParents(filter as VizFilter, data, filtersLookup)
108
+ }
109
+ generateValuesForFilter(filterCopy, filteredData)
110
+ if (filterCopy.values.length > 0) {
111
+ const queryStringFilterValue = getQueryStringFilterValue(filterCopy)
112
+ if (queryStringFilterValue) {
113
+ filterCopy.active = queryStringFilterValue
114
+ } else if (filterCopy.filterStyle === 'multi-select') {
115
+ const defaultValues = filterCopy.values
116
+ const active = Array.isArray(filterCopy.active) ? filterCopy.active : [filterCopy.active]
117
+ filterCopy.active = active.filter(val => includes(defaultValues, val))
118
+ } else {
119
+ const defaultValue = filterCopy.values[0]
120
+ const active = Array.isArray(filterCopy.active) ? filterCopy.active[0] : filterCopy.active
121
+ filterCopy.active = includes(filterCopy.values, active) ? active : defaultValue
122
+ }
123
+ }
124
+ if (filterCopy.subGrouping) {
125
+ const queryStringFilterValue = getQueryStringFilterValue(filterCopy.subGrouping)
126
+ const groupActive = filterCopy.active || filterCopy.values[0]
127
+ const defaultValue = filterCopy.subGrouping.valuesLookup[groupActive as string].values[0]
128
+ // if the value doesn't exist in the subGrouping then return the default
129
+ const filteredLookupValues = Object.values(filterCopy.subGrouping.valuesLookup).flatMap(v => v.values)
130
+ const activeValue = queryStringFilterValue || filterCopy.subGrouping.active
131
+ filterCopy.subGrouping.active = filteredLookupValues.includes(activeValue) ? activeValue : defaultValue
132
+ }
133
+ return filterCopy
134
+ })
135
+ }
@@ -1,15 +1,18 @@
1
1
  import chroma from 'chroma-js'
2
2
 
3
3
  /**
4
- * WCAG 2.0 Standard requires 4.5:1 contrast ratio
5
- * https://www.w3.org/TR/WCAG20-TECHS/G18.html
4
+ * WCAG 2.1 CONSTANTS
5
+ * TEXT (4.5 Contrast Ratio): https://www.w3.org/TR/WCAG20-TECHS/G18.html
6
+ * NON-TEXT (3.5 Contrast Ratio): https://www.w3.org/TR/WCAG20-TECHS/G145.html
6
7
  *
7
8
  * !important - DO NOT CHANGE.
8
9
  */
9
- export const WCAG_CONTRAST_RATIO = 4.5
10
+ export const WCAG_TEXT_CONTRAST_RATIO = 4.5
11
+ export const WCAG_NON_TEXT_CONTRAST_RATIO = 3.5
10
12
 
11
13
  export const getContrastColor = (textColor: string, bgColor: string) => {
12
- if (chroma.contrast(textColor, bgColor) < WCAG_CONTRAST_RATIO) {
14
+ if (!bgColor) return
15
+ if (chroma.contrast(textColor, bgColor) < WCAG_TEXT_CONTRAST_RATIO) {
13
16
  switch (textColor) {
14
17
  case '#FFF':
15
18
  return '#000'
@@ -21,3 +24,13 @@ export const getContrastColor = (textColor: string, bgColor: string) => {
21
24
  }
22
25
  return textColor
23
26
  }
27
+
28
+ export const checkColorContrast = (color1: string, color2: string) => {
29
+ if (!chroma.valid(color1) || !chroma.valid(color2)) return false
30
+ return chroma.contrast(color1, color2) >= WCAG_NON_TEXT_CONTRAST_RATIO
31
+ }
32
+
33
+ export const getColorContrast = (color1: string, color2: string) => {
34
+ if (!chroma.valid(color1) || !chroma.valid(color2)) return false
35
+ return chroma.contrast(color1, color2)
36
+ }
@@ -0,0 +1,2 @@
1
+ // TODO: handle with css
2
+ export const fontSizes = { small: 16, medium: 18, large: 20 }
@@ -1,21 +1,42 @@
1
1
  // If config key names or position in the config have been changed with a version change,
2
2
  // process those config entries and format old values into new
3
- import update_4_24_4 from './ver/4.23.4'
3
+ import update_4_24_4 from './ver/4.24.4'
4
4
  import update_4_24_3 from './ver/4.24.3'
5
5
  import update_4_24_5 from './ver/4.24.5'
6
+ import update_4_24_7 from './ver/4.24.7'
7
+ import update_4_24_9 from './ver/4.24.9'
8
+ import versionNeedsUpdate from './ver/versionNeedsUpdate'
9
+ import { UpdateFunction } from 'json-edit-react'
6
10
 
7
11
  export const coveUpdateWorker = config => {
12
+ if (config.multiDashboards) {
13
+ config.multiDashboards.forEach((dashboard, index) => {
14
+ config.multiDashboards[index] = coveUpdateWorker(dashboard)
15
+ })
16
+ }
8
17
  let genConfig = config
9
18
 
10
- genConfig = update_4_24_3(genConfig)
11
- genConfig = update_4_24_4(genConfig)
12
- genConfig = update_4_24_5(genConfig)
19
+ const versions = [
20
+ ['4.24.3', update_4_24_3],
21
+ ['4.24.4', update_4_24_4],
22
+ ['4.24.5', update_4_24_5],
23
+ ['4.24.7', update_4_24_7, true],
24
+ ['4.24.9', update_4_24_9]
25
+ ]
13
26
 
14
- return genConfig
15
- }
27
+ versions.forEach(([version, updateFunction, alwaysRun]: [string, UpdateFunction, boolean?]) => {
28
+ if (versionNeedsUpdate(genConfig.version, version) || alwaysRun) {
29
+ genConfig = updateFunction(genConfig)
30
+ }
31
+ if (genConfig.multiDashboards) {
32
+ genConfig.multiDashboards.forEach((dashboard, index) => {
33
+ dashboard.type = 'dashboard'
34
+ genConfig.multiDashboards[index] = coveUpdateWorker(dashboard)
35
+ })
36
+ }
37
+ })
16
38
 
17
- const asyncWorker = async config => {
18
- return await coveUpdateWorker(config)
39
+ return genConfig
19
40
  }
20
41
 
21
- export default asyncWorker
42
+ export default coveUpdateWorker
@@ -0,0 +1,49 @@
1
+ import { SubGrouping } from '../types/VizFilter'
2
+
3
+ type Filter = {
4
+ columnName: string
5
+ type?: string
6
+ values: (string | number)[]
7
+ filterStyle?: string
8
+ active?: string | number | (string | number)[]
9
+ parents?: (string | number)[]
10
+ subGrouping?: SubGrouping
11
+ }
12
+
13
+ export const filterVizData = (filters: Filter[], data) => {
14
+ if (!data) {
15
+ console.warn('COVE: No data to filter')
16
+ return []
17
+ }
18
+
19
+ if (!filters) return data
20
+ const filteredData: any[] = []
21
+
22
+ data?.forEach(row => {
23
+ let add = true
24
+ filters
25
+ .filter(filter => filter.type !== 'url')
26
+ .forEach(filter => {
27
+ if (filter.active === undefined) return
28
+ const value = row[filter.columnName]
29
+
30
+ if (Array.isArray(filter.active)) {
31
+ if (!filter.active.includes(value)) {
32
+ add = false
33
+ }
34
+ } else if (value != filter.active) {
35
+ add = false
36
+ }
37
+ if (filter.filterStyle === 'nested-dropdown' && filter.subGrouping && add === true) {
38
+ const subGroupActive = filter.subGrouping.active
39
+ const value = row[filter.subGrouping.columnName]
40
+ if (subGroupActive === undefined) return
41
+ if (value != subGroupActive) {
42
+ add = false
43
+ }
44
+ }
45
+ })
46
+ if (add) filteredData.push(row)
47
+ })
48
+ return filteredData
49
+ }
@@ -0,0 +1,95 @@
1
+ import Footnotes from '@cdc/core/types/Footnotes'
2
+ import { Visualization } from '@cdc/core/types/Visualization'
3
+ import { DashboardConfig } from '@cdc/dashboard/src/types/DashboardConfig'
4
+ import _ from 'lodash'
5
+
6
+ const cleanDashboardFootnotes = (config: DashboardConfig) => {
7
+ // strip any blank footnote visualizations
8
+ const footnoteIds: string[] = []
9
+
10
+ if (config.rows) {
11
+ config.rows.forEach(row => {
12
+ if (row.footnotesId) {
13
+ const { dataKey, staticFootnotes } = config.visualizations[row.footnotesId] as Footnotes
14
+ if (!dataKey && !staticFootnotes?.length) {
15
+ delete config.visualizations[row.footnotesId]
16
+ } else {
17
+ footnoteIds.push(row.footnotesId)
18
+ }
19
+ }
20
+ })
21
+ }
22
+
23
+ if (config.visualizations) {
24
+ Object.keys(config.visualizations).forEach(vizKey => {
25
+ const viz: Visualization = config.visualizations[vizKey]
26
+ if (viz.type === 'footnotes' && !footnoteIds.includes(vizKey)) {
27
+ // if footnote isn't being used by any rows, remove it
28
+ delete config.visualizations[vizKey]
29
+ }
30
+ })
31
+ }
32
+ }
33
+
34
+ const cleanDashboardData = (config: DashboardConfig) => {
35
+ if (config.datasets) {
36
+ Object.keys(config.datasets).forEach(datasetKey => {
37
+ delete config.datasets[datasetKey].formattedData
38
+ if (config.datasets[datasetKey].dataUrl) {
39
+ delete config.datasets[datasetKey].data
40
+ }
41
+ })
42
+ }
43
+ if (config.visualizations) {
44
+ Object.keys(config.visualizations).forEach(vizKey => {
45
+ config.visualizations[vizKey] = _.omit(config.visualizations[vizKey], ['runtime', 'formattedData', 'data', 'editing', 'originalFormattedData'])
46
+ })
47
+ }
48
+ if (config.rows) {
49
+ config.rows.forEach((row, i) => {
50
+ if (row.dataKey) {
51
+ config.rows[i] = _.omit(row, ['data', 'formattedData'])
52
+ }
53
+ })
54
+ }
55
+ }
56
+
57
+ const cleanSharedFilters = (config: DashboardConfig) => {
58
+ if (config.dashboard?.sharedFilters) {
59
+ config.dashboard.sharedFilters.forEach((filter, index) => {
60
+ delete config.dashboard.sharedFilters[index].active
61
+ if (filter.type === 'urlfilter') {
62
+ delete config.dashboard.sharedFilters[index].values
63
+ }
64
+ })
65
+ }
66
+ }
67
+
68
+ export const formatConfigBeforeSave = configToStrip => {
69
+ const strippedConfig = _.cloneDeep(configToStrip)
70
+ if (strippedConfig.type === 'dashboard') {
71
+ if (strippedConfig.multiDashboards) {
72
+ strippedConfig.multiDashboards.forEach((multiDashboard, i) => {
73
+ cleanDashboardData(strippedConfig.multiDashboards[i])
74
+ cleanSharedFilters(strippedConfig.multiDashboards[i])
75
+ cleanDashboardFootnotes(strippedConfig.multiDashboards[i])
76
+ })
77
+ delete strippedConfig.dashboard
78
+ delete strippedConfig.rows
79
+ delete strippedConfig.visualizations
80
+ delete strippedConfig.label
81
+ delete strippedConfig.activeDashboard
82
+ }
83
+ cleanDashboardData(strippedConfig)
84
+ cleanSharedFilters(strippedConfig)
85
+ cleanDashboardFootnotes(strippedConfig)
86
+ } else {
87
+ delete strippedConfig.runtime
88
+ delete strippedConfig.formattedData
89
+ if (strippedConfig.dataUrl) {
90
+ delete strippedConfig.data
91
+ }
92
+ }
93
+
94
+ return strippedConfig
95
+ }
@@ -1,7 +1,14 @@
1
- export const gatherQueryParams = (params: {key: string, value: string}[]) => {
2
- return params.map(({key, value}, i) => {
3
- const leadingCharacter = i === 0 ? '?' : '&'
4
- return leadingCharacter + key + '=' + value
5
- })
6
- .join('')
7
- }
1
+ import _ from 'lodash'
2
+
3
+ export const gatherQueryParams = (baseEndpoint: string, params: { key: string; value: string }[]) => {
4
+ const baseEndpointHasQueryParams = baseEndpoint.includes('?')
5
+ return params
6
+ .filter(({ value }) => value !== '')
7
+ .map(({ key, value }, i) => {
8
+ const leadingCharacter = i === 0 && !baseEndpointHasQueryParams ? '?' : '&'
9
+ const isStatementParam = key.match(/\$.*/)
10
+ if (!_.isNaN(parseInt(value)) || isStatementParam) return leadingCharacter + key + '=' + value
11
+ return leadingCharacter + key + '=' + `"${value}"`
12
+ })
13
+ .join('')
14
+ }
@@ -0,0 +1,15 @@
1
+ export const getGradientLegendWidth = (containerWidth: number, currentViewport: string): number => {
2
+ const newWidth = Number(containerWidth)
3
+ switch (currentViewport) {
4
+ case 'lg':
5
+ return newWidth / 3
6
+ case 'md':
7
+ return newWidth / 2
8
+ case 'sm':
9
+ case 'xs':
10
+ case 'xxs':
11
+ return newWidth / 1.4
12
+ default:
13
+ return newWidth
14
+ }
15
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Gets the width of the specified text using a given font.
3
+ * @param text The text to measure.
4
+ * @param font The CSS font declaration to use, defaults to the body's font if not provided.
5
+ * @returns The width of the text in pixels, or undefined if the context could not be obtained.
6
+ */
7
+ export const getTextWidth = (text: string, font?: string): number | undefined => {
8
+ // Create a canvas element
9
+ const canvas = document.createElement('canvas')
10
+ const context = canvas.getContext('2d')
11
+ if (!context) {
12
+ console.error('2D context not found')
13
+ return
14
+ }
15
+
16
+ context.font = font || getComputedStyle(document.body).font
17
+ return Math.ceil(context.measureText(text).width)
18
+ }
@@ -3,5 +3,6 @@ export const approvedCurveTypes = {
3
3
  Cardinal: 'curveCardinal',
4
4
  Natural: 'curveNatural',
5
5
  'Monotone X': 'curveMonotoneX',
6
- Step: 'curveStep'
6
+ Step: 'curveStep',
7
+ 'Curve Basis': 'curveBasis'
7
8
  }
@@ -0,0 +1,18 @@
1
+ import _ from 'lodash'
2
+
3
+ /** columnName is the column you'd like to select data values from to show as column headers.
4
+ * Pivot is the value column who's data you'd like to show under those respective columns*/
5
+ export const pivotData = (data: Record<string, any>[], columnName: string, pivot: string) => {
6
+ const grouped = _.groupBy(data, val => val[columnName])
7
+ const newData = []
8
+ for (const key in grouped) {
9
+ const group = grouped[key]
10
+ group.forEach((val, index) => {
11
+ const row = newData[index] || {}
12
+ row[key] = val[pivot]
13
+ const toAdd = _.omit(val, [columnName, pivot])
14
+ newData[index] = { ...toAdd, ...row }
15
+ })
16
+ }
17
+ return newData
18
+ }
@@ -0,0 +1,29 @@
1
+ export function getQueryStringFilterValue(filter) {
2
+ const urlParams = new URLSearchParams(window.location.search)
3
+ if (filter.setByQueryParameter) {
4
+ // Only check the query string if the filter is supposed to be set by QS param
5
+ const filterValue = urlParams.get(filter.setByQueryParameter)
6
+ if (filterValue && filter.values) {
7
+ for (let i = 0; i < filter.values.length; i++) {
8
+ if (filter.values[i] && filter.values[i].toLowerCase() === filterValue.toLowerCase()) {
9
+ return filter.values[i]
10
+ }
11
+ }
12
+ }
13
+ }
14
+ }
15
+
16
+ export function getQueryParams() {
17
+ const queryParams = {}
18
+ for (const [key, value] of Array.from(new URLSearchParams(window.location.search).entries())) {
19
+ queryParams[key] = value
20
+ }
21
+ return queryParams
22
+ }
23
+
24
+ export function updateQueryString(queryParams) {
25
+ const updateUrl = `${window.location.origin}${window.location.pathname}?${Object.keys(queryParams)
26
+ .map(queryParam => `${queryParam}=${encodeURIComponent(queryParams[queryParam])}`)
27
+ .join('&')}`
28
+ window.history.pushState({ path: updateUrl }, '', updateUrl)
29
+ }
@@ -0,0 +1,7 @@
1
+ export const invertValue = (xScale, value) => {
2
+ if (xScale && typeof xScale.invert === 'function') {
3
+ return xScale.invert(value)
4
+ } else {
5
+ return null
6
+ }
7
+ }
@@ -0,0 +1,55 @@
1
+ import _ from 'lodash'
2
+ import { VizFilter } from '../../types/VizFilter'
3
+ import { addValuesToFilters } from '../addValuesToFilters'
4
+ import { describe, it, expect, vi } from 'vitest'
5
+
6
+ describe('addValuesToFilters', () => {
7
+ const parentFilter = { columnName: 'parentColumn', id: 11, active: 'apple', values: [] } as VizFilter
8
+ const parentFilter2 = { columnName: 'parentColumn2', id: 22, active: '1', values: [] } as VizFilter
9
+ const childFilter = { columnName: 'childColumn', id: 33, parents: [11, 22], values: [] } as VizFilter
10
+ const data = [
11
+ { parentColumn: 'apple', parentColumn2: 3, childColumn: 'a' },
12
+ { parentColumn: 'apple', parentColumn2: 1, childColumn: 'b' },
13
+ { parentColumn: 'pear', parentColumn2: 4, childColumn: 'c' }
14
+ ]
15
+ const filters: VizFilter[] = [parentFilter, childFilter, parentFilter2]
16
+ it('adds filter values based on parent active values', () => {
17
+ const filtersCopy = _.cloneDeep(filters)
18
+ const newFilters = addValuesToFilters(filtersCopy, data)
19
+ expect(newFilters[0].values).toEqual(['apple', 'pear'])
20
+ expect(newFilters[2].values).toEqual([3, 1, 4])
21
+ expect(newFilters[1].values).toEqual(['b'])
22
+
23
+ filtersCopy[0].active = 'pear'
24
+ const newFilters2 = addValuesToFilters(filtersCopy, data)
25
+ expect(newFilters2[0].values).toEqual(['apple', 'pear'])
26
+ expect(newFilters2[2].values).toEqual([3, 1, 4])
27
+ expect(newFilters2[1].values).toEqual([])
28
+ })
29
+ it('works when data is an object', () => {
30
+ const filtersCopy = _.cloneDeep(filters)
31
+ const newFilters = addValuesToFilters(filtersCopy, { '0': data })
32
+ expect(newFilters[0].values).toEqual(['apple', 'pear'])
33
+ expect(newFilters[2].values).toEqual([3, 1, 4])
34
+ // This test is failing
35
+ // data is only an object when using map data according to Adam Doe.
36
+ //expect(newFilters[1].values).toEqual([])
37
+ })
38
+ it('works for nested dropdowns', () => {
39
+ const nestedParentFilter = { ...parentFilter, subGrouping: { columnName: 'childColumn' } }
40
+ const newFilters = addValuesToFilters([nestedParentFilter], data)
41
+ expect(newFilters[0].values).toEqual(['apple', 'pear'])
42
+ expect(newFilters[0].subGrouping.valuesLookup).toEqual({ apple: { values: ['a', 'b'] }, pear: { values: ['c'] } })
43
+ nestedParentFilter.active = 'apple'
44
+ expect(newFilters[0].subGrouping.active).toEqual('a')
45
+ })
46
+ it('maintains nested custom order', () => {
47
+ const nestedParentFilter = {
48
+ ...parentFilter,
49
+ subGrouping: { columnName: 'childColumn', valuesLookup: { apple: { orderedValues: ['b', 'a'] } } }
50
+ }
51
+ const newFilters = addValuesToFilters([nestedParentFilter], data)
52
+ expect(newFilters[0].values).toEqual(['apple', 'pear'])
53
+ expect(newFilters[0].subGrouping.valuesLookup.apple.orderedValues).toEqual(['b', 'a'])
54
+ })
55
+ })
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { filterVizData } from '../filterVizData'
3
+ describe('filterVizData', () => {
4
+ it('filters data when there is a nested filter', () => {
5
+ const nestedFilter = {
6
+ columnName: 'nestedColumn',
7
+ id: 1,
8
+ active: 'nestedValue',
9
+ values: [],
10
+ filterStyle: 'nested-dropdown',
11
+ subGrouping: {
12
+ active: 'subGroupValue',
13
+ columnName: 'subGroupColumn',
14
+ valuesLookup: { nestedValue: { values: ['subGroupValue'] } }
15
+ }
16
+ }
17
+ const normalFilter = { columnName: 'normalColumn', id: 2, active: 'normalValue', values: [] }
18
+
19
+ const data = [
20
+ { nestedColumn: 'nestedValue', subGroupColumn: 'subGroupValue', normalColumn: 'normalValue' },
21
+ { nestedColumn: 'X', subGroupColumn: 'subGroupValue', normalColumn: 'normalValue' },
22
+ { nestedColumn: 'nestedValue', subGroupColumn: 'X', normalColumn: 'normalValue' },
23
+ { nestedColumn: 'nestedValue', subGroupColumn: 'subGroupValue', normalColumn: 'X' },
24
+ { nestedColumn: 'nestedValue', subGroupColumn: 'subGroupValue', normalColumn: 'normalValue' }
25
+ ]
26
+
27
+ const filters = [nestedFilter, normalFilter]
28
+ const filteredData = filterVizData(filters, data)
29
+ expect(filteredData.length).toEqual(2)
30
+ })
31
+ })
@@ -0,0 +1,35 @@
1
+ // invertValue.test.js
2
+ import { invertValue } from '../scaling'
3
+ import { expect, describe, it, vi } from 'vitest'
4
+ describe('invertValue', () => {
5
+ it('should return the inverted value if xScale has an invert function', () => {
6
+ const xScale = {
7
+ invert: vi.fn().mockReturnValue(50)
8
+ }
9
+ const value = 100
10
+
11
+ const result = invertValue(xScale, value)
12
+
13
+ expect(xScale.invert).toHaveBeenCalledWith(value)
14
+ expect(result).toBe(50)
15
+ })
16
+
17
+ it('should return null if xScale does not have an invert function', () => {
18
+ const xScale = {}
19
+ const value = 100
20
+
21
+ const result = invertValue(xScale, value)
22
+
23
+ expect(result).toBeNull()
24
+ })
25
+
26
+ it('should return null if xScale is null or undefined', () => {
27
+ const value = 100
28
+
29
+ const resultWithNull = invertValue(null, value)
30
+ const resultWithUndefined = invertValue(undefined, value)
31
+
32
+ expect(resultWithNull).toBeNull()
33
+ expect(resultWithUndefined).toBeNull()
34
+ })
35
+ })
@@ -1,4 +1,5 @@
1
1
  import { updateFieldFactory } from '../updateFieldFactory'
2
+ import { expect, describe, it } from 'vitest'
2
3
 
3
4
  describe('updateFieldFactory', () => {
4
5
  it('should update the top level field when section and subsection are null', () => {
@@ -1,7 +1,7 @@
1
1
  import { UpdateFieldFunc } from '../types/UpdateFieldFunc'
2
2
 
3
3
  export const updateFieldFactory =
4
- (config, updateConfig, legacy = false): UpdateFieldFunc<any> =>
4
+ <T>(config, updateConfig, legacy = false): UpdateFieldFunc<T> =>
5
5
  (section, subsection, fieldName, newValue) => {
6
6
  // Top level
7
7
  if (null === section && null === subsection) {
@@ -0,0 +1,19 @@
1
+ interface ColorPalettes {
2
+ [paletteName: string]: string[]
3
+ }
4
+
5
+ export const updatePaletteNames = (colorPalettes: ColorPalettes): ColorPalettes => {
6
+ // This function adds REVERSE keyword to each palette
7
+ delete colorPalettes.qualitative9 // Delete palette before reversing
8
+
9
+ let paletteReversed: ColorPalettes = {}
10
+ for (const [paletteName, hexCodeArr] of Object.entries(colorPalettes)) {
11
+ const paletteStr = String(paletteName)
12
+
13
+ if (!paletteStr.endsWith('reverse')) {
14
+ let palette = paletteStr.concat('reverse') // Add to the end of the string "reverse"
15
+ paletteReversed[palette] = [...hexCodeArr].reverse() // Reverses array elements and create new keys on object
16
+ }
17
+ }
18
+ return { ...paletteReversed, ...colorPalettes }
19
+ }