Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@
const {regular: regularTypeface, bold: boldTypeface} = useChartDefaultTypeface();
return (
<>
{labelItems.map(({x, y, text, color, fontSize, fontWeight}) => {
{labelItems.map(({x, y, text, color, fontSize, fontWeight, textAnchor}, i) => {
const typeface = fontWeight === 'bold' ? boldTypeface : regularTypeface;
const font = typeface ? Skia.Font(typeface, fontSize) : null;
let drawX = x;
if (font && textAnchor !== 'start') {
const textWidth = font.getTextWidth(text);

Check failure on line 23 in src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

`getTextWidth` is deprecated. Use measureText or getGlyphWidths instead

Check failure on line 23 in src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

`getTextWidth` is deprecated. Use measureText or getGlyphWidths instead
drawX = textAnchor === 'middle' ? x - textWidth / 2 : x - textWidth;
}
return (
<SkText
key={`text-${x}-${y}`}
x={x}
key={`text-${x}-${y}-${i}`}

Check failure on line 28 in src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

Do not use Array index in keys

Check failure on line 28 in src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

Do not use Array index in keys
x={drawX}
y={y}
text={text}
font={font}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
import {useEffect} from 'react';
import Log from '@libs/Log';
import React from 'react';
import {Pie, PolarChart} from 'victory-native';
import {POLAR_COLOR_KEY, POLAR_LABEL_KEY, POLAR_VALUE_KEY} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants';
import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext';
import VictoryChartLabels from './VictoryChartLabels';
import VictoryChartLegend from './VictoryChartLegend';

/**
* Renders the PolarChart with data drawn from context.
* Renders the PolarChart (pie) with data, labels, and legend drawn from context.
*/
function VictoryChartPolar() {
useEffect(() => Log.warn('Trying to render unsupported polar charts'), []);
const {polarConfig, labelItems, legendItems} = useVictoryChartContext();

// Support for polar chars will be added in a follow up https://github.com/Expensify/App/issues/90546
return null;
if (!polarConfig) {
return null;
}

return (
<PolarChart
data={polarConfig.data}
labelKey={POLAR_LABEL_KEY}
valueKey={POLAR_VALUE_KEY}
colorKey={POLAR_COLOR_KEY}
>
<Pie.Chart
innerRadius={polarConfig.innerRadius}
size={polarConfig.outerRadius !== undefined ? polarConfig.outerRadius * 2 : undefined}
startAngle={polarConfig.startAngle}
circleSweepDegrees={polarConfig.circleSweepDegrees}
>
{() => <Pie.Slice />}
</Pie.Chart>
<VictoryChartLabels labelItems={labelItems} />
<VictoryChartLegend legendItems={legendItems} />
</PolarChart>
);
}

VictoryChartPolar.displayName = 'VictoryChartPolar';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
const X_KEY = 'x';
const Y_KEY_PREFIX = 'y';

const POLAR_LABEL_KEY = 'polarLabel' as const;
const POLAR_VALUE_KEY = 'polarValue' as const;
const POLAR_COLOR_KEY = 'polarColor' as const;

const CHART_TYPE = {
CARTESIAN: 'cartesian',
POLAR: 'polar',
} as const;

export {X_KEY, Y_KEY_PREFIX, CHART_TYPE};
export {X_KEY, Y_KEY_PREFIX, POLAR_LABEL_KEY, POLAR_VALUE_KEY, POLAR_COLOR_KEY, CHART_TYPE};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {TNode} from 'react-native-render-html';
import {useChartDefaultTypeface} from '@components/Charts/hooks';
import {CHART_TYPE} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants';
import processVictoryChartTree from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/processVictoryChartTree';
import type {ChartType, ProcessNodeResult} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types';
import type {ChartType, PolarConfig, ProcessNodeResult} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types';
import parseStyles from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseStyles';

type VictoryChartContextValue = {
Expand All @@ -15,6 +15,7 @@ type VictoryChartContextValue = {
yAxis: ProcessNodeResult['yAxis'];
labelItems: ProcessNodeResult['labelItems'];
legendItems: ProcessNodeResult['legendItems'];
polarConfig: PolarConfig | undefined;
chartContentStyles: ReturnType<typeof parseStyles>['nodeStyles'];
chartContainerStyles: ReturnType<typeof parseStyles>['parentNodeStyles'];
type: ChartType | null;
Expand All @@ -28,11 +29,11 @@ const VictoryChartContext = createContext<VictoryChartContextValue | null>(null)
*/
function VictoryChartProvider({tnode, children}: {tnode: TNode; children: React.ReactNode}) {
const {regular: regularTypeface} = useChartDefaultTypeface();
const {data, xKey, yKeys, xAxis, yAxis, labelItems, legendItems} = processVictoryChartTree(tnode, regularTypeface);
const {data, xKey, yKeys, xAxis, yAxis, labelItems, legendItems, polarConfig} = processVictoryChartTree(tnode, regularTypeface);
const {nodeStyles: chartContentStyles, parentNodeStyles: chartContainerStyles} = parseStyles(tnode);

const hasCartesianData = Object.keys(data).length > 0;
const hasPolarData = false;
const hasPolarData = (polarConfig?.data.length ?? 0) > 0;
let type: ChartType | null = null;

// XNOR Check. There must be one and only one valid chart
Expand All @@ -57,6 +58,7 @@ function VictoryChartProvider({tnode, children}: {tnode: TNode; children: React.
yAxis,
labelItems,
legendItems,
polarConfig,
chartContentStyles,
chartContainerStyles,
type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {NodeParser} from '@components/HTMLEngineProvider/HTMLRenderers/Vict
import parseVictoryAxisNode from './victoryAxisParser';
import parseVictoryLabelNode from './victoryLabelParser';
import parseVictoryLegendNode from './victoryLegendParser';
import parseVictoryPieNode from './victoryPieParser';
import parseVictorySeriesNode from './victorySeriesParser';

/**
Expand All @@ -14,6 +15,7 @@ const PARSER_REGISTRY: Partial<Record<string, NodeParser>> = {
victoryaxis: parseVictoryAxisNode,
victorylabel: parseVictoryLabelNode,
victorylegend: parseVictoryLegendNode,
victorypie: parseVictoryPieNode,
};

export default PARSER_REGISTRY;
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ function processVictoryChartTree(tnode: TNode, typeface: SkTypeface | null): Pro
let yAxis: ProcessNodeResult['yAxis'];
const labelItems: ProcessNodeResult['labelItems'] = [];
const legendItems: ProcessNodeResult['legendItems'] = [];
let polarConfig: ProcessNodeResult['polarConfig'];

const parser = PARSER_REGISTRY[tnode.tagName ?? ''];
if (parser) {
Expand All @@ -38,6 +39,9 @@ function processVictoryChartTree(tnode: TNode, typeface: SkTypeface | null): Pro
if (result.legendItems) {
legendItems.push(...result.legendItems);
}
if (result.polarConfig) {
polarConfig = result.polarConfig;
}
}

for (const child of tnode.children) {
Expand All @@ -52,9 +56,12 @@ function processVictoryChartTree(tnode: TNode, typeface: SkTypeface | null): Pro
}
labelItems.push(...childResult.labelItems);
legendItems.push(...childResult.legendItems);
if (childResult.polarConfig) {
polarConfig = childResult.polarConfig;
}
}

return {data, xKey: X_KEY, yKeys, xAxis, yAxis, labelItems, legendItems};
return {data, xKey: X_KEY, yKeys, xAxis, yAxis, labelItems, legendItems, polarConfig};
}

export default processVictoryChartTree;
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,60 @@ import type {TNode} from 'react-native-render-html';
import type {LabelItem, PartialProcessNodeResult, RawLabelStyle} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types';
import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute';

function extractLabelProps(style: RawLabelStyle | undefined): Pick<LabelItem, 'color' | 'fontSize' | 'fontWeight'> {
return {
color: style?.fill,
fontSize: style?.fontSize !== undefined ? Number(style.fontSize) : undefined,
fontWeight: Number(style?.fontWeight) === 700 ? 'bold' : undefined,
};
}

function parseTextAnchor(raw: string | undefined): LabelItem['textAnchor'] {
if (raw === 'middle' || raw === 'end') {
return raw;
}
return 'start';
}

/**
* Parse label config from a `<victorylabel>` node.
*/
function parseVictoryLabelNode(tnode: TNode): PartialProcessNodeResult {
const x = parseAttribute<number>(tnode.attributes.x) ?? 0;
const y = parseAttribute<number>(tnode.attributes.y) ?? 0;
const text = parseAttribute<string>(tnode.attributes.text) ?? '';
const style = parseAttribute<RawLabelStyle>(tnode.attributes.style);
const color = style?.fill;
const fontSize = style?.fontSize !== undefined ? Number(style.fontSize) : undefined;
const fontWeight = Number(style?.fontWeight) === 700 ? 'bold' : undefined;
const labelItem: LabelItem = {x, y, text, color, fontSize, fontWeight};
return {labelItems: [labelItem]};
const baseY = parseAttribute<number>(tnode.attributes.y) ?? 0;
const textAnchor = parseTextAnchor(tnode.attributes.textanchor);

const rawText = parseAttribute<string | string[]>(tnode.attributes.text);
const rawStyle = parseAttribute<RawLabelStyle | RawLabelStyle[]>(tnode.attributes.style);
const rawLineHeight = parseAttribute<number | number[]>(tnode.attributes.lineheight);

const texts = Array.isArray(rawText) ? rawText.map(String) : [String(rawText ?? '')];
const styles = Array.isArray(rawStyle) ? rawStyle : [rawStyle];
let lineHeights: number[];
if (Array.isArray(rawLineHeight)) {
lineHeights = rawLineHeight;
} else if (typeof rawLineHeight === 'number') {
lineHeights = [rawLineHeight];
} else {
lineHeights = [];
}

const labelItems: LabelItem[] = [];
let currentY = baseY;

for (let i = 0; i < texts.length; i++) {
const style = styles.at(i) ?? styles.at(0);
const props = extractLabelProps(style);
labelItems.push({x, y: currentY, text: texts.at(i) ?? '', textAnchor, ...props});

if (i < texts.length - 1) {
const lh = lineHeights.at(i) ?? 1.2;
const fs = props.fontSize ?? 13;
currentY += fs * lh;
}
}

return {labelItems};
}

export default parseVictoryLabelNode;
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type {TNode} from 'react-native-render-html';
import {getChartColor} from '@components/Charts/utils';
import {POLAR_COLOR_KEY, POLAR_LABEL_KEY, POLAR_VALUE_KEY} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants';
import type {PartialProcessNodeResult, PolarChartData, RawChartData} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types';
import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute';

/**
* Parse pie config from a `<victorypie>` node.
* Data points use the same {x, y} format as cartesian series; colorscale provides per-slice colors.
*/
function parseVictoryPieNode(tnode: TNode): PartialProcessNodeResult {
const points = parseAttribute<RawChartData[]>(tnode.attributes.data) ?? [];
const colorScale = parseAttribute<string[]>(tnode.attributes.colorscale) ?? [];
const innerRadius = parseAttribute<number>(tnode.attributes.innerradius);
const outerRadius = parseAttribute<number>(tnode.attributes.radius);
const startAngle = parseAttribute<number>(tnode.attributes.startangle);
const endAngle = parseAttribute<number>(tnode.attributes.endangle);

const circleSweepDegrees = endAngle !== undefined && startAngle !== undefined ? endAngle - startAngle : endAngle;

const data: PolarChartData[] = points.map((point, index) => ({
[POLAR_LABEL_KEY]: String(point.x),
[POLAR_VALUE_KEY]: point.y,
[POLAR_COLOR_KEY]: colorScale.at(index) ?? getChartColor(index),
}));

return {
polarConfig: {
data,
innerRadius,
outerRadius,
startAngle,
circleSweepDegrees,
},
};
}

export default parseVictoryPieNode;
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {ComponentProps} from 'react';
import type {CustomRendererProps, TBlock, TNode} from 'react-native-render-html';
import type {ValueOf} from 'type-fest';
import type {CartesianChart} from 'victory-native';
import type {CHART_TYPE, X_KEY, Y_KEY_PREFIX} from './constants';
import type {CHART_TYPE, POLAR_COLOR_KEY, POLAR_LABEL_KEY, POLAR_VALUE_KEY, X_KEY, Y_KEY_PREFIX} from './constants';

type VictoryChartRendererProps = CustomRendererProps<TBlock>;

Expand Down Expand Up @@ -49,6 +49,24 @@ type RawLegendStyle = {
type XKey = typeof X_KEY;
type YKey = `${typeof Y_KEY_PREFIX}${string}`;

type PolarLabelKey = typeof POLAR_LABEL_KEY;
type PolarValueKey = typeof POLAR_VALUE_KEY;
type PolarColorKey = typeof POLAR_COLOR_KEY;

type PolarChartData = {
polarLabel: string;
polarValue: number;
polarColor: string;
};

type PolarConfig = {
data: PolarChartData[];
innerRadius?: number;
outerRadius?: number;
startAngle?: number;
circleSweepDegrees?: number;
};

type CartesianChartData = {
[X_KEY]: string | number;
[key: `${YKey}`]: number;
Expand All @@ -72,6 +90,9 @@ type LabelItem = {

/** Font weight */
fontWeight?: 'normal' | 'bold';

/** Horizontal text alignment relative to x. 'middle' centers the text at x, 'end' right-aligns. Defaults to 'start'. */
textAnchor?: 'start' | 'middle' | 'end';
};

type LegendItemEntry = {
Expand Down Expand Up @@ -123,6 +144,7 @@ type ProcessNodeResult = {
yAxis: CartesianChartProps['yAxis'];
labelItems: LabelItem[];
legendItems: LegendItem[];
polarConfig?: PolarConfig;
};

/** Partial slice produced by a single per-tag parser before merging. */
Expand All @@ -141,6 +163,11 @@ export type {
RawLegendStyle,
XKey,
YKey,
PolarLabelKey,
PolarValueKey,
PolarColorKey,
PolarChartData,
PolarConfig,
CartesianChartData,
CartesianChartProps,
LabelItem,
Expand Down
Loading