diff --git a/.github/workflows/storybook-deploy.yml b/.github/workflows/storybook-deploy.yml index 72e8023222..6b4ac13b4f 100644 --- a/.github/workflows/storybook-deploy.yml +++ b/.github/workflows/storybook-deploy.yml @@ -10,6 +10,8 @@ on: jobs: build: runs-on: ubuntu-latest + env: + NODE_OPTIONS: --max-old-space-size=6144 steps: - uses: actions/checkout@v4 @@ -48,3 +50,4 @@ jobs: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 # or specific "vX.X.X" version tag for this action + diff --git a/packages/chart/index.html b/packages/chart/index.html index cfe19abd89..0dec96339d 100644 --- a/packages/chart/index.html +++ b/packages/chart/index.html @@ -11,20 +11,21 @@ min-height: calc(100vh + 1px); } - /* Add 1rem padding to mimic DFE when editor is not visible */ - .cdc-open-viz-module:not(.isEditor) { - padding: 1rem; - } - - - + .cdc-map-outer-container { + min-height: 100vh; + }
- + + +
+ + + + + - - diff --git a/packages/chart/src/CdcChartComponent.tsx b/packages/chart/src/CdcChartComponent.tsx index f7c7f943ca..0bd86cae70 100644 --- a/packages/chart/src/CdcChartComponent.tsx +++ b/packages/chart/src/CdcChartComponent.tsx @@ -240,6 +240,14 @@ const CdcChart: React.FC = ({ const convertLineToBarGraph = isConvertLineToBarGraph(config, filteredData) + // Declaratively calculate series keys for pie charts based on filtered data + const pieSeriesKeys = useMemo(() => { + if (config.visualizationType !== 'Pie' || !config.xAxis?.dataKey) return null + const data = filteredData?.length > 0 ? filteredData : excludedData + if (!data) return null + return _.uniq(data.map(d => d[config.xAxis.dataKey])) + }, [config.visualizationType, config.xAxis?.dataKey, filteredData, excludedData]) + const prepareConfig = (loadedConfig: ChartConfig) => { // Create defaults without version to avoid overriding legacy configs const defaultsWithoutPalette = { ...defaults } @@ -376,6 +384,7 @@ const CdcChart: React.FC = ({ const pieData = currentData.length > 0 ? currentData : newExcludedData newConfig.runtime.seriesKeys = _.uniq(pieData.map(d => d[newConfig.xAxis.dataKey])) newConfig.runtime.seriesLabelsAll = newConfig.runtime.seriesKeys + newConfig.runtime.isPieChart = true // Flag to know when to use derived keys } else { const finalData = dataOverride || newConfig.formattedData || newConfig.data newConfig.runtime.seriesKeys = (newConfig.runtime.series || []).flatMap(series => { @@ -710,6 +719,19 @@ const CdcChart: React.FC = ({ } }, [externalFilters]) // eslint-disable-line + // Declaratively update runtime series keys for pie charts when derived value changes + if (config.runtime?.isPieChart && pieSeriesKeys && !_.isEqual(pieSeriesKeys, config.runtime?.seriesKeys)) { + const newConfig = { + ...config, + runtime: { + ...config.runtime, + seriesKeys: pieSeriesKeys, + seriesLabelsAll: pieSeriesKeys + } + } + setConfig(newConfig) + } + // Generates color palette to pass to child chart component useEffect(() => { if (stateData && config.xAxis && config.runtime?.seriesKeys) { @@ -1360,7 +1382,7 @@ const CdcChart: React.FC = ({ return ( = { + title: 'Components/Templates/Chart/Regions/Categorical', + component: Chart +} + +type Story = StoryObj + +const categoricalData = [ + { category: 'Jan 1', value: 10 }, + { category: 'Jan 8', value: 25 }, + { category: 'Jan 15', value: 35 }, + { category: 'Jan 22', value: 45 }, + { category: 'Jan 29', value: 55 }, + { category: 'Feb 5', value: 40 }, + { category: 'Feb 12', value: 60 }, + { category: 'Feb 19', value: 75 }, + { category: 'Feb 26', value: 65 }, + { category: 'Mar 4', value: 80 } +] + +const baseCategoricalConfig = { + type: 'chart', + visualizationType: 'Line', + orientation: 'vertical', + showTitle: true, + theme: 'theme-blue', + animate: false, + xAxis: { + type: 'categorical', + dataKey: 'category', + size: '0', + hideAxis: false, + hideTicks: false + }, + yAxis: { + size: '50', + hideAxis: false, + hideTicks: false, + gridLines: true, + min: '0', + max: '100' + }, + series: [{ dataKey: 'value', type: 'Line', axis: 'Left', tooltip: true, name: 'Value' }], + legend: { hide: true }, + data: categoricalData, + regions: [] +} + +// LINE CHARTS + +export const Line_Fixed_From_Fixed_To: Story = { + args: { + config: { + ...baseCategoricalConfig, + title: 'Categorical - Line: Fixed From + Fixed To', + regions: [ + { + from: 'Jan 15', + to: 'Feb 12', + fromType: 'Fixed', + toType: 'Fixed', + label: 'Fixed Region', + background: '#0077cc', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +export const Line_Fixed_From_Last_Date: Story = { + args: { + config: { + ...baseCategoricalConfig, + title: 'Categorical - Line: Fixed From + Last Date', + regions: [ + { + from: 'Feb 5', + to: '', + fromType: 'Fixed', + toType: 'Last Date', + label: 'To Last Date', + background: '#00aa55', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +// BAR CHARTS + +export const Bar_Fixed_From_Fixed_To: Story = { + args: { + config: { + ...baseCategoricalConfig, + visualizationType: 'Bar', + barThickness: 0.7, + title: 'Categorical - Bar: Fixed From + Fixed To', + regions: [ + { + from: 'Jan 15', + to: 'Feb 12', + fromType: 'Fixed', + toType: 'Fixed', + label: 'Fixed Region', + background: '#0077cc', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +export const Bar_Fixed_From_Last_Date: Story = { + args: { + config: { + ...baseCategoricalConfig, + visualizationType: 'Bar', + barThickness: 0.7, + title: 'Categorical - Bar: Fixed From + Last Date', + regions: [ + { + from: 'Feb 5', + to: '', + fromType: 'Fixed', + toType: 'Last Date', + label: 'To Last Date', + background: '#00aa55', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +export default meta diff --git a/packages/chart/src/_stories/Chart.Regions.DateScale.stories.tsx b/packages/chart/src/_stories/Chart.Regions.DateScale.stories.tsx new file mode 100644 index 0000000000..da53931835 --- /dev/null +++ b/packages/chart/src/_stories/Chart.Regions.DateScale.stories.tsx @@ -0,0 +1,197 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import Chart from '../CdcChart' + +const meta: Meta = { + title: 'Components/Templates/Chart/Regions/Date Scale (Band)', + component: Chart +} + +type Story = StoryObj + +const dateData = [ + { date: '2024-01-01', value: 10 }, + { date: '2024-01-08', value: 25 }, + { date: '2024-01-15', value: 35 }, + { date: '2024-01-22', value: 45 }, + { date: '2024-01-29', value: 55 }, + { date: '2024-02-05', value: 40 }, + { date: '2024-02-12', value: 60 }, + { date: '2024-02-19', value: 75 }, + { date: '2024-02-26', value: 65 }, + { date: '2024-03-04', value: 80 } +] + +const baseDateConfig = { + type: 'chart', + visualizationType: 'Line', + orientation: 'vertical', + showTitle: true, + theme: 'theme-blue', + animate: false, + xAxis: { + type: 'date', + dataKey: 'date', + dateParseFormat: '%Y-%m-%d', + dateDisplayFormat: '%b %-d', + size: '0', + hideAxis: false, + hideTicks: false, + numTicks: '6' + }, + yAxis: { + size: '50', + hideAxis: false, + hideTicks: false, + gridLines: true, + min: '0', + max: '100' + }, + series: [{ dataKey: 'value', type: 'Line', axis: 'Left', tooltip: true, name: 'Value' }], + legend: { hide: true }, + data: dateData, + regions: [] +} + +// LINE CHARTS + +export const Line_Fixed_From_Fixed_To: Story = { + args: { + config: { + ...baseDateConfig, + title: 'Date Scale - Line: Fixed From + Fixed To', + regions: [ + { + from: '2024-01-15', + to: '2024-02-12', + fromType: 'Fixed', + toType: 'Fixed', + label: 'Fixed Region', + background: '#0077cc', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +export const Line_Fixed_From_Last_Date: Story = { + args: { + config: { + ...baseDateConfig, + title: 'Date Scale - Line: Fixed From + Last Date', + regions: [ + { + from: '2024-02-05', + to: '', + fromType: 'Fixed', + toType: 'Last Date', + label: 'To Last Date', + background: '#00aa55', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +export const Line_Previous_Days_Last_Date: Story = { + args: { + config: { + ...baseDateConfig, + title: 'Date Scale - Line: Previous Days + Last Date', + regions: [ + { + from: '28', + to: '', + fromType: 'Previous Days', + toType: 'Last Date', + label: 'Last 28 Days', + background: '#aa0077', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +// BAR CHARTS + +export const Bar_Fixed_From_Fixed_To: Story = { + args: { + config: { + ...baseDateConfig, + visualizationType: 'Bar', + barThickness: 0.7, + title: 'Date Scale - Bar: Fixed From + Fixed To', + regions: [ + { + from: '2024-01-15', + to: '2024-02-12', + fromType: 'Fixed', + toType: 'Fixed', + label: 'Fixed Region', + background: '#0077cc', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +export const Bar_Fixed_From_Last_Date: Story = { + args: { + config: { + ...baseDateConfig, + visualizationType: 'Bar', + barThickness: 0.7, + title: 'Date Scale - Bar: Fixed From + Last Date', + regions: [ + { + from: '2024-02-05', + to: '', + fromType: 'Fixed', + toType: 'Last Date', + label: 'To Last Date', + background: '#00aa55', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +export const Bar_Previous_Days_Last_Date: Story = { + args: { + config: { + ...baseDateConfig, + visualizationType: 'Bar', + barThickness: 0.7, + title: 'Date Scale - Bar: Previous Days + Last Date', + regions: [ + { + from: '28', + to: '', + fromType: 'Previous Days', + toType: 'Last Date', + label: 'Last 28 Days', + background: '#aa0077', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +export default meta diff --git a/packages/chart/src/_stories/Chart.Regions.DateTimeScale.stories.tsx b/packages/chart/src/_stories/Chart.Regions.DateTimeScale.stories.tsx new file mode 100644 index 0000000000..69c5de9dd7 --- /dev/null +++ b/packages/chart/src/_stories/Chart.Regions.DateTimeScale.stories.tsx @@ -0,0 +1,297 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import Chart from '../CdcChart' + +const meta: Meta = { + title: 'Components/Templates/Chart/Regions/Date-Time Scale (Continuous)', + component: Chart +} + +type Story = StoryObj + +const dateData = [ + { date: '2024-01-01', value: 10 }, + { date: '2024-01-08', value: 25 }, + { date: '2024-01-15', value: 35 }, + { date: '2024-01-22', value: 45 }, + { date: '2024-01-29', value: 55 }, + { date: '2024-02-05', value: 40 }, + { date: '2024-02-12', value: 60 }, + { date: '2024-02-19', value: 75 }, + { date: '2024-02-26', value: 65 }, + { date: '2024-03-04', value: 80 } +] + +const baseDateTimeConfig = { + type: 'chart', + visualizationType: 'Line', + orientation: 'vertical', + showTitle: true, + theme: 'theme-blue', + animate: false, + xAxis: { + type: 'date-time', + dataKey: 'date', + dateParseFormat: '%Y-%m-%d', + dateDisplayFormat: '%b %-d', + size: '0', + hideAxis: false, + hideTicks: false, + numTicks: '6' + }, + yAxis: { + size: '50', + hideAxis: false, + hideTicks: false, + gridLines: true, + min: '0', + max: '100' + }, + series: [{ dataKey: 'value', type: 'Line', axis: 'Left', tooltip: true, name: 'Value' }], + legend: { hide: true }, + data: dateData, + regions: [] +} + +// LINE CHARTS + +export const Line_Fixed_From_Fixed_To: Story = { + args: { + config: { + ...baseDateTimeConfig, + title: 'Date-Time Scale - Line: Fixed From + Fixed To', + regions: [ + { + from: '2024-01-15', + to: '2024-02-11', + fromType: 'Fixed', + toType: 'Fixed', + label: 'Fixed Region', + background: '#0077cc', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +export const Line_Fixed_From_Last_Date: Story = { + args: { + config: { + ...baseDateTimeConfig, + title: 'Date-Time Scale - Line: Fixed From + Last Date', + regions: [ + { + from: '2024-02-05', + to: '', + fromType: 'Fixed', + toType: 'Last Date', + label: 'To Last Date', + background: '#00aa55', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +export const Line_Previous_Days_Last_Date: Story = { + args: { + config: { + ...baseDateTimeConfig, + title: 'Date-Time Scale - Line: Previous Days + Last Date', + regions: [ + { + from: '8', + to: '', + fromType: 'Previous Days', + toType: 'Last Date', + label: 'Last 8 Days', + background: '#aa0077', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +// BAR CHARTS + +export const Bar_Fixed_From_Fixed_To: Story = { + args: { + config: { + ...baseDateTimeConfig, + visualizationType: 'Bar', + barThickness: 0.7, + title: 'Date-Time Scale - Bar: Fixed From + Fixed To', + regions: [ + { + from: '2024-01-15', + to: '2024-02-12', + fromType: 'Fixed', + toType: 'Fixed', + label: 'Fixed Region', + background: '#0077cc', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +export const Bar_Fixed_From_Last_Date: Story = { + args: { + config: { + ...baseDateTimeConfig, + visualizationType: 'Bar', + barThickness: 0.7, + title: 'Date-Time Scale - Bar: Fixed From + Last Date', + regions: [ + { + from: '2024-02-05', + to: '', + fromType: 'Fixed', + toType: 'Last Date', + label: 'To Last Date', + background: '#00aa55', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +export const Bar_Previous_Days_Last_Date: Story = { + args: { + config: { + ...baseDateTimeConfig, + visualizationType: 'Bar', + barThickness: 0.7, + title: 'Date-Time Scale - Bar: Previous Days + Last Date', + regions: [ + { + from: '28', + to: '', + fromType: 'Previous Days', + toType: 'Last Date', + label: 'Last 28 Days', + background: '#aa0077', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +// EDGE CASES + +export const Edge_Region_At_Start: Story = { + args: { + config: { + ...baseDateTimeConfig, + title: 'Edge Case: Region at Start', + regions: [ + { + from: '2024-01-01', + to: '2024-01-21', + fromType: 'Fixed', + toType: 'Fixed', + label: 'At Start', + background: '#0077cc', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +export const Edge_Region_At_End: Story = { + args: { + config: { + ...baseDateTimeConfig, + title: 'Edge Case: Region at End', + regions: [ + { + from: '2024-02-19', + to: '2024-03-04', + fromType: 'Fixed', + toType: 'Fixed', + label: 'At End', + background: '#00aa55', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +export const Edge_Full_Coverage: Story = { + args: { + config: { + ...baseDateTimeConfig, + title: 'Edge Case: Full Chart Coverage', + regions: [ + { + from: '2024-01-01', + to: '', + fromType: 'Fixed', + toType: 'Last Date', + label: 'Full Coverage', + background: '#cc7700', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +export const Multiple_Regions: Story = { + args: { + config: { + ...baseDateTimeConfig, + title: 'Multiple Regions', + regions: [ + { + from: '2024-01-08', + to: '2024-01-21', + fromType: 'Fixed', + toType: 'Fixed', + label: 'Region 1', + background: '#0077cc', + color: '#000000', + range: 'Custom' + }, + { + from: '2024-02-05', + to: '2024-02-18', + fromType: 'Fixed', + toType: 'Fixed', + label: 'Region 2', + background: '#00aa55', + color: '#000000', + range: 'Custom' + } + ] + }, + isEditor: true + } +} + +export default meta diff --git a/packages/chart/src/components/LinearChart.tsx b/packages/chart/src/components/LinearChart.tsx index 272d6af18b..3a37e45741 100644 --- a/packages/chart/src/components/LinearChart.tsx +++ b/packages/chart/src/components/LinearChart.tsx @@ -2,6 +2,7 @@ import React, { forwardRef, useContext, useEffect, + useLayoutEffect, useImperativeHandle, useMemo, useRef, @@ -149,6 +150,7 @@ const LinearChart = forwardRef(({ parentHeight, p const [point, setPoint] = useState({ x: 0, y: 0 }) const [suffixWidth, setSuffixWidth] = useState(0) const [calculatedSvgHeight, setCalculatedSvgHeight] = useState(null) + const [axisUpdateKey, setAxisUpdateKey] = useState(0) // REFS const axisBottomRef = useRef(null) @@ -194,7 +196,7 @@ const LinearChart = forwardRef(({ parentHeight, p if (!xAxisLabelRefs.current.length) return const tallestLabel = Math.max(...xAxisLabelRefs.current.map(label => label.getBBox().height)) return tallestLabel + X_TICK_LABEL_PADDING + DEFAULT_TICK_LENGTH - }, [parentWidth, config.xAxis, xAxisLabelRefs.current, config.xAxis.tickRotation]) + }, [parentWidth, config.xAxis, xAxisLabelRefs.current, config.xAxis.tickRotation, axisUpdateKey]) const yMax = initialHeight + forestRowsHeight @@ -221,6 +223,12 @@ const LinearChart = forwardRef(({ parentHeight, p } return result }, [xAxisDataMapped]) + + // Force update x axis ticks when filtering + useLayoutEffect(() => { + setAxisUpdateKey(prev => prev + 1) + }, [data.length, xAxisDataMapped?.[0], xAxisDataMapped?.[xAxisDataMapped.length - 1]]) + const { yScaleRight, hasRightAxis } = useRightAxis({ config, yMax, data }) const xMax = parentWidth - Number(runtime.yAxis.size) - (hasRightAxis ? config.yAxis.rightAxisSize : 0) @@ -956,13 +964,13 @@ const LinearChart = forwardRef(({ parentHeight, p return ( // prettier-ignore + key={`yAxis-${anchor.value}--${index}`} + strokeDasharray={handleLineType(anchor.lineStyle)} + stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'} + className='anchor-y' + from={{ x: Number(runtime.yAxis.size), y: position - middleOffset }} + to={{ x: Number(runtime.yAxis.size) + Number(xMax), y: position - middleOffset }} + /> ) })} {/* x anchors */} @@ -992,14 +1000,14 @@ const LinearChart = forwardRef(({ parentHeight, p return ( // prettier-ignore + key={`xAxis-${anchor.value}--${index}`} + strokeDasharray={handleLineType(anchor.lineStyle)} + stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'} + fill={anchor.color ? anchor.color : 'rgba(0,0,0,1)'} + className='anchor-x' + from={{ x: Number(anchorPosition) + Number(padding), y: 0 }} + to={{ x: Number(anchorPosition) + Number(padding), y: yMax }} + /> ) })} {/* we are handling regions in bar charts differently, so that we can calculate the bar group into the region space. */} diff --git a/packages/chart/src/components/Regions/components/Regions.tsx b/packages/chart/src/components/Regions/components/Regions.tsx index 63e0fbfd71..9525be71c5 100644 --- a/packages/chart/src/components/Regions/components/Regions.tsx +++ b/packages/chart/src/components/Regions/components/Regions.tsx @@ -1,250 +1,422 @@ -import React, { useContext, useMemo } from 'react' +import React, { useContext } from 'react' import ConfigContext from '../../../ConfigContext' import { ChartContext } from '../../../types/ChartContext' import { Text } from '@visx/text' import { Group } from '@visx/group' import { formatDate, isDateScale } from '@cdc/core/helpers/cove/date.js' +// Constants for visualization types +const VIZ_TYPES = { + BAR: 'Bar', + LINE: 'Line', + AREA: 'Area Chart', + COMBO: 'Combo' +} as const + +type Region = { + from: string + to: string + fromType?: 'Fixed' | 'Previous Days' + toType?: 'Fixed' | 'Last Date' + label: string + background: string + color: string +} + +type XScale = { + (value: unknown): number + domain: () => unknown[] + bandwidth?: () => number +} + type RegionsProps = { - xScale: Function + xScale: XScale yMax: number barWidth?: number totalBarsInGroup?: number xMax?: number } +type HighlightedAreaProps = { + x: number + width: number + yMax: number + background: string +} + +const HighlightedArea: React.FC = ({ x, width, yMax, background }) => ( + +) + +/** Find the closest date in domain to a target date */ +const findClosestDate = (targetTime: number, domain: T[], getTime: (d: T) => number): T => { + let closest = domain[0] + let minDiff = Math.abs(targetTime - getTime(closest)) + + for (let i = 1; i < domain.length; i++) { + const diff = Math.abs(targetTime - getTime(domain[i])) + if (diff < minDiff) { + minDiff = diff + closest = domain[i] + } + } + return closest +} + +/** Check if visualization type is line-like (Line or Area Chart) */ +const isLineLike = (type: string): boolean => type === VIZ_TYPES.LINE || type === VIZ_TYPES.AREA + +/** Check if visualization type is bar-like (Bar or Combo) */ +const isBarLike = (type: string): boolean => type === VIZ_TYPES.BAR || type === VIZ_TYPES.COMBO + // TODO: should regions be removed on categorical axis? const Regions: React.FC = ({ xScale, barWidth = 0, totalBarsInGroup = 1, yMax, xMax }) => { - const { parseDate, config, unfilteredData, getXAxisData } = useContext(ConfigContext) + const { parseDate, config } = useContext(ConfigContext) - const { runtime, regions, visualizationType, orientation, xAxis } = config - // Use full unfiltered data domain for regions, not the filtered/brushed domain - // This ensures regions stay fixed when brush selection changes - const fullDomain = useMemo(() => { - if (!unfilteredData || !Array.isArray(unfilteredData) || unfilteredData.length === 0) { - return xScale.domain() - } + const { regions, visualizationType, orientation, xAxis } = config - const dataKey = config.runtime.originalXAxis?.dataKey || config.xAxis.dataKey - const isDate = isDateScale(config.xAxis) + const getBarOffset = (): number => (barWidth * totalBarsInGroup) / 2 - if (isDate && getXAxisData) { - const mapped = unfilteredData.map(d => getXAxisData(d)) - // Sort dates if needed - return config.xAxis.sortByRecentDate ? [...mapped].reverse() : mapped - } else { - const mapped = unfilteredData.map(d => d[dataKey]) - return config.xAxis.sortByRecentDate ? [...mapped].reverse() : mapped - } - }, [unfilteredData, config, getXAxisData, xScale]) + // ============================================ + // HELPER FUNCTIONS FOR PREVIOUS DAYS + // ============================================ - // Use full domain for "Last Date" calculations to ensure regions stay fixed - // But use filtered domain for scale positioning (xScale is based on filtered data) - const filteredDomain = xScale.domain() + const calculatePreviousDaysFrom = (region: Region, axisType: string): number => { + const previousDays = Number(region.from) || 0 + const domain = xScale.domain() - const getFromValue = region => { - let from + // Determine the "to" reference date + const toRefDate = + region.toType === 'Last Date' + ? new Date(domain[domain.length - 1] as string | number).getTime() + : new Date(region.to) - // Fixed Date - if (!region?.fromType || region.fromType === 'Fixed') { - const date = new Date(region.from) - const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date)).getTime() - from = xScale(parsedDate) + const toFormatted = formatDate(config.xAxis.dateParseFormat, toRefDate) + const toDate = new Date(toFormatted) + const fromDate = new Date(toDate) + fromDate.setDate(fromDate.getDate() - previousDays) - if (visualizationType === 'Bar' && xAxis.type === 'date-time') { - from = from - (barWidth * totalBarsInGroup) / 2 - } + let closestValue: unknown + + if (axisType === 'date') { + const fromTime = new Date(formatDate(xAxis.dateParseFormat, fromDate)).getTime() + closestValue = findClosestDate(fromTime, domain as number[], d => d) + } else if (axisType === 'categorical') { + const fromTime = fromDate.getTime() + closestValue = findClosestDate(fromTime, domain as string[], d => new Date(d).getTime()) + } else if (axisType === 'date-time') { + closestValue = fromDate.getTime() } - // Previous Date + return xScale(closestValue) + } + + // ============================================ + // LINE/AREA CHART LOGIC + // ============================================ + + const getLineFromValue_Categorical = (region: Region): number => { + let from: number if (region.fromType === 'Previous Days') { - const previousDays = Number(region.from) || 0 - // Use full domain for "Last Date" calculations - const domainToUse = fullDomain.length > 0 ? fullDomain : filteredDomain - const categoricalDomain = domainToUse.map(d => formatDate(config.xAxis.dateParseFormat, new Date(d))) - const d = - region.toType === 'Last Date' ? new Date(domainToUse[domainToUse.length - 1]).getTime() : new Date(region.to) // on categorical charts force leading zero 03/15/2016 vs 3/15/2016 for valid date format - const to = - config.xAxis.type === 'categorical' - ? formatDate(config.xAxis.dateParseFormat, d) - : formatDate(config.xAxis.dateParseFormat, d) - const toDate = new Date(to) - from = new Date(toDate.setDate(toDate.getDate() - Number(previousDays))) - - if (xAxis.type === 'date') { - from = new Date(formatDate(xAxis.dateParseFormat, from)).getTime() - - const domainToUse = fullDomain.length > 0 ? fullDomain : filteredDomain - let closestDate = domainToUse[0] - let minDiff = Math.abs(from - closestDate) - - for (let i = 1; i < domainToUse.length; i++) { - const diff = Math.abs(from - domainToUse[i]) - if (diff < minDiff) { - minDiff = diff - closestDate = domainToUse[i] - } - } - from = closestDate - } + from = calculatePreviousDaysFrom(region, 'categorical') + } else { + from = xScale(region.from) + } + // Add left padding (yAxis.size) + half bandwidth to center on the category + let scalePadding = Number(config.yAxis.size) + if (xScale.bandwidth) { + scalePadding += xScale.bandwidth() / 2 + } + return from + scalePadding + } - // Here the domain is in the xScale.dateParseFormat - if (xAxis.type === 'categorical') { - const domainToUse = fullDomain.length > 0 ? fullDomain : filteredDomain - let closestDate = domainToUse[0] - let minDiff = Math.abs(new Date(from).getTime() - new Date(closestDate).getTime()) - - for (let i = 1; i < domainToUse.length; i++) { - const diff = Math.abs(new Date(from).getTime() - new Date(domainToUse[i]).getTime()) - if (diff < minDiff) { - minDiff = diff - closestDate = domainToUse[i] - } - } - from = closestDate - } + const getLineToValue_Categorical = (region: Region): number => { + if (region.toType === 'Last Date') { + return calculateLineLastDatePosition_Categorical() + } + let to = xScale(region.to) + // Add left padding (yAxis.size) + half bandwidth + let scalePadding = Number(config.yAxis.size) + if (xScale.bandwidth) { + scalePadding += xScale.bandwidth() / 2 + } + return to + scalePadding + } + + const getLineFromValue_Date = (region: Region): number => { + let from: number + if (region.fromType === 'Previous Days') { + from = calculatePreviousDaysFrom(region, 'date') + } else { + // For date scale (band), we need to find the value in the domain + // Parse the region date to match the format in the domain + const date = new Date(region.from) + const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date)).getTime() - from = xScale(from) + // For band scales, find the closest date in the domain + const domain = xScale.domain() as number[] + const closestDate = findClosestDate(parsedDate, domain, d => d) + from = xScale(closestDate) + } + // Add left padding (yAxis.size) + half bandwidth + let scalePadding = Number(config.yAxis.size) + if (xScale.bandwidth) { + scalePadding += xScale.bandwidth() / 2 } + return from + scalePadding + } - if (xAxis.type === 'categorical' && region.fromType !== 'Previous Days') { - from = xScale(region.from) + const getLineToValue_Date = (region: Region): number => { + if (region.toType === 'Last Date') { + return calculateLineLastDatePosition_Date() } + // For date scale (band), we need to find the value in the domain + // Parse the region date to match the format in the domain + const parsedDate = parseDate(region.to).getTime() + + // For band scales, find the closest date in the domain + const domain = xScale.domain() as number[] + const closestDate = findClosestDate(parsedDate, domain, d => d) + let to = xScale(closestDate) + + // Add left padding (yAxis.size) + half bandwidth + let scalePadding = Number(config.yAxis.size) + if (xScale.bandwidth) { + scalePadding += xScale.bandwidth() / 2 + } + return to + scalePadding + } - if (visualizationType === 'Line' || visualizationType === 'Area Chart') { - let scalePadding = Number(config.yAxis.size) - if (xScale.bandwidth) { - scalePadding += xScale.bandwidth() / 2 - } - from = from + scalePadding + const getLineFromValue_DateTime = (region: Region): number => { + if (region.fromType === 'Previous Days') { + const from = calculatePreviousDaysFrom(region, 'date-time') + return from + Number(config.yAxis.size) } + const date = new Date(region.from) + const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date)).getTime() + let from = xScale(parsedDate) + // For date-time, xScale returns correct position (no bandwidth), just add left padding + return from + Number(config.yAxis.size) + } - if (visualizationType === 'Bar' && config.xAxis.type === 'date-time' && region.fromType === 'Previous Days') { - from = from - (barWidth * totalBarsInGroup) / 2 + const getLineToValue_DateTime = (region: Region): number => { + if (region.toType === 'Last Date') { + return calculateLineLastDatePosition_DateTime() } + let to = xScale(parseDate(region.to).getTime()) + return to + Number(config.yAxis.size) + } + + const calculateLineLastDatePosition_Categorical = (): number => { + const chartStart = Number(config.yAxis.size || 0) + // Extend to the right edge of the chart + return chartStart + (xMax || 0) + } - return from + const calculateLineLastDatePosition_Date = (): number => { + const chartStart = Number(config.yAxis.size || 0) + // For date scale line charts with Last Date, extend to the right edge of the chart + return chartStart + (xMax || 0) } - const getToValue = region => { - let to + const calculateLineLastDatePosition_DateTime = (): number => { + const domain = xScale.domain() + const lastDate = domain[domain.length - 1] + const lastDatePosition = xScale(lastDate) + // Match the non-Last Date logic: just add yAxis.size + return Number(lastDatePosition + Number(config.yAxis.size)) + } - // when xScale is categorical leading zeros are removed, ie. 03/15/2016 is 3/15/2016 - if (xAxis.type === 'categorical') { - to = xScale(region.to) + // ============================================ + // BAR CHART LOGIC + // ============================================ + + const getBarFromValue_Categorical = (region: Region): number => { + if (region.fromType === 'Previous Days') { + return calculatePreviousDaysFrom(region, 'categorical') } + return xScale(region.from) + } - if (isDateScale(xAxis)) { - if (!region?.toType || region.toType === 'Fixed') { - to = xScale(parseDate(region.to).getTime()) - } + const getBarToValue_Categorical = (region: Region): number => { + if (region.toType === 'Last Date') { + return calculateBarLastDatePosition_Categorical() + } + let to = xScale(region.to) + return to + barWidth * totalBarsInGroup + } - if (visualizationType === 'Bar' || config.visualizationType === 'Combo') { - to = region.toType !== 'Last Date' ? xScale(parseDate(region.to).getTime()) + barWidth * totalBarsInGroup : to - } + const getBarFromValue_Date = (region: Region): number => { + if (region.fromType === 'Previous Days') { + return calculatePreviousDaysFrom(region, 'date') } + // For date scale (band), we need to find the value in the domain + const date = new Date(region.from) + const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date)).getTime() + + // For band scales, find the closest date in the domain + const domain = xScale.domain() as number[] + const closestDate = findClosestDate(parsedDate, domain, d => d) + return xScale(closestDate) + } + + const getBarToValue_Date = (region: Region): number => { if (region.toType === 'Last Date') { - // Use full domain to get the actual last date, not the filtered last date - const domainToUse = fullDomain.length > 0 ? fullDomain : filteredDomain - const lastDate = domainToUse[domainToUse.length - 1] - const lastDatePosition = xScale(lastDate) - - // If lastDate is not in the filtered domain, xScale might return undefined - // In that case, use xMax + yAxis.size to position at the end of the visible chart - if (lastDatePosition === undefined || isNaN(lastDatePosition)) { - const chartStart = Number(config.yAxis.size || 0) - to = xMax !== undefined ? chartStart + xMax : chartStart - } else { - // For band scales, xScale returns the start of the band - // To get to the end of the last band, we need to add the full bandwidth - const bandwidth = xScale.bandwidth ? xScale.bandwidth() : 0 - const chartStart = Number(config.yAxis.size || 0) - - if (visualizationType === 'Line' || visualizationType === 'Area Chart') { - // For Line/Area charts with band scales, add full bandwidth to reach end of band - // Then add chartStart to account for left padding - to = Number(lastDatePosition + bandwidth + chartStart) - } else if (visualizationType === 'Bar' || visualizationType === 'Combo') { - // For Bar charts, add barWidth instead of bandwidth - to = Number( - lastDatePosition + (config.xAxis.type === 'date' ? barWidth * totalBarsInGroup : bandwidth) + chartStart - ) - } else { - // For other chart types, just add bandwidth and chartStart - to = Number(lastDatePosition + bandwidth + chartStart) - } - } - } else { - // For non-"Last Date" regions, apply the standard padding - if (visualizationType === 'Line' || visualizationType === 'Area Chart') { - let scalePadding = Number(config.yAxis.size) - if (xScale.bandwidth) { - scalePadding += xScale.bandwidth() / 2 - } - to = to + scalePadding - } + return calculateBarLastDatePosition_Date() } + // For date scale (band), we need to find the value in the domain + const parsedDate = parseDate(region.to).getTime() + + // For band scales, find the closest date in the domain + const domain = xScale.domain() as number[] + const closestDate = findClosestDate(parsedDate, domain, d => d) + let to = xScale(closestDate) - if (visualizationType === 'Bar' && config.xAxis.type === 'date-time' && region.toType !== 'Last Date') { - to = to - (barWidth * totalBarsInGroup) / 2 + return to + barWidth * totalBarsInGroup + } + + const getBarFromValue_DateTime = (region: Region): number => { + let from: number + if (region.fromType === 'Previous Days') { + from = calculatePreviousDaysFrom(region, 'date-time') + } else { + const date = new Date(region.from) + const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date)).getTime() + from = xScale(parsedDate) } + return from - getBarOffset() + } - if ((visualizationType === 'Bar' || visualizationType === 'Combo') && xAxis.type === 'categorical') { - to = to + (visualizationType === 'Bar' || visualizationType === 'Combo' ? barWidth * totalBarsInGroup : 0) + const getBarToValue_DateTime = (region: Region): number => { + if (region.toType === 'Last Date') { + return calculateBarLastDatePosition_DateTime() } - return to + let to = xScale(parseDate(region.to).getTime()) + return to - getBarOffset() } - const getWidth = (to, from) => { - const width = to - from - // Ensure width is never negative - return Math.max(0, width) + const calculateBarLastDatePosition_Categorical = (): number => { + const domain = xScale.domain() + const lastDate = domain[domain.length - 1] + const lastDatePosition = xScale(lastDate) + const bandwidth = xScale.bandwidth ? xScale.bandwidth() : 0 + // For categorical bars, extend to the end of the last bar + // Don't add chartStart - xScale already returns positions in the chart coordinate space + return xMax } - if (regions && orientation === 'vertical') { - return regions.map(region => { - const from = getFromValue(region) - const to = getToValue(region) - let width = getWidth(to, from) + const calculateBarLastDatePosition_Date = (): number => { + const domain = xScale.domain() + const lastDate = domain[domain.length - 1] + const lastDatePosition = xScale(lastDate) + const offset = barWidth * totalBarsInGroup + // Don't add chartStart - xScale already returns positions in chart coordinate space + return Number(lastDatePosition + offset) + } - if (!from || from === undefined || isNaN(from)) return null - if (!to || to === undefined || isNaN(to)) return null + const calculateBarLastDatePosition_DateTime = (): number => { + const domain = xScale.domain() + const lastDate = domain[domain.length - 1] + const lastDatePosition = xScale(lastDate) + // For date-time bars, don't add chartStart - xScale returns positions in chart coordinate space + // Also don't subtract barOffset since we want to extend to the edge + return Number(lastDatePosition) + } - // Clip region to visible chart area (xMax) to prevent overflow - // xMax is the width of the chart area (excluding left padding), so chartEnd = chartStart + xMax - const chartStart = Number(config.yAxis.size || 0) - const chartEnd = xMax !== undefined ? chartStart + xMax : chartStart + 1000 // fallback if xMax not provided + // ============================================ + // MAIN ROUTING FUNCTIONS + // ============================================ + + const getFromValue = (region: Region): number => { + const isLine = isLineLike(visualizationType) + const isBar = isBarLike(visualizationType) + + // LINE/AREA CHARTS + if (isLine) { + if (xAxis.type === 'categorical') { + return getLineFromValue_Categorical(region) + } else if (xAxis.type === 'date') { + return getLineFromValue_Date(region) + } else if (xAxis.type === 'date-time') { + return getLineFromValue_DateTime(region) + } + } + + // BAR CHARTS + if (isBar) { + if (xAxis.type === 'categorical') { + return getBarFromValue_Categorical(region) + } else if (xAxis.type === 'date') { + return getBarFromValue_Date(region) + } else if (xAxis.type === 'date-time') { + return getBarFromValue_DateTime(region) + } + } - // Adjust from and to to be within visible bounds - let clippedFrom = Math.max(chartStart, from) - let clippedTo = Math.min(chartEnd, to) + return 0 + } - // Recalculate width after clipping - width = clippedTo - clippedFrom + const getToValue = (region: Region): number => { + const isLine = isLineLike(visualizationType) + const isBar = isBarLike(visualizationType) - // Don't render if width is 0 or negative after clipping - if (width <= 0) return null + // LINE/AREA CHARTS + if (isLine) { + if (xAxis.type === 'categorical') { + return getLineToValue_Categorical(region) + } else if (xAxis.type === 'date') { + return getLineToValue_Date(region) + } else if (xAxis.type === 'date-time') { + return getLineToValue_DateTime(region) + } + } - const HighlightedArea = () => { - return + // BAR CHARTS + if (isBar) { + if (xAxis.type === 'categorical') { + return getBarToValue_Categorical(region) + } else if (xAxis.type === 'date') { + return getBarToValue_Date(region) + } else if (xAxis.type === 'date-time') { + return getBarToValue_DateTime(region) } + } - return ( - - - - {region.label} - - - ) - }) + return 0 } + + const getWidth = (to: number, from: number): number => Math.max(0, to - from) + + if (!regions || orientation !== 'vertical') return null + + const chartStart = Number(config.yAxis.size || 0) + const chartEnd = xMax !== undefined ? chartStart + xMax : chartStart + 1000 + + return regions.map((region: Region) => { + const from = getFromValue(region) + const to = getToValue(region) + + // Validate computed positions + if (from === undefined || isNaN(from) || to === undefined || isNaN(to)) { + return null + } + + // Clip region to visible chart area + const clippedFrom = Math.max(chartStart, from) + const clippedTo = Math.min(chartEnd, to) + const width = getWidth(clippedTo, clippedFrom) + + if (width <= 0) return null + + return ( + + + + {region.label} + + + ) + }) } export default Regions diff --git a/packages/chart/src/components/ScatterPlot/ScatterPlot.jsx b/packages/chart/src/components/ScatterPlot/ScatterPlot.jsx index 2911d1597b..f0cc0b2dbc 100644 --- a/packages/chart/src/components/ScatterPlot/ScatterPlot.jsx +++ b/packages/chart/src/components/ScatterPlot/ScatterPlot.jsx @@ -42,8 +42,8 @@ const ScatterPlot = ({ xScale, yScale }) => { ? `${config.runtime.seriesLabels[s] || ''}
` : '' } - ${config.xAxis.label}: ${formatNumber(item[config.xAxis.dataKey], 'bottom')}
- ${config.yAxis.label}: ${formatNumber(item[s], 'left')}
+ ${config.runtime?.xAxis?.label || config.xAxis.label}: ${formatNumber(item[config.xAxis.dataKey], 'bottom')}
+ ${config.runtime?.yAxis?.label || config.yAxis.label}: ${formatNumber(item[s], 'left')}
${additionalColumns .map( ([label, name, options]) => diff --git a/packages/core/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx b/packages/core/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx index bbe76ff887..c676535ef3 100644 --- a/packages/core/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +++ b/packages/core/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx @@ -16,6 +16,7 @@ type NestedDropdownEditorProps = { updateField: Function updateFilterStyle: Function handleGroupingCustomOrder: (index1: number, index2: number) => void + onNestedDragAreaHover?: (isHovering: boolean) => void } const NestedDropdownEditor: React.FC = ({ @@ -25,7 +26,8 @@ const NestedDropdownEditor: React.FC = ({ handleNameChange: handleGroupColumnNameChange, filterIndex, rawData, - updateField + updateField, + onNestedDragAreaHover }) => { const filter = config.filters[filterIndex] const subGrouping = filter?.subGrouping @@ -159,7 +161,10 @@ const NestedDropdownEditor: React.FC = ({ void } const NestedDropDownDashboard: React.FC = ({ filter, config, isDashboard = false, - updateFilterProp + updateFilterProp, + onNestedDragAreaHover }) => { const subGrouping = filter?.subGrouping @@ -270,6 +272,7 @@ const NestedDropDownDashboard: React.FC = ({ )} @@ -298,6 +301,7 @@ const NestedDropDownDashboard: React.FC = ({ handleFilterOrder={(sourceIndex, destinationIndex) => { handleSubGroupingCustomOrder(sourceIndex, destinationIndex, orderedSubGroupValues, groupName) }} + onNestedDragAreaHover={onNestedDragAreaHover} /> ) diff --git a/packages/dashboard/src/components/DashboardFilters/DashboardFiltersWrapper.tsx b/packages/dashboard/src/components/DashboardFilters/DashboardFiltersWrapper.tsx index 284eb90b8b..62c43457e1 100644 --- a/packages/dashboard/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +++ b/packages/dashboard/src/components/DashboardFilters/DashboardFiltersWrapper.tsx @@ -297,9 +297,9 @@ const DashboardFiltersWrapper: React.FC = ({ // if all of the filters are hidden filters don't display the VisualizationWrapper const filters = visualizationConfig?.sharedFilterIndexes ?.map(Number) - .map(filterIndex => dashboardConfig.dashboard.sharedFilters[filterIndex]) + ?.map(filterIndex => dashboardConfig.dashboard.sharedFilters[filterIndex]) - const displayNone = filters.length ? filters.every(filter => filter.showDropdown === false) : false + const displayNone = filters?.length ? filters.every(filter => filter.showDropdown === false) : false if (displayNone && !isEditor) return <> return ( diff --git a/packages/dashboard/src/scss/main.scss b/packages/dashboard/src/scss/main.scss index 80499130de..e31c4e4de8 100644 --- a/packages/dashboard/src/scss/main.scss +++ b/packages/dashboard/src/scss/main.scss @@ -197,15 +197,13 @@ // Mobile layout adjustments @include breakpoint(xs) { .row { - // Row spacing (larger - hierarchical separation between rows) margin-bottom: 1.5rem; > [class*='col-'] { // Remove side padding when columns stack to full width padding-left: 0; padding-right: 0; - // Column spacing (tighter - related items within same row) - margin-bottom: 0.75rem; + margin-bottom: 1.5rem; } // Last column doesn't need margin (row margin handles gap to next row) @@ -342,8 +340,8 @@ } } - // Hide icon when in multi-column layout, unless using white background - .bite__style--tp5:not(.white-background-style) .cdc-callout__icon { + // Hide flag when in multi-column layout + .bite__style--tp5 .cdc-callout__flag { display: none; } } @@ -360,8 +358,16 @@ .cdc-callout { flex: 1; - display: flex; - flex-direction: column; + } + + // Hide icon when in multi-column layout + .cdc-callout__icon { + display: none; + } + + // Hide flag when in multi-column layout + .cdc-callout__flag { + display: none; } .cdc-callout__heading { diff --git a/packages/data-bite/src/CdcDataBite.tsx b/packages/data-bite/src/CdcDataBite.tsx index 2388d1ea9c..9b2359199f 100644 --- a/packages/data-bite/src/CdcDataBite.tsx +++ b/packages/data-bite/src/CdcDataBite.tsx @@ -33,6 +33,9 @@ import { Config } from './types/Config' import dataBiteReducer from './store/db.reducer' import { IMAGE_POSITION_LEFT, IMAGE_POSITION_RIGHT, IMAGE_POSITION_TOP, IMAGE_POSITION_BOTTOM } from './constants' +// images +import CalloutFlag from './images/callout-flag.svg?url' + import { DATA_FUNCTION_COUNT, DATA_FUNCTION_MAX, @@ -614,20 +617,20 @@ const CdcDataBite = (props: CdcDataBiteProps) => { > {/* Icon shows by default, hidden when white background is enabled */} {!config.visual?.whiteBackground && ( - + )} {config.visual?.showTitle && title && title.trim() && (

- {parse(processContentWithMarkup(title))} + {parse(processContentWithMarkup(title))}

)} -
+
{showBite && (
{calculateDataBite(true)}
)}
- {parse(processContentWithMarkup(biteBody))} +

{parse(processContentWithMarkup(biteBody))}

{subtext && !config.general.isCompactStyle && (

{parse(processContentWithMarkup(subtext))} diff --git a/packages/data-bite/src/images/callout-flag.svg b/packages/data-bite/src/images/callout-flag.svg new file mode 100644 index 0000000000..1e5d94ea5c --- /dev/null +++ b/packages/data-bite/src/images/callout-flag.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/packages/data-bite/src/scss/bite.scss b/packages/data-bite/src/scss/bite.scss index 0a0df0aecd..af985e48ca 100644 --- a/packages/data-bite/src/scss/bite.scss +++ b/packages/data-bite/src/scss/bite.scss @@ -79,6 +79,7 @@ } .cove-component__content { + padding: 0; // Visual: Shadow &.shadow { box-shadow: rgba(0, 0, 0, 0.2) 0 3px 10px; @@ -155,20 +156,19 @@ } .cdc-callout__databite { + padding-top: 2px; width: auto; line-height: 1; // Tight line height for numbers color: var(--colors-cyan-60v, #007a99); font-size: 2rem; } - .cdc-callout__icon { - color: var(--colors-cyan-60v, #007a99); - box-sizing: border-box; + .cdc-callout__flag { position: absolute; - font-family: var(--icons-cdc); - top: -0.65rem; - right: 1rem; - font-size: 2rem; + top: -0.36rem; + right: 1.08rem; + width: 1.84rem; + height: auto; } .cdc-callout__content { @@ -194,8 +194,7 @@ // White background with border (when both white background and Display Border are checked) &.white-background-style.display-border { .cdc-callout { - border: none !important; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); + box-shadow: 0 2px 4px rgb(159 159 159 / 10%); } } } diff --git a/packages/map/src/components/Legend/components/index.scss b/packages/map/src/components/Legend/components/index.scss index 4b8fad175b..0bdec647a0 100644 --- a/packages/map/src/components/Legend/components/index.scss +++ b/packages/map/src/components/Legend/components/index.scss @@ -126,6 +126,9 @@ &--pattern { cursor: default; } + &:focus { + outline: none; + } } .legend-container__ul:not(.single-row, .legend-container__ul--single-column) { diff --git a/packages/map/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx b/packages/map/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx index 2d22918226..59d515d61f 100644 --- a/packages/map/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +++ b/packages/map/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx @@ -30,8 +30,8 @@ const StateOutput: React.FC = ({ topoData, path, scale, runtim return stateLines.map((line, index) => ( diff --git a/packages/map/src/components/UsaMap/components/UsaMap.County.tsx b/packages/map/src/components/UsaMap/components/UsaMap.County.tsx index c1d98ccedc..f090ed65b9 100644 --- a/packages/map/src/components/UsaMap/components/UsaMap.County.tsx +++ b/packages/map/src/components/UsaMap/components/UsaMap.County.tsx @@ -15,7 +15,7 @@ import useGeoClickHandler from '../../../hooks/useGeoClickHandler' import { applyLegendToRow } from '../../../helpers/applyLegendToRow' import useApplyTooltipsToGeo from '../../../hooks/useApplyTooltipsToGeo' import { MapConfig } from '../../../types/MapConfig' -import { DEFAULT_MAP_BACKGROUND } from '../../../helpers/constants' +import { DEFAULT_MAP_BACKGROUND, DISABLED_MAP_COLOR } from '../../../helpers/constants' import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers' import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils' @@ -708,7 +708,7 @@ const CountyMap = () => { ? applyLegendToRow(runtimeData[key], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo) : false if (legendValues) { - if (legendValues?.[0] === '#000000' || legendValues?.[0] === DEFAULT_MAP_BACKGROUND) return + if (legendValues?.[0] === '#000000' || legendValues?.[0] === DISABLED_MAP_COLOR) return const shapeType = config.visual.cityStyle.toLowerCase() const shapeProperties = createShapeProperties(shapeType, pixelCoords, legendValues, config, geoRadius) if (shapeProperties) { diff --git a/packages/map/src/helpers/applyLegendToRow.ts b/packages/map/src/helpers/applyLegendToRow.ts index 5ca9b20ccf..c46f0e7ec3 100644 --- a/packages/map/src/helpers/applyLegendToRow.ts +++ b/packages/map/src/helpers/applyLegendToRow.ts @@ -43,6 +43,7 @@ export const applyLegendToRow = ( const idx = legendMemo.current.get(hash)! const disabledIdx = showSpecialClassesLast ? legendSpecialClassLastMemo.current.get(hash) ?? idx : idx + // Note: DISABLED_MAP_COLOR is used in UsaMap.County.tsx to check for hidden bubbles. Should be refactored to use the hidden value when that is implemented. if (runtimeLegend.items?.[disabledIdx]?.disabled || runtimeLegend.items?.[disabledIdx]?.hidden) { return generateColorsArray(DISABLED_MAP_COLOR) } diff --git a/packages/markup-include/src/CdcMarkupInclude.tsx b/packages/markup-include/src/CdcMarkupInclude.tsx index 4ee9698b1f..796148a6ac 100644 --- a/packages/markup-include/src/CdcMarkupInclude.tsx +++ b/packages/markup-include/src/CdcMarkupInclude.tsx @@ -202,6 +202,66 @@ const CdcMarkupInclude: React.FC = ({ return hasBody ? parse[1] : parse } + /** + * Transforms HTML by extracting