Home Reference Source

scripts/experiment-properties/graphing/d3-custom-plots/estimation_plot.js

export {create_estimation_plot, 
        create_estimation_interference_plot, 
        create_estimation_multi_interference_plot,
        create_estimation_bisection_plot};
    
var exp;

/**
 * Plots a regular estimation condition
 *
 * @param {object} experiment
 *        {object} attributes
 */
function create_estimation_plot(experiment, attributes) {

    exp = experiment;

    let chart = d3.select("#graph")
        .append("svg")
        .attr("width", attributes.chart.width)
        .attr("height", attributes.chart.height)
        .attr("style", "display: block");

    let left = attributes.left_shape;
    let right = attributes.right_shape;

    plot_shape(left.shape, chart, left.size, left.y, left.x, left.is_ref, left.outline, left.fill, left.options);
    plot_shape(right.shape, chart, right.size, right.y, right.x, right.is_ref, right.outline, right.fill, right.options);

    if (experiment.condition_name === "absolute_area_ratio"){
        plot_text(chart, "The target area ratio is " + attributes.chart.target_area_ratio + ".")
    }
}

/**
 * Plots a bisection estimation condition
 * (e.g. conditions with "bisection" in their name)
 *
 * @param {object} experiment
 *        {object} attributes
 */
function create_estimation_bisection_plot(experiment, attributes) {

    exp = experiment;

    let chart = d3.select("#graph")
        .append("svg")
        .attr("width", attributes.chart.width)
        .attr("height", attributes.chart.height)
        .attr("style", "display: block");

    let left = attributes.left_shape;
    let right = attributes.right_shape;
    let middle = attributes.middle_shape;

    plot_shape(left.shape, chart, left.size, left.y, left.x, left.is_ref, left.outline, left.fill, left.options);
    plot_shape(middle.shape, chart, middle.size, middle.y, middle.x, middle.is_ref, middle.outline, middle.fill, middle.options);
    plot_shape(right.shape, chart, right.size, right.y, right.x, right.is_ref, right.outline, right.fill, right.options);
}

/**
 * Plots an interference estimation condition 
 * (e.g. conditions with "interference" in their name but are not multi)
 *
 * @param {object} experiment
 *        {object} attributes
 */
function create_estimation_interference_plot(experiment, attributes) {

    exp = experiment;

    let chart = d3.select("#graph")
    .append("svg")
    .attr("width", attributes.chart.width)
    .attr("height", attributes.chart.height)
    .attr("style", "display: block");

    let left = attributes.left_shape;
    let right = attributes.right_shape;
    let interf = attributes.interf_shape;

    // Plot interference shape
    if (interf) {
        plot_shape(interf.shape, chart, interf.size, interf.y, interf.x, interf.is_ref, interf.outline, interf.fill, interf.options);
    }

    // Plot main shapes
    plot_shape(left.shape, chart, left.size, left.y, left.x, left.is_ref, left.outline, left.fill, left.options);
    plot_shape(right.shape, chart, right.size, right.y, right.x, right.is_ref, right.outline, right.fill, right.options);
}

/**
 * Plots a multi interference estimation condition 
 * (e.g. conditions with "interference" and "multi" in their name)
 *
 * @param {object} experiment
 *        {object} attributes
 */
function create_estimation_multi_interference_plot(experiment, attributes) {

    exp = experiment;

    let chart = d3.select("#graph")
    .append("svg")
    .attr("width", attributes.chart.width)
    .attr("height", attributes.chart.height)
    .attr("style", "display: block");

    let left = attributes.left_shape;
    let right = attributes.right_shape;
    let ref_sub = attributes.ref_sub_shape;
    let mod = attributes.mod_shape;

    // These conditions need main to go first as the sub+mod are on top
    if (experiment.condition_name.split("_").includes("fan") || 
        experiment.condition_name === "multi_square_cutout_interference" ||
        experiment.condition_name === "multi_square_cutout_interference_flicker") {

        // Plot main shapes
        plot_shape(left.shape, chart, left.size, left.y, left.x, left.is_ref, left.outline, left.fill, left.options);
        plot_shape(right.shape, chart, right.size, right.y, right.x, right.is_ref, right.outline, right.fill, right.options);

        // Plot ref sub shape
        plot_shape(ref_sub.shape, chart, ref_sub.size, ref_sub.y, ref_sub.x, ref_sub.is_ref, ref_sub.outline, ref_sub.fill, ref_sub.options);

        // Plot mod shape
        plot_shape(mod.shape, chart, mod.size, mod.y, mod.x, mod.is_ref, mod.outline, mod.fill, mod.options);

    } else {

        // Plot ref sub shape
        plot_shape(ref_sub.shape, chart, ref_sub.size, ref_sub.y, ref_sub.x, ref_sub.is_ref, ref_sub.outline, ref_sub.fill, ref_sub.options);

        // Plot mod shape
        plot_shape(mod.shape, chart, mod.size, mod.y, mod.x, mod.is_ref, mod.outline, mod.fill, mod.options);

        // Plot main shapes
        plot_shape(left.shape, chart, left.size, left.y, left.x, left.is_ref, left.outline, left.fill, left.options);
        plot_shape(right.shape, chart, right.size, right.y, right.x, right.is_ref, right.outline, right.fill, right.options);
    }
}

/**
 * Routes to correct plotting code depending on shape type.
 *
 * @param shape {string}
 * @param chart {object}
 * @param length {number} 
 * @param y_pos {number}
 * @param x_pos {number}
 * @param is_ref {boolean} if the shape is a reference shape or a modifiable shape
 * @param outline {string} outline color
 * @param fill {string} fill color
 * @param options {string} can be: {"scaling": "scales_with_mod"   This shape serves as bg to the modifiable shape
                                                OR 
                                               "scales_indep"      This shape is a bg shape, but is the one being modified
                                               },

                                   {"fan-attributes": {"slice-alignment"         : "top" or "bottom",   Position of slice in pie
                                                       "angle-size"              : some_value,          Angle in degrees of the fan
                                                      },          

                                   {"flicker": {"on": on_duration,    The duration in ms for the shape to appear
                                                "off": off_duration}} The duration in ms for the shape to disappear
 */
function plot_shape(shape, chart, length, y_pos, x_pos, is_ref, outline, fill, options) {
    switch (shape) {
        case "circle":
            plot_circle(chart, length, y_pos, x_pos, is_ref, outline, fill, options);
            break;
        case "fan":

            // When mod_side_alignment = slice-bottom, reference circle must have a sliced out
            // section equivalent to modifiable fan. Here we just plot another fan over the 
            // reference circle.
            if (options["fan-attributes"]["ref_shape_adjusts"]) {
                plot_fan(chart, length, y_pos-length/4, x_pos, is_ref, fill, fill, options);
            }

            plot_fan(chart, length, y_pos, x_pos, is_ref, outline, fill, options);

            break;
        case "triangle":
            plot_triangle(chart, length, y_pos, x_pos, is_ref, outline, fill, options);
            break;
        case "square":
            plot_square(chart, length, y_pos, x_pos, is_ref, outline, fill, options);
            break;
        case "line":
            plot_line(chart, length, y_pos, x_pos, is_ref, outline, options);
            break;
        case "rectangle":
            plot_rectangle(chart, length, y_pos, x_pos, is_ref, outline, fill, options);
            break;
        default:
            throw Error(shape + " shape is not supported.");
    }
}

/**
 * Plots a circle.
 *
 * @param chart {object}
 * @param diameter {number}
 * @param y_pos {number}
 * @param x_pos {number}
 * @param is_ref {boolean} if the shape is a reference shape or a modifiable shape
 * @param outline {string}
 * @param fill {string}
 * @param options {string}
 */
function plot_circle(chart, diameter, y_pos, x_pos, is_ref, outline, fill, options) {

    let radius = diameter / 2;
    let shape = chart.append("circle")
                        .attr("cx", x_pos)
                        .attr("cy", y_pos)
                        .attr("r", diameter / 2)
                        .attr("id", function(){
                            if (options && options.scaling && options.scaling === "scales_with_mod") {
                                exp.interf_shape_variables = {x_pos: x_pos, y_pos: y_pos, diameter: diameter};
                                return "bound_circle_shape"
                            }
                            return "circle_shape"}
                        )
                        .attr("is_ref", is_ref)
                        .attr("fill", fill)
                        .attr("stroke", outline);
    if (is_ref === false) {
        d3.select("body")
            .on("keydown", function () {
                let event = d3.event;
                // console.log(event);
                if (event.key === "m" || event.key === "z") {
                    diameter = calculate_size_change(event.key, diameter, "circle");
                    radius = diameter / 2;
                    d3.select("#circle_shape")
                        .attr("r", radius);
                }
            });
    }

    if (options && options.flicker) {
        shape.call(flicker_shape, fill, outline, options.flicker.on, options.flicker.off);
    }
}

/**
 * Plots a fan.
 *
 * @param chart {object}
 * @param width {number}
 * @param y_pos {number}
 * @param x_pos {number}
 * @param is_ref {boolean} if the shape is a reference shape or a modifiable shape,
 *                         is_ref === true if the shape is a reference shape
 * @param outline {string}
 * @param fill {string}
 * @param options {object}
 */
function plot_fan(chart, diameter, y_pos, x_pos, is_ref, outline, fill, options) {

    if (!options["fan-attributes"]) {throw Error("Fan attributes are needed in options to plot a fan.")};

    let radius = diameter/2;
    let angle_size = options["fan-attributes"]["angle_size"];

    let arc = d3.arc()
                .innerRadius(0)
                .outerRadius(radius)
                .startAngle(0 * Math.PI/180)
                .endAngle(function(){
                    return angle_size * Math.PI/180;
                });

    let shape = chart.append("g")
                     .attr("transform", function() {
                        return "translate(" + x_pos + "," + y_pos + ")";
                      }) 
                     .append("path")
                     .attr("id", function(){
                        if (is_ref) {
                            return "fan_shape_ref";
                        } else {
                            return "fan_shape_mod";
                        }
                      })
                     .attr("d", arc)
                     .attr("fill", fill)
                     .attr("stroke", outline)
                     .attr("stroke-width", 1)
                     .attr("transform", "rotate(" + compute_angle_shift(angle_size, options["fan-attributes"]["slice-alignment"]) + ")");
        
    if (is_ref === false) {
        d3.select("body")
          .on("keydown", function () {
                let event = d3.event;
                if (event.key === "m" || event.key === "z") {
                    // Estimated size = angle units
                    angle_size = calculate_angle_change(event.key, angle_size, radius);

                    let changed_arc = d3.arc()
                                        .innerRadius(0)
                                        .outerRadius(radius)
                                        .startAngle(0 * Math.PI/180)
                                        .endAngle(function(){
                                            return angle_size * Math.PI/180;
                                        });

                    d3.selectAll("#fan_shape_mod")
                        .attr("d", changed_arc)
                        .attr("transform", "rotate(" + compute_angle_shift(angle_size, options["fan-attributes"]["slice-alignment"]) + ")");
                }
        });
    }

    if (options && options.flicker) {
        shape.call(flicker_shape, fill, outline, options.flicker.on, options.flicker.off);
    }
}

/**
 * Returns the amount of rotational shift in degrees to align the angle 
 * at the desired alignment.
 *  
 * @param {double}   angle - size of angle in degrees
 *        {string}   alignment - e.g. bottom or top of circle
 */
function compute_angle_shift(angle, alignment) {

    let shift = (-1)*angle/2; // Shifts it so slice is perfectly centered and at top of pie

    if (alignment === "bottom") {
        shift += 180;
    } else if (alignment !== "top") {
        throw Error(alignment + " alignment not supported in computing angle shift for fan shapes.");
    }

    return shift;
}

/**
 * On conditions square,circle interference and circle interference, will trigger
 * the underlying bound bg shape to scale with the mod shape
 *  
 * @param {double}     size_change
 */
function adjust_interference_shape(size_change) {
    let subcond = exp.curr_conditions_constants[exp.curr_condition_index];
    let ratio = subcond.interf_ratio;

    let y_translation;
    let x_translation;

    if (subcond.interf_shape) {

        switch (subcond.interf_shape) {

            case "circle" :

                d3.select("#bound_circle_shape")
                  .attr("r", size_change * ratio)
                break;

            case "square" :

                x_translation = exp.interf_shape_variables.x_pos - ((size_change * ratio) / 2);
                y_translation = exp.interf_shape_variables.y_pos - ((size_change * ratio) / 2);

                d3.select("#bound_square_shape")
                  .attr("width", (size_change * ratio))
                  .attr("height", (size_change * ratio))
                  .attr("x", x_translation)
                  .attr("y", y_translation);
                break;
        }
    }
}

/**
 * Plots a square.
 *
 * @param chart {object}
 * @param width {number}
 * @param y_pos {number}
 * @param x_pos {number}
 * @param is_ref {boolean} if the shape is a reference shape or a modifiable shape,
 *                         is_ref === true if the shape is a reference shape
 * @param outline {string}
 * @param fill {string}
 * @param options {object}
 */
function plot_square(chart, width, y_pos, x_pos, is_ref, outline, fill, options) {

    let subcond = exp.curr_conditions_constants[exp.curr_condition_index];

    let shape;

    if (options && options.cutout_radius) {

        let cutout = options.cutout_radius;

        // Need to shift by half length to match centering done by regular rect svgs
        x_pos -= width/2;
        y_pos -= width/2;

        let poly = [{"x":(x_pos+cutout), "y":(y_pos)}, //bottom left of cutout
                    {"x":(x_pos+cutout), "y":(y_pos+cutout)}, //bottom right of cutout
                    {"x":(x_pos), "y":(y_pos+cutout)}, //top right of cutout

                    {"x":(x_pos), "y":(y_pos+width)}, //bottom left corner
                    {"x":(x_pos + width), "y":(y_pos + width)}, //bottom right corner
                    {"x":(x_pos + width), "y":(y_pos)}, //top right corner
                    ];

        shape = chart.append("polygon")
                        .attr("points",function() {
                            return poly.map(function(d) { return [d.x, d.y].join(","); }).join(" ");})
                        .attr("fill", fill)
                        .attr("stroke", outline)
                        .attr("id", is_ref? "square_cutout_shape_ref" : "square_cutout_shape_mod");

    } else {

        shape = chart.append("rect")
                        .attr("id", function(){
                            if (options && options.scaling && options.scaling === "scales_with_mod") {
                                exp.interf_shape_variables = {x_pos: x_pos, y_pos: y_pos, width: width};
                                return "bound_square_shape";
                            }

                            if (is_ref) {
                                return "square_shape_ref";
                            } else {
                                return "square_shape_mod"; // if is an interf and is not ref, becomes a mod
                            }
                        })
                        .attr("x", x_pos - width / 2)
                        .attr("y", y_pos - width / 2) // the x and y core_attributes for square
                                                      // refers to the position of the upper left corner
                                                      // however x_pos and y_pos specifies the center of the shape
                        .attr("width", width)
                        .attr("height", width)
                        .attr("fill", fill)
                        .attr("stroke", outline);

        if (is_ref === true && exp.curr_trial_data.ref_rotate_by) {
            let transform = "rotate(";
            transform = transform + exp.curr_trial_data.ref_rotate_by.toString();
            transform = transform + " " + (x_pos - width).toString();
            transform = transform + " " + (y_pos - width).toString();
            transform = transform + ")";
            d3.select("#square_shape_ref").attr("transform", transform);
        }
        if (is_ref === false) {
            d3.select("body")
                .on("keydown", () => {
                    let event = d3.event;
                    if (event.key === "m" || event.key === "z") {
                        width = calculate_size_change(event.key, width, "square");
                        d3.select("#square_shape_mod")
                            .attr("width", width)
                            .attr("height", width);

                        // Need to adjust translation for when adjustable
                        // interf is overlapping.
                        if (options && options.scaling && options.scaling === "scales_indep"){

                            let x_translation;
                            let y_translation;

                            // If interf is larger than ref
                            if (subcond.mod_ratio > 1) {
                                x_translation = x_pos - ((width * subcond.mod_ratio) / 4);
                                y_translation = y_pos - ((width * subcond.mod_ratio) / 4);
                            } else { // If interf is smaller than ref (so behind + center of ref)

                                x_translation = x_pos - ((width * subcond.mod_ratio));
                                y_translation = y_pos - ((width * subcond.mod_ratio));
                            }
                            
                            d3.select("#square_shape_mod")
                                .attr("x", x_translation)
                                .attr("y", y_translation);
                        }
                        
                    }
                });
        }
    }

    if (options && options.flicker) {
        shape.call(flicker_shape, fill, outline, options.flicker.on, options.flicker.off);
    }

}

/**
 * Plots a triangle. 
 *
 * @param chart {object}
 * @param radius {number}
 * @param y_pos {number}
 * @param x_pos {number}
 * @param is_ref {boolean} if the shape is a reference shape or a modifiable shape,
 *                         is_ref === true if the shape is a reference shape
 * @param outline {string}
 * @param fill {string}
 * @param options {object}
 */
function plot_triangle(chart, radius, y_pos, x_pos, is_ref, outline, fill, options) {

    // for equilateral triangles, height = side * sqrt(3) / 2;
    let short_side = radius;
    let long_side = radius;
    let height = 0, width = 0;

    let poly = [];
    if (!is_ref) {
        if (exp.curr_trial_data.width_height_ratio) {
            long_side = short_side * exp.curr_trial_data.width_height_ratio;
            height = Math.sqrt(Math.pow(long_side, 2) - Math.pow(short_side / 2, 2));
            width = short_side;
            if (exp.curr_trial_data.mod_rotate_by) {
                poly = [
                    {"x":(0.5 * height + x_pos), "y":(y_pos)},
                    {"x":(-0.5 * height + x_pos), "y":(-0.5 * width + y_pos)},
                    {"x":(-0.5 * height + x_pos), "y":(0.5 * width + y_pos)}];
            } else {
                poly = [
                    {"x":(x_pos), "y":(-0.5 * height + y_pos)},
                    {"x":(-0.5 * width + x_pos), "y":(0.5 * height + y_pos)},
                    {"x":(0.5 * width + x_pos), "y":(0.5 * height + y_pos)}];
            }
        } else {
            height = radius * Math.sqrt(3)/2;
            poly = [
                {"x":x_pos, "y":(-0.5 * height + y_pos)},
                {"x":(-0.5 * radius + x_pos), "y":(0.5 * height + y_pos)},
                {"x":(0.5 * radius + x_pos), "y":(0.5 * height + y_pos)}];
        }
    } else {
        height = radius * Math.sqrt(3)/2;
        poly = [
            {"x":x_pos, "y":(-0.5 * height + y_pos)},
            {"x":(-0.5 * radius + x_pos), "y":(0.5 * height + y_pos)},
            {"x":(0.5 * radius + x_pos), "y":(0.5 * height + y_pos)}];
    }

    let shape = chart.append("polygon")
                        .attr("points",function() {
                            return poly.map(function(d) { return [d.x, d.y].join(","); }).join(" ");})
                        .attr("fill", fill)
                        .attr("stroke", outline)
                        .attr("id", is_ref? "triangle_shape_ref" : "triangle_shape_mod");

    if (is_ref === false) {
        d3.select("body")
            .on("keydown", function () {
                let event = d3.event;
                if (event.key === "m" || event.key === "z") {
                    // decide the amount of change;
                    radius = calculate_size_change(event.key, radius, "triangle");
                    // plot the changed shape
                    short_side = radius;
                    if (exp.curr_trial_data.width_height_ratio) {
                        long_side = short_side * exp.curr_trial_data.width_height_ratio;
                        height = Math.sqrt(Math.pow(long_side, 2) - Math.pow(short_side / 2, 2));
                        width = short_side;
                        if (exp.curr_trial_data.mod_rotate_by) {
                            poly = [
                                {"x":(0.5 * height + x_pos), "y":(y_pos)},
                                {"x":(-0.5 * height + x_pos), "y":(-0.5 * width + y_pos)},
                                {"x":(-0.5 * height + x_pos), "y":(0.5 * width + y_pos)}];
                        } else {
                            poly = [
                                {"x":(x_pos), "y":(-0.5 * height + y_pos)},
                                {"x":(-0.5 * width + x_pos), "y":(0.5 * height + y_pos)},
                                {"x":(0.5 * width + x_pos), "y":(0.5 * height + y_pos)}];
                        }
                    } else {
                        height = short_side * Math.sqrt(3)/2;
                        poly = [
                            {"x":x_pos, "y":(-0.5 * height + y_pos)},
                            {"x":(-0.5 * short_side + x_pos), "y":(0.5 * height + y_pos)},
                            {"x":(0.5 * short_side + x_pos), "y":(0.5 * height + y_pos)}];
                    }

                   chart.select("#triangle_shape_mod")
                        .attr("points",function() {
                            return poly.map(function(d) { return [d.x, d.y].join(","); }).join(" ");});

                   adjust_interference_shape(radius);

                }
            });
    }

    if (options && options.flicker) {
        shape.call(flicker_shape, fill, outline, options.flicker.on, options.flicker.off);
    }
}

/**
 * Plots a rectangle.
 *
 * @param chart {object}
 * @param size {number}
 * @param y_pos {number}
 * @param x_pos {number}
 * @param is_ref {boolean}
 * @param outline {string}
 * @param fill {string}
 * @param options {object}
 */
function plot_rectangle(chart, size, y_pos, x_pos, is_ref, outline, fill, options) {
    let short_side = size;
    let long_side = size;
    let height = 0, width = 0;
    if (exp.curr_trial_data.width_height_ratio) {
        long_side = short_side * exp.curr_trial_data.width_height_ratio;
    }
    width = short_side;
    height = long_side;
    let shape = chart.append("rect")
                    .attr("id", is_ref? "rect_shape_ref": "rect_shape_mod")
                    .attr("x", x_pos - width / 2)
                    .attr("y", y_pos - height / 2) // the x and y core_attributes for square
                    // refers to the position of the upper left corner
                    // however x_pos and y_pos specifies the center of the shape
                    .attr("width", width)
                    .attr("height", height)
                    .attr("fill", fill)
        .attr("stroke", outline);
    if (is_ref === false && exp.curr_trial_data.mod_rotate_by) {
        let transform = "rotate(";
        transform = transform + exp.curr_trial_data.mod_rotate_by.toString();
        transform = transform + " " + (x_pos).toString();
        transform = transform + " " + (y_pos).toString();
        transform = transform + ")";
        console.log(transform);
        d3.select("#rect_shape_mod").attr("transform", transform);
    }

    if (is_ref === false) {
        d3.select("body")
            .on("keydown", function () {
                let event = d3.event;
                if (event.key === "m" || event.key === "z") {
                    size = calculate_size_change(event.key, size, "rectangle");
                    let short_side = size;
                    let long_side = exp.curr_trial_data.width_height_ratio * short_side;
                    let new_width = 0, new_height = 0;
                    new_width = short_side;
                    new_height = long_side;
                    d3.select("#rect_shape_mod")
                        .attr("width", new_width)
                        .attr("height", new_height);
                }
            });
    }

    if (options && options.flicker) {
        shape.call(flicker_shape, fill, outline, options.flicker.on, options.flicker.off);
    }

}

/**
 * Plots a line.
 *
 * @param chart {object}
 * @param width {number}
 * @param y_pos {number}
 * @param x_pos {number}
 * @param is_ref {boolean}
 * @param outline
 * @param options {object}
 */
function plot_line(chart, width, y_pos, x_pos, is_ref, outline, options) {

    let x1, x2, y1, y2;
    if (!is_ref) {
        x1 = x_pos - (width / 2) * Math.sin(exp.curr_trial_data.mod_rotate_by * Math.PI / 180);
        x2 = x_pos + (width / 2) * Math.sin(exp.curr_trial_data.mod_rotate_by * Math.PI / 180);
        y1 = y_pos - (width / 2) * Math.cos(exp.curr_trial_data.mod_rotate_by * Math.PI / 180);
        y2 = y_pos + (width / 2) * Math.cos(exp.curr_trial_data.mod_rotate_by * Math.PI / 180);
    } else {
        x1 = x_pos - width / 2;
        x2 = x_pos + width / 2;
        y1 = y_pos;
        y2 = y_pos;
    }
    let shape = chart.append("line")
                        .style("stroke", outline)
                        .style("stroke-width", exp.curr_trial_data.stroke_width)
                        .attr("id", is_ref? "line_shape_ref": "line_shape_mod")
                        .attr("x1", x1)
                        .attr("x2", x2)
                        .attr("y1", y1)
                        .attr("y2", y2);
    if (is_ref === false) {
        d3.select("body")
            .on("keydown", function () {
                let event = d3.event;
                if (event.key === "m" || event.key === "z") {
                    width = calculate_size_change(event.key, width, "line");
                    if (!is_ref) {
                        x1 = x_pos - (width / 2) * Math.sin(exp.curr_trial_data.mod_rotate_by * Math.PI / 180);
                        x2 = x_pos + (width / 2) * Math.sin(exp.curr_trial_data.mod_rotate_by * Math.PI / 180);
                        y1 = y_pos - (width / 2) * Math.cos(exp.curr_trial_data.mod_rotate_by * Math.PI / 180);
                        y2 = y_pos + (width / 2) * Math.cos(exp.curr_trial_data.mod_rotate_by * Math.PI / 180);
                    } else {
                        x1 = x_pos - width / 2;
                        x2 = x_pos + width / 2;
                        y1 = y_pos;
                        y2 = y_pos;
                    }
                    d3.select("#line_shape_mod")
                        .attr("x1", x1)
                        .attr("x2", x2)
                        .attr("y1", y1)
                        .attr("y2", y2);
                }
            });
    }

    if (options && options.flicker) {
        shape.call(flicker_shape, "none", outline, options.flicker.on, options.flicker.off);
    }
}

/**
 * Plots text centered at the bottom of the page. 
 *
 * @param chart {object}
 *        text  {string}
 */
function plot_text(chart, text){

    chart.append("text")
        .attr("x", "50%")
        .attr("y", "95%")
        .attr("text-anchor", "middle")
        .attr("font-family", "sans-serif")
        .attr("font-size", "28px")
        .attr("fill", "black")
        .text(()=>{
            return text;
        });
}

/**
 * Flickers on and off the D3 selection.
 *
 * @param {object} selection
 *        {string} fill
 *        {string} stroke
 *        {double} on_duration - duration selection displays for in ms
 *        {double} off_duration - duration selection becomes invisible for in ms
 */
function flicker_shape(selection, fill, stroke, on_duration, off_duration) {

    setInterval(display_off, on_duration);
    setInterval(display_on, on_duration + off_duration);

    function display_off() {
        selection.attr("fill", "none")
                 .attr("stroke", "WHITE");
    }

    function display_on() {
        selection.attr("fill", fill)
                 .attr("stroke", stroke);
    }
}

/**
 * Calculates the next size and saves the data.
 *
 * @param {object}  event_key  m to increase the size and z to decrease the size
 * @param {double}  size the previous size of the shape
 * @param {string}  the type of shape
 *
 * @returns {number}  the new size in pixels
 */
function calculate_size_change(event_key, size, shape_type) {

    let sign = event_key === "m" ? 1 : -1;
    let change = Math.random() * exp.PIXEL_TO_CM * exp.MAX_STEP_SIZE;
    let new_radius = size + sign * change;
    let size_in_px = new_radius;
    let size_in_cm = new_radius / exp.PIXEL_TO_CM;

    exp.save_adjustment(change * sign / exp.PIXEL_TO_CM);
    exp.save_estimated_size(size_in_cm, "cm");

    let area = exp.compute_shape_area(shape_type, size_in_px);
    exp.save_estimated_area(area);

    return size_in_px;
}

/**
 * Calculates the next angle and saves the data.
 *
 * @param {object}  event_key  m to increase the size and z to decrease the size
 * @param {double}  previous angle
 * @param {double}  radius of the fan
 *
 * @returns {number}  the new angle in degrees
 */
function calculate_angle_change(event_key, angle, radius) {

    let current_constants = exp.curr_conditions_constants[exp.curr_condition_index];
    let max_step_size = current_constants.max_step_size;

    let sign = event_key === "m" ? 1 : -1;
    let change = Math.random() * max_step_size;
    let diff_angle = sign * change;

    let new_angle = angle + diff_angle;

    exp.save_adjustment(diff_angle);
    exp.save_estimated_size(new_angle, "angle");

    let area = exp.compute_fan_area(new_angle, radius);
    exp.save_estimated_area(area);

    return new_angle;
}