Skip to content
21 changes: 6 additions & 15 deletions src/components/Charts/BarChart/VerticalBarChartContent.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import ActivityIndicator from '@components/ActivityIndicator';
import {BAR_CORNER_RADIUS, BAR_GROUP_INNER_GAP, BAR_HIT_GAP_RATIO, VERTICAL_BAR_DOMAIN_PADDING} from '@components/Charts/barChartConstants';
import ChartGridLines from '@components/Charts/components/ChartGridLines';
import ChartTooltipLayer from '@components/Charts/components/ChartTooltipLayer';
Expand All @@ -17,7 +16,8 @@ import {
} from '@components/Charts/hooks';
import type {ChartSeries, WrappedLabelConfig} from '@components/Charts/types';
import {getBarLayout, getCartesianPlotBounds, getXAxisLabel, getYAxisLabelWidth} from '@components/Charts/utils';
import VictoryTheme, {CARTESIAN_Y_AXIS, CHART_CONTENT_MIN_HEIGHT, GLYPH_PADDING, MAX_BAR_LABEL_LINES} from '@components/Charts/VictoryTheme';
import {getBarChartHeight, getXAxisLabelSpace} from '@components/Charts/utils/chartHeights';
import VictoryTheme, {CARTESIAN_Y_AXIS, GLYPH_PADDING, MAX_BAR_LABEL_LINES} from '@components/Charts/VictoryTheme';

import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
Expand All @@ -28,7 +28,6 @@ import variables from '@styles/variables';
import type {TupleToUnion} from 'type-fest';
import type {CartesianChartRenderArg, ChartBounds, PointsArray, Scale} from 'victory-native';

import {View} from 'react-native';
import {GestureDetector} from 'react-native-gesture-handler';
import Animated, {useAnimatedStyle, useSharedValue} from 'react-native-reanimated';
import {Bar, CartesianChart} from 'victory-native';
Expand Down Expand Up @@ -115,6 +114,7 @@ function VerticalBarChartCanvas({
canvasWidth: chartWidth,
}
: undefined;
// `getBarChartLoadingHeight` reserves the same lines while the chart loads, so update the two together.
const wrapLabelHeight = wrapConfig ? wrapConfig.maxLines * wrapConfig.lineHeight : 0;
// When wrapping, labelAreaWidth is 0 above so the hook returns EMPTY_LAYOUT (horizontal, skip 1).
// xAxisLabelHeight is 0 there too, so the wrap height has to be substituted explicitly.
Expand Down Expand Up @@ -242,8 +242,8 @@ function VerticalBarChartCanvas({
);
};

const labelSpace = shouldShowLabels ? VictoryTheme.axis.xAxisLabelGap + resolvedLabelHeight : 0;
const chartHeight = CHART_CONTENT_MIN_HEIGHT + labelSpace;
const labelSpace = shouldShowLabels ? getXAxisLabelSpace(resolvedLabelHeight) : 0;
const chartHeight = getBarChartHeight(shouldShowLabels, resolvedLabelHeight);
const chartPadding = {...VictoryTheme.axis.padding, bottom: labelSpace + VictoryTheme.axis.padding.bottom, right: chartPaddingRight};

return (
Expand Down Expand Up @@ -306,19 +306,10 @@ function VerticalBarChartCanvas({
}

function VerticalBarChartContentBody({isLoading = false, chartWidth, ...canvasProps}: BarChartContentProps) {
const styles = useThemeStyles();
const readyCanvas = useReadyCartesianCanvas(isLoading, chartWidth);

if (!isLoading && canvasProps.data.length === 0) {
return null;
}

if (!readyCanvas) {
return (
<View style={styles.chartActivityIndicator}>
<ActivityIndicator size="large" />
</View>
);
return null;
}

return (
Expand Down
11 changes: 8 additions & 3 deletions src/components/Charts/BarChart/index.native.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ChartWidthBox from '@components/Charts/components/ChartWidthBox';
import ChartFrame from '@components/Charts/components/ChartFrame';
import {getBarChartLoadingHeight} from '@components/Charts/utils/chartHeights';

import React from 'react';

Expand All @@ -8,14 +9,18 @@ import BarChartContent from './BarChartContent';

function BarChart(props: BarChartProps) {
return (
<ChartWidthBox>
<ChartFrame
isLoading={props.isLoading}
hasData={props.data.length > 0}
getLoadingHeight={() => getBarChartLoadingHeight(props.shouldShowLabels, props.shouldWrapLabels)}
>
{(chartWidth) => (
<BarChartContent
{...props}
chartWidth={chartWidth}
/>
)}
</ChartWidthBox>
</ChartFrame>
);
}

Expand Down
12 changes: 9 additions & 3 deletions src/components/Charts/BarChart/index.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import ChartWidthBox from '@components/Charts/components/ChartWidthBox';
import ChartFrame from '@components/Charts/components/ChartFrame';
import SkiaWebChart from '@components/Charts/SkiaWebChart';
import {getBarChartLoadingHeight} from '@components/Charts/utils/chartHeights';

import React from 'react';

import type BarChartProps from './types';

const getBarChartContent = () => import('./BarChartContent');

function BarChart(props: BarChartProps) {
return (
<ChartWidthBox>
<ChartFrame
isLoading={props.isLoading}
hasData={props.data.length > 0}
getLoadingHeight={() => getBarChartLoadingHeight(props.shouldShowLabels, props.shouldWrapLabels)}
>
{(chartWidth) => (
<SkiaWebChart
getComponent={getBarChartContent}
componentProps={{...props, chartWidth}}
/>
)}
</ChartWidthBox>
</ChartFrame>
);
}

Expand Down
29 changes: 5 additions & 24 deletions src/components/Charts/LineChart/LineChartContent.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import ActivityIndicator from '@components/ActivityIndicator';
import ActivePointIndicator from '@components/Charts/components/ActivePointIndicator';
import AreaGradient from '@components/Charts/components/AreaGradient';
import ChartGridLines from '@components/Charts/components/ChartGridLines';
Expand All @@ -18,17 +17,9 @@ import {
useReadyCartesianCanvas,
} from '@components/Charts/hooks';
import {getAxisLabelStep, getCartesianPlotBounds, getDomainPaddingForEdgeSpace, getLineRuns, getXAxisLabel, getYAxisLabelWidth, labelOverhang} from '@components/Charts/utils';
import {getCartesianChartHeight, getXAxisLabelSpace} from '@components/Charts/utils/chartHeights';
import type {LineRun} from '@components/Charts/utils/getLineRuns';
import VictoryTheme, {
CARTESIAN_Y_AXIS,
CHART_CONTENT_MIN_HEIGHT,
DASH_INTERVALS,
GLYPH_PADDING,
LABEL_PADDING,
LABEL_ROTATIONS,
SIN_45,
X_AXIS_LABEL_MIN_GAP,
} from '@components/Charts/VictoryTheme';
import VictoryTheme, {CARTESIAN_Y_AXIS, DASH_INTERVALS, GLYPH_PADDING, LABEL_PADDING, LABEL_ROTATIONS, SIN_45, X_AXIS_LABEL_MIN_GAP} from '@components/Charts/VictoryTheme';

import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
Expand All @@ -41,7 +32,6 @@ import type {CartesianChartRenderArg, ChartBounds, Scale} from 'victory-native';

import {DashPathEffect} from '@shopify/react-native-skia';
import React from 'react';
import {View} from 'react-native';
import {GestureDetector} from 'react-native-gesture-handler';
import Animated, {useAnimatedStyle, useDerivedValue, useSharedValue} from 'react-native-reanimated';
import {CartesianChart, Line} from 'victory-native';
Expand Down Expand Up @@ -186,7 +176,7 @@ function LineChartCanvas({data, series, yAxisUnit, yAxisUnitPosition = 'left', o
plotRight.set(bounds.right);
};

const labelSpace = VictoryTheme.axis.xAxisLabelGap + (xAxisLabelHeight ?? 0);
const labelSpace = getXAxisLabelSpace(xAxisLabelHeight);

const isInPlotArea = (args: HitTestArgs) => {
'worklet';
Expand Down Expand Up @@ -332,7 +322,7 @@ function LineChartCanvas({data, series, yAxisUnit, yAxisUnitPosition = 'left', o
);
};

const chartHeight = CHART_CONTENT_MIN_HEIGHT + labelSpace;
const chartHeight = getCartesianChartHeight(xAxisLabelHeight);
const chartPadding = {
...VictoryTheme.axis.padding,
bottom: labelSpace + VictoryTheme.axis.padding.bottom,
Expand Down Expand Up @@ -394,19 +384,10 @@ function LineChartCanvas({data, series, yAxisUnit, yAxisUnitPosition = 'left', o
}

function LineChartContentBody({isLoading = false, chartWidth, ...canvasProps}: LineChartContentProps) {
const styles = useThemeStyles();
const readyCanvas = useReadyCartesianCanvas(isLoading, chartWidth);

if (!isLoading && canvasProps.data.length === 0) {
return null;
}

if (!readyCanvas) {
return (
<View style={styles.chartActivityIndicator}>
<ActivityIndicator size="large" />
</View>
);
return null;
}

return (
Expand Down
11 changes: 8 additions & 3 deletions src/components/Charts/LineChart/index.native.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ChartWidthBox from '@components/Charts/components/ChartWidthBox';
import ChartFrame from '@components/Charts/components/ChartFrame';
import {getCartesianChartHeight} from '@components/Charts/utils/chartHeights';

import React from 'react';

Expand All @@ -8,14 +9,18 @@ import LineChartContent from './LineChartContent';

function LineChart(props: LineChartProps) {
return (
<ChartWidthBox>
<ChartFrame
isLoading={props.isLoading}
hasData={props.data.length > 0}
getLoadingHeight={() => getCartesianChartHeight()}
>
{(chartWidth) => (
<LineChartContent
{...props}
chartWidth={chartWidth}
/>
)}
</ChartWidthBox>
</ChartFrame>
);
}

Expand Down
12 changes: 9 additions & 3 deletions src/components/Charts/LineChart/index.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import ChartWidthBox from '@components/Charts/components/ChartWidthBox';
import ChartFrame from '@components/Charts/components/ChartFrame';
import SkiaWebChart from '@components/Charts/SkiaWebChart';
import {getCartesianChartHeight} from '@components/Charts/utils/chartHeights';

import React from 'react';

import type {LineChartProps} from './LineChartContent';

const getLineChartContent = () => import('./LineChartContent');

function LineChart(props: LineChartProps) {
return (
<ChartWidthBox>
<ChartFrame
isLoading={props.isLoading}
hasData={props.data.length > 0}
getLoadingHeight={() => getCartesianChartHeight()}
>
{(chartWidth) => (
<SkiaWebChart
getComponent={getLineChartContent}
componentProps={{...props, chartWidth}}
/>
)}
</ChartWidthBox>
</ChartFrame>
);
}

Expand Down
19 changes: 7 additions & 12 deletions src/components/Charts/PieChart/PieChartContent.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import ActivityIndicator from '@components/ActivityIndicator';
import {useReportChartLoading} from '@components/Charts/components/ChartReveal';
import ChartTooltipLayer from '@components/Charts/components/ChartTooltipLayer';
import {CHART_GESTURE_TOUCH_ACTION, TOOLTIP_BAR_GAP, useChartLabelFormats, useTouchPinnedTooltip} from '@components/Charts/hooks';
import type {ChartDataPoint, ChartProps, PieSlice, UnitPosition} from '@components/Charts/types';
import {findSliceAtPosition, getPrimaryValue, processDataIntoSlices} from '@components/Charts/utils';
import {hasRoomForSideLegend} from '@components/Charts/utils/chartHeights';
import VictoryTheme, {CHART_CONTENT_MIN_HEIGHT} from '@components/Charts/VictoryTheme';
import Text from '@components/Text';

Expand Down Expand Up @@ -65,7 +66,7 @@ function PieChartCanvas({data, series, valueUnit, valueUnitPosition, onSlicePres
const canvasHeight = CHART_CONTENT_MIN_HEIGHT;

// Side by side when the legend keeps at least its minimum width next to a square donut. Otherwise stacked.
const isLegendSideBySide = shouldUseResponsiveLegend && shouldShowLegend && chartWidth - canvasHeight - VictoryTheme.pie.legendGap >= VictoryTheme.pie.legendMinWidth;
const isLegendSideBySide = shouldUseResponsiveLegend && shouldShowLegend && hasRoomForSideLegend(chartWidth);
const donutWidth = isLegendSideBySide ? canvasHeight : chartWidth;

const radius = Math.min(donutWidth, canvasHeight) / 2;
Expand Down Expand Up @@ -206,6 +207,7 @@ function PieChartCanvas({data, series, valueUnit, valueUnitPosition, onSlicePres

// One legend entry: a color dot plus the slice name, wired to drive the tooltip on hover.
// The responsive variant gives each slice its own row with the dot aligned to the first line. The default variant is an inline item for the wrapping row below the donut.
// `getPieChartLoadingHeight` reserves the wrapping row as one line of normal-size text, so the label has to keep that size.
const renderLegendEntry = (slice: PieSlice, isResponsiveRow: boolean) => {
const dot = <View style={[styles.pieChartLegendDot, StyleUtils.getBackgroundColorStyle(slice.color)]} />;
return (
Expand Down Expand Up @@ -284,6 +286,7 @@ function PieChartCanvas({data, series, valueUnit, valueUnitPosition, onSlicePres
</GestureDetector>
);

// `getPieChartLoadingHeight` rebuilds the height of both layouts below from the same theme values, so update it with any change to their structure or spacing.
if (shouldUseResponsiveLegend) {
return (
<View style={isLegendSideBySide ? styles.pieChartResponsiveRow : styles.pieChartResponsiveColumn}>
Expand All @@ -304,18 +307,10 @@ function PieChartCanvas({data, series, valueUnit, valueUnitPosition, onSlicePres
}

function PieChartContent({isLoading = false, chartWidth, ...canvasProps}: PieChartContentProps) {
const styles = useThemeStyles();

if (!isLoading && canvasProps.data.length === 0) {
return null;
}
useReportChartLoading(isLoading);

if (isLoading || chartWidth === null) {
return (
<View style={styles.chartActivityIndicator}>
<ActivityIndicator size="large" />
</View>
);
return null;
}

return (
Expand Down
15 changes: 12 additions & 3 deletions src/components/Charts/PieChart/index.native.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import ChartWidthBox from '@components/Charts/components/ChartWidthBox';
import ChartFrame from '@components/Charts/components/ChartFrame';
import {getPieChartLoadingHeight} from '@components/Charts/utils/chartHeights';

import useResponsiveLayout from '@hooks/useResponsiveLayout';

import React from 'react';

Expand All @@ -7,15 +10,21 @@ import type {PieChartProps} from './PieChartContent';
import PieChartContent from './PieChartContent';

function PieChart(props: PieChartProps) {
const {shouldUseNarrowLayout} = useResponsiveLayout();

return (
<ChartWidthBox>
<ChartFrame
isLoading={props.isLoading}
hasData={props.data.length > 0}
getLoadingHeight={(chartWidth) => getPieChartLoadingHeight(chartWidth, shouldUseNarrowLayout, props.shouldShowLegend, props.shouldUseResponsiveLegend)}
>
{(chartWidth) => (
<PieChartContent
{...props}
chartWidth={chartWidth}
/>
)}
</ChartWidthBox>
</ChartFrame>
);
}

Expand Down
15 changes: 12 additions & 3 deletions src/components/Charts/PieChart/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import ChartWidthBox from '@components/Charts/components/ChartWidthBox';
import ChartFrame from '@components/Charts/components/ChartFrame';
import SkiaWebChart from '@components/Charts/SkiaWebChart';
import {getPieChartLoadingHeight} from '@components/Charts/utils/chartHeights';

import useResponsiveLayout from '@hooks/useResponsiveLayout';

import React from 'react';

Expand All @@ -8,15 +11,21 @@ import type {PieChartProps} from './PieChartContent';
const getPieChartContent = () => import('./PieChartContent');

function PieChart(props: PieChartProps) {
const {shouldUseNarrowLayout} = useResponsiveLayout();

return (
<ChartWidthBox>
<ChartFrame
isLoading={props.isLoading}
hasData={props.data.length > 0}
getLoadingHeight={(chartWidth) => getPieChartLoadingHeight(chartWidth, shouldUseNarrowLayout, props.shouldShowLegend, props.shouldUseResponsiveLegend)}
>
{(chartWidth) => (
<SkiaWebChart
getComponent={getPieChartContent}
componentProps={{...props, chartWidth}}
/>
)}
</ChartWidthBox>
</ChartFrame>
);
}

Expand Down
Loading
Loading