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 const tooltip_offset = {dx: 0, dy: 100} const tooltip_offset_dot = {dx: 20, dy: 60} const colorScale = d3.scaleOrdinal(d3.schemeCategory10); const y_ticks_num = 5 const legend_cell_size = 15; // colored scare size const legend_x = width + 20; // begin legend a bit after the end of the chart const dot_radius = 6; const xScale = d3.scaleBand() .range([0, width]) .padding(0.2); const yScale = d3.scaleLinear() .range([height, 0]) function build_chart(div_selector, data_url, project_name, category) { var chart_title = "" var category_title = "" var draw_total_line = function (data, categories) { } var draw_categories_bars = function (data, categories) { } if (category == 'capacity') { chart_title = "Charges par fonction" category_title = "Fonction" //draw_total_line draw_categories_bars = draw_categories_grouped } else if (category == 'service') { chart_title = "Charges par service" category_title = "Service" draw_total_line = draw_totalcharge_line draw_categories_bars = draw_categories_stacked } 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(100) .style("opacity", 0) } var mouseover = function (e, d) { var category_name = d3.select(this.parentNode).datum().key var category_charge = d.data[category_name] 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.05]) svg.append("g") .selectAll("g") // Enter in data = loop group per group .data(data) .enter() .append("g") .attr("transform", function (d) { return "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", function (d) { return xCategories(d.key); }) .attr("y", function (d) { return yScale(d.value); }) .attr("width", xCategories.bandwidth()) .attr("height", function (d) { return height - yScale(d.value); }) .attr("fill", function (d) { return colorScale(d.key); }); } 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 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 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 we intend 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 categori/s // // 1- Get the charge sum for each categories 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 (categories_total_charge.hasOwnProperty(k)) { categories_total_charge[k] += +d[k] } else { categories_total_charge[k] = 0 } } ) }) // 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 group // 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) // Yaxis // const yAxis = d3.axisLeft(yScale) .ticks(y_ticks_num) // 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)"); // 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(project_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({}) }