204 lines
8.3 KiB
Plaintext
204 lines
8.3 KiB
Plaintext
@{
|
|
ViewData["Title"] = "Ventas Mensuales";
|
|
Layout = "_Layout";
|
|
}
|
|
|
|
<div class="container my-4">
|
|
<div class="card p-4" style="background-color: rgba(255,255,255,0.8); border-radius: 10px;">
|
|
<h2 class="text-center mb-4">@ViewData["Title"]</h2>
|
|
|
|
<div id="salesTableContainer" class="table-responsive mb-4"></div>
|
|
|
|
<div class="row mb-4">
|
|
<div class="col-md-6">
|
|
<label for="productSelect" class="form-label">Productos</label>
|
|
<select id="productSelect" class="form-select" multiple size="5"></select>
|
|
<small class="form-text text-muted">
|
|
Mantén Ctrl para seleccionar varios productos.
|
|
</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="legend" class="mb-4"></div>
|
|
|
|
<div id="salesChart" style="position: relative;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
@section Scripts {
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
<script>
|
|
d3.json('@Url.Action("GetMonthlyData", "Sales")').then(rawData => {
|
|
const salesData = d3.group(rawData, d => d.productName);
|
|
const margin = { top: 30, right: 20, bottom: 60, left: 60 },
|
|
width = 800 - margin.left - margin.right,
|
|
height = 450 - margin.top - margin.bottom;
|
|
|
|
const svg = d3.select("#salesChart")
|
|
.append("svg")
|
|
.attr("width", width + margin.left + margin.right)
|
|
.attr("height", height + margin.top + margin.bottom)
|
|
.append("g")
|
|
.attr("transform", `translate(${margin.left},${margin.top})`);
|
|
|
|
const x0 = d3.scaleBand().range([0, width]).padding(0.2),
|
|
x1 = d3.scaleBand().padding(0.05),
|
|
y = d3.scaleLinear().range([height, 0]);
|
|
|
|
const xAxisG = svg.append("g").attr("transform", `translate(0,${height})`);
|
|
const yAxisG = svg.append("g");
|
|
|
|
const allProducts = Array.from(salesData.keys());
|
|
const color = d3.scaleOrdinal()
|
|
.domain(allProducts)
|
|
.range(d3.schemeCategory10);
|
|
|
|
const productSelect = d3.select("#productSelect");
|
|
productSelect.selectAll("option")
|
|
.data(allProducts)
|
|
.join("option")
|
|
.attr("value", d => d)
|
|
.text(d => d);
|
|
|
|
const tooltip = d3.select("body")
|
|
.append("div")
|
|
.attr("id", "tooltip");
|
|
|
|
function updateTable(selected, months) {
|
|
const container = d3.select("#salesTableContainer");
|
|
container.selectAll("*").remove();
|
|
if (!selected.length) return;
|
|
|
|
const table = container.append("table")
|
|
.attr("class", "table table-bordered");
|
|
const thead = table.append("thead");
|
|
const tbody = table.append("tbody");
|
|
|
|
const headerRow = thead.append("tr");
|
|
headerRow.append("th").text("Producto");
|
|
months.forEach(m => headerRow.append("th").text(m));
|
|
|
|
selected.forEach(prod => {
|
|
const row = tbody.append("tr");
|
|
row.append("td").text(prod);
|
|
months.forEach(m => {
|
|
const rec = salesData.get(prod).find(d => d.month === m);
|
|
row.append("td").text(rec ? rec.units : 0);
|
|
});
|
|
});
|
|
}
|
|
|
|
function updateChart(selected) {
|
|
d3.select("#legend").selectAll("*").remove();
|
|
if (!selected.length) {
|
|
svg.selectAll(".barGroup").remove();
|
|
xAxisG.call(d3.axisBottom(x0));
|
|
yAxisG.call(d3.axisLeft(y));
|
|
d3.select("#salesTableContainer").selectAll("*").remove();
|
|
return;
|
|
}
|
|
|
|
selected.forEach(key => {
|
|
const item = d3.select("#legend")
|
|
.append("div")
|
|
.style("display", "flex")
|
|
.style("align-items", "center")
|
|
.style("margin-right", "1rem");
|
|
item.append("div")
|
|
.style("width", "16px")
|
|
.style("height", "16px")
|
|
.style("background", color(key))
|
|
.style("margin-right", "6px");
|
|
item.append("span").text(key);
|
|
});
|
|
|
|
const months = Array.from(new Set(
|
|
selected.flatMap(p => salesData.get(p).map(d => d.month))
|
|
)).sort();
|
|
|
|
updateTable(selected, months);
|
|
|
|
const data = months.map(month => {
|
|
const obj = { month };
|
|
selected.forEach(p => {
|
|
const rec = salesData.get(p).find(d => d.month === month);
|
|
obj[p] = rec ? rec.units : 0;
|
|
});
|
|
return obj;
|
|
});
|
|
|
|
x0.domain(months);
|
|
x1.domain(selected).range([0, x0.bandwidth()]);
|
|
y.domain([0, d3.max(data, d => d3.max(selected, p => d[p])) * 1.1]);
|
|
|
|
xAxisG.call(d3.axisBottom(x0))
|
|
.selectAll("text")
|
|
.attr("transform", "rotate(-40)")
|
|
.style("text-anchor", "end");
|
|
yAxisG.call(d3.axisLeft(y));
|
|
|
|
const groups = svg.selectAll(".barGroup")
|
|
.data(data, d => d.month);
|
|
|
|
const groupsEnter = groups.join(
|
|
enter => enter.append("g")
|
|
.attr("class", "barGroup")
|
|
.attr("transform", d => `translate(${x0(d.month)},0)`),
|
|
update => update,
|
|
exit => exit.remove()
|
|
);
|
|
|
|
groupsEnter.selectAll("rect")
|
|
.data(d => selected.map(p => ({
|
|
key: p,
|
|
value: d[p],
|
|
month: d.month
|
|
})))
|
|
.join(
|
|
enter => enter.append("rect")
|
|
.attr("x", d => x1(d.key))
|
|
.attr("y", height)
|
|
.attr("width", x1.bandwidth())
|
|
.attr("height", 0)
|
|
.attr("fill", d => color(d.key))
|
|
.on("mouseover", (e, d) => {
|
|
tooltip.html(`<strong>${d.key}</strong><br>${d.month}: ${d.value}`)
|
|
.style("opacity", 1);
|
|
})
|
|
.on("mousemove", e => {
|
|
tooltip.style("left", (e.pageX + 10) + "px")
|
|
.style("top", (e.pageY - 28) + "px");
|
|
})
|
|
.on("mouseout", () => {
|
|
tooltip.style("opacity", 0);
|
|
})
|
|
.call(g => g.transition().duration(600)
|
|
.attr("y", d => y(d.value))
|
|
.attr("height", d => height - y(d.value))
|
|
),
|
|
update => update.call(u => u.transition().duration(600)
|
|
.attr("x", d => x1(d.key))
|
|
.attr("y", d => y(d.value))
|
|
.attr("width", x1.bandwidth())
|
|
.attr("height", d => height - y(d.value))
|
|
.attr("fill", d => color(d.key))
|
|
),
|
|
exit => exit.call(e => e.transition().duration(300)
|
|
.attr("y", height)
|
|
.attr("height", 0)
|
|
.remove()
|
|
)
|
|
);
|
|
}
|
|
|
|
productSelect.on("change", function () {
|
|
const sel = Array.from(this.selectedOptions).map(o => o.value);
|
|
updateChart(sel);
|
|
});
|
|
|
|
productSelect.selectAll("option").property("selected", true);
|
|
updateChart(allProducts);
|
|
})
|
|
.catch(err => console.error("Error cargando datos de ventas:", err));
|
|
</script>
|
|
} |