158 lines
4.5 KiB
TypeScript
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>
|
|
);
|
|
}
|