Skip to content

Commit 0d53d18

Browse files
authored
Merge pull request #92130 from s77rt/polar-charts-missing-styles
Victory Native: Add padAngle + Label indicator
2 parents 470555d + 96f63d3 commit 0d53d18

13 files changed

Lines changed: 297 additions & 41 deletions

cspell.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,9 @@
661661
"kilometre",
662662
"kilometres",
663663
"labelcomponent",
664+
"labelindicator",
665+
"labelindicatorinneroffset",
666+
"labelindicatorouteroffset",
664667
"labelledby",
665668
"labelradius",
666669
"laggy",
@@ -758,6 +761,7 @@
758761
"osdk",
759762
"otpauth",
760763
"outplant",
764+
"padangle",
761765
"parasharrajat",
762766
"passcodes",
763767
"passplus",
@@ -841,6 +845,7 @@
841845
"sharees",
842846
"shellcheck",
843847
"shellenv",
848+
"shiftedlinesegment",
844849
"shipit",
845850
"shouldshowellipsis",
846851
"signingkey",

patches/victory-native/details.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,17 @@
99
```
1010
1111
- Upstream PR/issue: Not yet. This is urgent patch with deadline.
12-
- E/App issue: https://github.com/Expensify/Expensify/issues/636601
12+
- E/App issue: https://github.com/Expensify/App/issues/91883
1313
- PR introducing patch: https://github.com/Expensify/App/pull/91659
14+
15+
### [victory-native+41.21.0+002+fix-piesliceangularinset-bug.patch](victory-native+41.21.0+002+fix-piesliceangularinset-bug.patch)
16+
17+
- Reason:
18+
19+
```
20+
Fix bug https://github.com/FormidableLabs/victory-native-xl/issues/652
21+
```
22+
23+
- Upstream PR/issue: https://github.com/FormidableLabs/victory-native-xl/pull/666
24+
- E/App issue: https://github.com/Expensify/App/issues/92114
25+
- PR introducing patch: https://github.com/Expensify/App/pull/92130
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
diff --git a/node_modules/victory-native/dist/pie/hooks/useSliceAngularInsetPath.d.ts b/node_modules/victory-native/dist/pie/hooks/useSliceAngularInsetPath.d.ts
2+
index 18425b7..5508130 100644
3+
--- a/node_modules/victory-native/dist/pie/hooks/useSliceAngularInsetPath.d.ts
4+
+++ b/node_modules/victory-native/dist/pie/hooks/useSliceAngularInsetPath.d.ts
5+
@@ -4,5 +4,5 @@ type SliceAngularInsetPathArgs = {
6+
slice: PieSliceData;
7+
angularInset: PieSliceAngularInsetData;
8+
};
9+
-export declare const useSliceAngularInsetPath: ({ angularInset, slice, }: SliceAngularInsetPathArgs) => readonly [import("@shopify/react-native-skia").SkPath, import("@shopify/react-native-skia").SkPaint];
10+
+export declare const useSliceAngularInsetPath: ({ angularInset, slice, }: SliceAngularInsetPathArgs) => readonly [import("@shopify/react-native-skia").SkPath];
11+
export {};
12+
diff --git a/node_modules/victory-native/src/pie/PieSliceAngularInset.tsx b/node_modules/victory-native/src/pie/PieSliceAngularInset.tsx
13+
index bede090..0523900 100644
14+
--- a/node_modules/victory-native/src/pie/PieSliceAngularInset.tsx
15+
+++ b/node_modules/victory-native/src/pie/PieSliceAngularInset.tsx
16+
@@ -21,7 +21,7 @@ type PieSliceAngularInsetProps = {
17+
export const PieSliceAngularInset = (props: PieSliceAngularInsetProps) => {
18+
const { angularInset, children, animate, ...rest } = props;
19+
const { slice } = usePieSliceContext();
20+
- const [path, insetPaint] = useSliceAngularInsetPath({ slice, angularInset });
21+
+ const [path] = useSliceAngularInsetPath({ slice, angularInset });
22+
23+
// If the path is empty, don't render anything
24+
if (path.toSVGString() === "M0 0L0 0M0 0L0 0") {
25+
@@ -34,7 +34,7 @@ export const PieSliceAngularInset = (props: PieSliceAngularInsetProps) => {
26+
27+
const Component = animate ? AnimatedPath : Path;
28+
return (
29+
- <Component path={path} paint={insetPaint} animate={animate} {...rest}>
30+
+ <Component path={path} style="stroke" color={angularInset.angularStrokeColor} strokeWidth={angularInset.angularStrokeWidth} animate={animate} {...rest}>
31+
{children}
32+
</Component>
33+
);
34+
diff --git a/node_modules/victory-native/src/pie/hooks/useSliceAngularInsetPath.ts b/node_modules/victory-native/src/pie/hooks/useSliceAngularInsetPath.ts
35+
index 022dc49..2da6c64 100644
36+
--- a/node_modules/victory-native/src/pie/hooks/useSliceAngularInsetPath.ts
37+
+++ b/node_modules/victory-native/src/pie/hooks/useSliceAngularInsetPath.ts
38+
@@ -15,7 +15,7 @@ export const useSliceAngularInsetPath = ({
39+
angularInset,
40+
slice,
41+
}: SliceAngularInsetPathArgs) => {
42+
- const [path, paint] = useMemo(() => {
43+
+ const [path] = useMemo(() => {
44+
const { radius, center, innerRadius } = slice;
45+
46+
const path = Skia.Path.Make();
47+
@@ -72,14 +72,8 @@ export const useSliceAngularInsetPath = ({
48+
path.moveTo(center.x, center.y);
49+
path.lineTo(endPoint.x, endPoint.y);
50+
}
51+
-
52+
- // Create Paint for inset
53+
- const insetPaint = Skia.Paint();
54+
- insetPaint.setColor(Skia.Color(angularInset.angularStrokeColor));
55+
- insetPaint.setStyle(PaintStyle.Stroke);
56+
- insetPaint.setStrokeWidth(angularInset.angularStrokeWidth);
57+
- return [path, insetPaint] as const;
58+
+ return [path] as const;
59+
}, [slice, angularInset]);
60+
61+
- return [path, paint] as const;
62+
+ return [path] as const;
63+
};

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type ProcessedLine = {
2121
* Renders floating Skia text labels (from `<victorylabel>` nodes) over the chart canvas.
2222
* Intended for use inside CartesianChart's `renderOutside` callback.
2323
*/
24-
function VictoryChartLabel({x, y, text, color, fontSize, fontWeight, fontFamily, fontStyle, lineHeight, textAnchor = 'start', verticalAnchor = 'start'}: VictoryChartLabelsProps) {
24+
function VictoryChartLabel({x, y, text, color, fontSize, fontWeight, fontFamily, fontStyle, lineHeight, textAnchor = 'start', verticalAnchor = 'middle'}: VictoryChartLabelsProps) {
2525
const typefaces = useChartTypefaces();
2626
const processedLines = text.split('\n').reduce(
2727
(acc, line, index) => {

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

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import React from 'react';
2-
import {useAmbientTRenderEngine} from 'react-native-render-html';
32
import type {TNode} from 'react-native-render-html';
3+
import {HTMLContentModel, useAmbientTRenderEngine} from 'react-native-render-html';
44
import {Pie} from 'victory-native';
5+
import VictoryTheme from '@components/Charts/VictoryTheme';
6+
import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext';
7+
import parseShiftedLineSegmentNode from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/shiftedLineSegmentParser';
58
import parseVictoryLabelNode from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser';
9+
import type {PolarChartData} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types';
10+
import convertAngleToArcLength from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/convertAngleToArcLength';
611
import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute';
712
import parseComponent from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseComponent';
813
import VictoryChartPieLabel from './VictoryChartPieLabel';
@@ -12,14 +17,41 @@ type VictoryChartPieProps = {tnode: TNode};
1217
// Victory Chart's 0° angle is equivalent to 270° in Victory Native
1318
const START_ANGLE = 270;
1419

20+
/** Alternating multipliers for pie slice label distance (base `labelradius` from HTML). */
21+
const EVEN_SLICE_LABEL_RADIUS_FACTOR = 1;
22+
const ODD_SLICE_LABEL_RADIUS_FACTOR = 0.8;
23+
1524
function VictoryChartPie({tnode}: VictoryChartPieProps) {
25+
const {data, chartContainerStyles} = useVictoryChartContext();
1626
const renderEngine = useAmbientTRenderEngine();
17-
const labelComponentNode = parseComponent(tnode.attributes.labelcomponent, renderEngine, 'victorylabel');
18-
const labelItemTemplate = labelComponentNode ? parseVictoryLabelNode(labelComponentNode).labelItems?.at(0) : undefined;
19-
27+
const labelComponentNode = parseComponent(tnode.attributes.labelcomponent, renderEngine, 'victorylabel', HTMLContentModel.textual);
28+
const baseLabelItem = labelComponentNode ? parseVictoryLabelNode(labelComponentNode).labelItems?.at(0) : undefined;
29+
const pieLabels = parseAttribute<string[]>(tnode.attributes.labels);
30+
const labelRadius = tnode.attributes.labelradius !== undefined ? Number(parseAttribute(tnode.attributes.labelradius)) : undefined;
2031
const innerRadius = tnode.attributes.innerradius !== undefined ? Number(parseAttribute(tnode.attributes.innerradius)) : undefined;
32+
const padAngle = tnode.attributes.padangle !== undefined ? Number(parseAttribute(tnode.attributes.padangle)) : undefined;
2133
const radius = tnode.attributes.radius !== undefined ? Number(parseAttribute(tnode.attributes.radius)) : undefined;
2234
const size = radius ? radius * 2 : undefined;
35+
const angularStrokeWidth = padAngle && radius ? 2 * convertAngleToArcLength(padAngle, radius) : 0;
36+
const angularStrokeColor = typeof chartContainerStyles.backgroundColor === 'string' ? chartContainerStyles.backgroundColor : VictoryTheme.colors.default;
37+
const labelIndicatorNode = parseComponent(tnode.attributes.labelindicator, renderEngine, 'shiftedlinesegment', HTMLContentModel.block);
38+
const labelIndicatorStyles = labelIndicatorNode ? parseShiftedLineSegmentNode(labelIndicatorNode) : undefined;
39+
const {xShift: labelIndicatorXShift, yShift: labelIndicatorYShift, stroke: labelIndicatorStroke, strokeWidth: labelIndicatorStrokeWidth} = labelIndicatorStyles ?? {};
40+
const labelIndicatorInnerOffset = tnode.attributes.labelindicatorinneroffset !== undefined ? Number(parseAttribute(tnode.attributes.labelindicatorinneroffset)) : undefined;
41+
const labelIndicatorOuterOffset = tnode.attributes.labelindicatorouteroffset !== undefined ? Number(parseAttribute(tnode.attributes.labelindicatorouteroffset)) : undefined;
42+
const perSliceData = Object.values(data)
43+
.map((entry) => (entry as PolarChartData).label)
44+
.reduce(
45+
(slicesData, dataLabel, index) => {
46+
// eslint-disable-next-line no-param-reassign
47+
slicesData[dataLabel] = {
48+
customLabel: pieLabels?.[index],
49+
customLabelRadius: labelRadius ? labelRadius * (index % 2 === 0 ? EVEN_SLICE_LABEL_RADIUS_FACTOR : ODD_SLICE_LABEL_RADIUS_FACTOR) : undefined,
50+
};
51+
return slicesData;
52+
},
53+
{} as Record<string, {customLabelRadius: number | undefined; customLabel: string | undefined}>,
54+
);
2355

2456
return (
2557
<Pie.Chart
@@ -28,13 +60,30 @@ function VictoryChartPie({tnode}: VictoryChartPieProps) {
2860
size={size}
2961
>
3062
{({slice}) => (
31-
<Pie.Slice>
32-
<VictoryChartPieLabel
33-
tnode={tnode}
34-
slice={slice}
35-
labelItemTemplate={labelItemTemplate}
63+
<>
64+
<Pie.Slice>
65+
{!!baseLabelItem && (
66+
<VictoryChartPieLabel
67+
slice={slice}
68+
baseLabelItem={baseLabelItem}
69+
label={perSliceData[slice.label].customLabel ?? slice.label}
70+
labelRadius={perSliceData[slice.label].customLabelRadius}
71+
labelIndicatorXShift={labelIndicatorXShift}
72+
labelIndicatorYShift={labelIndicatorYShift}
73+
labelIndicatorStroke={labelIndicatorStroke}
74+
labelIndicatorStrokeWidth={labelIndicatorStrokeWidth}
75+
labelIndicatorInnerOffset={labelIndicatorInnerOffset}
76+
labelIndicatorOuterOffset={labelIndicatorOuterOffset}
77+
/>
78+
)}
79+
</Pie.Slice>
80+
<Pie.SliceAngularInset
81+
angularInset={{
82+
angularStrokeWidth,
83+
angularStrokeColor,
84+
}}
3685
/>
37-
</Pie.Slice>
86+
</>
3887
)}
3988
</Pie.Chart>
4089
);
Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,66 @@
1+
import type {Color} from '@shopify/react-native-skia';
12
import React from 'react';
2-
import type {TNode} from 'react-native-render-html';
33
import type {PieSliceData} from 'victory-native';
4-
import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext';
5-
import type {LabelItem, PolarChartData} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types';
6-
import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute';
4+
import type {LabelItem} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types';
5+
import convertDegreeToRadian from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/convertDegreeToRadian';
76
import VictoryChartLabel from './VictoryChartLabel';
7+
import VictoryChartPieLabelIndicator from './VictoryChartPieLabelIndicator';
88

99
type VictoryChartPieLabelProps = {
10-
tnode: TNode;
1110
slice: PieSliceData;
12-
labelItemTemplate?: LabelItem;
11+
baseLabelItem: LabelItem;
12+
label: string;
13+
labelRadius: number | undefined;
14+
labelIndicatorXShift: number | undefined;
15+
labelIndicatorYShift: number | undefined;
16+
labelIndicatorStroke: Color | undefined;
17+
labelIndicatorStrokeWidth: number | undefined;
18+
labelIndicatorInnerOffset: number | undefined;
19+
labelIndicatorOuterOffset: number | undefined;
1320
};
1421

15-
const RADIAN = Math.PI / 180;
16-
17-
function VictoryChartPieLabel({tnode, slice, labelItemTemplate}: VictoryChartPieLabelProps) {
18-
const {data} = useVictoryChartContext();
19-
20-
if (!labelItemTemplate) {
21-
return null;
22-
}
23-
24-
const dataLabels = Object.values(data).map((entry) => (entry as PolarChartData).label);
25-
const labels = parseAttribute<string[]>(tnode.attributes.labels);
26-
const text = labels?.[dataLabels.indexOf(slice.label)] ?? slice.label;
27-
28-
const labelRadius = tnode.attributes.labelradius !== undefined ? Number(parseAttribute(tnode.attributes.labelradius)) : slice.radius;
29-
const midAngle = (slice.startAngle + slice.endAngle) / 2;
30-
const x = slice.center.x + labelRadius * Math.cos(-midAngle * RADIAN);
31-
const y = slice.center.y + labelRadius * Math.sin(midAngle * RADIAN);
22+
function VictoryChartPieLabel({
23+
slice,
24+
baseLabelItem,
25+
label,
26+
labelRadius,
27+
labelIndicatorXShift,
28+
labelIndicatorYShift,
29+
labelIndicatorStroke,
30+
labelIndicatorStrokeWidth,
31+
labelIndicatorInnerOffset,
32+
labelIndicatorOuterOffset,
33+
}: VictoryChartPieLabelProps) {
34+
const midAngle = convertDegreeToRadian((slice.startAngle + slice.endAngle) / 2);
35+
const x = slice.center.x + (labelRadius ?? slice.radius) * Math.cos(midAngle);
36+
const y = slice.center.y + (labelRadius ?? slice.radius) * Math.sin(midAngle);
3237

3338
const labelItem: LabelItem = {
34-
...labelItemTemplate,
35-
text,
39+
...baseLabelItem,
40+
text: label,
3641
x,
3742
y,
3843
textAnchor: 'middle',
44+
verticalAnchor: 'middle',
3945
};
4046

41-
return <VictoryChartLabel {...labelItem} />;
47+
return (
48+
<>
49+
{!!labelIndicatorStrokeWidth && (
50+
<VictoryChartPieLabelIndicator
51+
slice={slice}
52+
labelRadius={labelRadius ?? slice.radius}
53+
labelIndicatorXShift={labelIndicatorXShift}
54+
labelIndicatorYShift={labelIndicatorYShift}
55+
labelIndicatorStroke={labelIndicatorStroke}
56+
labelIndicatorStrokeWidth={labelIndicatorStrokeWidth}
57+
labelIndicatorInnerOffset={labelIndicatorInnerOffset}
58+
labelIndicatorOuterOffset={labelIndicatorOuterOffset}
59+
/>
60+
)}
61+
<VictoryChartLabel {...labelItem} />
62+
</>
63+
);
4264
}
4365

4466
export default VictoryChartPieLabel;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {Path, Skia} from '@shopify/react-native-skia';
2+
import type {Color} from '@shopify/react-native-skia';
3+
import React from 'react';
4+
import type {PieSliceData} from 'victory-native';
5+
import convertDegreeToRadian from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/convertDegreeToRadian';
6+
7+
type VictoryChartPieLabelIndicatorProps = {
8+
slice: PieSliceData;
9+
labelRadius: number;
10+
labelIndicatorXShift: number | undefined;
11+
labelIndicatorYShift: number | undefined;
12+
labelIndicatorStroke: Color | undefined;
13+
labelIndicatorStrokeWidth: number;
14+
labelIndicatorInnerOffset: number | undefined;
15+
labelIndicatorOuterOffset: number | undefined;
16+
};
17+
18+
function VictoryChartPieLabelIndicator({
19+
slice,
20+
labelRadius,
21+
labelIndicatorXShift,
22+
labelIndicatorYShift,
23+
labelIndicatorStroke,
24+
labelIndicatorStrokeWidth,
25+
labelIndicatorInnerOffset,
26+
labelIndicatorOuterOffset,
27+
}: VictoryChartPieLabelIndicatorProps) {
28+
const midAngle = convertDegreeToRadian((slice.startAngle + slice.endAngle) / 2);
29+
const midRadius = (slice.radius + slice.innerRadius) / 2;
30+
const labelIndicatorInnerRadius = midRadius + (labelIndicatorInnerOffset ?? 0);
31+
const labelIndicatorOuterRadius = labelRadius - (labelIndicatorOuterOffset ?? 0);
32+
33+
const x1 = slice.center.x + labelIndicatorInnerRadius * Math.cos(midAngle) + (labelIndicatorXShift ?? 0);
34+
const y1 = slice.center.y + labelIndicatorInnerRadius * Math.sin(midAngle) + (labelIndicatorYShift ?? 0);
35+
36+
const x2 = slice.center.x + labelIndicatorOuterRadius * Math.cos(midAngle) + (labelIndicatorXShift ?? 0);
37+
const y2 = slice.center.y + labelIndicatorOuterRadius * Math.sin(midAngle) + (labelIndicatorYShift ?? 0);
38+
39+
const path = Skia.Path.Make();
40+
path.moveTo(x1, y1);
41+
path.lineTo(x2, y2);
42+
43+
return (
44+
<Path
45+
path={path}
46+
// This is an external component and `style` is not an object
47+
// eslint-disable-next-line react/style-prop-object
48+
style="stroke"
49+
strokeWidth={labelIndicatorStrokeWidth}
50+
color={labelIndicatorStroke}
51+
/>
52+
);
53+
}
54+
55+
VictoryChartPieLabelIndicator.displayName = 'VictoryChartPieLabelIndicator';
56+
57+
export default VictoryChartPieLabelIndicator;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type {TNode} from 'react-native-render-html';
2+
import type {RawShiftedLineSegmentStyle} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types';
3+
import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute';
4+
5+
/**
6+
* Parse label indicator config from a `<ShiftedLineSegment>` node.
7+
*/
8+
function parseShiftedLineSegmentNode(tnode: TNode) {
9+
const xShift = parseAttribute<number>(tnode.attributes.dx) ?? 0;
10+
const yShift = parseAttribute<number>(tnode.attributes.dy) ?? 0;
11+
const style = parseAttribute<RawShiftedLineSegmentStyle>(tnode.attributes.style);
12+
const stroke = style?.stroke;
13+
const strokeWidth = style?.strokeWidth;
14+
return {xShift, yShift, stroke, strokeWidth};
15+
}
16+
17+
export default parseShiftedLineSegmentNode;

0 commit comments

Comments
 (0)