scatterplot.org 7.2 KB

For every javascript to be able to communicate with the shiny server the following has to be included, and all the rest of the code goes inside it.


Shiny.addCustomMessageHandler("scatterplot_values", function(message) {

    <<data treatment>>
    <<plot>>
    <<brush>>
    <<the rest>>

});

var dataset = message;

The data we get from R is a two dimensional array of strings, positions of the form dataset[i][0] contain a time string in the form of a 4 digit number representing a time in a 24 hour clock, for instance "0700" would be representing 07:00. Javascript works with Date objects so we define two functions, the first parseTime will convert a string of the form explained above to a Date object. The other function formatTime will serve to print this object properly later. Now a simply loop through the dataset will parse all the time strings.


var dataset = message;

var parseTime = d3.timeParse("%H%M");
var formatTime = d3.timeFormat("%H:%M");

for (i = 0; i < dataset.length; i++) {
    dataset[i][0] = parseTime(dataset[i][0]);
}

We have to define the size of the area that will hold our plot. The total area is given by width and height variables, but inside it we will define a plot area, that is an area inside the svg element offset by the margins.


var margin = {top: 20, right: 50, bottom: 50, left: 85},
    width = 600,
    height = 500,
    plot_width = width - margin.right - margin.left,
    plot_height = height - margin.top - margin.bottom;

First element in the array is of the type Date so we have to use a time scale, the second element is number (actually a string that will be parsed to a number automatically) so we use a linear scale. The third element is a string describing the type of accident and is not used now.


var xScale = d3.scaleTime()
    .domain(d3.extent(dataset, function(d) { return (d[0]); }))
    .range([margin.left, plot_width]);

var yScale = d3.scaleLinear()
    .domain(d3.extent(dataset, function(d) { return d[1]; }))
    .range([plot_height, margin.top]);

We define two functions to create the axes of the graph, the X axis will be bellow the graphic and Y to the left of it. The function tickFormat receives a a function as argument that defines explicitely the formatting of the data appearing in an axis, here we pass the formatTime function defined previously.


var axis_x = d3.axisBottom(xScale)
    .tickFormat(formatTime);
var axis_y = d3.axisLeft(yScale);

Now we create an svg element where the visualization will be created. Following the project coding style there is a div named #scatterplot_area that will contain the svg, it is selected with d3.select and the svg is element is appended to it, we use the toal area (width e height), not only the plot area as the svg will also contain other graphic elements, like the axes.


var scatterplot = d3.select("#scatterplot_area")
    .append("svg")
    .attr("width", width)
    .attr("height", height);

Now we create a group for the plot points, each point receives an initial id of points.

Using the defined variable for the plot points we bind our dataset to svg circles, assigning a class of point to each circle and updating the id to the form of points-i, where i is a simple index from 0 to the dataset length - 1. The coordinates of the circles are given by the hour of the accident(d[0]) and the age of the victimn(d[1]).


var points = scatterplot.append("g").attr("id", "points");

// Criar círculos
points.selectAll("circle")
    .data(dataset)
    .enter()
    .append("circle")
    .attr("class", "point")
    .attr("id", function(d, i) {
	return "point-" + i;
    })
    .attr("r", 3)
    .attr("cx", function(d) {
	return xScale(d[0]);
    })
    .attr("cy", function(d) {
	return yScale(d[1]);
    });

Finally, after the graph is done we draw the axes.


scatterplot.append("g")
    .attr("id", "axis_x")
    .attr("transform", "translate(0," + (plot_height + margin.bottom / 2) + ")")
    .call(axis_x);

scatterplot.append("g")
    .attr("id", "axis_y")
    .attr("transform", "translate(" + (margin.left / 2) + ", 0)")
    .call(axis_y);

d3.select("#axis_x")
    .append("text")
    .attr("transform", "translate(420, -5)")
    .text("Hora");

d3.select("#axis_y")
    .append("text")
    .attr("transform", "rotate(-90) translate(-20, 15)")
    .text("Idade");

For this scatterplot we are using a third-party library called polybrush, it has the same API as d3's brush module, but works very differently, it enables polygonal brush selections, the first major difference is during instatiation we have to pass x and y values that define the area that it will work. The start interaction beggins with a click, it resets the selected class of all points. For the brush interaction itself we pass a function that will highlight the brushed circles, and on the end interaction another functions will display some information of the selected circles in a table.


var brush = d3.polybrush()
    .x(d3.scaleLinear().range([margin.left / 2, width]))
    .y(d3.scaleLinear().range([margin.top / 2, height]))
    .on("start", function() {
	scatterplot.selectAll(".selected").classed("selected", false);
    })
    .on("brush", highlightBrushedPoints)
    .on("end", displayTable);

scatterplot.append("g")
    .attr("class", "brush")
    .call(brush);

function clearTableRows() {

    hideTableColNames();
    d3.selectAll(".row_data").remove();
}

function hideTableColNames() {
    d3.select("table").style("visibility", "hidden");
}

function showTableColNames() {
    d3.select("table").style("visibility", "visible");
}

function highlightBrushedPoints() {

    scatterplot.selectAll(".point").classed("selected", function(d) {
        //get the associated circle
        var id = d3.select(this).attr("id");
        var i = id.substr(id.indexOf("-") + 1, id.length);

        if (brush.isWithinExtent(xScale(d[0]), yScale(d[1]))) {
            return true;
        } else {
            return false;
        }
    });
}

function displayTable() {

    var d_brushed =  d3.selectAll(".point.selected").data();

    console.log(d_brushed);

    // populate table if one or more elements is brushed
    if (d_brushed.length > 0) {
	clearTableRows();
	d_brushed.forEach(d_row => populateTableRow(d_row));
    } else {
	clearTableRows();
    }
}

function populateTableRow(d_row) {

    showTableColNames();

    var d_row_filter = [d_row[2], // Agente Causador
                        formatTime(d_row[0]), // Hora
                        d_row[1]]; //Idade

    d3.select("table")
        .append("tr")
        .attr("class", "row_data")
        .selectAll("td")
        .data(d_row_filter)
        .enter()
        .append("td")
        .attr("align", (d, i) => i == 0 ? "left" : "right")
        .text(d => d);
}