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
6 changes: 6 additions & 0 deletions .changeset/multiline-axis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"example": patch
"victory-native": patch
---

Add multiline axis label support with proper spacing and layout handling.
137 changes: 137 additions & 0 deletions example/app/multiline-axis.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { useFont } from "@shopify/react-native-skia";
import * as React from "react";
import { SafeAreaView, ScrollView, StyleSheet, View } from "react-native";
import { CartesianChart, Line, Scatter } from "victory-native";
import { useDarkMode } from "react-native-dark";
import inter from "../assets/inter-medium.ttf";
import { appColors } from "../consts/colors";
import { InfoCard } from "../components/InfoCard";
import { descriptionForRoute } from "../consts/routes";

const DATA = Array.from({ length: 6 }, (_, index) => ({
month: index,
sales: Math.random() * 100 + 50,
profit: Math.random() * 50 + 25,
}));

const MONTH_NAMES = [
"January\n2024",
"February\n2024",
"March\n2024",
"April\n2024",
"May\n2024",
"June\n2024",
];

const METRIC_LABELS = [
"Low\nPerformance",
"Below\nAverage",
"Average\nPerformance",
"Good\nPerformance",
"Excellent\nPerformance",
];

export default function MultilineAxis(props: { segment: string }) {
const description = descriptionForRoute(props.segment);
const isDark = useDarkMode();
const font = useFont(inter, 12);

return (
<SafeAreaView style={styles.safeView}>
<View style={styles.chart}>
<CartesianChart
xKey="month"
padding={20}
yKeys={["sales", "profit"]}
data={DATA}
xAxis={{
font,
labelColor: isDark ? appColors.text.dark : appColors.text.light,
formatXLabel: (value) =>
MONTH_NAMES[value as number] || `Month ${value}`,
labelOffset: 8,
}}
yAxis={[
{
font,
labelColor: isDark ? appColors.text.dark : appColors.text.light,
formatYLabel: (value) => {
const index = Math.floor((value as number) / 25);
return (
METRIC_LABELS[Math.min(index, METRIC_LABELS.length - 1)] ||
`${value}`
);
},
labelOffset: 8,
yKeys: ["sales", "profit"],
},
]}
>
{({ points }) => (
<>
<Line
points={points.sales}
color="#8b5cf6"
strokeWidth={3}
animate={{ type: "spring" }}
/>
<Line
points={points.profit}
color="#06b6d4"
strokeWidth={3}
animate={{ type: "spring" }}
/>
<Scatter
radius={4}
points={points.sales}
animate={{ type: "spring" }}
color="#8b5cf6"
/>
<Scatter
radius={4}
points={points.profit}
animate={{ type: "spring" }}
color="#06b6d4"
/>
</>
)}
</CartesianChart>
</View>
<ScrollView
style={styles.optionsScrollView}
contentContainerStyle={styles.options}
>
<InfoCard>
{description ||
"This example demonstrates multiline axis labels. X-axis shows months with years on separate lines, while Y-axis shows performance categories with descriptive text on multiple lines. Use \\n in your formatXLabel or formatYLabel functions to create multiline labels."}
</InfoCard>
</ScrollView>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
safeView: {
flex: 1,
backgroundColor: appColors.viewBackground.light,
$dark: {
backgroundColor: appColors.viewBackground.dark,
},
},
chart: {
flex: 1,
},
optionsScrollView: {
flex: 0.3,
backgroundColor: appColors.cardBackground.light,
$dark: {
backgroundColor: appColors.cardBackground.dark,
},
},
options: {
paddingHorizontal: 20,
paddingVertical: 15,
alignItems: "flex-start",
justifyContent: "flex-start",
},
});
6 changes: 6 additions & 0 deletions example/consts/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ export const ChartRoutes: {
description: "This example demonstrates chart interactions using refs.",
path: "/chart-refs",
},
{
title: "Multiline Axis Labels",
description:
"Example demonstrating multiline axis labels using both newline characters and string arrays.",
path: "/multiline-axis",
},
];

if (__DEV__) {
Expand Down
1 change: 1 addition & 0 deletions lib/src/cartesian/CartesianChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ function CartesianChartContent<
domainPadding,
normalizedAxisProps,
viewport,
axisOptions?.axisScales,
]);

React.useEffect(() => {
Expand Down
122 changes: 88 additions & 34 deletions lib/src/cartesian/components/CartesianAxis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,24 +121,15 @@ export const CartesianAxis = <

const yAxisNodes = yTicksNormalized.map((tick) => {
const contentY = formatYLabel(tick as never);
const labelWidth = getFontGlyphWidth(contentY, font);

// Handle both string and string array formats for multiline labels
const lines = Array.isArray(contentY) ? contentY : contentY.split("\n");

// Calculate width for each line and find the max width
const lineWidths = lines.map((line: string) =>
getFontGlyphWidth(line, font),
);
const labelY = yScale(tick) + fontSize / 3;
const labelX = (() => {
// left, outset
if (yAxisPosition === "left" && yLabelPosition === "outset") {
return xScale(x1) - (labelWidth + yLabelOffset);
}
// left, inset
if (yAxisPosition === "left" && yLabelPosition === "inset") {
return xScale(x1) + yLabelOffset;
}
// right, outset
if (yAxisPosition === "right" && yLabelPosition === "outset") {
return xScale(x2) + yLabelOffset;
}
// right, inset
return xScale(x2) - (labelWidth + yLabelOffset);
})();

const canFitLabelContent = labelY > fontSize && labelY < yScale(y2);

Expand All @@ -154,15 +145,55 @@ export const CartesianAxis = <
) : null}
{font
? canFitLabelContent && (
<Text
color={
typeof labelColor === "string" ? labelColor : labelColor.y
}
text={contentY}
font={font}
y={labelY}
x={labelX}
/>
<>
{lines.map((line: string, lineIndex: number) => {
// Calculate x position for each line based on line width
const lineWidth = lineWidths[lineIndex];
const lineLabelX = (() => {
// left, outset
if (
yAxisPosition === "left" &&
yLabelPosition === "outset"
) {
return xScale(x1) - ((lineWidth || 0) + yLabelOffset);
}
// left, inset
if (
yAxisPosition === "left" &&
yLabelPosition === "inset"
) {
return xScale(x1) + yLabelOffset;
}
// right, outset
if (
yAxisPosition === "right" &&
yLabelPosition === "outset"
) {
return xScale(x2) + yLabelOffset;
}
// right, inset
return xScale(x2) - ((lineWidth || 0) + yLabelOffset);
})();

// Calculate y position for each line
const lineY = labelY + lineIndex * fontSize;

return (
<Text
key={`y-line-${lineIndex}`}
color={
typeof labelColor === "string"
? labelColor
: labelColor.y
}
text={line}
font={font}
y={lineY}
x={lineLabelX}
/>
);
})}
</>
)
: null}
</React.Fragment>
Expand All @@ -172,7 +203,15 @@ export const CartesianAxis = <
const xAxisNodes = xTicksNormalized.map((tick) => {
const val = isNumericalData ? tick : ix[tick];
const contentX = formatXLabel(val as never);
const labelWidth = getFontGlyphWidth(contentX, font);

// Handle both string and string array formats for multiline labels
const lines = Array.isArray(contentX) ? contentX : contentX.split("\n");

// Calculate width for each line and find the max width
const lineWidths = lines.map((line: string) =>
getFontGlyphWidth(line, font),
);
const labelWidth = Math.max(...lineWidths, 0);
const labelX = xScale(tick) - (labelWidth ?? 0) / 2;
const canFitLabelContent =
yAxisPosition === "left" ? labelX + labelWidth < x2r : x1r < labelX;
Expand Down Expand Up @@ -205,13 +244,28 @@ export const CartesianAxis = <
/>
) : null}
{font && labelWidth && canFitLabelContent ? (
<Text
color={typeof labelColor === "string" ? labelColor : labelColor.x}
text={contentX}
font={font}
y={labelY}
x={labelX}
/>
<>
{lines.map((line: string, lineIndex: number) => {
// Calculate x position for center alignment
const lineWidth = lineWidths[lineIndex];
const lineLabelX = xScale(tick) - (lineWidth || 0) / 2;
// Calculate y position for each line
const lineY = labelY + lineIndex * fontSize;

return (
<Text
key={`x-line-${lineIndex}`}
color={
typeof labelColor === "string" ? labelColor : labelColor.x
}
text={line}
font={font}
y={lineY}
x={lineLabelX}
/>
);
})}
</>
) : null}
</React.Fragment>
);
Expand Down
53 changes: 36 additions & 17 deletions lib/src/cartesian/components/XAxis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,18 @@ export const XAxis = <
const val = isNumericalData ? tick : ix[tick];

const contentX = formatXLabel(val as never);
const labelWidth =
font
?.getGlyphWidths?.(font.getGlyphIDs(contentX))
.reduce((sum, value) => sum + value, 0) ?? 0;

// Handle both string and string array formats for multiline labels
const lines = Array.isArray(contentX) ? contentX : contentX.split("\n");

// Calculate width for each line and find the max width
const lineWidths = lines.map(
(line: string) =>
font
?.getGlyphWidths?.(font.getGlyphIDs(line))
.reduce((sum, value) => sum + value, 0) ?? 0,
);
const labelWidth = Math.max(...lineWidths, 0);
const labelX = xScale(tick) - (labelWidth ?? 0) / 2;
const canFitLabelContent =
xScale(tick) >= chartBounds.left &&
Expand Down Expand Up @@ -139,19 +147,30 @@ export const XAxis = <
) : null}
{font && labelWidth && canFitLabelContent ? (
<Group transform={[{ translateY: rotateOffset }]}>
<Text
transform={[
{
rotate: (Math.PI / 180) * (labelRotate ?? 0),
},
]}
origin={origin}
color={labelColor}
text={contentX}
font={font}
y={labelY}
x={labelX}
/>
{lines.map((line: string, lineIndex: number) => {
// Calculate x position for center alignment
const lineWidth = lineWidths[lineIndex];
const lineLabelX = xScale(tick) - (lineWidth || 0) / 2;
// Calculate y position for each line
const lineY = labelY + lineIndex * fontSize;

return (
<Text
key={`line-${lineIndex}`}
transform={[
{
rotate: (Math.PI / 180) * (labelRotate ?? 0),
},
]}
origin={origin}
color={labelColor}
text={line}
font={font}
y={lineY}
x={lineLabelX}
/>
);
})}
</Group>
) : null}
<></>
Expand Down
Loading