victory icon indicating copy to clipboard operation
victory copied to clipboard

Performance issues on Android with events

Open therealkh opened this issue 2 years ago • 2 comments

Describe the bug Every time onActivated event of VictoryVoronoiContainer is fired - all children components of VictoryChart are being rerendered.

Victory version "victory-core": "^36.5.0", "victory-native": "^36.5.0",

Code Sandbox link

type ChartValueItemFragment = {
  value: number | null;
  valueLow: number | null;
  valueHigh: number | null;
  date: any | null;
  timestamp: number | null;
};


type TransformedCompareValueItem = Omit<ComparisonChartData, 'date'> & {
  date: Date;
};

type TransformedChartValueItem = Omit<ChartValueItemFragment, 'date'> & {
  date: Date;
};

type Point = {
  _x: number;
  _y: number;
};

type Props = {
  xDomain: [Date, Date];
  yDomain: [number, number];
  comparedData: TransformedCompareValueItem[];
  selectedData: TransformedChartValueItem[];
  setActivePoint: (point: Point | null) => void; // setState from parent component
  setComparePoint: (point: Point | null) => void; // setState from parent component
  activePoint: Point | null;
};

function formatLocalDate(
  data: string | Date,
  formatMask: string,
): string {
  const date = new Date(data);
  const offsetInHours = date.getTimezoneOffset();
  return dayjs(data).subtract(offsetInHours, 'minute').format(formatMask);
}

export function formatBigNumber(num: number, digits: number): string {
  const lookup = [
    { value: 1, symbol: '' },
    { value: 1e3, symbol: 'k' },
    { value: 1e6, symbol: 'M' },
    { value: 1e9, symbol: 'G' },
    { value: 1e12, symbol: 'T' },
    { value: 1e15, symbol: 'P' },
    { value: 1e18, symbol: 'E' },
  ];
  const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
  const item = lookup
    .slice()
    .reverse()
    .find(function (item) {
      return num >= item.value;
    });
  return item
    ? (num / item.value).toFixed(digits).replace(rx, '$1') + item.symbol
    : '0';
}


export const ExampleVictoryChart = React.memo(
  ({
    xDomain,
    yDomain,
    setActivePoint,
    setComparePoint,
    comparedData,
    selectedData,
    activePoint,
  }: Props) => {
    const chartColor = colors.green;
    const comparedChartColor = colors.gray;
    const comparedChartStyle = {
      data: {
        stroke: comparedChartColor,
        strokeWidth: 3,
      },
    };

    const verticalLineStyle = {
      data: {
        strokeDasharray: '6,6',
      },
    };
    const chartStyle = {
      data: {
        stroke: chartColor,
        strokeWidth: 3,
      },
    };

    return (
      <VictoryChart
        padding={{
          top: 0,
          right: 0,
          bottom: 50,
          left: 0,
        }}
        theme={VictoryTheme.material}
        domain={{
          x:
            xDomain[0].getTime() === xDomain[1].getTime()
              ? [
                  dayjs(xDomain[0]).subtract(5, 'months').toDate(),
                  new Date(xDomain[0]),
                ]
              : xDomain,
          y: yDomain,
        }}
        containerComponent={
          <VictoryVoronoiContainer
            onActivated={(points) => {
              const mainPoint = points.find(
                (point) => point.childName === 'all',
              );

              setActivePoint(mainPoint || null);

              const comparePoint = points.find(
                (point) => point.childName === 'compare',
              );

              setComparePoint(comparePoint || null);
            }}
            voronoiBlacklist={['line']}
            voronoiDimension="x"
          />
        }>
        <VictoryGroup color={comparedChartColor}>
          <VictoryLine
            name="compare"
            data={comparedData}
            y={(datum: TransformedCompareValueItem) => datum.index}
            x={(datum: ChartValueItemFragment) => new Date(datum.date)}
            style={comparedChartStyle}
          />
        </VictoryGroup>
        <VictoryAxis
          fixLabelOverlap
          domain={xDomain}
          standalone={false}
          tickCount={4}
          tickFormat={(t) => formatLocalDate(t, 'MMM YYYY')}
          theme={VictoryTheme.material}
          style={{
            grid: {
              strokeWidth: 0,
            },
            axis: {
              stroke: colors.silverGray,
              strokeDasharray: '2, 4',
              strokeWidth: 1,
            },
          }}
        />
        <VictoryAxis
          tickFormat={(t: number) => formatBigNumber(t, 1)}
          dependentAxis
          fixLabelOverlap
          domain={yDomain}
          standalone={false}
          theme={VictoryTheme.material}
          style={{
            grid: {
              stroke: colors.silverGray,
              strokeDasharray: '2, 4',
              strokeWidth: 1,
            },
            axis: {
              strokeWidth: 0,
            },
          }}
        />
        <VictoryGroup color={chartColor}>
          <VictoryLine
            data={selectedData}
            name="all"
            y={(datum: ChartValueItemFragment) => datum.value as number}
            x={(datum: ChartValueItemFragment) => new Date(datum.date)}
            style={chartStyle}
          />
        </VictoryGroup>
        <VictoryLine
          x={() => activePoint?._x || 0}
          name="line"
          style={verticalLineStyle}
        />
      </VictoryChart>
    );
  },
);

To Reproduce Steps to reproduce the behavior:

  1. To reproduce you can mock data, try run this code and observe

Expected behavior I expect at least 40-50 fps, not 0-2

Screenshots Screen Shot 2022-09-23 at 21 02 18 🎥Demo: https://user-images.githubusercontent.com/65399389/192005279-b24c899e-61bc-456b-8369-305e45b109f3.mp4

Desktop (please complete the following information):

  • OS: -
  • Browser -
  • Version -

Smartphone (please complete the following information):

  • Device: Samsung Galaxy A53
  • OS: Android
  • Browser -
  • Version: 12

Additional context Please feel free to make comments about code, I am not very familiar about how victory-native works. maybe my code breaks some good practices of victory native. I just need help as soon as possible.

therealkh avatar Sep 23 '22 16:09 therealkh

Hi @scottrippey, there seems to be some reports on performance issues on android. Oldest I could found was reported in February of 2022. Is there a plan for prioritizing this? (Sry if tagging the wrong person)

hamol355 avatar Nov 30 '22 13:11 hamol355

Hello, we just switched from highcharts to victory and our android users report heavy performance regressions even crashing the app :/ we have about 120 data points on a victory line within a voronoi conainer custom labels etc.. Is somebody by any chance working on or planning to work on this topic? Heres some pseudo example of our setup:

const Crosshair = React.memo(({ x, y }) => (
  <G>
    <Circle cx={x} cy={y} r="4" fill="#ffffff" />
    <Circle cx={x} cy={y} r="3" fill={config.color.TEXT_RGBA} />
    <Line x1={0} y1={y} x2={500} y2={y} stroke={config.color.LIGHTGREY} strokeDasharray={1} />
    <Line x1={x} y1={0} x2={x} y2={500} stroke={config.color.LIGHTGREY} strokeDasharray={1} />
  </G>
))

const CrosshairLabel = React.memo(props => <VictoryLabel
  {...props}
  lineHeight={[ 1, 1.5 ]}
  backgroundPadding={10}
  backgroundStyle={{
    stroke: config.color.PRIMARY,
    fill: config.color.WHITE,
    fillOpacity: .8,
  }}
  style={[
    { fontSize: 12 },
    { fontSize: 14, fontWeight: 'bold' },
  ]}
/>)

const CrosshairWithLabel = props => {
  const { activePoints, datum } = props
  const isPointActive = !!(activePoints.some(activePoint => moment(activePoint.x).isSame(moment(datum.x))))
  
  if (!isPointActive) return null

  /**
   * Calculate the offset from the label to the crosshair to prevent the user's thumb covering the label
   */
  const getYOffset = ({ height, y, flyoutHeight, flyoutPadding }) => {
    const DEFAULT_OFFSET = -15
    const thumbOffset = DEFAULT_OFFSET * 2
    const isInLowerHalf = y > height / 2
    const heightOfTooltip = flyoutPadding.top + (flyoutHeight) + flyoutPadding.bottom
    const overlapsWithTop = y - heightOfTooltip + DEFAULT_OFFSET < 0

    return isInLowerHalf ? thumbOffset
      : overlapsWithTop ? heightOfTooltip
        : DEFAULT_OFFSET
  }

  /**
   * Calculates an offset so the tooltip does not move out of visible chart area
   * Usually this is happening by providing the prop `constrainToVisibleArea` to the <VictoryTooltip />
   * but there is a bug that this does not work for victory-native
   *
   * Related ticket: https://github.com/FormidableLabs/victory/issues/2593
   */
  const getXOffset = ({ flyoutPadding, flyoutWidth, width, x }) => {
    const widthOfTooltip = flyoutPadding.left + flyoutWidth + flyoutPadding.right
    const halfWidthOfTooltip = widthOfTooltip / 2
    const overlapsWithYAxis = x + widthOfTooltip > width
    
    return overlapsWithYAxis ? halfWidthOfTooltip * -1 : 0
  }

  const centerOffset = {
    y: getYOffset,
    x: getXOffset,
  }

  return (
    <VictoryTooltip
      animate={false}
      {...props}
      active
      renderInPortal={false}
      cornerRadius={0}
      constrainToVisibleArea
      flyoutComponent={<Crosshair {...props} />}
      labelComponent={<CrosshairLabel />}
      centerOffset={centerOffset }
    />
  )
}

// Main Chart:
const chart = <VictoryChart
    scale={scale}
    theme={myTheme}
    domainPadding={{ y: 10 }}
    width={chartWidth}
    padding={{ top: 30, bottom: 50, right: PADDING_RIGHT }}
    containerComponent={
      <VictoryVoronoiContainer
        voronoiDimension="x"
        voronoiBlacklist={[ 'current-price' ]}
        onActivated={setActivePoints}
      />
    }
  >
    <VictoryAxis {...xAxisConfig} />
    <VictoryAxis {...yAxisConfig} tickLabelComponent={<YAxisTickLabel currentPrice={lastPoint.y} />} />
    <VictoryLine
      data={data}
      labels={getLabels}
      labelComponent={<CrosshairWithLabel data={data} activePoints={activePoints} />}
    />
    <VictoryLine
      animate={false}
      name="current-price"
      data={currentPriceData}
      style={{ data: { strokeDasharray: 2 }}}
    />
  </VictoryChart>

sem4phor avatar Jun 15 '23 14:06 sem4phor

We are focusing our efforts on a rebuild of Victory specific to native in https://commerce.nearform.com/open-source/victory-native/

carbonrobot avatar Feb 23 '24 19:02 carbonrobot