Search code examples
jquerycsshtmlbackbone.jsd3.js

D3.js drawing bar graph rectangles on one line only


Trying to follow this demo that uses Backbone and D3.js to create dynamically updated charts/graphs. Just did a quick copy/paste to try and play around with the code and I cannot get the bars to line up horizontally. Each bar in the bar graph is drawn on the first line of the graph, creating a "graph" that looks like this

enter image description here

Interestingly enough, the text is written correctly, however it's just the <rect> tags that seem to be drawn on the same line. First time using this plugin and very new Backbone (and CSS for that matter) so any ideas/suggestions greatly appreciated.

SliderApp.js

var w = 440,
h = 200;


var DataPoint = Backbone.Model.extend({

    initialize: function (x) {
        this.set({
            x: x
        });
    },

    type: "point",

    randomize: function () {
        this.set({
            x: Math.round(Math.random() * 10)
        });
    }

});

var DataSeries = Backbone.Collection.extend({

    model: DataPoint,

    fetch: function () {
        this.reset();
        this.add([
            new DataPoint(10),
            new DataPoint(12),
            new DataPoint(15),
            new DataPoint(18)
        ]);
    },

    randomize: function () {
        this.each(function (m) {
            m.randomize();
        });
    }

});

var BarGraph = Backbone.View.extend({

    "el": "#graph",

    initialize: function () {

        _.bindAll(this, "render", "frame");
        this.collection.bind("reset", this.frame);
        this.collection.bind("change", this.render);


        this.chart = d3.selectAll($(this.el)).append("svg").attr("class", "chart").attr("width", w).attr("height", h).append("g").attr("transform", "translate(10,15)");

        this.collection.fetch();
    },

    render: function () {

        var data = this.collection.models;

        var x = d3.scale.linear().domain([0, d3.max(data, function (d) {
            return d.get("x");
        })]).range([0, w - 10]);

        var y = d3.scale.ordinal().domain([0, 1, 2, 3]).rangeBands([0, h - 20]);

        var self = this;
        var rect = this.chart.selectAll("rect").data(data, function (d, i) {
            return i;
        });

        rect.enter().insert("rect", "text").attr("y", function (d) {
            return y(d.get("x"));
        }).attr("width", function (d) {
            return x(d.get("x"));
        }).attr("height", y.rangeBand());

        rect.transition().duration(1000).attr("width", function (d) {
            return x(d.get("x"));
        }).attr("height", y.rangeBand());

        rect.exit().remove();

        var text = this.chart.selectAll("text").data(data, function (d, i) {
            return i;
        });

        text.enter().append("text")
         .attr("x", function (d) {
             return x(d.get("x"));
         })
         .attr("y", function (d, i) { return y(i) + y.rangeBand() / 2; })
         .attr("dx", -3) // padding-right
         .attr("dy", ".35em") // vertical-align: middle
         .attr("text-anchor", "end") // text-align: right
            .text(function (d) { return d.get("x"); });

        text
        .transition()
        .duration(1100)
        .attr("x", function (d) {
            return x(d.get("x"));
        })
         .text(function (d) { return d.get("x"); });
    },

    frame: function () {

        this.chart.append("line").attr("y1", 0).attr("y2", h - 10).style("stroke", "#000");

        this.chart.append("line").attr("x1", 0).attr("x2", w).attr("y1", h - 10).attr("y2", h - 10).style("stroke", "#000");
    }


});


$(function () {

    var dataSeries = new DataSeries();
    new BarGraph({
        collection: dataSeries
    }).render();

    setInterval(function () {
        dataSeries.randomize();
    }, 2000);
});

Razor/Layout.cshtml

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width" />
        <title>@ViewBag.Title</title>
        @Styles.Render("~/Content/ChartCss")
        @Styles.Render("~/Content/css")
        @Styles.Render("~/Content/uiSlider")
        @Scripts.Render("~/bundles/jquery")
        @Scripts.Render("~/bundles/jqueryui")
        @Scripts.Render("~/bundles/jsMVC")
        @Scripts.Render("~/bundles/BackboneApps")
        @Scripts.Render("~/bundles/SignalR")
        @Scripts.Render("~/bundles/Charts")
        <script src="/signalr/hubs"></script>
    </head>
<body>
    @RenderBody()
</body>
</html>

Razor/Index.cshtml

<div id="graph">
</div>

Rendered HTML

<html>
   <head>
      <meta charset="utf-8"/>
      <meta name="viewport" content="width=device-width"/>
      <title>
      </title>
      <link href="/Content/Charts.css" rel="stylesheet"/>
      <link href="/Content/site.css" rel="stylesheet"/>
      <link href="/Content/themes/base/jquery-ui.css" rel="stylesheet"/>
      <link href="/Content/themes/base/jquery-ui.structure.css" rel="stylesheet"/>
      <link href="/Content/themes/base/jquery.ui.slider.css" rel="stylesheet"/>
      <script src="/Scripts/jquery.js">
      </script>
      <script src="/Scripts/jquery-ui.js">
      </script>
      <script src="/Scripts/underscore.js">
      </script>
      <script src="/Scripts/backbone.js">
      </script>
      <script src="/Scripts/SliderApp.js">
      </script>
      <script src="/Scripts/jquery.signalR-2.1.2.js">
      </script>
      <script src="/Scripts/SignalRApp.js">
      </script>
      <script src="/Scripts/d3.js">
      </script>
      <script src="/Scripts/ChartView.js">
      </script>
      <script src="/signalr/hubs">
      </script>
   </head>
   <body>
      <div id="graph">
         <svg class="chart" width="440" height="200">
            <g transform="translate(10,15)">
               <line y1="0" y2="190" style="stroke: rgb(0, 0, 0);">
               </line>
               <line x1="0" x2="440" y1="190" y2="190" style="stroke: rgb(0, 0, 0);">
               </line>
               <rect width="286.66666666666663" height="45">
               </rect>
               <rect width="430" height="45">
               </rect>
               <rect width="382.22222222222223" height="45">
               </rect>
               <rect width="191.11111111111111" height="45">
               </rect>
               <text x="286.66666666666663" y="22.5" dx="-3" dy=".35em" text-anchor="end">
                  6
               </text>
               <text x="430" y="67.5" dx="-3" dy=".35em" text-anchor="end">
                  9
               </text>
               <text x="382.22222222222223" y="112.5" dx="-3" dy=".35em" text-anchor="end">
                  8
               </text>
               <text x="191.11111111111111" y="157.5" dx="-3" dy=".35em" text-anchor="end">
                  4
               </text>
            </g>
         </svg>
      </div>
   </body>
</html>

site.css

html {
    background-color: #e2e2e2;
    margin: 0;
    padding: 0;
}

body {
    background-color: #fff;
    border-top: solid 10px #000;
    color: #333;
    font-size: .85em;
    font-family: "Segoe UI", Verdana, Helvetica, Sans-Serif;
    margin: 0;
    padding: 0;
}

Charts.css

.chart rect {
   stroke: white;
   fill: steelblue;
}

Solution

  • I haven't tested it, and I don't know Backbone at all, but it looks to me like you're using your y scale incorrectly.

    In:

        rect.enter().insert("rect", "text").attr("y", function (d) {
            return y(d.get("x"));
        }).attr("width", function (d) {
            return x(d.get("x"));
        }).attr("height", y.rangeBand());
    

    the code y(d.get("x")) is probably not returning anything useful. The y domain is defined as 0..3, and you're passing in d.get("x") which is going to be 10, 12, 15... When you pass in a value outside the domain to an ordinal scale, you get NaN back.

    What you probably want is:

        rect.enter().insert("rect", "text").attr("y", function (d, i) {
            return y(i);
        }).attr("width", function (d) {
            return x(d.get("x"));
        }).attr("height", y.rangeBand());
    

    That'll pass in the index of the data element, rather than it's value to the y scale, which should result in a corresponding value being spit out. The difference is in the first two lines. I've added the index variable i as a parameter to the anonymous function, and instead I'm returning y(i).

    If you look at the code you've got for creating the text elements for each bar, you've actually got it right there, you're using y(i) in the function for calculating the position of your y attribute.

    Here's a snippet that I developed from your code (the data is defined statically and the corresponding ways of referencing it have changed, otherwise it's a copy and paste):

    var data = [
    { x: 10, },
    { x: 12, },
    { x: 15, },
    { x: 18, }
    ];
    
    var w = 440, h = 200;
    
    var x = d3.scale.linear().domain([0, d3.max(data, function (d) {
                return d.x;
                })]).range([0, w - 10]);
    
    var y = d3.scale.ordinal().domain([0, 1, 2, 3]).rangeBands([0, h - 20]);
    
    var svg = d3.selectAll("div#graph").append("svg")
    .attr("class", "chart")
    .attr("width", w).attr("height", h)
    .append("g")
    .attr("transform", "translate(10,15)");
    
    var rect = svg.selectAll("rect").data(data, function (d, i) {
            return i;
            });
    
    rect.enter().insert("rect", "text").attr("y", function (d) {
            return y(d.x);
    }).attr("width", function (d) {
        return x(d.x);
    }).attr("height", y.rangeBand());
    
    rect.transition().duration(1000).attr("width", function (d) {
            return x(d.x);
    }).attr("height", y.rangeBand());
    
    rect.exit().remove();
    
    var text = svg.selectAll("text").data(data, function (d, i) {
            return i;
            });
    
    text.enter().append("text")
    .attr("x", function (d) {
            return x(d.x);
    })
    .attr("y", function (d, i) { return y(i) + y.rangeBand() / 2; })
    .attr("dx", -3) // padding-right
    .attr("dy", ".35em") // vertical-align: middle
    .attr("text-anchor", "end") // text-align: right
    .text(function (d) { return d.x; });
    
        text
        .transition()
    .duration(1100)
        .attr("x", function (d) {
                return x(d.x);
                })
    .text(function (d) { return d.x; });
    .chart rect {
      stroke: white;
      fill: steelblue;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
    <div id="graph"></div>

    Here's a corrected snippet:

    var data = [
    { x: 10, },
    { x: 12, },
    { x: 15, },
    { x: 18, }
    ];
    
    var w = 440, h = 200;
    
    var x = d3.scale.linear().domain([0, d3.max(data, function (d) {
                return d.x;
                })]).range([0, w - 10]);
    
    var y = d3.scale.ordinal().domain([0, 1, 2, 3]).rangeBands([0, h - 20]);
    
    var svg = d3.selectAll("div#graph").append("svg")
    .attr("class", "chart")
    .attr("width", w).attr("height", h)
    .append("g")
    .attr("transform", "translate(10,15)");
    
    var rect = svg.selectAll("rect").data(data, function (d, i) {
            return i;
            });
    
    rect.enter().insert("rect", "text").attr("y", function (d, i) {
            return y(i);
    }).attr("width", function (d) {
        return x(d.x);
    }).attr("height", y.rangeBand());
    
    rect.transition().duration(1000).attr("width", function (d) {
            return x(d.x);
    }).attr("height", y.rangeBand());
    
    rect.exit().remove();
    
    var text = svg.selectAll("text").data(data, function (d, i) {
            return i;
            });
    
    text.enter().append("text")
    .attr("x", function (d) {
            return x(d.x);
    })
    .attr("y", function (d, i) { return y(i) + y.rangeBand() / 2; })
    .attr("dx", -3) // padding-right
    .attr("dy", ".35em") // vertical-align: middle
    .attr("text-anchor", "end") // text-align: right
    .text(function (d) { return d.x; });
    
        text
        .transition()
    .duration(1100)
        .attr("x", function (d) {
                return x(d.x);
                })
    .text(function (d) { return d.x; });
    .chart rect {
      stroke: white;
      fill: steelblue;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
    <div id="graph">
    </div>

    I've modified your code to remove the Backbone stuff, and the data is defined statically, along with the changes that brings when referencing the data in your code. Otherwise the only differences between them are as I've described above.

    Note, you may want to setup your y scale like so:

    var y = d3.scale.ordinal().domain(d3.range(0, data.length-1)).rangeBands([0, h - 20]);
    

    It will then split the y scale into as many bands as you have records in your data array, rather than hard coding the domain.