diff --git a/.changeset/multiline-axis.md b/.changeset/multiline-axis.md
new file mode 100644
index 00000000..098f2f61
--- /dev/null
+++ b/.changeset/multiline-axis.md
@@ -0,0 +1,6 @@
+---
+"example": patch
+"victory-native": patch
+---
+
+Add multiline axis label support with proper spacing and layout handling.
diff --git a/example/app/multiline-axis.tsx b/example/app/multiline-axis.tsx
new file mode 100644
index 00000000..e5e7fd9c
--- /dev/null
+++ b/example/app/multiline-axis.tsx
@@ -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 (
+
+
+
+ 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 }) => (
+ <>
+
+
+
+
+ >
+ )}
+
+
+
+
+ {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."}
+
+
+
+ );
+}
+
+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",
+ },
+});
diff --git a/example/consts/routes.ts b/example/consts/routes.ts
index 896e1294..2ed5c8e8 100644
--- a/example/consts/routes.ts
+++ b/example/consts/routes.ts
@@ -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__) {
diff --git a/lib/src/cartesian/CartesianChart.tsx b/lib/src/cartesian/CartesianChart.tsx
index 9b82b54f..96fd53ca 100644
--- a/lib/src/cartesian/CartesianChart.tsx
+++ b/lib/src/cartesian/CartesianChart.tsx
@@ -295,6 +295,7 @@ function CartesianChartContent<
domainPadding,
normalizedAxisProps,
viewport,
+ axisOptions?.axisScales,
]);
React.useEffect(() => {
diff --git a/lib/src/cartesian/components/CartesianAxis.tsx b/lib/src/cartesian/components/CartesianAxis.tsx
index 51127299..cff87ba1 100644
--- a/lib/src/cartesian/components/CartesianAxis.tsx
+++ b/lib/src/cartesian/components/CartesianAxis.tsx
@@ -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);
@@ -154,15 +145,55 @@ export const CartesianAxis = <
) : null}
{font
? canFitLabelContent && (
-
+ <>
+ {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 (
+
+ );
+ })}
+ >
)
: null}
@@ -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;
@@ -205,13 +244,28 @@ export const CartesianAxis = <
/>
) : null}
{font && labelWidth && canFitLabelContent ? (
-
+ <>
+ {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 (
+
+ );
+ })}
+ >
) : null}
);
diff --git a/lib/src/cartesian/components/XAxis.tsx b/lib/src/cartesian/components/XAxis.tsx
index 6d834722..4033c372 100644
--- a/lib/src/cartesian/components/XAxis.tsx
+++ b/lib/src/cartesian/components/XAxis.tsx
@@ -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 &&
@@ -139,19 +147,30 @@ export const XAxis = <
) : null}
{font && labelWidth && canFitLabelContent ? (
-
+ {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 (
+
+ );
+ })}
) : null}
<>>
diff --git a/lib/src/cartesian/components/YAxis.tsx b/lib/src/cartesian/components/YAxis.tsx
index 478f6f02..4390faae 100644
--- a/lib/src/cartesian/components/YAxis.tsx
+++ b/lib/src/cartesian/components/YAxis.tsx
@@ -33,27 +33,19 @@ export const YAxis = <
const fontSize = font?.getSize() ?? 0;
const yAxisNodes = yTicksNormalized.map((tick) => {
const contentY = formatYLabel(tick as never);
- const labelWidth =
- font
- ?.getGlyphWidths?.(font.getGlyphIDs(contentY))
- .reduce((sum, value) => sum + value, 0) ?? 0;
+
+ // 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) =>
+ font
+ ?.getGlyphWidths?.(font.getGlyphIDs(line))
+ .reduce((sum, value) => sum + value, 0) ?? 0,
+ );
+
const labelY = yScale(tick) + fontSize / 3;
- const labelX = (() => {
- // left, outset
- if (axisSide === "left" && labelPosition === "outset") {
- return chartBounds.left - (labelWidth + labelOffset);
- }
- // left, inset
- if (axisSide === "left" && labelPosition === "inset") {
- return chartBounds.left + labelOffset;
- }
- // right, outset
- if (axisSide === "right" && labelPosition === "outset") {
- return chartBounds.right + labelOffset;
- }
- // right, inset
- return chartBounds.right - (labelWidth + labelOffset);
- })();
const canFitLabelContent = labelY > fontSize && labelY < yScale(y2);
@@ -73,13 +65,44 @@ export const YAxis = <
) : null}
{font
? canFitLabelContent && (
-
+ <>
+ {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 (axisSide === "left" && labelPosition === "outset") {
+ return (
+ chartBounds.left - ((lineWidth || 0) + labelOffset)
+ );
+ }
+ // left, inset
+ if (axisSide === "left" && labelPosition === "inset") {
+ return chartBounds.left + labelOffset;
+ }
+ // right, outset
+ if (axisSide === "right" && labelPosition === "outset") {
+ return chartBounds.right + labelOffset;
+ }
+ // right, inset
+ return chartBounds.right - ((lineWidth || 0) + labelOffset);
+ })();
+
+ // Calculate y position for each line
+ const lineY = labelY + lineIndex * fontSize;
+
+ return (
+
+ );
+ })}
+ >
)
: null}
diff --git a/lib/src/cartesian/utils/transformInputData.ts b/lib/src/cartesian/utils/transformInputData.ts
index 8d4403c2..85acc5bd 100644
--- a/lib/src/cartesian/utils/transformInputData.ts
+++ b/lib/src/cartesian/utils/transformInputData.ts
@@ -133,11 +133,19 @@ export const transformInputData = <
xTick as unknown as Parameters[0],
)
: String(xTick);
- const labelStr = String(labelValue);
- if (!xAxis.font) return 0;
- const glyphIDs = xAxis.font.getGlyphIDs(labelStr);
- const widths = xAxis.font.getGlyphWidths?.(glyphIDs) ?? [];
- return widths.reduce((sum, w) => sum + w, 0);
+
+ // Handle multiline labels by finding the width of the longest line
+ const lines = Array.isArray(labelValue)
+ ? labelValue
+ : String(labelValue).split("\n");
+ const lineWidths = lines.map((line: string) => {
+ if (!xAxis.font) return 0;
+ const glyphIDs = xAxis.font.getGlyphIDs(line);
+ const widths = xAxis.font.getGlyphWidths?.(glyphIDs) ?? [];
+ return widths.reduce((sum, w) => sum + w, 0);
+ });
+
+ return Math.max(...lineWidths, 0);
}),
);
@@ -268,16 +276,21 @@ export const transformInputData = <
});
const maxYLabel = Math.max(
- ...yTicksNormalized.map(
- (yTick) =>
- yAxis?.font
- ?.getGlyphWidths?.(
- yAxis.font.getGlyphIDs(
- yAxis?.formatYLabel?.(yTick as RawData[YK]) || String(yTick),
- ),
- )
- .reduce((sum, value) => sum + value, 0) ?? 0,
- ),
+ ...yTicksNormalized.map((yTick) => {
+ const label =
+ yAxis?.formatYLabel?.(yTick as RawData[YK]) || String(yTick);
+
+ // Handle multiline labels by finding the width of the longest line
+ const lines = Array.isArray(label) ? label : label.split("\n");
+ const lineWidths = lines.map(
+ (line: string) =>
+ yAxis?.font
+ ?.getGlyphWidths?.(yAxis.font.getGlyphIDs(line))
+ .reduce((sum, value) => sum + value, 0) ?? 0,
+ );
+
+ return Math.max(...lineWidths, 0);
+ }),
);
return {
diff --git a/lib/src/types.ts b/lib/src/types.ts
index a3a4c816..dcc874f7 100644
--- a/lib/src/types.ts
+++ b/lib/src/types.ts
@@ -146,8 +146,8 @@ export type AxisProps<
| AxisLabelPosition
| { x: AxisLabelPosition; y: AxisLabelPosition };
axisSide?: { x: XAxisSide; y: YAxisSide };
- formatXLabel?: (label: InputFields[XK]) => string;
- formatYLabel?: (label: RawData[YK]) => string;
+ formatXLabel?: (label: InputFields[XK]) => string | string[];
+ formatYLabel?: (label: RawData[YK]) => string | string[];
domain?: YAxisDomain;
isNumericalData?: boolean;
ix?: InputFields[XK][];
@@ -176,8 +176,8 @@ export type OptionalAxisProps<
> = {
tickValues?: number[] | { x: number[]; y: number[] };
font?: SkFont | null;
- formatXLabel?: (label: InputFields[XK]) => string;
- formatYLabel?: (label: RawData[YK]) => string;
+ formatXLabel?: (label: InputFields[XK]) => string | string[];
+ formatYLabel?: (label: RawData[YK]) => string | string[];
};
type DashPathEffectProps = React.ComponentProps;
@@ -189,7 +189,7 @@ export type XAxisInputProps<
> = {
axisSide?: XAxisSide;
font?: SkFont | null;
- formatXLabel?: (label: InputFields[XK]) => string;
+ formatXLabel?: (label: InputFields[XK]) => string | string[];
labelColor?: string;
labelOffset?: number;
labelPosition?: AxisLabelPosition;
@@ -201,6 +201,11 @@ export type XAxisInputProps<
yAxisSide?: YAxisSide;
linePathEffect?: DashPathEffectComponent;
enableRescaling?: boolean;
+ multiline?: {
+ enabled?: boolean;
+ maxLines?: number;
+ lineHeight?: number;
+ };
};
export type XAxisPropsWithDefaults<
@@ -209,7 +214,12 @@ export type XAxisPropsWithDefaults<
> = Required<
Omit<
XAxisInputProps,
- "font" | "tickValues" | "linePathEffect" | "enableRescaling" | "labelRotate"
+ | "font"
+ | "tickValues"
+ | "linePathEffect"
+ | "enableRescaling"
+ | "labelRotate"
+ | "multiline"
>
> &
Partial<
@@ -220,6 +230,7 @@ export type XAxisPropsWithDefaults<
| "linePathEffect"
| "enableRescaling"
| "labelRotate"
+ | "multiline"
>
>;
@@ -241,7 +252,7 @@ export type YAxisInputProps<
> = {
axisSide?: YAxisSide;
font?: SkFont | null;
- formatYLabel?: (label: RawData[YK]) => string;
+ formatYLabel?: (label: RawData[YK]) => string | string[];
labelColor?: string;
labelOffset?: number;
labelPosition?: AxisLabelPosition;
@@ -253,6 +264,11 @@ export type YAxisInputProps<
domain?: YAxisDomain;
linePathEffect?: DashPathEffectComponent;
enableRescaling?: boolean;
+ multiline?: {
+ enabled?: boolean;
+ maxLines?: number;
+ lineHeight?: number;
+ };
};
export type YAxisPropsWithDefaults<
@@ -261,13 +277,13 @@ export type YAxisPropsWithDefaults<
> = Required<
Omit<
YAxisInputProps,
- "font" | "tickValues" | "linePathEffect" | "enableRescaling"
+ "font" | "tickValues" | "linePathEffect" | "enableRescaling" | "multiline"
>
> &
Partial<
Pick<
YAxisInputProps,
- "font" | "tickValues" | "linePathEffect" | "enableRescaling"
+ "font" | "tickValues" | "linePathEffect" | "enableRescaling" | "multiline"
>
>;