airbnb / visx

🐯 visx | visualization components
https://airbnb.io/visx
MIT License
19.4k stars 712 forks source link

Date Alignment Question #1853

Closed gabeb03 closed 3 months ago

gabeb03 commented 3 months ago

I'm making a bar chart using visx, and I'm running into this issue where the dates on the x-axis are one day behind the actual data:

Screenshot 2024-06-13 at 11 53 28 AM

The domain that was passed to the bar chart component was from May 31st, but you can see that the leftmost tick is May 30. This may be related to this question, but I'm looking to use band scales instead of scaleUtc, which was the solution for that question.

Here's how I am setting up the xScale:

const dateDomain = (start: Date, end: Date) => {
  const domain = [];
  let current = start;
  while (current < end) {
    domain.push(current.toISOString().substring(0, 10));
    current = new Date(current.getTime() + 86400000);
  }
  return domain;
};

const xScale = scaleBand({
    domain: dateDomain(start, end),
    range: [marginLeft, width - marginRight],
    padding: 0.45,
});

dateDomain is returning the correct array (if start is June 1, the first element of the returned array is June 1). Here's what I wrote to render the bar chart:

const format = timeFormat("%b %d");

const formatDate = (value: Date | NumberLike | string): string => {
  if (value instanceof Date) {
    return format(value);
  } else if (typeof value === "string") {
    return format(new Date(value));
  }

  return String(value.valueOf());
};

const getDate = (d: DataPoint) => d.date;
const getDateDomain = (d: DataPoint) => getDate(d).toISOString().substring(0, 10);

const dateDomain = (start: Date, end: Date) => {
  const domain = [];
  let current = start;
  while (current < end) {
    domain.push(current.toISOString().substring(0, 10));
    current = new Date(current.getTime() + 86400000);
  }
  return domain;
};

// x axis:

 <AxisBottom
  scale={xScale}
  top={height - marginBottom}
  tickFormat={formatDate}
  hideTicks
  hideAxisLine
  numTicks={5}
  tickLabelProps={() => ({
    fontFamily: $theme.typography.font200.fontFamily,
    fill: $theme.colors.primary500,
    fontSize: "12px",
    textAnchor: "middle",
  })}
/>

// Rendering the bars: 

<BarStack color={colorScale} data={data} keys={keys} x={getDateDomain} xScale={xScale} yScale={yScale}>
    {(barStacks) => {
      return barStacks.map((barStack) => {
        return barStack.bars.map((bar) => {
          return (
            <rect
              key={`barstack-${barStack.index}-${bar.index}`}
              x={bar.x}
              y={bar.y}
              width={bar.width}
              height={bar.height}
              fill={bar.color}
              onMouseLeave={() => {
                tooltipTimeout.current = setTimeout(() => {
                  hideTooltip();
                }, 150);
              }}
              onMouseMove={(event) => {
                if (tooltipTimeout.current) clearTimeout(tooltipTimeout.current);

                const eventSvgCoords = localPoint(event);
                const left = bar.x + bar.width / 2;

                showTooltip({
                  tooltipData: bar,
                  tooltipTop: eventSvgCoords?.y,
                  tooltipLeft: left,
                });
              }}
            />
          );
        });
      });
    }}
</BarStack>

Does anything jump out at you?

williaster commented 3 months ago

scaleBand really should just be treating things as strings, so if you verified that the domain is correct my guess would be the date formatter is off - e.g., when it gets 06-01-2024, it looks like it's creating a new Date and maybe in that process it's throwing the timezone off and going back to May? you could possibly verify by returning the string directly in the formatDate function?

gabeb03 commented 3 months ago

That was it!

Apparently, it's an issue with the way the Date object in JavaScript interprets the date string. When you pass a date string in the format "YYYY-MM-DD" to the Date constructor, it is treated as a UTC date by default, not as a local date.

Here's how I fixed formatDate so that it converted the date to the local time zone:

const formatDate = (value: Date | NumberLike | string): string => {
  if (value instanceof Date) {
    return format(value);
  } else if (typeof value === "string") { 
    // When Date is given the year, month & day as separate parameters, it returns the Date in the local timezone... weird
    const [year, month, day] = value.split("-").map(Number);
    return format(new Date(year, month - 1, day));
  }

  return String(value.valueOf());
};

Thanks so William, I really appreciate how you tirelessly help devs like me use the tool :) ! I absolutely love visx; it's such a joy to use.