diff --git a/.changeset/dark-trams-fry.md b/.changeset/dark-trams-fry.md new file mode 100644 index 0000000000..5643a8e695 --- /dev/null +++ b/.changeset/dark-trams-fry.md @@ -0,0 +1,5 @@ +--- +'@lg-charts/core': minor +--- + +Small values including zero will show at least a thin bar (1px line minimum) on Bar charts for visibility. Bars with zero values are rendered with 30% opacity to visually differentiate them from bars with small non-zero values. diff --git a/charts/core/src/BarChart.stories.tsx b/charts/core/src/BarChart.stories.tsx index 0d0afb6072..290f292491 100644 --- a/charts/core/src/BarChart.stories.tsx +++ b/charts/core/src/BarChart.stories.tsx @@ -208,3 +208,69 @@ export const BarWithCategoryAxisLabel: StoryObj<{ ); }, }; + +export const BarWithMinimumHeight: StoryObj<{ + barMinHeight: number; +}> = { + args: { + barMinHeight: 1, + }, + argTypes: { + barMinHeight: { + control: { type: 'number', min: 0, max: 10, step: 1 }, + description: 'Minimum height of bars in pixels', + }, + }, + render: ({ barMinHeight }) => { + // Create data with extreme value differences to demonstrate the feature + const extremeValueData = [ + { + name: 'Large Values', + data: [ + [new Date('2024-01-01').getTime(), 1000000000], + [new Date('2024-01-02').getTime(), 950000000], + [new Date('2024-01-03').getTime(), 1100000000], + [new Date('2024-01-04').getTime(), 1050000000], + ] as Array<[number, number]>, + }, + { + name: 'Small Values', + data: [ + [new Date('2024-01-01').getTime(), 100], + [new Date('2024-01-02').getTime(), 50], + [new Date('2024-01-03').getTime(), 200], + [new Date('2024-01-04').getTime(), 0], + ] as Array<[number, number]>, + }, + { + name: 'Medium Values', + data: [ + [new Date('2024-01-01').getTime(), 500000], + [new Date('2024-01-02').getTime(), 450000], + [new Date('2024-01-03').getTime(), 550000], + [new Date('2024-01-04').getTime(), 10], + ] as Array<[number, number]>, + }, + ]; + + return ( + + { + const date = new Date(value); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + }); + }} + /> + + + {extremeValueData.map(({ name, data }) => ( + + ))} + + ); + }, +}; diff --git a/charts/core/src/ChartTooltip/ChartTooltip.types.ts b/charts/core/src/ChartTooltip/ChartTooltip.types.ts index ebe2591a91..b93e7335b6 100644 --- a/charts/core/src/ChartTooltip/ChartTooltip.types.ts +++ b/charts/core/src/ChartTooltip/ChartTooltip.types.ts @@ -8,6 +8,41 @@ import { type CallbackDataParams } from 'echarts/types/dist/shared'; */ export type OptionDataValue = string | number | Date; +/** + * Data can be in array format [x, y] or object format { value: [x, y], itemStyle: {...} } + * when individual data points have custom styling (e.g., zero values with reduced opacity). + */ +export type SeriesDataItem = + | Array + | { value: Array; itemStyle?: Record }; + +/** + * Helper function to extract the data array from either format. + * ECharts passes data differently depending on whether it's a simple array or an object with itemStyle. + */ +export function getDataArray( + data: SeriesDataItem | undefined, +): Array | undefined { + if (!data) return undefined; + + // Check if data is an object with a 'value' property (object format) + if ( + typeof data === 'object' && + !Array.isArray(data) && + 'value' in data && + Array.isArray(data.value) + ) { + return data.value; + } + + // Otherwise, assume it's already an array + if (Array.isArray(data)) { + return data; + } + + return undefined; +} + export const AxisPointerType = { None: 'none', Line: 'line', @@ -53,7 +88,11 @@ export interface CallbackSeriesDataPoint extends CallbackDataParams { axisType: string; axisValue: string | number; axisValueLabel: string | number; - data: Array; + /** + * Data can be in array format [x, y] or object format { value: [x, y], itemStyle: {...} } + * Use getDataArray() helper to extract the array from either format. + */ + data: SeriesDataItem; /** * Echarts returns a custom color type which doesn't map to string but is one */ diff --git a/charts/core/src/ChartTooltip/CustomTooltip/CustomTooltip.tsx b/charts/core/src/ChartTooltip/CustomTooltip/CustomTooltip.tsx index 204ade29c3..773c1bc570 100644 --- a/charts/core/src/ChartTooltip/CustomTooltip/CustomTooltip.tsx +++ b/charts/core/src/ChartTooltip/CustomTooltip/CustomTooltip.tsx @@ -6,6 +6,8 @@ import { IconButton } from '@leafygreen-ui/icon-button'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; import { isDefined } from '@leafygreen-ui/lib'; +import { getDataArray } from '../ChartTooltip.types'; + import { closeButtonStyles, getHeaderStyles, @@ -43,7 +45,10 @@ export function CustomTooltip({ }: CustomTooltipProps) { const { theme } = useDarkMode(darkMode); - if (seriesData.length === 0 || !isDefined(seriesData[0].data[0])) { + // Extract data array from either format (array or object with value property) + const firstDataArray = getDataArray(seriesData[0]?.data); + + if (seriesData.length === 0 || !isDefined(firstDataArray?.[0])) { return null; } diff --git a/charts/core/src/ChartTooltip/CustomTooltip/SeriesList/SeriesList.tsx b/charts/core/src/ChartTooltip/CustomTooltip/SeriesList/SeriesList.tsx index 1bb9738377..37775bd402 100644 --- a/charts/core/src/ChartTooltip/CustomTooltip/SeriesList/SeriesList.tsx +++ b/charts/core/src/ChartTooltip/CustomTooltip/SeriesList/SeriesList.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { SeriesName } from '@lg-charts/series-provider'; -import { OptionDataValue } from '../../ChartTooltip.types'; +import { getDataArray, OptionDataValue } from '../../ChartTooltip.types'; import { SeriesListItem } from '../SeriesListItem'; import { getSeriesListStyles } from './SeriesList.styles'; @@ -32,8 +32,11 @@ export function SeriesList({
    {seriesData .sort((a, b) => { - const [nameA, valueA] = a.data; - const [nameB, valueB] = b.data; + // Extract data arrays from either format (array or object with value property) + const dataArrayA = getDataArray(a.data) || []; + const dataArrayB = getDataArray(b.data) || []; + const [nameA, valueA] = dataArrayA; + const [nameB, valueB] = dataArrayB; if (sort) { return sort( @@ -44,16 +47,20 @@ export function SeriesList({ return descendingCompareFn(valueA, valueB); }) - .map(({ seriesName, data, color }) => ( - - ))} + .map(({ seriesName, data, color }) => { + // Extract data array from either format + const dataArray = getDataArray(data) || []; + return ( + + ); + })}
); } diff --git a/charts/core/src/ChartTooltip/CustomTooltip/SeriesListItem/SeriesListItem.types.ts b/charts/core/src/ChartTooltip/CustomTooltip/SeriesListItem/SeriesListItem.types.ts index 94f54cee66..f1100cdffb 100644 --- a/charts/core/src/ChartTooltip/CustomTooltip/SeriesListItem/SeriesListItem.types.ts +++ b/charts/core/src/ChartTooltip/CustomTooltip/SeriesListItem/SeriesListItem.types.ts @@ -1,4 +1,7 @@ -import { CallbackSeriesDataPoint } from '../../ChartTooltip.types'; +import { + CallbackSeriesDataPoint, + OptionDataValue, +} from '../../ChartTooltip.types'; import { SeriesListProps } from '../SeriesList/SeriesList.types'; export interface SeriesListItemProps @@ -7,6 +10,7 @@ export interface SeriesListItemProps 'seriesValueFormatter' | 'seriesNameFormatter' > { seriesName?: CallbackSeriesDataPoint['seriesName']; - data: CallbackSeriesDataPoint['data']; + /** Data array in [x, y] format (already extracted from object format if needed) */ + data: Array; color: CallbackSeriesDataPoint['color']; } diff --git a/charts/core/src/Series/Bar/Bar.tsx b/charts/core/src/Series/Bar/Bar.tsx index 4138f248f9..2d987b16db 100644 --- a/charts/core/src/Series/Bar/Bar.tsx +++ b/charts/core/src/Series/Bar/Bar.tsx @@ -45,12 +45,33 @@ export const Bar = ({ stack, hoverBehavior = BarHoverBehavior.None, }: BarProps) => { + // Transform data to apply opacity to zero values only + const transformedData = React.useMemo(() => { + return data.map(([x, y]) => { + const value = y as number; + + // Apply 30% opacity (0.3) to zero values to differentiate from 1px minimum height + if (value === 0) { + return { + value: [x, y], + itemStyle: { + opacity: 0.3, + }, + }; + } + + // Keep non-zero values in array format for tooltip compatibility + return [x, y]; + }); + }, [data]); + const options = useCallback< (stylingContext: StylingContext) => EChartSeriesOptions['bar']['options'] >( stylingContext => ({ clip: false, stack, + barMinHeight: 1, emphasis: { focus: getEmphasisFocus(hoverBehavior), }, @@ -61,7 +82,9 @@ export const Bar = ({ [stack, hoverBehavior], ); - return ; + return ( + + ); }; Bar.displayName = 'Bar';