2

I am trying to understand out how this Javascript code from a D3 example works. The part that doesn't make sense to me are the xScale and yScale "functions" within the buildLine() function.

Taking xScale, it looks like a simple variable that is instantiated to a value:

    var xScale = d3.scale.linear()
                .domain([
                            d3.min(ds.monthlySales, function(d){ return d.month;}) ,
                            d3.max(ds.monthlySales, function(d){ return d.month;})
                        ])                
                .range([0, w])
                .nice(); 

...but then further down within buildLine(), it appears that they are also being called as functions:

    var lineFun = d3.svg.line()
        .x(function (d) {return xScale(d.month); } )
        .y(function (d) {return yScale(d.sales); })
        .interpolate("linear");

More specifically, in:

.x(function (d) {return xScale(d.month); } )

...I'm not understanding where the value of d comes from (and how it has a .month attribute), and most importantly how that is being being received by the xScale "function".

I think there is a fundamental principle of Javascript I need to understand for this code to make sense, but what might that be?


The underlying data can be seen here:

https://github.com/bsullins/d3js-resources/blob/master/monthlySalesbyCategoryMultiple.json

The entire source code:

<!DOCTYPE html>
<html>
    <head>
        <script src="d3.min.js" charset="utf-8"></script>
    </head>
    <body>
    <script>
        var h=100;
        var w=400;
        var padding = 20;             


        //build line
        function buildLine(ds) {
            console.log('xscale-max: '+ d3.max(ds.monthlySales, function (d){ return d.month; }));
            console.log('yscale-max: '+ d3.max(ds.monthlySales, function (d){ return d.sales; }));
            //scales
            var xScale = d3.scale.linear()
                        .domain([
                                    d3.min(ds.monthlySales, function(d){ return d.month;}) ,
                                    d3.max(ds.monthlySales, function(d){ return d.month;})
                                ])                
                        .range([0, w])
                        .nice(); 

            var yScale = d3.scale.linear()
                        .domain([0, d3.max(ds.monthlySales, function(d){ return d.sales;})])
                        .range([h,0])
                        .nice();

            var lineFun = d3.svg.line()
                .x(function (d) {return xScale(d.month); } )
                .y(function (d) {return yScale(d.sales); })
                .interpolate("linear");

            var svg = d3.select("body").append("svg").attr({ width:w, height:h});

            var viz = svg.append("path")
                        .attr({
                            d: lineFun(ds.monthlySales),
                            "stroke" : "purple",
                            "stroke-width": 2,
                            "fill" : "none"
                        });

        }

        //show header
        function showHeader(ds) {
            d3.select("body").append("h1")
                .text(ds.category + " Sales (2013)");
        }


        //get data and draw things  
        d3.json("https://api.github.com/repos/bsullins/d3js-resources/contents/monthlySalesbyCategoryMultiple.json", function(error, data) {

           if(error) {
               console.log(error);
           } else {
               console.log(data); //we're golden!
           }

            var decodedData = JSON.parse(window.atob(data.content));

            console.log(decodedData.contents);


            decodedData.contents.forEach(function(content){
                ds=content;
                console.log(ds);
                showHeader(ds);         
                buildLine(ds);                   
            })

        });  


    </script>
    </body>
</html>
tbone
  • 5,715
  • 20
  • 87
  • 134
  • 1
    That's what's called a `callback function`. You are passing a function AS a parameter to another method. When the method is finished, it *calls back* to the function that you passed in. It sends `d` along with it. (Which in this case, appears to be an object.) You can pass functions anonymously (and inline, as you are doing here), or you can name them elsewhere and pass them in, if that is easier to visualize. – Antiga Dec 04 '15 at 18:43
  • d is the item in each row. Basically used with the collection associated. – Vince Dec 04 '15 at 18:44

1 Answers1

3

Well javascript has first class functions so xScale is a variable that contains a function. Which means you can pass it around and call it.

More specifically, the first code snippet you posted builds a function that accepts a value in the interval specified by .domain([d3.min ..., d3.max ... ]) and returns a value in the interval specified by .range([0, w]). It also says that the value the function returns should be "nice" (.nice()) i.e. truncated to not have several decimals. The function built according to these specs is assigned to the xScale variable.

Later, you use this function in the d3.svg.line generator which is, like d3.scale.linear a function/object that generates (hence, generator) a function, in this case a function that, when provided with a dataset as input, returns a string you can use as the d attribute of the svg path element to draw the path you want. This function is assigned to the variable lineFun (the Fun part!).

The lineFun function, built using the d3.svg.line generator, takes as input a dataset:

lineFun(ds.monthlySales)

and, for each of the datapoints in the dataset, it passes it as input to the accessor functions (that's where the d parameter comes from) so that they can create a x and y value from and for the datapoint. The accessor functions are therefore functions that accept a datapoint and return a value, generally according to some property of the datapoint. So, for example:

function (d) {return xScale(d.month);

Which, given a datapoint d, returns the value the xScale function returns when passing the month attribute of the datapoint as input.

I know I didn't write the most clear explanation, but I hope it can help you a little.

The key points are:

  • in Javascript functions can be assigned to variables and passed around as parameters, just like integers or strings
  • d3.scale.linear and d3.svg.line are generator: they build and return a function
  • d3 uses the shapes generators to return, when given the data as input, the svg attributes that are used to draw paths to represent your data.

From https://github.com/mbostock/d3/wiki/SVG-Shapes#path-data-generators

A path generator, such as that returned by d3.svg.line, is both an object and a function. That is: you can call the generator like any other function, and the generator has additional methods that change its behavior. Like other classes in D3, path generators follow the method chaining pattern where setter methods return the generator itself, allowing multiple setters to be invoked in a concise statement.

LeartS
  • 2,866
  • 2
  • 25
  • 45
  • 1
    Thanks for this, the light bulb in my head hasn't gone off yet but I think you've named some of the principles I need to understand before I can understand this code. – tbone Dec 04 '15 at 19:27
  • xScale is essentially a pointer to the D3.Scale() function, so *that* is what is receiving the d.Month argument....correct? – tbone Dec 07 '15 at 04:47
  • A blog that walks through usage of scales: http://alignedleft.com/tutorials/d3/scales ....the origin of "d" is still a bit of a mystery to me. – tbone Dec 07 '15 at 04:59
  • SO question with lots of educational links: http://stackoverflow.com/questions/16930748/a-good-book-for-learning-d3-js – tbone Dec 07 '15 at 05:08
  • D3 wiki root: https://github.com/mbostock/d3/wiki More: http://alignedleft.com/tutorials/d3/ Excellent free online book: http://chimera.labs.oreilly.com/books/1230000000345/index.html – tbone Dec 07 '15 at 05:09