{
/*
* CODEXSTATS — DATA INTELLIGENCE MAP
*
* demand = relevanța pentru cercetare
* expertise = valoarea pentru business
* impact = potențialul de automatizare
*
* Nivelurile sunt orientative și folosesc pragurile 50%, 75% și 95%.
*/
const services = [
// 1. DESIGN
{service: "Research Design", category: "Design", demand: 95, expertise: 75, impact: 50},
{service: "Hypothesis Testing", category: "Design", demand: 95, expertise: 75, impact: 50},
{service: "Sampling & Power", category: "Design", demand: 95, expertise: 75, impact: 50},
{service: "A/B Testing", category: "Design", demand: 75, expertise: 95, impact: 75},
{service: "Clinical Trials", category: "Design", demand: 95, expertise: 75, impact: 50},
// 2. COLECTARE ȘI PRELUCRARE DATE
{service: "Data Collection", category: "Colectare și prelucrare date", demand: 95, expertise: 75, impact: 50},
{service: "Data Cleaning", category: "Colectare și prelucrare date", demand: 95, expertise: 95, impact: 75},
{service: "SQL", category: "Colectare și prelucrare date", demand: 75, expertise: 95, impact: 95},
{service: "Excel", category: "Colectare și prelucrare date", demand: 50, expertise: 75, impact: 75},
{service: "Feature Engineering", category: "Colectare și prelucrare date", demand: 75, expertise: 95, impact: 75},
// 3. ANALIZA DATELOR
{service: "Regression & ANOVA", category: "Analiza datelor", demand: 95, expertise: 75, impact: 75},
{service: "Machine Learning", category: "Analiza datelor", demand: 75, expertise: 95, impact: 75},
{service: "Model Evaluation", category: "Analiza datelor", demand: 95, expertise: 95, impact: 75},
{service: "Time Series", category: "Analiza datelor", demand: 75, expertise: 95, impact: 95},
{service: "Survival Analysis", category: "Analiza datelor", demand: 95, expertise: 75, impact: 50},
// 4. VIZUALIZARE ȘI AUTOMATIZARE
{service: "Power BI", category: "Vizualizare și automatizare", demand: 50, expertise: 95, impact: 95},
{service: "Tableau", category: "Vizualizare și automatizare", demand: 50, expertise: 95, impact: 75},
{service: "R & Python", category: "Vizualizare și automatizare", demand: 95, expertise: 75, impact: 95},
{service: "Interactive Dashboards", category: "Vizualizare și automatizare", demand: 75, expertise: 95, impact: 95},
{service: "Automated Reporting", category: "Vizualizare și automatizare", demand: 75, expertise: 95, impact: 95}
];
/*
* Aceleași trei culori ca în graficul original.
*/
const series = [
{
key: "demand",
label: "Relevanță pentru cercetare",
color: "#a8b3c5"
},
{
key: "expertise",
label: "Valoare pentru business",
color: "#2563eb"
},
{
key: "impact",
label: "Potențial de automatizare",
color: "#22c7e8"
}
];
const formatNumber = new Intl.NumberFormat("ro-RO", {
maximumFractionDigits: 0
});
/*
* Dimensiunile graficului.
* width este reactiv în Observable și urmărește containerul.
*/
const chartWidth = Math.max(
360,
Math.min(width, 1180)
);
const radius = chartWidth / 2;
const innerRadius = radius * 0.18;
const outerRadius = radius * 0.70;
const labelRadius = radius * 0.82;
const maxValue = 100;
/*
* Folosim o scală liniară pentru ca pragurile să fie
* reprezentate proporțional.
*/
const radialScale = d3
.scaleLinear()
.domain([0, maxValue])
.range([innerRadius, outerRadius]);
const angleStep =
(Math.PI * 2) / services.length;
const serviceGap =
angleStep * 0.17;
const usableAngle =
angleStep - serviceGap;
const seriesGap =
usableAngle * 0.06;
const seriesAngle =
(
usableAngle -
seriesGap * (series.length - 1)
) / series.length;
/*
* Fundalul fiecărei competențe.
*/
const tracks = services.map(
(service, serviceIndex) => ({
...service,
startAngle:
serviceIndex * angleStep +
serviceGap / 2,
endAngle:
(serviceIndex + 1) * angleStep -
serviceGap / 2
})
);
/*
* Construirea celor trei bare pentru fiecare competență.
*/
const bars = services.flatMap(
(service, serviceIndex) =>
series.map((item, seriesIndex) => {
const startAngle =
serviceIndex * angleStep +
serviceGap / 2 +
seriesIndex *
(seriesAngle + seriesGap);
return {
...service,
series: item.label,
color: item.color,
value: service[item.key],
startAngle,
endAngle: startAngle + seriesAngle
};
})
);
/*
* Containerul și tooltipul.
*/
const wrapper =
document.createElement("div");
wrapper.className =
"radial-chart-wrapper";
/*
* Filtru interactiv pentru roluri.
*/
const roleFilter =
document.createElement("div");
roleFilter.className =
"radial-role-filter";
roleFilter.setAttribute(
"aria-label",
"Filtrează graficul după etapa analizei statistice"
);
const roleOptions = [
{value: "all", label: "Sistemul de analiză statistică"},
{value: "Design", label: "Design"},
{value: "Colectare și prelucrare date", label: "Colectare & prelucrare"},
{value: "Analiza datelor", label: "Analiza datelor"},
{value: "Vizualizare și automatizare", label: "Vizualizare & automatizare"}
];
const roleButtons =
roleOptions.map((option, index) => {
const button =
document.createElement("button");
button.type = "button";
button.className =
`radial-role-button${index === 0 ? " is-active" : ""}`;
button.textContent = option.label;
button.dataset.role = option.value;
button.setAttribute(
"aria-pressed",
index === 0 ? "true" : "false"
);
roleFilter.appendChild(button);
return button;
});
const tooltip =
document.createElement("div");
tooltip.className =
"radial-tooltip";
tooltip.setAttribute(
"role",
"tooltip"
);
tooltip.setAttribute(
"aria-hidden",
"true"
);
/*
* Elementul SVG.
*/
const svg = d3
.create("svg")
.attr(
"viewBox",
`${-radius} ${-radius} ${chartWidth} ${chartWidth}`
)
.attr("width", "100%")
.attr("height", "auto")
.attr("role", "img")
.attr(
"aria-label",
"Hartă radială cu 20 de activități grupate în etapele unei analize statistice"
);
/*
* Zona centrală.
*/
svg
.append("circle")
.attr("r", innerRadius - 7)
.attr(
"fill",
"rgba(238, 242, 255, 0.84)"
)
.attr(
"stroke",
"rgba(148, 163, 184, 0.45)"
)
.attr("stroke-width", 1);
/*
* Cercurile de referință: 50%, 75% și 95%.
*/
const tickValues = [
50,
75,
95
];
svg
.append("g")
.selectAll("circle")
.data(tickValues)
.join("circle")
.attr(
"r",
d => radialScale(d)
)
.attr("fill", "none")
.attr(
"stroke",
"rgba(148, 163, 184, 0.34)"
)
.attr("stroke-width", 0.8)
.attr(
"stroke-dasharray",
"3 4"
);
/*
* Fundalurile discrete ale serviciilor.
*/
const trackArc = d3
.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.startAngle(d => d.startAngle)
.endAngle(d => d.endAngle)
.cornerRadius(2);
svg
.append("g")
.selectAll("path")
.data(tracks)
.join("path")
.attr("d", trackArc)
.attr(
"fill",
"rgba(226, 232, 240, 0.48)"
);
/*
* Barele radiale.
*/
const barArc = d3
.arc()
.innerRadius(innerRadius)
.outerRadius(
d => radialScale(d.value)
)
.startAngle(d => d.startAngle)
.endAngle(d => d.endAngle)
.cornerRadius(2);
const reducedMotion =
window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
const barPaths = svg
.append("g")
.selectAll("path")
.data(bars)
.join("path")
.attr(
"d",
d => barArc({
...d,
value: 0
})
)
.attr(
"fill",
d => d.color
)
.attr(
"stroke",
"rgba(255, 255, 255, 0.92)"
)
.attr("stroke-width", 0.55)
.attr("opacity", 0.92)
.attr("tabindex", 0)
.attr(
"aria-label",
d =>
`${d.service}, ${d.series}: ` +
`${formatNumber.format(d.value)} din 100`
)
.style("cursor", "pointer");
/*
* Aceeași animație de creștere este folosită atât
* la încărcarea paginii, cât și la selectarea filtrelor.
*/
let activeCategory = "all";
const belongsToActiveCategory = d =>
activeCategory === "all" ||
d.category === activeCategory;
function growBars(selectedCategory = "all") {
activeCategory = selectedCategory;
const visibleServiceNames = services
.filter(
d =>
selectedCategory === "all" ||
d.category === selectedCategory
)
.map(d => d.service);
const isVisible = d =>
selectedCategory === "all" ||
d.category === selectedCategory;
barPaths
.interrupt()
.interrupt("radial-growth")
.attr(
"d",
d => barArc({
...d,
value: 0
})
)
.attr(
"opacity",
d => isVisible(d) ? 0.92 : 0
)
.style(
"pointer-events",
d => isVisible(d) ? "auto" : "none"
)
.attr(
"tabindex",
d => isVisible(d) ? 0 : -1
);
if (reducedMotion) {
barPaths
.filter(isVisible)
.attr("d", barArc);
return;
}
barPaths
.filter(isVisible)
.transition("radial-growth")
.delay(d => {
const serviceIndex =
visibleServiceNames.indexOf(d.service);
const seriesIndex =
series.findIndex(
item => item.label === d.series
);
return (
serviceIndex * 42 +
seriesIndex * 70
);
})
.duration(900)
.ease(d3.easeCubicOut)
.attrTween("d", d => {
const interpolateValue =
d3.interpolateNumber(0, d.value);
return progress =>
barArc({
...d,
value: interpolateValue(progress)
});
});
}
growBars("all");
/*
* Clasificarea competențelor.
*/
/*
* Conținutul tooltipului.
*/
function tooltipContent(d) {
return `
<div class="radial-tooltip-header">
<div>
<strong>${d.service}</strong>
<span>${d.category}</span>
</div>
</div>
<div class="radial-tooltip-current">
<span>${d.series}</span>
<strong>
${formatNumber.format(d.value)}
<small>/ 100</small>
</strong>
</div>
<div class="radial-tooltip-grid">
<div>
<span>Cercetare</span>
<strong>
${formatNumber.format(d.demand)}
</strong>
</div>
<div>
<span>Business</span>
<strong>
${formatNumber.format(d.expertise)}
</strong>
</div>
<div>
<span>Automatizare</span>
<strong>
${formatNumber.format(d.impact)}
</strong>
</div>
<div>
<span>Etapă</span>
<strong>
${d.category}
</strong>
</div>
</div>
`;
}
/*
* Poziționarea tooltipului.
*/
function positionTooltip(event) {
const [pointerX, pointerY] =
d3.pointer(event, wrapper);
const container =
wrapper.getBoundingClientRect();
const box =
tooltip.getBoundingClientRect();
const offset = 14;
let left =
pointerX + offset;
let top =
pointerY + offset;
if (
left + box.width >
container.width - 5
) {
left =
pointerX -
box.width -
offset;
}
if (
top + box.height >
container.height - 5
) {
top =
pointerY -
box.height -
offset;
}
tooltip.style.left =
`${Math.max(6, left)}px`;
tooltip.style.top =
`${Math.max(6, top)}px`;
}
/*
* Evidențierea serviciului selectat.
*/
function highlightService(service) {
barPaths
.attr(
"opacity",
d =>
!belongsToActiveCategory(d)
? 0
: d.service === service
? 1
: 0.24
)
.attr(
"stroke",
d =>
d.service === service
? "#0f172a"
: "rgba(255, 255, 255, 0.92)"
)
.attr(
"stroke-width",
d =>
d.service === service
? 1.1
: 0.55
);
}
function resetBars() {
barPaths
.attr(
"opacity",
d => belongsToActiveCategory(d) ? 0.92 : 0
)
.attr(
"stroke",
"rgba(255, 255, 255, 0.92)"
)
.attr(
"stroke-width",
0.55
);
}
function showTooltip(event, d) {
tooltip.style.transform = "";
tooltip.innerHTML =
tooltipContent(d);
tooltip.classList.add(
"is-visible"
);
tooltip.setAttribute(
"aria-hidden",
"false"
);
highlightService(d.service);
positionTooltip(event);
}
function hideTooltip() {
tooltip.classList.remove(
"is-visible"
);
tooltip.setAttribute(
"aria-hidden",
"true"
);
tooltip.style.transform = "";
resetBars();
}
/*
* Interacțiunile mouse, touch și tastatură.
*/
barPaths
.on(
"pointerenter",
showTooltip
)
.on(
"pointermove",
event => {
if (
tooltip.classList.contains(
"is-visible"
)
) {
positionTooltip(event);
}
}
)
.on(
"pointerleave",
hideTooltip
)
.on(
"click",
function(event, d) {
event.stopPropagation();
showTooltip(
event,
d
);
}
)
.on(
"focus",
function(event, d) {
tooltip.innerHTML =
tooltipContent(d);
tooltip.classList.add(
"is-visible"
);
tooltip.setAttribute(
"aria-hidden",
"false"
);
tooltip.style.left = "50%";
tooltip.style.top = "50%";
tooltip.style.transform =
"translate(-50%, -50%)";
highlightService(
d.service
);
}
)
.on(
"blur",
hideTooltip
);
wrapper.addEventListener(
"click",
hideTooltip
);
/*
* Conversia coordonatelor polare.
*/
const polar =
(angle, distance) => [
Math.sin(angle) * distance,
-Math.cos(angle) * distance
];
/*
* Separatoarele celor patru domenii:
*
* 0–4 Design
* 5–9 Colectare și prelucrare date
* 10–14 Analiza datelor
* 15–19 Vizualizare și automatizare
*/
svg
.append("g")
.selectAll("line")
.data([
0,
5,
10,
15
])
.join("line")
.attr(
"x1",
d =>
polar(
d * angleStep,
innerRadius - 4
)[0]
)
.attr(
"y1",
d =>
polar(
d * angleStep,
innerRadius - 4
)[1]
)
.attr(
"x2",
d =>
polar(
d * angleStep,
outerRadius + 10
)[0]
)
.attr(
"y2",
d =>
polar(
d * angleStep,
outerRadius + 10
)[1]
)
.attr(
"stroke",
"#64748b"
)
.attr(
"stroke-width",
0.85
)
.attr(
"opacity",
0.5
);
/*
* Etichetele serviciilor.
*/
const labels = services.map(
(service, index) => {
const angle =
(index + 0.5) *
angleStep;
const [x, y] =
polar(
angle,
labelRadius
);
const onLeft =
Math.sin(angle) < 0;
const degrees =
angle * 180 / Math.PI;
return {
...service,
x,
y,
onLeft,
rotation:
onLeft
? degrees + 180
: degrees
};
}
);
svg
.append("g")
.selectAll("text")
.data(labels)
.join("text")
.attr(
"transform",
d =>
`translate(${d.x},${d.y}) rotate(${d.rotation})`
)
.attr(
"text-anchor",
d =>
d.onLeft
? "end"
: "start"
)
.attr(
"dominant-baseline",
"middle"
)
.attr(
"dx",
d =>
d.onLeft
? -4
: 4
)
.attr(
"fill",
"#475569"
)
.attr(
"font-size",
10
)
.attr(
"font-weight",
600
)
.text(
d => d.service
);
/*
* Indicatorii din centrul graficului.
*/
const compactCenter =
chartWidth < 560;
const centerFontSize =
compactCenter
? Math.max(6.4, innerRadius * 0.21)
: 10;
const centerLabel = svg
.append("text")
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("fill", "#0f172a")
.attr("font-size", centerFontSize)
.attr("font-weight", 750);
function splitCenterLabel(text) {
const maxCharacters =
compactCenter ? 12 : 28;
const words = text
.trim()
.split(/\s+/);
const lines = [];
let currentLine = "";
words.forEach(word => {
const candidate =
currentLine
? `${currentLine} ${word}`
: word;
if (
currentLine &&
candidate.length > maxCharacters
) {
lines.push(currentLine);
currentLine = word;
} else {
currentLine = candidate;
}
});
if (currentLine) {
lines.push(currentLine);
}
return lines;
}
function renderCenterLabel(text) {
const lines =
splitCenterLabel(text);
const lineHeight =
centerFontSize * 1.12;
centerLabel
.selectAll("tspan")
.data(lines)
.join("tspan")
.attr("x", 0)
.attr(
"y",
(line, index) =>
(
index -
(lines.length - 1) / 2
) * lineHeight
)
.text(line => line);
}
renderCenterLabel(
"Unde folosim statistica?"
);
/*
* Actualizarea graficului când este selectat un rol.
*/
function filterByRole(selectedRole) {
hideTooltip();
roleButtons.forEach(button => {
const isActive =
button.dataset.role === selectedRole;
button.classList.toggle(
"is-active",
isActive
);
button.setAttribute(
"aria-pressed",
isActive ? "true" : "false"
);
});
growBars(selectedRole);
const centerTitleByStage = {
"Design": "Sistem",
"Colectare și prelucrare date": "Sistem",
"Analiza datelor": "Sistem",
"Vizualizare și automatizare": "Sistem"
};
renderCenterLabel(
selectedRole === "all"
? "Unde folosim statistica?"
: centerTitleByStage[selectedRole]
);
}
roleButtons.forEach(button => {
button.addEventListener(
"click",
() => filterByRole(button.dataset.role)
);
});
/*
* Inserarea graficului în pagină.
*/
wrapper.appendChild(
roleFilter
);
wrapper.appendChild(
svg.node()
);
wrapper.appendChild(
tooltip
);
return wrapper;
}