diff --git a/packages/chart/index.html b/packages/chart/index.html index b5e2f508b..37128876b 100644 --- a/packages/chart/index.html +++ b/packages/chart/index.html @@ -68,12 +68,12 @@ -
+ -
+ diff --git a/packages/chart/src/CdcChart.tsx b/packages/chart/src/CdcChart.tsx index 6231d7165..c9a3802e4 100644 --- a/packages/chart/src/CdcChart.tsx +++ b/packages/chart/src/CdcChart.tsx @@ -1,24 +1,33 @@ -import React, { useState, useEffect, useCallback, useRef, useId, useMemo } from 'react' +import React, { useState, useEffect, useCallback, useRef, useId } from 'react' // IE11 import ResizeObserver from 'resize-observer-polyfill' import 'whatwg-fetch' -import * as d3 from 'd3-array' +// Core components import Layout from '@cdc/core/components/Layout' -import Button from '@cdc/core/components/elements/Button' - +import Confirm from '@cdc/core/components/elements/Confirm' +import Error from '@cdc/core/components/elements/Error' +import SkipTo from '@cdc/core/components/elements/SkipTo' +import Title from '@cdc/core/components/ui/Title' +import DataTable from '@cdc/core/components/DataTable' +// Local Components +import LegendWrapper from './components/LegendWrapper' //types import { DimensionsType } from '@cdc/core/types/Dimensions' import { type DashboardConfig } from '@cdc/dashboard/src/types/DashboardConfig' - +import type { TableConfig } from '@cdc/core/components/DataTable/types/TableConfig' +import { AllChartsConfig, ChartConfig } from './types/ChartConfig' +import { type ViewportSize } from './types/ChartConfig' +import { Pivot } from '@cdc/core/types/Table' +import { Runtime } from '@cdc/core/types/Runtime' +import { Label } from './types/Label' // External Libraries -import { scaleOrdinal } from '@visx/scale' import ParentSize from '@visx/responsive/lib/components/ParentSize' import { timeParse, timeFormat } from 'd3-time-format' import Papa from 'papaparse' import parse from 'html-react-parser' import 'react-tooltip/dist/react-tooltip.css' - +import _ from 'lodash' // Primary Components import ConfigContext from './ConfigContext' import PieChart from './components/PieChart' @@ -34,7 +43,7 @@ import defaults from './data/initial-state' import EditorPanel from './components/EditorPanel' import { abbreviateNumber } from './helpers/abbreviateNumber' import { handleChartTabbing } from './helpers/handleChartTabbing' -import { getQuartiles } from './helpers/getQuartiles' + import { handleChartAriaLabels } from './helpers/handleChartAriaLabels' import { lineOptions } from './helpers/lineOptions' import { handleLineType } from './helpers/handleLineType' @@ -44,36 +53,29 @@ import Loading from '@cdc/core/components/Loading' import Filters from '@cdc/core/components/Filters' import MediaControls from '@cdc/core/components/MediaControls' import Annotation from './components/Annotations' - -// Helpers +// Core Helpers +import { DataTransform } from '@cdc/core/helpers/DataTransform' +import { isLegendWrapViewport } from '@cdc/core/helpers/viewports' +import { missingRequiredSections } from '@cdc/core/helpers/missingRequiredSections' +import { filterVizData } from '@cdc/core/helpers/filterVizData' +import { getFileExtension } from '@cdc/core/helpers/getFileExtension' +import { addValuesToFilters } from '@cdc/core/helpers/addValuesToFilters' import { publish, subscribe, unsubscribe } from '@cdc/core/helpers/events' +import { isSolrCsv, isSolrJson } from '@cdc/core/helpers/isSolr' import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses' import numberFromString from '@cdc/core/helpers/numberFromString' import getViewport from '@cdc/core/helpers/getViewport' -import { DataTransform } from '@cdc/core/helpers/DataTransform' import cacheBustingString from '@cdc/core/helpers/cacheBustingString' import isNumber from '@cdc/core/helpers/isNumber' import coveUpdateWorker from '@cdc/core/helpers/coveUpdateWorker' +// Local helpers import { isConvertLineToBarGraph } from './helpers/isConvertLineToBarGraph' -import { isLegendWrapViewport } from '@cdc/core/helpers/viewports' - +import { getBoxPlotConfig } from './helpers/getBoxPlotConfig' +import { getComboChartConfig } from './helpers/getComboChartConfig' +import { getExcludedData } from './helpers/getExcludedData' +import { getColorScale } from './helpers/getColorScale' +// styles import './scss/main.scss' -// load both then config below determines which to use -import DataTable from '@cdc/core/components/DataTable' -import type { TableConfig } from '@cdc/core/components/DataTable/types/TableConfig' -import { getFileExtension } from '@cdc/core/helpers/getFileExtension' -import Title from '@cdc/core/components/ui/Title' -import { AllChartsConfig, ChartConfig } from './types/ChartConfig' -import { Label } from './types/Label' -import { type ViewportSize } from './types/ChartConfig' -import { isSolrCsv, isSolrJson } from '@cdc/core/helpers/isSolr' -import SkipTo from '@cdc/core/components/elements/SkipTo' -import { filterVizData } from '@cdc/core/helpers/filterVizData' -import LegendWrapper from './components/LegendWrapper' -import _ from 'lodash' -import { addValuesToFilters } from '@cdc/core/helpers/addValuesToFilters' -import { Runtime } from '@cdc/core/types/Runtime' -import { Pivot } from '@cdc/core/types/Table' interface CdcChartProps { configUrl?: string @@ -97,7 +99,6 @@ const CdcChart = ({ isDashboard = false, setConfig: setParentConfig, setEditing, - hostname, link, setSharedFilter, setSharedFilterValue, @@ -159,6 +160,33 @@ const CdcChart = ({ return isConvertLineToBarGraph(config.visualizationType, filteredData, config.allowLineToBarGraph) } + const loadConfig = async (configObj: ChartConfig, configUrl: string): Promise => { + const response = _.cloneDeep(configObj) || (await (await fetch(configUrl)).json()) + + return response + } + + const prepareConfig = (loadedConfig: ChartConfig, data): ChartConfig => { + let newConfig = _.defaultsDeep(loadedConfig, defaults) + _.defaultsDeep(newConfig, { + table: { showVertical: false } + }) + + _.set(newConfig, 'table.show', _.get(newConfig, 'table.show', !isDashboard)) + + _.forEach(newConfig.series, series => { + _.defaults(series, { + tooltip: true, + axis: 'Left' + }) + }) + + if (data) { + newConfig.data = data + } + return { ...coveUpdateWorker(newConfig) } + } + const reloadURLData = async () => { if (config.dataUrl) { const dataUrl = new URL(config.runtimeDataUrl || config.dataUrl, window.location.origin) @@ -227,10 +255,7 @@ const CdcChart = ({ } } - const loadConfig = async () => { - const response = _.cloneDeep(configObj) || (await (await fetch(configUrl)).json()) - - // If data is included through a URL, fetch that and store + const loadData = async response => { let data: any[] = response.data || [] const urlFilters = response.filters @@ -280,37 +305,7 @@ const CdcChart = ({ } data = handleRankByValue(data, response) - - if (data) { - setStateData(data) - setExcludedData(data) - } - - // force showVertical for data tables false if it does not exist - if (response !== undefined && response.table !== undefined) { - if (!response.table || !response.table.showVertical) { - response.table = response.table || {} - response.table.showVertical = false - } - } - let newConfig = { ...defaults, ...response } - - if (undefined === newConfig.table.show) newConfig.table.show = !isDashboard - - newConfig.series.forEach(series => { - if (series.tooltip === undefined || series.tooltip === null) { - series.tooltip = true - } - if (!series.axis) series.axis = 'Left' - }) - - if (data) { - newConfig.data = data - } - - const processedConfig = { ...coveUpdateWorker(newConfig) } - - updateConfig(processedConfig, data) + return data } const updateConfig = (_config: AllChartsConfig, dataOverride?: any[]) => { @@ -326,40 +321,7 @@ const CdcChart = ({ } }) - let newExcludedData: any[] = [] - - if (newConfig.exclusions && newConfig.exclusions.active) { - if (newConfig.xAxis.type === 'categorical' && newConfig.exclusions.keys?.length > 0) { - newExcludedData = data.filter(e => !newConfig.exclusions.keys.includes(e[newConfig.xAxis.dataKey])) - } else if ( - isDateScale(newConfig.xAxis) && - (newConfig.exclusions.dateStart || newConfig.exclusions.dateEnd) && - newConfig.xAxis.dateParseFormat - ) { - // Filter dates - const timestamp = e => new Date(e).getTime() - - let startDate = timestamp(newConfig.exclusions.dateStart) - let endDate = timestamp(newConfig.exclusions.dateEnd) + 86399999 //Increase by 24h in ms (86400000ms - 1ms) to include selected end date for .getTime() comparative - - let startDateValid = undefined !== typeof startDate && false === isNaN(startDate) - let endDateValid = undefined !== typeof endDate && false === isNaN(endDate) - - if (startDateValid && endDateValid) { - newExcludedData = data.filter( - e => timestamp(e[newConfig.xAxis.dataKey]) >= startDate && timestamp(e[newConfig.xAxis.dataKey]) <= endDate - ) - } else if (startDateValid) { - newExcludedData = data.filter(e => timestamp(e[newConfig.xAxis.dataKey]) >= startDate) - } else if (endDateValid) { - newExcludedData = data.filter(e => timestamp(e[newConfig.xAxis.dataKey]) <= endDate) - } - } else { - newExcludedData = dataOverride || stateData - } - } else { - newExcludedData = dataOverride || stateData - } + const newExcludedData: any[] = getExcludedData(newConfig, dataOverride || stateData) setExcludedData(newExcludedData) @@ -438,102 +400,12 @@ const CdcChart = ({ } if (newConfig.visualizationType === 'Box Plot' && newConfig.series) { - const combinedData = filteredData || data - const groups = _.uniq(_.map(combinedData, newConfig.xAxis.dataKey)) - const seriesKeys = _.map(newConfig.series, 'dataKey') - const plots: any[] = [] - - groups.forEach(g => { - seriesKeys.forEach(seriesKey => { - try { - if (!g) throw new Error('No groups resolved in box plots') - - // Start handle operations on combinedData - const { count, sortedData } = _.chain(combinedData) - // Filter by xAxis data key - .filter(item => item[newConfig.xAxis.dataKey] === g) - // perform multiple operations on the filtered data - .thru(filteredData => ({ - count: filteredData.length, - sortedData: _.map(filteredData, item => Number(item[seriesKey])).sort() - })) - // get the results from the chain - .value() - - // ! - Notice d3.quantile doesn't work here, and we had to take a custom route. - const quartiles = getQuartiles(sortedData) - - if (!sortedData) throw new Error('boxplots dont have data yet') - if (!plots) throw new Error('boxplots dont have plots yet') - - const q1 = quartiles.q1 - const q3 = quartiles.q3 - - const iqr = q3 - q1 - const lowerBounds = q1 - 1.5 * iqr - const upperBounds = q3 + 1.5 * iqr - const filteredData = sortedData.filter(d => d <= upperBounds) - const max = d3.max(filteredData) - plots.push({ - columnCategory: g, - columnMax: max, - columnThirdQuartile: _.round(q3, newConfig.dataFormat.roundTo), - columnMedian: Number(d3.median(sortedData)).toFixed(newConfig.dataFormat.roundTo), - columnFirstQuartile: _.round(q1, newConfig.dataFormat.roundTo), - columnMin: _.min(sortedData), - columnCount: count, - columnSd: Number(d3.deviation(sortedData)).toFixed(newConfig.dataFormat.roundTo), - columnMean: Number(d3.mean(sortedData)).toFixed(newConfig.dataFormat.roundTo), - columnIqr: _.round(iqr, newConfig.dataFormat.roundTo), - values: sortedData, - columnLowerBounds: lowerBounds, - columnUpperBounds: upperBounds, - columnOutliers: _.filter(sortedData, value => value < lowerBounds || value > upperBounds), - columnNonOutliers: _.filter(sortedData, value => value >= lowerBounds && value <= upperBounds) - }) - } catch (e) { - console.error('COVE: ', e.message) // eslint-disable-line - } - }) - }) - // Generate a flat list of categories based on seriesKeys and groups - const categories = - seriesKeys.length > 1 - ? _.flatMap(groups, value => _.map(seriesKeys, key => `${_.capitalize(key)} - ${_.capitalize(value)}`)) - : groups - + const [plots, categories] = getBoxPlotConfig(newConfig, stateData) newConfig.boxplot['categories'] = categories newConfig.boxplot.plots = plots } - if (newConfig.visualizationType === 'Combo' && newConfig.series) { - newConfig.runtime.barSeriesKeys = [] - newConfig.runtime.lineSeriesKeys = [] - newConfig.runtime.areaSeriesKeys = [] - newConfig.runtime.forecastingSeriesKeys = [] - - newConfig.series.forEach(series => { - if (series.type === 'Area Chart') { - newConfig.runtime.areaSeriesKeys.push(series) - } - if (series.type === 'Forecasting') { - newConfig.runtime.forecastingSeriesKeys.push(series) - } - if (series.type === 'Bar' || series.type === 'Combo') { - newConfig.runtime.barSeriesKeys.push(series.dataKey) - } - if ( - series.type === 'Line' || - series.type === 'dashed-sm' || - series.type === 'dashed-md' || - series.type === 'dashed-lg' - ) { - newConfig.runtime.lineSeriesKeys.push(series.dataKey) - } - if (series.type === 'Combo') { - series.type = 'Bar' - } - }) + newConfig.runtime = getComboChartConfig(newConfig) } if (newConfig.visualizationType === 'Forecasting' && newConfig.series) { @@ -645,14 +517,25 @@ const CdcChart = ({ setContainer(node) }, []) // eslint-disable-line - const isEmpty = obj => { - return Object.keys(obj).length === 0 - } - // Load data when component first mounts useEffect(() => { - loadConfig() - }, [configObj?.data?.length ? configObj.data : null]) // eslint-disable-line + const load = async () => { + try { + const loadedConfig = await loadConfig(configObj, configUrl) + const data = await loadData(loadedConfig) + if (data && loadedConfig) { + const preparedConfig = await prepareConfig(loadedConfig, data) + setStateData(data) + setExcludedData(data) + updateConfig(preparedConfig, data) + } + } catch (err) { + console.error('Could not Load!') + } + } + + load() + }, []) useEffect(() => { reloadURLData() @@ -662,7 +545,7 @@ const CdcChart = ({ * When cove has a config and container ref publish the cove_loaded event. */ useEffect(() => { - if (container && !isEmpty(config) && !coveLoadedEventRan) { + if (container && !_.isEmpty(config) && !coveLoadedEventRan) { publish('cove_loaded', { config: config }) setCoveLoadedEventRan(true) } @@ -732,26 +615,7 @@ const CdcChart = ({ // Generates color palette to pass to child chart component useEffect(() => { if (stateData && config.xAxis && config.runtime?.seriesKeys) { - const configPalette = ['Paired Bar', 'Deviation Bar'].includes(config.visualizationType) - ? config.twoColor.palette - : config.palette - const allPalettes: Record = { ...colorPalettes, ...twoColorPalette } - let palette = config.customColors || allPalettes[configPalette] - let numberOfKeys = config.runtime.seriesKeys.length - let newColorScale - - while (numberOfKeys > palette.length) { - palette = palette.concat(palette) - } - - palette = palette.slice(0, numberOfKeys) - - newColorScale = () => - scaleOrdinal({ - domain: config.runtime.seriesLabelsAll, - range: palette, - unknown: null - }) + const newColorScale = getColorScale(config) setColorScale(newColorScale) setLoading(false) @@ -1026,157 +890,6 @@ const CdcChart = ({ return String(result) } - const missingRequiredSections = () => { - if (config.visualizationType === 'Sankey') return false // skip checks for now - if (config.visualizationType === 'Forecasting') return false // skip required checks for now. - if (config.visualizationType === 'Forest Plot') return false // skip required checks for now. - if (config.visualizationType === 'Pie') { - if (undefined === config?.yAxis.dataKey) { - return true - } - } else { - if ((undefined === config?.series || false === config?.series.length > 0) && !config?.dynamicSeries) { - return true - } - } - - if (!config.xAxis.dataKey) { - return true - } - - return false - } - - // used for Additional Column - const displayDataAsText = (value, columnName) => { - if (value === null || value === '' || value === undefined) { - return '' - } - - if (typeof value === 'string' && value.length > 0 && config.legend.type === 'equalnumber') { - return value - } - - let formattedValue = value - - let columnObj //= config.columns[columnName] - // config.columns not an array but a hash of objects - if (Object.keys(config.columns).length > 0) { - Object.keys(config.columns).forEach(function (key) { - var column = config.columns[key] - // add if not the index AND it is enabled to be added to data table - if (column.name === columnName) { - columnObj = column - } - }) - } - - if (columnObj === undefined) { - // then use left axis config - columnObj = config.type === 'chart' ? config.dataFormat : config.primary - // NOTE: Left Value Axis uses different names - // so map them below so the code below works - // - copy commas to useCommas to work below - columnObj['useCommas'] = columnObj.commas - // - copy roundTo to roundToPlace to work below - columnObj['roundToPlace'] = columnObj.roundTo ? columnObj.roundTo : '' - } - - if (columnObj) { - // If value is a number, apply specific formattings - let hasDecimal = false - let decimalPoint = 0 - if (Number(value)) { - if (columnObj.roundToPlace >= 0) { - hasDecimal = columnObj.roundToPlace ? columnObj.roundToPlace !== '' || columnObj.roundToPlace !== null : false - decimalPoint = columnObj.roundToPlace ? Number(columnObj.roundToPlace) : 0 - - // Rounding - if (columnObj.hasOwnProperty('roundToPlace') && hasDecimal) { - formattedValue = Number(value).toFixed(decimalPoint) - } - } - - if (columnObj.hasOwnProperty('useCommas') && columnObj.useCommas === true) { - // Formats number to string with commas - allows up to 5 decimal places, if rounding is not defined. - // Otherwise, uses the rounding value set at 'columnObj.roundToPlace'. - formattedValue = Number(value).toLocaleString('en-US', { - style: 'decimal', - minimumFractionDigits: hasDecimal ? decimalPoint : 0, - maximumFractionDigits: hasDecimal ? decimalPoint : 5 - }) - } - } - - // add prefix and suffix if set - formattedValue = (columnObj.prefix || '') + formattedValue + (columnObj.suffix || '') - } - - return formattedValue - } - - const Confirm = () => { - const confirmDone = e => { - if (e) { - e.preventDefault() - } - - let newConfig = { ...config } - delete newConfig.newViz - - updateConfig(newConfig) - } - - const styles = { - position: 'relative', - height: '100vh', - width: '100%', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - gridArea: 'content' - } - - return ( -
-
-

Finish Configuring

-

Set all required options to the left and confirm below to display a preview of the chart.

- -
-
- ) - } - - const Error = () => { - const styles = { - position: 'absolute', - background: 'white', - zIndex: '999', - height: '100vh', - width: '100%', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - gridArea: 'content' - } - return ( -
-
-

Error With Configuration

-

{config.runtime.editorErrorMessage}

-
-
- ) - } - // this is passed DOWN into the various components // then they do a lookup based on the bin number as index into here (TT) const applyLegendToRow = rowObj => { @@ -1231,10 +944,10 @@ const CdcChart = ({ let body = const makeClassName = string => { - if (!string || !string.toLowerCase) return - return string.toLowerCase().replaceAll(/ /g, '-') - } + if (!_.isString(string)) return undefined + return _.kebabCase(string) + } const getChartWrapperClasses = () => { const isLegendOnBottom = legend?.position === 'bottom' || isLegendWrapViewport(currentViewport) const classes = ['chart-container', 'p-relative'] @@ -1270,13 +983,16 @@ const CdcChart = ({ {config.dataKey} (Go to Table) ) + body = ( <> {isEditor && } - {config.newViz && } - {undefined === config.newViz && isEditor && config.runtime && config.runtime?.editorErrorMessage && } - {!missingRequiredSections() && !config.newViz && ( + {config.newViz && } + {undefined === config.newViz && isEditor && config.runtime && config.runtime?.editorErrorMessage && ( + + )} + {!missingRequiredSections(config) && !config.newViz && (
@@ -1456,7 +1172,6 @@ const CdcChart = ({ expandDataTable={config.table.expanded} columns={config.columns} defaultSortBy={dataTableDefaultSortBy} - displayDataAsText={displayDataAsText} displayGeoName={name => name} applyLegendToRow={applyLegendToRow} tableTitle={config.table.label} diff --git a/packages/chart/src/components/Annotations/components/AnnotationDraggable.tsx b/packages/chart/src/components/Annotations/components/AnnotationDraggable.tsx index 48a7f7fcf..0899992c6 100644 --- a/packages/chart/src/components/Annotations/components/AnnotationDraggable.tsx +++ b/packages/chart/src/components/Annotations/components/AnnotationDraggable.tsx @@ -16,8 +16,6 @@ import { handleTextY } from './helpers' -import useColorScale from '../../../hooks/useColorScale' - // visx import { HtmlLabel, CircleSubject, EditableAnnotation, Connector, Annotation as VisxAnnotation } from '@visx/annotation' import { Drag } from '@visx/drag' @@ -29,17 +27,12 @@ import './AnnotationDraggable.styles.css' const Annotations = ({ xScale, yScale, xScaleAnnotation, xMax, svgRef, onDragStateChange }) => { // prettier-ignore - const { - config, - dimensions, - isEditor, - updateConfig - } = useContext(ConfigContext) + const { config, dimensions, isEditor, updateConfig, colorScale } = useContext(ConfigContext) // destructure config items here... const { annotations } = config const [height] = dimensions - const { colorScale } = useColorScale() + const AnnotationComponent = isEditor ? EditableAnnotation : VisxAnnotation return ( diff --git a/packages/chart/src/components/Annotations/components/AnnotationDropdown.tsx b/packages/chart/src/components/Annotations/components/AnnotationDropdown.tsx index b49f3d48e..2a2ab18fb 100644 --- a/packages/chart/src/components/Annotations/components/AnnotationDropdown.tsx +++ b/packages/chart/src/components/Annotations/components/AnnotationDropdown.tsx @@ -21,7 +21,7 @@ const AnnotationDropdown = () => { } const handleAccordionClassName = () => { - const classNames = ['data-table-heading', 'annotation__dropdown-list'] + const classNames = ['data-table-heading', 'annotation__dropdown-list', 'p-3'] if (!expanded) { classNames.push('collapsed') } diff --git a/packages/chart/src/components/EditorPanel/EditorPanel.tsx b/packages/chart/src/components/EditorPanel/EditorPanel.tsx index 844d08732..f99124418 100644 --- a/packages/chart/src/components/EditorPanel/EditorPanel.tsx +++ b/packages/chart/src/components/EditorPanel/EditorPanel.tsx @@ -963,7 +963,7 @@ const EditorPanel = () => { const convertStateToConfig = () => { let strippedState = JSON.parse(JSON.stringify(config)) - if (false === missingRequiredSections()) { + if (false === missingRequiredSections(config)) { delete strippedState.newViz } delete strippedState.runtime diff --git a/packages/chart/src/components/LinearChart.tsx b/packages/chart/src/components/LinearChart.tsx index 93903b9bd..c7ead46be 100644 --- a/packages/chart/src/components/LinearChart.tsx +++ b/packages/chart/src/components/LinearChart.tsx @@ -36,7 +36,7 @@ import useMinMax from '../hooks/useMinMax' import useReduceData from '../hooks/useReduceData' import useRightAxis from '../hooks/useRightAxis' import useScales, { getTickValues, filterAndShiftLinearDateTicks } from '../hooks/useScales' -import useTopAxis from '../hooks/useTopAxis' +import getTopAxis from '../helpers/getTopAxis' import { useTooltip as useCoveTooltip } from '../hooks/useTooltip' import { useEditorPermissions } from './EditorPanel/useEditorPermissions' import Annotation from './Annotations' @@ -103,7 +103,7 @@ const LinearChart = forwardRef(({ parentHeight, p // HOOKS % STATES const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(config, data) const { visSupportsReactTooltip } = useEditorPermissions() - const { hasTopAxis } = useTopAxis(config) + const { hasTopAxis } = getTopAxis(config) const [animatedChart, setAnimatedChart] = useState(false) const [point, setPoint] = useState({ x: 0, y: 0 }) const [suffixWidth, setSuffixWidth] = useState(0) diff --git a/packages/chart/src/helpers/getBoxPlotConfig.ts b/packages/chart/src/helpers/getBoxPlotConfig.ts new file mode 100644 index 000000000..47ec4c6f0 --- /dev/null +++ b/packages/chart/src/helpers/getBoxPlotConfig.ts @@ -0,0 +1,73 @@ +import _ from 'lodash' +import { ChartConfig } from '../types/ChartConfig' +import { getQuartiles } from './getQuartiles' +import * as d3 from 'd3-array' + +export const getBoxPlotConfig = (newConfig: ChartConfig, data: object[]) => { + const combinedData = data + const groups = _.uniq(_.map(combinedData, newConfig.xAxis.dataKey)) + const seriesKeys = _.map(newConfig.series, 'dataKey') + const plots: any[] = [] + + groups.forEach(g => { + seriesKeys.forEach(seriesKey => { + try { + if (!g) throw new Error('No groups resolved in box plots') + + // Start handle operations on combinedData + const { count, sortedData } = _.chain(combinedData) + // Filter by xAxis data key + .filter(item => item[newConfig.xAxis.dataKey] === g) + // perform multiple operations on the filtered data + .thru(filteredData => ({ + count: filteredData.length, + sortedData: _.map(filteredData, item => Number(item[seriesKey])).sort() + })) + // get the results from the chain + .value() + + // ! - Notice d3.quantile doesn't work here, and we had to take a custom route. + const quartiles = getQuartiles(sortedData) + + if (!sortedData) throw new Error('boxplots dont have data yet') + if (!plots) throw new Error('boxplots dont have plots yet') + + const q1 = quartiles.q1 + const q3 = quartiles.q3 + + const iqr = q3 - q1 + const lowerBounds = q1 - 1.5 * iqr + const upperBounds = q3 + 1.5 * iqr + const filteredData = sortedData.filter(d => d <= upperBounds) + const max = d3.max(filteredData) + plots.push({ + columnCategory: g, + columnMax: max, + columnThirdQuartile: _.round(q3, newConfig.dataFormat.roundTo), + columnMedian: Number(d3.median(sortedData)).toFixed(newConfig.dataFormat.roundTo), + columnFirstQuartile: _.round(q1, newConfig.dataFormat.roundTo), + columnMin: _.min(sortedData), + columnCount: count, + columnSd: Number(d3.deviation(sortedData)).toFixed(newConfig.dataFormat.roundTo), + columnMean: Number(d3.mean(sortedData)).toFixed(newConfig.dataFormat.roundTo), + columnIqr: _.round(iqr, newConfig.dataFormat.roundTo), + values: sortedData, + columnLowerBounds: lowerBounds, + columnUpperBounds: upperBounds, + columnOutliers: _.filter(sortedData, value => value < lowerBounds || value > upperBounds), + columnNonOutliers: _.filter(sortedData, value => value >= lowerBounds && value <= upperBounds) + }) + } catch (e) { + console.error('COVE: ', e.message) // eslint-disable-line + } + }) + }) + + // Generate a flat list of categories based on seriesKeys and groups + const categories = + seriesKeys.length > 1 + ? _.flatMap(groups, value => _.map(seriesKeys, key => `${_.capitalize(key)} - ${_.capitalize(value)}`)) + : groups + + return [plots, categories] +} diff --git a/packages/chart/src/helpers/getColorScale.ts b/packages/chart/src/helpers/getColorScale.ts new file mode 100644 index 000000000..f3d773c2d --- /dev/null +++ b/packages/chart/src/helpers/getColorScale.ts @@ -0,0 +1,28 @@ +import { colorPalettesChart as colorPalettes, twoColorPalette } from '@cdc/core/data/colorPalettes' +import { scaleOrdinal } from '@visx/scale' +import { ChartConfig } from '../types/ChartConfig' + +export const getColorScale = (config: ChartConfig): ((value: string) => string) => { + const configPalette = ['Paired Bar', 'Deviation Bar'].includes(config.visualizationType) + ? config.twoColor.palette + : config.palette + const allPalettes: Record = { ...colorPalettes, ...twoColorPalette } + let palette = config.customColors || allPalettes[configPalette] + let numberOfKeys = config.runtime.seriesKeys.length + let newColorScale + + while (numberOfKeys > palette.length) { + palette = palette.concat(palette) + } + + palette = palette.slice(0, numberOfKeys) + + newColorScale = () => + scaleOrdinal({ + domain: config.runtime.seriesLabelsAll, + range: palette, + unknown: null + }) + + return newColorScale +} diff --git a/packages/chart/src/helpers/getComboChartConfig.ts b/packages/chart/src/helpers/getComboChartConfig.ts new file mode 100644 index 000000000..a4de7fbad --- /dev/null +++ b/packages/chart/src/helpers/getComboChartConfig.ts @@ -0,0 +1,42 @@ +import _ from 'lodash' +import { ChartConfig } from '../types/ChartConfig' +import * as d3 from 'd3-array' + +export const getComboChartConfig = (newConfig: ChartConfig) => { + if (newConfig.visualizationType !== 'Combo' || !newConfig.series) return + + const runtimeKeys = { + barSeriesKeys: [], + lineSeriesKeys: [], + areaSeriesKeys: [], + forecastingSeriesKeys: [] + } + + // Define a mapping of series types to runtime keys + const seriesTypeMap = new Map([ + ['Area Chart', 'areaSeriesKeys'], + ['Forecasting', 'forecastingSeriesKeys'], + ['Bar', 'barSeriesKeys'], + ['Combo', 'barSeriesKeys'], + ['Line', 'lineSeriesKeys'], + ['dashed-sm', 'lineSeriesKeys'], + ['dashed-md', 'lineSeriesKeys'], + ['dashed-lg', 'lineSeriesKeys'] + ]) + + newConfig.series.forEach(series => { + const runtimeKey = seriesTypeMap.get(series.type) + if (runtimeKey) { + const valueToPush = runtimeKey === 'barSeriesKeys' || runtimeKey === 'lineSeriesKeys' ? series.dataKey : series + runtimeKeys[runtimeKey].push(valueToPush) + } + + // Change Combo series type to Bar + if (series.type === 'Combo') { + series.type = 'Bar' + } + }) + + // Assign the processed runtime keys to the configuration + return { ...newConfig.runtime, ...runtimeKeys } +} diff --git a/packages/chart/src/helpers/getExcludedData.ts b/packages/chart/src/helpers/getExcludedData.ts new file mode 100644 index 000000000..843f4b315 --- /dev/null +++ b/packages/chart/src/helpers/getExcludedData.ts @@ -0,0 +1,37 @@ +import { isDateScale } from '@cdc/core/helpers/cove/date' +import _ from 'lodash' +import { ChartConfig } from '../types/ChartConfig' +export const getExcludedData = (newConfig: ChartConfig, data: object[]) => { + let newExcludedData = data + if (newConfig.exclusions && newConfig.exclusions.active) { + if (newConfig.xAxis.type === 'categorical' && newConfig.exclusions.keys?.length > 0) { + newExcludedData = data.filter(e => !newConfig.exclusions.keys.includes(e[newConfig.xAxis.dataKey])) + } else if ( + isDateScale(newConfig.xAxis) && + (newConfig.exclusions.dateStart || newConfig.exclusions.dateEnd) && + newConfig.xAxis.dateParseFormat + ) { + // Filter dates + const timestamp = e => new Date(e).getTime() + + let startDate = timestamp(newConfig.exclusions.dateStart) + let endDate = timestamp(newConfig.exclusions.dateEnd) + 86399999 //Increase by 24h in ms (86400000ms - 1ms) to include selected end date for .getTime() comparative + + let startDateValid = undefined !== typeof startDate && false === isNaN(startDate) + let endDateValid = undefined !== typeof endDate && false === isNaN(endDate) + + if (startDateValid && endDateValid) { + newExcludedData = data.filter( + e => timestamp(e[newConfig.xAxis.dataKey]) >= startDate && timestamp(e[newConfig.xAxis.dataKey]) <= endDate + ) + } else if (startDateValid) { + newExcludedData = data.filter(e => timestamp(e[newConfig.xAxis.dataKey]) >= startDate) + } else if (endDateValid) { + newExcludedData = data.filter(e => timestamp(e[newConfig.xAxis.dataKey]) <= endDate) + } + } else { + newExcludedData = data + } + } + return newExcludedData +} diff --git a/packages/chart/src/helpers/getTopAxis.ts b/packages/chart/src/helpers/getTopAxis.ts new file mode 100644 index 000000000..c430036fd --- /dev/null +++ b/packages/chart/src/helpers/getTopAxis.ts @@ -0,0 +1,7 @@ +export default function getTopAxis(config) { + // When to show top axis + const hasTopAxis = + config.visualizationType === 'Bar' || config.visualizationType === 'Combo' || config.visualizationType === 'Line' + + return { hasTopAxis } +} diff --git a/packages/chart/src/hooks/useColorScale.ts b/packages/chart/src/hooks/useColorScale.ts deleted file mode 100644 index e02fc7448..000000000 --- a/packages/chart/src/hooks/useColorScale.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { colorPalettesChart as colorPalettes, twoColorPalette } from '@cdc/core/data/colorPalettes' -import { scaleOrdinal } from '@visx/scale' -import { useContext } from 'react' -import ConfigContext from '../ConfigContext' - -const useColorScale = () => { - const { config, data } = useContext(ConfigContext) - const { visualizationSubType, visualizationType, series, legend } = config - - const generatePalette = colorsCount => { - if (!series?.length) return [] - const isSpecialType = ['Paired Bar', 'Deviation Bar'].includes(visualizationType) - const chosenPalette = isSpecialType ? config.twoColor.palette : config.palette - const allPalettes = { ...colorPalettes, ...twoColorPalette } - let palette = config.customColors || allPalettes[chosenPalette] - while (colorsCount > palette.length) palette = palette.concat(palette) - return palette.slice(0, colorsCount) - } - - let colorScale = scaleOrdinal({ - domain: config?.runtime?.seriesLabelsAll, - range: generatePalette(series.length) - }) - - if (visualizationType === 'Deviation Bar') { - const { targetLabel } = config.xAxis - colorScale = scaleOrdinal({ - domain: [`Below ${targetLabel}`, `Above ${targetLabel}`], - range: generatePalette(2) - }) - } - if (visualizationType === 'Bar' && visualizationSubType === 'regular' && series?.length === 1 && legend?.colorCode) { - const set = new Set(data?.map(d => d[legend.colorCode])) - colorScale = scaleOrdinal({ - domain: [...set], - range: generatePalette([...set].length) - }) - } - if (config.series.some(s => s.name)) { - const set = new Set(series.map(d => d.name || d.dataKey)) - colorScale = colorScale = scaleOrdinal({ - domain: [...set], - range: generatePalette(series.length) - }) - } - - return { colorScale } -} - -export default useColorScale diff --git a/packages/chart/src/hooks/useHighlightedBars.js b/packages/chart/src/hooks/useHighlightedBars.ts similarity index 97% rename from packages/chart/src/hooks/useHighlightedBars.js rename to packages/chart/src/hooks/useHighlightedBars.ts index 21660c50d..1e763ef73 100644 --- a/packages/chart/src/hooks/useHighlightedBars.js +++ b/packages/chart/src/hooks/useHighlightedBars.ts @@ -1,7 +1,8 @@ import React, { useContext } from 'react' import ConfigContext from '../ConfigContext' +import { ChartConfig } from '../types/ChartConfig' -export const useHighlightedBars = (config, updateConfig) => { +export const useHighlightedBars = (config: ChartConfig, updateConfig: (config) => void) => { const { formatDate, parseDate } = useContext(ConfigContext) let highlightedSeries = [] // only allow single series for highlights diff --git a/packages/chart/src/hooks/useIntersectionObserver.jsx b/packages/chart/src/hooks/useIntersectionObserver.jsx deleted file mode 100644 index d9ae3a307..000000000 --- a/packages/chart/src/hooks/useIntersectionObserver.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useEffect, useState } from 'react' - -export default function useIntersectionObserver(elementRef, { threshold = 0, root = null, rootMargin = '0%', freezeOnceVisible = false }) { - const [entry, setEntry] = useState() - - const frozen = entry?.isIntersecting && freezeOnceVisible - - const updateEntry = ([entry]) => { - setEntry(entry) - } - - useEffect(() => { - setTimeout(() => { - const node = elementRef?.current - const hasIOSupport = !!window.IntersectionObserver - - if (!hasIOSupport || frozen || !node) return - - const observerParams = { threshold, root, rootMargin } - const observer = new IntersectionObserver(updateEntry, observerParams) - - observer.observe(node) - - return () => observer.disconnect() - }, 500) - }, [elementRef, threshold, root, rootMargin, frozen]) - - return entry -} diff --git a/packages/chart/src/hooks/useIntersectionObserver.ts b/packages/chart/src/hooks/useIntersectionObserver.ts new file mode 100644 index 000000000..c84eea926 --- /dev/null +++ b/packages/chart/src/hooks/useIntersectionObserver.ts @@ -0,0 +1,37 @@ +import { useEffect, useState, MutableRefObject } from 'react' + +interface IntersectionObserverOptions { + threshold?: number | number[] + root?: Element | null + rootMargin?: string + freezeOnceVisible?: boolean +} + +export default function useIntersectionObserver( + elementRef: MutableRefObject, + { threshold = 0, root = null, rootMargin = '0%', freezeOnceVisible = false }: IntersectionObserverOptions +) { + const [entry, setEntry] = useState() + + const frozen = entry?.isIntersecting && freezeOnceVisible + + const updateEntry = ([entry]: IntersectionObserverEntry[]) => { + setEntry(entry) + } + + useEffect(() => { + const node = elementRef?.current + const hasIOSupport = !!window.IntersectionObserver + + if (!hasIOSupport || frozen || !node) return + + const observerParams = { threshold, root, rootMargin } + const observer = new IntersectionObserver(updateEntry, observerParams) + + observer.observe(node) + + return () => observer.disconnect() + }, [elementRef, threshold, root, rootMargin, frozen]) + + return entry +} diff --git a/packages/chart/src/hooks/useTopAxis.js b/packages/chart/src/hooks/useTopAxis.js deleted file mode 100644 index 42fa796c3..000000000 --- a/packages/chart/src/hooks/useTopAxis.js +++ /dev/null @@ -1,6 +0,0 @@ -export default function useTopAxis(config) { - // When to show top axis - const hasTopAxis = config.visualizationType === 'Bar' || config.visualizationType === 'Combo' || config.visualizationType === 'Line' - - return { hasTopAxis } -} diff --git a/packages/core/components/DataTable/DataTable.tsx b/packages/core/components/DataTable/DataTable.tsx index 1ccd087e0..69c1b5388 100644 --- a/packages/core/components/DataTable/DataTable.tsx +++ b/packages/core/components/DataTable/DataTable.tsx @@ -31,7 +31,6 @@ export type DataTableProps = { config: TableConfig dataConfig?: Object defaultSortBy?: string - displayDataAsText?: Function displayGeoName?: Function expandDataTable: boolean formatLegendLocation?: Function diff --git a/packages/core/components/Layout/components/Visualization/visualizations.scss b/packages/core/components/Layout/components/Visualization/visualizations.scss index 9dfeef319..2543d6e65 100644 --- a/packages/core/components/Layout/components/Visualization/visualizations.scss +++ b/packages/core/components/Layout/components/Visualization/visualizations.scss @@ -1,6 +1,6 @@ .cdc-open-viz-module { .cdc-chart-inner-container .cove-component__content { - padding: 1rem 15px 27px 0 !important; + padding: 0 15px 27px 0 !important; } &.isEditor { overflow: auto; diff --git a/packages/core/components/_stories/DataTable.stories.tsx b/packages/core/components/_stories/DataTable.stories.tsx index 62490c47a..4b7c3b9f7 100644 --- a/packages/core/components/_stories/DataTable.stories.tsx +++ b/packages/core/components/_stories/DataTable.stories.tsx @@ -45,8 +45,7 @@ export const CityState: Story = { tabbingId: '#asdf', columns: CityStateExample.columns, applyLegendToRow: () => ['#000'], - displayGeoName, - displayDataAsText: d => d + displayGeoName } } diff --git a/packages/core/components/elements/Confirm.tsx b/packages/core/components/elements/Confirm.tsx new file mode 100644 index 000000000..70a9649b6 --- /dev/null +++ b/packages/core/components/elements/Confirm.tsx @@ -0,0 +1,45 @@ +import Button from './Button.jsx' +import React from 'react' +import { missingRequiredSections } from '../../helpers/missingRequiredSections.js' +const Confirm = props => { + const { updateConfig, config } = props + const confirmDone = e => { + if (e) { + e.preventDefault() + } + + let newConfig = { ...config } + delete newConfig.newViz + + updateConfig(newConfig) + } + + const styles: React.CSSProperties = { + position: 'relative', + height: '100vh', + width: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + gridArea: 'content' + } + + return ( +
+
+

Finish Configuring

+

Set all required options to the left and confirm below to display a preview of the chart.

+ +
+
+ ) +} + +export default Confirm diff --git a/packages/core/components/elements/Error.tsx b/packages/core/components/elements/Error.tsx new file mode 100644 index 000000000..2351e8837 --- /dev/null +++ b/packages/core/components/elements/Error.tsx @@ -0,0 +1,24 @@ +import React from 'react' +const Error = ({ errorMessage }) => { + const styles: React.CSSProperties = { + position: 'absolute', + background: 'white', + zIndex: '999', + height: '100vh', + width: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + gridArea: 'content' + } + return ( +
+
+

Error With Configuration

+

{errorMessage}

+
+
+ ) +} + +export default Error diff --git a/packages/core/components/ui/Title/index.tsx b/packages/core/components/ui/Title/index.tsx index 0ec7a2427..9fddd3ee8 100644 --- a/packages/core/components/ui/Title/index.tsx +++ b/packages/core/components/ui/Title/index.tsx @@ -16,7 +16,7 @@ const Title = (props: HeaderProps) => { const { isDashboard, title, superTitle, classes = [], showTitle = true, ariaLevel = 2 } = props // standard classes every vis should have - const updatedClasses = ['cove-component__header', 'component__header', ...classes] + const updatedClasses = ['cove-component__header', 'component__header', 'mb-3', ...classes] return ( title && diff --git a/packages/core/helpers/coveUpdateWorker.ts b/packages/core/helpers/coveUpdateWorker.ts index 7186804d6..63b930d81 100644 --- a/packages/core/helpers/coveUpdateWorker.ts +++ b/packages/core/helpers/coveUpdateWorker.ts @@ -10,6 +10,7 @@ import update_4_24_7 from './ver/4.24.7' import update_4_24_9 from './ver/4.24.9' import versionNeedsUpdate from './ver/versionNeedsUpdate' import update_4_24_10 from './ver/4.24.10' +import update_4_24_11 from './ver/4.24.11' import update_4_25_1 from './ver/4.25.1' export const coveUpdateWorker = config => { @@ -22,6 +23,7 @@ export const coveUpdateWorker = config => { ['4.24.7', update_4_24_7, true], ['4.24.9', update_4_24_9], ['4.24.10', update_4_24_10], + ['4.24.11', update_4_24_11], ['4.25.1', update_4_25_1] ] diff --git a/packages/core/helpers/displayDataAsText.ts b/packages/core/helpers/displayDataAsText.ts index 4cd007d0f..783ab0696 100644 --- a/packages/core/helpers/displayDataAsText.ts +++ b/packages/core/helpers/displayDataAsText.ts @@ -1,12 +1,19 @@ import { MapConfig } from '@cdc/map/src/types/MapConfig' export const displayDataAsText = (value: string | number, columnName, state: MapConfig) => { + if (!state) return value + if (value === null || value === '' || value === undefined) { return '' } // if string of letters like 'Home' then don't need to format as a number - if (typeof value === 'string' && value.length > 0 && /[a-zA-Z]/.test(value) && state.legend.type === 'equalnumber') { + if ( + typeof value === 'string' && + value.length > 0 && + /[a-zA-Z]/.test(value) && + state?.legend?.type === 'equalnumber' + ) { return value } diff --git a/packages/core/helpers/isOlderVersion.ts b/packages/core/helpers/isOlderVersion.ts new file mode 100644 index 000000000..9fac1a8a2 --- /dev/null +++ b/packages/core/helpers/isOlderVersion.ts @@ -0,0 +1,20 @@ +/** + * Determines if the previous version is older than the current version. + * + * This function compares two version strings in the format "major.minor.patch". + * It returns `true` if the `previousVersion` is older than the `currentVersion`, + * otherwise it returns `false`. + * + * @param previousVersion - The version string to compare against the current version. + * @param currentVersion - The version string to compare with the previous version. + * @returns `true` if the previous version is older, otherwise `false`. + */ +export default function isOlderVersion(previousVersion: string, currentVersion: string): boolean { + if (!previousVersion) return true + const [prevMajor, prevMinor, prevPatch] = previousVersion.split('.').map(Number) + const [currMajor, currMinor, currPatch] = currentVersion.split('.').map(Number) + if (currMajor > prevMajor) return false + if (currMajor === prevMajor && currMinor > prevMinor) return false + if (currMajor === prevMajor && currMinor === prevMinor && currPatch > prevPatch) return false + return true +} diff --git a/packages/core/helpers/missingRequiredSections.ts b/packages/core/helpers/missingRequiredSections.ts new file mode 100644 index 000000000..edab74f92 --- /dev/null +++ b/packages/core/helpers/missingRequiredSections.ts @@ -0,0 +1,20 @@ +export const missingRequiredSections = config => { + if (config.visualizationType === 'Sankey') return false // skip checks for now + if (config.visualizationType === 'Forecasting') return false // skip required checks for now. + if (config.visualizationType === 'Forest Plot') return false // skip required checks for now. + if (config.visualizationType === 'Pie') { + if (undefined === config?.yAxis.dataKey) { + return true + } + } else { + if ((undefined === config?.series || false === config?.series.length > 0) && !config?.dynamicSeries) { + return true + } + } + + if (!config.xAxis.dataKey) { + return true + } + + return false +} diff --git a/packages/core/helpers/useDataVizClasses.ts b/packages/core/helpers/useDataVizClasses.ts index b30513e40..4c626bcc2 100644 --- a/packages/core/helpers/useDataVizClasses.ts +++ b/packages/core/helpers/useDataVizClasses.ts @@ -1,7 +1,7 @@ import useResizeObserver from '@cdc/map/src/hooks/useResizeObserver' import { isBelowBreakpoint } from './viewports' -export default function useDataVizClasses(config) { +export default function useDataVizClasses(config, viewport = null) { const { legend, lineDatapointStyle, @@ -15,8 +15,6 @@ export default function useDataVizClasses(config) { shadow } = config - const { currentViewport: viewport } = useResizeObserver(false) - let lineDatapointClass = '' if (lineDatapointStyle === 'hover') { diff --git a/packages/core/helpers/ver/4.24.11.ts b/packages/core/helpers/ver/4.24.11.ts new file mode 100644 index 000000000..bd71f4c6d --- /dev/null +++ b/packages/core/helpers/ver/4.24.11.ts @@ -0,0 +1,18 @@ +import _ from 'lodash' + +const addColorMigration = config => { + // add new property + config.migrations = config.migrations || {} + config.migrations.addColorMigration = true + return config +} + +const update_4_24_11 = config => { + const ver = '4.24.11' + const newConfig = _.cloneDeep(config) + addColorMigration(newConfig) + newConfig.version = ver + return newConfig +} + +export default update_4_24_11 diff --git a/packages/core/helpers/ver/4.25.1.ts b/packages/core/helpers/ver/4.25.1.ts index 29ed14ae6..efc9ee1d5 100644 --- a/packages/core/helpers/ver/4.25.1.ts +++ b/packages/core/helpers/ver/4.25.1.ts @@ -10,7 +10,7 @@ const removeTerritoriesLabel = config => { const update_4_25_1 = config => { const ver = '4.25.1' const newConfig = _.cloneDeep(config) - removeTerritoriesLabel(config) + removeTerritoriesLabel(newConfig) newConfig.version = ver return newConfig } diff --git a/packages/core/types/Runtime.ts b/packages/core/types/Runtime.ts index 195e28d71..b975d860d 100644 --- a/packages/core/types/Runtime.ts +++ b/packages/core/types/Runtime.ts @@ -10,6 +10,7 @@ export type ForecastingSeriesKey = { export type Runtime = { barSeriesKeys?: string[] + areaSeriesKeys: object[] forecastingSeriesKeys?: ForecastingSeriesKey[] originalXAxis: { dataKey: string diff --git a/packages/dashboard/index.html b/packages/dashboard/index.html index c4ef4d4e9..bc06c978a 100644 --- a/packages/dashboard/index.html +++ b/packages/dashboard/index.html @@ -30,7 +30,7 @@ -
+
diff --git a/packages/dashboard/src/CdcDashboardComponent.tsx b/packages/dashboard/src/CdcDashboardComponent.tsx index 9bb9502d1..5c5eb7dd7 100644 --- a/packages/dashboard/src/CdcDashboardComponent.tsx +++ b/packages/dashboard/src/CdcDashboardComponent.tsx @@ -534,7 +534,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug = classes={[`dashboard-title`, `${config.dashboard.theme ?? 'theme-blue'}`]} /> {/* Description */} - {description &&
{parse(description)}
} + {description &&
{parse(description)}
} {/* Visualizations */} {config.rows && config.rows diff --git a/packages/dashboard/src/_stories/Dashboard.stories.tsx b/packages/dashboard/src/_stories/Dashboard.stories.tsx index b74f64f0c..c6b5dc2cc 100644 --- a/packages/dashboard/src/_stories/Dashboard.stories.tsx +++ b/packages/dashboard/src/_stories/Dashboard.stories.tsx @@ -24,6 +24,10 @@ import { ConfigRow } from '../types/ConfigRow' import BumpChartConfig from './_mock/bump-chart.json' import MethodologyConfig from './_mock/methodology.json' import methodologyAPI from './_mock/methodologyAPI' +import TopSpacing_1 from './_mock/data-bite-dash-test.json' +import TopSpacing_2 from './_mock/data-bite-dash-test_1.json' +import TopSpacing_3 from './_mock/data-bite-dash-test_1_1.json' +import TopSpacing_4 from './_mock/data-bite-dash-test_1_1_1.json' const meta: Meta = { title: 'Components/Pages/Dashboard', @@ -403,4 +407,32 @@ export const RegressionMultiVisualization: Story = { } } +export const Top_Spacing_1: Story = { + args: { + config: TopSpacing_1, + isEditor: false + } +} + +export const Top_Spacing_2: Story = { + args: { + config: TopSpacing_2, + isEditor: false + } +} + +export const Top_Spacing_3: Story = { + args: { + config: TopSpacing_3, + isEditor: false + } +} + +export const Top_Spacing_4: Story = { + args: { + config: TopSpacing_4, + isEditor: false + } +} + export default meta diff --git a/packages/dashboard/src/_stories/_mock/data-bite-dash-test.json b/packages/dashboard/src/_stories/_mock/data-bite-dash-test.json new file mode 100644 index 000000000..c7365a18f --- /dev/null +++ b/packages/dashboard/src/_stories/_mock/data-bite-dash-test.json @@ -0,0 +1 @@ +{"dashboard":{"theme":"theme-blue","sharedFilters":[],"description":"Dash Description","title":"Dashboard Name"},"rows":[{"columns":[{"width":6,"widget":"data-bite1722537849962"},{"width":6,"widget":"data-bite1728059122204"}],"dataDescription":{"horizontal":false,"series":true,"singleRow":true},"dataKey":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv"}],"visualizations":{"data-bite1722537849962":{"type":"data-bite","dataBite":"","dataFunction":"Sum","dataColumn":"specimens tested","bitePosition":"Left","biteFontSize":24,"fontSize":"medium","biteBody":"specimens tested that would have detected influenza A(H5) or other novel influenza viruses","imageData":{"display":"none","url":"","alt":"","options":[]},"dataFormat":{"roundToPlace":0,"commas":true,"prefix":"","suffix":"+"},"biteStyle":"title","filters":[],"subtext":"","title":"Specimens tested","theme":"theme-blue","shadow":false,"visual":{"border":false,"accent":false,"background":false,"hideBackgroundColor":false,"borderColorTheme":false},"general":{"isCompactStyle":false},"filterBehavior":"Filter Change","openModal":true,"uid":"data-bite1722537849962","visualizationType":"data-bite","dataDescription":{"horizontal":false,"series":true,"singleRow":true},"version":"4.24.10","dataKey":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv"},"data-bite1728059122204":{"type":"data-bite","dataBite":"","dataFunction":"Sum","dataColumn":"Human cases","bitePosition":"Left","biteFontSize":24,"fontSize":"medium","biteBody":"case detected through national flu surveillance","imageData":{"display":"none","url":"","alt":"","options":[]},"dataFormat":{"roundToPlace":0,"commas":true,"prefix":"","suffix":""},"biteStyle":"title","filters":[],"subtext":"","title":"Human cases","theme":"theme-blue","shadow":false,"visual":{"border":false,"accent":false,"background":false,"hideBackgroundColor":false,"borderColorTheme":false},"general":{"isCompactStyle":false},"filterBehavior":"Filter Change","openModal":true,"uid":"data-bite1728059122204","visualizationType":"data-bite","dataDescription":{"horizontal":false,"series":true,"singleRow":true},"version":"4.24.10","dataKey":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv"}},"table":{"label":"Data Table","show":false,"showDownloadUrl":false,"showVertical":true},"datasets":{"/bird-flu/modules/situation-summary/national-flu-surveillance.csv":{"dataFileSize":39,"dataFileName":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv","dataFileSourceType":"url","dataFileFormat":"OCTET-STREAM","preview":true,"dataUrl":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv"}},"type":"dashboard","version":"4.24.10","uuid":1722537847428} \ No newline at end of file diff --git a/packages/dashboard/src/_stories/_mock/data-bite-dash-test_1.json b/packages/dashboard/src/_stories/_mock/data-bite-dash-test_1.json new file mode 100644 index 000000000..50c987f02 --- /dev/null +++ b/packages/dashboard/src/_stories/_mock/data-bite-dash-test_1.json @@ -0,0 +1 @@ +{"dashboard":{"theme":"theme-blue","sharedFilters":[],"description":"Dash Description","title":""},"rows":[{"columns":[{"width":6,"widget":"data-bite1722537849962"},{"width":6,"widget":"data-bite1728059122204"}],"dataDescription":{"horizontal":false,"series":true,"singleRow":true},"dataKey":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv"}],"visualizations":{"data-bite1722537849962":{"type":"data-bite","dataBite":"","dataFunction":"Sum","dataColumn":"specimens tested","bitePosition":"Left","biteFontSize":24,"fontSize":"medium","biteBody":"specimens tested that would have detected influenza A(H5) or other novel influenza viruses","imageData":{"display":"none","url":"","alt":"","options":[]},"dataFormat":{"roundToPlace":0,"commas":true,"prefix":"","suffix":"+"},"biteStyle":"title","filters":[],"subtext":"","title":"Specimens tested","theme":"theme-blue","shadow":false,"visual":{"border":false,"accent":false,"background":false,"hideBackgroundColor":false,"borderColorTheme":false},"general":{"isCompactStyle":false},"filterBehavior":"Filter Change","openModal":true,"uid":"data-bite1722537849962","visualizationType":"data-bite","dataDescription":{"horizontal":false,"series":true,"singleRow":true},"version":"4.24.10","dataKey":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv"},"data-bite1728059122204":{"type":"data-bite","dataBite":"","dataFunction":"Sum","dataColumn":"Human cases","bitePosition":"Left","biteFontSize":24,"fontSize":"medium","biteBody":"case detected through national flu surveillance","imageData":{"display":"none","url":"","alt":"","options":[]},"dataFormat":{"roundToPlace":0,"commas":true,"prefix":"","suffix":""},"biteStyle":"title","filters":[],"subtext":"","title":"Human cases","theme":"theme-blue","shadow":false,"visual":{"border":false,"accent":false,"background":false,"hideBackgroundColor":false,"borderColorTheme":false},"general":{"isCompactStyle":false},"filterBehavior":"Filter Change","openModal":true,"uid":"data-bite1728059122204","visualizationType":"data-bite","dataDescription":{"horizontal":false,"series":true,"singleRow":true},"version":"4.24.10","dataKey":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv"}},"table":{"label":"Data Table","show":false,"showDownloadUrl":false,"showVertical":true},"datasets":{"/bird-flu/modules/situation-summary/national-flu-surveillance.csv":{"dataFileSize":39,"dataFileName":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv","dataFileSourceType":"url","dataFileFormat":"OCTET-STREAM","preview":true,"dataUrl":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv"}},"type":"dashboard","version":"4.24.10","uuid":1722537847428} \ No newline at end of file diff --git a/packages/dashboard/src/_stories/_mock/data-bite-dash-test_1_1.json b/packages/dashboard/src/_stories/_mock/data-bite-dash-test_1_1.json new file mode 100644 index 000000000..898d5a5f8 --- /dev/null +++ b/packages/dashboard/src/_stories/_mock/data-bite-dash-test_1_1.json @@ -0,0 +1 @@ +{"dashboard":{"theme":"theme-blue","sharedFilters":[],"description":"","title":"Title no description"},"rows":[{"columns":[{"width":6,"widget":"data-bite1722537849962"},{"width":6,"widget":"data-bite1728059122204"}],"dataDescription":{"horizontal":false,"series":true,"singleRow":true},"dataKey":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv"}],"visualizations":{"data-bite1722537849962":{"type":"data-bite","dataBite":"","dataFunction":"Sum","dataColumn":"specimens tested","bitePosition":"Left","biteFontSize":24,"fontSize":"medium","biteBody":"specimens tested that would have detected influenza A(H5) or other novel influenza viruses","imageData":{"display":"none","url":"","alt":"","options":[]},"dataFormat":{"roundToPlace":0,"commas":true,"prefix":"","suffix":"+"},"biteStyle":"title","filters":[],"subtext":"","title":"Specimens tested","theme":"theme-blue","shadow":false,"visual":{"border":false,"accent":false,"background":false,"hideBackgroundColor":false,"borderColorTheme":false},"general":{"isCompactStyle":false},"filterBehavior":"Filter Change","openModal":true,"uid":"data-bite1722537849962","visualizationType":"data-bite","dataDescription":{"horizontal":false,"series":true,"singleRow":true},"version":"4.24.10","dataKey":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv"},"data-bite1728059122204":{"type":"data-bite","dataBite":"","dataFunction":"Sum","dataColumn":"Human cases","bitePosition":"Left","biteFontSize":24,"fontSize":"medium","biteBody":"case detected through national flu surveillance","imageData":{"display":"none","url":"","alt":"","options":[]},"dataFormat":{"roundToPlace":0,"commas":true,"prefix":"","suffix":""},"biteStyle":"title","filters":[],"subtext":"","title":"Human cases","theme":"theme-blue","shadow":false,"visual":{"border":false,"accent":false,"background":false,"hideBackgroundColor":false,"borderColorTheme":false},"general":{"isCompactStyle":false},"filterBehavior":"Filter Change","openModal":true,"uid":"data-bite1728059122204","visualizationType":"data-bite","dataDescription":{"horizontal":false,"series":true,"singleRow":true},"version":"4.24.10","dataKey":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv"}},"table":{"label":"Data Table","show":false,"showDownloadUrl":false,"showVertical":true},"datasets":{"/bird-flu/modules/situation-summary/national-flu-surveillance.csv":{"dataFileSize":39,"dataFileName":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv","dataFileSourceType":"url","dataFileFormat":"OCTET-STREAM","preview":true,"dataUrl":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv"}},"type":"dashboard","version":"4.24.10","uuid":1722537847428} \ No newline at end of file diff --git a/packages/dashboard/src/_stories/_mock/data-bite-dash-test_1_1_1.json b/packages/dashboard/src/_stories/_mock/data-bite-dash-test_1_1_1.json new file mode 100644 index 000000000..b168030f8 --- /dev/null +++ b/packages/dashboard/src/_stories/_mock/data-bite-dash-test_1_1_1.json @@ -0,0 +1 @@ +{"dashboard":{"theme":"theme-blue","sharedFilters":[],"description":"","title":""},"rows":[{"columns":[{"width":6,"widget":"data-bite1722537849962"},{"width":6,"widget":"data-bite1728059122204"}],"dataDescription":{"horizontal":false,"series":true,"singleRow":true},"dataKey":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv"}],"visualizations":{"data-bite1722537849962":{"type":"data-bite","dataBite":"","dataFunction":"Sum","dataColumn":"specimens tested","bitePosition":"Left","biteFontSize":24,"fontSize":"medium","biteBody":"specimens tested that would have detected influenza A(H5) or other novel influenza viruses","imageData":{"display":"none","url":"","alt":"","options":[]},"dataFormat":{"roundToPlace":0,"commas":true,"prefix":"","suffix":"+"},"biteStyle":"title","filters":[],"subtext":"","title":"Specimens tested","theme":"theme-blue","shadow":false,"visual":{"border":false,"accent":false,"background":false,"hideBackgroundColor":false,"borderColorTheme":false},"general":{"isCompactStyle":false},"filterBehavior":"Filter Change","openModal":true,"uid":"data-bite1722537849962","visualizationType":"data-bite","dataDescription":{"horizontal":false,"series":true,"singleRow":true},"version":"4.24.10","dataKey":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv"},"data-bite1728059122204":{"type":"data-bite","dataBite":"","dataFunction":"Sum","dataColumn":"Human cases","bitePosition":"Left","biteFontSize":24,"fontSize":"medium","biteBody":"case detected through national flu surveillance","imageData":{"display":"none","url":"","alt":"","options":[]},"dataFormat":{"roundToPlace":0,"commas":true,"prefix":"","suffix":""},"biteStyle":"title","filters":[],"subtext":"","title":"Human cases","theme":"theme-blue","shadow":false,"visual":{"border":false,"accent":false,"background":false,"hideBackgroundColor":false,"borderColorTheme":false},"general":{"isCompactStyle":false},"filterBehavior":"Filter Change","openModal":true,"uid":"data-bite1728059122204","visualizationType":"data-bite","dataDescription":{"horizontal":false,"series":true,"singleRow":true},"version":"4.24.10","dataKey":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv"}},"table":{"label":"Data Table","show":false,"showDownloadUrl":false,"showVertical":true},"datasets":{"/bird-flu/modules/situation-summary/national-flu-surveillance.csv":{"dataFileSize":39,"dataFileName":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv","dataFileSourceType":"url","dataFileFormat":"OCTET-STREAM","preview":true,"dataUrl":"/bird-flu/modules/situation-summary/national-flu-surveillance.csv"}},"type":"dashboard","version":"4.24.10","uuid":1722537847428} \ No newline at end of file diff --git a/packages/dashboard/src/components/MultiConfigTabs/MultiConfigTabs.tsx b/packages/dashboard/src/components/MultiConfigTabs/MultiConfigTabs.tsx index e6e5f01c9..de1660e7c 100644 --- a/packages/dashboard/src/components/MultiConfigTabs/MultiConfigTabs.tsx +++ b/packages/dashboard/src/components/MultiConfigTabs/MultiConfigTabs.tsx @@ -117,7 +117,7 @@ const MultiConfigTabs = () => { if (!config.multiDashboards) return null return ( -