function build_chart(div_selector, data_url, entity_name, category_type) {
const main_elt = document.getElementById("main")
const margin = {top: 60, right: 350, bottom: 100, left: 90},
width = main_elt.offsetWidth * 0.95 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
const height_ratio = 1.2; // how murch room to give above chart for lisibility
const tooltip_offset = {dx: 0, dy: 100}
const tooltip_offset_dot = {dx: 20, dy: 60}
const y_ticks_num = 5 // dont show to many y ticks for lisibility
const legend_cell_size = 15; // size of legend colored scare
const legend_x = width + 20; // begin legend a bit after the end of the chart
const dot_radius = 6; // size of total line dot
const xScale = d3.scaleBand()
.range([0, width])
.padding(0.4);
const yScale = d3.scaleLinear()
.range([height, 0]);
var colorScale = d3.scaleOrdinal([]); // Will be really set later by category type
//
// Configure chart by the category given as arg
//
var chart_title = ""
var category_title = ""
var draw_total_line = () => void 0;
var draw_categories_bars = () => void 0;
if (category_type == 'capacity') {
chart_title = "Charges par fonction"
category_title = "Fonction"
//draw_total_line
draw_categories_bars = draw_categories_grouped
colorScale = d3.scaleOrdinal(d3.schemeSet3);
} else if (category_type == 'service') {
chart_title = "Charges par service"
category_title = "Service"
draw_total_line = draw_totalcharge_line
draw_categories_bars = draw_categories_stacked
colorScale = d3.scaleOrdinal(d3.schemeTableau10);
} else if (category_type == 'project') {
chart_title = "Charges par projet"
category_title = "Projet"
draw_categories_bars = draw_categories_stacked
colorScale = d3.scaleOrdinal(d3.schemeTableau10);
} else {
alert("ALERT ! Every body shall quit the boat, we are sinking ! ALERT !")
}
const svg = d3.select(div_selector).append("svg")
.attr("id", "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 tooltip = d3.select('html')
.append("div")
.style("opacity", 0)
.attr("class", "tooltip")
var mousemove = function (e, d) {
tooltip
.style("left", (e.pageX - tooltip_offset.dx) + "px")
.style("top", (e.pageY - tooltip_offset.dy) + "px")
}
var mouseleave = function (d) {
tooltip
.transition()
.duration(900)
.style("opacity", 0)
}
var mouseover = function (e, d) {
var category_name = d3.select(this.parentNode).datum().key
var category_charge = d.data[category_name]
show_tooltip(e, category_name, category_charge)
}
var mouseovergrouped = function (e, d) {
var category_name = d.key
var category_charge = d.value
show_tooltip(e, category_name, category_charge)
}
var show_tooltip = function (e, category_name, category_charge) {
tooltip
.transition()
.duration(200)
.style("opacity", 1);
tooltip
.html("" + category_title + ": " + category_name + "
" + "Charge: " + category_charge + "%")
.style("left", (e.pageX - tooltip_offset.dx) + "px")
.style("top", (e.pageY - tooltip_offset.dy) + "px")
}
var mouseleavedot = function (e, d) {
d3.select(this).transition()
.duration(1)
.attr("r", dot_radius);
mouseleave(d);
}
var mouseoverdot = function (e, d) {
d3.select(this).transition()
.duration(1)
.attr("r", dot_radius * 1.5);
tooltip
.transition()
.duration(200)
.style("opacity", 1)
tooltip
.html("" + d.period + ": " + d.total + "%")
.style("left", (e.pageX - tooltip_offset_dot.dx) + "px")
.style("top", (e.pageY - tooltip_offset_dot.dy) + "px")
}
var addlegend = function (color_scale) {
let reverse_keys = color_scale.domain().reverse();
let legend = svg.append('g')
.attr('transform', 'translate(' + legend_x + ', -30)');
legend.selectAll()
.data(reverse_keys)
.enter().append('rect')
.attr('height', legend_cell_size + 'px')
.attr('width', legend_cell_size + 'px')
.attr('class', 'legend')
.attr('x', 5)
.attr('y', (d, i) => i * legend_cell_size)
.style("fill", d => color_scale(d));
legend.selectAll()
.data(reverse_keys)
.enter().append('text')
.attr("transform", (d, i) => "translate(30, " + (i * legend_cell_size + legend_cell_size / 1.6) + ")")
.attr('class', 'legend')
.style("fill", "black")
.text(d => d);
}
function draw_categories_grouped(data, categories) {
var y_max = d3.max(data, function (d) {
return d3.max(categories, function (c) {
return +d[c]
})
});
y_max = y_max * height_ratio
yScale.domain([0, y_max])
// Another scale for subgroup position
var xCategories = d3.scaleBand()
.domain(categories)
.range([0, xScale.bandwidth()])
.padding([0.2])
svg.append("g")
.selectAll("g")
// Enter in data = loop group per group
.data(data)
.enter()
.append("g")
.attr("transform", d => "translate(" + xScale(d.period) + ",0)")
.selectAll("rect")
.data(function (d) {
return categories.map(function (key) {
return {key: key, value: d[key]};
});
})
.enter().append("rect")
.attr("x", d => xCategories(d.key))
.attr("y", d => yScale(d.value))
.attr("width", xCategories.bandwidth())
.attr("height", d => height - yScale(d.value))
.attr("fill", d => colorScale(d.key))
.attr("class", "bar")
.on("mouseover", mouseovergrouped)
.on("mousemove", mousemove)
.on("mouseleave", mouseleave);
}
function draw_categories_stacked(data, categories) {
// Now build the stacked data for stacked bars
//
var stack = d3.stack()
.keys(categories)
// .order(d3.stackOrderNone)
// .offset(d3.stackOffsetNone);
var stacked_data = stack(data)
// Get the max y to plot
var y_max = d3.max(stacked_data[stacked_data.length - 1], d => d[1]);
if (y_max == 0) {
y_max = 100
}
// Enhance it by %ratio to leave room on the top of the graph
y_max = y_max * height_ratio
yScale.domain([0, y_max]);
// first draw one group for one category containing all bars over periods
let groups = svg.selectAll("g.category")
.data(stacked_data)
.enter()
.append("g")
.style("fill", (d) => colorScale(d.key));
// then draw each period/category bar
let rect = groups.selectAll("rect")
.data(d => d)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", d => xScale(d.data.period))
.attr("width", xScale.bandwidth())
// .attr("width", 5)
.attr("y", d => yScale(d[1]))
.attr("height", d => height - yScale(d[1] - d[0]))
.on("mouseover", mouseover)
.on("mousemove", mousemove)
.on("mouseleave", mouseleave);
}
function draw_totalcharge_line(data) {
// Build the total charge by period;
// it will be used for the line drawing above bars.
//
var periods_total_charge = []
data.forEach(function (d) {
// get the list of values for all columns except the first one which is the period name
var period_values = Object.values(d).slice(1)
var row = {}
row['period'] = d.period
row['total'] = d3.sum(period_values)
periods_total_charge.push(row)
});
// the line itselet
//
const line = d3.line()
.x(d => (xScale.bandwidth() / 2) + xScale(d.period)) // décalage pour centrer au milieu des barres
.y(d => yScale(d.total))
.curve(d3.curveMonotoneX); // Fonction de courbe permettant de l'adoucir
svg.append("path")
.datum(periods_total_charge)
.attr("class", "total-line")
.attr("d", line)
.lower();// set it below bars if called after
// data dots at each point
//
svg.selectAll("line-circle")
.data(periods_total_charge)
.enter().append("circle")
.attr("class", "total-circle")
.attr("r", dot_radius)
.attr("cx", function (d) {
return (xScale.bandwidth() / 2) + xScale(d.period)
})
.attr("cy", function (d) {
return yScale(d.total);
})
.on("mouseover", mouseoverdot)
.on("mouseleave", mouseleavedot)
.lower();// set it below bars if called after
;
}
d3.csv(data_url).then(data => {
// we could get categories that way,
// but instead we want to filter by non zero, see later
// var categories = data.columns.slice(1)
var periods = d3.map(data, d => d.period)
xScale.domain(periods)
// Filter datas to only keep non zero categories
// TODO: should be done on server (python/flask) side
//
// 1- Get the charge sum for each category over periods
// That will leave '0' to categories with no charge at all
// TODO: to be done with Object.keys, Object.values and d3 filtering methods
//
var categories_total_charge = {}
data.forEach(function (d) {
Object.keys(d).forEach(function (k) {
if (k === 'period') {
return;
}
if (categories_total_charge.hasOwnProperty(k)) {
categories_total_charge[k] += +d[k]
} else {
categories_total_charge[k] = d[k]
}
}
)
})
// 2- Now, filter only categories that have a non-zero charge
// TODO: to be done with Object.keys, Object.values and d3 filtering methods
//
var categories = []
for (var key in categories_total_charge) {
if (categories_total_charge[key] > 0) {
categories.push(key)
}
}
//
// Draw the bars, stacked or grouped
//
draw_categories_bars(data, categories)
//
// Draw the total charge line ( may be)
//
draw_total_line(data, categories)
//
// This former list we use as color_scale domain.
// And that allows us to build the legend
//
colorScale.domain(categories)
addlegend(colorScale)
// Xaxis
//
const xAxis = d3.axisBottom(xScale)
// Draw Xaxis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.9em")
.attr("dy", ".15em")
.attr("transform", "rotate(-65)");
// Yaxis
//
const yAxis = d3.axisLeft(yScale)
.ticks(y_ticks_num)
// Draw Yaxis
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
// Draw horizontal lines
svg.selectAll("y axis")
.data(yScale.ticks(y_ticks_num))
.enter()
.append("line")
.attr("class", d => (d == 0 ? "horizontalY0" : "horizontalY"))
.attr("x1", 0)
.attr("x2", width)
.attr("y1", d => yScale(d))
.attr("y2", d => yScale(d))
.lower();// set it below bars if called after
// Write Y axis title
svg.append("text")
.attr("text-anchor", "end")
.attr("transform", "rotate(-90)")
.attr("y", -margin.left + 40)
.attr("x", -margin.top - 70)
.text("Charge (% ETP)");
//
// Write chart Title
//
// part 1
svg.append("text")
.attr("x", (width / 2))
.attr("y", 0 - (margin.top / 2) - 10)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.text(entity_name);
// part 2
svg.append("text")
.attr("x", (width / 2))
.attr("y", 0 - (margin.top / 2) + 10)
.attr("text-anchor", "middle")
.style("font-size", "12px")
.text(chart_title);
}); // end of d3.csv().then({})
}