Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: axis labels #461

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions .changeset/slow-cheetahs-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"victory-native": minor
---

Add axis labels
9 changes: 9 additions & 0 deletions example/app/bar-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,13 @@ export default function BarChartPage(props: { segment: string }) {
const date = new Date(2023, value - 1);
return date.toLocaleString("default", { month: "short" });
},
axisSide: "bottom",
linePathEffect: <DashPathEffect intervals={[4, 4]} />,
labelRotate: -45,
title: {
text: "Months",
font,
},
}}
frame={{
lineWidth: 0,
Expand All @@ -113,6 +119,9 @@ export default function BarChartPage(props: { segment: string }) {
yKeys: ["listenCount"],
font,
linePathEffect: <DashPathEffect intervals={[4, 4]} />,
title: {
text: "Listen count",
},
},
]}
data={data}
Expand Down
26 changes: 20 additions & 6 deletions example/app/dashed-axes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as React from "react";
import { SafeAreaView, ScrollView, StyleSheet, View } from "react-native";
import { Area, CartesianChart, Line } from "victory-native";
import inter from "../assets/inter-medium.ttf";
import notosans from "../assets/notosans.ttf";
import { appColors } from "../consts/colors";
import { InfoCard } from "../components/InfoCard";
import { descriptionForRoute } from "../consts/routes";
Expand All @@ -30,6 +31,7 @@ const generateData = () =>
export default function DashedAxesPage(props: { segment: string }) {
const description = descriptionForRoute(props.segment);
const font = useFont(inter, 12);
const notoFont = useFont(notosans, 16);
const [data, setData] = React.useState(generateData);
const [, setW] = React.useState(0);
const [, setH] = React.useState(0);
Expand All @@ -42,20 +44,32 @@ export default function DashedAxesPage(props: { segment: string }) {
data={data}
xKey="month"
yKeys={["low", "high"]}
padding={16}
padding={{
top: 20,
right: 20,
}}
domain={{ y: [0, 65] }}
domainPadding={{ top: 20 }}
xAxis={{
font,
labelOffset: 4,
labelOffset: 0,
linePathEffect: <DashPathEffect intervals={[4, 4]} />,
title: {
text: "Month",
font: notoFont,
position: "center",
yOffset: 0,
},
}}
yAxis={[
{
labelOffset: 8,

labelOffset: 4,
font,

title: {
text: "Temperature",
font: notoFont,
position: "center",
xOffset: 12,
},
linePathEffect: <DashPathEffect intervals={[4, 4]} />,
},
]}
Expand Down
15 changes: 15 additions & 0 deletions example/app/multiple-y-axes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ export default function MultipleYAxesPage() {
return value.toFixed(0);
},
lineColor: "pink",
title: {
text: "Bottom",
position: "center",
yOffset: 2,
},
}}
frame={{
lineColor: "black",
Expand All @@ -132,6 +137,11 @@ export default function MultipleYAxesPage() {
},
lineColor: "pink",
enableRescaling: true,
title: {
text: "Temperature",
position: "center",
xOffset: 2,
},
},
{
yKeys: ["profit"],
Expand All @@ -143,6 +153,11 @@ export default function MultipleYAxesPage() {
axisSide: "right",
lineWidth: 0,
tickValues: [10000, 10030],
title: {
text: "Profit",
position: "center",
xOffset: 0,
},
},
]}
data={data}
Expand Down
Binary file added example/assets/notosans.ttf
Binary file not shown.
78 changes: 76 additions & 2 deletions lib/src/cartesian/components/XAxis.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useMemo } from "react";
import { StyleSheet } from "react-native";
import {
Group,
Expand All @@ -17,6 +17,7 @@ import type {
XAxisProps,
XAxisPropsWithDefaults,
} from "../../types";
import { getFontGlyphWidth } from "../../utils/getFontGlyphWidth";

export const XAxis = <
RawData extends Record<string, unknown>,
Expand All @@ -42,6 +43,7 @@ export const XAxis = <
chartBounds,
enableRescaling,
zoom,
title,
}: XAxisProps<RawData, XK>) => {
const xScale = zoom ? zoom.rescaleX(xScaleProp) : xScaleProp;
const [y1 = 0, y2 = 0] = yScale.domain();
Expand All @@ -52,6 +54,20 @@ export const XAxis = <
? xScale.ticks(tickCount)
: xScaleProp.ticks(tickCount);

const longestLabel = xTicksNormalized.reduce((longest, tick) => {
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;
return labelWidth > longest ? labelWidth : longest;
}, 0);

const maxLabelHeight = labelRotate
? Math.abs(longestLabel * getOffsetFromAngle(labelRotate))
: 0;

const xAxisNodes = xTicksNormalized.map((tick) => {
const p1 = vec(xScale(tick), yScale(y2));
const p2 = vec(xScale(tick), yScale(y1));
Expand Down Expand Up @@ -159,7 +175,65 @@ export const XAxis = <
);
});

return xAxisNodes;
const AxisTitle = useMemo(() => {
if (!title) return null;

const {
text,
position: titlePosition = "center",
yOffset = 2,
font: titleFont,
} = title;

const titleFontToUse = titleFont ?? font;
const titleWidth = getFontGlyphWidth(text, titleFontToUse);
const titleSize = titleFontToUse?.getSize() ?? fontSize;

// Calculate horizontal position
const titleX = (() => {
if (titlePosition === "left") return chartBounds.left;
if (titlePosition === "right") return chartBounds.right - titleWidth;
// Center by default
return (chartBounds.left + chartBounds.right - titleWidth) / 2;
})();

// Calculate vertical offset from axis
// offset by the maxLabelHeight (if rotated labels) + font size of the ticks + the font size of this title itself + the label offset optional prop + y offset optional prop
const baseOffset =
maxLabelHeight + fontSize + titleSize + labelOffset + (yOffset ?? 0);
const translateY =
axisSide === "bottom"
? chartBounds.bottom + baseOffset
: chartBounds.top - baseOffset;

return (
<Group transform={[{ translateX: titleX }, { translateY }]}>
<Text
color={labelColor}
text={text}
font={titleFontToUse!}
x={0}
y={0}
/>
</Group>
);
}, [
title,
chartBounds,
font,
fontSize,
labelOffset,
axisSide,
labelColor,
maxLabelHeight,
]);

return (
<>
{xAxisNodes}
{AxisTitle}
</>
);
};

export const XAxisDefaults = {
Expand Down
68 changes: 66 additions & 2 deletions lib/src/cartesian/components/YAxis.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import React, { useMemo } from "react";
import { StyleSheet } from "react-native";
import { Group, Line, Text, vec } from "@shopify/react-native-skia";
import { getFontGlyphWidth } from "../../utils/getFontGlyphWidth";
import { boundsToClip } from "../../utils/boundsToClip";
import type {
InputDatum,
Expand All @@ -27,10 +28,21 @@
formatYLabel = (label: ValueOf<InputDatum>) => String(label),
linePathEffect,
chartBounds,
title,
}: YAxisProps<RawData, YK>) => {
const [x1 = 0, x2 = 0] = xScale.domain();
const [_ = 0, y2 = 0] = yScale.domain();
const fontSize = font?.getSize() ?? 0;

const longestLabel = yTicksNormalized.reduce((longest, tick) => {
const contentY = formatYLabel(tick as never);
const labelWidth =
font
?.getGlyphWidths?.(font.getGlyphIDs(contentY))
.reduce((sum, value) => sum + value, 0) ?? 0;
return labelWidth > longest ? labelWidth : longest;
}, 0);

const yAxisNodes = yTicksNormalized.map((tick) => {
const contentY = formatYLabel(tick as never);
const labelWidth =
Expand Down Expand Up @@ -86,7 +98,59 @@
);
});

return yAxisNodes;
const AxisTitle = useMemo(() => {
if (!title) return null;

const {
text,
position: titlePosition = "center",
xOffset = 2,
font: titleFont,
} = title;

const titleFontToUse = titleFont ?? font;
const titleWidth = getFontGlyphWidth(text, titleFontToUse);
const isRightSide = axisSide === "right";

// Calculate vertical position
const titleY = (() => {
const center = (chartBounds.bottom + chartBounds.top) / 2;
if (titlePosition === "top") return chartBounds.top;
if (titlePosition === "bottom") return chartBounds.bottom;
return center;
})();

// Calculate horizontal offset from axis
const baseOffset = longestLabel + labelOffset + (xOffset ?? 0);
const translateX = isRightSide
? chartBounds.right + baseOffset
: chartBounds.left - baseOffset;

return (
<Group
transform={[
{ translateX },
{ translateY: titleY },
{ rotate: isRightSide ? Math.PI / 2 : -Math.PI / 2 },
]}
>
<Text
color={labelColor}
text={text}
font={titleFontToUse!}
x={-titleWidth / 2} // Center text horizontally
y={0}
/>
</Group>
);
}, [title, chartBounds, font, labelOffset, fontSize, labelColor, axisSide]);

Check warning on line 146 in lib/src/cartesian/components/YAxis.tsx

View workflow job for this annotation

GitHub Actions / build

React Hook useMemo has a missing dependency: 'longestLabel'. Either include it or remove the dependency array

return (
<>
{yAxisNodes}
{AxisTitle}
</>
);
};

export const YAxisDefaults = {
Expand Down
41 changes: 41 additions & 0 deletions lib/src/cartesian/utils/transformInputData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,47 @@ export const transformInputData = <
}
}

if (xAxis?.title) {
const fontSize = xAxis.title.font?.getSize() ?? xAxis.font?.getSize() ?? 0;

const yScaleRange0 = yAxesTransformed[0]?.yScale.range().at(0) as number;
const yScaleRange1 = yAxesTransformed[0]?.yScale.range().at(-1) as number;
const offset = fontSize + (xAxis?.title?.yOffset ?? 0);

// bottom, outset
if (xAxis?.axisSide === "bottom" && xAxis?.labelPosition === "outset") {
yAxesTransformed[0]?.yScale.range([yScaleRange0, yScaleRange1 - offset]);
}

// top, outset
if (xAxis?.axisSide === "top" && xAxis?.labelPosition === "outset") {
yAxesTransformed[0]?.yScale.range([yScaleRange0 + offset, yScaleRange1]);
}
}

if (yAxes.some((yAxis) => yAxis.title)) {
yAxes.forEach((yAxis) => {
if (yAxis.title) {
const fontSize =
yAxis.title.font?.getSize() ?? yAxis.font?.getSize() ?? 0;
const offset = fontSize + (yAxis?.title?.xOffset ?? 0);

const xScaleRange0 = xScale.range().at(0) as number;
const xScaleRange1 = xScale.range().at(-1) as number;

// left, outset
if (yAxis?.axisSide === "left" && yAxis?.labelPosition === "outset") {
xScale.range([xScaleRange0 + offset, xScaleRange1]);
}

// right, outset
if (yAxis?.axisSide === "right" && yAxis?.labelPosition === "outset") {
xScale.range([xScaleRange0, xScaleRange1 - offset]);
}
}
});
}

const ox = ixNum.map((x) => xScale(x)!);

return {
Expand Down
Loading