Making Dynamic Scales and Axes
FREE     Duration: 16:45
Part of Course: Intermediate D3 Course
 

Takeaways:

  • You can use an HTML form element to change the visual representation of data - in this example, you can have the data represented as seconds or milliseconds
  • Because the D3 Scale (linear in this example) is both an object and a function, you can redefine the domain and range as many times as you would like
  • When the D3 Scale is used, it will use the last defined range and domain
  • To redraw the D3 SVG Axis, you not only need to redefine the D3 Scale that was used for that particular Axis, you also have to call it again on the selection that represents the old D3 SVG Axis
  • This redrawing / updating of the scale + axis works for all of the different D3 Scales (linear and ordinal in this example)

Transcript:

Making Dynamic Scales and Axes

The Goal


Let's take a close look at what we are going to be building in this video.

BROWSER: Chart is going (see source code)

This web page has the D3 library imported from the d3js.org website.

We have opened the Chrome Developer Tools and are in the Console Section.

We take the chart we made in the previous video and focus on the seconds.

Here we see a bar chart that shows what seconds my system currently has for the time.

If the seconds is even then it colors the bar chart blue.

If the second is odd then it colors the bar chart green.

Above the bar is the actual second that is being displayed by the chart.

The Y axis is demarcated by the number of seconds.

The X axis has an ordinal scale that defines the bar as seconds.


What this video will cover is what happens when we switch from seconds to milliseconds.

BROWSER: Every few seconds click on the seconds or milliseconds button

One second is one thousand milliseconds so we will have to change a few things on the fly:

First - we will have to change the scaling function of the Y axis

Second - we will have to change the actual Y axis as it is drawn on the screen

Third - we will have to change the scaling function of the X axis

Fourth - we will have to change the actual X axis as it is drawn on the screen.

Fifth - we will have to redraw the bar with the new scaling functions

Sixth - we will have to make sure the text label above the chart now reflects the data in the units that we are using.


By changing those things on the fly what we are doing is making our scales and axis dynamic using Dynamic Data and the Update Data Function.

Additionally, we will use part of the D3 General Update Pattern in our Update Data Function called redraw.



Making Dynamic Scales


This is a normal D3 linear scale.

var yAxisScale = d3.scale.linear()
    .domain([0, 59])
    .range([height, 0])
    .nice();

We define the domain - which in this case goes from 0 seconds to 59 seconds.

We define the range as going from height to 0.

This is done for the inversion of the Y Axis.

Finally, we use the D3 scale nice method to extend the domain so that it starts and ends on nice round values.

This is particularly useful for making the Y Axis as drawn on the screen look pretty.


The linear scale is a linear relationship between the domain and the range.

var tempScale = d3.scale.linear()
    .domain([0,  10])
    .range( [0, 100]);
  
tempScale(5);
// returns 50

It can be expressed as a y = mx + b formula.

In this case, the tempScale will take in the number 5 and return the number 50.


When we think of a d3 linear scale, we are actually talking about an object and a function.

// d3.scale.linear is both an object and a function.
d3.scale.linear
    .domain(...)
    .range(...)

The reason why it's important to think about it like an object, is that we can use the setters and getters of the object.

The setters and getters we are using here are the domain and range.

Which means that we can set and then later get the domain of the object.

As well as set and then later get the range of the object.


This is important because it means that we can easily redefine the domain of the scale object.

var tempScale = d3.scale.linear()
    .domain([0,  10])
    .range( [0, 100]);

    tempScale.domain([0, 100]);
  
    tempScale(5);
    // returns 5

We just call the method again with the new domain we want.

In this case, the tempScale will take in the number 5 and return the number 5.


It also means that we can easily redefine the range of the scale object.

var tempScale = d3.scale.linear()
    .domain([0,  10])
    .range( [0, 100]);

    tempScale.range([0,  10]);
  
    tempScale(5);
    // returns 5

We just call the method again with the new range we want.

In this case, the tempScale will take in the number 5 and return the number 5.


We can define and redefine the range or domain as many times as we want.

var tempScale = d3.scale.linear()
    .domain([0,  10])
    .range( [0, 100]);

tempScale.range([0,  10]);

tempScale.range([0, 100]);

tempScale.range([0,  10]);
  
tempScale.range([0, 100]);

tempScale(5);
// returns 5

When we use the scale as a function, it will use the last defined range and domain.


This is useful because in our Update Data Function, we can redefine the domain and/or range every time we run the function.

function redraw(type) { 
    if      (type === "short") { tempScale.range([0,  10]) }
    else if (type === "long") { tempScale.range([0, 100]) };
}

This is how we will be able to make dynamic linear scales.

This simple example shows that we can just pass a string to the redraw function and it will automatically redefine the tempScale for us.


We can do the same thing with an ordinal scale.

var xAxisScale = d3.scale.ordinal()
    .domain(["seconds"])
    .rangeRoundBands([0, width], .1);
  
xAxisScale.domain(["milliseconds"]);

An ordinal scale also behaves like a function and an object.

So we can go in and redefine the domain and / or range of the ordinal scale as many times as we want and when we want to.

This is how we can make dynamic ordinal scales as well.

We can program a redraw function that redefines the scale domain or range according to some given rules.



Making Dynamic Axes


In order to make an axis, you have to do two things.

var myXAxis = d3.svg.axis().scale(xAxisScale).orient("bottom");
  
var axisXGroup = innerSpace.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(myXAxis);

One - you have to define it and provide a scale and orientation.

Two - you have to call the axis function you defined on a selection to apply it.


When we change the scale of a chart or graph we are using, then we have to redefine the axis that used the old scale as well as redraw the axis that was using the old axis.

// 1) change the scale
// 2) redefine the axis
// 3) redraw the axis

So it turns into a three step process when we change the scale.

In the bar chart example we are going to make, this process will need to happen to both the X Axis as well as the Y Axis.


Just like the we can get and set the domain and range in the scale, with the d3.svg.axis, we can get and set the scale we are going to use.

var myXAxis = d3.svg.axis().scale(xAxisScale).orient("bottom");
  
// redefine xAxisScale //
  
myXAxis.scale(xAxisScale);

So to redefine the scale, we just have to pass in the new scale.

Once the new scale is passed in, the axis will be using the new scaling function.


Once we have redefined the new axis, we have to call it to apply it to the selection that contains the axis components.

d3.select(".x")
    .call(myXAxis);

In this case, we are selecting the "x" axis and all it's child components.

Then we call the new axis that has the new scale and apply it to the selection.

This behaves in the exact same way as if we were just doing it for the first time.


To follow object constancy within the scale and to make it clear we are switching scales, we add a transition so that the visual axis will transition from the old axis to the new one.

d3.select(".x")
    .transition()
    .call(myXAxis);


Note that we can do this with both the ordinal scales and linear scales.

d3.scale.ordinal() => d3.svg.axis().scale(...)
// => ......call(xAxis);

d3.scale.linear()  => d3.svg.axis().scale(...)
// => ......call(yAxis);

In the example we are covering we will do both.

With the ordinal scale representing the X axis and the linear scale representing the Y axis.


So in order to make the scales, axis and visual axis dynamic based on the data that is coming in, we have to follow the procedure on the screen.

function redraw(...) {
  
    // 1) change the X scale
    // 2) redefine the X axis
    // 3) redraw the X axis

    // 4) change the Y scale
    // 5) redefine the Y axis
    // 6) redraw the Y axis

    // 7) redraw rectangle based on new x scale and new y scale

    // 8) redraw text based on new x scale and new y scale

}

For each new scale we are going to use we have to go through a series of steps for each axis.

Then, lastly, we have to use the new scale to draw the SVG rectangle and SVG Text on the screen.



Redraw Function Walk Through


Because we just covered how to create the bar chart in the last video (Dynamic Data and Update Data Function), this video will cover the new things that are important to just this video.

var intervalName = "seconds";

The first thing that is important is that we will use a variable to remember what type of units we are using.

That way, the redraw function can be called over and over again with this variable and it will change it's behavior based on what value the intervalName variable has at the time of calling.


Next, we define the data generation function.

function updateData(usingSeconds) {

    var currentTime = new Date();
  
    return [{ "time_unit": (usingSeconds) ? "second" : "millisecond",
              "time_data": currentTime.getSeconds() * ((usingSeconds) ? 1 : 1000) }];

}

This function is passed a boolean that is true if the intervalName we are using is seconds or false if the intervalName is not seconds.

Based on this, the data generation function will return an array of 1 JSON object.

The object has two keys - a) the time unit and b) the time data.

The time unit value is either seconds or milliseconds depending on the usingSeconds boolean.

The time data value is either seconds or milliseconds.

Remember, there are 1000 milliseconds in a second, so we just do a multiplication based on the usingSeconds boolean.


We generate the chart once using the D3 pattern and using seconds as our base case of units.

// generate the chart


Given what we have covered, we can break down the redraw function into 10 separate steps.

function redraw(intervalName) {
  
    // 1) calculate usingSeconds boolean
    // 2) generate data

    // 3) redefine the Y scale
    // 4) redefine the Y axis
    // 5) redraw the Y axis

    // 6) redefine the X scale
    // 7) redefine the X axis
    // 8) redraw the X axis

    // 9) redraw the rectangle
    // 10) redraw the text label

}

They can also be grouped into 4 different groups.

Group 1 does the generation of the data.

Group 2 does the redefining of the Y scale and redrawing of the Y axis

Group 3 does the redefining of the X scale and redrawing of the X axis

Group 4 does the redrawing of the visual data elements.


This is the Group 1 set of commands - the ones that generate the data.

var usingSeconds = intervalName === "seconds";

var data = updateData(usingSeconds);

First, we take in the interval name and if it is equal to the string "seconds" then we assign the true boolean to the usingSeconds variable.

Otherwise we assign the false boolean to the usingSeconds variable.

Then we pass in the usingSeconds variable to the data generation function called updateData.

This will pass back an array containing 1 JSON object with the data we will use to run the partial D3 General Update Pattern.


This is the Group 2 set of commands - the ones that redefine the Y scale and redraw the Y axis.

var yAxisScale = d3.scale.linear()
    .domain([0, 59 * ((usingSeconds) ? 1 : 1000)])
    .range([height, 0])
    .nice();
        
myYAxis.scale(yAxisScale);
  
d3.select(".y")
    .transition()
    .duration(1000)
    .call(myYAxis);

First, the new yAxisScale linear scale is redefined according to whether we are using seconds or milliseconds.

We focus in on the domain as the height of the Inner Drawing Space is not changing.

Then we update the d3 svg axis function with the new updated scale.

Lastly, we select the y axis svg group element and apply the updated d3 svg axis function with the new updated scale.

We include a transition so that it is clear we are transitioning to a new scale and to make it pretty.


This is the Group 3 set of commands - the ones that redefine the X scale and redraw the X axis.

var xAxisScale = d3.scale.ordinal()
    .domain([(usingSeconds) ? "seconds" : "milliseconds"])
    .rangeRoundBands([0, width], .1);

myXAxis.scale(xAxisScale);
  
d3.select(".x")
    .transition()
    .duration(1000)
    .call(myXAxis);

First, the new xAxisScale ordinal scale is redefined according to whether we are using seconds or milliseconds.

We focus in on the domain as the width of the Inner Drawing Space is not changing.

Then we update the d3 svg axis function with the new updated scale.

Lastly, we select the x axis svg group element and apply the updated d3 svg axis function with the new updated scale.

We include a transition so that it is clear we are transitioning to a new scale and to make it pretty.


This code redraws the bar after the new scales have been defined and updated.

innerSpace.selectAll(".bar")
    .data(data)
        .transition()
            .duration(1000)
            .attr("y", function(d, i) { return yAxisScale(d.time_data); })
            .attr("height", function(d, i) { return height - yAxisScale(d.time_data); })
            .style("fill", function(d, i) {
                if(d.time_data % 2 === 0) { return "steelblue";}
                else { return "green" }
            });

We do a partial D3 general update pattern where we only care about the update selection as we are not removing or adding any new elements.

We use the data object returned by the data generation function to construct the SVG rectangle attributes.

Then, like in the last video, we use the time data to style the SVG Rectangle.

Notice that because we have already updated and redefined the scales, we don't have to do any math related to whether we are using seconds or milliseconds here.

The functionality works the same way for both cases.


Lastly, we do the update of the text label.

innerSpace.select("#text_label")
    .data(data)
        .transition()
            .duration(1000)
            .attr("y", function(d, i) { return yAxisScale(d.time_data) - 15; })
            .text(function(d, i) { return d.time_data; });

We do a partial D3 general update pattern where we only care about the update selection as we are not removing or adding any new elements.

Again, like the SVG rectangle, we don't have to do any math related to whether we are using seconds or milliseconds because the yAxisScale function will take care of it for us.


Then we add event listeners to each button to listen for the click event.

secondsButton.on("click", function(d, i) { return intervalName = "seconds"; });

millisecondsButton.on("click", function(d, i) { return intervalName = "milliseconds"; });

If one of the buttons is clicked, it redefines the intervalName variable to either seconds or milliseconds.

This makes it so that when the setInterval function calls the redraw function next, it will use the interval that we last clicked.


Lastly, to continuously run the animation, we need to use the setInterval JavaScript command to have the redraw function execute once every second until we stop it.

function redraw(intervalName) {
    // 1) calculate usingSeconds boolean
    // 2) generate data
    // 3) redefine the Y scale
    // 4) redefine the Y axis
    // 5) redraw the Y axis
    // 6) redefine the X scale
    // 7) redefine the X axis
    // 8) redraw the X axis
    // 9) redraw the rectangle
    // 10) redraw the text label
}

// ** HIGHLIGHT **
var originalSetInterval = setInterval(function() { redraw(intervalName); }, 1000);


Alright, let's walk through building the code up in the JavaScript console and then set the animation moving.



JavaScript Console Walk Through


This web page has the D3 library imported from the d3js.org website.


We have opened the Chrome Developer Tools and are in the Console Section.


We start by creating the Seconds button

var secondsButton = d3.select("body")
  .append("input")
    .attr("type", "button")
    .attr("value", "Seconds")
    .attr("id", "button_seconds")
    .style("font-size", "2em");

This is just a normal usage of D3 to add in an HTML element.

Note that we don't add in any functionality, later, with d3, we will add an event listener to it.


Then we create the "Milliseconds" button

var millisecondsButton = d3.select("body")
  .append("input")
    .attr("type", "button")
    .attr("value", "Milliseconds")
    .attr("id", "button_milliseconds")
    .style("font-size", "2em");

Note on this button as well, that no functionality was added in because we will add an event listener to it later.


We add in an HTML break to separate the buttons from the SVG container.

d3.select("body").append("br");


We define the interval variable as seconds to start off with.

var intervalName = "seconds";


Then we define the data generation function.

function updateData(usingSeconds) {

    var currentTime = new Date();
  
    return [{ "time_unit": (usingSeconds) ? "second" : "millisecond",
              "time_data": currentTime.getSeconds() * ((usingSeconds) ? 1 : 1000) }];

}


Then we define the SVG Viewport, D3 Margin Convention and the Inner Drawing Space

var svgViewport = d3.select("body")
  .append("svg")
    .attr("width",300)
    .attr("height",300);

var margin = {top: 50, right: 50, bottom: 50, left: 75},
    width  = 300 - margin.left - margin.right,           
    height = 300 - margin.top  - margin.bottom;

var innerSpace = svgViewport.append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

Note, the three commands were run at the same times as we have covered them enough times that we don't have to go through them one by one.


Next, we do the initial definitions of the x and y scaling functions, the x and y axis functions and the drawing of the x and y axes.

var xAxisScale = d3.scale.ordinal()
    .domain(["seconds"])
    .rangeRoundBands([0, width], .1);

var yAxisScale = d3.scale.linear()
    .domain([0, 59])
    .range([height, 0])
    .nice();

var myXAxis = d3.svg.axis().scale(xAxisScale).orient("bottom");

var myYAxis = d3.svg.axis().scale(yAxisScale).orient("left");

var axisXGroup = innerSpace.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(myXAxis);

var axisYGroup = innerSpace.append("g")
    .attr("class", "y axis")
    .call(myYAxis);

Again - we run the 6 commands at the same time as we have covered them many times before.

Now we have our axes ready for the bar chart and the text label.


We generate the data using the function updateData we defined before.

var data = updateData(true);

data;

BROWSER - click into data

Because we are staring out with seconds on the first go around, we pass the boolean true to the function.

The function uses this to set the internal variable, usingSeconds, so it will generate the correct data for the seconds.


Next, we do the D3 pattern using the data we just generated to create the initial bar for the bar chart.

var rectangles = innerSpace.selectAll(".bar")
    .data(data)
  .enter().append("rect")
    .attr("class", "bar")
    .attr("x", function(d, i) { return xAxisScale(d.time_unit); })
    .attr("y", function(d, i) { return yAxisScale(d.time_data); })
    .attr("width", xAxisScale.rangeBand())
    .attr("height", function(d, i) { return height - yAxisScale(d.time_data); })
    .style("fill", "steelblue");

Note that we are still using the original scaling functions.


Finally, we do the D3 pattern using the data we generated to create the initial text label above the bar chart.

var text = innerSpace.append("g").attr("class", "bar_label").selectAll("text")
    .data(data)
  .enter().append("text")
    .attr("id", "text_label")
    .attr("x", function(d, i) { 
        return xAxisScale(d.time_unit) + (xAxisScale.rangeBand() / 2); })
    .attr("y", function(d, i) { return yAxisScale(d.time_data) - 15; })
    .attr("font-family", "sans-serif")
    .attr("font-size", "22px")
    .attr("fill", "red")
    .attr("text-anchor", "middle")
    .text(function(d, i) { return d.time_data; });

Perfect - the bar chart and text label are all created.


Let's now manually run through the redraw function once together before we define it as a function onto itself.


First let's change the intervalName to milliseconds.

var intervalName = "milliseconds";


Next, let's define the usingSeconds variable.

var usingSeconds = intervalName === "seconds";

usingSeconds;

You can see that the usingSeconds variable is now the false boolean.


Next, let's generate new data using the intervalName variable.

var data = updateData(usingSeconds);

data;

BROWSER - click into the data.

You can see that the time unit is now milliseconds and that the number in the 10's of thousands.


Next is where we start redefining scales, axes functions and visual axes.


First, we redefine the Y scale with the new updated data.

var yAxisScale = d3.scale.linear()
    .domain([0, 59 * ((usingSeconds) ? 1 : 1000)])
    .range([height, 0])
    .nice();

yAxisScale.domain();

You can see that because we are doing milliseconds, that the domain of the scale now goes from 0 to 60 thousand.


Next, we redefine the Y axis.

myYAxis.scale(yAxisScale);

Instantly see the code of the function.

Don't worry about understanding this, it's not useful for what we are doing.


Next, we redraw the Y Axis with a transition of 3 seconds.

d3.select(".y")
    .transition()
    .duration(3000)
    .call(myYAxis);

You will have seen the numbers from the old axis move down as the new ones came in from the top.

We can now see the Y axis going from 0 to 60 thousand.


Next, we redefine the X scale.

var xAxisScale = d3.scale.ordinal()
    .domain([(usingSeconds) ? "seconds" : "milliseconds"])
    .rangeRoundBands([0, width], .1);

xAxisScale.domain();

You can see that because we are doing milliseconds, that the domain of the scale is now the string "milliseconds".


Next, we redefine the X Axis with the new updated scale.

myXAxis.scale(xAxisScale);

Again - you can ignore the result of the function.


Then, we redraw the X Axis with a transition of 3 seconds.

d3.select(".x")
    .transition()
    .duration(3000)
    .call(myXAxis);

We can see that the axis label is milliseconds instead of seconds.


Next, we redraw the rectangle using the D3 general update pattern, though in this case we only care about the update selection.

innerSpace.selectAll(".bar")
    .data(data)
    .transition()
        .duration(1000)
        .attr("y", function(d, i) { return yAxisScale(d.time_data); })
        .attr("height", function(d, i) { return height - yAxisScale(d.time_data); })
        .style("fill", function(d, i) {
            if(d.time_data % 2 === 0) { return "steelblue";}
            else { return "green" }
        });

Because we are using updated scales and updated data, we don't have to specify anything about the units or whether we are using seconds or milliseconds.


Lastly, we update the text label also using the D3 general update pattern.

innerSpace.select("#text_label")
    .data(data)
    .transition()
        .duration(1000)
        .attr("y", function(d, i) { return yAxisScale(d.time_data) - 15; })
        .text(function(d, i) { return d.time_data; });

Fantastic - we now have the text label match the bar chart and the axes.


Let's now define the redraw function so that it does everything we just did manually.

function redraw(intervalName) {

    var usingSeconds = intervalName === "seconds";
    var data = updateData(usingSeconds);
  
    var yAxisScale = d3.scale.linear()
        .domain([0, 59 * ((usingSeconds) ? 1 : 1000)])
        .range([height, 0])
        .nice();

    myYAxis.scale(yAxisScale);

    d3.select(".y").transition().duration(1000).call(myYAxis);

    var xAxisScale = d3.scale.ordinal()
        .domain([(usingSeconds) ? "seconds" : "milliseconds"])
        .rangeRoundBands([0, width], .1);

    myXAxis.scale(xAxisScale);
  
    d3.select(".x").transition().duration(1000).call(myXAxis);

    innerSpace.selectAll(".bar")
        .data(data)
        .transition()
            .duration(1000)
            .attr("y", function(d, i) { return yAxisScale(d.time_data); })
            .attr("height", function(d, i) { return height - yAxisScale(d.time_data); })
            .style("fill", function(d, i) {
                if(d.time_data % 2 === 0) { return "steelblue";}
                else { return "green" }
            });
  
    innerSpace.select("#text_label")
        .data(data)
        .transition()
            .duration(1000)
            .attr("y", function(d, i) { return yAxisScale(d.time_data) - 15; })
            .text(function(d, i) { return d.time_data; });
}

redraw("seconds");

We define the function and then run it with the string of "seconds".

You can see that it now automatically changes all the things for us.


Next, we define the event listeners for both buttons.

secondsButton.on("click", function(d, i) { return intervalName = "seconds"; });

millisecondsButton.on("click", function(d, i) { return intervalName = "milliseconds"; });


Let's press the Millisecond button and then check to see what the intervalName is.

BROWSER press button

intervalName;

The intervalName is "milliseconds", so it worked correctly.


Lastly, let's use the setInterval function to call the redraw function once a second.

var originalSetInterval = setInterval(function() { redraw(intervalName); }, 1000);

You can now see the bar chart updating once a second.


Now, let's test pressing the two different HTML buttons.

BROWSER alternate pressing the buttons.


And with that we have covered the basics of thinking about and Making Dynamic Scales and Axes through the use of the redraw function.


The Update Data Function redefines the scale, the axis function and then redraws the visual axis in a way that makes it follow the data we are generating.


This allows for greater control of creating dynamic data visualizations.

<< Back To D3 Screencast and Written Tutorials Index