Skip to content
Closed
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
2 changes: 1 addition & 1 deletion src/components/Charts/VictoryTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import colors from '@styles/theme/colors';

/** Font families used by all chart label components (Paragraph API multi-font fallback). */
const CHART_FONT_FAMILIES = ['ExpensifyNeue', 'NotoSansSymbols', 'NotoSansSCMonths'];
const CHART_FONT_FAMILIES = ['ExpensifyNeue', 'ExpensifyNewKansas', 'NotoSansSymbols', 'NotoSansSCMonths'];

/**
* Expensify Chart Color Palette.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ function useChartFontManager(): SkTypefaceFontProvider | null {
require('@assets/fonts/native/ExpensifyNeue-Italic.otf') as DataModule,
require('@assets/fonts/native/ExpensifyNeue-BoldItalic.otf') as DataModule,
],
ExpensifyNewKansas: [require('@assets/fonts/native/ExpensifyNewKansas-Medium.otf') as DataModule, require('@assets/fonts/native/ExpensifyNewKansas-MediumItalic.otf') as DataModule],
NotoSansSymbols: [require('@assets/fonts/NotoSans-Symbols.ttf') as DataModule],
NotoSansSCMonths: [require('@assets/fonts/NotoSansSC-Months.ttf') as DataModule],
});
Expand All @@ -17,7 +18,10 @@ function useChartFontManager(): SkTypefaceFontProvider | null {
function useChartDefaultTypeface() {
const regular = useTypeface(require('@assets/fonts/native/ExpensifyNeue-Regular.otf') as DataModule);
const bold = useTypeface(require('@assets/fonts/native/ExpensifyNeue-Bold.otf') as DataModule);
return {regular, bold};
const newKansas = useTypeface(require('@assets/fonts/native/ExpensifyNewKansas-Medium.otf') as DataModule);
const newKansasItalic = useTypeface(require('@assets/fonts/native/ExpensifyNewKansas-MediumItalic.otf') as DataModule);

return {regular, bold, newKansas, newKansasItalic};
}

export {useChartDefaultTypeface};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ function useChartFontManager(): SkTypefaceFontProvider | null {
webFont(require('@assets/fonts/web/ExpensifyNeue-Italic.woff2') as string),
webFont(require('@assets/fonts/web/ExpensifyNeue-BoldItalic.woff2') as string),
],
ExpensifyNewKansas: [
webFont(require('@assets/fonts/web/ExpensifyNewKansas-Medium.woff2') as string),
webFont(require('@assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff2') as string),
],
NotoSansSymbols: [webFont(require('@assets/fonts/NotoSans-Symbols.ttf') as string)],
NotoSansSCMonths: [webFont(require('@assets/fonts/NotoSansSC-Months.ttf') as string)],
});
Expand All @@ -25,7 +29,10 @@ function useChartFontManager(): SkTypefaceFontProvider | null {
function useChartDefaultTypeface() {
const regular = useTypeface(webFont(require('@assets/fonts/web/ExpensifyNeue-Regular.woff2') as string));
const bold = useTypeface(webFont(require('@assets/fonts/web/ExpensifyNeue-Bold.woff2') as string));
return {regular, bold};
const newKansas = useTypeface(webFont(require('@assets/fonts/web/ExpensifyNewKansas-Medium.woff2') as string));
const newKansasItalic = useTypeface(webFont(require('@assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff2') as string));

return {regular, bold, newKansas, newKansasItalic};
}

export {useChartDefaultTypeface};
Expand Down
17 changes: 15 additions & 2 deletions src/components/Charts/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {SkParagraph} from '@shopify/react-native-skia';
import type {SkParagraph, SkTypeface} from '@shopify/react-native-skia';
import type {ValueOf} from 'type-fest';
import type {LABEL_ROTATIONS} from './VictoryTheme';

Expand Down Expand Up @@ -71,4 +71,17 @@ type LabelRotation = ValueOf<typeof LABEL_ROTATIONS>;

type ParagraphWithWidth = {para: SkParagraph | null; width: number};

export type {ChartDataPoint, ChartProps, CartesianChartProps, LabelRotation, ParagraphWithWidth, PieSlice, UnitPosition, UnitWithFallback};
type ChartLabelFontStyle = {
fontFamily?: string;
fontWeight?: string | number;
fontStyle?: string;
};

type ChartTypefaces = {
regular: SkTypeface | null;
bold: SkTypeface | null;
newKansas: SkTypeface | null;
newKansasItalic: SkTypeface | null;
};

export type {ChartDataPoint, ChartProps, CartesianChartProps, ChartLabelFontStyle, ChartTypefaces, LabelRotation, ParagraphWithWidth, PieSlice, UnitPosition, UnitWithFallback};
60 changes: 60 additions & 0 deletions src/components/Charts/utils/getChartLabelTypeface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type {SkTypeface} from '@shopify/react-native-skia';
import type {ChartLabelFontStyle, ChartTypefaces} from '@components/Charts/types';

/**
* Normalizes Victory/HTML font family names to Skia-registered family keys.
*/
function normalizeChartFontFamily(fontFamily?: string): 'ExpensifyNeue' | 'ExpensifyNewKansas' | undefined {
if (!fontFamily) {
return undefined;
}

const normalized = fontFamily.replace(/\s+/g, '').toLowerCase();

Check failure on line 12 in src/components/Charts/utils/getChartLabelTypeface.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Prefer `String#replaceAll()` over `String#replace()`

if (normalized === 'expensifynewkansas') {
return 'ExpensifyNewKansas';
}

if (normalized === 'expensifyneue') {
return 'ExpensifyNeue';
}

return undefined;
}

/**
* Returns true when a label style should use the bold chart typeface.
*/
function isBoldFontWeight(fontWeight?: string | number): boolean {
if (fontWeight === 'bold') {
return true;
}

const numericWeight = Number(fontWeight);
return Number.isFinite(numericWeight) && numericWeight >= 700;
}

/**
* Resolves the Skia typeface for a chart label style.
*/
function getChartLabelTypeface(typefaces: ChartTypefaces, style?: ChartLabelFontStyle): SkTypeface | null {
const fontFamily = normalizeChartFontFamily(style?.fontFamily);
const isBold = isBoldFontWeight(style?.fontWeight);
const isItalic = style?.fontStyle === 'italic';

if (fontFamily === 'ExpensifyNewKansas') {
if (isItalic) {
return typefaces.newKansasItalic ?? typefaces.newKansas ?? typefaces.regular;
}

return typefaces.newKansas ?? typefaces.regular;
}

if (isBold) {
return typefaces.bold ?? typefaces.regular;
}

return typefaces.regular;
}

export {getChartLabelTypeface, isBoldFontWeight, normalizeChartFontFamily};
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,10 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim
tagName: 'victorygroup',
contentModel: HTMLContentModel.block,
}),
victorypie: HTMLElementModel.fromCustomModel({
tagName: 'victorypie',
contentModel: HTMLContentModel.block,
}),
}),
[
styles.taskTitleMenuItem,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,19 @@
import {Skia, Text as SkText} from '@shopify/react-native-skia';
import React from 'react';

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

View workflow job for this annotation

GitHub Actions / ESLint check

'React' is defined but never used
import {useChartDefaultTypeface} from '@components/Charts/hooks';
import type {LabelItem} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types';
import renderVictoryChartLabelElements from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/renderVictoryChartLabelElements';

type VictoryChartLabelsProps = {
labelItems: LabelItem[];
};

/**
* Renders floating Skia text labels (from `<victorylabel>` nodes) over the chart canvas.
* Intended for use inside CartesianChart's `renderOutside` callback.
*/
function VictoryChartLabels({labelItems}: VictoryChartLabelsProps) {
const {regular: regularTypeface, bold: boldTypeface} = useChartDefaultTypeface();
return (
<>
{labelItems.map(({x, y, text, color, fontSize, fontWeight}) => {
const typeface = fontWeight === 'bold' ? boldTypeface : regularTypeface;
const font = typeface ? Skia.Font(typeface, fontSize) : null;
return (
<SkText
key={`text-${x}-${y}`}
x={x}
y={y}
text={text}
font={font}
color={color}
/>
);
})}
</>
);
const typefaces = useChartDefaultTypeface();

return renderVictoryChartLabelElements({labelItems, typefaces});
}

VictoryChartLabels.displayName = 'VictoryChartLabels';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,101 @@
import {useEffect} from 'react';
import Log from '@libs/Log';
import {Canvas, Group} from '@shopify/react-native-skia';
import React, {useCallback, useMemo, useState} from 'react';
import type {LayoutChangeEvent} from 'react-native';
import {StyleSheet, View} from 'react-native';
import {Pie, PolarChart} from 'victory-native';
import {useChartDefaultTypeface} from '@components/Charts/hooks';
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 {getPieChartPadAngleLayout} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getPieChartPadAngleLayout';
import renderPieSliceAngularInsetElements from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/renderPieSliceAngularInsetElements';
import renderAllPieSliceLabelElements from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/renderPieSliceLabelElements';
import renderVictoryChartLabelElements from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/renderVictoryChartLabelElements';

const styles = StyleSheet.create({
container: {
flex: 1,
},
chart: {
flex: 1,
},
labelOverlay: StyleSheet.absoluteFill,
});

/**
* Renders the PolarChart with data drawn from context.
* Renders the PolarChart with pie data drawn from context.
*/
function VictoryChartPolar() {
useEffect(() => Log.warn('Trying to render unsupported polar charts'), []);
const {polarData, pieConfig, labelItems, chartContentStyles} = useVictoryChartContext();
const typefaces = useChartDefaultTypeface();
const [canvasSize, setCanvasSize] = useState(() => ({
width: typeof chartContentStyles.width === 'number' ? chartContentStyles.width : 0,
height: typeof chartContentStyles.height === 'number' ? chartContentStyles.height : 0,
}));

const handleLayout = useCallback((event: LayoutChangeEvent) => {
const {width, height} = event.nativeEvent.layout;
setCanvasSize({width, height});
}, []);

const overlayLabelElements = useMemo(() => {
if (!pieConfig || polarData.length === 0 || canvasSize.width <= 0 || canvasSize.height <= 0) {
return [];
}

const floatingLabels = renderVictoryChartLabelElements({labelItems, typefaces});
const sliceLabels = renderAllPieSliceLabelElements({
polarData,
pieConfig,
canvasWidth: canvasSize.width,
canvasHeight: canvasSize.height,
typefaces,
});
const angularInsets = renderPieSliceAngularInsetElements({
polarData,
pieConfig,
canvasWidth: canvasSize.width,
canvasHeight: canvasSize.height,
});

return [...floatingLabels, ...sliceLabels, ...angularInsets];
}, [canvasSize.height, canvasSize.width, labelItems, pieConfig, polarData, typefaces]);

if (!pieConfig || polarData.length === 0) {
return null;
}

const pieSize = pieConfig.radius ? pieConfig.radius * 2 : undefined;
const {circleSweepDegrees, startAngle} = getPieChartPadAngleLayout(pieConfig.padAngle, polarData.length);

// Support for polar chars will be added in a follow up https://github.com/Expensify/App/issues/90546
return null;
return (
<View
style={styles.container}
onLayout={handleLayout}
>
<PolarChart
data={polarData}
labelKey={POLAR_LABEL_KEY}
valueKey={POLAR_VALUE_KEY}
colorKey={POLAR_COLOR_KEY}
containerStyle={styles.chart}
>
<Pie.Chart
innerRadius={pieConfig.innerRadius}
size={pieSize}
circleSweepDegrees={circleSweepDegrees}
startAngle={startAngle}
/>
</PolarChart>
{canvasSize.width > 0 && canvasSize.height > 0 && (
<Canvas
style={styles.labelOverlay}
pointerEvents="none"
>
<Group>{overlayLabelElements}</Group>
</Canvas>
)}
</View>
);
}

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 = 'label';
const POLAR_VALUE_KEY = 'y';
const POLAR_COLOR_KEY = 'color';

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 @@ -13,6 +13,8 @@ type VictoryChartContextValue = {
yKeys: ProcessNodeResult['yKeys'];
xAxis: ProcessNodeResult['xAxis'];
yAxis: ProcessNodeResult['yAxis'];
polarData: ProcessNodeResult['polarData'];
pieConfig: ProcessNodeResult['pieConfig'];
labelItems: ProcessNodeResult['labelItems'];
legendItems: ProcessNodeResult['legendItems'];
chartContentStyles: ReturnType<typeof parseStyles>['nodeStyles'];
Expand All @@ -28,11 +30,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, polarData, pieConfig, labelItems, legendItems} = processVictoryChartTree(tnode, regularTypeface);
const {nodeStyles: chartContentStyles, parentNodeStyles: chartContainerStyles} = parseStyles(tnode);

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

// XNOR Check. There must be one and only one valid chart
Expand All @@ -55,6 +57,8 @@ function VictoryChartProvider({tnode, children}: {tnode: TNode; children: React.
yKeys,
xAxis,
yAxis,
polarData,
pieConfig,
labelItems,
legendItems,
chartContentStyles,
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;
Loading
Loading