last ...
This commit is contained in:
@@ -1869,7 +1869,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 220px;
|
height: 320px;
|
||||||
|
min-height: 320px;
|
||||||
|
max-height: 320px;
|
||||||
|
overflow: auto;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-chart-body svg {
|
.dashboard-chart-body svg {
|
||||||
@@ -1889,6 +1893,63 @@
|
|||||||
padding: 20px;
|
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 {
|
.dashboard-empty-chart span {
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
@@ -1913,20 +1974,39 @@
|
|||||||
|
|
||||||
.d3-tooltip {
|
.d3-tooltip {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background: var(--primary-dark);
|
background: rgba(15, 35, 65, 0.95);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 3000;
|
z-index: 10000;
|
||||||
display: none;
|
display: none;
|
||||||
font-family: "Vazirmatn", sans-serif;
|
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 {
|
.d3-tooltip.visible {
|
||||||
display: block;
|
display: block;
|
||||||
|
animation: tooltipFadeIn 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tooltipFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rb-toolbar {
|
.rb-toolbar {
|
||||||
@@ -3581,8 +3661,8 @@
|
|||||||
<div id="versionAnalysisContainer" class="dashboard-container"></div>
|
<div id="versionAnalysisContainer" class="dashboard-container"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<div class="dashboard-footer-stats" id="verAnalysisFooterStats"></div>
|
|
||||||
<button class="btn btn-ghost" data-close-modal>بستن</button>
|
<button class="btn btn-ghost" data-close-modal>بستن</button>
|
||||||
|
<div class="dashboard-footer-stats" id="verAnalysisFooterStats"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -4053,6 +4133,7 @@
|
|||||||
<div class="galaxy-tooltip" id="galaxyTooltip"></div>
|
<div class="galaxy-tooltip" id="galaxyTooltip"></div>
|
||||||
<div class="d3-tooltip" id="d3Tooltip"></div>
|
<div class="d3-tooltip" id="d3Tooltip"></div>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
<script>
|
<script>
|
||||||
/* ==================== DATA ==================== */
|
/* ==================== DATA ==================== */
|
||||||
const projectData = {
|
const projectData = {
|
||||||
@@ -4236,10 +4317,28 @@
|
|||||||
function showD3Tooltip(e, text) {
|
function showD3Tooltip(e, text) {
|
||||||
let tt = document.getElementById("d3Tooltip");
|
let tt = document.getElementById("d3Tooltip");
|
||||||
if (!tt) return;
|
if (!tt) return;
|
||||||
|
if (!text) {
|
||||||
|
hideD3Tooltip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
tt.textContent = text;
|
tt.textContent = text;
|
||||||
tt.classList.add("visible");
|
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() {
|
function hideD3Tooltip() {
|
||||||
const tt = document.getElementById("d3Tooltip");
|
const tt = document.getElementById("d3Tooltip");
|
||||||
@@ -6123,6 +6222,14 @@
|
|||||||
d.pwd + d.pwod > 0
|
d.pwd + d.pwod > 0
|
||||||
? Math.round((d.pwd / (d.pwd + d.pwod)) * 100)
|
? Math.round((d.pwd / (d.pwd + d.pwod)) * 100)
|
||||||
: 0;
|
: 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 =
|
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>`;
|
`<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";
|
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 ==================== */
|
/* ==================== GALAXY CHART ==================== */
|
||||||
const GalaxyChartManager = {
|
const GalaxyChartManager = {
|
||||||
svg: null,
|
svg: null,
|
||||||
@@ -6916,6 +7155,95 @@
|
|||||||
}, 2000);
|
}, 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 ==================== */
|
/* ==================== AUTOSAVE MANAGER ==================== */
|
||||||
const AutosaveManager = {
|
const AutosaveManager = {
|
||||||
@@ -8454,6 +8782,14 @@ ${checklistHtml}
|
|||||||
this.renderTypeDonut("depTypeChart", d.typeCounts);
|
this.renderTypeDonut("depTypeChart", d.typeCounts);
|
||||||
this.renderMatrix("depMatrixChart", d.allProjects, d.matrix);
|
this.renderMatrix("depMatrixChart", d.allProjects, d.matrix);
|
||||||
this.renderRiskList("depRiskList", d);
|
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 =
|
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>`;
|
`<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.renderProductBarChart("deployProductChart", d.ppc);
|
||||||
this.renderRiskList("deployRiskList", d.riskList);
|
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 =
|
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>`;
|
`<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);
|
const c = document.getElementById(cid);
|
||||||
if (!c) return;
|
if (!c) return;
|
||||||
c.innerHTML = "";
|
c.innerHTML = "";
|
||||||
const data = Object.entries(edc).map(([k, v]) => ({
|
|
||||||
label: k,
|
const entries = Object.entries(envCounts).filter((d) => d[1] > 0);
|
||||||
value: v,
|
|
||||||
}));
|
if (entries.length === 0) {
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
const w = 320,
|
|
||||||
hgt = 220,
|
const w = 320, hgt = 220, m = { top: 20, right: 15, bottom: 45, left: 30 };
|
||||||
m = { top: 20, right: 15, bottom: 45, left: 30 };
|
const iw = w - m.left - m.right, ih = hgt - m.top - m.bottom;
|
||||||
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 svg = d3
|
const g = svg.append("g").attr("transform", `translate(${m.left},${m.top})`);
|
||||||
.select(c)
|
|
||||||
.append("svg")
|
const xScale = d3.scaleBand().domain(entries.map((d) => d[0])).range([0, iw]).padding(0.3);
|
||||||
.attr("width", w)
|
const yScale = d3.scaleLinear().domain([0, d3.max(entries, (d) => d[1]) || 1]).nice().range([ih, 0]);
|
||||||
.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]);
|
|
||||||
g.append("g")
|
g.append("g")
|
||||||
.attr("transform", `translate(0,${ih})`)
|
.attr("transform", `translate(0,${ih})`)
|
||||||
.call(d3.axisBottom(x).tickSize(0))
|
.call(d3.axisBottom(xScale).tickSize(0))
|
||||||
.selectAll("text")
|
.selectAll("text")
|
||||||
.attr("fill", "var(--text-muted)")
|
.attr("fill", "var(--text-muted)")
|
||||||
.attr("font-size", "8px")
|
.attr("font-size", "8px")
|
||||||
.attr("font-family", "Vazirmatn")
|
.attr("font-family", "Vazirmatn")
|
||||||
.attr("transform", "rotate(-20)")
|
.attr("transform", "rotate(-20)")
|
||||||
.attr("text-anchor", "end");
|
.attr("text-anchor", "end");
|
||||||
|
|
||||||
g.append("g")
|
g.append("g")
|
||||||
.call(d3.axisLeft(y).ticks(4).tickSize(-iw))
|
.call(d3.axisLeft(yScale).ticks(4).tickSize(-iw))
|
||||||
.selectAll("text")
|
.selectAll("text")
|
||||||
.attr("fill", "var(--text-muted)")
|
.attr("fill", "var(--text-muted)")
|
||||||
.attr("font-size", "9px");
|
.attr("font-size", "9px");
|
||||||
|
|
||||||
|
// ✅ اصلاح: استفاده از d به جای x
|
||||||
g.selectAll("rect")
|
g.selectAll("rect")
|
||||||
.data(data)
|
.data(entries)
|
||||||
.join("rect")
|
.join("rect")
|
||||||
.attr("x", (d) => x(d.label))
|
.attr("x", (d) => xScale(d[0]))
|
||||||
.attr("y", (d) => y(d.value))
|
.attr("y", (d) => yScale(d[1]))
|
||||||
.attr("width", x.bandwidth())
|
.attr("width", xScale.bandwidth())
|
||||||
.attr("height", (d) => ih - y(d.value))
|
.attr("height", (d) => ih - yScale(d[1]))
|
||||||
.attr("fill", "#38a169")
|
.attr("fill", "#38a169")
|
||||||
.attr("rx", 4)
|
.attr("rx", 4)
|
||||||
.style("cursor", "pointer")
|
.style("cursor", "pointer")
|
||||||
.on("mouseover", function (e, d) {
|
.on("mouseover", function (e, d) {
|
||||||
d3.select(this).attr("fill", "var(--accent)");
|
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("mousemove", (e) => showD3Tooltip(e, ""))
|
||||||
.on("mouseout", function () {
|
.on("mouseout", function () {
|
||||||
d3.select(this).attr("fill", "#38a169");
|
d3.select(this).attr("fill", "#38a169");
|
||||||
hideD3Tooltip();
|
hideD3Tooltip();
|
||||||
});
|
});
|
||||||
|
|
||||||
g.selectAll(".bar-label")
|
g.selectAll(".bar-label")
|
||||||
.data(data)
|
.data(entries)
|
||||||
.join("text")
|
.join("text")
|
||||||
.attr("x", (d) => x(d.label) + x.bandwidth() / 2)
|
.attr("x", (d) => xScale(d[0]) + xScale.bandwidth() / 2)
|
||||||
.attr("y", (d) => y(d.value) - 5)
|
.attr("y", (d) => yScale(d[1]) - 5)
|
||||||
.attr("text-anchor", "middle")
|
.attr("text-anchor", "middle")
|
||||||
.attr("fill", "var(--text)")
|
.attr("fill", "var(--text)")
|
||||||
.attr("font-size", "10px")
|
.attr("font-size", "10px")
|
||||||
.attr("font-weight", "700")
|
.attr("font-weight", "700")
|
||||||
.text((d) => d.value);
|
.text((d) => d[1]);
|
||||||
},
|
},
|
||||||
renderStatusDonut(cid, pwd, pwod, coverage) {
|
renderStatusDonut(cid, pwd, pwod, coverage) {
|
||||||
const c = document.getElementById(cid);
|
const c = document.getElementById(cid);
|
||||||
@@ -8991,47 +9324,36 @@ ${checklistHtml}
|
|||||||
const c = document.getElementById(cid);
|
const c = document.getElementById(cid);
|
||||||
if (!c) return;
|
if (!c) return;
|
||||||
c.innerHTML = "";
|
c.innerHTML = "";
|
||||||
|
|
||||||
const data = ppc.map((p) => ({
|
const data = ppc.map((p) => ({
|
||||||
label:
|
label: p.title.length > 15 ? p.title.substring(0, 15) + "..." : p.title,
|
||||||
p.title.length > 15 ? p.title.substring(0, 15) + "..." : p.title,
|
|
||||||
value: p.count,
|
value: p.count,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
c.innerHTML =
|
c.innerHTML = '<div class="dashboard-empty-chart"><span>📊</span><div>دادهای نیست</div></div>';
|
||||||
'<div class="dashboard-empty-chart"><span>📊</span><div>دادهای نیست</div></div>';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const bh = 28,
|
|
||||||
gap = 6;
|
const bh = 28, gap = 6;
|
||||||
const w = 320,
|
const w = 320, hgt = data.length * (bh + gap) + 30;
|
||||||
hgt = data.length * (bh + gap) + 30;
|
|
||||||
const m = { top: 10, right: 35, bottom: 10, left: 130 };
|
const m = { top: 10, right: 35, bottom: 10, left: 130 };
|
||||||
const iw = w - m.left - m.right,
|
const iw = w - m.left - m.right, ih = hgt - m.top - m.bottom;
|
||||||
ih = hgt - m.top - m.bottom;
|
|
||||||
const svg = d3
|
const svg = d3.select(c).append("svg").attr("width", w).attr("height", hgt);
|
||||||
.select(c)
|
const g = svg.append("g").attr("transform", `translate(${m.left},${m.top})`);
|
||||||
.append("svg")
|
|
||||||
.attr("width", w)
|
const xScale = d3.scaleLinear().domain([0, d3.max(data, (d) => d.value) || 1]).range([0, iw]);
|
||||||
.attr("height", hgt);
|
const yScale = d3.scaleBand().domain(data.map((d) => d.label)).range([0, ih]).padding(0.2);
|
||||||
const g = svg
|
|
||||||
.append("g")
|
// ✅ این متد از قبل درست بود (از d استفاده میکرد)
|
||||||
.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);
|
|
||||||
g.selectAll("rect")
|
g.selectAll("rect")
|
||||||
.data(data)
|
.data(data)
|
||||||
.join("rect")
|
.join("rect")
|
||||||
.attr("x", 0)
|
.attr("x", 0)
|
||||||
.attr("y", (d) => y(d.label))
|
.attr("y", (d) => yScale(d.label))
|
||||||
.attr("width", (d) => x(d.value))
|
.attr("width", (d) => xScale(d.value))
|
||||||
.attr("height", y.bandwidth())
|
.attr("height", yScale.bandwidth())
|
||||||
.attr("fill", "#3182ce")
|
.attr("fill", "#3182ce")
|
||||||
.attr("rx", 4)
|
.attr("rx", 4)
|
||||||
.style("cursor", "pointer")
|
.style("cursor", "pointer")
|
||||||
@@ -9044,22 +9366,24 @@ ${checklistHtml}
|
|||||||
d3.select(this).attr("fill", "#3182ce");
|
d3.select(this).attr("fill", "#3182ce");
|
||||||
hideD3Tooltip();
|
hideD3Tooltip();
|
||||||
});
|
});
|
||||||
|
|
||||||
g.selectAll(".label")
|
g.selectAll(".label")
|
||||||
.data(data)
|
.data(data)
|
||||||
.join("text")
|
.join("text")
|
||||||
.attr("x", -8)
|
.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("dy", "0.35em")
|
||||||
.attr("text-anchor", "end")
|
.attr("text-anchor", "end")
|
||||||
.attr("fill", "var(--text)")
|
.attr("fill", "var(--text)")
|
||||||
.attr("font-size", "9px")
|
.attr("font-size", "9px")
|
||||||
.attr("font-weight", "600")
|
.attr("font-weight", "600")
|
||||||
.text((d) => d.label);
|
.text((d) => d.label);
|
||||||
|
|
||||||
g.selectAll(".value")
|
g.selectAll(".value")
|
||||||
.data(data)
|
.data(data)
|
||||||
.join("text")
|
.join("text")
|
||||||
.attr("x", (d) => x(d.value) + 5)
|
.attr("x", (d) => xScale(d.value) + 5)
|
||||||
.attr("y", (d) => y(d.label) + y.bandwidth() / 2)
|
.attr("y", (d) => yScale(d.label) + yScale.bandwidth() / 2)
|
||||||
.attr("dy", "0.35em")
|
.attr("dy", "0.35em")
|
||||||
.attr("fill", "var(--text-muted)")
|
.attr("fill", "var(--text-muted)")
|
||||||
.attr("font-size", "10px")
|
.attr("font-size", "10px")
|
||||||
@@ -9249,6 +9573,14 @@ ${checklistHtml}
|
|||||||
this.renderEnvBarChart("verEnvChart", d.envCounts);
|
this.renderEnvBarChart("verEnvChart", d.envCounts);
|
||||||
this.renderEnvMatrix("verEnvMatrix", d.envMatrix);
|
this.renderEnvMatrix("verEnvMatrix", d.envMatrix);
|
||||||
this.renderRiskList("verRiskList", d.riskList);
|
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 =
|
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>`;
|
`<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);
|
const c = document.getElementById(cid);
|
||||||
if (!c) return;
|
if (!c) return;
|
||||||
c.innerHTML = "";
|
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) {
|
if (entries.length === 0) {
|
||||||
c.innerHTML =
|
c.innerHTML = '<div class="dashboard-empty-chart"><span>📊</span><div>دادهای نیست</div></div>';
|
||||||
'<div class="dashboard-empty-chart"><span>📊</span><div>دادهای نیست</div></div>';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const w = 320,
|
|
||||||
hgt = 220,
|
const w = 320, hgt = 220, m = { top: 20, right: 15, bottom: 40, left: 30 };
|
||||||
m = { top: 20, right: 15, bottom: 40, left: 30 };
|
const iw = w - m.left - m.right, ih = hgt - m.top - m.bottom;
|
||||||
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 svg = d3
|
const g = svg.append("g").attr("transform", `translate(${m.left},${m.top})`);
|
||||||
.select(c)
|
|
||||||
.append("svg")
|
const xScale = d3.scaleBand().domain(entries.map((d) => d[0])).range([0, iw]).padding(0.3);
|
||||||
.attr("width", w)
|
const yScale = d3.scaleLinear().domain([0, d3.max(entries, (d) => d[1]) || 1]).nice().range([ih, 0]);
|
||||||
.attr("height", hgt);
|
|
||||||
const g = svg
|
const colors = ["#4299e1", "#48bb78", "#ecc94b", "#ed64a6", "#9f7aea", "#ed8936"];
|
||||||
.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",
|
|
||||||
];
|
|
||||||
g.append("g")
|
g.append("g")
|
||||||
.attr("transform", `translate(0,${ih})`)
|
.attr("transform", `translate(0,${ih})`)
|
||||||
.call(d3.axisBottom(x).tickSize(0))
|
.call(d3.axisBottom(xScale).tickSize(0))
|
||||||
.selectAll("text")
|
.selectAll("text")
|
||||||
.attr("fill", "var(--text-muted)")
|
.attr("fill", "var(--text-muted)")
|
||||||
.attr("font-size", "9px")
|
.attr("font-size", "9px")
|
||||||
.attr("font-family", "Vazirmatn");
|
.attr("font-family", "Vazirmatn");
|
||||||
|
|
||||||
g.append("g")
|
g.append("g")
|
||||||
.call(d3.axisLeft(y).ticks(4).tickSize(-iw))
|
.call(d3.axisLeft(yScale).ticks(4).tickSize(-iw))
|
||||||
.selectAll("text")
|
.selectAll("text")
|
||||||
.attr("fill", "var(--text-muted)")
|
.attr("fill", "var(--text-muted)")
|
||||||
.attr("font-size", "9px");
|
.attr("font-size", "9px");
|
||||||
|
|
||||||
|
// ✅ اصلاح: استفاده از d به جای x
|
||||||
g.selectAll("rect")
|
g.selectAll("rect")
|
||||||
.data(entries)
|
.data(entries)
|
||||||
.join("rect")
|
.join("rect")
|
||||||
.attr("x", (x) => x(x[0]))
|
.attr("x", (d) => xScale(d[0]))
|
||||||
.attr("y", (x) => y(x[1]))
|
.attr("y", (d) => yScale(d[1]))
|
||||||
.attr("width", x.bandwidth())
|
.attr("width", xScale.bandwidth())
|
||||||
.attr("height", (x) => ih - y(x[1]))
|
.attr("height", (d) => ih - yScale(d[1]))
|
||||||
.attr("fill", (x, i) => colors[i % colors.length])
|
.attr("fill", (d, i) => colors[i % colors.length])
|
||||||
.attr("rx", 4)
|
.attr("rx", 4)
|
||||||
.style("cursor", "pointer")
|
.style("cursor", "pointer")
|
||||||
.on("mouseover", function (e, x) {
|
.on("mouseover", function (e, d) {
|
||||||
d3.select(this).attr("fill", "var(--accent)");
|
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("mousemove", (e) => showD3Tooltip(e, ""))
|
||||||
.on("mouseout", function (e, x) {
|
.on("mouseout", function (e, d) {
|
||||||
d3.select(this).attr(
|
d3.select(this).attr("fill", colors[entries.indexOf(d) % colors.length]);
|
||||||
"fill",
|
|
||||||
colors[entries.indexOf(x) % colors.length],
|
|
||||||
);
|
|
||||||
hideD3Tooltip();
|
hideD3Tooltip();
|
||||||
});
|
});
|
||||||
|
|
||||||
g.selectAll(".bar-label")
|
g.selectAll(".bar-label")
|
||||||
.data(entries)
|
.data(entries)
|
||||||
.join("text")
|
.join("text")
|
||||||
.attr("x", (x) => x(x[0]) + x.bandwidth() / 2)
|
.attr("x", (d) => xScale(d[0]) + xScale.bandwidth() / 2)
|
||||||
.attr("y", (x) => y(x[1]) - 5)
|
.attr("y", (d) => yScale(d[1]) - 5)
|
||||||
.attr("text-anchor", "middle")
|
.attr("text-anchor", "middle")
|
||||||
.attr("fill", "var(--text)")
|
.attr("fill", "var(--text)")
|
||||||
.attr("font-size", "10px")
|
.attr("font-size", "10px")
|
||||||
.attr("font-weight", "700")
|
.attr("font-weight", "700")
|
||||||
.text((x) => x[1]);
|
.text((d) => d[1]);
|
||||||
},
|
},
|
||||||
renderEnvBarChart(cid, envCounts) {
|
renderEnvBarChart(cid, envCounts) {
|
||||||
const c = document.getElementById(cid);
|
const c = document.getElementById(cid);
|
||||||
@@ -9633,6 +9946,14 @@ ${checklistHtml}
|
|||||||
this.renderSeverityDonut("riskSeverityChart", d);
|
this.renderSeverityDonut("riskSeverityChart", d);
|
||||||
this.renderTypeBarChart("riskTypeChart", d.risks);
|
this.renderTypeBarChart("riskTypeChart", d.risks);
|
||||||
this.renderRiskList("riskDetailList", 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 =
|
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>`;
|
`<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);
|
const c = document.getElementById(cid);
|
||||||
if (!c) return;
|
if (!c) return;
|
||||||
c.innerHTML = "";
|
c.innerHTML = "";
|
||||||
|
|
||||||
const typeLabels = {
|
const typeLabels = {
|
||||||
broken: "وابستگی شکسته",
|
broken: "وابستگی شکسته",
|
||||||
"broken-req": "نیازمندی شکسته",
|
"broken-req": "نیازمندی شکسته",
|
||||||
@@ -9730,6 +10052,7 @@ ${checklistHtml}
|
|||||||
"no-owner": "بدون مالک",
|
"no-owner": "بدون مالک",
|
||||||
"empty-environment": "محیط خالی",
|
"empty-environment": "محیط خالی",
|
||||||
};
|
};
|
||||||
|
|
||||||
const typeColors = {
|
const typeColors = {
|
||||||
broken: "#e53e3e",
|
broken: "#e53e3e",
|
||||||
"broken-req": "#e53e3e",
|
"broken-req": "#e53e3e",
|
||||||
@@ -9740,82 +10063,73 @@ ${checklistHtml}
|
|||||||
"no-owner": "#d69e2e",
|
"no-owner": "#d69e2e",
|
||||||
"empty-environment": "#3182ce",
|
"empty-environment": "#3182ce",
|
||||||
};
|
};
|
||||||
|
|
||||||
const typeCounts = {};
|
const typeCounts = {};
|
||||||
risks.forEach((r) => {
|
risks.forEach((r) => {
|
||||||
typeCounts[r.type] = (typeCounts[r.type] || 0) + 1;
|
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) {
|
if (entries.length === 0) {
|
||||||
c.innerHTML =
|
c.innerHTML = '<div class="dashboard-empty-chart"><span>✅</span><div>ریسکی نیست</div></div>';
|
||||||
'<div class="dashboard-empty-chart"><span>✅</span><div>ریسکی نیست</div></div>';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const bh = 28,
|
|
||||||
gap = 6;
|
const bh = 28, gap = 6;
|
||||||
const w = 320,
|
const w = 320, hgt = entries.length * (bh + gap) + 30;
|
||||||
hgt = entries.length * (bh + gap) + 30;
|
|
||||||
const m = { top: 10, right: 35, bottom: 10, left: 130 };
|
const m = { top: 10, right: 35, bottom: 10, left: 130 };
|
||||||
const iw = w - m.left - m.right,
|
const iw = w - m.left - m.right, ih = hgt - m.top - m.bottom;
|
||||||
ih = hgt - m.top - m.bottom;
|
|
||||||
const svg = d3
|
const svg = d3.select(c).append("svg").attr("width", w).attr("height", hgt);
|
||||||
.select(c)
|
const g = svg.append("g").attr("transform", `translate(${m.left},${m.top})`);
|
||||||
.append("svg")
|
|
||||||
.attr("width", w)
|
const xScale = d3.scaleLinear().domain([0, d3.max(entries, (d) => d[1]) || 1]).range([0, iw]);
|
||||||
.attr("height", hgt);
|
const yScale = d3.scaleBand().domain(entries.map((d) => d[0])).range([0, ih]).padding(0.2);
|
||||||
const g = svg
|
|
||||||
.append("g")
|
// ✅ اصلاح: استفاده از d به جای x در arrow functions
|
||||||
.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);
|
|
||||||
g.selectAll("rect")
|
g.selectAll("rect")
|
||||||
.data(entries)
|
.data(entries)
|
||||||
.join("rect")
|
.join("rect")
|
||||||
.attr("x", 0)
|
.attr("x", 0)
|
||||||
.attr("y", (x) => y(x[0]))
|
.attr("y", (d) => yScale(d[0]))
|
||||||
.attr("width", (x) => x(x[1]))
|
.attr("width", (d) => xScale(d[1]))
|
||||||
.attr("height", y.bandwidth())
|
.attr("height", yScale.bandwidth())
|
||||||
.attr("fill", (x) => typeColors[x[0]] || "#718096")
|
.attr("fill", (d) => typeColors[d[0]] || "#718096")
|
||||||
.attr("rx", 4)
|
.attr("rx", 4)
|
||||||
.style("cursor", "pointer")
|
.style("cursor", "pointer")
|
||||||
.on("mouseover", function (e, x) {
|
.on("mouseover", function (e, d) {
|
||||||
d3.select(this).attr("fill", "var(--accent)");
|
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("mousemove", (e) => showD3Tooltip(e, ""))
|
||||||
.on("mouseout", function (e, x) {
|
.on("mouseout", function (e, d) {
|
||||||
d3.select(this).attr("fill", typeColors[x[0]] || "#718096");
|
d3.select(this).attr("fill", typeColors[d[0]] || "#718096");
|
||||||
hideD3Tooltip();
|
hideD3Tooltip();
|
||||||
});
|
});
|
||||||
|
|
||||||
g.selectAll(".label")
|
g.selectAll(".label")
|
||||||
.data(entries)
|
.data(entries)
|
||||||
.join("text")
|
.join("text")
|
||||||
.attr("x", -8)
|
.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("dy", "0.35em")
|
||||||
.attr("text-anchor", "end")
|
.attr("text-anchor", "end")
|
||||||
.attr("fill", "var(--text)")
|
.attr("fill", "var(--text)")
|
||||||
.attr("font-size", "9px")
|
.attr("font-size", "9px")
|
||||||
.attr("font-weight", "600")
|
.attr("font-weight", "600")
|
||||||
.text((x) => typeLabels[x[0]] || x[0]);
|
.text((d) => typeLabels[d[0]] || d[0]);
|
||||||
|
|
||||||
g.selectAll(".value")
|
g.selectAll(".value")
|
||||||
.data(entries)
|
.data(entries)
|
||||||
.join("text")
|
.join("text")
|
||||||
.attr("x", (x) => x(x[1]) + 5)
|
.attr("x", (d) => xScale(d[1]) + 5)
|
||||||
.attr("y", (x) => y(x[0]) + y.bandwidth() / 2)
|
.attr("y", (d) => yScale(d[0]) + yScale.bandwidth() / 2)
|
||||||
.attr("dy", "0.35em")
|
.attr("dy", "0.35em")
|
||||||
.attr("fill", "var(--text-muted)")
|
.attr("fill", "var(--text-muted)")
|
||||||
.attr("font-size", "10px")
|
.attr("font-size", "10px")
|
||||||
.attr("font-weight", "700")
|
.attr("font-weight", "700")
|
||||||
.text((x) => x[1]);
|
.text((d) => d[1]);
|
||||||
},
|
},
|
||||||
renderRiskList(cid, risks) {
|
renderRiskList(cid, risks) {
|
||||||
const c = document.getElementById(cid);
|
const c = document.getElementById(cid);
|
||||||
@@ -9929,6 +10243,7 @@ ${checklistHtml}
|
|||||||
document
|
document
|
||||||
.getElementById("galaxyExportPNG")
|
.getElementById("galaxyExportPNG")
|
||||||
.addEventListener("click", () => {
|
.addEventListener("click", () => {
|
||||||
|
GalaxyChartManager.exportPNG();
|
||||||
showToast("📷 خروجی ذخیره شد", "success");
|
showToast("📷 خروجی ذخیره شد", "success");
|
||||||
});
|
});
|
||||||
function handleToolSelect(tool, group) {
|
function handleToolSelect(tool, group) {
|
||||||
+585
-181
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user