whytheq
whytheq

Reputation: 35557

Bar chart to take account of negative values

I have the following data in a csv file called BarData.csv:

Fruit,dt,amount
Apple,12/28/2016,-1256
Apple,12/29/2016,-500
Apple,12/30/2016,3694
Apple,12/31/2016,5586
Apple,1/1/2017,4558
Apple,1/2/2017,6696
Apple,1/3/2017,7757
Apple,1/4/2017,8528
Apple,1/5/2017,5543
Apple,1/6/2017,3363
Apple,1/7/2017,5464
Pear,12/25/2017,250
Pear,12/26/2017,669
Pear,12/27/2017,441
Pear,12/28/2017,159
Pear,12/29/2017,357
Pear,12/30/2017,775
Pear,12/31/2017,669

The following html, css, and javascript is in one .html file:

<!DOCTYPE html>
<html>

<head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
    <title>BAR SINGLE FUNCTION</title>
    <script src="http://d3js.org/d3.v3.js"></script>
    <style type="text/css">
    #radioDiv {
        top: 45px;
        font-family: verdana;
        font-size: 8px;
        width: 455px;
    }

    #TOPbarChart {
        position: absolute;
        top: 50px;
        left: 30px;
        width: 750px;
        height: 195px;
    }

    .axis--y path,
    .axis--x path {
        display: none;
    }

    .axis--x line,
    .axis--y line {
        stroke: black;
        fill: none;
        stroke-width: 2px
    }

    .yAxis text,
    .xAxis text {
        font: 7pt Verdana;
        stroke: none;
        fill: black;
    }

    .title,
    .titleX {
        font-family: Verdana;
        font-size: 10px;
    }
    </style>
</head>

<body>
    <div id="radioDiv">
        <label>
            <input id="radioFrt" type="radio" name="frt" value="Apple" class="radioB" checked> APPLE
        </label>
        <label>
            <input type="radio" name="frt" value="Pear" class="radioB"> PEAR
        </label>
    </div>
    <div id="TOPbarChart"></div>
    <script type="text/javascript">
    var currentFruit = "Apple";
    var currentColr = "#00a5b6";

    var barDataCSV_Dly = "BarData.csv";

    //
    //
    // radio button
    document.getElementById("radioFrt").checked = true;
    d3.selectAll('input[name="frt"]').on("change", function change() {
        currentFruit = this.value;
        TOPbarChart(currentFruit, currentColr);
    });

    //FORMATS 
    var parseDate = d3.time.format("%m/%d/%Y").parse;



    // 
    // BASIC SIZING
    // 
    function barChartBasics() {
        var margin = {
                top: 25,
                right: 35,
                bottom: 25,
                left: 70
            },
            width = 550 - margin.left - margin.right,
            height = 155 - margin.top - margin.bottom,
            colorBar = d3.scale.category20(),
            barPaddingFine = 1,
            barPaddingThick = 2;
        return {
            margin: margin,
            width: width,
            height: height,
            colorBar: colorBar,
            barPaddingFine: barPaddingFine,
            barPaddingThick: barPaddingThick
        };
    }


    // create svg element
    var basics = barChartBasics();
    var svg = d3.select("#TOPbarChart")
        .append("svg")
        .attr({
            "width": basics.width + basics.margin.left + basics.margin.right,
            "height": basics.height + basics.margin.top + basics.margin.bottom,
            id: "svgTOPbarChart"
        });

    // create svg  group
    var plot = svg
        .append("g")
        .attr({
            "transform": "translate(" + basics.margin.left + "," + basics.margin.top + ")",
            id: "svgPlotTOPbarChart"
        });

    var axisPadding = 2;
    var leftAxisGroup = svg
        .append('g')
        .attr({
            transform: 'translate(' + (basics.margin.left - axisPadding) + ',' + (basics.margin.top) + ')',
            'class': "yAxis axis--y",
            id: "yAxisGTOPbarChart"
        });

    var bottomAxisGroup = svg
        .append('g')
        .attr({
            'class': "xAxis axis--x",
            id: "xAxisGTOPbarChart"
        });

    var titleTxt = svg.append("text")
        .attr({
            x: basics.margin.left + 12,
            y: 20,
            'class': "title",
            'text-anchor': "start"
        })

    // create scales with ranges
    var xScale = d3.time.scale().range([0, basics.width]);
    var yScale = d3.scale.linear().range([basics.height, 0]);




    function TOPbarChart(
        frt, colorChosen) {

        // get the data
        d3.csv(barDataCSV_Dly, function(rows) {

            TOPbarData = rows.map(function(d) {
                return {
                    "Fruit": d.Fruit,
                    "dt": parseDate(d.dt),
                    "amount": +d.amount
                };
            }).filter(function(row) {
                if (row['Fruit'] == frt) {
                    return true;
                }
            });


            // create domains for the scales
            xScale.domain(d3.extent(TOPbarData, function(d) {
                return d.dt;
            }));
            var amounts = TOPbarData.map(function(d) {
                return d.amount;
            });
            var yMax = d3.max(amounts);
            var yMin = d3.min(amounts);
            var yMinFinal = 0;
            if (yMin < 0) {
                yMinFinal = yMin;
            }
            yScale.domain([yMinFinal, yMax]);


            // introduce the bars
            // var plot = d3.select("#svgPlotTOPbarChart")
            var sel = plot.selectAll("rect")
                .data(TOPbarData);

            sel.enter()
                .append("rect")
                .attr({
                    x: function(d, i) {
                        return xScale(d.dt);
                    },
                    y: function(d) {
                        return yScale(d.amount);
                    },
                    width: (basics.width / TOPbarData.length - basics.barPaddingFine),
                    height: function(d) {
                        return basics.height - yScale(d.amount);
                    },
                    fill: colorChosen,
                    'class': "bar"
                });

            // this little function will create a small ripple affect during transition
            var dlyRipple = function(d, i) {
                return i * 100;
            };
            sel
                .transition()
                .duration(dlyRipple) //1000
                .attr({
                    x: function(d, i) {
                        return xScale(d.dt);
                    },
                    y: function(d) {
                        return yScale(d.amount);
                    },
                    width: (basics.width / TOPbarData.length - basics.barPaddingFine),
                    height: function(d) {
                        return basics.height - yScale(d.amount);
                    },
                    fill: colorChosen
                });

            sel.exit().remove();


            // add/transition y axis - with ticks and tick markers
            var axisY = d3.svg.axis()
                .orient('left')
                .scale(yScale)
                .tickFormat(d3.format("s")) // use abbreviations, e.g. 5M for 5 Million
                .outerTickSize(0);
            leftAxisGroup.transition().duration(1000).call(axisY);

            // add/transition x axis - with ticks and tick markers
            var axisX = d3.svg.axis()
                .orient('bottom')
                .scale(xScale);

            bottomAxisGroup
                .attr({
                    transform: 'translate(' + (basics.margin.left + ((basics.width / TOPbarData.length) / 2)) + ',' + (basics.margin.top + basics.height) + ')',
                })
                .transition().duration(1000).call(axisX.ticks(5));


            titleTxt.text("Daily: last " + TOPbarData.length + " days");
            // console.log(TOPbarData.length)

        });
    }

    //
    //
    //
    //
    TOPbarChart(currentFruit, currentColr);
    //
    //
    //
    //
    </script>
</body>

</html>

When all the data is positive everything is pretty much ok - but when some of the data is negative we can see the result in this plunker demo:

http://plnkr.co/edit/1hudJYkRq2MnuIlwxXZi?p=preview

How do I amend the code so that: - the negative bars are shown? - the base of the positive bars moves vertically up when negative numbers are included? - the vertical movement is also included in the transition?

Above is more than 1 question but help on any would be appreciated.

Upvotes: 0

Views: 608

Answers (1)

sparta93
sparta93

Reputation: 3854

The key is to play with the y and height attributes of the bars to position them correctly.

For y, change it to:

  y: function(d) {
          return yScale(Math.max(0, d.amount));
        },

And for the height, change it to:

height: function(d) {
              return Math.abs(yScale(d.amount) - yScale(0));
            },

You can then style the negative bars to make them a different color.

Check the updated Plunkr - http://plnkr.co/edit/q7dQsPW0PiPuwFTy8gLN?p=preview

Edit:

For the coloring part, you can achieve it with a 1 liner if you want to reduce lines and want more simplicity.

Instead of:

  fill: function(d) {
                var col = colorChosen
                if (d.amount < 0) {
                    col = "#FF0000";
                }
                return col;
            },
  });

You can do:

  fill: function(d) {
                        return d.amount < 0 ? "#FF0000" : colorChosen;
                    }, 

Upvotes: 1

Related Questions