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)}:       $${
d.pointTotal
}` +
"<br/>" +
`YTD:     $${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!!!