scripts/experiment-properties/graphing/d3-base-plots/scatter_plot.js
export {create_scatter_plot, plot_scatter_points};
var BUFFER = 60;
var RANGE_ADJUSTMENT = 15;
/**
* D3 code for setting up scatter plot chart area
*
* @param {object} attributes
*/
function create_scatter_plot(attributes) {
let dataset = attributes["dataset"];
let properties = attributes["graph_attributes"];
var window_height_multiplier = 0.65
var window_width_multiplier = 0.5
// numerosity experiments require a larger window size due to the nature of the task
if (properties.alternate_scaling) {
BUFFER = 150;
window_height_multiplier = 1.15;
window_width_multiplier = .75;
}
// Size of the graph
let height = window.innerHeight * window_height_multiplier;
let width = height * window_width_multiplier;
// Create scales:
// ** D3 creates a function that takes in input between [0, 100] and
// outputs between [0, width].
// Basically, domain = input, range = ouput.
let xscale = d3.scaleLinear()
.range([0, width - RANGE_ADJUSTMENT]);
let yscale = d3.scaleLinear()
.range([height/2, 0 + RANGE_ADJUSTMENT]);
if (properties.alternate_scaling) {
// Numerosity experiments do not scale the distribution
// coordinates down to the range of [0,1] so the
// call to .domain() in the scaleLinear() method is needed
// scale the domain down into the appropriate range
var x_max = properties.row;
var y_max = properties.col;
xscale = d3.scaleLinear()
.domain([0, x_max])
.range([0, width - RANGE_ADJUSTMENT]);
yscale = d3.scaleLinear()
.domain([0, y_max])
.range([height/2, 0 + RANGE_ADJUSTMENT]);
}
// Create axes:
let x_axis = d3.axisBottom()
.scale(xscale)
.tickSize([0]);
let y_axis = d3.axisLeft()
.scale(yscale)
.tickSize([0]);
// Append SVG into graph div
let chart = d3.select("#graph")
.append("svg")
.attr("width", width + BUFFER)
.attr("height", height)
.attr("style", `margin-right: ${width/2}; margin-top: 25vh; margin-left: ${BUFFER}`);
// Creating transform SVG elements + append to SVG:
let yAxisElements = chart.append("g")
.attr("transform", "translate(50, 10)")
.call(y_axis);
let xAxisTranslate = height/2 + 10;
let xAxisElements = chart.append("g")
.attr("transform", "translate(50, " + xAxisTranslate +")")
.call(x_axis)
// Check if is a mix subcondition
if (!isEmpty(properties["mix_by_attribute"])){
prepare_mix_graph(chart, xscale, yscale, dataset, properties);
} else if (!isEmpty(properties["mix_by_attribute_targeted"])) {
prepare_targeted_mix_graph(chart, xscale, yscale, dataset, properties);
} else {
plot_scatter_points(chart, xscale, yscale, dataset, properties);
}
// Set axis color
chart.selectAll("path")
.attr("stroke", properties["axis_color"]);
// Remove tick labels
chart.selectAll("text").remove();
}
/**
* Will plot points depending on what is defined as the mix attribute,
* and will sample (w/o replacement) from the dataset to display points
* with all defined values for the mix attribute.
*
* E.g. if properties["mix_by_attribute"] = {"point_size" = [1, 2, 3, 4]}
* and there are 100 points in the dataset, will plot 25 points with
* point_size = 1, 25 points with point_size = 2, etc...
*
* @param {object} chart
* @param {function} xscale
* @param {function} yscale
* @param {array} dataset ([x_value, y_value])
* @param {assoc. array} e.g. {"point_shape": "square", "point_size": 5 .... }
*/
function prepare_mix_graph(chart, xscale, yscale, dataset, properties) {
let mix_attrib = Object.keys(properties["mix_by_attribute"])[0];
let NUM_GROUPS = properties["mix_by_attribute"][mix_attrib].length;
let POINTS_PER_GROUP = dataset.length / NUM_GROUPS;
for (let i = 0; i < NUM_GROUPS; i++) {
let grouped_dataset = [];
for (let j = 0; j < POINTS_PER_GROUP; j++) {
let rand_index = Math.floor(Math.random() * dataset.length);
grouped_dataset.push(dataset[rand_index]);
// Remove from dataset
dataset.splice(rand_index, 1);
}
// Override the property if it is a mix attribute
properties[mix_attrib] = properties["mix_by_attribute"][mix_attrib][i];
// Plot this group of points with the ith value for the mix attribute
plot_scatter_points(chart, xscale, yscale, grouped_dataset, properties);
}
}
/**
* The same as prepare_mix_graph but now the first item in the mix by attributes array
* is treated as the target
* The user can specify how many target points there are
* The rest of the mix by attributes are treated as distractors
* and the remaining number of points is distributed evenly among them
*
* e.g. if if properties["mix_by_attribute_targeted"] = {"point_size" = [1, 2, 3, 4], num_target: 10}
* and there are 100 points in the dataset, will plot 10 points with
* point_size = 1, 30 points with point_size = 2, 3 and 4.
*
* @param {object} chart
* @param {function} xscale
* @param {function} yscale
* @param {array} dataset ([x_value, y_value])
* @param {assoc. array} e.g. {"point_shape": "square", "point_size": 5 .... }
*/
function prepare_targeted_mix_graph(chart, xscale, yscale, dataset, properties) {
let mix_attrib = Object.keys(properties["mix_by_attribute_targeted"])[0];
let POINTS_FOR_TARGET = properties["mix_by_attribute_targeted"]["num_target"];
let NUM_GROUPS = properties["mix_by_attribute_targeted"][mix_attrib].length;
if (POINTS_FOR_TARGET < 0) {
NUM_GROUPS = NUM_GROUPS - 1;
}
let POINTS_PER_GROUP = (dataset.length - POINTS_FOR_TARGET) / NUM_GROUPS;
for (let i = 0; i < NUM_GROUPS; i++) {
let grouped_dataset = [];
if (i == 0 && POINTS_FOR_TARGET == 0) {
continue;
}
if (i == 0) {
for (let h = 0; h < POINTS_FOR_TARGET; h++) {
let rand_index = Math.floor(Math.random() * dataset.length);
grouped_dataset.push(dataset[rand_index]);
// Remove from dataset
dataset.splice(rand_index, 1);
}
} else {
for (let j = 0; j < POINTS_PER_GROUP; j++) {
let rand_index = Math.floor(Math.random() * dataset.length);
grouped_dataset.push(dataset[rand_index]);
// Remove from dataset
dataset.splice(rand_index, 1);
}
}
// Override the property if it is a mix attribute
properties[mix_attrib] = properties["mix_by_attribute_targeted"][mix_attrib][i];
// Plot this group of points with the ith value for the mix attribute
plot_scatter_points(chart, xscale, yscale, grouped_dataset, properties);
}
}
/**
* D3 code for appending data to the graph depending on point shape type.
*
* @param {object} chart
* @param {function} xscale
* @param {function} yscale
* @param {array} data ([x_value, y_value])
* @param {assoc. array} properties e.g. {"point_shape": "square", "point_size": 5 .... }
*/
function plot_scatter_points(chart, xscale, yscale, data, properties) {
// var GRAPH_TYPES comes from /config/graphing-config.js
if (!GRAPH_TYPES["scatter"]["attributes"]["point_shape"]["valid_inputs"].includes(properties["point_shape"])){
throw Error("Point shape " + properties["point_shape"] + " is not a valid shape for graph type scatter.");
}
switch(properties["point_shape"]){
case "square":
chart.selectAll("square_data")
.data(data)
.enter()
.append("rect")
.attr("x", function (d){
return xscale(d[0]) + BUFFER;
})
.attr("y", function (d){
return yscale(d[1]);
})
.attr("width", properties["point_size"])
.attr("height", properties["point_size"])
.style('fill', properties["point_color"]);
break;
case "diamond":
chart.selectAll("square_data")
.data(data)
.enter()
.append("rect")
.attr("x", function (d){
return xscale(d[0]) + BUFFER;
})
.attr("y", function (d){
return yscale(d[1]);
})
.attr("width", properties["point_size"])
.attr("height", properties["point_size"])
.style('fill', properties["point_color"])
.attr('transform', function(d){
// Adapted from: https://stackoverflow.com/questions/44817414/rotate-svg-in-place-using-d3-js
var x1 = xscale(d[0]) + BUFFER + properties["point_size"]/2; //the center x about which you want to rotate
var y1 = yscale(d[1]) + properties["point_size"]/2; //the center y about which you want to rotate
return `rotate(45, ${x1}, ${y1})`; //rotate 180 degrees about x and y
});
break;
case "circle":
chart.selectAll("circle_data")
.data(data)
.enter()
.append("circle") // Creating the circles for each entry in data set
.attr("cx", function (d) { // d is a subarray of the dataset i.e coordinates [5, 20]
return xscale(d[0]) + BUFFER; // +60 is for buffer (points going -x, even if they are positive)
})
.attr("cy", function (d) {
return yscale(d[1]);
})
.attr("r", function () {
if (properties["convert_size_cm_to_pixel"]){
return (properties["point_size"] * properties["pixels_per_cm"]) / 2
}
return properties["point_size"]/2;
})
.attr("fill", properties["point_color"]);
break;
case "hollow_circle":
chart.selectAll("circle_data")
.data(data)
.enter()
.append("circle") // Creating the circles for each entry in data set
.attr("cx", function (d) { // d is a subarray of the dataset i.e coordinates [5, 20]
return xscale(d[0]) + BUFFER; // +60 is for buffer (points going -x, even if they are positive)
})
.attr("cy", function (d) {
return yscale(d[1]);
})
.attr("r", properties["point_size"]/2).style("fill", "none").style("stroke", properties["point_color"]).style("stroke-width", "3");
break;
case "bullseye_circle":
chart.selectAll("circle_data")
.data(data)
.enter()
.append("circle") // Creating the circles for each entry in data set
.attr("cx", function (d) { // d is a subarray of the dataset i.e coordinates [5, 20]
return xscale(d[0]) + BUFFER; // +60 is for buffer (points going -x, even if they are positive)
})
.attr("cy", function (d) {
return yscale(d[1]);
})
.attr("r", properties["point_size"]/4).style("fill", properties["point_color"]);
chart.selectAll("circle_data")
.data(data)
.enter()
.append("circle") // Creating the circles for each entry in data set
.attr("cx", function (d) { // d is a subarray of the dataset i.e coordinates [5, 20]
return xscale(d[0]) + BUFFER; // +60 is for buffer (points going -x, even if they are positive)
})
.attr("cy", function (d) {
return yscale(d[1]);
})
.attr("r", properties["point_size"]/2).style("fill", "none").style("stroke", properties["point_color"]).style("stroke-width", "1");
break;
case "thin_hollow_circle":
chart.selectAll("circle_data")
.data(data)
.enter()
.append("circle") // Creating the circles for each entry in data set
.attr("cx", function (d) { // d is a subarray of the dataset i.e coordinates [5, 20]
return xscale(d[0]) + BUFFER; // +60 is for buffer (points going -x, even if they are positive)
})
.attr("cy", function (d) {
return yscale(d[1]);
})
.attr("r", properties["point_size"]/2).style("fill", "none").style("stroke", properties["point_color"]).style("stroke-width", "1");
break;
}
}
// https://coderwall.com/p/_g3x9q/how-to-check-if-javascript-object-is-empty
function isEmpty(obj) {
for(var key in obj) {
if(obj.hasOwnProperty(key))
return false;
}
return true;
}