A better way for a stacked area chart toggle between a percent scale and absolute scale?

StackOverflow https://stackoverflow.com/questions/17349773

Вопрос

I am a newcomer to javascript and D3.

I am trying to create a stacked area chart that toggles between showing the data on an absolute scale and a percent scale. I realize that this involves playing around with

d3.layout.stack().offset("zero") and d3.layout.stack().offset("expand").

I've succeeded in getting the chart to do what I want:

http://jsfiddle.net/dSJ4E/

...but I'm not proud of my approach and am sure there is a better way to do it. Any ideas? Are there simple examples you might be aware of?

I am not happy with my code because upon setting up my if/else statement, I simply redeclare everything I've written prior to then, changing only the offset variable. That seems like a clunky approach. Also, I don't think this will allow me to set up transitions.

data = [{"type": "Group1",
             "values": [
                {"x":0, "y": 2.5},
                {"x":1, "y": 2.4},
                {"x":2, "y": 0.3}]},
            {"type": "Group2",
             "values": [
                {"x":0, "y": 1.5},
                {"x":1, "y": 1.3},
                {"x":2, "y": 1.1}]}
            ];
var stackZero = d3.layout.stack()
        .values(function(d){return d.values;})  
        .offset("zero");

stackZero(data);
var xScale = d3.scale.linear()
        .domain([0,2])
        .range([0, width]);

    var yScale = d3.scale.linear()
        .range([height,0])
        .domain([0, d3.max(data, function(d){return d3.max(d.values, function(d){return d.y0 + d.y;});})]);

    var area = d3.svg.area()
        .x(function(d){return xScale(d.x);})
        .y0(function(d){return yScale(d.y0);})
        .y1(function(d){return yScale(d.y0 + d.y);});       

    var svg = d3.selectAll("body")
        .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 +")");

    svg.selectAll(".layers")
        .data(data)
        .enter()
        .append("path")
        .attr("class", "layer")
        .attr("d", function(d){return area(d.values);})
        .style("fill", function (d,i){return colors(i)});

    var yAxis = d3.svg.axis()
        .scale(yScale)
        .orient("left");

    svg.append("g")
        .attr("class", "y axis")
        .attr("transform", "translate(0,0)")
        .call(yAxis);   

    d3.select("p") //now we start to interact with the chart
        .on("click", function() { 

            console.log("entering variable is " + stackType);

            svg.selectAll("path").data([]).exit().remove();
            svg.selectAll(".y.axis").data([]).exit().remove();
if(stackType){ //enter true, or expanded data

            var stackExpand = d3.layout.stack()
                .values(function(d){return d.values;})
                .offset("expand");

            stackExpand(data);

            console.log(data);

            var yScale = d3.scale.linear()
                .range([height,0])
                .domain([0, d3.max(data, function(d){return d3.max(d.values, function(d){return d.y0 + d.y;});})]);

            var area = d3.svg.area()
                .x(function(d){return xScale(d.x);})
                .y0(function(d){return yScale(d.y0);})
                .y1(function(d){return yScale(d.y0 + d.y);});

            svg.selectAll(".layers")
                //.data(stackZero(data))
                .data(stackExpand(data))
                .enter()
                .append("path")
                .attr("class", "layer")
                .attr("d", function(d){return area(d.values);})
                .style("fill", function (d,i){return colors(i)});

            formatter = d3.format(".0%");

            var yAxis = d3.svg.axis()
                .scale(yScale)
                .orient("left")
                .tickFormat(formatter);

            svg.append("g")
                .attr("class", "y axis")
                .attr("transform", "translate(0,0)")
                .call(yAxis);


            stackType = false;
            console.log("exiting variable is " + stackType);

            } else { //enter false, or zero data

            data = [{"type": "Group1",
             "values": [
                {"x":0, "y": 2.5},
                {"x":1, "y": 2.4},
                {"x":2, "y": 0.3}]},
            {"type": "Group2",
             "values": [
                {"x":0, "y": 1.5},
                {"x":1, "y": 1.3},
                {"x":2, "y": 1.1}]}
            ];

            var stackZero = d3.layout.stack()
                .values(function(d){return d.values;})
                .offset("zero");

            stackZero(data);

            console.log(data);

            var yScale = d3.scale.linear()
                .range([height,0])
                .domain([0, d3.max(data, function(d){return d3.max(d.values, function(d){return d.y0 + d.y;});})]);

            var area = d3.svg.area()
                .x(function(d){return xScale(d.x);})
                .y0(function(d){return yScale(d.y0);})
                .y1(function(d){return yScale(d.y0 + d.y);});

            svg.selectAll(".layers")
                .data(stackZero(data))
                //.data(stackExpand(data))
                .enter()
                .append("path")
                .attr("class", "layer")
                .attr("d", function(d){return area(d.values);})
                .style("fill", function (d,i){return colors(i)});   

            stackType = true;
            console.log("exiting variable is" + stackType);

            var yAxis = d3.svg.axis()
                .scale(yScale)
                .orient("left");

            svg.append("g")
                .attr("class", "y axis")
                .attr("transform", "translate(0,0)")
                .call(yAxis);

            };  

});
    </script>
Это было полезно?

Решение

So there are a lot of things you can do to avoid repeating yourself in the code. First of all a lot of the D3 functions you are using can be reused - they are general functions. This together with the concept of javascript closures means you only have to declare them once and initialize the parts that won't change.

The stack layout does overwrite the y and y0 values, so to get the toggle to work I renamed your initial data to raw_x and raw_y and adjusted the accessors appropriately.

Then the work of figuring out the yScale domain and actually updating the paths I wrapped into a function:

function drawChart() {
    yScale.domain([0, d3.max(data, function (d) {
        return d3.max(d.values, function (d) {
            return d.y0 + d.y;
        });
    })]);

    // new data items need to get added
    var areas = svg.selectAll(".layer")
    .data(data, function(d) { return d.type; });

    areas.enter()
        .append("path")
        .attr("class", "layer")
        .style("fill", function (d, i) {
            return colors(i)
    });

    // Added and updated items need to be updated
    areas.attr("d", function (d) {
        return area(d.values);
    })

    // Old items need to be removed - we should not actually need this with the data as it is
    areas.exit().remove();

    svg.selectAll("g.y.axis").call(yAxis);
}

It's really just a start and you will want to reorganize based on your needs, but take a look at an updated fiddle

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top