Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,10 @@ const CHART_TYPE = {
POLAR: 'polar',
} as const;

export {X_KEY, Y_KEY_PREFIX, LABEL_KEY, VALUE_KEY, COLOR_KEY, CHART_TYPE};
/** Gap left between a left-axis label and the chart's outer edge when `padding.left` is shrunk to fit the label content. */
const LEFT_AXIS_LABEL_PADDING = 32;

/** Max gap between a left-axis label and the chart it labels, clamping the XML-provided `tickLabels.padding` when that value is larger. */
const LEFT_AXIS_LABEL_OFFSET_MAX = 2;

export {X_KEY, Y_KEY_PREFIX, LABEL_KEY, VALUE_KEY, COLOR_KEY, CHART_TYPE, LEFT_AXIS_LABEL_PADDING, LEFT_AXIS_LABEL_OFFSET_MAX};
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {X_KEY} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants';
import type {ProcessNodeResult} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types';
import resolvePadding from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/resolvePadding';

import type {SkTypeface} from '@shopify/react-native-skia';
import type {TNode} from 'react-native-render-html';
Expand All @@ -20,6 +21,7 @@ function processVictoryChartTree(tnode: TNode, typeface: SkTypeface | null, root
let domain: ProcessNodeResult['domain'];
let domainPadding: ProcessNodeResult['domainPadding'];
let padding: ProcessNodeResult['padding'];
let leftAxisLabelPadding: ProcessNodeResult['leftAxisLabelPadding'];
let isHorizontal: ProcessNodeResult['isHorizontal'];
let categories: ProcessNodeResult['categories'];
const labelItems: ProcessNodeResult['labelItems'] = [];
Expand Down Expand Up @@ -49,6 +51,9 @@ function processVictoryChartTree(tnode: TNode, typeface: SkTypeface | null, root
if (result.padding) {
padding = result.padding;
}
if (result.leftAxisLabelPadding !== undefined) {
leftAxisLabelPadding = result.leftAxisLabelPadding;
}
if (result.isHorizontal) {
isHorizontal = result.isHorizontal;
}
Expand All @@ -64,7 +69,21 @@ function processVictoryChartTree(tnode: TNode, typeface: SkTypeface | null, root
}

// If we have `rootProcessedResult` then forward it as is, otherwise we must be the root so pass the data that we just built
const rootProcessedNodeResult = rootProcessedResult ?? {data, xKey: X_KEY, yKeys, xAxis, yAxis, domain, domainPadding, padding, isHorizontal, categories, labelItems, legendItems};
const rootProcessedNodeResult = rootProcessedResult ?? {
data,
xKey: X_KEY,
yKeys,
xAxis,
yAxis,
domain,
domainPadding,
padding,
leftAxisLabelPadding,
isHorizontal,
categories,
labelItems,
legendItems,
};

for (const child of tnode.children) {
const childResult = processVictoryChartTree(child, typeface, rootProcessedNodeResult);
Expand All @@ -85,6 +104,9 @@ function processVictoryChartTree(tnode: TNode, typeface: SkTypeface | null, root
if (childResult.padding) {
padding = childResult.padding;
}
if (childResult.leftAxisLabelPadding !== undefined) {
leftAxisLabelPadding = childResult.leftAxisLabelPadding;
}
Comment on lines +107 to +109

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the widest left-axis label measurement

When a horizontal chart supplies multiple left-side y-axis configurations, this assignment overwrites the earlier measurement with the last axis encountered. yAxis is intentionally accumulated as an array in the same traversal, so a later axis with shorter labels can make resolvePadding() shrink padding.left below the width required by an earlier axis, clipping that axis's labels. Retain the maximum measured width instead of the last value.

Useful? React with 👍 / 👎.

if (childResult.isHorizontal) {
isHorizontal = childResult.isHorizontal;
}
Expand All @@ -95,7 +117,21 @@ function processVictoryChartTree(tnode: TNode, typeface: SkTypeface | null, root
legendItems.push(...childResult.legendItems);
}

return {data, xKey: X_KEY, yKeys, xAxis, yAxis, domain, domainPadding, padding, isHorizontal, categories, labelItems, legendItems};
return {
data,
xKey: X_KEY,
yKeys,
xAxis,
yAxis,
domain,
domainPadding,
padding: resolvePadding(padding, leftAxisLabelPadding),
leftAxisLabelPadding,
isHorizontal,
categories,
labelItems,
legendItems,
};
}

export default processVictoryChartTree;
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {LEFT_AXIS_LABEL_OFFSET_MAX} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants';
import type {PartialProcessNodeResult, ProcessNodeResult} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types';
import getFontGlyphWidth from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getFontGlyphWidth';
import {
parseAttributeAsNumber,
parseAttributeAsNumberArray,
Expand All @@ -7,11 +9,24 @@ import {
} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute';
import parseRawAxisStyle from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseRawAxisStyle';

import type {SkTypeface} from '@shopify/react-native-skia';
import type {SkFont, SkTypeface} from '@shopify/react-native-skia';
import type {TNode} from 'react-native-render-html';

import {Skia} from '@shopify/react-native-skia';

/**
* The widest rendered label on a left-side y-axis, plus its offset from the axis line —
* i.e. how much horizontal space the axis actually needs for its labels. Used to shrink
* the chart's `padding.left` when the configured value is more than the content needs.
*/
function computeLeftAxisLabelPadding(axisSide: 'left' | 'right', labels: Array<string | number> | undefined, font: SkFont | null, labelOffset: number | undefined): number | undefined {
if (axisSide !== 'left' || !font || !labels?.length) {
return undefined;
}
const maxLabelWidth = Math.max(...labels.map((label) => getFontGlyphWidth(String(label), font)));
return maxLabelWidth + (labelOffset ?? 0);
}

/**
* Parse axis config from a `<victoryaxis>` node.
* Dependent axes become yAxis entries; independent axes become the xAxis.
Expand All @@ -34,6 +49,7 @@ function parseVictoryAxisNode(tnode: TNode, typeface: SkTypeface | null, rootPro
const labelOffset = style?.tickLabels?.padding !== undefined ? Number(style.tickLabels.padding) : undefined;
const fontSize = style?.tickLabels?.fontSize !== undefined ? Number(style.tickLabels.fontSize) : undefined;
const font = typeface && fontSize ? Skia.Font(typeface, fontSize) : null;
const labelsForMeasurement = tickValues?.map(formatLabel) ?? tickFormat;

if (isDependentAxis) {
return isHorizontal
Expand Down Expand Up @@ -66,35 +82,39 @@ function parseVictoryAxisNode(tnode: TNode, typeface: SkTypeface | null, rootPro
],
};
}
return isHorizontal
? {
yAxis: [
{
tickCount,
tickValues,
formatYLabel: formatLabel,
axisSide: orientation === 'top' ? 'right' : 'left',
lineColor,
lineWidth,
labelColor,
labelOffset,
font,
},
],
}
: {
xAxis: {
tickCount,
tickValues,
formatXLabel: formatLabel,
axisSide: orientation === 'top' ? 'top' : 'bottom',
lineColor,
lineWidth,
labelColor,
labelOffset,
font,
},
};
if (isHorizontal) {
const axisSide = orientation === 'top' ? 'right' : 'left';
const resolvedLabelOffset = axisSide === 'left' && labelOffset !== undefined ? Math.min(labelOffset, LEFT_AXIS_LABEL_OFFSET_MAX) : labelOffset;
return {
yAxis: [
{
tickCount,
tickValues,
formatYLabel: formatLabel,
axisSide,
lineColor,
lineWidth,
labelColor,
labelOffset: resolvedLabelOffset,
font,
},
],
leftAxisLabelPadding: computeLeftAxisLabelPadding(axisSide, labelsForMeasurement, font, resolvedLabelOffset),
};
}
return {
xAxis: {
tickCount,
tickValues,
formatXLabel: formatLabel,
axisSide: orientation === 'top' ? 'top' : 'bottom',
lineColor,
lineWidth,
labelColor,
labelOffset,
font,
},
};
}

export default parseVictoryAxisNode;
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ type ProcessNodeResult = {
domain: CartesianChartProps['domain'];
domainPadding: CartesianChartProps['domainPadding'];
padding: CartesianChartProps['padding'];
// Pixel width the left y-axis actually needs for its widest label, used to shrink `padding.left` when it's larger than the content requires.
leftAxisLabelPadding: number | undefined;
isHorizontal: boolean | undefined;
categories: string[] | undefined;
labelItems: LabelItem[];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type {SkFont} from '@shopify/react-native-skia';

/**
* Sums glyph advance widths for `text`. Mirrors victory-native's internal label-width
* calculation so measurements line up with what `CartesianAxis` actually renders.
*/
function getFontGlyphWidth(text: string, font: SkFont | null): number {
if (!font) {
return 0;
}
return font.getGlyphWidths(font.getGlyphIDs(text)).reduce((sum, width) => sum + width, 0);
}

export default getFontGlyphWidth;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {LEFT_AXIS_LABEL_PADDING} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants';
import type {ProcessNodeResult} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types';

import lodashIsObject from 'lodash/isObject';

/**
* Shrinks `padding.left` down to the left axis's measured label width (plus a small edge buffer)
* whenever the configured padding is wider than the label content actually needs.
*/
function resolvePadding(padding: ProcessNodeResult['padding'], leftAxisLabelPadding: ProcessNodeResult['leftAxisLabelPadding']): ProcessNodeResult['padding'] {
if (leftAxisLabelPadding === undefined || !lodashIsObject(padding) || typeof padding.left !== 'number') {
return padding;
}
return {...padding, left: Math.min(padding.left, Math.ceil(leftAxisLabelPadding) + LEFT_AXIS_LABEL_PADDING)};
}

export default resolvePadding;
Loading