/** * @fileOverview A D3 based distribution chart system. Supports: Box plots, Violin plots, Notched box plots, trend lines, beeswarm plot * @version 3.0 */ /** * Creates a box plot, violin plot, and or notched box plot * @param settings Configuration options for the base plot * @param settings.data The data for the plot * @param settings.xName The name of the column that should be used for the x groups * @param settings.yName The name of the column used for the y values * @param {string} settings.selector The selector string for the main chart div * @param [settings.axisLabels={}] Defaults to the xName and yName * @param [settings.yTicks = 1] 1 = default ticks. 2 = double, 0.5 = half * @param [settings.scale='linear'] 'linear' or 'log' - y scale of the chart * @param [settings.chartSize={width:800, height:400}] The height and width of the chart itself (doesn't include the container) * @param [settings.margin={top: 15, right: 60, bottom: 40, left: 50}] The margins around the chart (inside the main div) * @param [settings.constrainExtremes=false] Should the y scale include outliers? * @returns {object} chart A chart object */ function makeDistroChart(settings) { var chart = {}; // Defaults chart.settings = { data: null, xName: null, yName: null, selector: null, axisLables: null, yTicks: 1, scale: 'linear', chartSize: {width: 800, height: 400}, margin: {top: 15, right: 60, bottom: 40, left: 50}, constrainExtremes: false, color: d3.scale.category10() }; for (var setting in settings) { chart.settings[setting] = settings[setting] } function formatAsFloat(d) { if (d % 1 !== 0) { return d3.format(".2f")(d); } else { return d3.format(".0f")(d); } } function logFormatNumber(d) { var x = Math.log(d) / Math.log(10) + 1e-6; return Math.abs(x - Math.floor(x)) < 0.6 ? formatAsFloat(d) : ""; } chart.yFormatter = formatAsFloat; chart.data = chart.settings.data; chart.groupObjs = {}; //The data organized by grouping and sorted as well as any metadata for the groups chart.objs = {mainDiv: null, chartDiv: null, g: null, xAxis: null, yAxis: null}; chart.colorFunct = null; /** * Takes an array, function, or object mapping and created a color function from it * @param {function|[]|object} colorOptions * @returns {function} Function to be used to determine chart colors */ function getColorFunct(colorOptions) { if (typeof colorOptions == 'function') { return colorOptions } else if (Array.isArray(colorOptions)) { // If an array is provided, map it to the domain var colorMap = {}, cColor = 0; for (var cName in chart.groupObjs) { colorMap[cName] = colorOptions[cColor]; cColor = (cColor + 1) % colorOptions.length; } return function (group) { return colorMap[group]; } } else if (typeof colorOptions == 'object') { // if an object is provided, assume it maps to the colors return function (group) { return colorOptions[group]; } } else { return d3.scale.category10(); } } /** * Takes a percentage as returns the values that correspond to that percentage of the group range witdh * @param objWidth Percentage of range band * @param gName The bin name to use to get the x shift * @returns {{left: null, right: null, middle: null}} */ function getObjWidth(objWidth, gName) { var objSize = {left: null, right: null, middle: null}; var width = chart.xScale.rangeBand() * (objWidth / 100); var padding = (chart.xScale.rangeBand() - width) / 2; var gShift = chart.xScale(gName); objSize.middle = chart.xScale.rangeBand() / 2 + gShift; objSize.left = padding + gShift; objSize.right = objSize.left + width; return objSize; } /** * Adds jitter to the scatter point plot * @param doJitter true or false, add jitter to the point * @param width percent of the range band to cover with the jitter * @returns {number} */ function addJitter(doJitter, width) { if (doJitter !== true || width == 0) { return 0 } return Math.floor(Math.random() * width) - width / 2; } function shallowCopy(oldObj) { var newObj = {}; for (var i in oldObj) { if (oldObj.hasOwnProperty(i)) { newObj[i] = oldObj[i]; } } return newObj; } /** * Closure that creates the tooltip hover function * @param groupName Name of the x group * @param metrics Object to use to get values for the group * @returns {Function} A function that provides the values for the tooltip */ function tooltipHover(groupName, metrics) { var tooltipString = "Group: " + groupName; tooltipString += "Max: " + formatAsFloat(metrics.max, 0.1); tooltipString += "Q3: " + formatAsFloat(metrics.quartile3); tooltipString += "Median: " + formatAsFloat(metrics.median); tooltipString += "Q1: " + formatAsFloat(metrics.quartile1); tooltipString += "Min: " + formatAsFloat(metrics.min); return function () { chart.objs.tooltip.transition().duration(200).style("opacity", 0.9); chart.objs.tooltip.html(tooltipString) }; } /** * Parse the data and calculates base values for the plots */ !function prepareData() { function calcMetrics(values) { var metrics = { //These are the original non�scaled values max: null, upperOuterFence: null, upperInnerFence: null, quartile3: null, median: null, mean: null, iqr: null, quartile1: null, lowerInnerFence: null, lowerOuterFence: null, min: null }; metrics.min = d3.min(values); metrics.quartile1 = d3.quantile(values, 0.25); metrics.median = d3.median(values); metrics.mean = d3.mean(values); metrics.quartile3 = d3.quantile(values, 0.75); metrics.max = d3.max(values); metrics.iqr = metrics.quartile3 - metrics.quartile1; //The inner fences are the closest value to the IQR without going past it (assumes sorted lists) var LIF = metrics.quartile1 - (1.5 * metrics.iqr); var UIF = metrics.quartile3 + (1.5 * metrics.iqr); for (var i = 0; i <= values.length; i++) { if (values[i] < LIF) { continue; } if (!metrics.lowerInnerFence && values[i] >= LIF) { metrics.lowerInnerFence = values[i]; continue; } if (values[i] > UIF) { metrics.upperInnerFence = values[i - 1]; break; } } metrics.lowerOuterFence = metrics.quartile1 - (3 * metrics.iqr); metrics.upperOuterFence = metrics.quartile3 + (3 * metrics.iqr); if (!metrics.lowerInnerFence) { metrics.lowerInnerFence = metrics.min; } if (!metrics.upperInnerFence) { metrics.upperInnerFence = metrics.max; } return metrics } var current_x = null; var current_y = null; var current_row; // Group the values for (current_row = 0; current_row < chart.data.length; current_row++) { current_x = chart.data[current_row][chart.settings.xName]; current_y = chart.data[current_row][chart.settings.yName]; if (chart.groupObjs.hasOwnProperty(current_x)) { chart.groupObjs[current_x].values.push(current_y); } else { chart.groupObjs[current_x] = {}; chart.groupObjs[current_x].values = [current_y]; } } for (var cName in chart.groupObjs) { chart.groupObjs[cName].values.sort(d3.ascending); chart.groupObjs[cName].metrics = {}; chart.groupObjs[cName].metrics = calcMetrics(chart.groupObjs[cName].values); } }(); /** * Prepare the chart settings and chart div and svg */ !function prepareSettings() { //Set base settings chart.margin = chart.settings.margin; chart.divWidth = chart.settings.chartSize.width; chart.divHeight = chart.settings.chartSize.height; chart.width = chart.divWidth - chart.margin.left - chart.margin.right; chart.height = chart.divHeight - chart.margin.top - chart.margin.bottom; if (chart.settings.axisLabels) { chart.xAxisLable = chart.settings.axisLabels.xAxis; chart.yAxisLable = chart.settings.axisLabels.yAxis; } else { chart.xAxisLable = chart.settings.xName; chart.yAxisLable = chart.settings.yName; } if (chart.settings.scale === 'log') { chart.yScale = d3.scale.log(); chart.yFormatter = logFormatNumber; } else { chart.yScale = d3.scale.linear(); } if (chart.settings.constrainExtremes === true) { var fences = []; for (var cName in chart.groupObjs) { fences.push(chart.groupObjs[cName].metrics.lowerInnerFence); fences.push(chart.groupObjs[cName].metrics.upperInnerFence); } chart.range = d3.extent(fences); } else { chart.range = d3.extent(chart.data, function (d) {return d[chart.settings.yName];}); } chart.colorFunct = getColorFunct(chart.settings.colors); // Build Scale functions chart.yScale.range([chart.height, 0]).domain(chart.range).nice().clamp(true); chart.xScale = d3.scale.ordinal().domain(Object.keys(chart.groupObjs)).rangeBands([0, chart.width]); //Build Axes Functions chart.objs.yAxis = d3.svg.axis() .scale(chart.yScale) .orient("left") .tickFormat(chart.yFormatter) .outerTickSize(0) .innerTickSize(-chart.width + (chart.margin.right + chart.margin.left)); chart.objs.yAxis.ticks(chart.objs.yAxis.ticks()*chart.settings.yTicks); chart.objs.xAxis = d3.svg.axis().scale(chart.xScale).orient("bottom").tickSize(5); }(); /** * Updates the chart based on the current settings and window size * @returns {*} */ chart.update = function () { // Update chart size based on view port size chart.width = parseInt(chart.objs.chartDiv.style("width"), 10) - (chart.margin.left + chart.margin.right); chart.height = parseInt(chart.objs.chartDiv.style("height"), 10) - (chart.margin.top + chart.margin.bottom); // Update scale functions chart.xScale.rangeBands([0, chart.width]); chart.yScale.range([chart.height, 0]); // Update the yDomain if the Violin plot clamp is set to -1 meaning it will extend the violins to make nice points if (chart.violinPlots && chart.violinPlots.options.show == true && chart.violinPlots.options._yDomainVP != null) { chart.yScale.domain(chart.violinPlots.options._yDomainVP).nice().clamp(true); } else { chart.yScale.domain(chart.range).nice().clamp(true); } //Update axes chart.objs.g.select('.x.axis').attr("transform", "translate(0," + chart.height + ")").call(chart.objs.xAxis) .selectAll("text") .attr("y", 5) .attr("x", -5) .attr("transform", "rotate(-45)") .style("text-anchor", "end"); chart.objs.g.select('.x.axis .label').attr("x", chart.width / 2); chart.objs.g.select('.y.axis').call(chart.objs.yAxis.innerTickSize(-chart.width)); chart.objs.g.select('.y.axis .label').attr("x", -chart.height / 2); chart.objs.chartDiv.select('svg').attr("width", chart.width + (chart.margin.left + chart.margin.right)).attr("height", chart.height + (chart.margin.top + chart.margin.bottom)); return chart; }; /** * Prepare the chart html elements */ !function prepareChart() { // Build main div and chart div chart.objs.mainDiv = d3.select(chart.settings.selector) .style("max-width", chart.divWidth + "px"); // Add all the divs to make it centered and responsive chart.objs.mainDiv.append("div") .attr("class", "inner-wrapper") .style("padding-bottom", (chart.divHeight / chart.divWidth) * 100 + "%") .append("div").attr("class", "outer-box") .append("div").attr("class", "inner-box"); // Capture the inner div for the chart (where the chart actually is) chart.selector = chart.settings.selector + " .inner-box"; chart.objs.chartDiv = d3.select(chart.selector); d3.select(window).on('resize.' + chart.selector, chart.update); // Create the svg chart.objs.g = chart.objs.chartDiv.append("svg") .attr("class", "chart-area") .attr("width", chart.width + (chart.margin.left + chart.margin.right)) .attr("height", chart.height + (chart.margin.top + chart.margin.bottom)) .append("g") .attr("transform", "translate(" + chart.margin.left + "," + chart.margin.top + ")"); // Create axes chart.objs.axes = chart.objs.g.append("g").attr("class", "axis"); chart.objs.axes.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + chart.height + ")") .call(chart.objs.xAxis); chart.objs.axes.append("g") .attr("class", "y axis") .call(chart.objs.yAxis) .append("text") .attr("class", "label") .attr("transform", "rotate(-90)") .attr("y", -42) .attr("x", -chart.height / 2) .attr("dy", ".71em") .style("text-anchor", "middle") .text(chart.yAxisLable); // Create tooltip div chart.objs.tooltip = chart.objs.mainDiv.append('div').attr('class', 'tooltip'); for (var cName in chart.groupObjs) { chart.groupObjs[cName].g = chart.objs.g.append("g").attr("class", "group"); chart.groupObjs[cName].g.on("mouseover", function () { chart.objs.tooltip .style("display", null) .style("left", (d3.event.pageX) + "px") .style("top", (d3.event.pageY - 28) + "px"); }).on("mouseout", function () { chart.objs.tooltip.style("display", "none"); }).on("mousemove", tooltipHover(cName, chart.groupObjs[cName].metrics)) } chart.update(); }(); /** * Render a violin plot on the current chart * @param options * @param [options.showViolinPlot=true] True or False, show the violin plot * @param [options.resolution=100 default] * @param [options.bandwidth=10 default] May need higher bandwidth for larger data sets * @param [options.width=50] The max percent of the group rangeBand that the violin can be * @param [options.interpolation=''] How to render the violin * @param [options.clamp=0 default] * 0 = keep data within chart min and max, clamp once data = 0. May extend beyond data set min and max * 1 = clamp at min and max of data set. Possibly no tails * -1 = extend chart axis to make room for data to interpolate to 0. May extend axis and data set min and max * @param [options.colors=chart default] The color mapping for the violin plot * @returns {*} The chart object */ chart.renderViolinPlot = function (options) { chart.violinPlots = {}; var defaultOptions = { show: true, showViolinPlot: true, resolution: 100, bandwidth: 20, width: 50, interpolation: 'cardinal', clamp: 1, colors: chart.colorFunct, _yDomainVP: null // If the Violin plot is set to close all violin plots, it may need to extend the domain, that extended domain is stored here }; chart.violinPlots.options = shallowCopy(defaultOptions); for (var option in options) { chart.violinPlots.options[option] = options[option] } var vOpts = chart.violinPlots.options; // Create violin plot objects for (var cName in chart.groupObjs) { chart.groupObjs[cName].violin = {}; chart.groupObjs[cName].violin.objs = {}; } /** * Take a new set of options and redraw the violin * @param updateOptions */ chart.violinPlots.change = function (updateOptions) { if (updateOptions) { for (var key in updateOptions) { vOpts[key] = updateOptions[key] } } for (var cName in chart.groupObjs) { chart.groupObjs[cName].violin.objs.g.remove() } chart.violinPlots.prepareViolin(); chart.violinPlots.update(); }; chart.violinPlots.reset = function () { chart.violinPlots.change(defaultOptions) }; chart.violinPlots.show = function (opts) { if (opts !== undefined) { opts.show = true; if (opts.reset) { chart.violinPlots.reset() } } else { opts = {show: true}; } chart.violinPlots.change(opts); }; chart.violinPlots.hide = function (opts) { if (opts !== undefined) { opts.show = false; if (opts.reset) { chart.violinPlots.reset() } } else { opts = {show: false}; } chart.violinPlots.change(opts); }; /** * Update the violin obj values */ chart.violinPlots.update = function () { var cName, cViolinPlot; for (cName in chart.groupObjs) { cViolinPlot = chart.groupObjs[cName].violin; // Build the violins sideways, so use the yScale for the xScale and make a new yScale var xVScale = chart.yScale.copy(); // Create the Kernel Density Estimator Function cViolinPlot.kde = kernelDensityEstimator(eKernel(vOpts.bandwidth), xVScale.ticks(vOpts.resolution)); cViolinPlot.kdedata = cViolinPlot.kde(chart.groupObjs[cName].values); var interpolateMax = chart.groupObjs[cName].metrics.max, interpolateMin = chart.groupObjs[cName].metrics.min; if (vOpts.clamp == 0 || vOpts.clamp == -1) { // // When clamp is 0, calculate the min and max that is needed to bring the violin plot to a point // interpolateMax = the Minimum value greater than the max where y = 0 interpolateMax = d3.min(cViolinPlot.kdedata.filter(function (d) { return (d.x > chart.groupObjs[cName].metrics.max && d.y == 0) }), function (d) { return d.x; }); // interpolateMin = the Maximum value less than the min where y = 0 interpolateMin = d3.max(cViolinPlot.kdedata.filter(function (d) { return (d.x < chart.groupObjs[cName].metrics.min && d.y == 0) }), function (d) { return d.x; }); // If clamp is -1 we need to extend the axises so that the violins come to a point if (vOpts.clamp == -1) { kdeTester = eKernelTest(eKernel(vOpts.bandwidth), chart.groupObjs[cName].values); if (!interpolateMax) { var interMaxY = kdeTester(chart.groupObjs[cName].metrics.max); var interMaxX = chart.groupObjs[cName].metrics.max; var count = 25; // Arbitrary limit to make sure we don't get an infinite loop while (count > 0 && interMaxY != 0) { interMaxY = kdeTester(interMaxX); interMaxX += 1; count -= 1; } interpolateMax = interMaxX; } if (!interpolateMin) { var interMinY = kdeTester(chart.groupObjs[cName].metrics.min); var interMinX = chart.groupObjs[cName].metrics.min; var count = 25; // Arbitrary limit to make sure we don't get an infinite loop while (count > 0 && interMinY != 0) { interMinY = kdeTester(interMinX); interMinX -= 1; count -= 1; } interpolateMin = interMinX; } } // Check to see if the new values are outside the existing chart range // If they are assign them to the master _yDomainVP if (!vOpts._yDomainVP) vOpts._yDomainVP = chart.range.slice(0); if (interpolateMin && interpolateMin < vOpts._yDomainVP[0]) { vOpts._yDomainVP[0] = interpolateMin; } if (interpolateMax && interpolateMax > vOpts._yDomainVP[1]) { vOpts._yDomainVP[1] = interpolateMax; } } if (vOpts.showViolinPlot) { chart.update(); xVScale = chart.yScale.copy(); // Need to recalculate the KDE because the xVScale changed cViolinPlot.kde = kernelDensityEstimator(eKernel(vOpts.bandwidth), xVScale.ticks(vOpts.resolution)); cViolinPlot.kdedata = cViolinPlot.kde(chart.groupObjs[cName].values); } cViolinPlot.kdedata = cViolinPlot.kdedata .filter(function (d) { return (!interpolateMin || d.x >= interpolateMin) }) .filter(function (d) { return (!interpolateMax || d.x <= interpolateMax) }); } for (cName in chart.groupObjs) { cViolinPlot = chart.groupObjs[cName].violin; // Get the violin width var objBounds = getObjWidth(vOpts.width, cName); var width = (objBounds.right - objBounds.left) / 2; var yVScale = d3.scale.linear() .range([width, 0]) .domain([0, d3.max(cViolinPlot.kdedata, function (d) {return d.y;})]) .clamp(true); var area = d3.svg.area() .interpolate(vOpts.interpolation) .x(function (d) {return xVScale(d.x);}) .y0(width) .y1(function (d) {return yVScale(d.y);}); var line = d3.svg.line() .interpolate(vOpts.interpolation) .x(function (d) {return xVScale(d.x);}) .y(function (d) {return yVScale(d.y)}); if (cViolinPlot.objs.left.area) { cViolinPlot.objs.left.area .datum(cViolinPlot.kdedata) .attr("d", area); cViolinPlot.objs.left.line .datum(cViolinPlot.kdedata) .attr("d", line); cViolinPlot.objs.right.area .datum(cViolinPlot.kdedata) .attr("d", area); cViolinPlot.objs.right.line .datum(cViolinPlot.kdedata) .attr("d", line); } // Rotate the violins cViolinPlot.objs.left.g.attr("transform", "rotate(90,0,0) translate(0,-" + objBounds.left + ") scale(1,-1)"); cViolinPlot.objs.right.g.attr("transform", "rotate(90,0,0) translate(0,-" + objBounds.right + ")"); } }; /** * Create the svg elements for the violin plot */ chart.violinPlots.prepareViolin = function () { var cName, cViolinPlot; if (vOpts.colors) { chart.violinPlots.color = getColorFunct(vOpts.colors); } else { chart.violinPlots.color = chart.colorFunct } if (vOpts.show == false) {return} for (cName in chart.groupObjs) { cViolinPlot = chart.groupObjs[cName].violin; cViolinPlot.objs.g = chart.groupObjs[cName].g.append("g").attr("class", "violin-plot"); cViolinPlot.objs.left = {area: null, line: null, g: null}; cViolinPlot.objs.right = {area: null, line: null, g: null}; cViolinPlot.objs.left.g = cViolinPlot.objs.g.append("g"); cViolinPlot.objs.right.g = cViolinPlot.objs.g.append("g"); if (vOpts.showViolinPlot !== false) { //Area cViolinPlot.objs.left.area = cViolinPlot.objs.left.g.append("path") .attr("class", "area") .style("fill", chart.violinPlots.color(cName)); cViolinPlot.objs.right.area = cViolinPlot.objs.right.g.append("path") .attr("class", "area") .style("fill", chart.violinPlots.color(cName)); //Lines cViolinPlot.objs.left.line = cViolinPlot.objs.left.g.append("path") .attr("class", "line") .attr("fill", 'none') .style("stroke", chart.violinPlots.color(cName)); cViolinPlot.objs.right.line = cViolinPlot.objs.right.g.append("path") .attr("class", "line") .attr("fill", 'none') .style("stroke", chart.violinPlots.color(cName)); } } }; function kernelDensityEstimator(kernel, x) { return function (sample) { return x.map(function (x) { return {x:x, y:d3.mean(sample, function (v) {return kernel(x - v);})}; }); }; } function eKernel(scale) { return function (u) { return Math.abs(u /= scale) <= 1 ? .75 * (1 - u * u) / scale : 0; }; } // Used to find the roots for adjusting violin axis // Given an array, find the value for a single point, even if it is not in the domain function eKernelTest(kernel, array) { return function (testX) { return d3.mean(array, function (v) {return kernel(testX - v);}) } } chart.violinPlots.prepareViolin(); d3.select(window).on('resize.' + chart.selector + '.violinPlot', chart.violinPlots.update); chart.violinPlots.update(); return chart; }; /** * Render a box plot on the current chart * @param options * @param [options.show=true] Toggle the whole plot on and off * @param [options.showBox=true] Show the box part of the box plot * @param [options.showWhiskers=true] Show the whiskers * @param [options.showMedian=true] Show the median line * @param [options.showMean=false] Show the mean line * @param [options.medianCSize=3] The size of the circle on the median * @param [options.showOutliers=true] Plot outliers * @param [options.boxwidth=30] The max percent of the group rangeBand that the box can be * @param [options.lineWidth=boxWidth] The max percent of the group rangeBand that the line can be * @param [options.outlierScatter=false] Spread out the outliers so they don't all overlap (in development) * @param [options.outlierCSize=2] Size of the outliers * @param [options.colors=chart default] The color mapping for the box plot * @returns {*} The chart object */ chart.renderBoxPlot = function (options) { chart.boxPlots = {}; // Defaults var defaultOptions = { show: true, showBox: true, showWhiskers: true, showMedian: true, showMean: false, medianCSize: 3.5, showOutliers: true, boxWidth: 30, lineWidth: null, scatterOutliers: false, outlierCSize: 2.5, colors: chart.colorFunct }; chart.boxPlots.options = shallowCopy(defaultOptions); for (var option in options) { chart.boxPlots.options[option] = options[option] } var bOpts = chart.boxPlots.options; //Create box plot objects for (var cName in chart.groupObjs) { chart.groupObjs[cName].boxPlot = {}; chart.groupObjs[cName].boxPlot.objs = {}; } /** * Calculates all the outlier points for each group */ !function calcAllOutliers() { /** * Create lists of the outliers for each content group * @param cGroup The object to modify * @return null Modifies the object in place */ function calcOutliers(cGroup) { var cExtremes = []; var cOutliers = []; var cOut, idx; for (idx = 0; idx <= cGroup.values.length; idx++) { cOut = {value: cGroup.values[idx]}; if (cOut.value < cGroup.metrics.lowerInnerFence) { if (cOut.value < cGroup.metrics.lowerOuterFence) { cExtremes.push(cOut); } else { cOutliers.push(cOut); } } else if (cOut.value > cGroup.metrics.upperInnerFence) { if (cOut.value > cGroup.metrics.upperOuterFence) { cExtremes.push(cOut); } else { cOutliers.push(cOut); } } } cGroup.boxPlot.objs.outliers = cOutliers; cGroup.boxPlot.objs.extremes = cExtremes; } for (var cName in chart.groupObjs) { calcOutliers(chart.groupObjs[cName]); } }(); /** * Take updated options and redraw the box plot * @param updateOptions */ chart.boxPlots.change = function (updateOptions) { if (updateOptions) { for (var key in updateOptions) { bOpts[key] = updateOptions[key] } } for (var cName in chart.groupObjs) { chart.groupObjs[cName].boxPlot.objs.g.remove() } chart.boxPlots.prepareBoxPlot(); chart.boxPlots.update() }; chart.boxPlots.reset = function () { chart.boxPlots.change(defaultOptions) }; chart.boxPlots.show = function (opts) { if (opts !== undefined) { opts.show = true; if (opts.reset) { chart.boxPlots.reset() } } else { opts = {show: true}; } chart.boxPlots.change(opts) }; chart.boxPlots.hide = function (opts) { if (opts !== undefined) { opts.show = false; if (opts.reset) { chart.boxPlots.reset() } } else { opts = {show: false}; } chart.boxPlots.change(opts) }; /** * Update the box plot obj values */ chart.boxPlots.update = function () { var cName, cBoxPlot; for (cName in chart.groupObjs) { cBoxPlot = chart.groupObjs[cName].boxPlot; // Get the box width var objBounds = getObjWidth(bOpts.boxWidth, cName); var width = (objBounds.right - objBounds.left); var sMetrics = {}; //temp var for scaled (plottable) metric values for (var attr in chart.groupObjs[cName].metrics) { sMetrics[attr] = null; sMetrics[attr] = chart.yScale(chart.groupObjs[cName].metrics[attr]); } // Box if (cBoxPlot.objs.box) { cBoxPlot.objs.box .attr("x", objBounds.left) .attr('width', width) .attr("y", sMetrics.quartile3) .attr("rx", 1) .attr("ry", 1) .attr("height", -sMetrics.quartile3 + sMetrics.quartile1) } // Lines var lineBounds = null; if (bOpts.lineWidth) { lineBounds = getObjWidth(bOpts.lineWidth, cName) } else { lineBounds = objBounds } // --Whiskers if (cBoxPlot.objs.upperWhisker) { cBoxPlot.objs.upperWhisker.fence .attr("x1", lineBounds.left) .attr("x2", lineBounds.right) .attr('y1', sMetrics.upperInnerFence) .attr("y2", sMetrics.upperInnerFence); cBoxPlot.objs.upperWhisker.line .attr("x1", lineBounds.middle) .attr("x2", lineBounds.middle) .attr('y1', sMetrics.quartile3) .attr("y2", sMetrics.upperInnerFence); cBoxPlot.objs.lowerWhisker.fence .attr("x1", lineBounds.left) .attr("x2", lineBounds.right) .attr('y1', sMetrics.lowerInnerFence) .attr("y2", sMetrics.lowerInnerFence); cBoxPlot.objs.lowerWhisker.line .attr("x1", lineBounds.middle) .attr("x2", lineBounds.middle) .attr('y1', sMetrics.quartile1) .attr("y2", sMetrics.lowerInnerFence); } // --Median if (cBoxPlot.objs.median) { cBoxPlot.objs.median.line .attr("x1", lineBounds.left) .attr("x2", lineBounds.right) .attr('y1', sMetrics.median) .attr("y2", sMetrics.median); cBoxPlot.objs.median.circle .attr("cx", lineBounds.middle) .attr("cy", sMetrics.median) } // --Mean if (cBoxPlot.objs.mean) { cBoxPlot.objs.mean.line .attr("x1", lineBounds.left) .attr("x2", lineBounds.right) .attr('y1', sMetrics.mean) .attr("y2", sMetrics.mean); cBoxPlot.objs.mean.circle .attr("cx", lineBounds.middle) .attr("cy", sMetrics.mean); } // Outliers var pt; if (cBoxPlot.objs.outliers) { for (pt in cBoxPlot.objs.outliers) { cBoxPlot.objs.outliers[pt].point .attr("cx", objBounds.middle + addJitter(bOpts.scatterOutliers, width)) .attr("cy", chart.yScale(cBoxPlot.objs.outliers[pt].value)); } } if (cBoxPlot.objs.extremes) { for (pt in cBoxPlot.objs.extremes) { cBoxPlot.objs.extremes[pt].point .attr("cx", objBounds.middle + addJitter(bOpts.scatterOutliers, width)) .attr("cy", chart.yScale(cBoxPlot.objs.extremes[pt].value)); } } } }; /** * Create the svg elements for the box plot */ chart.boxPlots.prepareBoxPlot = function () { var cName, cBoxPlot; if (bOpts.colors) { chart.boxPlots.colorFunct = getColorFunct(bOpts.colors); } else { chart.boxPlots.colorFunct = chart.colorFunct } if (bOpts.show == false) { return } for (cName in chart.groupObjs) { cBoxPlot = chart.groupObjs[cName].boxPlot; cBoxPlot.objs.g = chart.groupObjs[cName].g.append("g").attr("class", "box-plot"); //Plot Box (default show) if (bOpts.showBox) { cBoxPlot.objs.box = cBoxPlot.objs.g.append("rect") .attr("class", "box") .style("fill", chart.boxPlots.colorFunct(cName)) .style("stroke", chart.boxPlots.colorFunct(cName)); //A stroke is added to the box with the group color, it is // hidden by default and can be shown through css with stroke-width } //Plot Median (default show) if (bOpts.showMedian) { cBoxPlot.objs.median = {line: null, circle: null}; cBoxPlot.objs.median.line = cBoxPlot.objs.g.append("line") .attr("class", "median"); cBoxPlot.objs.median.circle = cBoxPlot.objs.g.append("circle") .attr("class", "median") .attr('r', bOpts.medianCSize) .style("fill", chart.boxPlots.colorFunct(cName)); } // Plot Mean (default no plot) if (bOpts.showMean) { cBoxPlot.objs.mean = {line: null, circle: null}; cBoxPlot.objs.mean.line = cBoxPlot.objs.g.append("line") .attr("class", "mean"); cBoxPlot.objs.mean.circle = cBoxPlot.objs.g.append("circle") .attr("class", "mean") .attr('r', bOpts.medianCSize) .style("fill", chart.boxPlots.colorFunct(cName)); } // Plot Whiskers (default show) if (bOpts.showWhiskers) { cBoxPlot.objs.upperWhisker = {fence: null, line: null}; cBoxPlot.objs.lowerWhisker = {fence: null, line: null}; cBoxPlot.objs.upperWhisker.fence = cBoxPlot.objs.g.append("line") .attr("class", "upper whisker") .style("stroke", chart.boxPlots.colorFunct(cName)); cBoxPlot.objs.upperWhisker.line = cBoxPlot.objs.g.append("line") .attr("class", "upper whisker") .style("stroke", chart.boxPlots.colorFunct(cName)); cBoxPlot.objs.lowerWhisker.fence = cBoxPlot.objs.g.append("line") .attr("class", "lower whisker") .style("stroke", chart.boxPlots.colorFunct(cName)); cBoxPlot.objs.lowerWhisker.line = cBoxPlot.objs.g.append("line") .attr("class", "lower whisker") .style("stroke", chart.boxPlots.colorFunct(cName)); } // Plot outliers (default show) if (bOpts.showOutliers) { if (!cBoxPlot.objs.outliers) calcAllOutliers(); var pt; if (cBoxPlot.objs.outliers.length) { var outDiv = cBoxPlot.objs.g.append("g").attr("class", "boxplot outliers"); for (pt in cBoxPlot.objs.outliers) { cBoxPlot.objs.outliers[pt].point = outDiv.append("circle") .attr("class", "outlier") .attr('r', bOpts.outlierCSize) .style("fill", chart.boxPlots.colorFunct(cName)); } } if (cBoxPlot.objs.extremes.length) { var extDiv = cBoxPlot.objs.g.append("g").attr("class", "boxplot extremes"); for (pt in cBoxPlot.objs.extremes) { cBoxPlot.objs.extremes[pt].point = extDiv.append("circle") .attr("class", "extreme") .attr('r', bOpts.outlierCSize) .style("stroke", chart.boxPlots.colorFunct(cName)); } } } } }; chart.boxPlots.prepareBoxPlot(); d3.select(window).on('resize.' + chart.selector + '.boxPlot', chart.boxPlots.update); chart.boxPlots.update(); return chart; }; /** * Render a notched box on the current chart * @param options * @param [options.show=true] Toggle the whole plot on and off * @param [options.showNotchBox=true] Show the notch box * @param [options.showLines=false] Show lines at the confidence intervals * @param [options.boxWidth=35] The width of the widest part of the box * @param [options.medianWidth=20] The width of the narrowist part of the box * @param [options.lineWidth=50] The width of the confidence interval lines * @param [options.notchStyle=null] null=traditional style, 'box' cuts out the whole notch in right angles * @param [options.colors=chart default] The color mapping for the notch boxes * @returns {*} The chart object */ chart.renderNotchBoxes = function (options) { chart.notchBoxes = {}; //Defaults var defaultOptions = { show: true, showNotchBox: true, showLines: false, boxWidth: 35, medianWidth: 20, lineWidth: 50, notchStyle: null, colors: null }; chart.notchBoxes.options = shallowCopy(defaultOptions); for (var option in options) { chart.notchBoxes.options[option] = options[option] } var nOpts = chart.notchBoxes.options; //Create notch objects for (var cName in chart.groupObjs) { chart.groupObjs[cName].notchBox = {}; chart.groupObjs[cName].notchBox.objs = {}; } /** * Makes the svg path string for a notched box * @param cNotch Current notch box object * @param notchBounds objBound object * @returns {string} A string in the proper format for a svg polygon */ function makeNotchBox(cNotch, notchBounds) { var scaledValues = []; if (nOpts.notchStyle == 'box') { scaledValues = [ [notchBounds.boxLeft, chart.yScale(cNotch.metrics.quartile1)], [notchBounds.boxLeft, chart.yScale(cNotch.metrics.lowerNotch)], [notchBounds.medianLeft, chart.yScale(cNotch.metrics.lowerNotch)], [notchBounds.medianLeft, chart.yScale(cNotch.metrics.median)], [notchBounds.medianLeft, chart.yScale(cNotch.metrics.upperNotch)], [notchBounds.boxLeft, chart.yScale(cNotch.metrics.upperNotch)], [notchBounds.boxLeft, chart.yScale(cNotch.metrics.quartile3)], [notchBounds.boxRight, chart.yScale(cNotch.metrics.quartile3)], [notchBounds.boxRight, chart.yScale(cNotch.metrics.upperNotch)], [notchBounds.medianRight, chart.yScale(cNotch.metrics.upperNotch)], [notchBounds.medianRight, chart.yScale(cNotch.metrics.median)], [notchBounds.medianRight, chart.yScale(cNotch.metrics.lowerNotch)], [notchBounds.boxRight, chart.yScale(cNotch.metrics.lowerNotch)], [notchBounds.boxRight, chart.yScale(cNotch.metrics.quartile1)] ]; } else { scaledValues = [ [notchBounds.boxLeft, chart.yScale(cNotch.metrics.quartile1)], [notchBounds.boxLeft, chart.yScale(cNotch.metrics.lowerNotch)], [notchBounds.medianLeft, chart.yScale(cNotch.metrics.median)], [notchBounds.boxLeft, chart.yScale(cNotch.metrics.upperNotch)], [notchBounds.boxLeft, chart.yScale(cNotch.metrics.quartile3)], [notchBounds.boxRight, chart.yScale(cNotch.metrics.quartile3)], [notchBounds.boxRight, chart.yScale(cNotch.metrics.upperNotch)], [notchBounds.medianRight, chart.yScale(cNotch.metrics.median)], [notchBounds.boxRight, chart.yScale(cNotch.metrics.lowerNotch)], [notchBounds.boxRight, chart.yScale(cNotch.metrics.quartile1)] ]; } return scaledValues.map(function (d) { return [d[0], d[1]].join(","); }).join(" "); } /** * Calculate the confidence intervals */ !function calcNotches() { var cNotch, modifier; for (var cName in chart.groupObjs) { cNotch = chart.groupObjs[cName]; modifier = (1.57 * (cNotch.metrics.iqr / Math.sqrt(cNotch.values.length))); cNotch.metrics.upperNotch = cNotch.metrics.median + modifier; cNotch.metrics.lowerNotch = cNotch.metrics.median - modifier; } }(); /** * Take a new set of options and redraw the notch boxes * @param updateOptions */ chart.notchBoxes.change = function (updateOptions) { if (updateOptions) { for (var key in updateOptions) { nOpts[key] = updateOptions[key] } } for (var cName in chart.groupObjs) { chart.groupObjs[cName].notchBox.objs.g.remove() } chart.notchBoxes.prepareNotchBoxes(); chart.notchBoxes.update(); }; chart.notchBoxes.reset = function () { chart.notchBoxes.change(defaultOptions) }; chart.notchBoxes.show = function (opts) { if (opts !== undefined) { opts.show = true; if (opts.reset) { chart.notchBoxes.reset() } } else { opts = {show: true}; } chart.notchBoxes.change(opts) }; chart.notchBoxes.hide = function (opts) { if (opts !== undefined) { opts.show = false; if (opts.reset) { chart.notchBoxes.reset() } } else { opts = {show: false}; } chart.notchBoxes.change(opts) }; /** * Update the notch box obj values */ chart.notchBoxes.update = function () { var cName, cGroup; for (cName in chart.groupObjs) { cGroup = chart.groupObjs[cName]; // Get the box size var boxBounds = getObjWidth(nOpts.boxWidth, cName); var medianBounds = getObjWidth(nOpts.medianWidth, cName); var notchBounds = { boxLeft: boxBounds.left, boxRight: boxBounds.right, middle: boxBounds.middle, medianLeft: medianBounds.left, medianRight: medianBounds.right }; // Notch Box if (cGroup.notchBox.objs.notch) { cGroup.notchBox.objs.notch .attr("points", makeNotchBox(cGroup, notchBounds)); } if (cGroup.notchBox.objs.upperLine) { var lineBounds = null; if (nOpts.lineWidth) { lineBounds = getObjWidth(nOpts.lineWidth, cName) } else { lineBounds = objBounds } var confidenceLines = { upper: chart.yScale(cGroup.metrics.upperNotch), lower: chart.yScale(cGroup.metrics.lowerNotch) }; cGroup.notchBox.objs.upperLine .attr("x1", lineBounds.left) .attr("x2", lineBounds.right) .attr('y1', confidenceLines.upper) .attr("y2", confidenceLines.upper); cGroup.notchBox.objs.lowerLine .attr("x1", lineBounds.left) .attr("x2", lineBounds.right) .attr('y1', confidenceLines.lower) .attr("y2", confidenceLines.lower); } } }; /** * Create the svg elements for the notch boxes */ chart.notchBoxes.prepareNotchBoxes = function () { var cName, cNotch; if (nOpts && nOpts.colors) { chart.notchBoxes.colorFunct = getColorFunct(nOpts.colors); } else { chart.notchBoxes.colorFunct = chart.colorFunct } if (nOpts.show == false) { return } for (cName in chart.groupObjs) { cNotch = chart.groupObjs[cName].notchBox; cNotch.objs.g = chart.groupObjs[cName].g.append("g").attr("class", "notch-plot"); // Plot Box (default show) if (nOpts.showNotchBox) { cNotch.objs.notch = cNotch.objs.g.append("polygon") .attr("class", "notch") .style("fill", chart.notchBoxes.colorFunct(cName)) .style("stroke", chart.notchBoxes.colorFunct(cName)); //A stroke is added to the notch with the group color, it is // hidden by default and can be shown through css with stroke-width } //Plot Confidence Lines (default hide) if (nOpts.showLines) { cNotch.objs.upperLine = cNotch.objs.g.append("line") .attr("class", "upper confidence line") .style("stroke", chart.notchBoxes.colorFunct(cName)); cNotch.objs.lowerLine = cNotch.objs.g.append("line") .attr("class", "lower confidence line") .style("stroke", chart.notchBoxes.colorFunct(cName)); } } }; chart.notchBoxes.prepareNotchBoxes(); d3.select(window).on('resize.' + chart.selector + '.notchBox', chart.notchBoxes.update); chart.notchBoxes.update(); return chart; }; /** * Render a raw data in various forms * @param options * @param [options.show=true] Toggle the whole plot on and off * @param [options.showPlot=false] True or false, show points * @param [options.plotType='none'] Options: no scatter = (false or 'none'); scatter points= (true or [amount=% of width (default=10)]); beeswarm points = ('beeswarm') * @param [options.pointSize=6] Diameter of the circle in pizels (not the radius) * @param [options.showLines=['median']] Can equal any of the metrics lines * @param [options.showbeanLines=false] Options: no lines = false * @param [options.beanWidth=20] % width * @param [options.colors=chart default] * @returns {*} The chart object * */ chart.renderDataPlots = function (options) { chart.dataPlots = {}; //Defaults var defaultOptions = { show: true, showPlot: false, plotType: 'none', pointSize: 6, showLines: false,//['median'], showBeanLines: false, beanWidth: 20, colors: null }; chart.dataPlots.options = shallowCopy(defaultOptions); for (var option in options) { chart.dataPlots.options[option] = options[option] } var dOpts = chart.dataPlots.options; //Create notch objects for (var cName in chart.groupObjs) { chart.groupObjs[cName].dataPlots = {}; chart.groupObjs[cName].dataPlots.objs = {}; } // The lines don't fit into a group bucket so they live under the dataPlot object chart.dataPlots.objs = {}; /** * Take updated options and redraw the data plots * @param updateOptions */ chart.dataPlots.change = function (updateOptions) { if (updateOptions) { for (var key in updateOptions) { dOpts[key] = updateOptions[key] } } chart.dataPlots.objs.g.remove(); for (var cName in chart.groupObjs) { chart.groupObjs[cName].dataPlots.objs.g.remove() } chart.dataPlots.preparePlots(); chart.dataPlots.update() }; chart.dataPlots.reset = function () { chart.dataPlots.change(defaultOptions) }; chart.dataPlots.show = function (opts) { if (opts !== undefined) { opts.show = true; if (opts.reset) { chart.dataPlots.reset() } } else { opts = {show: true}; } chart.dataPlots.change(opts) }; chart.dataPlots.hide = function (opts) { if (opts !== undefined) { opts.show = false; if (opts.reset) { chart.dataPlots.reset() } } else { opts = {show: false}; } chart.dataPlots.change(opts) }; /** * Update the data plot obj values */ chart.dataPlots.update = function () { var cName, cGroup, cPlot; // Metrics lines if (chart.dataPlots.objs.g) { var halfBand = chart.xScale.rangeBand() / 2; // find the middle of each band for (var cMetric in chart.dataPlots.objs.lines) { chart.dataPlots.objs.lines[cMetric].line .x(function (d) { return chart.xScale(d.x) + halfBand }); chart.dataPlots.objs.lines[cMetric].g .datum(chart.dataPlots.objs.lines[cMetric].values) .attr('d', chart.dataPlots.objs.lines[cMetric].line); } } for (cName in chart.groupObjs) { cGroup = chart.groupObjs[cName]; cPlot = cGroup.dataPlots; if (cPlot.objs.points) { if (dOpts.plotType == 'beeswarm') { var swarmBounds = getObjWidth(100, cName); var yPtScale = chart.yScale.copy() .range([Math.floor(chart.yScale.range()[0] / dOpts.pointSize), 0]) .interpolate(d3.interpolateRound) .domain(chart.yScale.domain()); var maxWidth = Math.floor(chart.xScale.rangeBand() / dOpts.pointSize); var ptsObj = {}; var cYBucket = null; // Bucket points for (var pt = 0; pt < cGroup.values.length; pt++) { cYBucket = yPtScale(cGroup.values[pt]); if (ptsObj.hasOwnProperty(cYBucket) !== true) { ptsObj[cYBucket] = []; } ptsObj[cYBucket].push(cPlot.objs.points.pts[pt] .attr("cx", swarmBounds.middle) .attr("cy", yPtScale(cGroup.values[pt]) * dOpts.pointSize)); } // Plot buckets var rightMax = Math.min(swarmBounds.right - dOpts.pointSize); for (var row in ptsObj) { var leftMin = swarmBounds.left + (Math.max((maxWidth - ptsObj[row].length) / 2, 0) * dOpts.pointSize); var col = 0; for (pt in ptsObj[row]) { ptsObj[row][pt].attr("cx", Math.min(leftMin + col * dOpts.pointSize, rightMax) + dOpts.pointSize / 2); col++ } } } else { // For scatter points and points with no scatter var plotBounds = null, scatterWidth = 0, width = 0; if (dOpts.plotType == 'scatter' || typeof dOpts.plotType == 'number') { //Default scatter percentage is 20% of box width scatterWidth = typeof dOpts.plotType == 'number' ? dOpts.plotType : 20; } plotBounds = getObjWidth(scatterWidth, cName); width = plotBounds.right - plotBounds.left; for (var pt = 0; pt < cGroup.values.length; pt++) { cPlot.objs.points.pts[pt] .attr("cx", plotBounds.middle + addJitter(true, width)) .attr("cy", chart.yScale(cGroup.values[pt])); } } } if (cPlot.objs.bean) { var beanBounds = getObjWidth(dOpts.beanWidth, cName); for (var pt = 0; pt < cGroup.values.length; pt++) { cPlot.objs.bean.lines[pt] .attr("x1", beanBounds.left) .attr("x2", beanBounds.right) .attr('y1', chart.yScale(cGroup.values[pt])) .attr("y2", chart.yScale(cGroup.values[pt])); } } } }; /** * Create the svg elements for the data plots */ chart.dataPlots.preparePlots = function () { var cName, cPlot; if (dOpts && dOpts.colors) { chart.dataPlots.colorFunct = getColorFunct(dOpts.colors); } else { chart.dataPlots.colorFunct = chart.colorFunct } if (dOpts.show == false) { return } // Metrics lines chart.dataPlots.objs.g = chart.objs.g.append("g").attr("class", "metrics-lines"); if (dOpts.showLines && dOpts.showLines.length > 0) { chart.dataPlots.objs.lines = {}; var cMetric; for (var line in dOpts.showLines) { cMetric = dOpts.showLines[line]; chart.dataPlots.objs.lines[cMetric] = {}; chart.dataPlots.objs.lines[cMetric].values = []; for (var cGroup in chart.groupObjs) { chart.dataPlots.objs.lines[cMetric].values.push({ x: cGroup, y: chart.groupObjs[cGroup].metrics[cMetric] }) } chart.dataPlots.objs.lines[cMetric].line = d3.svg.line() .interpolate("cardinal") .y(function (d) { return chart.yScale(d.y) }); chart.dataPlots.objs.lines[cMetric].g = chart.dataPlots.objs.g.append("path") .attr("class", "line " + cMetric) .attr("data-metric", cMetric) .style("fill", 'none') .style("stroke", chart.colorFunct(cMetric)); } } for (cName in chart.groupObjs) { cPlot = chart.groupObjs[cName].dataPlots; cPlot.objs.g = chart.groupObjs[cName].g.append("g").attr("class", "data-plot"); // Points Plot if (dOpts.showPlot) { cPlot.objs.points = {g: null, pts: []}; cPlot.objs.points.g = cPlot.objs.g.append("g").attr("class", "points-plot"); for (var pt = 0; pt < chart.groupObjs[cName].values.length; pt++) { cPlot.objs.points.pts.push(cPlot.objs.points.g.append("circle") .attr("class", "point") .attr('r', dOpts.pointSize / 2)// Options is diameter, r takes radius so divide by 2 .style("fill", chart.dataPlots.colorFunct(cName))); } } // Bean lines if (dOpts.showBeanLines) { cPlot.objs.bean = {g: null, lines: []}; cPlot.objs.bean.g = cPlot.objs.g.append("g").attr("class", "bean-plot"); for (var pt = 0; pt < chart.groupObjs[cName].values.length; pt++) { cPlot.objs.bean.lines.push(cPlot.objs.bean.g.append("line") .attr("class", "bean line") .style("stroke-width", '1') .style("stroke", chart.dataPlots.colorFunct(cName))); } } } }; chart.dataPlots.preparePlots(); d3.select(window).on('resize.' + chart.selector + '.dataPlot', chart.dataPlots.update); chart.dataPlots.update(); return chart; }; return chart; }