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

domainPadding does not work as expected #472

Open
ljukas opened this issue Jan 10, 2025 · 7 comments
Open

domainPadding does not work as expected #472

ljukas opened this issue Jan 10, 2025 · 7 comments
Labels
bug Something isn't working enhancement New feature or request

Comments

@ljukas
Copy link
Contributor

ljukas commented Jan 10, 2025

Describe Your Environment

What version of victory-native-xl are you using? (can be found by running npm list --depth 0 victory-native)
[email protected]

What version of React and React Native are you using?
"react": "18.2.0",
"react-native": "0.74.5",

What version of Reanimated and React Native Skia are you using?
"react-native-reanimated": "3.12.1",
"@shopify/react-native-skia": "1.2.3",

Are you using Expo or React Native CLI?
Expo

What platform are you on? (e.g., iOS, Android)
Both

Describe the Problem

Using domainPadding does not insert the points with the set value. Which results in making predictable bar graphs very hard.

import { printJson } from '@/utils';
import { trpc } from '@/utils/trpc';
import { memo } from 'react';
import { Bar, CartesianChart } from 'victory-native';

export const MortgageInterestChart = memo(() => {
  const [interestForecast] = trpc.mortgage.interestForecast.useSuspenseQuery();

  return (
    <CartesianChart
      data={interestForecast.data}
      xKey="year"
      yKeys={['amountPerMonth']}
    >
      {({ points, chartBounds }) => {
        return <Bar points={points.amountPerMonth} chartBounds={chartBounds} />;
      }}
    </CartesianChart>
  );
});

This very simple code renders a very basic bar graph.

Data is the following:

[
  {
    "year": 2025,
    "amountPerMonth": 20084
  },
  {
    "year": 2026,
    "amountPerMonth": 19705
  },
  {
    "year": 2027,
    "amountPerMonth": 19485
  }
]

Which results in the following points by default:

[
  {
    "x": 4,
    "xValue": 2025,
    "y": 4.726153846153842,
    "yValue": 20084
  },
  {
    "x": 165.5,
    "xValue": 2026,
    "y": 116.67692307692309,
    "yValue": 19705
  },
  {
    "x": 327,
    "xValue": 2027,
    "y": 181.66153846153844,
    "yValue": 19485
  }
]

We can se here that the first bar has its midpoint in x = 4

We now want to use barWidth together with domainPadding to make sure each bar renders within the bounds and no part of each bar is outside the rendered area (as it is by default). So we set barWidth to 50, and domainPadding left and right to 25. My logic here would be that the rendered chart is moved from the left and right side 25 "pixels" and the rendered x would now be 4 + 25 for the first point and 327 - 25 for the last point. Hence moving the graph according to the domainPadding.

However, doing this as below.

import { printJson } from '@/utils';
import { trpc } from '@/utils/trpc';
import { memo } from 'react';
import { Bar, CartesianChart } from 'victory-native';

export const MortgageInterestChart = memo(() => {
  const [interestForecast] = trpc.mortgage.interestForecast.useSuspenseQuery();

  return (
    <CartesianChart
      data={interestForecast.data}
      xKey="year"
      yKeys={['amountPerMonth']}
      domainPadding={{ left: 25, right: 25 }}
    >
      {({ points, chartBounds }) => {
        return (
          <Bar
            points={points.amountPerMonth}
            barWidth={50}
            chartBounds={chartBounds}
          />
        );
      }}
    </CartesianChart>
  );
});

Leads to the following chart data:

[
  {
    "x": 25.648793565694813,
    "xValue": 2025,
    "y": 4.726153846153842,
    "yValue": 20084
  },
  {
    "x": 165.5000000000159,
    "xValue": 2026,
    "y": 116.67692307692309,
    "yValue": 19705
  },
  {
    "x": 305.351206434337,
    "xValue": 2027,
    "y": 181.66153846153844,
    "yValue": 19485
  }
]

As seen here the first bar is not inset 4 + 25, but instead to 25.64.... This leads to some of the bar not being inside the drawn area.

This can easily be seen in the rendered graph

image

Expected behavior: [What you expect to happen]
I expect domainPadding to actually move the graph the given number

Actual behavior: [What actually happens]
DomainPadding does render the graph at the expected position instead it does some calculation internally and ends up with the incorrect position for the graphed points

@carbonrobot carbonrobot added the bug Something isn't working label Jan 10, 2025
@djm158 djm158 self-assigned this Jan 14, 2025
@trujic1000
Copy link

domainPadding seems to be messing with scrolling (panning) as well. I don't have a full example, but the issue that I had is when setting domainPadding={10} and enabled panning (on the x axis) with a preset viewport, I was only able to scroll within the viewport and not through the whole graph. After commenting out the domainPadding, everything worked as expected

@zibs
Copy link
Contributor

zibs commented Jan 20, 2025

If you do want to provide a full example @trujic1000 we can take a look.

@trujic1000
Copy link

@zibs Here's a minimal reproduction code

import { View } from "react-native"
import {
  CartesianChart,
  Line,
  Scatter,
  useChartTransformState,
} from "victory-native"

const MONTH_WIDTH = 60
const mockData = Array.from({ length: 10 }, (_, i) => ({
  x: i * MONTH_WIDTH,
  y: Math.random() * 10,
}))

export function MinimalReproduction() {
  const { state } = useChartTransformState({})

  return (
    <View style={{ height: 350 }}>
      <CartesianChart
        data={mockData}
        xKey="x"
        yKeys={["y"]}
        viewport={{ x: [0, MONTH_WIDTH * 4] }} // Show first 4 months
        padding={{ right: 20, bottom: 40 }}
        domainPadding={10} // This causes the issue
        transformState={state}
        transformConfig={{
          pan: {
            enabled: true,
            dimensions: "x",
          },
          pinch: {
            enabled: false,
          },
        }}
      >
        {({ points }) => (
          <>
            <Line
              points={points.y}
              color="blue"
              curveType="linear"
              strokeWidth={1}
            />
            <Scatter
              points={points.y}
              shape="circle"
              radius={4}
              color="blue"
            />
          </>
        )}
      </CartesianChart>
    </View>
  )
}

The issue can be described as follows:

  1. When domainPadding is set (in this case to 10), the chart's internal domain calculations seem to conflict with the viewport and panning functionality.
  2. Expected behavior:
  • User should be able to pan/scroll horizontally through all data points
  • Panning should work smoothly across the entire dataset
  1. Actual behavior:
  • Panning is restricted to the initial viewport area
  • Cannot scroll to see data points beyond the initial viewport
  • The chart appears to be "stuck" within the initial view range
  1. The problem appears to be related to how domainPadding affects the internal domain calculations and their interaction with the viewport boundaries.
  2. To reproduce:
  • Run the minimal example
  • Try to pan/scroll horizontally
  • Notice that you cannot scroll past the initial viewport area (first 4 months)
  • Remove domainPadding={10} and observe that panning works as expected

@djm158
Copy link

djm158 commented Jan 24, 2025

Hi @ljukas thanks for the detailed bug report! I took a look into this issue.

The cause of the bar inset differential you're seeing is due to how we generate the x axis in cartesian charts. Every chart type (including bar charts with categorical data) scales its x axis using a linear scale where we account for the domain padding here. This scale is then used to generate the points array here.

In order to generate the points array as you expect in your reproduction, we would need to support different scale types (for this example, likely https://d3js.org/d3-scale/band).

@zibs going to mark this as an enhancement and then spin out a separate issue for the panning issue reported by @trujic1000

@djm158
Copy link

djm158 commented Jan 24, 2025

For future contributors, I created a branch with the reproductions in this thread https://github.com/FormidableLabs/victory-native-xl/tree/fix/472-bar-domain-padding-clip-bounds

I attempted a fix by adjusting the clipRect bounds, but ultimately wasn't able to solve the root cause of the discrepancy.

@djm158 djm158 added the enhancement New feature or request label Jan 24, 2025
@djm158 djm158 removed their assignment Jan 24, 2025
@ljukas
Copy link
Contributor Author

ljukas commented Jan 28, 2025

@djm158 Thanks for the reply.

This means the documentation is incorrect:

Image

DomainPadding should draw the chart for example 20 points from the left when domainPadding = { left: 20 }, not do something weird with the domain, just output the chart 20 points from the left.

I have in my app written a new CartesianChart wrapper so this can be done more predictable.

Instead of the inverting you do here and doing domainPadding on the domain I instead just add padding the range as can be seen here:

Image

This means we dont need to use band scales or similar. In my implementation I use the exact some way to generate the ox as you linked above.

Another weird quirk from the victory-native cartesian chart regarding the output range is that given no domainPadding at all, the leftMost point of the chart will be 4, instead of 0.

Image

This chart, gives the following chartBounds:

chartBounds {"bottom": 100, "left": 4, "right": 327, "top": 0}

You can see that the left most point is not on the edge, like the last point is in this screenshot

Image

I think this is because you generate some padding from calculating stuff for the axis even though no axis is rendered.

This is the same chart generated via my cartesianChart component

Image

As you can se here the left most and rightmost points are on the edge

You can find my reimplementation of the CartesianChart and related stuff here:
https://github.com/ljukas/cartersian-chart

This is very basic implementation so there is not press-stuff or zoom etc. This is just so I can render basic charts correctly without weird padding etc

@adbl
Copy link

adbl commented Jan 29, 2025

@ljukas great bug report! I'm also struggling with this issue, trying to get precise bar widths/paddings. In my case, I depend on touch interactions etc. Do you think a workaround would be support a custom (x) scale function as prop to CartesianChart?

@djm158 @zibs thanks for your work with this library! if this is a low-prio bug, would you accept a PR with the above suggestions?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

6 participants