var svg_dx = 600,
    svg_dy = 350,
    sd_plot_dx = 300,
    sd_plot_dy = 350,
    margin_sd_plot = { 
        top: sd_plot_dy * 0.08,
        bottom: sd_plot_dy * 0.22,
        left: sd_plot_dx * 0.10,
        right: sd_plot_dx * 0.10 
    };

// vertical midline of jitterplot for x positioning
var jitter_plot_x_midline = sd_plot_dx + 50;

// track whether jitterplot is visible
var is_jitter_plot_visible = false;

// random number generator for jitter 
var xJitter = d3.randomUniform(-jitter_plot_x_midline * 0.10, 
                                jitter_plot_x_midline * 0.10);

// sd distance to be outlier
var sd_mult = 2.0;

var svg = d3.select("#block")
            .append("svg")
            .attr("height", svg_dy)
            .attr("width", svg_dx);

var sd_plot = svg.append("g")
                 .attr("id", "sd_plot");

var tooltip = d3.select("body")
                .append("div")	
                .attr("id", "tooltip")				
                .style("opacity", 0);

d3.csv("/data/mtcars_melted.csv", d => {

    // group melted data by variable
    var d_grouped = d3.nest()
                      .key(d => d.variable)
                      .entries(d);

    // stats by variable
    calcStatsByVar(d_grouped);

    // sort variables by max SD 
    sortBySd(d_grouped);

    // initially all variables in SD plot and not expanded to jitterplot
    d_grouped.forEach(v => v._jittered = false);

    // max and min SD dist for x-axis
    var sd_dist = {
        max : d3.max(d_grouped, d => d._extentSD[1]),
        min : d3.min(d_grouped, d => d._extentSD[0])
    };

    // SD plot x scale and axis
    var xScale = d3.scaleLinear()
                   .domain([sd_dist.min, sd_dist.max])
                   .range([margin_sd_plot.left, sd_plot_dx - margin_sd_plot.right]);

    var xAxis = d3.axisBottom(xScale);

    // SD plot y scale
    var yScale = d3.scalePoint()
                   .domain(d_grouped.map(d => d.key))
                   .range([margin_sd_plot.top, sd_plot_dy - margin_sd_plot.bottom]);


    // group elements by variable
    var vars = sd_plot.selectAll("g")
                      .data(d_grouped)
                      .enter()
                      .append("g")
                      .attr("class", "variable");

    // for each variable, plot rects for each datum
    // note: arrow function does not bind 'this'
    vars.each(function(v) {

        d3.select(this)
          .selectAll("rect")
          .data(v.values)
          .enter()
          .append("rect")
          .attr("x", d => xScale(+d._sdMult))
          .attr("y", () => yScale(v.key))
          .attr("width", 5)
          .attr("height", yScale.step() * 0.75)
          .attr("class", d => d._outlier ? "outlier rect_data" : "non-outlier rect_data")
          .on("mouseover", d => displayTooltip(d.model))
          .on("mouseout", hideTooltip);
     });

    // overlay grouping rect
    // note: rect needed because in Chrome event listeners to g element
    // are bound to constitutive elements and not entire g rect
    vars.append("rect")
        .attr("class", "rect_g")
        .attr("x", margin_sd_plot.left)
        .attr("y", v => yScale(v.key))
        .attr("width", sd_plot_dx - margin_sd_plot.right - margin_sd_plot.left)
        .attr("height", yScale.step() * 0.75)
        .on("mouseover", v => displayTooltip(v.key))
        .on("mousemove", v => displayTooltip(v.key))
        .on("mouseout", hideTooltip)
        .on("click", function(v) { 

            if (!is_jitter_plot_visible && v._jittered == false) {

                expandToJitterplot(this, v); 

                v._jittered = true;
                is_jitter_plot_visible = true;

            } else if (is_jitter_plot_visible && v._jittered == true) {

                collapseToSdPlot(this, v, xScale, yScale);

                v._jittered = false;
                is_jitter_plot_visible = false;

            } 
        });

    // plot x-axis
    sd_plot.append("g")
           .attr("id", "z_score_axis")
           .call(xAxis);

    // x-axis label
    d3.select("#z_score_axis")
      .append("text")
      .attr("id", "z_score_label")
      .text("z-score")
      .attr("transform", "translate(20, 15)");

    // plot SD line
    sd_plot.append("g")
       .attr("id", "SD_annotation")
       .attr("transform", "translate(" + xScale(sd_mult) + ",0)")
       .call(addLine, "SD_line", "SD_label", sd_mult + " SD");

    // plot mean line
    sd_plot.append("g")
       .attr("id", "mean_annotation")
       .attr("transform", "translate(" + xScale(0) + ",0)")
       .call(addLine, "mean_line", "mean_label", "mean");

});

function collapseToSdPlot(rect_g, d, xScale, yScale) {

    // remove toolTip
    hideTooltip();

    // revert data rects
    d3.select(rect_g.parentNode)
      .selectAll(".rect_data")
      .classed("jittered_d", false)
      .transition()
      .duration(500)
      .attr("x", d => xScale(+d._sdMult))
      .attr("y", () => yScale(d.key))
      .attr("width", 5)
      .attr("height", yScale.step() * 0.75)
      .attr("rx", 0)
      .attr("ry", 0);

    // revert grouping rect and re-apply tooltip
    d3.select(rect_g)
      .attr("x", margin_sd_plot.left)
      .attr("y", v => yScale(v.key))
      .attr("width", sd_plot_dx - margin_sd_plot.right - margin_sd_plot.left)
      .attr("height", yScale.step() * 0.75)
      .on("mouseover", v => displayTooltip(v.key))
      .on("mousemove", v => displayTooltip(v.key))
      .on("mouseout", hideTooltip);

    // raise rect to mask circle mouseover events
    d3.select(rect_g)
       .raise();

    // remove x-axis of jitterplot
    d3.select("#jitter_plot_axis")
      .remove();

    // remove jitterplot title
    d3.select("#jitter_plot_label")
      .remove();
    
}

function expandToJitterplot(rect_g, d) {

    // min and max of variable values
    var d_extent = d3.extent(d.values, d => +d.value);

    // extend range for aesthetics
    var d_extent_plus = {
        min : d_extent[0] - (d_extent[0] * 0.05),
        max : d_extent[1] + (d_extent[1] * 0.05),
    };

    // y scale for jitterplot
    var yScaleJittered = d3.scaleLinear()
                           .domain([d_extent_plus.min, d_extent_plus.max])
                           .range([svg_dy - margin_sd_plot.bottom, margin_sd_plot.top]);

    // y axis for jitterplot
    var yAxisJittered = d3.axisLeft(yScaleJittered);

    // remove tooltip of variable names
    hideTooltip();

    // circle dimensions
    var dim = 8; 

    // jitterplot transition
    d3.select(rect_g.parentNode)
      .selectAll(".rect_data")
      .classed("jittered_d", true)
      .transition()
      .duration(500)
      .attr("height", dim)
      .attr("width", dim)
      .attr("rx", dim)
      .attr("ry", dim)
      .attr("x", () => sd_plot_dx + 100 + xJitter())
      .attr("y", d => yScaleJittered(+d.value))
      .attr("transform", "translate(0," + -(dim / 2) + ")");  // centers rect  

    // transition grouping rect and remove its event listeners
    d3.select(rect_g)
      .classed("jittered", true)
      .attr("x", sd_plot_dx)
      .attr("y", 0)
      .attr("width", svg_dx - sd_plot_dx)
      .attr("height", svg_dy)
      .on("mouseover", null)
      .on("mousemove", null)
      .on("mouseout", null);

    // lower group rect to display circle mouseover events
    d3.select(rect_g)
       .lower();

    // add y-axis
    yAxisJitter = d3.select("svg")
                    .append("g")
                    .attr("id", "jitter_plot_axis")
                    .call(yAxisJittered);

    yAxisJitter.attr("transform", "translate(" + (sd_plot_dx + 50) + ", 0)");

    // add jitterplot title
    d3.select(rect_g.parentNode)
       .append("text")
       .attr("id", "jitter_plot_label")
       .text(d.key)
       .attr("transform", "translate(" + jitter_plot_x_midline + "," + 17 + ")");
}

function displayTooltip(d) {

    tooltip.html(d)	
           .style("left", (d3.event.pageX + 8) + "px")		
           .style("top", (d3.event.pageY - 20) + "px")
           .style("opacity", 0.9);
}

function hideTooltip() {

    tooltip.style("opacity", 0);	

}

function addLine(selection, line_ID, text_ID, text) {

    selection.append("line")
             .attr("class", "annotate_line")
             .attr("id", line_ID)
             .attr("x1", 0)
             .attr("y1", margin_sd_plot.top)
             .attr("x2", 0)
             .attr("y2", sd_plot_dy);

    selection.append("text")
             .text(text)
             .attr("class", "annotate_text")
             .attr("id", text_ID)
             .attr("transform", "translate(-4," + sd_plot_dy + ") rotate(270)");
}

function isOutlier(value, group) {

    var is_gt_eq_thes = value >= group._mean + (sd_mult * group._sd),
        is_lt_eq_thres = value <= group._mean - (sd_mult * group._sd);

    if (is_gt_eq_thes | is_lt_eq_thres) {
        return true;
    } else {
        return false;
    }
}

function calcStatsByVar(d_grouped) {

    // for each variable
    d_grouped.forEach(v => {

        // standard deviation
        v._sd = d3.deviation(v.values, d => +d.value);

        // mean
        v._mean = d3.mean(v.values, d => +d.value);

        // flag outliers
        v.values.forEach(d => isOutlier(+d.value, v) ? d._outlier = true : d._outlier = false);

        // flag variables having outlier
        v._hasOutlier = v.values.map(d => d._outlier).includes(true);

        // multiple of SD from mean (+ / -)
        v.values.forEach(d => d._sdMult = (d.value - v._mean) / v._sd)

        // min and max of SDs 
        v._extentSD = d3.extent(v.values, d => +d._sdMult);

    });
}

function sortBySd(d_grouped) {

    // sort groups by max SD multiple distance, in descending order
    d_grouped.sort((a, b) => d3.descending(a._extentSD[1], b._extentSD[1]));
}