inogen/app/components/dashboard/d3-image-info.tsx

158 lines
4.5 KiB
TypeScript

import React, { useEffect, useRef, useState } from "react";
import * as d3 from "d3";
import { formatNumber } from "~/lib/utils";
export type CompanyInfo = {
id: string;
imageUrl: string;
name: string;
costReduction: number; // absolute value
revenue?: number;
capacity?: number;
};
export type D3ImageInfoProps = {
companies: CompanyInfo[]; // exactly 6 items
width?: number;
height?: number;
};
export function D3ImageInfo({ companies, width = 900, height = 400 }: D3ImageInfoProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const svgRef = useRef<SVGSVGElement | null>(null);
const draw = () => {
if (!containerRef.current || !svgRef.current) return;
const container = containerRef.current;
const svg = d3.select(svgRef.current);
const W = Math.max(480, container.clientWidth || width);
const H = Math.max(300, height);
svg.attr("width", W).attr("height", H);
svg.selectAll("*").remove();
const padding = 10;
const cols = 3;
const rows = 2;
const boxWidth = (W - padding * (cols + 1)) / cols;
const boxHeight = (H - padding * (rows + 1)) / rows;
const group = svg.append("g").attr("transform", `translate(${padding}, ${padding})`);
companies.forEach((company, i) => {
const col = i % cols;
const row = Math.floor(i / cols);
const x = col * (boxWidth + padding);
const y = row * (boxHeight + padding);
const companyGroup = group.append("g").attr("transform", `translate(${x}, ${y})`);
// Draw background box
companyGroup
.append("rect")
.attr("width", boxWidth)
.attr("height", boxHeight)
.attr("rx", 10)
.attr("ry", 10)
.attr("fill", "transparent")
// Draw image
const imgSize = Math.min(boxWidth, boxHeight) * 0.5;
companyGroup
.append("image")
.attr("href", company.imageUrl)
.attr("x", 10)
.attr("y", 10)
.attr("width", imgSize)
.attr("height", imgSize)
.attr("preserveAspectRatio", "xMidYMid slice")
.style("background", "transparent");
// Adjust positions to match picture
// Position image slightly left and info box to right with spacing
const infoX = imgSize + 30;
const infoY = 10;
const infoWidth = 120;
const infoHeight = imgSize;
const infoGroup = companyGroup.append("g");
infoGroup
.append("rect")
.attr("x", infoX)
.attr("y", infoY)
.attr("width", infoWidth)
.attr("height", infoHeight)
.attr("rx", 10)
.attr("ry", 10)
.attr("fill", "transparent")
.attr("stroke", "#3F415A")
.attr("stroke-width", 1);
// Add text inside info box
const lineHeight = 20;
infoGroup
.append("text")
.attr("x", imgSize + 10 )
.attr("y", infoY + imgSize + 10)
.attr("fill", "#FFFFFF")
.attr("font-weight", "700")
.attr("font-size", 14)
.text(company.name);
infoGroup
.append("text")
.attr("x", infoX + imgSize)
.attr("y", infoY + lineHeight )
.attr("fill", "#FFFFFF")
.attr("font-size", 12)
.text(`درآمد: ${formatNumber(company?.revenue || "0")}`);
infoGroup
.append("text")
.attr("x", infoX + imgSize -20 )
.attr("y", infoY + lineHeight +5 )
.attr("fill", "#ACACAC")
.attr("font-size", 6)
.text(`میلیون ریال`);
infoGroup
.append("text")
.attr("x", infoX + imgSize)
.attr("y", infoY + lineHeight *2 )
.attr("fill", "#FFFFFF")
.attr("font-size", 12)
.text(`هزینه: ${formatNumber(company?.cost || "0")} میلیون ریال`);
infoGroup
.append("text")
.attr("x", infoX + imgSize)
.attr("y", infoY + lineHeight * 3 )
.attr("fill", "#FFFFFF")
.attr("font-size", 12)
.text(`ظرفیت: ${formatNumber(company?.capacity || "0")} تن در سال`);
// Remove click handlers and popup
companyGroup.style("cursor", "default");
});
};
useEffect(() => {
draw();
const ro = new ResizeObserver(() => draw());
if (containerRef.current) ro.observe(containerRef.current);
return () => ro.disconnect();
}, [companies, width, height]);
return (
<div className="w-full h-full">
<div ref={containerRef} className="w-full h-[400px]">
<svg ref={svgRef} className="block w-full h-full"></svg>
</div>
</div>
);
}