Success creating first line chart! But how to make width responsive?

special thanks, @libbey for your help getting me up and running on my line chart! But now I’m having trouble trying to get it to be responsive. I have the chart being rendered inside a responsive CSS box, but the things I’m trying to make the chart’s width resize are not working. I have been trying to experiment with viewbox settings so far, but they don’t seem to be responsive to window changes. I’ve also tried creating a function with a window listener event for referencing width but was unsuccessful. If anyone can offer any insight I’d greatly appreciate it!

Here is my current code for my DrawChart component:

import * as d3 from "d3";
import * as React from "react";

interface ChartPoint {
  date: Date;
  pointTotal: number;
  cum: number;
  ytd: number;
}

interface ChartLine {
  color: string;
  name: string;
  points: ChartPoint[];
}

function lineChart(svgRef: React.RefObject<SVGSVGElement>, data: ChartLine[]) {
  const svg = d3.select(svgRef.current);

  const width = 1200;
  const height = 580;
  const margin = 50;
  const duration = 250;
  const lineOpacity = "1";
  const circleOpacity = "0.85";
  const circleRadius = 7;
  const circleRadiusHover = 6;

  const tooltip = d3
    .select("body")
    .append("div")
    .style("position", "absolute")
    .style("z-index", "10")
    .style("visibility", "hidden")
    .style("background-color", "#ffffff");

  const emptyChart = 0 === data.length;

  var d = new Date();

  /* Scale - // map through all the months for empty chart dates*/
  const [minX, maxX] = emptyChart
    ? [d.setMonth(d.getMonth() - 24), d.setMonth(d.getMonth() - 24)]
    : d3.extent<ChartPoint, Date>(data[0].points, (d) => d.date);

  const xScale = d3
    .scaleTime()
    .domain([minX!, maxX!])
    .range([0, width - margin]);

  const [minY, maxY] = emptyChart
    ? [0, 100000]
    : d3.extent<number, number>(
        data.map((set) => set.points.map((point) => point.pointTotal)).flat(),
        (d) => d
      );

  // @ts-ignore
  const yScale = d3
    .scaleLinear()
    .domain([minY!, maxY!])
    .range([height - margin, 0])
    .nice();

  /* Add SVG */
  svg
    .attr("width", width)
    .attr("height", height)
    .attr("preserveAspectRatio", "xMidYMid meet")
    .append("g")
    .attr("transform", `translate(${margin}, ${margin})`);

  const xAxis = d3
    .axisBottom(xScale)
    .tickSizeOuter(0)
    .tickSize(height - margin)
    .tickFormat(
      d3.timeFormat("%b") as unknown as (
        dv: number | { valueOf(): number },
        i: number
      ) => string
    )
    .ticks(23)
    .tickPadding(15);

  const yAxis = d3
    .axisLeft(yScale)
    .tickSize(margin - width)
    .tickSizeOuter(0)
    .ticks(12)
    .tickPadding(30);

  // Add the X Axis
  svg
    .append("g")
    .attr("class", "x axis")
    .attr("transform", `translate(${margin}, ${margin})`)
    .attr("font-family", '"Roboto", "sans-serif"')
    .call(xAxis)
    .call((g) =>
      g
        .selectAll(".tick line")
        .attr("class", "axis_tick")
        .attr("stroke", "#556066")
    );

  // Add the Y Axis
  svg
    .append("g")
    .attr("class", "y axis")
    .attr("transform", `translate(${margin}, ${margin})`)
    .attr("font-family", '"Roboto", "sans-serif"')
    .call(yAxis)
    .call((g) =>
      g
        .selectAll(".tick line")
        .attr("class", "axis_bar")
        .attr("stroke", "#556066")
    )
    .attr("stroke-dasharray", "5")
    .append("line")
    .attr("class", "zero-line")
    .attr("stroke", "#000000")
    .attr("stroke-width", 3)
    .attr("x1", 0)
    .attr("y1", yScale(0))
    .attr("x2", width - margin)
    .attr("y2", yScale(0));

  if (!emptyChart) {
    /* Add line into SVG */
    const line = d3
      .line<ChartPoint>()
      .x((d) => xScale(d.date))
      .y((d) => yScale(d.ytd));

    const lines = svg
      .append("g")
      .attr("class", "lines")
      .attr("transform", `translate(${margin}, ${margin})`);

    // draws out line and different points
    lines
      .selectAll("line-group")
      .data(data)
      .enter()
      .append("g")
      .attr("class", "line-group")
      .append("path")
      .attr("class", "line")
      .attr("d", (d) => line(d.points))
      // line color that connects dots
      // map through different lines with different colors for each line
      .style("stroke", (d, i) => d.color)
      .style("fill", "none")
      .style("opacity", lineOpacity)
      .style("stroke-width", 5);

    // /* Add circles in the line */
    lines
      .selectAll("circle-group")
      .data(data)
      .enter()
      .append("g")
      .style("fill", (d, i) => d.color)
      .selectAll("circle")
      .data((d) => d.points)
      .enter()
      .append("g")
      .attr("class", "circle")
      .on("mouseover", function (_e: MouseEvent, d) {
        // display amount on hovering of points -- tooltip
        d3.select<SVGGElement, ChartPoint>(this).style("cursor", "pointer");
        tooltip
          .html(
            `${d3.timeFormat("%b %Y")(d.date)}: &nbsp &nbsp &nbsp $${
              d.pointTotal
            }` +
              "<br/>" +
              `YTD: &nbsp &nbsp $${d.ytd}`
          )
          .style("visibility", "visible")
          .style("color", "white")
          .style("background-color", "#403D38")
          .style("font-size", "12px")
          .style("width", "130px")
          // padding
          .style("padding", ".7rem 1.5rem .7rem 1.5rem")
          .style("font-weight", "100")
          .style("font-family", "Roboto")
          .style("border-radius", "4px")
          .style("left", _e.pageX + 5 + "px")
          .style("top", _e.pageY - 28 + "px");
      })
      .on("mouseout", function () {
        d3.select(this)
          .style("cursor", "none")
          .transition()
          .duration(duration)
          .selectAll(".text")
          .remove();
        tooltip.style("visibility", "hidden");
      })
      .append("circle")
      .attr("cx", (d) => xScale(d.date!))
      .attr("cy", (d) => yScale(d.ytd))
      .attr("r", circleRadius)
      .style("opacity", circleOpacity)
      .on("mouseover", function () {
        d3.select(this)
          .transition()
          .duration(duration)
          .attr("r", circleRadiusHover);
      })
      .on("mouseout", function () {
        d3.select(this).transition().duration(duration).attr("r", circleRadius);
      });
  }
}

interface DrawChart {
  data: ChartLine[];
}

export const DrawChart = ({ data }: DrawChart) => {
  const svgRef = React.useRef<SVGSVGElement>(null);

  React.useEffect(() => {
    d3.select(svgRef.current).selectAll("g > *").remove();
    lineChart(svgRef, data);
  }, [data]);

  return <svg ref={svgRef} />;
};

And here is my MonthlyLineReport component that imports it:

import { ReportListing } from "../../../generatedTypes";
import getSortedDates from "../../../utils/getSortedDates";
import { CircleLed } from "../StatusLed";
import { DrawChart } from "./DrawChart";
interface MonthlyLineReportProps {
  lineGraph: ReportListing;
}

export const MonthlyLineReport = ({ lineGraph }: MonthlyLineReportProps) => {
  const sortedRevenueByDate = getSortedDates(lineGraph?.revenue ?? []);
  const sortedProfitByDate = getSortedDates(lineGraph?.profit ?? []);
  const sortedExpensesByDate = getSortedDates(lineGraph?.expenses ?? []);

  return (
    <>
      <div className="c-line-widget">
        <div className="l-flex-between">
          <div className="c-line-widget__title">
            Financials / Year-Over-Year
          </div>

          <div className="l-flex-between">
            <div className="c-stats-widget__section">
              <CircleLed fill="#CC493D" />
              <div className="c-line-widget__title u-padding-side">
                Expenses
              </div>
            </div>
            <div className="c-stats-widget__section">
              <CircleLed fill="#4CBF4C" />
              <div className="c-line-widget__title u-padding-side">Profit</div>
            </div>
            <div className="c-stats-widget__section">
              <CircleLed fill="#2E99E6" />
              <div className="c-line-widget__title u-padding-side">Revenue</div>
            </div>
          </div>
        </div>

        <div className="c-svg-box">
          <DrawChart
            data={[
              {
                name: "Revenue",
                color: "#2E99E6",
                points:
                  sortedRevenueByDate?.map((points) => ({
                    date: new Date(points?.date as string),
                    pointTotal: points.pointTotal as number,
                    cum: points.cum as number,
                    ytd: points.ytd as number,
                  })) || [],
              },
              {
                name: "Profit",
                color: "#4CBF4C",
                points:
                  sortedProfitByDate?.map((points) => ({
                    date: new Date(points.date as string),
                    pointTotal: points.pointTotal as number,
                    cum: points.cum as number,
                    ytd: points.ytd as number,
                  })) || [],
              },
              {
                name: "Expenses",
                color: "#CC493D",
                points:
                  sortedExpensesByDate?.map((points) => ({
                    date: new Date(points.date as string),
                    pointTotal: points.pointTotal as number,
                    cum: points.cum as number,
                    ytd: points.ytd as number,
                  })) || [],
              },
            ]}
          />
        </div>
      </div>
    </>
  );
};

Thanks!!!

1 Like

Hi! Glad you’re up and running! At first glance, it looks like width gets set to a constant value of 1200, and isn’t changed after that, so that may be part of the issue? But on a separate note, this doesn’t appear to be an Observable notebook, so may be more of a D3 question, and you may want to post the question in the D3 Community Slack org. Here is the link to join that org.