visx icon indicating copy to clipboard operation
visx copied to clipboard

[xy-chart] Responsiveness is broken in some cases?

Open RIP21 opened this issue 5 years ago • 9 comments

I have responsiveness broken in my app, probably due to flex and grid positioning it somehow breaks it. But I'll let you decide whom to blame.

Click to expand:
import React, { useEffect, useMemo } from 'react'
import {
  lightTheme as lt,
  darkTheme as dt,
  Grid,
  AreaSeries,
  Axis,
  XYChart,
  Tooltip,
  buildChartTheme,
} from '@visx/xychart'
import { ThemeConfig } from '@visx/xychart/lib/theme/buildChartTheme'
import { curveMonotoneX } from '@visx/curve'
import { LinearGradient } from '@visx/gradient'
import { Box, useColorMode, Stack, useBreakpointValue } from '@chakra-ui/react'
import { ParentSize } from '@visx/responsive'
import { css } from '@emotion/css'
import { MetricHistoryPoint } from '__generated__/graphql-types.generated'
import { formatMoney } from 'packlets/utils'

const dateScaleConfig = { type: 'band', paddingInner: 0.9 } as const
const moneyScaleConfig = { type: 'linear' } as const

const darkTheme = buildChartTheme(({
  ...dt,
  colors: ['#109CF1'],
} as unknown) as ThemeConfig)

const lightTheme = buildChartTheme(({
  ...lt,
  colors: ['#109CF1'],
} as unknown) as ThemeConfig)

const lineStyle = css`
  stroke-width: 3;
`

export const DashboardGraph = ({ data }: { data: MetricHistoryPoint[] }) => {
  const config = useMemo(
    () => ({
      x: dateScaleConfig,
      y: moneyScaleConfig,
    }),
    [],
  )
  const { colorMode } = useColorMode()
  const theme = colorMode === 'light' ? lightTheme : darkTheme

  useEffect(() => {
    setTimeout(() => {
      window.dispatchEvent(new CustomEvent('scroll'))
    }, 300)
  })

  const xTicks = useBreakpointValue({ base: 3, xl: 5 })

  return (
    <ParentSize>
      {({ width }) => (
        <XYChart
          theme={theme}
          xScale={config.x}
          yScale={config.y}
          height={300}
          width={width - 16}
        >
          <LinearGradient
            id="area-gradient"
            from="#109CF1"
            to="#109CF1"
            toOpacity={0.1}
          />
          <Grid
            rows
            columns={false}
            strokeDasharray="10, 5"
            // @ts-expect-error
            strokeOpacity={0.3}
          />
          <AreaSeries
            dataKey="Cash in"
            data={data}
            fillOpacity={0.4}
            xAccessor={(it) => it.date}
            yAccessor={(it) => it.value}
            fill="url(#area-gradient)"
            stroke="url(#area-gradient)"
            lineProps={{
              className: lineStyle,
            }}
            curve={curveMonotoneX}
          />
          <Axis
            orientation="bottom"
            strokeWidth={0}
            numTicks={xTicks}
            hideAxisLine
            hideTicks
          />
          <Axis orientation="left" numTicks={5} hideAxisLine hideTicks />
          <Tooltip<{ date: string; value: number }>
            showVerticalCrosshair
            snapTooltipToDatumX
            snapTooltipToDatumY
            verticalCrosshairStyle={{
              strokeOpacity: 0.4,
            }}
            showSeriesGlyphs
            glyphStyle={{
              r: 4,
              fill: colorMode === 'light' ? 'white' : 'black',
              strokeWidth: 1.5,
              stroke: '#109CF1',
            }}
            renderTooltip={({ tooltipData }) => (
              <Stack borderRadius={4}>
                <Box>{tooltipData?.nearestDatum?.datum.date}</Box>
                <Box>{formatMoney(tooltipData?.nearestDatum?.datum.value ?? 0)}</Box>
              </Stack>
            )}
          />
        </XYChart>
      )}
    </ParentSize>
  )
}

In order to have my graphs to resize I have to deduct padding of the div that holds XYChart (I guess it's div -> XY Chart generated div -> actual SVGs) So basically padding of the block that holds component above inside.

Here is how it looks when I add into XYChart width prop width - 16 Peek 2021-01-09 02-41

And here when I don't add that or remove <ParentSize/> completely. I'm getting horizontal scrolling after page refresh and no resize. Note that the 2 graphs above are in regular display: flex, and 2x2 graphs at the bottom are in display: grid.

Peek 2021-01-09 02-43

RIP21 avatar Jan 11 '21 21:01 RIP21

Are you facing problems with the longer tickLabels for AnimatedAxis in smaller screen sizes ?

VigneshTrantor avatar Jan 19 '21 10:01 VigneshTrantor

Code above says it all :)

RIP21 avatar Jan 19 '21 10:01 RIP21

If you set your padding to 0, and want your chart to have 100% width, this happens too.

pedroapfilho avatar Mar 31 '21 16:03 pedroapfilho

I saw this problem using react-use-measure too, here: https://github.com/pmndrs/react-use-measure/issues/43

pedroapfilho avatar Apr 08 '21 19:04 pedroapfilho

Seems like this is an issue in react-use-measure then? Will leave this open to track progress on the link @pedroapfilho provided (thanks for digging in there!)

williaster avatar Apr 22 '21 20:04 williaster

@williaster having display: grid; or display: flex; anywhere up the parent chain set for the ParentSize component will break responsive behavior on shrink (smaller than initial render width).

One would be able to reproduce by adding:

body {
    display: grid | flex;
}

to the sandbox-styles.css inside the xychart demo

Preventable by rendering all of your chart stuff inside ScaleSVG component, but xychart simply uses svg. (I've found it out by making my own "xychart")

moki avatar Jun 29 '21 23:06 moki

Seems like the problem is caused by the automatic minimum size of flex item (I guess this also applies to grid items)

The "solution" to get the chart to resize when its parent shrinks is set min-width: 0 on the chart parent element.

Also, from the linked stack overflow response:

If you're dealing with flex items on multiple levels of the HTML structure, it may be necessary to override the default min-width: auto / min-height: auto on items at higher levels.

In my case I had to set min-width: 0 in some parent flex items as well but it did the trick.

m-mujica avatar Jul 11 '22 17:07 m-mujica

I still can't figure out the issue on my end. Can someone assist please? I'm basically using an XYChart but not the ParentSize component as my assumption is that the XYChart comes responsiveness by default compared to the regular Bar or Line components

Here's the below code

export const BarGraph = ({
  bottomNumTicks,
  data,
  hasCurrencyInTooltip = true,
  height,
  margin,
  numTicks,
  tooltipSubtext,
  width,
  xAccessor,
  xValueFormat,
  yAccessor,
  yValueFormat,
}: IGraphProps) => {
  const defaultMargin = { top: 0, right: 0, bottom: 40, left: 80 };

  return (
    <XYChart
      height={height}
      width={width}
      xScale={{ type: "band", padding: handleBarGraphPadding(data) }}
      yScale={{ type: "linear" }}
      margin={margin ? margin : defaultMargin}
    >
      <Grid
        columns={false}
        numTicks={numTicks}
        lineStyle={{
          stroke: colors.black[25],
          strokeWidth: 1,
        }}
      />
      {xAccessor && (
        <Axis
          hideAxisLine
          hideTicks
          orientation="bottom"
          numTicks={bottomNumTicks}
          tickFormat={xValueFormat}
          tickLength={16}
          tickLabelProps={{
            fontSize: 10,
            lineHeight: 14,
            fill: colors.black[300],
            fontFamily: "CircularXXWeb-Regular",
          }}
        />
      )}
      {yAccessor && (
        <Axis
          hideAxisLine
          hideTicks
          orientation="left"
          numTicks={numTicks}
          tickFormat={yValueFormat}
          tickLabelProps={{
            fontSize: 10,
            lineHeight: 14,
            fill: colors.black[300],
            fontFamily: "CircularXXWeb-Regular",
          }}
        />
      )}
      <AnimatedBarSeries
        data={data}
        dataKey="primary"
        xAccessor={xAccessor}
        yAccessor={yAccessor}
        radius={4}
        radiusAll
        colorAccessor={() => colors.blue[500]}
      />
      <Tooltip
        snapTooltipToDatumX
        snapTooltipToDatumY
        style={tooltipStyles}
        renderTooltip={({ tooltipData }) => {
          return (
            <div>
              {Object.entries(tooltipData.datumByKey).map(lineDataArray => {
                const [key, value] = lineDataArray;

                return (
                  <div className={sharedTooltipClasses("top")} key={key}>
                    {hasCurrencyInTooltip ? (
                      <span className="font-medium">
                        AED{" "}
                        {convertToLocaleStringHelper(
                          yAccessor(value.datum as any),
                        )}
                      </span>
                    ) : (
                      <span className="font-medium">
                        {yAccessor(value.datum as any)}
                      </span>
                    )}

                    {tooltipSubtext && (
                      <span className="text-black-200">
                        {tooltipSubtext(value.datum)}
                      </span>
                    )}
                  </div>
                );
              })}
            </div>
          );
        }}
      />
    </XYChart>
  );
};

https://github.com/airbnb/visx/assets/86906789/83b9050c-87a0-4da4-82a8-5217a1c36124

mamo-muj avatar Nov 10 '23 06:11 mamo-muj

okay so looks like flex is an issue. So I switched to grid and that did the trick

mamo-muj avatar Nov 10 '23 07:11 mamo-muj

Closing for now as this isn't a visx-related bug.

hshoff avatar Mar 23 '24 14:03 hshoff