Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | 1x 1x | import React, { FC, useRef, useMemo, useLayoutEffect } from "react";
import { ScaleLinear } from "d3-scale";
import * as d3 from "d3";
// Simple text width estimation (average character width ~7px for 12px font)
const estimateTextWidth = (text: string): number => {
return text.length * 7;
};
interface Props {
yScale: ScaleLinear<number, number>;
width: number;
height: number;
highlightZeroLine?: boolean;
margin: { top: number; right: number; bottom: number; left: number };
yAxisFormat?: (d: number) => string;
yTicksQty?: number;
}
const YaxisLinear: FC<Props> = ({
yScale,
width,
height,
highlightZeroLine = true,
margin,
yTicksQty,
yAxisFormat,
}) => {
const ref = useRef<SVGGElement>(null);
const yAxisConfig = useMemo(() => {
const axis = d3
.axisLeft(yScale)
.tickSize(0)
.tickPadding(10)
.ticks(yTicksQty || 10);
if (yAxisFormat) {
axis.tickFormat(yAxisFormat);
}
return axis;
}, [yScale, yTicksQty, yAxisFormat]);
// Memoize the previous yScale domain to detect changes
const prevYScaleDomain = useRef(yScale.domain());
useLayoutEffect(() => {
const g = d3.select(ref.current);
const currentYScaleDomain = yScale.domain();
const yScaleChanged =
JSON.stringify(currentYScaleDomain) !== JSON.stringify(prevYScaleDomain.current);
prevYScaleDomain.current = currentYScaleDomain;
// Initial render with transition
g.transition()
.duration(750)
.attr("transform", `translate(${margin.left > 0 ? margin.left : 0},0)`)
.call(yAxisConfig)
.call(g => g.select(".domain").attr("stroke-opacity", 0))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").attr("stroke-opacity", 0))
.call(g => g.selectAll(".tick line").remove())
.call(g => g.selectAll(".tick-line").remove());
// Remove existing tick lines before updating
g.selectAll(".tick line").remove();
// Update transitions
g.selectAll(".tick text").transition().duration(750).style("opacity", 1);
// Calculate dynamic tick line length based on label width
const tickData = g.selectAll(".tick").data();
const maxLabelWidth = Math.max(...tickData.map(d => {
const formatValue = yAxisFormat ? yAxisFormat(d as number) : String(d);
return estimateTextWidth(formatValue);
}));
// Calculate adjusted grid width: full width minus label overlap
const labelOverlapBuffer = 10; // 10px buffer
const adjustedGridWidth = Math.max(
width - margin.right - margin.left - Math.max(0, maxLabelWidth - margin.left + labelOverlapBuffer),
50 // Minimum grid line length
);
// Only animate tick lines if y-scale changed
const tickLines = g
.selectAll(".tick")
.append("line")
.attr("class", "tick-line")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", 0)
.attr("y2", 0)
.style("stroke-dasharray", "2,2")
.style("stroke", "lightgray")
.style("opacity", 1);
if (yScaleChanged) {
tickLines
.transition()
.duration(750)
.attr("x2", adjustedGridWidth)
.each(function (d) {
if (d === 0) {
d3.select(this)
.classed("zero-line", true)
.attr("stroke", highlightZeroLine ? "#000" : "lightgray")
.attr("stroke-width", "1");
}
});
} else {
tickLines.attr("x2", adjustedGridWidth).each(function (d) {
if (d === 0) {
d3.select(this)
.classed("zero-line", true)
.attr("stroke", highlightZeroLine ? "#000" : "lightgray")
.attr("stroke-width", "1");
}
});
}
}, [yScale, width, height, margin, highlightZeroLine, yAxisConfig]);
return <g ref={ref}></g>;
};
export default YaxisLinear;
|