Skip to content

Commit

Permalink
Fix stacked bar rounded corners for non-uniform datasets (#386)
Browse files Browse the repository at this point in the history
Co-authored-by: David Josefson <[email protected]>
  • Loading branch information
davidjosefson and davidjosefsonfnx authored Oct 7, 2024
1 parent ababd9a commit e1f8914
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 27 deletions.
6 changes: 6 additions & 0 deletions .changeset/pink-flowers-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"example": patch
"victory-native": patch
---

Fix stacked bar rounded corners for non-uniform datasets and add support for positive and negative values in the same column.
2 changes: 1 addition & 1 deletion example/app/consts/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const ChartRoutes: {
{
title: "Negative Bar Charts",
description:
"These charts demonstrate how negative values look with Bar and Bar Group charts.",
"These charts demonstrate how negative values look with Bar, Bar Group and Stacked Bar charts.",
path: "/negative-bar-charts",
},
{
Expand Down
110 changes: 104 additions & 6 deletions example/app/negative-bar-charts.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import { LinearGradient, useFont, vec } from "@shopify/react-native-skia";
import React, { useState } from "react";
import { SafeAreaView, ScrollView, StyleSheet, View } from "react-native";
import { Bar, BarGroup, CartesianChart } from "victory-native";
import { Bar, BarGroup, CartesianChart, StackedBar } from "victory-native";
import { useDarkMode } from "react-native-dark";
import inter from "../assets/inter-medium.ttf";
import { appColors } from "./consts/colors";
import { Button } from "../components/Button";
import { InfoCard } from "../components/InfoCard";
import { Text } from "../components/Text";
import { descriptionForRoute } from "./consts/routes";
import { Checkbox } from "../components/Checkbox";

const STACKED_DATA = (length: number = 5) =>
Array.from({ length }, (_, index) => {
const y = Math.random() < 0.4 ? 0 : Math.floor(Math.random() * 9);
const z = Math.random() < 0.4 ? 0 : Math.floor(Math.random() * -9);
return {
x: index + 1,
y: y === 0 && z === 0 ? Math.floor(Math.random() * 9) : y,
z: z === 0 && y === 0 ? Math.floor(Math.random() * -9) : z,
};
});

const GROUP_DATA = (length: number = 10) =>
Array.from({ length }, (_, index) => ({
Expand All @@ -29,12 +42,19 @@ export default function NegativeBarChartsPage(props: { segment: string }) {
const isDark = useDarkMode();
const [data, setData] = useState(DATA(10));
const [groupData, setGroupData] = useState(GROUP_DATA(5));
const [stackedData, setStackedData] = useState(STACKED_DATA(5));
const [innerPadding] = useState(0.33);

const [roundedCorner] = useState(5);
const [topLeft, setTopLeft] = useState(true);
const [topRight, setTopRight] = useState(true);
const [bottomRight, setBottomRight] = useState(false);
const [bottomLeft, setBottomLeft] = useState(false);

function shuffleData() {
setData(DATA(10));
setGroupData(GROUP_DATA(5));
setStackedData(STACKED_DATA(5));
}

return (
Expand Down Expand Up @@ -62,8 +82,10 @@ export default function NegativeBarChartsPage(props: { segment: string }) {
animate={{ type: "spring" }}
innerPadding={innerPadding}
roundedCorners={{
topLeft: roundedCorner,
topRight: roundedCorner,
topLeft: topLeft ? roundedCorner : 0,
topRight: topRight ? roundedCorner : 0,
bottomLeft: bottomLeft ? roundedCorner : 0,
bottomRight: bottomRight ? roundedCorner : 0,
}}
>
<LinearGradient
Expand Down Expand Up @@ -96,8 +118,10 @@ export default function NegativeBarChartsPage(props: { segment: string }) {
betweenGroupPadding={0.4}
withinGroupPadding={0.1}
roundedCorners={{
topLeft: roundedCorner,
topRight: roundedCorner,
topLeft: topLeft ? roundedCorner : 0,
topRight: topRight ? roundedCorner : 0,
bottomLeft: bottomLeft ? roundedCorner : 0,
bottomRight: bottomRight ? roundedCorner : 0,
}}
>
<BarGroup.Bar points={points.y} animate={{ type: "timing" }}>
Expand Down Expand Up @@ -125,6 +149,46 @@ export default function NegativeBarChartsPage(props: { segment: string }) {
)}
</CartesianChart>
</View>
<View style={styles.chart}>
<CartesianChart
xKey="x"
yKeys={["y", "z"]}
padding={5}
domainPadding={{ left: 50, right: 50, top: 0 }}
domain={{ y: [-10, 10] }}
axisOptions={{
font,
formatXLabel: (value) => {
const date = new Date(2023, value - 1);
return date.toLocaleString("default", { month: "short" });
},
lineColor: isDark ? "#71717a" : "#d4d4d8",
labelColor: isDark ? appColors.text.dark : appColors.text.light,
}}
data={stackedData}
>
{({ points, chartBounds }) => {
return (
<StackedBar
animate={{ type: "timing" }}
barWidth={25}
innerPadding={0.33}
chartBounds={chartBounds}
points={[points.y, points.z]}
colors={["rgba(59,239,101,0.67)", "rgba(244,71,71,0.63)"]}
barOptions={({ isBottom, isTop }) => ({
roundedCorners: {
topLeft: topLeft && isTop ? roundedCorner : 0,
topRight: topRight && isTop ? roundedCorner : 0,
bottomLeft: bottomLeft && isBottom ? roundedCorner : 0,
bottomRight: bottomRight && isBottom ? roundedCorner : 0,
},
})}
/>
);
}}
</CartesianChart>
</View>

<ScrollView
style={styles.optionsScrollView}
Expand All @@ -145,6 +209,40 @@ export default function NegativeBarChartsPage(props: { segment: string }) {
title="Shuffle Data"
/>
</View>
<Text style={{ fontWeight: "bold" }}>Rounded corners</Text>
<View
style={{
flexDirection: "row",
gap: 12,
marginTop: 10,
marginBottom: 16,
}}
>
<View style={{ gap: 10 }}>
<Checkbox
label={"Top left"}
checked={topLeft}
onChange={() => setTopLeft(!topLeft)}
/>
<Checkbox
label={"Bottom left"}
checked={bottomLeft}
onChange={() => setBottomLeft(!bottomLeft)}
/>
</View>
<View style={{ gap: 10 }}>
<Checkbox
label={"Top right"}
checked={topRight}
onChange={() => setTopRight(!topRight)}
/>
<Checkbox
label={"Bottom right"}
checked={bottomRight}
onChange={() => setBottomRight(!bottomRight)}
/>
</View>
</View>
</ScrollView>
</SafeAreaView>
</>
Expand All @@ -160,7 +258,7 @@ const styles = StyleSheet.create({
},
},
chart: {
flex: 1.5,
flex: 1,
},
optionsScrollView: {
flex: 1,
Expand Down
62 changes: 62 additions & 0 deletions example/app/stacked-bar-charts-complex.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,61 @@ const CustomChildrenChart = () => {
);
};

const NonUniformDataSet = () => {
const font = useFont(inter, 12);
const isDark = useDarkMode();
const data = [
{ month: 1, favouriteCount: 50, listenCount: 50, sales: 50 },
{ month: 2, listenCount: 50, sales: 50 },
{ month: 3, sales: 50 },
{ month: 4, listenCount: 50, sales: 0 },
{ month: 5, favouriteCount: 50, listenCount: 50, sales: 0 },
];
const roundedCorner = 5;

return (
<View style={{ flex: 1 }}>
<CartesianChart
xKey="month"
padding={5}
yKeys={["favouriteCount", "listenCount", "sales"]}
domainPadding={{ left: 50, right: 50, top: 0 }}
domain={{ y: [0, 200] }}
axisOptions={{
font,
formatXLabel: (value) => {
const date = new Date(2023, value - 1);
return date.toLocaleString("default", { month: "short" });
},
lineColor: isDark ? "#71717a" : "#d4d4d8",
labelColor: isDark ? appColors.text.dark : appColors.text.light,
}}
data={data}
>
{({ points, chartBounds }) => {
return (
<StackedBar
barWidth={45}
innerPadding={0.33}
chartBounds={chartBounds}
points={[points.favouriteCount, points.listenCount, points.sales]}
colors={["blue", "red", "green"]}
barOptions={({ isBottom, isTop }) => ({
roundedCorners: {
topLeft: isTop ? roundedCorner : 0,
topRight: isTop ? roundedCorner : 0,
bottomRight: isBottom ? roundedCorner : 0,
bottomLeft: isBottom ? roundedCorner : 0,
},
})}
/>
);
}}
</CartesianChart>
</View>
);
};

export default function StackedBarChartsComplexPage() {
return (
<>
Expand All @@ -238,6 +293,13 @@ export default function StackedBarChartsComplexPage() {
<Text style={styles.title}>Stacked chart with custom children</Text>
<CustomChildrenChart />
</View>
<View style={styles.chartContainer}>
<Text style={styles.title}>
Rounded corners where some data points are missing or values are
zero
</Text>
<NonUniformDataSet />
</View>
</ScrollView>
</SafeAreaView>
</>
Expand Down
40 changes: 40 additions & 0 deletions example/components/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as React from "react";
import { Pressable, StyleSheet, View } from "react-native";
import Ionicons from "@expo/vector-icons/Ionicons";
import { Text } from "./Text";

type Props = {
label?: string;
checked: boolean;
onChange(): void;
};

export const Checkbox = ({ label, checked, onChange }: Props) => {
return (
<View style={{ flexDirection: "row", gap: 10, alignItems: "center" }}>
<Pressable
style={[styles.checkboxBase, checked && styles.checkboxChecked]}
onPress={onChange}
>
{checked && <Ionicons name="checkmark" size={18} color="white" />}
</Pressable>
{label ? <Text>{label}</Text> : null}
</View>
);
};

const styles = StyleSheet.create({
checkboxBase: {
width: 24,
height: 24,
justifyContent: "center",
alignItems: "center",
borderRadius: 4,
borderWidth: 2,
borderColor: "coral",
backgroundColor: "transparent",
},
checkboxChecked: {
backgroundColor: "coral",
},
});
Loading

0 comments on commit e1f8914

Please sign in to comment.