655321
655321

Reputation: 421

Dynamically changing bins and height of a d3.js histogram

I want to be able to use a range slider to dynamically change the number of bins/bars/rectangles. The code below is almost there but there are a few problems I haven't been able to figure out.

  1. When I use the range slider, it seems to skip some of the values.
  2. The height of the bars/rectangles goes beyond the height once you slide to the low numbers.

The jsfiddle is here.

css:

body {
    font: 10px sans-serif;
}
.bar rect {
    fill: steelblue;
    shape-rendering: crispEdges;
}
.bar text {
    fill: #fff;
}

.axis path,
.axis line {
    fill: none;
    stroke: #000;
    shape-rendering: crispEdges;
}
#slider-range-max {
    width: 100%;
}

html:

<div id="histogram_container">
    <div id="histogram"></div>
    <div align="center">
        <p>
            <label for="bins">Number of bins:</label>
            <input type="text" id="bins" readonly style="border:0; color:#f6931f; font-weight:bold;">
        </p>
        <div id="slider-range-max"></div>
    </div>
</div>

js:

var grades_data = [],
    grades = [];

var score_max = 100,
    score_min = 0;

var age_max = 18,
    age_min = 5;

var data_generator = (function() {
    var gen = d3.random.normal(score_max / 2, 15);
    return function() {
        return Math.floor(Math.max(score_min, Math.min(gen(), score_max)));
    }
}());

for (var i = 0; i < 1001; i++) {
    var age = Math.floor(Math.random() * (age_max - age_min + 1)) + age_min;
    var grade = data_generator();

    grades_data.push({
        age,
        grade
    });
    grades.push(grade);
}

var margin = {
        top: 20,
        right: 20,
        bottom: 30,
        left: 40
    },
    width = 600 - margin.left - margin.right,
    height = 600 - margin.top - margin.bottom;

var format_count = d3.format(",.0f");

var x_gh_scale = d3.scale.linear().range([0, width])
    .domain([0, 100]),
    x_gh_axis = d3.svg.axis().scale(x_gh_scale).orient("bottom");

var data = d3.layout.histogram()
    .bins(x_gh_scale.ticks(20))
    (grades);

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

var grade_histo_svg = d3.select("#histogram").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 + ")");

var bar = grade_histo_svg.selectAll(".bar")
    .data(data)
    .enter().append("g")
    .attr("class", "bar")
    .attr("transform", function(d) {
        return "translate(" + x_gh_scale(d.x) + "," + y_gh_scale(d.y) + ")";
    });

bar.append("rect")
    .attr("x", 1)
    .attr("width", x_gh_scale(data[0].dx) - 1)
    .attr("height", function(d) {
        return height - y_gh_scale(d.y);
    });

bar.append("text")
    .attr("dy", ".75em")
    .attr("y", 6)
    .attr("x", x_gh_scale(data[0].dx) / 2)
    .attr("text-anchor", "middle")
    .text(function(d) {
        return format_count(d.y);
    });

grade_histo_svg.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(x_gh_axis);

$(function() {
    $("#slider-range-max").slider({
        range: "max",
        min: 1,
        max: 20,
        value: 20,
        slide: function(event, ui) {
            $("#bins").val(ui.value);

            var data = d3.layout.histogram()
                .bins(x_gh_scale.ticks(ui.value))
                (grades);

            var bar = grade_histo_svg.selectAll(".bar")
                .remove()
                .data(data)
                .enter().append("g")
                .attr("class", "bar")
                .attr("transform", function(d) {
                    return "translate(" + x_gh_scale(d.x) + "," + y_gh_scale(d.y) + ")";
                });

            bar.append("rect")
                .attr("x", 1)
                .attr("width", x_gh_scale(data[0].dx) - 1)
                .attr("height", function(d) {
                    return height - y_gh_scale(d.y);
                });

            bar.append("text")
                .attr("dy", ".75em")
                .attr("y", 6)
                .attr("x", x_gh_scale(data[0].dx) / 2)
                .attr("text-anchor", "middle")
                .text(function(d) {
                    return format_count(d.y);
                });
        }
    });
    $("#bins").val($("#slider-range-max").slider("value"));
});

Upvotes: 1

Views: 1874

Answers (1)

Mark
Mark

Reputation: 108567

Couple strange things in your code.

First,

 var data = d3.layout.histogram()
   .bins(x_gh_scale.ticks(ui.value)) //<-- what is your intention here?
   (grades);

.ticks returns an array of where the ticks are. You seem to be using this as thresholds, but this can produce undesired result:

The specified count is only a hint; the scale may return more or fewer values depending on the input domain.

If you want N bins just call it as:

var data = d3.layout.histogram()
   .bins(ui.value)
   (grades);

Second, you need to adjust your y scale domain after you re-bin to keep your rects from flowing off the page. This seems to do the trick:

y_gh_scale.domain([0, d3.max(data, function(d){
  return d.length
})]);

Third, you can't chain your .remove() call in the way you are doing. It returns the removed elements and you just want to re-enter fresh:

 grade_histo_svg.selectAll(".bar")
   .remove()

 var bar = grade_histo_svg.selectAll(".bar")
  .data(data)
  .enter().append("g")
  .attr("class", "bar")
  .attr("transform", function(d) {
    return "translate(" + x_gh_scale(d.x) + "," + y_gh_scale(d.y) + ")";
  });

Here's an updated fiddle with this changes applied.

Upvotes: 2

Related Questions