The goal of this vignette is to explain the older resamplers:
ResamplingVariableSizeTrainCV and ResamplingSameOtherCV, which
output some data which are useful for visualizing the train/test
splits. If you do not want to visualize the train/test splits, then it
is recommended to instead use the newer resampler,
ResamplingSameOtherSizesCV (see other vignette).
Same/Other/All resampler
The goal of thie section is to explain how to quantify the extent to
which it is possible to train on one data subset, and predict on
another data subset. This kind of problem occurs frequently in many
different problem domains:
geography: can we train on one region (say Europe) and accurately
predict on another? (North America)
time series: can we train on one time period (2000) and accurately
predict on another? (2001)
personalization: can we train on one person (Alice) and accurately
predict on another? (Bob)
The ideas are similar to my previous blog posts about how to do this
in
python
and R. Below
we explain how to use mlr3resampling for this purpose, in simulated
regression and classification problems. To use this method in
real data, the important sections to read below are named “Benchmark:
computing test error,” which show how to create these cross-validation
experiments using mlr3 code.
Simulated regression problems
We begin by generating some data which can be used with regression algorithms.
Assume there is a data set with some rows from one person, some rows
from another,
The table above shows some simulated data for two regression problems:
easy problem has the same pattern for each person, so it is possible/easy to train on one person, and accurately predict on another.
impossible problem has a different pattern for each person, so it is impossible to train on one person, and accurately predict on another.
when adapting the code above to real data, the important part is the
mlr3::TaskRegr line which tells mlr3 what data set to use, what is
the target column, and what is the subset/stratum column.
for the easy pattern, it is the same for both people, so it should
be possible/easy to train on one person, and accurately predict on
another.
for the impossible pattern, it is different for each person, so it
should not be possible to train on one person, and accurately
predict on another.
Benchmark: computing test error
In the code below, we define a K-fold cross-validation experiment.
(reg_same_other <- mlr3resampling::ResamplingSameOtherCV$new())
#> <ResamplingSameOtherCV> : Same versus Other Cross-Validation
#> * Iterations:
#> * Instantiated: FALSE
#> * Parameters:
#> List of 1
#> $ folds: int 3
In the code below, we define two learners to compare,
In the code below, we define the benchmark grid, which is all
combinations of tasks (easy and impossible), learners (rpart and
featureless), and the one resampling method.
for the easy task, training on same is just as good as all or other
subsets. rpart has much lower test error than featureless, in all
three train subsets.
for the impossible task, the least test error is using rpart with same train subsets; featureless with same train subsets is next best; training on all is substantially worse (for both featureless and rpart); training on other is even worse (patterns in the two people are completely different).
in a real data task, training on other will most likely not be quite as bad as in the impossible task above, but also not as good as in the easy task.
Interactive visualization of data, test error, and splits
The code below can be used to create an interactive data visualization
which allows exploring how different functions are learned during
different splits.
// Constructor for animint Object.
var animint = function (to_select, json_file) {
var steps = [];
var default_axis_px = 16;
function wait_until_then(timeout, condFun, readyFun) {
var args=arguments
function checkFun() {
if(condFun()) {
readyFun(args[3],args[4]);
} else{
setTimeout(checkFun, timeout);
}
}
checkFun();
}
function convert_R_types(resp_array, types){
return resp_array.map(function (d) {
for (var v_name in d) {
if(!is_interactive_aes(v_name)){
var r_type = types[v_name];
if (r_type == "integer") {
d[v_name] = parseInt(d[v_name]);
} else if (r_type == "numeric") {
d[v_name] = parseFloat(d[v_name]);
} else if (r_type == "factor" || r_type == "rgb"
|| r_type == "linetype" || r_type == "label"
|| r_type == "character") {
// keep it as a character
} else if (r_type == "character" & v_name == "outliers") {
d[v_name] = parseFloat(d[v_name].split(" @ "));
}
}
}
return d;
});
}
// replacing periods in variable with an underscore this makes sure
// that selector doesn't confuse . in name with css selectors
function safe_name(unsafe_name){
return unsafe_name.replace(/[ .]/g, '_');
}
function legend_class_name(selector_name){
return safe_name(selector_name) + "_variable";
}
function is_interactive_aes(v_name){
if(v_name.indexOf("clickSelects") > -1){
return true;
}
if(v_name.indexOf("showSelected") > -1){
return true;
}
return false;
}
var linetypesize2dasharray = function (lt, size) {
var isInt = function(n) {
return typeof n === 'number' && parseFloat(n) == parseInt(n, 10) && !isNaN(n);
};
if(isInt(lt)){ // R integer line types.
if(lt == 1){
return null;
}
var o = {
0: size * 0 + "," + size * 10,
2: size * 4 + "," + size * 4,
3: size + "," + size * 2,
4: size + "," + size * 2 + "," + size * 4 + "," + size * 2,
5: size * 8 + "," + size * 4,
6: size * 2 + "," + size * 2 + "," + size * 6 + "," + size * 2
};
} else { // R defined line types
if(lt == "solid" || lt === null){
return null;
}
var o = {
"blank": size * 0 + "," + size * 10,
"none": size * 0 + "," + size * 10,
"dashed": size * 4 + "," + size * 4,
"dotted": size + "," + size * 2,
"dotdash": size + "," + size * 2 + "," + size * 4 + "," + size * 2,
"longdash": size * 8 + "," + size * 4,
"twodash": size * 2 + "," + size * 2 + "," + size * 6 + "," + size * 2,
"22": size * 2 + "," + size * 2,
"42": size * 4 + "," + size * 2,
"44": size * 4 + "," + size * 4,"13": size + "," + size * 3,
"1343": size + "," + size * 3 + "," + size * 4 + "," + size * 3,
"73": size * 7 + "," + size * 3,
"2262": size * 2 + "," + size * 2 + "," + size * 6 + "," + size * 2,
"12223242": size + "," + size * 2 + "," + size * 2 + "," + size * 2 + "," + size * 3 + "," + size * 2 + "," + size * 4 + "," + size * 2,
"F282": size * 15 + "," + size * 2 + "," + size * 8 + "," + size * 2,
"F4448444": size * 15 + "," + size * 4 + "," + size * 4 + "," + size * 4 + "," + size * 8 + "," + size * 4 + "," + size * 4 + "," + size * 4,
"224282F2": size * 2 + "," + size * 2 + "," + size * 4 + "," + size * 2 + "," + size * 8 + "," + size * 2 + "," + size * 16 + "," + size * 2,
"F1": size * 16 + "," + size
};
}
if (lt in o){
return o[lt];
} else{ // manually specified line types
str = lt.split("");
strnum = str.map(function (d) {
return size * parseInt(d, 16);
});
return strnum;
}
};
var isArray = function(o) {
return Object.prototype.toString.call(o) === '[object Array]';
};
// create a dummy element, apply the appropriate classes,
// and then measure the element
// Inspired from http://jsfiddle.net/uzddx/2/
var measureText = function(pText, pFontSize, pAngle, pStyle) {
if (!pText || pText.length === 0) return {height: 0, width: 0};
if (pAngle === null || isNaN(pAngle)) pAngle = 0;
var container = element.append('svg');
// do we need to set the class so that styling is applied?
//.attr('class', classname);
container.append('text')
.attr({x: -1000, y: -1000})
.attr("transform", "rotate(" + pAngle + ")")
.attr("style", pStyle)
.attr("font-size", pFontSize)
.text(pText);
var bbox = container.node().getBBox();
container.remove();
return {height: bbox.height, width: bbox.width};
};
var nest_by_group = d3.nest().key(function(d){ return d.group; });
var dirs = json_file.split("/");
dirs.pop(); //if a directory path exists, remove the JSON file from dirs
var element = d3.select(to_select);
this.element = element;
var viz_id = element.attr("id");
var plot_widget_table = element.append("table");
var plot_td = plot_widget_table.append("tr").append("td");
plot_td.attr("class","plot_content");
var widget_td = plot_widget_table.append("tr").append("td");
var Widgets = {};
this.Widgets = Widgets;
var Selectors = {};
this.Selectors = Selectors;
var Plots = {};
this.Plots = Plots;
var Geoms = {};
this.Geoms = Geoms;
// SVGs must be stored separately from Geoms since they are
// initialized first, with the Plots.
var SVGs = {};
this.SVGs = SVGs;
var Animation = {};
this.Animation = Animation;
var all_geom_names = {};
this.all_geom_names = all_geom_names;
//creating an array to contain the selectize widgets
var selectized_array = [];
var data_object_geoms = {
"line":true,
"path":true,
"ribbon":true,
"polygon":true
};
var css = document.createElement('style');
css.type = 'text/css';
var styles = [".axis path{fill: none;stroke: black;shape-rendering: crispEdges;}",
".axis line{fill: none;stroke: black;shape-rendering: crispEdges;}",
".axis text {font-family: sans-serif;font-size: 11px;}"];
var add_geom = function (g_name, g_info) {
// Determine if data will be an object or an array.
// added geom properties in steps array
var geom = g_info.classed;
var title = g_info.params.title || g_info.classed;
var helpText = g_info.params.help || '';
var help_showSelected = g_info.params.help_showSelected || '';
var help_clickSelects = g_info.params.help_clickSelects || '';
var description = helpText;
if(g_info.params.hasOwnProperty("showSelected")){
if(description != "")description += ' ';
description += 'Data are shown for the current selection of: ' + help_showSelected;
}
if(g_info.params.hasOwnProperty("clickSelects")){
if(description != "")description += ' ';
description += 'Click to select: ' + help_clickSelects;
}
if(description == ""){
description = "No interactions available";
}
steps.push({ // this add the geom to the steps array for guided tour
element: '#' + viz_id + ' .' + geom,
popover: {
title: title,
description: description
}
});
if(g_info.geom in data_object_geoms){
g_info.data_is_object = true;
}else{
g_info.data_is_object = false;
}
// Add a row to the loading table.
g_info.tr = Widgets["loading"].append("tr");
g_info.tr.append("td").text(g_name);
g_info.tr.append("td").attr("class", "chunk");
g_info.tr.append("td").attr("class", "downloaded").text(0);
g_info.tr.append("td").text(g_info.total);
g_info.tr.append("td").attr("class", "status").text("initialized");
// load chunk tsv
g_info.data = {};
g_info.download_status = {};
Geoms[g_name] = g_info;
// Determine whether common chunk tsv exists
// If yes, load it
if(g_info.hasOwnProperty("columns") && g_info.columns.common){
var common_tsv = get_tsv(g_info, "_common");
g_info.common_tsv = common_tsv;
var common_path = getTSVpath(common_tsv);
d3.tsv(common_path, function (error, response) {
var converted = convert_R_types(response, g_info.types);
g_info.data[common_tsv] = nest_by_group.map(converted);
});
} else {
g_info.common_tsv = null;
}
// Save this geom and load it!
update_geom(g_name, null);
};
var add_plot = function (p_name, p_info) {
// Each plot may have one or more legends. To make space for the
// legends, we put each plot in a table with one row and two
// columns: tdLeft and tdRight.
var plot_table = plot_td.append("table").style("display", "inline-block");
var plot_tr = plot_table.append("tr");
var tdLeft = plot_tr.append("td");
var tdRight = plot_tr.append("td").attr("class", p_name+"_legend");
if(viz_id === null){
p_info.plot_id = p_name;
}else{
p_info.plot_id = viz_id + "_" + p_name;
}
var svg = tdLeft.append("svg")
.attr("id", p_info.plot_id)
.attr("height", p_info.options.height)
.attr("width", p_info.options.width);
// divvy up width/height based on the panel layout
var nrows = Math.max.apply(null, p_info.layout.ROW);
var ncols = Math.max.apply(null, p_info.layout.COL);
var panel_names = p_info.layout.PANEL;
var npanels = Math.max.apply(null, panel_names);
// Note axis names are "shared" across panels (just like the title)
var xtitlepadding = 5 + measureText(p_info["xtitle"], default_axis_px).height;
var ytitlepadding = 5 + measureText(p_info["ytitle"], default_axis_px).height;
// 'margins' are fixed across panels and do not
// include title/axis/label padding (since these are not
// fixed across panels). They do, however, account for
// spacing between panels
var text_height_pixels = measureText("foo", 11).height;
var margin = {
left: 0,
right: text_height_pixels * p_info.panel_margin_lines,
top: text_height_pixels * p_info.panel_margin_lines,
bottom: 0
};
var plotdim = {
width: 0,
height: 0,
xstart: 0,
xend: 0,
ystart: 0,
yend: 0,
graph: {
width: 0,
height: 0
},
margin: margin,
xlab: {
x: 0,
y: 0
},
ylab: {
x: 0,
y: 0
},
title: {
x: 0,
y: 0
}
};
// Draw the title
var titlepadding = measureText(p_info.title, p_info.title_size).height;
// why are we giving the title padding if it is undefined?
if (p_info.title === undefined) titlepadding = 0;
plotdim.title.x = p_info.options.width / 2;
plotdim.title.y = titlepadding;
svg.append("text")
.text(p_info.title)
.attr("class", "plottitle")
.attr("font-family", "sans-serif")
.attr("font-size", p_info.title_size)
.attr("transform", "translate(" + plotdim.title.x + "," +
plotdim.title.y + ")")
.style("text-anchor", "middle");
// grab max text size over axis labels and facet strip labels
var axispaddingy = 5;
if(p_info.hasOwnProperty("ylabs") && p_info.ylabs.length){
axispaddingy += Math.max.apply(null, p_info.ylabs.map(function(entry){
// + 5 to give a little extra space to avoid bad axis labels
// in shiny.
return measureText(entry, p_info.ysize).width + 5;
}));
}
var axispaddingx = 30; // distance between tick marks and x axis name.
if(p_info.hasOwnProperty("xlabs") && p_info.xlabs.length){
// TODO: throw warning if text height is large portion of plot height?
axispaddingx += Math.max.apply(null, p_info.xlabs.map(function(entry){
return measureText(entry, p_info.xsize, p_info.xangle).height;
}));
// TODO: carefully calculating this gets complicated with rotating xlabs
//margin.right += 5;
}
plotdim.margin = margin;
var strip_heights = p_info.strips.top.map(function(entry){
return measureText(entry, p_info.strip_text_xsize).height;
});
var strip_widths = p_info.strips.right.map(function(entry){
return measureText(entry, p_info.strip_text_ysize).height;
});
// compute the number of x/y axes, max strip height per row, and
// max strip width per columns, for calculating height/width of
// graphing region.
var row_strip_heights = [];
var col_strip_widths = [];
var n_xaxes = 0;
var n_yaxes = 0;
var current_row, current_col;
for (var layout_i = 0; layout_i < npanels; layout_i++) {
current_row = p_info.layout.ROW[layout_i] - 1;
current_col = p_info.layout.COL[layout_i] - 1;
if(row_strip_heights[current_row] === undefined){
row_strip_heights[current_row] = [];
}
if(col_strip_widths[current_col] === undefined){
col_strip_widths[current_col] = [];
}
row_strip_heights[current_row].push(strip_heights[layout_i]);
col_strip_widths[current_col].push(strip_widths[layout_i]);
if (p_info.layout.COL[layout_i] == 1) {
n_xaxes += p_info.layout.AXIS_X[layout_i];
}
if (p_info.layout.ROW[layout_i] == 1) {
n_yaxes += p_info.layout.AXIS_Y[layout_i];
}
}
function cumsum_array(array_of_arrays){
var cumsum = [], max_value, cumsum_value = 0;
for(var i=0; i 1) {
background.append("rect")
.attr("x", plotdim.xstart)
.attr("y", plotdim.ystart)
.attr("width", plotdim.xend - plotdim.xstart)
.attr("height", plotdim.yend - plotdim.ystart)
.attr("class", "background_rect")
.style("fill", p_info.panel_background.fill)
.style("stroke", p_info.panel_background.colour)
.style("stroke-dasharray", function() {
return linetypesize2dasharray(p_info.panel_background.linetype,
p_info.panel_background.size);
});
}
// drawing the grid lines
["grid_minor", "grid_major"].forEach(function(grid_class){
var grid_background = p_info[grid_class];
// if grid lines are defined
if(grid_background.hasOwnProperty("size")) {
var grid = background.append("g")
.attr("class", grid_class);
["x","y"].forEach(function(scale_var){
var const_var;
if(scale_var == "x"){
const_var = "y";
}else{
const_var = "x";
}
grid.append("g")
.attr("class", scale_var)
.selectAll("line")
.data(grid_background.loc[scale_var][layout_i])
.enter()
.append("line")
.attr(const_var + "1", plotdim[const_var + "start"])
.attr(const_var + "2", plotdim[const_var + "end"])
.attr(scale_var + "1", function(d) {
return scales[panel_i][scale_var](d);
})
.attr(scale_var + "2", function(d) {
return scales[panel_i][scale_var](d);
})
.style("stroke", grid_background.colour)
.style("stroke-linecap", grid_background.lineend)
.style("stroke-width", grid_background.size)
.style("stroke-dasharray", linetypesize2dasharray(
grid_background.linetype, grid_background.size))
;
});
}
});
// drawing border
// uses insert to draw it right before the #plottitle
if(Object.keys(p_info.panel_border).length > 1) {
background.append("rect")
.attr("x", plotdim.xstart)
.attr("y", plotdim.ystart)
.attr("width", plotdim.xend - plotdim.xstart)
.attr("height", plotdim.yend - plotdim.ystart)
.attr("class", "border_rect")
.style("fill", p_info.panel_border.fill)
.style("stroke", p_info.panel_border.colour)
.style("stroke-dasharray", function() {
return linetypesize2dasharray(p_info.panel_border.linetype,
p_info.panel_border.size);
});
}
} //end of for(layout_i
// After drawing all backgrounds, we can draw the axis labels.
if(p_info["ytitle"]){
svg.append("text")
.text(p_info["ytitle"])
.attr("class", "ytitle")
.style("text-anchor", "middle")
.style("font-size", default_axis_px + "px")
.attr("transform", "translate(" +
ytitle_x +
"," +
(ytitle_top + ytitle_bottom)/2 +
")rotate(270)")
;
}
if(p_info["xtitle"]){
svg.append("text")
.text(p_info["xtitle"])
.attr("class", "xtitle")
.style("text-anchor", "middle")
.style("font-size", default_axis_px + "px")
.attr("transform", "translate(" +
(xtitle_left + xtitle_right)/2 +
"," +
xtitle_y +
")")
;
}
Plots[p_name].scales = scales;
}; //end of add_plot()
function update_legend_opacity(v_name){
var s_info = Selectors[v_name];
s_info.legend_tds.style("opacity", s_info.legend_update_fun);
}
var add_selector = function (s_name, s_info) {
Selectors[s_name] = s_info;
if(s_info.type == "multiple"){
if(!isArray(s_info.selected)){
s_info.selected = [s_info.selected];
}
// legend_update_fun is evaluated in the context of the
// td.legend_entry_label.
s_info.legend_update_fun = function(d){
var i_value = s_info.selected.indexOf(this.textContent);
if(i_value == -1){
return 0.5;
}else{
return 1;
}
}
}else{
s_info.legend_update_fun = function(d){
if(this.textContent == s_info.selected){
return 1;
}else{
return 0.5;
}
}
}
s_info.legend_tds =
element.selectAll("tr."+legend_class_name(s_name)+" td.legend_entry_label")
;
update_legend_opacity(s_name);
}; //end of add_selector()
function get_tsv(g_info, chunk_id){
return g_info.classed + "_chunk" + chunk_id + ".tsv";
}
function getTSVpath(tsv_name){
return dirs.concat(tsv_name).join("/");
}
/**
* copy common chunk tsv to varied chunk tsv, returning an array of
* objects.
*/
function copy_chunk(g_info, varied_chunk) {
var varied_by_group = nest_by_group.map(varied_chunk);
var common_by_group = g_info.data[g_info.common_tsv];
var new_varied_chunk = [];
for(group_id in varied_by_group){
var varied_one_group = varied_by_group[group_id];
var common_one_group = common_by_group[group_id];
var common_i = 0;
for(var varied_i=0; varied_i < varied_one_group.length; varied_i++){
// there are two cases: each group of varied data is of length
// 1, or of length of the common data.
if(common_one_group.length == varied_one_group.length){
common_i = varied_i;
}
var varied_obj = varied_one_group[varied_i];
var common_obj = common_one_group[common_i];
for(col in common_obj){
if(col != "group"){
varied_obj[col] = common_obj[col];
}
}
new_varied_chunk.push(varied_obj);
}
}
return new_varied_chunk;
}
// update_geom is called from add_geom and update_selector. It
// downloads data if necessary, and then calls draw_geom.
var update_geom = function (g_name, selector_name) {
var g_info = Geoms[g_name];
// First apply chunk_order selector variables.
var chunk_id = g_info.chunks;
g_info.chunk_order.forEach(function (v_name) {
if(chunk_id == null){
return; // no data in a higher up chunk var.
}
var value = Selectors[v_name].selected;
if(chunk_id.hasOwnProperty(value)){
chunk_id = chunk_id[value];
}else{
chunk_id = null; // no data to show in this subset.
}
});
if(chunk_id == null){
draw_panels(g_info, [], selector_name); //draw nothing.
return;
}
var tsv_name = get_tsv(g_info, chunk_id);
// get the data if it has not yet been downloaded.
g_info.tr.select("td.chunk").text(tsv_name);
if(g_info.data.hasOwnProperty(tsv_name)){
draw_panels(g_info, g_info.data[tsv_name], selector_name);
}else{
g_info.tr.select("td.status").text("downloading");
var svg = SVGs[g_name];
var loading = svg.append("text")
.attr("class", "loading"+tsv_name)
.text("Downloading "+tsv_name+"...")
.attr("font-size", 9)
//.attr("x", svg.attr("width")/2)
.attr("y", 10)
.style("fill", "red");
download_chunk(g_info, tsv_name, function(chunk){
loading.remove();
draw_panels(g_info, chunk, selector_name);
});
}
};
var draw_panels = function(g_info, chunk, selector_name) {
// derive the plot name from the geometry name
var g_names = g_info.classed.split("_");
var p_name = g_names[g_names.length - 1];
var panels = Plots[p_name].layout.PANEL;
panels.forEach(function(panel) {
draw_geom(g_info, chunk, selector_name, panel);
});
};
function download_next(g_name){
var g_info = Geoms[g_name];
var selector_value = Animation.sequence[g_info.seq_i];
var chunk_id = g_info.chunks[selector_value];
var tsv_name = get_tsv(g_info, chunk_id);
g_info.seq_count += 1;
if(Animation.sequence.length == g_info.seq_count){
Animation.done_geoms[g_name] = 1;
return;
}
g_info.seq_i += 1;
if(g_info.seq_i == Animation.sequence.length){
g_info.seq_i = 0;
}
if(typeof(chunk_id) == "string"){
download_chunk(g_info, tsv_name, function(chunk){
download_next(g_name);
})
}else{
download_next(g_name);
}
}
// download_chunk is called from update_geom and download_next.
function download_chunk(g_info, tsv_name, funAfter){
if(g_info.download_status.hasOwnProperty(tsv_name)){
var chunk;
if(g_info.data_is_object){
chunk = {};
}else{
chunk = [];
}
funAfter(chunk);
return; // do not download twice.
}
g_info.download_status[tsv_name] = "downloading";
// prefix tsv file with appropriate path
var tsv_file = getTSVpath(tsv_name);
d3.tsv(tsv_file, function (error, response) {
// First convert to correct types.
g_info.download_status[tsv_name] = "processing";
response = convert_R_types(response, g_info.types);
wait_until_then(500, function(){
if(g_info.common_tsv) {
return g_info.data.hasOwnProperty(g_info.common_tsv);
}else{
return true;
}
}, function(){
if(g_info.common_tsv) {
// copy data from common tsv to varied tsv
response = copy_chunk(g_info, response);
}
var nest = d3.nest();
g_info.nest_order.forEach(function (v_name) {
nest.key(function (d) {
return d[v_name];
});
});
var chunk = nest.map(response);
g_info.data[tsv_name] = chunk;
g_info.tr.select("td.downloaded").text(d3.keys(g_info.data).length);
g_info.download_status[tsv_name] = "saved";
funAfter(chunk);
});
});
}//download_chunk.
// update_geom is responsible for obtaining a chunk of downloaded
// data, and then calling draw_geom to actually draw it.
var draw_geom = function(g_info, chunk, selector_name, PANEL){
g_info.tr.select("td.status").text("displayed");
var svg = SVGs[g_info.classed];
// derive the plot name from the geometry name
var g_names = g_info.classed.split("_");
var p_name = g_names[g_names.length - 1];
var scales = Plots[p_name].scales[PANEL];
var selected_arrays = [ [] ]; //double array necessary.
var has_clickSelects = g_info.aes.hasOwnProperty("clickSelects");
var has_clickSelects_variable =
g_info.aes.hasOwnProperty("clickSelects.variable");
g_info.subset_order.forEach(function (aes_name) {
var selected, values;
var new_arrays = [];
if(0 < aes_name.indexOf(".variable")){
selected_arrays.forEach(function(old_array){
var some_data = chunk;
old_array.forEach(function(value){
if(some_data.hasOwnProperty(value)) {
some_data = some_data[value];
} else {
some_data = {};
}
})
values = d3.keys(some_data);
values.forEach(function(s_name){
var selected = Selectors[s_name].selected;
var new_array = old_array.concat(s_name).concat(selected);
new_arrays.push(new_array);
})
})
}else{//not .variable aes:
if(aes_name == "PANEL"){
selected = PANEL;
}else{
var s_name = g_info.aes[aes_name];
selected = Selectors[s_name].selected;
}
if(isArray(selected)){
values = selected; //multiple selection.
}else{
values = [selected]; //single selection.
}
values.forEach(function(value){
selected_arrays.forEach(function(old_array){
var new_array = old_array.concat(value);
new_arrays.push(new_array);
})
})
}
selected_arrays = new_arrays;
});
// data can be either an array[] if it will be directly involved
// in a data-bind, or an object{} if it will be involved in a
// data-bind by group (e.g. geom_line).
var data;
if(g_info.data_is_object){
data = {};
}else{
data = [];
}
selected_arrays.forEach(function(value_array){
var some_data = chunk;
value_array.forEach(function(value){
if (some_data.hasOwnProperty(value)) {
some_data = some_data[value];
} else {
if(g_info.data_is_object){
some_data = {};
}else{
some_data = [];
}
}
});
if(g_info.data_is_object){
if(isArray(some_data) && some_data.length){
data["0"] = some_data;
}else{
for(k in some_data){
data[k] = some_data[k];
}
}
}else{//some_data is an array.
data = data.concat(some_data);
}
});
var aes = g_info.aes;
var toXY = function (xy, a) {
return function (d) {
return scales[xy](d[a]);
};
};
var layer_g_element = svg.select("g." + g_info.classed);
var panel_g_element = layer_g_element.select("g.PANEL" + PANEL);
var elements = panel_g_element.selectAll(".geom");
// helper functions so we can write code that works for both
// grouped and ungrouped geoms. get_one_row returns one row of
// data (not one group), in both cases.
var get_fun = function(fun){
return function(input){
var d = get_one_row(input);
return fun(d);
};
};
var get_attr = function(attr_name){
return get_fun(function(d){
return d[attr_name];
});
};
var size = 2;
var get_size;
if(aes.hasOwnProperty("size")){
get_size = get_attr("size");
}else{
get_size = function(d){
return size;
};
}
var get_style_on_stroke_width = get_size;
// stroke_width for geom_point
var stroke_width = 1; // by default ggplot2 has 0.5, animint has 1
var get_stroke_width;
if(aes.hasOwnProperty("stroke")){
get_stroke_width = get_attr("stroke");
}else{
get_stroke_width = function(d){
return stroke_width;
};
}
var linetype = "solid";
var get_linetype;
if(aes.hasOwnProperty("linetype")){
get_linetype = get_attr("linetype");
}else{
get_linetype = function(d){
return linetype;
};
}
var get_dasharray = function(d){
var lt = get_linetype(d);
return linetypesize2dasharray(lt, get_size(d));
};
var alpha = 1, alpha_off = 0.5;
var get_alpha;
var get_alpha_off = function (d) {
return alpha_off;
};
if(aes.hasOwnProperty("alpha")){
get_alpha = get_attr("alpha");
get_alpha_off = get_attr("alpha");
} else {
get_alpha = function(d){
return alpha;
};
}
var colour = "black", colour_off;
var get_colour;
var get_colour_off = function (d) {
return colour_off;
};
if(aes.hasOwnProperty("colour")){
get_colour = get_attr("colour");
get_colour_off = get_colour;
}else{
get_colour = function (d) {
return colour;
};
}
var get_colour_off_default = get_colour;
var fill = "black", fill_off = "black";
var get_fill = function (d) {
return fill;
};
var get_fill_off = function (d) {
return fill_off;
};
var angle = 0;
var get_angle;
if(aes.hasOwnProperty("angle")){
get_angle = get_attr("angle");
}else{
get_angle = function(d){
return angle;
};
}
var get_rotate = function(d){
// x and y are the coordinates to rotate around, we choose the center
// point of the text because otherwise it will rotate around (0,0) of its
// coordinate system, which is the top left of the plot
x = scales["x"](d["x"]);
y = scales["y"](d["y"]);
var angle = get_angle(d);
// ggplot expects angles to be in degrees CCW, SVG uses degrees CW, so
// we negate the angle.
return `rotate(${-angle}, ${x}, ${y})`;
};
// For aes(hjust) the compiler should make an "anchor" column.
var text_anchor = "middle";
var get_text_anchor;
if(g_info.aes.hasOwnProperty("hjust")) {
get_text_anchor = function(d){
return d["anchor"];
}
}else{
get_text_anchor = function(d){
return text_anchor;
}
}
var eActions, eAppend;
var key_fun = null;
if(g_info.aes.hasOwnProperty("key")){
key_fun = function(d){
return d.key;
};
}
var get_one_row;//different for grouped and ungrouped geoms.
var data_to_bind;
g_info.style_list = [
"opacity","stroke","stroke-width","stroke-dasharray","fill"];
var line_style_list = [
"opacity","stroke","stroke-width","stroke-dasharray"];
var fill_comes_from="fill", fill_off_comes_from="fill_off";
if(g_info.data_is_object) {
// Lines, paths, polygons, and ribbons are a bit special. For
// every unique value of the group variable, we take the
// corresponding data rows and make 1 path. The tricky part is
// that to use d3 I do a data-bind of some "fake" data which are
// just group ids, which is the kv variable in the code below
// // case of only 1 line and no groups.
// if(!aes.hasOwnProperty("group")){
// kv = [{"key":0,"value":0}];
// data = {0:data};
// }else{
// // we need to use a path for each group.
// var kv = d3.entries(d3.keys(data));
// kv = kv.map(function(d){
// d[aes.group] = d.value;
// return d;
// });
// }
// For an example consider breakpointError$error which is
// defined using this R code
// geom_line(aes(segments, error, group=bases.per.probe,
// clickSelects=bases.per.probe), data=only.error, lwd=4)
// Inside update_geom the variables take the following values
// (pseudo-Javascript code)
// var kv = [{"key":"0","value":"133","bases.per.probe":"133"},
// {"key":"1","value":"2667","bases.per.probe":"2667"}];
// var data = {"133":[array of 20 points used to draw the line for group 133],
// "2667":[array of 20 points used to draw the line for group 2667]};
// I do elements.data(kv) so that when I set the d attribute of
// each path, I need to select the correct group before
// returning anything.
// e.attr("d",function(group_info){
// var one_group = data[group_info.value];
// return lineThing(one_group);
// })
// To make color work I think you just have to select the group
// and take the color of the first element, e.g.
// .style("stroke",function(group_info){
// var one_group = data[group_info.value];
// var one_row = one_group[0];
// return get_color(one_row);
// }
// In order to get d3 lines to play nice, bind fake "data" (group
// id's) -- the kv variable. Then each separate object is plotted
// using path (case of only 1 thing and no groups).
// we need to use a path for each group.
var keyed_data = {}, one_group, group_id, k;
for(group_id in data){
one_group = data[group_id];
one_row = one_group[0];
if(one_row.hasOwnProperty("key")){
k = one_row.key;
}else{
k = group_id;
}
keyed_data[k] = one_group;
}
var kv_array = d3.entries(d3.keys(keyed_data));
var kv = kv_array.map(function (d) {
//d[aes.group] = d.value;
// Need to store the clickSelects value that will
// be passed to the selector when we click on this
// item.
d.clickSelects = keyed_data[d.value][0].clickSelects;
return d;
});
// line, path, and polygon use d3.svg.line(),
// ribbon uses d3.svg.area()
// we have to define lineThing accordingly.
if (g_info.geom == "ribbon") {
var lineThing = d3.svg.area()
.x(toXY("x", "x"))
.y(toXY("y", "ymax"))
.y0(toXY("y", "ymin"));
} else {
var lineThing = d3.svg.line()
.x(toXY("x", "x"))
.y(toXY("y", "y"));
}
if(["line","path"].includes(g_info.geom)){
fill = "none";
fill_off = "none";
}
// select the correct group before returning anything.
key_fun = function(group_info){
return group_info.value;
};
data_to_bind = kv;
get_one_row = function(group_info) {
var one_group = keyed_data[group_info.value];
var one_row = one_group[0];
return one_row;
};
eActions = function (e) {
e.attr("d", function (d) {
var one_group = keyed_data[d.value];
// filter NaN since they make the whole line disappear!
var no_na = one_group.filter(function(d){
if(g_info.geom == "ribbon"){
return !isNaN(d.x) && !isNaN(d.ymin) && !isNaN(d.ymax);
}else{
return !isNaN(d.x) && !isNaN(d.y);
}
});
return lineThing(no_na);
})
};
eAppend = "path";
}else{
get_one_row = function(d){
return d;
}
data_to_bind = data;
if (g_info.geom == "segment") {
g_info.style_list = line_style_list;
eActions = function (e) {
e.attr("x1", function (d) {
return scales.x(d["x"]);
})
.attr("x2", function (d) {
return scales.x(d["xend"]);
})
.attr("y1", function (d) {
return scales.y(d["y"]);
})
.attr("y2", function (d) {
return scales.y(d["yend"]);
})
};
eAppend = "line";
}
if (g_info.geom == "linerange") {
g_info.style_list = line_style_list;
eActions = function (e) {
e.attr("x1", function (d) {
return scales.x(d["x"]);
})
.attr("x2", function (d) {
return scales.x(d["x"]);
})
.attr("y1", function (d) {
return scales.y(d["ymax"]);
})
.attr("y2", function (d) {
return scales.y(d["ymin"]);
})
;
};
eAppend = "line";
}
if (g_info.geom == "vline") {
g_info.style_list = line_style_list;
eActions = function (e) {
e.attr("x1", toXY("x", "xintercept"))
.attr("x2", toXY("x", "xintercept"))
.attr("y1", scales.y.range()[0])
.attr("y2", scales.y.range()[1])
;
};
eAppend = "line";
}
if (g_info.geom == "hline") {
g_info.style_list = line_style_list;
eActions = function (e) {
e.attr("y1", toXY("y", "yintercept"))
.attr("y2", toXY("y", "yintercept"))
.attr("x1", scales.x.range()[0])
.attr("x2", scales.x.range()[1])
;
};
eAppend = "line";
}
if (g_info.geom == "text") {
size = 12;//default
get_colour = function(d){
return "none";
};
get_colour_off = function(d) {
return "none";
};
fill_comes_from = "colour";
fill_off_comes_from = "colour_off";
g_info.style_list = [
"opacity","fill"];
eActions = function (e) {
e.attr("x", toXY("x", "x"))
.attr("y", toXY("y", "y"))
.attr("font-size", get_size)
.style("text-anchor", get_text_anchor)
.attr("transform", get_rotate)
.text(function (d) {
return d.label;
})
;
};
eAppend = "text";
}
if (g_info.geom == "point") {
// point is special because it takes SVG fill from ggplot
// colour, if fill is not specified.
if(!(
g_info.params.hasOwnProperty("fill") ||
aes.hasOwnProperty("fill")
)){
fill_comes_from = "colour";
}
if(!g_info.params.hasOwnProperty("fill_off")){
fill_off_comes_from = "colour_off";
}
get_style_on_stroke_width = get_stroke_width;//not size.
eActions = function (e) {
e.attr("cx", toXY("x", "x"))
.attr("cy", toXY("y", "y"))
.attr("r", get_size)
;
};
eAppend = "circle";
}
var rect_geoms = ["tallrect","widerect","rect"];
if(rect_geoms.includes(g_info.geom)){
eAppend = "rect";
if (g_info.geom == "tallrect") {
eActions = function (e) {
e.attr("x", toXY("x", "xmin"))
.attr("width", function (d) {
return scales.x(d["xmax"]) - scales.x(d["xmin"]);
})
.attr("y", scales.y.range()[1])
.attr("height", scales.y.range()[0] - scales.y.range()[1])
;
};
}
if (g_info.geom == "widerect") {
eActions = function (e) {
e.attr("y", toXY("y", "ymax"))
.attr("height", function (d) {
return scales.y(d["ymin"]) - scales.y(d["ymax"]);
})
.attr("x", scales.x.range()[0])
.attr("width", scales.x.range()[1] - scales.x.range()[0])
;
};
}
if (g_info.geom == "rect") {
alpha_off = alpha;
colour_off = "transparent";
get_colour_off_default = get_colour_off;
eActions = function (e) {
e.attr("x", toXY("x", "xmin"))
.attr("width", function (d) {
return Math.abs(scales.x(d.xmax) - scales.x(d.xmin));
})
.attr("y", toXY("y", "ymax"))
.attr("height", function (d) {
return Math.abs(scales.y(d.ymin) - scales.y(d.ymax));
})
;
};
}
}
}
// set params after geom-specific code, because each geom may have
// a different default.
if (g_info.params.hasOwnProperty("stroke")) {
stroke_width = g_info.params.stroke;
}
if (g_info.params.hasOwnProperty("linetype")) {
linetype = g_info.params.linetype;
}
if(g_info.params.hasOwnProperty("alpha")){
alpha = g_info.params.alpha;
alpha_off = alpha - 0.5
}
if(g_info.params.hasOwnProperty("alpha_off")){
alpha_off = g_info.params.alpha_off;
}
if(g_info.params.hasOwnProperty("anchor")){
text_anchor = g_info.params["anchor"];
}
if(g_info.params.hasOwnProperty("colour")){
colour = g_info.params.colour;
}
if(g_info.params.hasOwnProperty("colour_off")){
colour_off = g_info.params.colour_off;
}else{
get_colour_off = get_colour_off_default;
}
if (g_info.params.hasOwnProperty("angle")) {
angle = g_info.params["angle"];
}
if (g_info.params.hasOwnProperty(fill_comes_from)) {
fill = g_info.params[fill_comes_from];
}
if (g_info.params.hasOwnProperty(fill_off_comes_from)) {
fill_off = g_info.params[fill_off_comes_from];
}else{
fill_off = fill;
}
if(aes.hasOwnProperty(fill_comes_from)){
get_fill = get_attr(fill_comes_from);
get_fill_off = get_attr(fill_comes_from);
};
if (g_info.params.hasOwnProperty("size")) {
size = g_info.params.size;
}
var styleActions = function(e){
g_info.style_list.forEach(function(s){
e.style(s, function(d) {
var style_on_fun = style_on_funs[s];
return style_on_fun(d);
});
});
};
var style_on_funs = {
"opacity": get_alpha,
"stroke": get_colour,
"fill": get_fill,
"stroke-width": get_style_on_stroke_width,
"stroke-dasharray": get_dasharray
};
var style_off_funs = {
"opacity": get_alpha_off,
"stroke": get_colour_off,
"fill": get_fill_off
};
// TODO cleanup.
var select_style_default = ["opacity","stroke","fill"];
g_info.select_style = select_style_default.filter(
X => g_info.style_list.includes(X));
var over_fun = function(e){
g_info.select_style.forEach(function(s){
e.style(s, function (d) {
return style_on_funs[s](d);
});
});
};
var out_fun = function(e){
g_info.select_style.forEach(function(s){
e.style(s, function (d) {
var select_on = style_on_funs[s](d);
var select_off = style_off_funs[s](d);
if(has_clickSelects){
return ifSelectedElse(
d.clickSelects,
g_info.aes.clickSelects,
select_on, select_off);
}else if(has_clickSelects_variable){
return ifSelectedElse(
d["clickSelects.value"],
d["clickSelects.variable"],
select_on, select_off);
}
});
});
};
elements = elements.data(data_to_bind, key_fun);
elements.exit().remove();
var enter = elements.enter();
if(g_info.aes.hasOwnProperty("href")){
enter = enter.append("svg:a")
.append("svg:"+eAppend);
}else{
enter = enter.append(eAppend)
.attr("class", "geom");
}
var moreActions = function(e){};
if (has_clickSelects || has_clickSelects_variable) {
moreActions = out_fun;
elements.call(out_fun)
.on("mouseover", function (d) {
d3.select(this).call(over_fun);
})
.on("mouseout", function (d) {
d3.select(this).call(out_fun);
})
;
if(has_clickSelects){
elements.on("click", function (d) {
var s_name = g_info.aes.clickSelects;
update_selector(s_name, d.clickSelects);
});
}else{
elements.on("click", function(d){
var s_name = d["clickSelects.variable"];
var s_value = d["clickSelects.value"];
update_selector(s_name, s_value);
});
}
}
// Set attributes of only the entering elements. This is needed to
// prevent things from flying around from the upper left when they
// enter the plot.
var doActions = function(e) {
eActions(e);
styleActions(e);
moreActions(e)
};
doActions(enter); // DO NOT DELETE!
var has_tooltip = g_info.aes.hasOwnProperty("tooltip");
if(has_clickSelects || has_tooltip || has_clickSelects_variable){
var text_fun;
if(has_tooltip){
text_fun = function(d){
return d.tooltip;
};
}else if(has_clickSelects){
text_fun = function(d){
var v_name = g_info.aes.clickSelects;
return v_name + " " + d.clickSelects;
};
}else{ //clickSelects_variable
text_fun = function(d){
return d["clickSelects.variable"] + " " + d["clickSelects.value"];
};
}
// if elements have an existing title, remove it.
elements.selectAll("title").remove();
elements.append("svg:title")
.text(get_fun(text_fun))
;
}
if(Selectors.hasOwnProperty(selector_name)){
var milliseconds = Selectors[selector_name].duration;
elements = elements.transition().duration(milliseconds);
}
if(g_info.aes.hasOwnProperty("id")){
elements.attr("id", get_attr("id"));
}
if(g_info.aes.hasOwnProperty("href")){
// elements are , children are e.g.
var linked_geoms = elements.select(eAppend);
doActions(linked_geoms);
elements.attr("xlink:href", get_attr("href"))
.attr("target", "_blank")
.attr("class", "geom");
}else{
// elements are e.g.
doActions(elements); // Set the attributes of all elements (enter/exit/stay)
}
};
var value_tostring = function(selected_values) {
//function that is helpful to change the format of the string
var selector_url="#"
for (var selc_var in selected_values){
if(selected_values.hasOwnProperty(selc_var)){
var values_str=selected_values[selc_var].join();
var sub_url=selc_var.concat("=","{",values_str,"}");
selector_url=selector_url.concat(sub_url);
}
}
var url_nohash=window.location.href.match(/(^[^#]*)/)[0];
selector_url=url_nohash.concat(selector_url);
return selector_url;
};
var get_values=function(){
// function that is useful to get the selected values
var selected_values={}
for(var s_name in Selectors){
var s_info=Selectors[s_name];
var initial_selections = [];
if(s_info.type==="single"){
initial_selections=[s_info.selected];
}
else{
for(var i in s_info.selected) {
initial_selections[i] = s_info.selected[i];
}
}
selected_values[s_name]=initial_selections;
}
return selected_values;
};
// update scales for the plots that have update_axes option in
// theme_animint
function update_scales(p_name, axes, v_name, value){
// Get pre-computed domain
var axis_domains = Plots[p_name]["axis_domains"];
if(!isArray(axes)){
axes = [axes];
}
if(axis_domains != null){
axes.forEach(function(xyaxis){
// For Each PANEL, update the axes
Plots[p_name].layout.PANEL.forEach(function(panel_i, i){
// Determine whether this panel has a scale or not
// If not we just update the scales according to the common
// scale and skip the updating of axis
var draw_axes = Plots[p_name].layout["AXIS_"+ xyaxis.toUpperCase()][i];
if(draw_axes){
var use_panel = panel_i;
}else{
var use_panel = Plots[p_name].layout.PANEL[0];
}
// We update the current selection of the plot every time
// and use it to index the correct domain
var curr_select = axis_domains[xyaxis].curr_select;
if(axis_domains[xyaxis].selectors.indexOf(v_name) > -1){
curr_select[v_name] = value;
var str = use_panel+".";
for(selec in curr_select){
str = str + curr_select[selec] + "_";
}
str = str.substring(0, str.length - 1); // Strip off trailing underscore
var use_domain = axis_domains[xyaxis]["domains"][str];
}
if(use_domain != null){
Plots[p_name]["scales"][panel_i][xyaxis].domain(use_domain);
var scales = Plots[p_name]["scales"][panel_i][xyaxis];
// major and minor grid lines as calculated in the compiler
var grid_vals = Plots[p_name]["axis_domains"][xyaxis]["grids"][str];
// Once scales are updated, update the axis ticks if needed
if(draw_axes){
// Tick values are same as major grid lines
update_axes(p_name, xyaxis, panel_i, grid_vals[1]);
}
// Update major and minor grid lines
update_grids(p_name, xyaxis, panel_i, grid_vals, scales);
}
});
});
}
}
// Update the axis ticks etc. once plot is zoomed in/out
// currently called from update_scales.
function update_axes(p_name, axes, panel_i, tick_vals){
var orientation;
if(axes == "x"){
orientation = "bottom";
}else{
orientation = "left";
}
if(!isArray(tick_vals)){
tick_vals = [tick_vals];
}
var xyaxis = d3.svg.axis()
.scale(Plots[p_name]["scales"][panel_i][axes])
.orient(orientation)
.tickValues(tick_vals);
// update existing axis
var xyaxis_g = element.select("#plot_"+p_name).select("."+axes+"axis_"+panel_i)
.transition()
.duration(1000)
.call(xyaxis);
}
// Update major/minor grids once axes ticks have been updated
function update_grids(p_name, axes, panel_i, grid_vals, scales){
// Select panel to update
var bgr = element.select("#plot_"+p_name).select(".bgr"+panel_i);
// Update major and minor grid lines
["minor", "major"].forEach(function(grid_class, j){
var lines = bgr.select(".grid_"+grid_class).select("."+axes);
var xy1, xy2;
if(axes == "x"){
xy1 = lines.select("line").attr("y1");
xy2 = lines.select("line").attr("y2");
}else{
xy1 = lines.select("line").attr("x1");
xy2 = lines.select("line").attr("x2");
}
// Get default values for grid lines like colour, stroke etc.
var grid_background = Plots[p_name]["grid_"+grid_class];
var col = grid_background.colour;
var lt = grid_background.linetype;
var size = grid_background.size;
var cap = grid_background.lineend;
// Remove old lines
lines.selectAll("line")
.remove();
if(!isArray(grid_vals[j])){
grid_vals[j] = [grid_vals[j]];
}
if(axes == "x"){
lines.selectAll("line")
.data(grid_vals[j])
.enter()
.append("line")
.attr("y1", xy1)
.attr("y2", xy2)
.attr("x1", function(d) { return scales(d); })
.attr("x2", function(d) { return scales(d); })
.style("stroke", col)
.style("stroke-linecap", cap)
.style("stroke-width", size)
.style("stroke-dasharray", function() {
return linetypesize2dasharray(lt, size);
});
}else{
lines.selectAll("line")
.data(grid_vals[j])
.enter()
.append("line")
.attr("x1", xy1)
.attr("x2", xy2)
.attr("y1", function(d) { return scales(d); })
.attr("y2", function(d) { return scales(d); })
.style("stroke", col)
.style("stroke-linecap", cap)
.style("stroke-width", size)
.style("stroke-dasharray", function() {
return linetypesize2dasharray(lt, size);
});
}
});
}
var update_selector = function (v_name, value) {
if(!Selectors.hasOwnProperty(v_name)){
return;
}
value = value + "";
var s_info = Selectors[v_name];
if(s_info.type == "single"){
// value is the new selection.
s_info.selected = value;
}else{
// value should be added or removed from the selection.
var i_value = s_info.selected.indexOf(value);
if(i_value == -1){
// not found, add to selection.
s_info.selected.push(value);
}else{
// found, remove from selection.
s_info.selected.splice(i_value, 1);
}
}
// update_selector_url()
// if there are levels, then there is a selectize widget which
// should be updated.
if(isArray(s_info.levels)){
// the jquery ids
if(s_info.type == "single") {
var selected_ids = v_name.concat("___", value);
} else {
var selected_ids = [];
for(i in s_info.selected) {
selected_ids[i] = v_name.concat("___", s_info.selected[i]);
}
}
// from
// https://github.com/brianreavis/selectize.js/blob/master/docs/api.md:
// setValue(value, silent) If "silent" is truthy, no change
// event will be fired on the original input.
selectized_array[v_name].setValue(selected_ids, true);
}
// For each updated geom, check if the axes of the plot need to be
// updated and update them
s_info.update.forEach(function(g_name){
var plot_name = g_name.split("_").pop();
var axes = Plots[plot_name]["options"]["update_axes"];
if(axes != null){
update_scales(plot_name, axes, v_name, value);
}
});
update_legend_opacity(v_name);
s_info.update.forEach(function(g_name){
update_geom(g_name, v_name);
});
};
var ifSelectedElse = function (s_value, s_name, selected, not_selected) {
var is_selected;
var s_info = Selectors[s_name];
if(s_info.type == "single"){
is_selected = s_value == s_info.selected;
}else{
is_selected = s_info.selected.indexOf(s_value) != -1;
}
if(is_selected){
return selected;
} else {
return not_selected;
}
};
function update_next_animation(){
var values = d3.values(Animation.done_geoms);
if(d3.sum(values) == values.length){
// If the values in done_geoms are all 1, then we have loaded
// all of the animation-related chunks, and we can start
// playing the animation.
var v_name = Animation.variable;
var cur = Selectors[v_name].selected;
var next = Animation.next[cur];
update_selector(v_name, next);
}
}
// The main idea of how legends work:
// 1. In getLegend in animint.R I export the legend entries as a
// list of rows that can be used in a data() bind in D3.
// 2. Here in add_legend I create a