Skip to content

Commit 39db014

Browse files
authored
Merge pull request #91895 from s77rt/polar-charts-2
[Payment due @situchan] Victory Native: Support polar charts
2 parents ed5d6ff + 68bc360 commit 39db014

20 files changed

Lines changed: 444 additions & 117 deletions

cspell.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@
168168
"Idology",
169169
"Inactives",
170170
"Inclusivity",
171+
"innerradius",
171172
"Intacct",
172173
"Invoicify",
173174
"Italiano",
@@ -495,6 +496,7 @@
495496
"codegen",
496497
"codeshare",
497498
"codesign",
499+
"colorscale",
498500
"commentbubbles",
499501
"contenteditable",
500502
"copiloted",
@@ -656,7 +658,9 @@
656658
"killall",
657659
"kilometre",
658660
"kilometres",
661+
"labelcomponent",
659662
"labelledby",
663+
"labelradius",
660664
"laggy",
661665
"lastiPhoneLogin",
662666
"lastname",
@@ -665,6 +669,7 @@
665669
"lightningcss",
666670
"linecap",
667671
"linejoin",
672+
"lineheight",
668673
"lintable",
669674
"lintrk",
670675
"listformat",
@@ -868,6 +873,7 @@
868873
"tabindex",
869874
"teachersunite",
870875
"testflight",
876+
"textanchor",
871877
"threadsafe",
872878
"thumbsup",
873879
"tickcount",
@@ -910,13 +916,15 @@
910916
"useMemo",
911917
"usernotifications",
912918
"utilise",
919+
"verticalanchor",
913920
"victoryaxis",
914921
"victorybar",
915922
"victorychart",
916923
"victorygroup",
917924
"victorylabel",
918925
"victorylegend",
919926
"victoryline",
927+
"victorypie",
920928
"viewability",
921929
"viewport",
922930
"viewports",

src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim
228228
tagName: 'victorygroup',
229229
contentModel: HTMLContentModel.block,
230230
}),
231+
victorypie: HTMLElementModel.fromCustomModel({
232+
tagName: 'victorypie',
233+
contentModel: HTMLContentModel.block,
234+
}),
231235
}),
232236
[
233237
styles.taskTitleMenuItem,

src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import React from 'react';
22
import {CartesianChart} from 'victory-native';
33
import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext';
44
import {VictoryChartRenderArgsProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext';
5-
import getYKey from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey';
6-
import VictoryChartLabels from './VictoryChartLabels';
5+
import getHierarchyID from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getHierarchyID';
6+
import VictoryChartLabel from './VictoryChartLabel';
77
import VictoryChartLegend from './VictoryChartLegend';
88
import VictoryChartSeries from './VictoryChartSeries';
99

@@ -26,16 +26,26 @@ function VictoryChartCartesian() {
2626
padding={padding}
2727
renderOutside={(renderArgs) => (
2828
<VictoryChartRenderArgsProvider value={renderArgs}>
29-
<VictoryChartLabels labelItems={labelItems} />
30-
<VictoryChartLegend legendItems={legendItems} />
29+
{labelItems.map((labelItem) => (
30+
<VictoryChartLabel
31+
key={`label-${labelItem.x}-${labelItem.y}`}
32+
{...labelItem}
33+
/>
34+
))}
35+
{legendItems.map((legendItem) => (
36+
<VictoryChartLegend
37+
key={`legend-${legendItem.x}-${legendItem.y}`}
38+
{...legendItem}
39+
/>
40+
))}
3141
</VictoryChartRenderArgsProvider>
3242
)}
3343
>
3444
{(renderArgs) => (
3545
<VictoryChartRenderArgsProvider value={renderArgs}>
3646
{tnode.children.map((child) => (
3747
<VictoryChartSeries
38-
key={`${child.tagName ?? 'node'}-${getYKey(child)}`}
48+
key={`${child.tagName ?? 'node'}-${getHierarchyID(child)}`}
3949
tnode={child}
4050
isHorizontal={isHorizontal}
4151
/>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from 'react';
2+
import type {TNode} from 'react-native-render-html';
3+
import VictoryChartPie from './VictoryChartPie';
4+
5+
type VictoryChartCategoriesProps = {tnode: TNode};
6+
7+
type CategoriesComponent = (props: VictoryChartCategoriesProps) => React.ReactElement | null;
8+
9+
/**
10+
* Dispatches a chart child node to its categories renderer based on the HTML tag name.
11+
* To support a new categories type, add its tag name here and create the renderer component.
12+
*/
13+
const CATEGORIES_RENDERERS: Partial<Record<string, CategoriesComponent>> = {
14+
victorypie: VictoryChartPie,
15+
};
16+
17+
function VictoryChartCategories({tnode}: VictoryChartCategoriesProps) {
18+
const CategoriesRenderer = CATEGORIES_RENDERERS[tnode.tagName ?? ''];
19+
20+
if (!CategoriesRenderer) {
21+
return null;
22+
}
23+
24+
return <CategoriesRenderer tnode={tnode} />;
25+
}
26+
27+
VictoryChartCategories.displayName = 'VictoryChartCategories';
28+
29+
export default VictoryChartCategories;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {Skia, Text as SkText} from '@shopify/react-native-skia';
2+
import type {Color, SkFont} from '@shopify/react-native-skia';
3+
import React from 'react';
4+
import {useChartDefaultTypeface} from '@components/Charts/hooks';
5+
import type {LabelItem} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types';
6+
import computeTextAnchorPosition from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/computeTextAnchorPosition';
7+
8+
type VictoryChartLabelsProps = LabelItem;
9+
10+
type ProcessedLine = {
11+
lineX: number;
12+
lineY: number;
13+
line: string;
14+
lineFont: SkFont | null;
15+
lineColor: Color | undefined;
16+
lineWidth: number;
17+
};
18+
19+
/**
20+
* Renders floating Skia text labels (from `<victorylabel>` nodes) over the chart canvas.
21+
* Intended for use inside CartesianChart's `renderOutside` callback.
22+
*/
23+
function VictoryChartLabel({x, y, text, color, fontSize, fontWeight, lineHeight, textAnchor = 'start', verticalAnchor = 'start'}: VictoryChartLabelsProps) {
24+
const {regular: regularTypeface, bold: boldTypeface} = useChartDefaultTypeface();
25+
const processedLines = text.split('\n').reduce(
26+
(acc, line, index) => {
27+
const lineColor = color?.[index];
28+
const lineFontSize = fontSize?.[index];
29+
const lineFontWeight = fontWeight?.[index];
30+
const lineLineHeight = lineHeight?.[index];
31+
const typeface = lineFontWeight === 'bold' ? boldTypeface : regularTypeface;
32+
const lineFont = typeface && lineFontSize ? Skia.Font(typeface, lineFontSize) : null;
33+
const fontMetrics = lineFont?.getMetrics();
34+
const lineWidth = lineFont?.getGlyphWidths(lineFont.getGlyphIDs(line)).reduce((totalWidth, width) => totalWidth + width, 0) ?? 0;
35+
const customLineHeight = lineLineHeight ? lineLineHeight * (lineFontSize ?? 0) : 0;
36+
const metricsLineHeight = fontMetrics ? -fontMetrics.ascent + fontMetrics.descent + fontMetrics.leading : 0;
37+
const lineX = x;
38+
const lineY = acc.y - (fontMetrics?.ascent ?? 0);
39+
acc.y += customLineHeight || metricsLineHeight;
40+
41+
acc.lines.push({
42+
lineX,
43+
lineY,
44+
line,
45+
lineFont,
46+
lineColor,
47+
lineWidth,
48+
});
49+
return acc;
50+
},
51+
{lines: [] as ProcessedLine[], y},
52+
);
53+
return processedLines.lines.map(({lineX, lineY, line, lineFont, lineColor, lineWidth}) => {
54+
return (
55+
<SkText
56+
key={`text-${lineX}-${lineY}`}
57+
x={computeTextAnchorPosition(lineX, lineWidth, textAnchor)}
58+
y={computeTextAnchorPosition(lineY, processedLines.y - y, verticalAnchor)}
59+
text={line}
60+
font={lineFont}
61+
color={lineColor}
62+
/>
63+
);
64+
});
65+
}
66+
67+
VictoryChartLabel.displayName = 'VictoryChartLabel';
68+
69+
export default VictoryChartLabel;

src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx

Lines changed: 0 additions & 38 deletions
This file was deleted.

src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx

Lines changed: 62 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,78 @@
11
import {Circle, Skia, Text as SkText} from '@shopify/react-native-skia';
2+
import type {Color, SkFont} from '@shopify/react-native-skia';
23
import React, {Fragment} from 'react';
34
import {useChartDefaultTypeface} from '@components/Charts/hooks';
45
import type {LegendItem} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types';
56

6-
type VictoryChartLegendProps = {
7-
legendItems: LegendItem[];
7+
type VictoryChartLegendProps = LegendItem;
8+
9+
type ProcessedEntry = {
10+
symbolX: number;
11+
symbolY: number;
12+
symbolSize: number | undefined;
13+
symbolColor: Color | undefined;
14+
textX: number;
15+
textY: number;
16+
text: string;
17+
font: SkFont | null;
18+
color: Color | undefined;
819
};
920

1021
/**
1122
* Renders Skia legend symbols and labels (from `<victorylegend>` nodes) over the chart canvas.
1223
* Intended for use inside CartesianChart's `renderOutside` callback.
1324
*/
14-
function VictoryChartLegend({legendItems}: VictoryChartLegendProps) {
25+
function VictoryChartLegend({x, y, entries, gutter, symbolSpacer}: VictoryChartLegendProps) {
1526
const {regular: regularTypeface, bold: boldTypeface} = useChartDefaultTypeface();
16-
return (
17-
<>
18-
{legendItems.map(({x: startX, y, entries, gutter, symbolSpacer}) => {
19-
let x = startX;
20-
return entries.map(({text, color, fontSize, fontWeight, symbolColor, symbolSize}) => {
21-
const typeface = fontWeight === 'bold' ? boldTypeface : regularTypeface;
22-
const font = typeface ? Skia.Font(typeface, fontSize) : null;
23-
const fontMetrics = font?.getMetrics();
24-
const lineHeight = fontMetrics ? fontMetrics.ascent + fontMetrics.descent + fontMetrics.leading : 0;
25-
const symbolX = x;
26-
x += (symbolSize ?? 0) + (symbolSpacer ?? 0);
27-
const textX = x;
28-
x += (font?.getGlyphWidths(font.getGlyphIDs(text)).reduce((acc, width) => acc + width, 0) ?? 0) + (gutter ?? 0);
29-
return (
30-
<Fragment key={`legend-${x}-${y}`}>
31-
{!!symbolSize && (
32-
<Circle
33-
cx={symbolX}
34-
cy={y}
35-
r={symbolSize}
36-
color={symbolColor}
37-
/>
38-
)}
39-
<SkText
40-
x={textX}
41-
y={y - lineHeight / 2}
42-
text={text}
43-
font={font}
44-
color={color}
45-
/>
46-
</Fragment>
47-
);
48-
});
49-
})}
50-
</>
27+
const processedEntries = entries.reduce(
28+
(acc, {text, color, fontSize, fontWeight, symbolColor, symbolSize}) => {
29+
const typeface = fontWeight === 'bold' ? boldTypeface : regularTypeface;
30+
const font = typeface && fontSize ? Skia.Font(typeface, fontSize) : null;
31+
const fontMetrics = font?.getMetrics();
32+
const lineHeight = fontMetrics ? fontMetrics.ascent + fontMetrics.descent + fontMetrics.leading : 0;
33+
const symbolX = acc.x;
34+
const symbolY = y;
35+
acc.x += (symbolSize ?? 0) + (symbolSpacer ?? 0);
36+
const textX = acc.x;
37+
const textY = y - lineHeight / 2;
38+
acc.x += (font?.getGlyphWidths(font.getGlyphIDs(text)).reduce((totalWidth, width) => totalWidth + width, 0) ?? 0) + (gutter ?? 0);
39+
40+
acc.entries.push({
41+
symbolX,
42+
symbolY,
43+
symbolSize,
44+
symbolColor,
45+
textX,
46+
textY,
47+
text,
48+
font,
49+
color,
50+
});
51+
return acc;
52+
},
53+
{entries: [] as ProcessedEntry[], x},
5154
);
55+
return processedEntries.entries.map(({symbolX, symbolY, symbolSize, symbolColor, textX, textY, text, font, color}) => {
56+
return (
57+
<Fragment key={`legend-${textX}-${textY}`}>
58+
{!!symbolSize && (
59+
<Circle
60+
cx={symbolX}
61+
cy={symbolY}
62+
r={symbolSize}
63+
color={symbolColor}
64+
/>
65+
)}
66+
<SkText
67+
x={textX}
68+
y={textY}
69+
text={text}
70+
font={font}
71+
color={color}
72+
/>
73+
</Fragment>
74+
);
75+
});
5276
}
5377

5478
VictoryChartLegend.displayName = 'VictoryChartLegend';

0 commit comments

Comments
 (0)