last ...
This commit is contained in:
@@ -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
@@ -1,3 +1,3 @@
|
||||
- تولتیپ های نمودار D3js بدرستی نمایش نمی یابند.
|
||||
- در گزارش ها و نمودار هایی که با استفاده از D3js تولید می کنی اندازه همه کارت ها رو یکسان در نظر بگیر و اگر ابعاد نمودار بزرگتر از اندازه کارت بود حالت اسکرول و قابلیت زوم کردن توسط ماوس رو براشون فعال کن.
|
||||
- در مودال نمودار کهکشانی گزینه خروجی png عمل نمی کند.
|
||||
- در مودال نمودار کهکشانی گزینه خروجی png عمل نمی کند.
|
||||
+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