This commit is contained in:
2026-09-21 17:42:25 +03:30
parent 6dbb50b57f
commit 37d1770b16
4 changed files with 11752 additions and 358 deletions
@@ -1869,7 +1869,11 @@
display: flex;
justify-content: center;
align-items: center;
min-height: 220px;
height: 320px;
min-height: 320px;
max-height: 320px;
overflow: auto;
position: relative;
}
.dashboard-chart-body svg {
@@ -1889,6 +1893,63 @@
padding: 20px;
}
.chart-zoom-container {
transform-origin: center center;
transition: transform 0.15s ease;
cursor: grab;
}
.chart-zoom-container:active {
cursor: grabbing;
}
.chart-zoom-indicator {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(26, 54, 93, 0.85);
color: #fff;
padding: 3px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: 700;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
z-index: 5;
}
.chart-zoom-indicator.visible {
opacity: 1;
}
.chart-zoom-reset {
position: absolute;
top: 8px;
right: 8px;
background: rgba(26, 54, 93, 0.85);
color: #fff;
border: none;
padding: 4px 8px;
border-radius: 6px;
font-size: 10px;
font-weight: 700;
cursor: pointer;
opacity: 0;
transition: opacity 0.3s;
z-index: 5;
font-family: inherit;
}
.chart-zoom-reset.visible {
opacity: 1;
}
.chart-zoom-reset:hover {
background: var(--accent);
color: var(--primary-dark);
}
.dashboard-empty-chart span {
font-size: 32px;
opacity: 0.4;
@@ -1913,20 +1974,39 @@
.d3-tooltip {
position: fixed;
background: var(--primary-dark);
background: rgba(15, 35, 65, 0.95);
color: #fff;
padding: 8px 12px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
pointer-events: none;
z-index: 3000;
z-index: 10000;
display: none;
font-family: "Vazirmatn", sans-serif;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(201, 169, 97, 0.4);
max-width: 280px;
word-wrap: break-word;
direction: rtl;
backdrop-filter: none;
}
.d3-tooltip.visible {
display: block;
animation: tooltipFadeIn 0.15s ease;
}
@keyframes tooltipFadeIn {
from {
opacity: 0;
transform: translateY(2px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.rb-toolbar {
@@ -3581,8 +3661,8 @@
<div id="versionAnalysisContainer" class="dashboard-container"></div>
</div>
<div class="modal-footer">
<div class="dashboard-footer-stats" id="verAnalysisFooterStats"></div>
<button class="btn btn-ghost" data-close-modal>بستن</button>
<div class="dashboard-footer-stats" id="verAnalysisFooterStats"></div>
</div>
</div>
</div>
@@ -4053,6 +4133,7 @@
<div class="galaxy-tooltip" id="galaxyTooltip"></div>
<div class="d3-tooltip" id="d3Tooltip"></div>
<!-- Scripts -->
<script>
/* ==================== DATA ==================== */
const projectData = {
@@ -4236,10 +4317,28 @@
function showD3Tooltip(e, text) {
let tt = document.getElementById("d3Tooltip");
if (!tt) return;
if (!text) {
hideD3Tooltip();
return;
}
tt.textContent = text;
tt.classList.add("visible");
tt.style.left = e.clientX + 12 + "px";
tt.style.top = e.clientY + 12 + "px";
const ttRect = tt.getBoundingClientRect();
const padding = 12;
let x = e.clientX + padding;
let y = e.clientY + padding;
if (x + ttRect.width > window.innerWidth) {
x = e.clientX - ttRect.width - padding;
}
if (y + ttRect.height > window.innerHeight) {
y = e.clientY - ttRect.height - padding;
}
tt.style.left = x + "px";
tt.style.top = y + "px";
}
function hideD3Tooltip() {
const tt = document.getElementById("d3Tooltip");
@@ -6123,6 +6222,14 @@
d.pwd + d.pwod > 0
? Math.round((d.pwd / (d.pwd + d.pwod)) * 100)
: 0;
setTimeout(() => {
document.querySelectorAll("#dashboardContainer .dashboard-chart-body").forEach((body) => {
const svg = body.querySelector("svg");
if (svg) {
ChartZoomManager.enable(body, svg);
}
});
}, 200);
document.getElementById("dashboardFooterStats").innerHTML =
`<span>📦 محصولات: <span class="stat-highlight">${d.products}</span></span><span>🌿 پروژه‌ها: <span class="stat-highlight">${d.projects}</span></span><span>🚀 استقرارها: <span class="stat-highlight">${d.deploys}</span></span><span>📈 پوشش استقرار: <span class="stat-highlight">${coverage}%</span></span>`;
},
@@ -6368,6 +6475,138 @@
return c[t] || "#718096";
},
};
/* ==================== CHART ZOOM MANAGER ==================== */
const ChartZoomManager = {
_zoomStates: new Map(),
/**
* فعال‌سازی zoom روی یک container
* @param {HTMLElement} container - عنصر dashboard-chart-body
* @param {SVGElement} svg - عنصر SVG داخلی
*/
enable(container, svg) {
if (!container || !svg || container.dataset.zoomEnabled === "true") return;
container.dataset.zoomEnabled = "true";
const state = { scale: 1, translateX: 0, translateY: 0, isPanning: false };
this._zoomStates.set(container, state);
// 🆕 ایجاد wrapper برای transform
const wrapper = document.createElement("div");
wrapper.className = "chart-zoom-container";
wrapper.style.width = "100%";
wrapper.style.height = "100%";
wrapper.style.display = "flex";
wrapper.style.justifyContent = "center";
wrapper.style.alignItems = "center";
// انتقال SVG به wrapper
while (container.firstChild) {
wrapper.appendChild(container.firstChild);
}
container.appendChild(wrapper);
// 🆕 ایجاد نشانگر zoom
const indicator = document.createElement("div");
indicator.className = "chart-zoom-indicator";
indicator.textContent = "🔍 100%";
container.appendChild(indicator);
// 🆕 ایجاد دکمه reset
const resetBtn = document.createElement("button");
resetBtn.className = "chart-zoom-reset";
resetBtn.textContent = "🎯 بازنشانی";
resetBtn.onclick = (e) => {
e.stopPropagation();
this.resetZoom(container);
};
container.appendChild(resetBtn);
// 🆕 event listener برای wheel (zoom)
container.addEventListener("wheel", (e) => {
e.preventDefault();
e.stopPropagation();
const delta = e.deltaY > 0 ? 0.9 : 1.1;
const newScale = Math.max(0.5, Math.min(5, state.scale * delta));
state.scale = newScale;
this._applyTransform(container, wrapper, state);
indicator.textContent = `🔍 ${Math.round(newScale * 100)}%`;
indicator.classList.add("visible");
resetBtn.classList.toggle("visible", newScale !== 1);
clearTimeout(state._hideTimer);
state._hideTimer = setTimeout(() => {
indicator.classList.remove("visible");
}, 1500);
}, { passive: false });
// 🆕 event listener برای pan (drag)
container.addEventListener("mousedown", (e) => {
if (e.button !== 0) return;
if (state.scale <= 1) return; // فقط وقتی zoom شده
state.isPanning = true;
state._startX = e.clientX;
state._startY = e.clientY;
state._startTranslateX = state.translateX;
state._startTranslateY = state.translateY;
wrapper.style.cursor = "grabbing";
});
window.addEventListener("mousemove", (e) => {
if (!state.isPanning) return;
const dx = e.clientX - state._startX;
const dy = e.clientY - state._startY;
state.translateX = state._startTranslateX + dx;
state.translateY = state._startTranslateY + dy;
this._applyTransform(container, wrapper, state);
});
window.addEventListener("mouseup", () => {
if (state.isPanning) {
state.isPanning = false;
wrapper.style.cursor = state.scale > 1 ? "grab" : "default";
}
});
// 🆕 dblclick برای reset
container.addEventListener("dblclick", (e) => {
if (e.target === svg || e.target === container) {
this.resetZoom(container);
}
});
},
_applyTransform(container, wrapper, state) {
wrapper.style.transform =
`translate(${state.translateX}px, ${state.translateY}px) scale(${state.scale})`;
},
resetZoom(container) {
const state = this._zoomStates.get(container);
if (!state) return;
state.scale = 1;
state.translateX = 0;
state.translateY = 0;
const wrapper = container.querySelector(".chart-zoom-container");
if (wrapper) {
this._applyTransform(container, wrapper, state);
wrapper.style.cursor = "default";
}
const indicator = container.querySelector(".chart-zoom-indicator");
const resetBtn = container.querySelector(".chart-zoom-reset");
if (indicator) {
indicator.textContent = "🔍 100%";
indicator.classList.add("visible");
setTimeout(() => indicator.classList.remove("visible"), 1500);
}
if (resetBtn) resetBtn.classList.remove("visible");
}
};
/* ==================== GALAXY CHART ==================== */
const GalaxyChartManager = {
svg: null,
@@ -6916,6 +7155,95 @@
}, 2000);
}
},
exportPNG() {
const svg = this.svg;
if (!svg) {
showToast("❌ نموداری برای خروجی وجود ندارد", "error");
return;
}
try {
// 🆕 کپی از SVG اصلی
const svgNode = svg.node();
const clone = svgNode.cloneNode(true);
// 🆕 تنظیم ابعاد
const width = svgNode.clientWidth || 1200;
const height = svgNode.clientHeight || 800;
clone.setAttribute("width", width);
clone.setAttribute("height", height);
clone.setAttribute("viewBox", `0 0 ${width} ${height}`);
// 🆕 اضافه کردن xmlns (الزامی برای تبدیل به تصویر)
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
// 🆕 تبدیل style های inline به attributes (برای سازگاری)
clone.querySelectorAll("*").forEach((el) => {
const computedStyle = window.getComputedStyle(
svgNode.querySelector(`[class="${el.getAttribute("class")}"]`) || el
);
// حفظ font-family برای متن‌ها
if (el.tagName === "text") {
el.setAttribute("font-family", "Vazirmatn, Tahoma, sans-serif");
}
});
// 🆕 serialize به string
const serializer = new XMLSerializer();
let svgString = serializer.serializeToString(clone);
// 🆕 اضافه کردن declaration
svgString = '<?xml version="1.0" standalone="no"?>\r\n' + svgString;
// 🆕 تبدیل به data URL
const svgBlob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
const url = URL.createObjectURL(svgBlob);
// 🆕 ایجاد Image و تبدیل به Canvas
const img = new Image();
img.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = width * 2; // 🆕 کیفیت 2x برای وضوح بیشتر
canvas.height = height * 2;
const ctx = canvas.getContext("2d");
ctx.scale(2, 2);
ctx.fillStyle = "#0a0e27"; // 🆕 پس‌زمینه تیره نمودار کهکشانی
ctx.fillRect(0, 0, width, height);
ctx.drawImage(img, 0, 0, width, height);
// 🆕 دانلود فایل
canvas.toBlob((blob) => {
if (!blob) {
showToast("❌ خطا در تولید تصویر", "error");
return;
}
const pngUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = pngUrl;
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
a.download = `x-release-assistant-chart-${timestamp}.png`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(pngUrl);
URL.revokeObjectURL(url);
showToast("📷 خروجی PNG با موفقیت ذخیره شد", "success");
}, "image/png", 1.0);
};
img.onerror = () => {
showToast("❌ خطا در بارگذاری SVG", "error");
URL.revokeObjectURL(url);
};
img.src = url;
} catch (err) {
showToast("❌ خطا: " + err.message, "error");
}
},
};
/* ==================== AUTOSAVE MANAGER ==================== */
const AutosaveManager = {
@@ -8454,6 +8782,14 @@ ${checklistHtml}
this.renderTypeDonut("depTypeChart", d.typeCounts);
this.renderMatrix("depMatrixChart", d.allProjects, d.matrix);
this.renderRiskList("depRiskList", d);
setTimeout(() => {
document.querySelectorAll(".dashboard-chart-body").forEach((body) => {
const svg = body.querySelector("svg");
if (svg) {
ChartZoomManager.enable(body, svg);
}
});
}, 200);
document.getElementById("depAnalysisFooterStats").innerHTML =
`<span>🔗 کل: <span class="stat-highlight">${d.totalDeps}</span></span><span>⚠️ گلوگاه: <span class="stat-highlight">${d.bottlenecks.length}</span></span><span>🔴 شکسته: <span class="stat-highlight">${d.brokenDeps}</span></span><span>🔵 ایزوله: <span class="stat-highlight">${d.isolated.length}</span></span>`;
},
@@ -8826,88 +9162,85 @@ ${checklistHtml}
);
this.renderProductBarChart("deployProductChart", d.ppc);
this.renderRiskList("deployRiskList", d.riskList);
setTimeout(() => {
document.querySelectorAll(".dashboard-chart-body").forEach((body) => {
const svg = body.querySelector("svg");
if (svg) {
ChartZoomManager.enable(body, svg);
}
});
}, 200);
document.getElementById("deployAnalysisFooterStats").innerHTML =
`<span>🚀 کل: <span class="stat-highlight">${d.total}</span></span><span>📈 پوشش: <span class="stat-highlight">${d.coverage}%</span></span><span>⚠️ ریسک‌ها: <span class="stat-highlight">${d.riskList.length}</span></span>`;
},
renderEnvBarChart(cid, edc) {
renderEnvBarChart(cid, envCounts) {
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
const data = Object.entries(edc).map(([k, v]) => ({
label: k,
value: v,
}));
if (data.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>📊</span><div>داده‌ای نیست</div></div>';
const entries = Object.entries(envCounts).filter((d) => d[1] > 0);
if (entries.length === 0) {
c.innerHTML = '<div class="dashboard-empty-chart"><span>📊</span><div>داده‌ای نیست</div></div>';
return;
}
const w = 320,
hgt = 220,
m = { top: 20, right: 15, bottom: 45, left: 30 };
const iw = w - m.left - m.right,
ih = hgt - m.top - m.bottom;
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", hgt);
const g = svg
.append("g")
.attr("transform", `translate(${m.left},${m.top})`);
const x = d3
.scaleBand()
.domain(data.map((d) => d.label))
.range([0, iw])
.padding(0.3);
const y = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.value) || 1])
.nice()
.range([ih, 0]);
const w = 320, hgt = 220, m = { top: 20, right: 15, bottom: 45, left: 30 };
const iw = w - m.left - m.right, ih = hgt - m.top - m.bottom;
const svg = d3.select(c).append("svg").attr("width", w).attr("height", hgt);
const g = svg.append("g").attr("transform", `translate(${m.left},${m.top})`);
const xScale = d3.scaleBand().domain(entries.map((d) => d[0])).range([0, iw]).padding(0.3);
const yScale = d3.scaleLinear().domain([0, d3.max(entries, (d) => d[1]) || 1]).nice().range([ih, 0]);
g.append("g")
.attr("transform", `translate(0,${ih})`)
.call(d3.axisBottom(x).tickSize(0))
.call(d3.axisBottom(xScale).tickSize(0))
.selectAll("text")
.attr("fill", "var(--text-muted)")
.attr("font-size", "8px")
.attr("font-family", "Vazirmatn")
.attr("transform", "rotate(-20)")
.attr("text-anchor", "end");
g.append("g")
.call(d3.axisLeft(y).ticks(4).tickSize(-iw))
.call(d3.axisLeft(yScale).ticks(4).tickSize(-iw))
.selectAll("text")
.attr("fill", "var(--text-muted)")
.attr("font-size", "9px");
// ✅ اصلاح: استفاده از d به جای x
g.selectAll("rect")
.data(data)
.data(entries)
.join("rect")
.attr("x", (d) => x(d.label))
.attr("y", (d) => y(d.value))
.attr("width", x.bandwidth())
.attr("height", (d) => ih - y(d.value))
.attr("x", (d) => xScale(d[0]))
.attr("y", (d) => yScale(d[1]))
.attr("width", xScale.bandwidth())
.attr("height", (d) => ih - yScale(d[1]))
.attr("fill", "#38a169")
.attr("rx", 4)
.style("cursor", "pointer")
.on("mouseover", function (e, d) {
d3.select(this).attr("fill", "var(--accent)");
showD3Tooltip(e, `${d.label}: ${d.value}`);
showD3Tooltip(e, `${d[0]}: ${d[1]}`);
})
.on("mousemove", (e) => showD3Tooltip(e, ""))
.on("mouseout", function () {
d3.select(this).attr("fill", "#38a169");
hideD3Tooltip();
});
g.selectAll(".bar-label")
.data(data)
.data(entries)
.join("text")
.attr("x", (d) => x(d.label) + x.bandwidth() / 2)
.attr("y", (d) => y(d.value) - 5)
.attr("x", (d) => xScale(d[0]) + xScale.bandwidth() / 2)
.attr("y", (d) => yScale(d[1]) - 5)
.attr("text-anchor", "middle")
.attr("fill", "var(--text)")
.attr("font-size", "10px")
.attr("font-weight", "700")
.text((d) => d.value);
.text((d) => d[1]);
},
renderStatusDonut(cid, pwd, pwod, coverage) {
const c = document.getElementById(cid);
@@ -8991,47 +9324,36 @@ ${checklistHtml}
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
const data = ppc.map((p) => ({
label:
p.title.length > 15 ? p.title.substring(0, 15) + "..." : p.title,
label: p.title.length > 15 ? p.title.substring(0, 15) + "..." : p.title,
value: p.count,
}));
if (data.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>📊</span><div>داده‌ای نیست</div></div>';
c.innerHTML = '<div class="dashboard-empty-chart"><span>📊</span><div>داده‌ای نیست</div></div>';
return;
}
const bh = 28,
gap = 6;
const w = 320,
hgt = data.length * (bh + gap) + 30;
const bh = 28, gap = 6;
const w = 320, hgt = data.length * (bh + gap) + 30;
const m = { top: 10, right: 35, bottom: 10, left: 130 };
const iw = w - m.left - m.right,
ih = hgt - m.top - m.bottom;
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", hgt);
const g = svg
.append("g")
.attr("transform", `translate(${m.left},${m.top})`);
const x = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.value) || 1])
.range([0, iw]);
const y = d3
.scaleBand()
.domain(data.map((d) => d.label))
.range([0, ih])
.padding(0.2);
const iw = w - m.left - m.right, ih = hgt - m.top - m.bottom;
const svg = d3.select(c).append("svg").attr("width", w).attr("height", hgt);
const g = svg.append("g").attr("transform", `translate(${m.left},${m.top})`);
const xScale = d3.scaleLinear().domain([0, d3.max(data, (d) => d.value) || 1]).range([0, iw]);
const yScale = d3.scaleBand().domain(data.map((d) => d.label)).range([0, ih]).padding(0.2);
// ✅ این متد از قبل درست بود (از d استفاده می‌کرد)
g.selectAll("rect")
.data(data)
.join("rect")
.attr("x", 0)
.attr("y", (d) => y(d.label))
.attr("width", (d) => x(d.value))
.attr("height", y.bandwidth())
.attr("y", (d) => yScale(d.label))
.attr("width", (d) => xScale(d.value))
.attr("height", yScale.bandwidth())
.attr("fill", "#3182ce")
.attr("rx", 4)
.style("cursor", "pointer")
@@ -9044,22 +9366,24 @@ ${checklistHtml}
d3.select(this).attr("fill", "#3182ce");
hideD3Tooltip();
});
g.selectAll(".label")
.data(data)
.join("text")
.attr("x", -8)
.attr("y", (d) => y(d.label) + y.bandwidth() / 2)
.attr("y", (d) => yScale(d.label) + yScale.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "end")
.attr("fill", "var(--text)")
.attr("font-size", "9px")
.attr("font-weight", "600")
.text((d) => d.label);
g.selectAll(".value")
.data(data)
.join("text")
.attr("x", (d) => x(d.value) + 5)
.attr("y", (d) => y(d.label) + y.bandwidth() / 2)
.attr("x", (d) => xScale(d.value) + 5)
.attr("y", (d) => yScale(d.label) + yScale.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("fill", "var(--text-muted)")
.attr("font-size", "10px")
@@ -9249,6 +9573,14 @@ ${checklistHtml}
this.renderEnvBarChart("verEnvChart", d.envCounts);
this.renderEnvMatrix("verEnvMatrix", d.envMatrix);
this.renderRiskList("verRiskList", d.riskList);
setTimeout(() => {
document.querySelectorAll(".dashboard-chart-body").forEach((body) => {
const svg = body.querySelector("svg");
if (svg) {
ChartZoomManager.enable(body, svg);
}
});
}, 200);
document.getElementById("verAnalysisFooterStats").innerHTML =
`<span>🔢 کل: <span class="stat-highlight">${d.total}</span></span><span>📦 یکتا: <span class="stat-highlight">${d.uniqueVersions}</span></span><span>📈 میانگین: <span class="stat-highlight">${d.avgVersion}</span></span><span>⚠️ ریسک‌ها: <span class="stat-highlight">${d.riskList.length}</span></span>`;
},
@@ -9256,89 +9588,70 @@ ${checklistHtml}
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
const entries = Object.entries(majorCounts).sort((a, b) =>
a[0].localeCompare(b[0]),
);
const entries = Object.entries(majorCounts).sort((a, b) => a[0].localeCompare(b[0]));
if (entries.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>📊</span><div>داده‌ای نیست</div></div>';
c.innerHTML = '<div class="dashboard-empty-chart"><span>📊</span><div>داده‌ای نیست</div></div>';
return;
}
const w = 320,
hgt = 220,
m = { top: 20, right: 15, bottom: 40, left: 30 };
const iw = w - m.left - m.right,
ih = hgt - m.top - m.bottom;
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", hgt);
const g = svg
.append("g")
.attr("transform", `translate(${m.left},${m.top})`);
const x = d3
.scaleBand()
.domain(entries.map((x) => x[0]))
.range([0, iw])
.padding(0.3);
const y = d3
.scaleLinear()
.domain([0, d3.max(entries, (x) => x[1]) || 1])
.nice()
.range([ih, 0]);
const colors = [
"#4299e1",
"#48bb78",
"#ecc94b",
"#ed64a6",
"#9f7aea",
"#ed8936",
];
const w = 320, hgt = 220, m = { top: 20, right: 15, bottom: 40, left: 30 };
const iw = w - m.left - m.right, ih = hgt - m.top - m.bottom;
const svg = d3.select(c).append("svg").attr("width", w).attr("height", hgt);
const g = svg.append("g").attr("transform", `translate(${m.left},${m.top})`);
const xScale = d3.scaleBand().domain(entries.map((d) => d[0])).range([0, iw]).padding(0.3);
const yScale = d3.scaleLinear().domain([0, d3.max(entries, (d) => d[1]) || 1]).nice().range([ih, 0]);
const colors = ["#4299e1", "#48bb78", "#ecc94b", "#ed64a6", "#9f7aea", "#ed8936"];
g.append("g")
.attr("transform", `translate(0,${ih})`)
.call(d3.axisBottom(x).tickSize(0))
.call(d3.axisBottom(xScale).tickSize(0))
.selectAll("text")
.attr("fill", "var(--text-muted)")
.attr("font-size", "9px")
.attr("font-family", "Vazirmatn");
g.append("g")
.call(d3.axisLeft(y).ticks(4).tickSize(-iw))
.call(d3.axisLeft(yScale).ticks(4).tickSize(-iw))
.selectAll("text")
.attr("fill", "var(--text-muted)")
.attr("font-size", "9px");
// ✅ اصلاح: استفاده از d به جای x
g.selectAll("rect")
.data(entries)
.join("rect")
.attr("x", (x) => x(x[0]))
.attr("y", (x) => y(x[1]))
.attr("width", x.bandwidth())
.attr("height", (x) => ih - y(x[1]))
.attr("fill", (x, i) => colors[i % colors.length])
.attr("x", (d) => xScale(d[0]))
.attr("y", (d) => yScale(d[1]))
.attr("width", xScale.bandwidth())
.attr("height", (d) => ih - yScale(d[1]))
.attr("fill", (d, i) => colors[i % colors.length])
.attr("rx", 4)
.style("cursor", "pointer")
.on("mouseover", function (e, x) {
.on("mouseover", function (e, d) {
d3.select(this).attr("fill", "var(--accent)");
showD3Tooltip(e, `${x[0]}: ${x[1]}`);
showD3Tooltip(e, `${d[0]}: ${d[1]}`);
})
.on("mousemove", (e) => showD3Tooltip(e, ""))
.on("mouseout", function (e, x) {
d3.select(this).attr(
"fill",
colors[entries.indexOf(x) % colors.length],
);
.on("mouseout", function (e, d) {
d3.select(this).attr("fill", colors[entries.indexOf(d) % colors.length]);
hideD3Tooltip();
});
g.selectAll(".bar-label")
.data(entries)
.join("text")
.attr("x", (x) => x(x[0]) + x.bandwidth() / 2)
.attr("y", (x) => y(x[1]) - 5)
.attr("x", (d) => xScale(d[0]) + xScale.bandwidth() / 2)
.attr("y", (d) => yScale(d[1]) - 5)
.attr("text-anchor", "middle")
.attr("fill", "var(--text)")
.attr("font-size", "10px")
.attr("font-weight", "700")
.text((x) => x[1]);
.text((d) => d[1]);
},
renderEnvBarChart(cid, envCounts) {
const c = document.getElementById(cid);
@@ -9633,6 +9946,14 @@ ${checklistHtml}
this.renderSeverityDonut("riskSeverityChart", d);
this.renderTypeBarChart("riskTypeChart", d.risks);
this.renderRiskList("riskDetailList", d.risks);
setTimeout(() => {
document.querySelectorAll(".dashboard-chart-body").forEach((body) => {
const svg = body.querySelector("svg");
if (svg) {
ChartZoomManager.enable(body, svg);
}
});
}, 200);
document.getElementById("riskAnalysisFooterStats").innerHTML =
`<span>🎯 سلامت: <span class="stat-highlight">${d.healthScore}%</span></span><span>⚠️ ریسک‌ها: <span class="stat-highlight">${d.total}</span></span><span>🔴 بحرانی: <span class="stat-highlight">${d.crit}</span></span><span>🟠 بالا: <span class="stat-highlight">${d.high}</span></span>`;
},
@@ -9720,6 +10041,7 @@ ${checklistHtml}
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
const typeLabels = {
broken: "وابستگی شکسته",
"broken-req": "نیازمندی شکسته",
@@ -9730,6 +10052,7 @@ ${checklistHtml}
"no-owner": "بدون مالک",
"empty-environment": "محیط خالی",
};
const typeColors = {
broken: "#e53e3e",
"broken-req": "#e53e3e",
@@ -9740,82 +10063,73 @@ ${checklistHtml}
"no-owner": "#d69e2e",
"empty-environment": "#3182ce",
};
const typeCounts = {};
risks.forEach((r) => {
typeCounts[r.type] = (typeCounts[r.type] || 0) + 1;
});
const entries = Object.entries(typeCounts).sort(
(a, b) => b[1] - a[1],
);
const entries = Object.entries(typeCounts).sort((a, b) => b[1] - a[1]);
if (entries.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>✅</span><div>ریسکی نیست</div></div>';
c.innerHTML = '<div class="dashboard-empty-chart"><span>✅</span><div>ریسکی نیست</div></div>';
return;
}
const bh = 28,
gap = 6;
const w = 320,
hgt = entries.length * (bh + gap) + 30;
const bh = 28, gap = 6;
const w = 320, hgt = entries.length * (bh + gap) + 30;
const m = { top: 10, right: 35, bottom: 10, left: 130 };
const iw = w - m.left - m.right,
ih = hgt - m.top - m.bottom;
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", hgt);
const g = svg
.append("g")
.attr("transform", `translate(${m.left},${m.top})`);
const x = d3
.scaleLinear()
.domain([0, d3.max(entries, (x) => x[1]) || 1])
.range([0, iw]);
const y = d3
.scaleBand()
.domain(entries.map((x) => x[0]))
.range([0, ih])
.padding(0.2);
const iw = w - m.left - m.right, ih = hgt - m.top - m.bottom;
const svg = d3.select(c).append("svg").attr("width", w).attr("height", hgt);
const g = svg.append("g").attr("transform", `translate(${m.left},${m.top})`);
const xScale = d3.scaleLinear().domain([0, d3.max(entries, (d) => d[1]) || 1]).range([0, iw]);
const yScale = d3.scaleBand().domain(entries.map((d) => d[0])).range([0, ih]).padding(0.2);
// ✅ اصلاح: استفاده از d به جای x در arrow functions
g.selectAll("rect")
.data(entries)
.join("rect")
.attr("x", 0)
.attr("y", (x) => y(x[0]))
.attr("width", (x) => x(x[1]))
.attr("height", y.bandwidth())
.attr("fill", (x) => typeColors[x[0]] || "#718096")
.attr("y", (d) => yScale(d[0]))
.attr("width", (d) => xScale(d[1]))
.attr("height", yScale.bandwidth())
.attr("fill", (d) => typeColors[d[0]] || "#718096")
.attr("rx", 4)
.style("cursor", "pointer")
.on("mouseover", function (e, x) {
.on("mouseover", function (e, d) {
d3.select(this).attr("fill", "var(--accent)");
showD3Tooltip(e, `${typeLabels[x[0]] || x[0]}: ${x[1]}`);
showD3Tooltip(e, `${typeLabels[d[0]] || d[0]}: ${d[1]}`);
})
.on("mousemove", (e) => showD3Tooltip(e, ""))
.on("mouseout", function (e, x) {
d3.select(this).attr("fill", typeColors[x[0]] || "#718096");
.on("mouseout", function (e, d) {
d3.select(this).attr("fill", typeColors[d[0]] || "#718096");
hideD3Tooltip();
});
g.selectAll(".label")
.data(entries)
.join("text")
.attr("x", -8)
.attr("y", (x) => y(x[0]) + y.bandwidth() / 2)
.attr("y", (d) => yScale(d[0]) + yScale.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "end")
.attr("fill", "var(--text)")
.attr("font-size", "9px")
.attr("font-weight", "600")
.text((x) => typeLabels[x[0]] || x[0]);
.text((d) => typeLabels[d[0]] || d[0]);
g.selectAll(".value")
.data(entries)
.join("text")
.attr("x", (x) => x(x[1]) + 5)
.attr("y", (x) => y(x[0]) + y.bandwidth() / 2)
.attr("x", (d) => xScale(d[1]) + 5)
.attr("y", (d) => yScale(d[0]) + yScale.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("fill", "var(--text-muted)")
.attr("font-size", "10px")
.attr("font-weight", "700")
.text((x) => x[1]);
.text((d) => d[1]);
},
renderRiskList(cid, risks) {
const c = document.getElementById(cid);
@@ -9929,6 +10243,7 @@ ${checklistHtml}
document
.getElementById("galaxyExportPNG")
.addEventListener("click", () => {
GalaxyChartManager.exportPNG();
showToast("📷 خروجی ذخیره شد", "success");
});
function handleToolSelect(tool, group) {
+1 -1
View File
@@ -1,3 +1,3 @@
- تولتیپ های نمودار D3js بدرستی نمایش نمی یابند.
- در گزارش ها و نمودار هایی که با استفاده از D3js تولید می کنی اندازه همه کارت ها رو یکسان در نظر بگیر و اگر ابعاد نمودار بزرگتر از اندازه کارت بود حالت اسکرول و قابلیت زوم کردن توسط ماوس رو براشون فعال کن.
- در مودال نمودار کهکشانی گزینه خروجی png عمل نمی کند.
- در مودال نمودار کهکشانی گزینه خروجی png عمل نمی کند.
+585 -181
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff