Skip to main content

Ivan Teoh

Something personal yet public

D3: Tutorial - Scale graph

First Protovis, now D3. Both are works from Mike Bostock. Both are data visualization JavaScript libraries.

This tutorial we’re going to create a basic dot graph with scalable y-axis. It is adapted from one of the examples in D3, dot.html.

Step 1: Default html

First of all, we create a default html that contains a div for input button and another for the graph. We also link the d3 JavaScript library. The rest of tutorial will be concentrate of JavaScript code that will be written after the comment, "Scale graph code here."

176-step1.html (Source)

<!DOCTYPE html>
<html>
<head>
    <title>Scale Graph</title>
</head>
<body>
    <div id="demoContainer">
        <div id="option">
            <input name="updateButton" type="button" value="Update"/>
        </div>
        <div id="mainGraph">
        </div>
    </div>
    <script type="text/javascript"
      src="http://mbostock.github.com/d3/d3.js?1.27.1"></script>
    <script type="text/javascript">
    <!-- Scale graph code here. -->
    </script>
</body>
</html>

Step 2: Random data

We need to generate some random data using Math.random(). We create randomData function for returning a list of ten x and y values. For example, [{“x”: 0, “y”: 0.3}, {“x”: 0.1, “y”: 0.4}, ...]

176-step2.js (Source)

function randomData() {
    return d3.range(10).map(function(i) {
        return {x: i / 9, y: Math.random()};
    });
}

Step 3: Maximum value of y-axis

For this tutorial, only y axis values are random. We need to get maximum value of the y axis using d3.max, in order to scale the y axis based on the maximum value of the data.

176-step3.js (Source)

var data = randomData();
var newMaxY = d3.max(data, function(d) {return d.y;});

Step 4: Ceil value

Sometime, the maximum value doesn’t suitable for domain range in scale axis. For example, 0.33333. Mike suggested a method to increase the size of domain to next highest rounded value.

176-step4.js (Source)

function reDomain(maxValue) {
    var dy = Math.pow(10, Math.round(Math.log(maxValue) / Math.log(10)) - 1);
    return Math.ceil(maxValue / dy) * dy;
}

var newCeilY = reDomain(newMaxY);

Step 5: SVG container

Now, we determine the size of the graph, which are w and h for width and height. We need a svg container for the graph. We will append it inside #mainGraph div with width and height attributes. This is optional, which offset the graph with modify coordinate using translate in transform in a group. The <g> element is used to group SVG shapes together. For example, <g transform="translate(50,50)">` will translate coordinate **["x": 50, "y": 50] as zero in the that group.

176-step5.js (Source)

var w = 450,
    h = 450,
    p = 50,
    x = d3.scale.linear().domain([0, 1]).range([0, w]),
    y = d3.scale.linear().domain([0, newCeilY]).range([h, 0]);

var chart = d3.select("#mainGraph")
    .append("svg:svg")
    .attr("width", w + p * 2)
    .attr("height", h + p * 2);

var vis = chart.append("svg:g")
    .attr("transform", "translate(" + p + "," + p + ")");

Step 6: Rulers

Now, we add both x and y axises line grids and labels. We also notice that d3 supports css style. We make #ccc colour stroke style for both x and y line grids.

176-step6.js (Source)

var xrule = vis.selectAll("g.x")
    .data(x.ticks(10))
    .enter().append("svg:g")
    .attr("class", "x");

xrule.append("svg:line")
    .style("stroke", "#ccc")
    .style("shape-rendering", "crispEdges")
    .attr("x1", x)
    .attr("x2", x)
    .attr("y1", 0)
    .attr("y2", h);

xrule.append("svg:text")
    .attr("x", x)
    .attr("y", h + 3)
    .attr("dy", ".71em")
    .attr("text-anchor", "middle")
    .text(x.tickFormat(10));

var yrule = vis.selectAll("g.y")
    .data(y.ticks(10))
    .enter().append("svg:g")
    .attr("class", "y");

yrule.append("svg:line")
    .attr("class", "yLine")
    .style("stroke", "#ccc")
    .style("shape-rendering", "crispEdges")
    .attr("x1", 0)
    .attr("x2", w)
    .attr("y1", y)
    .attr("y2", y);

yrule.append("svg:text")
    .attr("class", "yText")
    .attr("x", -3)
    .attr("y", y)
    .attr("dy", ".35em")
    .attr("text-anchor", "end")
    .text(y.tickFormat(10));

Step 7: Dots

This is the last step for the dots graph. All the dots are paths. What kind of symbols of the path is determined by d attribute. We also add title attribute in the path, so that when mouse is hovering the dots, tool tip is shown. Color of the dots fill is changed too. After you completed this steps, we will see a basic dot graph.

176-step7.js (Source)

var node = vis.selectAll("path.dot")
        .data(data)
        .enter().append("svg:path")
        .attr("class", "dot")
        .style("fill", "white")
        .style("stroke-width", "1.5px")
        .attr("stroke", "#9acd32")
        .attr("transform", function(d) { return "translate(" + x(d.x) + "," +
          y(d.y) + ")"; })
        .attr("d", d3.svg.symbol())
        .on("mouseover", function(d,i) {
            d3.select(this).transition().duration(300).style("fill","#00ffff");
        })
        .on("mouseout", function(d,i) {
            d3.select(this).transition().duration(300).style("fill","white");
        });

node.append("svg:title")
    .attr("class", "dotTitle")
    .text(function(d) {
      return "X: " + d.x.toFixed(3) + ", Y: " + d.y.toFixed(3);
    });

Step 8: Update button

This step is explaining what will happen after update button is clicked. Before that, we need to add onclick attribute in button type input with the update function name. For example, <input name="updateButton" type="button" value="Update" onclick="updateData()"/>. In the update function, we will get a new data list from randomData function. Then get the maximum ceil value, which is similar in initData function. Since, we are only want to rescale the y axis, we will select g.y. Make sure to select their parent, #mainGraph svg g, first before selecting them, in order to append the extra line grid in the right parent. For updating the line grids and labels, we are going to use update, enter and exit selections. Since we are append new line grid and label, we need to set all the attributes that are needed. On the other hand, we only update attributes that need to be changes in update and exit selections. Lastly, don't forget about updating the dots data too.

176-step8.js (Source)

function updateData() {
    var data = randomData();
    var newMaxY = d3.max(data, function(d) {return d.y;});
    var newCeilY = reDomain(newMaxY);

    var w = 450,
        h = 450,
        x = d3.scale.linear().domain([0, 1]).range([0, w]),
        y = d3.scale.linear().domain([0, newCeilY]).range([h, 0]);

    var vis = d3.select("#mainGraph svg g");

    var yrule = vis.selectAll("g.y")
        .data(y.ticks(10));

    // yRule Enter
    var newrule = yrule.enter().append("svg:g")
        .attr("class", "y");

    newrule.append("svg:line")
        .attr("class", "yLine")
        .style("stroke", "#ccc")
        .style("shape-rendering", "crispEdges")
        .attr("x1", 0)
        .attr("x2", w)
        .attr("y1", 0)
        .attr("y2", 0)
        .transition()
        .duration(2000)
        .attr("y1", y)
        .attr("y2", y);

    newrule.append("svg:text")
        .attr("class", "yText")
        .attr("x", -3)
        .attr("dy", ".35em")
        .attr("text-anchor", "end")
        .attr("y", 0)
        .transition()
        .duration(2000)
        .attr("y", y)
        .text(y.tickFormat(10));

    // yLine Update
    yrule.select("line.yLine")
        .transition()
        .duration(2000)
        .attr("y1", y)
        .attr("y2", y);

    // yText Update
    yrule.select("text.yText")
        .transition()
        .duration(2000)
        .attr("y", y)
        .text(y.tickFormat(10));

    // yrule Remove
    var oldrule = yrule.exit();

    oldrule.select("line.yLine")
        .transition()
        .duration(2000)
        .attr("y1", 0)
        .attr("y2", 0)
        .remove();


    oldrule.select("text.yText")
        .transition()
        .duration(2000)
        .attr("y", 0)
        .remove();

    oldrule.transition()
        .duration(2000).remove();

    // Dots
    var node = vis.selectAll("path.dot")
        .data(data)
        .transition()
        .duration(2000)
        .attr("transform", function(d) {
          return "translate(" + x(d.x) + "," + y(d.y) + ")";
        });

    node.select("title.dotTitle")
        .text(function(d) {
          return "X: " + d.x.toFixed(3) + ", Y: " + d.y.toFixed(3);
        });

}

Step 9: Summary

This tutorial is written based on my personal understanding on D3. I am still learning. Any feedback are welcome.

176-step9.html (Source)

<!DOCTYPE html>
<html>
<head>
    <title>Scale Graph</title>
</head>
<body>
    <div id="demoContainer">
        <div id="option">
            <input name="updateButton" type="button" value="Update"
              onclick="updateData()" />
        </div>
        <div id="mainGraph">
        </div>
    </div>
    <script type="text/javascript"
      src="http://mbostock.github.com/d3/d3.js?1.27.1"></script>
    <script type="text/javascript">
    /* Global variables */

    /* Definition code */
    function randomData() {
        return d3.range(10).map(function(i) {
            return {x: i / 9, y: Math.random()};
        });
    }

    function reDomain(maxValue) {
        var dy = Math.pow(10, Math.round(Math.log(maxValue) /
          Math.log(10)) - 1);
        return Math.ceil(maxValue / dy) * dy;
    }

    function updateData() {
        var data = randomData();
        var newMaxY = d3.max(data, function(d) {return d.y;});
        var newCeilY = reDomain(newMaxY);

        var w = 450,
            h = 450,
            x = d3.scale.linear().domain([0, 1]).range([0, w]),
            y = d3.scale.linear().domain([0, newCeilY]).range([h, 0]);

        var vis = d3.select("#mainGraph svg g");

        var yrule = vis.selectAll("g.y")
            .data(y.ticks(10));

        // yRule Enter
        var newrule = yrule.enter().append("svg:g")
            .attr("class", "y");

        newrule.append("svg:line")
            .attr("class", "yLine")
            .style("stroke", "#ccc")
            .style("shape-rendering", "crispEdges")
            .attr("x1", 0)
            .attr("x2", w)
            .attr("y1", 0)
            .attr("y2", 0)
            .transition()
            .duration(2000)
            .attr("y1", y)
            .attr("y2", y);

        newrule.append("svg:text")
            .attr("class", "yText")
            .attr("x", -3)
            .attr("dy", ".35em")
            .attr("text-anchor", "end")
            .attr("y", 0)
            .transition()
            .duration(2000)
            .attr("y", y)
            .text(y.tickFormat(10));

        // yLine Update
        yrule.select("line.yLine")
            .transition()
            .duration(2000)
            .attr("y1", y)
            .attr("y2", y);

        // yText Update
        yrule.select("text.yText")
            .transition()
            .duration(2000)
            .attr("y", y)
            .text(y.tickFormat(10));

        // yrule Remove
        var oldrule = yrule.exit();

        oldrule.select("line.yLine")
            .transition()
            .duration(2000)
            .attr("y1", 0)
            .attr("y2", 0)
            .remove();


        oldrule.select("text.yText")
            .transition()
            .duration(2000)
            .attr("y", 0)
            .remove();

        oldrule.transition()
            .duration(2000).remove();

        // Dots
        var node = vis.selectAll("path.dot")
            .data(data)
            .transition()
            .duration(2000)
            .attr("transform", function(d) {
              return "translate(" + x(d.x) + "," + y(d.y) + ")";
            });

        node.select("title.dotTitle")
            .text(function(d) {
              return "X: " + d.x.toFixed(3) + ", Y: " + d.y.toFixed(3);
            });

    }

    var initData = function() {
        var data = randomData();
        var newMaxY = d3.max(data, function(d) {return d.y;});
        var newCeilY = reDomain(newMaxY);

        var w = 450,
            h = 450,
            p = 50,
            x = d3.scale.linear().domain([0, 1]).range([0, w]),
            y = d3.scale.linear().domain([0, newCeilY]).range([h, 0]);

        var chart = d3.select("#mainGraph")
            .append("svg:svg")
            .attr("width", w + p * 2)
            .attr("height", h + p * 2);

        var vis = chart.append("svg:g")
            .attr("transform", "translate(" + p + "," + p + ")");

        var xrule = vis.selectAll("g.x")
            .data(x.ticks(10))
            .enter().append("svg:g")
            .attr("class", "x");

        xrule.append("svg:line")
            .style("stroke", "#ccc")
            .style("shape-rendering", "crispEdges")
            .attr("x1", x)
            .attr("x2", x)
            .attr("y1", 0)
            .attr("y2", h);

        xrule.append("svg:text")
            .attr("x", x)
            .attr("y", h + 3)
            .attr("dy", ".71em")
            .attr("text-anchor", "middle")
            .text(x.tickFormat(10));

        var yrule = vis.selectAll("g.y")
            .data(y.ticks(10))
            .enter().append("svg:g")
            .attr("class", "y");

        yrule.append("svg:line")
            .attr("class", "yLine")
            .style("stroke", "#ccc")
            .style("shape-rendering", "crispEdges")
            .attr("x1", 0)
            .attr("x2", w)
            .attr("y1", y)
            .attr("y2", y);

        yrule.append("svg:text")
            .attr("class", "yText")
            .attr("x", -3)
            .attr("y", y)
            .attr("dy", ".35em")
            .attr("text-anchor", "end")
            .text(y.tickFormat(10));

        var node = vis.selectAll("path.dot")
                .data(data)
                .enter().append("svg:path")
                .attr("class", "dot")
                .style("fill", "white")
                .style("stroke-width", "1.5px")
                .attr("stroke", "#9acd32")
                .attr("transform", function(d) {
                  return "translate(" + x(d.x) + "," + y(d.y) + ")";
                })
                .attr("d", d3.svg.symbol())
                .on("mouseover", function(d,i) {
                    d3.select(this).transition().duration(300).style(
                      "fill","#00ffff"); })
                .on("mouseout", function(d,i) {
                    d3.select(this).transition().duration(300).style(
                      "fill","white"); });

        node.append("svg:title")
            .attr("class", "dotTitle")
            .text(function(d) {
              return "X: " + d.x.toFixed(3) + ", Y: " + d.y.toFixed(3);
            });
    }

    /* UI Events */
    /* Execution code */
    initData();
    </script>
</body>
</html>

Updates: 15 Jul 2011

Ricardo mentions transitioning scales example, Date Ticks by mbostock.

Comments

Comments powered by Disqus