delete the useless files ,also update the chart and canvas #7
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useEffect, useRef, useState } from "react";
|
||||
import * as d3 from "d3";
|
||||
import React from "react";
|
||||
import { formatNumber } from "~/lib/utils";
|
||||
|
||||
export type CompanyInfo = {
|
||||
|
|
@ -17,141 +16,162 @@ export type D3ImageInfoProps = {
|
|||
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]);
|
||||
|
||||
const InfoBox = ({ company, style }: { company: CompanyInfo; style :any }) => {
|
||||
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 className={`info-box`} style={style}>
|
||||
<div className="info-box-content">
|
||||
<div className="info-row">
|
||||
<div className="info-label">درآمد:</div>
|
||||
<div className="info-value revenue">{formatNumber(company?.revenue || 0)}</div>
|
||||
<div className="info-unit">میلیون ریال</div>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<div className="info-label">هزینه:</div>
|
||||
<div className="info-value cost">{formatNumber(company?.cost || 0)}</div>
|
||||
<div className="info-unit">میلیون ریال</div>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<div className="info-label">ظرفیت:</div>
|
||||
<div className="info-value capacity">{formatNumber(company?.capacity || 0)}</div>
|
||||
<div className="info-unit">تن در سال</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
||||
// Ensure we have exactly 6 companies
|
||||
const displayCompanies = companies;
|
||||
|
||||
// Positions inside a 5x4 grid (col, row)
|
||||
// Layout keeps same visual logic: left/middle/right on two bands with spacing grid around
|
||||
const gridPositions = [
|
||||
{ col: 2, row: 2 , colI : 1 , rowI : 2 , name : "بسپاران"}, // left - top band
|
||||
{ col: 3, row: 2 , colI : 3 , rowI : 1 , name : "خوارزمی"}, // middle top (image sits in row 2, info box goes to row 1)
|
||||
{ col: 4, row: 2 ,colI : 5 , rowI : 2 , name : "فراورش 1"}, // right - top band
|
||||
{ col: 2, row: 3 , colI : 1 , rowI : 3 , name : "کیمیا"}, // left - bottom band
|
||||
{ col: 3, row: 3 , colI : 3, rowI : 4 , name : "آب نیرو"}, // middle bottom (image sits in row 3, info box goes to row 4)
|
||||
{ col: 4, row: 3 , colI : 5 , rowI : 3 , name : "فراورش 2"}, // right - bottom band
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full h-[500px] rounded-xl p-4">
|
||||
<div dir="ltr" className="company-grid-container">
|
||||
{displayCompanies.map((company, index) => {
|
||||
const gp = gridPositions.find(v => v.name === company.name) || { col: (index % 5) + 1, row: Math.floor(index / 5) + 1 };
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
key={company.id}
|
||||
className={`company-item`}
|
||||
style={{ gridColumn: gp.col, gridRow: gp.row }}
|
||||
>
|
||||
<div className="company-image-containe">
|
||||
<img
|
||||
src={company.imageUrl}
|
||||
alt={company.name}
|
||||
className="company-image"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{company.name}
|
||||
</div>
|
||||
<InfoBox company={company} key={index +10} style={{ gridColumn: gp.colI , gridRow: gp.rowI }} />
|
||||
</>);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.company-grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-rows: repeat(4, 1fr);
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.company-item {
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.company-image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.company-image {
|
||||
object-fit: contain;
|
||||
height : 100px;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
border: 1px solid #3F415A;
|
||||
border-radius: 10px;
|
||||
height: max-content;
|
||||
align-self : center;
|
||||
justify-self : center;
|
||||
padding : .2rem 0 ;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
|
||||
.info-box-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
position : relative;
|
||||
margin: 0rem 1rem;
|
||||
display: flex;
|
||||
gap : 1rem;
|
||||
justify-content : space-between;
|
||||
padding: 0rem .8rem;
|
||||
direction: rtl;
|
||||
|
||||
&:has(.info-value.revenue) {border-bottom: 1px solid #3AEA83;}
|
||||
&:has(.info-value.cost) {border-bottom: 1px solid #F76276;}
|
||||
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #FFFFFF;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #34D399;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
margin-bottom : .5rem;
|
||||
}
|
||||
|
||||
.info-value.revenue { color: #fff;}
|
||||
.info-value.cost { color: #fff; }
|
||||
.info-value.capacity { color: #fff; }
|
||||
|
||||
.info-unit {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
bottom: 0;
|
||||
color: #9CA3AF;
|
||||
font-size: 8px;
|
||||
font-weight: 400;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export function DashboardCustomBarChart({
|
|||
<span className="text-white font-bold text-base">
|
||||
{formatNumber(item.value)}
|
||||
</span>
|
||||
<span className="text-white font-persian font-medium text-sm w-max">
|
||||
<span className="text-[#3F415A] font-persian font-medium text-sm w-max">
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -126,19 +126,19 @@ export function DashboardHome() {
|
|||
let incCapacityTotal = 0;
|
||||
const chartRows = rows.map((r) => {
|
||||
const rel = r?.related_company ?? "-";
|
||||
const preFee = Number(r?.pre_innovation_fee_sum ?? 0);
|
||||
const costRed = Number(r?.innovation_cost_reduction_sum ?? 0);
|
||||
const preCap = Number(r?.pre_project_production_capacity_sum ?? 0);
|
||||
const incCap = Number(r?.increased_capacity_after_innovation_sum ?? 0);
|
||||
const preInc = Number(r?.pre_project_income_sum ?? 0);
|
||||
const incInc = Number(r?.increased_income_after_innovation_sum ?? 0);
|
||||
const preFee = Number(r?.pre_innovation_fee_sum ?? 0) > 0 ? r?.pre_innovation_fee_sum : 0;
|
||||
const costRed = Number(r?.innovation_cost_reduction_sum ?? 0) > 0 ? r?.innovation_cost_reduction_sum : 0;
|
||||
const preCap = Number(r?.pre_project_production_capacity_sum ?? 0) > 0 ? r?.pre_project_production_capacity_sum : 0;
|
||||
const incCap = Number(r?.increased_capacity_after_innovation_sum ?? 0) > 0 ? r?.increased_capacity_after_innovation_sum : 0;
|
||||
const preInc = Number(r?.pre_project_income_sum ?? 0) > 0 ? r?.pre_project_income_sum : 0;
|
||||
const incInc = Number(r?.increased_income_after_innovation_sum ?? 0) > 0 ? r?.increased_income_after_innovation_sum : 0;
|
||||
|
||||
incCapacityTotal += incCap;
|
||||
|
||||
const capacityPct = preCap > 0 ? (incCap / preCap) * 100 : 0;
|
||||
const revenuePct = preInc > 0 ? (incInc / preInc) * 100 : 0;
|
||||
const costPct = preFee > 0 ? (costRed / preFee) * 100 : 0;
|
||||
|
||||
console.log(costRed)
|
||||
return {
|
||||
category: rel,
|
||||
capacity: isFinite(capacityPct) ? capacityPct : 0,
|
||||
|
|
@ -475,7 +475,7 @@ export function DashboardHome() {
|
|||
<div className="flex items-center justify-center flex-col">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-center">
|
||||
<p className="text-5xl font-bold text-green-400">
|
||||
<p className="text-4xl font-bold text-green-400">
|
||||
{formatNumber(
|
||||
dashboardData.topData
|
||||
?.technology_innovation_based_revenue_growth || "0",
|
||||
|
|
@ -487,7 +487,7 @@ export function DashboardHome() {
|
|||
</div>
|
||||
<span className="text-6xl font-thin text-gray-600">/</span>
|
||||
<div className="text-center">
|
||||
<p className="text-5xl font-bold text-green-400">
|
||||
<p className="text-4xl font-bold text-green-400">
|
||||
{formatNumber(
|
||||
Math.round(
|
||||
dashboardData.topData
|
||||
|
|
@ -518,7 +518,7 @@ export function DashboardHome() {
|
|||
<div className="flex items-center justify-center flex-col">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-center">
|
||||
<p className="text-5xl font-bold text-green-400">
|
||||
<p className="text-4xl font-bold text-green-400">
|
||||
{formatNumber(
|
||||
Math.round(
|
||||
parseFloat(
|
||||
|
|
@ -536,7 +536,7 @@ export function DashboardHome() {
|
|||
</div>
|
||||
<span className="text-6xl font-thin text-gray-600">/</span>
|
||||
<div className="text-center">
|
||||
<p className="text-5xl font-bold text-green-400">
|
||||
<p className="text-4xl font-bold text-green-400">
|
||||
{formatNumber(
|
||||
Math.round(
|
||||
dashboardData.topData
|
||||
|
|
@ -642,7 +642,7 @@ export function DashboardHome() {
|
|||
</ChartContainer>
|
||||
<div className="font-bold font-persian text-center">
|
||||
<div className="flex flex-col justify-between items-center gap-2">
|
||||
<span className="flex font-bold items-center gap-1">
|
||||
<span className="flex font-bold items-center gap-1 mr-auto">
|
||||
<div className="font-light">مصوب :</div>
|
||||
{formatNumber(
|
||||
Math.round(
|
||||
|
|
@ -655,7 +655,7 @@ export function DashboardHome() {
|
|||
),
|
||||
)}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 font-bold">
|
||||
<span className="flex items-center gap-1 font-bold mr-auto">
|
||||
<div className="font-light">جذب شده :</div>
|
||||
{formatNumber(
|
||||
Math.round(
|
||||
|
|
@ -700,7 +700,7 @@ export function DashboardHome() {
|
|||
</TabsContent>
|
||||
|
||||
<TabsContent value="canvas" className="w-ful h-full">
|
||||
<div className="p-4">
|
||||
<div className="p-4 h-full">
|
||||
<D3ImageInfo
|
||||
companies={
|
||||
companyChartData.map((item) => {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
type ChartConfig,
|
||||
ChartContainer,
|
||||
} from "~/components/ui/chart";
|
||||
import { formatNumber } from "~/lib/utils";
|
||||
|
||||
export type CompanyChartDatum = {
|
||||
category: string; // related_company
|
||||
|
|
@ -22,7 +23,7 @@ export type CompanyChartDatum = {
|
|||
const chartConfig = {
|
||||
capacity: {
|
||||
label: "افزایش ظرفیت",
|
||||
color: "#60A5FA", // Blue-400
|
||||
color: "#69C8EA",
|
||||
},
|
||||
revenue: {
|
||||
label: "افزایش درآمد",
|
||||
|
|
@ -34,6 +35,7 @@ const chartConfig = {
|
|||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
|
||||
export function InteractiveBarChart({
|
||||
data,
|
||||
}: {
|
||||
|
|
@ -47,7 +49,8 @@ export function InteractiveBarChart({
|
|||
accessibilityLayer
|
||||
data={data}
|
||||
margin={{ left: 12, right: 12 }}
|
||||
barCategoryGap="42%"
|
||||
barGap={15}
|
||||
barSize={8}
|
||||
>
|
||||
<CartesianGrid vertical={false} stroke="#475569" />
|
||||
<XAxis
|
||||
|
|
@ -64,14 +67,14 @@ export function InteractiveBarChart({
|
|||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tick={{ fill: "#94a3b8", fontSize: 12 }}
|
||||
tickFormatter={(value) => `${value}%`}
|
||||
tickFormatter={(value) => `${formatNumber(Math.round(value))}%`}
|
||||
/>
|
||||
<Bar dataKey="capacity" fill={chartConfig.capacity.color} radius={[8, 8, 0, 0]}>
|
||||
<LabelList
|
||||
dataKey="capacity"
|
||||
position="top"
|
||||
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
|
||||
formatter={(v: number) => `${Math.round(v)}%`}
|
||||
formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
|
||||
/>
|
||||
</Bar>
|
||||
<Bar dataKey="revenue" fill={chartConfig.revenue.color} radius={[8, 8, 0, 0]}>
|
||||
|
|
@ -79,7 +82,7 @@ export function InteractiveBarChart({
|
|||
dataKey="revenue"
|
||||
position="top"
|
||||
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
|
||||
formatter={(v: number) => `${Math.round(v)}%`}
|
||||
formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
|
||||
/>
|
||||
</Bar>
|
||||
<Bar dataKey="cost" fill={chartConfig.cost.color} radius={[8, 8, 0, 0]}>
|
||||
|
|
@ -87,7 +90,7 @@ export function InteractiveBarChart({
|
|||
dataKey="cost"
|
||||
position="top"
|
||||
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
|
||||
formatter={(v: number) => `${Math.round(v)}%`}
|
||||
formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
|
||||
/>
|
||||
</Bar>
|
||||
</BarChart>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user