1

So, I've been using R & ggplot2 to make good progress and share the results of our data analysis. However, we want something more interactive. We've used Highcharts.js, but it breaks under the weight of many data points and isn't very flexible in customization. So, I've played a bit with protovis in the past, but am now looking to make D3 work.

My two key problems are as follows:

  • How to reuse a line definition by passing in a pointer to part of the dataset
  • Tooltips (made some progress and I'll follow up on another post unless someone sees an obvious answer here)

In the code below, I've highlighed the line declaration and then where those functions are called for adding to the svg & styling.

What I'm looking for is help figuring out how to pass in a parameter to the line function to specify which data element to add to a given line. It is possible that I need to format the data differently, and that is perfectly possible if that's the best solution.

Any input is appreciated!

----------------UPDATE----------------

Help needed here:

        // MPGL
    var line1 = d3.svg.line()
        .x(function(d,i) {return x(startTime + (timeStep*i));})
        .y(function(d) {return y(d.MPGL+d.MPGI+d.MTTFB);})

Instead of passing

 .y(function(d) {return y(d.MPGL+d.MPGI+d.MTTFB);}) 

I would like to have a more generic declaration like:

.y(function(d) {return y(Z);})

and then call that generic line statement in something like this:

 graph.append("svg:path").attr("d", line1(<something_corresponding_to_Z_here>)).attr("class", "MPGL");

So, is this possible, or am I just dreaming? Do I need to carve up my data differently?

----------------/UPDATE----------------

Full Code here:

<html>
<head>
    <title>D3.js Test</title>
    <script src="http://d3js.org/d3.v3.min.js"></script>
    <style>
        body { font-family: "Helvetica Neue", Helvetica;}

        /* tell the SVG path to be a thin line without any area fill */
        path { stroke-width: 1; fill: none;}

        /* Define Line color based on category class */
        .MPGL {stroke: #644a9b;}
        .MPGI {stroke: #009966;}
        .MTTFB {stroke: #0066FF;}

        /* Axis Formatting/styling */
        .axis {
          shape-rendering: crispEdges;
        }

        .x.axis line {
          stroke: lightgrey;
        }

        .x.axis .minor {
          stroke-opacity: .5;
        }

        .x.axis path {
          display: none;
        }

        .x.axis text {
          font-size: 10px;
        }

        .y.axis line, .y.axis path {
          fill: none;
          stroke: lightgrey;
        }

        .y.axis text {
            font-size: 12px;
        }

    </style>
</head>
    <body>


    <div id="graph" class="aGraph" style="position:absolute;top:0px;left:0; float:left;"></div>


    <script>
        // define dimensions of graph
        var m = [20, 20, 20, 40]; // margins
        var w = 550;    // width
        var h = 300; // height

    data = [ { "WK" : 14, "EOBSHR" : 1364839200000, "VOL" : 71383, "MPGL" : 2.901, "MPGI" : 1.203, "MTTFB" : 1.221, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 }, 
             { "WK" : 14, "EOBSHR" : 1364842800000, "VOL" : 70447, "MPGL" : 2.804, "MPGI" : 1.182, "MTTFB" : 1.211, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 }, 
             { "WK" : 14, "EOBSHR" : 1364846400000, "VOL" : 64878, "MPGL" : 2.781, "MPGI" : 1.169, "MTTFB" : 1.172, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 }, 
             { "WK" : 14, "EOBSHR" : 1364850000000, "VOL" : 56668, "MPGL" : 2.734, "MPGI" : 1.18, "MTTFB" : 1.153, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 }, 
             { "WK" : 14, "EOBSHR" : 1364853600000, "VOL" : 48721, "MPGL" : 2.722, "MPGI" : 1.134, "MTTFB" : 1.137, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 },
             { "WK" : 14, "EOBSHR" : 1364857200000, "VOL" : 45605, "MPGL" : 2.862, "MPGI" : 1.155, "MTTFB" : 1.116, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 },
             { "WK" : 14, "EOBSHR" : 1364860800000, "VOL" : 51002, "MPGL" : 3.219, "MPGI" : 1.136, "MTTFB" : 1.124, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 },
             { "WK" : 14, "EOBSHR" : 1364864400000, "VOL" : 62180, "MPGL" : 3.7, "MPGI" : 1.13, "MTTFB" : 1.143, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 }, 
             { "WK" : 14, "EOBSHR" : 1364868000000, "VOL" : 67299, "MPGL" : 3.965, "MPGI" : 1.198, "MTTFB" : 1.221, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 }, 
             { "WK" : 14, "EOBSHR" : 1364871600000, "VOL" : 58893, "MPGL" : 3.953, "MPGI" : 1.275, "MTTFB" : 1.24, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 }, 
             { "WK" : 14, "EOBSHR" : 1364875200000, "VOL" : 52752, "MPGL" : 4.082, "MPGI" : 1.373, "MTTFB" : 1.295, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 },
             { "WK" : 14, "EOBSHR" : 1364878800000, "VOL" : 55881, "MPGL" : 4.401, "MPGI" : 1.393, "MTTFB" : 1.39, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 }, 
             { "WK" : 14, "EOBSHR" : 1364882400000, "VOL" : 65844, "MPGL" : 4.608, "MPGI" : 1.394, "MTTFB" : 1.37, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 }, 
             { "WK" : 14, "EOBSHR" : 1364886000000, "VOL" : 78441, "MPGL" : 4.484, "MPGI" : 1.366, "MTTFB" : 1.399, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 },
             { "WK" : 14, "EOBSHR" : 1364889600000, "VOL" : 91130, "MPGL" : 4.047, "MPGI" : 1.321, "MTTFB" : 1.57, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 }, 
             { "WK" : 14, "EOBSHR" : 1364893200000, "VOL" : 89911, "MPGL" : 3.674, "MPGI" : 1.248, "MTTFB" : 1.314, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 },
             { "WK" : 14, "EOBSHR" : 1364896800000, "VOL" : 81673, "MPGL" : 3.548, "MPGI" : 1.298, "MTTFB" : 1.301, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 },
             { "WK" : 14, "EOBSHR" : 1364900400000, "VOL" : 85718, "MPGL" : 3.515, "MPGI" : 1.245, "MTTFB" : 1.289, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 },
             { "WK" : 14, "EOBSHR" : 1364904000000, "VOL" : 95746, "MPGL" : 3.541, "MPGI" : 1.236, "MTTFB" : 1.306, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 },
             { "WK" : 14, "EOBSHR" : 1364907600000, "VOL" : 105282, "MPGL" : 3.296, "MPGI" : 1.201, "MTTFB" : 1.273, "mPGL" : 3.549, "mPGI" : 1.274, "mTTFB" : 1.248 }]

// Find timeframe of dataset to create variables for display
    var startTime = d3.min(data, function(d) { return d.EOBSHR; });
    var endTime = d3.max(data, function(d) { return d.EOBSHR; })
    var timeStep = 3600000;

// Setup scales to adjust display size based on content 
    var x = d3.time.scale().domain([startTime, endTime]).range([0, w]);
    x.tickFormat(d3.time.format("%Y-%m-%d"));
    var y = d3.scale.linear().domain([0, d3.max(data, function(d) { return d.MPGL+d.MPGI+d.MTTFB; })]).range([h, 0]);

// create a line function that can convert data[] into x and y points
//*********************************************
//*********************************************
//
//      This is the area where input is needed!
//
//*********************************************
//*********************************************
    // MPGL
    var line1 = d3.svg.line()
        .x(function(d,i) {return x(startTime + (timeStep*i));})
        .y(function(d) {return y(d.MPGL+d.MPGI+d.MTTFB);})
    // MPGI
    var line2 = d3.svg.line()
        .x(function(d,i) {return x(startTime + (timeStep*i));})
        .y(function(d) {return y(d.MPGI+d.MTTFB);})

    // MTTFB
    var line3 = d3.svg.line()
        .x(function(d,i) { return x(startTime + (timeStep*i)); })
        .y(function(d) { return y(d.MTTFB);})


//--------------------------------------------------------------------
// Creating the visualization by pulling all of the elements together
//--------------------------------------------------------------------
// Add an SVG element with the desired dimensions and margin.
    var graph = d3.select("#graph").append("svg:svg")
          .attr("width", w + m[1] + m[3])
          .attr("height", h + m[0] + m[2])
        .append("svg:g")
          .attr("transform", "translate(" + m[3] + "," + m[0] + ")");

// create yAxis
    var xAxis = d3.svg.axis().scale(x).tickSize(-h).tickSubdivide(1);

// Add the x-axis.
    graph.append("svg:g")
          .attr("class", "x axis")
          .attr("transform", "translate(0," + h + ")")
          .call(xAxis);


// create left yAxis
    var yAxisLeft = d3.svg.axis().scale(y).ticks(6).orient("left");
// Add the y-axis to the left
    graph.append("svg:g")
          .attr("class", "y axis")
          .attr("transform", "translate(-10,0)")
          .call(yAxisLeft);

//*********************************************************
   //  How do I pass something in the line_x_(...) portion
   //     that would give the function a clue which data point
   //     to add to the line/path?
   //*********************************************************
// add lines
// do this AFTER the axes above so that the line is above the tick-lines
    graph.append("svg:path").attr("d", line1(data)).attr("class", "MPGL");
    graph.append("svg:path").attr("d", line2(data)).attr("class", "MPGI");
    graph.append("svg:path").attr("d", line3(data)).attr("class", "MTTFB");

</script>
</body>
</html>
BenH
  • 167
  • 3
  • 10

1 Answers1

1

When using D3, you're much better of sticking to the selections pattern when passing in data. See this tutorial for example. In your particular case, you would give your data to the datum() function and then add lines accordingly. That is, the last three lines of your code would become

graph.append("path")
     .datum(data)
     .attr("class", "MPGL")
     .attr("d", line1);

and similarly for the other lines. Now you can replace the data argument with the subset of the data that you want to draw and update the line. You can use any D3 or Javascript functions to filter the data. This tutorial goes into a bit more detail on updating data.

For tooltips, this answer should help.

Community
  • 1
  • 1
Lars Kotthoff
  • 107,425
  • 16
  • 204
  • 204
  • I hate to be the poor initiate that has to say, "I don't get it," but that's the sad state of affairs. I've read the docs and through the tutorials mentioned above, but it's not clicking. When I drop the lines like this: graph.append("svg:path").attr("d", line1(data)).attr("class", "MPGL"); And replace with what is mentioned above, I get the axes and all, but nothing else. Too, I've tried hard-coding data into that field to no avail. Sadly, there isn't any console output to offer direction either. I think I get the _substance_ of your comment, but the application isn't coming. – BenH Apr 29 '13 at 20:05
  • I'm sorry, there's a typo in my answer -- it should be `datum` instead of `data`. I'll fix that. – Lars Kotthoff Apr 30 '13 at 08:50
  • Thanks for the update. I've been playing with this on and off today, and I am just stumped. It keeps giving me: Uncaugt TypeError: Object [object Array] has no method 'enter' I guess that makes sense because the data _is_ an array of objects. However, what was supplied seems like it _should_ work. Do I maybe need to change my input data format? I wanted to keep each "record" together, but R can easily spit out (by default!) all of the values for a given column (e.g. MPGL) in one array. – BenH Apr 30 '13 at 20:10
  • Ok, this is getting embarrassing. My mistake again -- the `selectAll` pattern doesn't work with `datum`. I'll fix it yet again to hopefully produce something that works. – Lars Kotthoff Apr 30 '13 at 21:41
  • Makes perfect sense. And, thankfully, I'm learning. However, I think this isn't fundamentally different from what I had above - at least from a functionality perspective. I've updated the question to hopefully illustrate more clearly what I hoped to achieve - that I want _one_ line declaration statement to which I can pass the appropriate parameters to select the right elements from the input data. In other words, one generic line function with many calls to that function for different data & style characteristics. It _seems_ like this should be possible, it's just not clicking. – BenH May 01 '13 at 12:57
  • The proper way to do this in D3 is to modify the `data` that you pass in to something like the above, *not* to modify the function that generates the line. There are many advantages to this -- most notably, you'll be able to do meaningful transitions easier. – Lars Kotthoff May 01 '13 at 18:15
  • So, that makes complete sense. I've read tutorials, though and it doesn't seem clear. Usually, their datasets are far too simplistic. So, instead of: .datum(data) Should I do something like: .datum(function(d){return d.MPGL}) That still doesn't seem right, but nothing else is making any sense. – BenH May 01 '13 at 21:30
  • You would nest it at the top level rather than each individual item. That is, have an object with attributes MPGL etc, each of which is an array. Then you can pass it in as `.datum(data.MPGL)`. But yes, you can certainly use code like you've just posted as well. This would allow you to use the same line definition with everything. – Lars Kotthoff May 02 '13 at 08:25
  • Fantastic! I had to fix the data output from R, but now that that is resolved, I'm getting exactly what I wanted. Thank you for the patience and explanation! – BenH May 03 '13 at 17:31