charges.js 12.9 KB
function build_chart(div_selector, data_url, entity_name, category) {
    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 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])

    var colorScale = d3.scaleOrdinal([])

    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
        colorScale = d3.scaleOrdinal(d3.schemeSet3);
    } else if (category == '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 == '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(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]
        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("<b>" + category_title + ":</b> " + category_name + "<br>" + "<b>Charge:</b> " + 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("<b>" + d.period + ": </b>" + 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", 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 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(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({})

}