Dynamic data transition with d3.json problem

1,123 views
Skip to first unread message

Jonas Langvad

unread,
Apr 28, 2014, 2:25:52 AM4/28/14
to d3...@googlegroups.com
So I've been working on d3 for a little while. I'm trying to get a bar chart to transition to refreshed data based on user input parameters. However, I'm running into a problem where the transition won't render for some reason. Here is my general setup:

I have a bar chart loading data with d3.json("getdata.php", function(data).....

getdata.php has a default query specified when no input parameters are set. this part of my code works like it is meant to.

Here is what doesn't work:

A user can select different inputs/parameters. These are sent via ajax to getdata.php and the update query is run. To make sure the query returns the updated data I am outputting the data using a callback function within the ajax request. I can indeed tell that it is returning the correct information. However, the transition is not run and nothing happens.

This is my update function:

d3.select("#submit-filter").on("click", function() {
d3.json("getdata.php", function(data) {
          chart.selectAll("rect")
                        .data(data)
                        .transition()
  .delay(500)
                        .duration(1000)
                        .attr("y", function(d) {
                    return yScale(d.cases);
})
.attr("height", function(d) {
return c1bh - yScale(d.cases);
});
console.log(d3.max(d3.values(data)));

});
});

Have anyone run into this problem before? would it make sense to put the ajax request into the update function and use the callback to get the data? The other thing I can think of would be to have some sort of delay in the transition to make sure the data is received.

Any suggestions are welcome.




kgu87

unread,
Apr 28, 2014, 9:21:04 AM4/28/14
to d3...@googlegroups.com
I think you want something what would include enter().append("rect"), i.e.

chart
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr(...)
.transition()
...

Jonas Langvad

unread,
Apr 28, 2014, 12:28:07 PM4/28/14
to d3...@googlegroups.com
@kgu87:

I appreciate you taking the time to respond. However, I don't think that is the answer I am looking for. A little more information I should have included in the original post:

I get "cannot read property 'length' of null" in the console. This leads me to believe the update function is run before the updated data is loaded. Also, if I change the "getdata.php" data to static data (no user input/parameter in the query) the transition actually works. Again, it seems the checks and query I run in "getdata.php" are slower than the update function. So the update function runs before the data is ready. 

Has anyone encountered something similar before?

Thanks!

Jonas Langvad

unread,
Apr 28, 2014, 9:22:33 PM4/28/14
to d3...@googlegroups.com
So I added error output to the console, I now get "SyntaxError: Unexpected token <" It points to my update function line 3 here:

d3.select("#submit-filter").on("click", function() { d3.json("getdata.php", function(error, dataset) { if(error) return console.log("data could not be loaded: " + error); console.log(d3.values(dataset)); chart.selectAll(".bar") .data(dataset) .transition() .delay(500) .duration(1000) .attr("y", function(d) { return yScale(d.cases); }) .attr("height", function(d) { return c1bh - yScale(d.cases); });
}); });

However, I'm pretty sure it has to do with the way I'm running my query in getdata.php. If I change it to a static query (no user input/parameters) I don't get the error and the chart goes through the transition. As soon as I try it with the user input I get the error. 

Here is getdata.php:

$location = implode("', '",$_POST['location']);
$location = "'". $location ."'"; 

$records = DB::getInstance()->query("SELECT SUM(value) as cases , date from cases WHERE location in ($location) AND date between '2014-01-01' AND '2014-01-30' GROUP BY date");
if($records->count()) {
echo json_encode($records->results());

} else {
return false;
        }

Any suggestions are much appreciated.

kgu87

unread,
Apr 29, 2014, 6:46:16 AM4/29/14
to d3...@googlegroups.com
What do you see if you console.log(dataset) ? 

Victor Powell

unread,
Apr 29, 2014, 11:46:52 AM4/29/14
to d3...@googlegroups.com
Seems like you've got an issue with the data you're sending from the `getdata.php` file. Make sure the response coming from the server is valid JSON. One way to debug this would be to instead make a simple XHR request and then see what the response text looks like. Then, try and parse it as JSON (which I'm guessing will fail.)

d3.xhr('getdata.php', function(err, res){
  if(err) throw err;
  console.log(res); // should be the data as a string
  var data = JSON.parse(res); // if this fails, you're not sending JSON from `getdata.php`
});

Jonas Langvad

unread,
Apr 29, 2014, 5:18:41 PM4/29/14
to d3...@googlegroups.com
Thank you Victor! I suspect it has to do with the format of the data from getdata.php and the way I'm trying to parse it. I'll give it a try tonight.

Jonas Langvad

unread,
May 6, 2014, 1:11:17 AM5/6/14
to d3...@googlegroups.com
Victor,

I finally got around to debugging like you suggested. It fails as expected when I run it and returns the following:

XMLHttpRequest {statusText"OK"status200response"[{"cases":"374","date":"2014-01-01 00:00:00"},{"ca…0"},{"cases":"374","date":"2014-01-30 00:00:00"}]"responseType""responseXMLnull}
  1. onabortnull
  2. onerrorfunction respond() {
  3. onloadfunction respond() {
  4. onloadendnull
  5. onloadstartnull
  6. onprogressfunction (event) {
  7. onreadystatechangenull
  8. ontimeoutnull
  9. readyState4
  10. response"[{"cases":"374","date":"2014-01-01 00:00:00"},{"cases":"353","date":"2014-01-02 00:00:00"},{"cases":"72","date":"2014-01-03 00:00:00"},{"cases":"22","date":"2014-01-04 00:00:00"},{"cases":"411","date":"2014-01-05 00:00:00"},{"cases":"439","date":"2014-01-06 00:00:00"},{"cases":"455","date":"2014-01-07 00:00:00"},{"cases":"387","date":"2014-01-08 00:00:00"},{"cases":"374","date":"2014-01-09 00:00:00"},{"cases":"89","date":"2014-01-10 00:00:00"},{"cases":"22","date":"2014-01-11 00:00:00"},{"cases":"411","date":"2014-01-12 00:00:00"},{"cases":"439","date":"2014-01-13 00:00:00"},{"cases":"455","date":"2014-01-14 00:00:00"},{"cases":"387","date":"2014-01-15 00:00:00"},{"cases":"374","date":"2014-01-16 00:00:00"},{"cases":"89","date":"2014-01-17 00:00:00"},{"cases":"22","date":"2014-01-18 00:00:00"},{"cases":"411","date":"2014-01-19 00:00:00"},{"cases":"439","date":"2014-01-20 00:00:00"},{"cases":"455","date":"2014-01-21 00:00:00"},{"cases":"387","date":"2014-01-22 00:00:00"},{"cases":"374","date":"2014-01-23 00:00:00"},{"cases":"89","date":"2014-01-24 00:00:00"},{"cases":"22","date":"2014-01-25 00:00:00"},{"cases":"411","date":"2014-01-26 00:00:00"},{"cases":"439","date":"2014-01-27 00:00:00"},{"cases":"455","date":"2014-01-28 00:00:00"},{"cases":"387","date":"2014-01-29 00:00:00"},{"cases":"374","date":"2014-01-30 00:00:00"}]"
  11. responseText"[{"cases":"374","date":"2014-01-01 00:00:00"},{"cases":"353","date":"2014-01-02 00:00:00"},{"cases":"72","date":"2014-01-03 00:00:00"},{"cases":"22","date":"2014-01-04 00:00:00"},{"cases":"411","date":"2014-01-05 00:00:00"},{"cases":"439","date":"2014-01-06 00:00:00"},{"cases":"455","date":"2014-01-07 00:00:00"},{"cases":"387","date":"2014-01-08 00:00:00"},{"cases":"374","date":"2014-01-09 00:00:00"},{"cases":"89","date":"2014-01-10 00:00:00"},{"cases":"22","date":"2014-01-11 00:00:00"},{"cases":"411","date":"2014-01-12 00:00:00"},{"cases":"439","date":"2014-01-13 00:00:00"},{"cases":"455","date":"2014-01-14 00:00:00"},{"cases":"387","date":"2014-01-15 00:00:00"},{"cases":"374","date":"2014-01-16 00:00:00"},{"cases":"89","date":"2014-01-17 00:00:00"},{"cases":"22","date":"2014-01-18 00:00:00"},{"cases":"411","date":"2014-01-19 00:00:00"},{"cases":"439","date":"2014-01-20 00:00:00"},{"cases":"455","date":"2014-01-21 00:00:00"},{"cases":"387","date":"2014-01-22 00:00:00"},{"cases":"374","date":"2014-01-23 00:00:00"},{"cases":"89","date":"2014-01-24 00:00:00"},{"cases":"22","date":"2014-01-25 00:00:00"},{"cases":"411","date":"2014-01-26 00:00:00"},{"cases":"439","date":"2014-01-27 00:00:00"},{"cases":"455","date":"2014-01-28 00:00:00"},{"cases":"387","date":"2014-01-29 00:00:00"},{"cases":"374","date":"2014-01-30 00:00:00"}]"
  12. responseType""
  13. responseXMLnull
  14. status200
  15. statusText"OK"
  16. timeout0
  17. uploadXMLHttpRequestUpload
  18. withCredentialsfalse
  19. __proto__XMLHttpRequest
charts.js:269
  1. Uncaught SyntaxError: Unexpected token o
    1. (anonymous function)
    2. (anonymous function)
    3. event
    4. respond

So it seems the problem is with the way I parse the data. How would I pass the data in this format? This is a noob question but this is a JSON array right?

Thank you!



On Tuesday, April 29, 2014 8:46:52 AM UTC-7, Victor Powell wrote:

kgu87

unread,
May 6, 2014, 9:24:23 AM5/6/14
to d3...@googlegroups.com
You should set the content type in getdata.php before json_encode - 

header('Content-Type: application/json');
On Monday, April 28, 2014 2:25:52 AM UTC-4, Jonas Langvad wrote:

Kevin Western

unread,
May 6, 2014, 12:08:49 PM5/6/14
to d3...@googlegroups.com
I'm not sure if it matters in php, but it looks like you want to get using $_GET and not $_POST as you're not posting any data.

Jonas Langvad

unread,
May 9, 2014, 3:33:11 PM5/9/14
to d3...@googlegroups.com
Thank you kgu87. Unfortunately, adding the header as you suggested doesn't fix the problem. The type is now set to application/json but the data is still not loading. I guess there can be 2 explanations:

1) I'm returning the data in the right format but not parsing it correctly.
2) I'm not returning the data in the right format for getdata.php

2) seems like the most obvious answer since I tried to debug the script using:

d3.xhr('getdata.php', function(err, res){
  if(err) throw err;
  console.log(res); // should be the data as a string
  var data = JSON.parse(res); // if this fails, you're not sending JSON from `getdata.php`
});

In my getdata.php script I use "echo json_encode(data)" but it must still not be returning the right data. It's just weird because in the console I get:
Any other ideas I can try? I'm running out of ideas here.

Thanks!

Artan Simeqi

unread,
May 9, 2014, 7:00:15 PM5/9/14
to d3...@googlegroups.com
From the code you have shown, the php code seems wrong.

This line:
$location = implode("', '",$_POST['location']);
is looking for a POST parameter named "location". But in the
Javascript code you are doing a GET request and you are not
setting any parameters.

You need to do something like this:

d3.json("getdata.php")
.post(JSON.stringify({location: "London"}), function(error, dataset) {
if(error) return console.log("data could not be loaded: " + error);
/* work with the dataset here */
> --
> You received this message because you are subscribed to the Google Groups
> "d3-js" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to d3-js+un...@googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.

Jonas Langvad

unread,
May 9, 2014, 10:44:03 PM5/9/14
to d3...@googlegroups.com
Thank you Artan. I realized I haven't posted my entire code which makes it hard to diagnose the problem. Your suggestion pointed me in the right direction though. For others who may be looking at doing the same thing I'll post my entire code below and try to add comments. Hopefully that can save you some time. Also, if there are better ways to do this please feel free to comment. 

HTML: 
-------------------------------------I'm using bootstrap but left out all standard stuff like the scripts and css links. This page is just a basic form with checkboxes and an svg element where the chart is generated--------------------------------------------

<html>
<body>
      
    <div class="container">
    <form action="" method="post" id="form">
        <input type="checkbox" name="Location[]" value="AB">AB<br>
        <input type="checkbox" name="Location[]" value="CD">CD<br>
        <input type="submit" name="submit" value="submit" id="submit">
    </form>
    </div>
    
    <div class="container">
      
     <svg id="chart"></svg>
             
    </div>
    
</body>
</html>

PHP:
------------------------------------------------------------------------------------------------------------Here I just want to query my mysql database and return a default starting result set and then run the query again based on the data submitted in the form---------------------------------------------------------------

<?php

$hostname = 'localhost';

$db = '***';

$username = '****';

$password = '*****';


$dbh = new PDO("mysql:host=$hostname;dbname=$db", $username, $password, array(
    PDO::ATTR_PERSISTENT => true
));

$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

if(isset($_POST['ajax']) && $_POST['ajax'] === '1') {                  //ajax=1 is added to form data when submitted in order to check if submit has occured. I realize this could represent a problem if form is submitted without a checkbox selection. I'll add a fix for this later


    $values = $_POST['Location'];

    $qmarks = str_repeat('?,', count($values) - 1) . '?';


    $sql = $dbh->prepare("SELECT * FROM cases WHERE location IN ($qmarks) AND date between '2014-01-01' AND '2014-01-31'");

    foreach($values as $k => $value) {
        $sql->bindValue(($k+1), $value);
    }


    $sql->execute();

  $results = $sql->fetchAll(PDO::FETCH_ASSOC);

    header('Content-Type: application/json');


echo json_encode($results);
  
 
} else {                        // default query runs when form hasn't been submitted
 

    $sql = $dbh->prepare("SELECT * FROM cases where location = 'AB' AND date between '2014-01-01' AND '2014-01-31'");

   
    $sql->execute();

    $results = $sql->fetchAll(PDO::FETCH_ASSOC);

    header('Content-Type: application/json');

    echo json_encode($results);
}


Returned Data:

[{"location":"AB","Date":"2014-01-01 00:00:00","Value":"50"},{"location":"CD","Date":"2014-01-01 00:00:00","Value":"75"},............]


Javascript:
-----------------------------------------------------I am using an external file for my d3 code. The solution I finally came up with was to combine the onclick and submit event to send the form data to my PHP script and then grab the data right in the callback function rather than do it separately as I was trying to do in the past.----------------------------------------------------

var margin = {top:15, right:15, bottom:15, left:15},
    height = 500 - margin.top - margin.bottom,
    width = 900 - margin.left - margin.right;

var parseDate = d3.time.format("%Y-%m-%d %H:%M:%S").parse;

var formatTime = d3.time.format("%H:%M:%S");           //Not doing anything with this yet as I haven't added axis.

var x = d3.scale.ordinal()
    .rangeRoundBands([0, width], .1);

var y = d3.scale.linear()
    .range([height, 0]);

var chart = d3.select("#chart")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

d3.json("getdata.php", function(error, data) {                    //Loading the default data from PHP file.
     
    data.forEach(function(d) {
        d.Date = parseDate(d.Date);
        d.Value = +d.Value;
    });
     
    
    x.domain(data.map(function(d) { return d.Date; }));
    y.domain([0, d3.max(data, function(d) { return d.Value; })]);
    
   chart.selectAll(".bar")
        .data(data)
        .enter()
        .append("rect")
        .attr("class", "bar")
        .attr("x", function(d) { return x(d.Date); })
        .attr("y", function(d) { return y(d.Value); })
        .attr("height", function(d) { return height - y(d.Value); })
        .attr("width", x.rangeBand());
        
});

$('#form').submit(function(e) {                              //This is the main change I made and what made the code work. Basically I combined the onclick event of the submit button with the submit event and the d3.transition method with the returned data. I was trying to do                                                                                       this all separately before and it somehow got screwed up. Now it works!
            e.preventDefault();

            var data = $(this).serialize() + "&ajax=1";                          //adding ajax=1 in order to be able to properly check when form is submitted. 

            $.ajax('getdata.php', {
              type: "POST",
              dataType: 'json',
              data: data,
              success: function (data) {
data.forEach(function(d) {
        d.Date = parseDate(d.Date);
        d.Value = +d.Value;
    });
               
                   x.domain(data.map(function(d) { return d.Date; }));                          //Didn't update y.domain since it would distort the effect of the transition in this example. I'll add it later when I have axis implemented.
    
                               chart.selectAll("rect")
                                   .data(data)
                                   .transition()
                                   .duration(1000)
                                   .attr("y", function(d) { return y(d.Value); })
                                   .attr("height", function(d) { return height - y(d.Value); });
                                    }});
});


Phew finally figured it out. Again, there may be a better solution so feel free to comment. Thanks for all the help.
Reply all
Reply to author
Forward
0 new messages