All files / components/shared YaxisLinear.tsx

5.26% Statements 2/38
0% Branches 0/19
0% Functions 0/12
5.26% Lines 2/38

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;