diff --git a/src/app/vis/menucomponents/new-graphmenu.js b/src/app/vis/menucomponents/new-graphmenu.js
index 5d30accf..a504b201 100644
--- a/src/app/vis/menucomponents/new-graphmenu.js
+++ b/src/app/vis/menucomponents/new-graphmenu.js
@@ -32,6 +32,9 @@ angular.module('plotter.vis.menucomponents.new-graphmenu',
case 2:
return { 'type': 'heatmap', 'data': $scope.heatmap.selection };
+
+ case 3:
+ return { 'type': 'boxplot', 'data': $scope.boxplot.selection };
}
}
@@ -54,6 +57,10 @@ angular.module('plotter.vis.menucomponents.new-graphmenu',
selection: []
};
+ $scope.boxplot = {
+ selection: []
+ };
+
// controllers of the menu dialog:
$scope.canSubmit = function() {
var selection = getSelection();
@@ -63,7 +70,11 @@ angular.module('plotter.vis.menucomponents.new-graphmenu',
else if($scope.tab.ind === 1) {
return _.size(selection.data.x) > 0 &&
_.size(selection.data.y) > 0;
- } else if($scope.tab.ind === 2) {
+ }
+ else if($scope.tab.ind === 2) {
+ return selection.data.length > 0;
+ }
+ else if($scope.tab.ind === 3) {
return selection.data.length > 0;
}
};
@@ -100,6 +111,15 @@ angular.module('plotter.vis.menucomponents.new-graphmenu',
error = true;
}
}
+
+ else if(selection.type == 'boxplot') {
+ dimensionCount = DimensionService.getPrimary().availableDimensionsCount();
+ if(dimensionCount < selection.data.length) {
+ NotifyService.addSticky('Too many selected variables', 'Please select a maximum of ' + dimensionCount + ' variables. You can free variables by first closing unnecessary figure windows on this tab.',
+ 'error', { referenceId: 'graphinfo' });
+ error = true;
+ }
+ }
return error;
}
@@ -134,6 +154,9 @@ angular.module('plotter.vis.menucomponents.new-graphmenu',
},
{
label: 'Heatmap'
+ },
+ {
+ label: 'Boxplot'
}
];
diff --git a/src/app/vis/menucomponents/new.graphmenu.tpl.html b/src/app/vis/menucomponents/new.graphmenu.tpl.html
index e0f5ec79..bd3ef81d 100644
--- a/src/app/vis/menucomponents/new.graphmenu.tpl.html
+++ b/src/app/vis/menucomponents/new.graphmenu.tpl.html
@@ -3,7 +3,7 @@
Histogram
Histogram
-
+
Scatterplot
@@ -23,6 +23,11 @@ Heatmap
+
+ Box plot
+ Box plot
+
+
diff --git a/src/app/vis/plotting/boxplot.js b/src/app/vis/plotting/boxplot.js
new file mode 100644
index 00000000..ddf5b0d2
--- /dev/null
+++ b/src/app/vis/plotting/boxplot.js
@@ -0,0 +1,378 @@
+angular.module('plotter.vis.plotting.boxplot',
+ [
+ 'ui.router',
+ 'services.dimensions',
+ 'services.dataset',
+ 'services.som',
+ 'services.window',
+ 'ext.d3',
+ 'ext.dc',
+ 'ext.lodash'
+ ])
+
+.controller('BoxplotController', function BoxplotController($scope, $injector, $timeout,
+ DimensionService, DatasetFactory, FilterService,
+ GRID_WINDOW_PADDING, constants,
+ d3, dc, _) {
+
+ $scope.dimensionService = $scope.window.handler().getDimensionService();
+
+ $scope.isSpecial = function() {
+ return $scope.window.extra().somSpecial;
+ };
+
+ function getTotalCount() {
+ return _.sum($scope.totalGroup.all(), function(d) { return d.value.valueOf(); });
+ }
+
+ function initDefault() {
+ $scope.dimensionInst = $scope.dimensionService.getDatasetDimension();
+ $scope.dimension = $scope.dimensionInst.get();
+ $scope.groupInst = $scope.dimensionInst.groupDefault();
+ $scope.dimensionService.getReducedBoxplot($scope.groupInst, $scope.window.variables());
+ $scope.reduced = $scope.groupInst.get();
+
+ $scope.colorScale = DatasetFactory.getColorScale();
+ }
+
+ if( $scope.isSpecial() ) {
+ } else {
+ initDefault();
+ }
+
+ $scope.window.resetFn(function() {
+ // pass
+ });
+
+ // share information with the plot window
+ $scope.window.headerText(['', $scope.window.variables().labelName(), '']);
+
+ $scope.filterDefault = function(group) {
+ return {
+ 'all': function() {
+ var datasets = DatasetFactory.getSets();
+ return group.all().filter(function(d) {
+ return datasets[d.key].active();
+ });
+ }
+ };
+
+ };
+
+ $scope.getHeight = function(ele) {
+ return ele.height() - GRID_WINDOW_PADDING;
+ };
+
+ $scope.getWidth = function(ele) {
+ return ele.width();
+ };
+
+ $scope.drawSOMSpecial = function(config) {
+ var resizeSVG = function(chart) {
+ var ratio = config.size.aspectRatio === 'stretch' ? 'none' : 'xMinYMin';
+ chart.select("svg")
+ .attr("viewBox", "0 0 " + [config.size.width, config.size.height].join(" ") )
+ .attr("preserveAspectRatio", ratio)
+ .attr("width", "100%")
+ .attr("height", "100%");
+ };
+
+ var plainchart = function() {
+ $scope.chart = dc.rowChart(config.element[0], config.chartGroup)
+ .margins({
+ top: 0,
+ right: 20,
+ bottom: 40,
+ left: 20
+ })
+ .elasticX(true)
+ .label(function(d) {
+ var name = _.capitalize(d.key.name),
+ arr = [name, ", count: ", d.value.count],
+ label = d.value.type == 'total' ? undefined : " (circle " + d.value.circle.name() + ")";
+ arr.push(label);
+ return arr.join("");
+ })
+ .title(function(d) {
+ var label = d.value.type == 'total' ? undefined : 'Circle: ' + d.value.circle.name(),
+ arr = [label,
+ 'Category: ' + d.key.name,
+ 'Count: ' + d.value.count];
+ return arr.join("\n");
+ })
+ .renderTitleLabel(false)
+ .titleLabelOffsetX(5)
+ .width($scope.getWidth(config.element))
+ .height($scope.getHeight(config.element))
+ // .width(config.size.width)
+ // .height(config.size.height)
+ // .x(d3.scale.linear().domain(config.extent))
+ .renderLabel(true)
+ .dimension(config.dimension)
+ .group(config.reduced)
+ //.group(config.filter(config.reduced))
+ .valueAccessor(function(d) {
+ return d.value.count;
+ })
+ .colors(config.colorScale.scale())
+ .colorAccessor(function(d) {
+ var type = d.value.type;
+ if(type == 'circle') {
+ return d.value.circle.name();
+ } else {
+ return '9';
+ }
+ })
+ // .on("postRender", resizeSVG)
+ // .on("postRedraw", resizeSVG)
+ .ordering(function(d) {
+ return d.value.type == 'total' ? 'total|' : d.value.circle.id() + "|" + d.key.name;
+ });
+
+ // disable filtering
+ $scope.chart.onClick = function() {};
+
+ };
+
+ plainchart();
+
+ };
+
+
+ $scope.drawDefault = function(config) {
+ var resizeSVG = function(chart) {
+ var ratio = config.size.aspectRatio === 'stretch' ? 'none' : 'xMinYMin';
+ chart.select("svg")
+ .attr("viewBox", "0 0 " + [config.size.width, config.size.height].join(" ") )
+ .attr("preserveAspectRatio", ratio)
+ .attr("width", "100%")
+ .attr("height", "100%");
+ };
+
+ function plainchart() {
+ $scope.chart = dc.boxPlot(config.element[0], config.chartGroup)
+ // .gap(10)
+ .margins({
+ top: 25,
+ right: 10,
+ bottom: 20,
+ left: 40
+ })
+ .elasticX(true)
+ .elasticY(false)
+ .width($scope.getWidth(config.element))
+ .height($scope.getHeight(config.element))
+ .renderLabel(true)
+ .dimension(config.dimension)
+ .colorAccessor(function(d) {
+ return d.key.dataset;
+ })
+ .group(config.filter(config.reduced))
+ // .keyAccessor(function(d) {
+ // return d.key;
+ // })
+ // .valueAccessor(function(d) {
+ // return d.value;
+ // })
+ .colors(config.colorScale.scale())
+ .colorAccessor(function(d) {
+ return config.colorScale.getAccessor(d.key);
+ })
+ .tickFormat(constants.tickFormat);
+ // .on("postRender", resizeSVG)
+ // .on("postRedraw", resizeSVG)
+
+ $scope.chart.filter = function() {};
+
+ $scope.chart.yAxis().tickFormat(constants.tickFormat);
+
+ }
+
+ plainchart();
+
+ };
+
+})
+
+.directive('plBoxplot', function plBoxplot(constants, $timeout, $rootScope, $injector, CLASSED_BARCHART_SIZE, GRID_WINDOW_PADDING, _) {
+ function postLink($scope, ele, attrs, ctrl) {
+
+ function initDropdown() {
+ var selector = _.template('#<%= id %> <%= element %>'),
+ id = $scope.element.parent().attr('id');
+
+ $scope.window.addDropdown({
+ type: "export:svg",
+ selector: selector({ id: id, element: 'svg' }),
+ scope: $scope,
+ source: 'svg',
+ window: $scope.window
+ });
+
+ $scope.window.addDropdown({
+ type: "export:png",
+ selector: selector({ id: id, element: 'svg' }),
+ scope: $scope,
+ source: 'svg',
+ window: $scope.window
+ });
+ }
+
+ $scope.element = ele;
+
+ var drawFunction = null,
+ config;
+
+ if($scope.isSpecial()) {
+ drawFunction = $scope.drawSOMSpecial;
+ config = {
+ element: $scope.element,
+ // extent: $scope.extent,
+ filter: $scope.filterSOMSpecial,
+ colorScale: $scope.colorScale,
+ dimension: $scope.dimension,
+ reduced: $scope.reduced,
+ chartGroup: constants.groups.histogram.nonInteractive,
+ variable: $scope.window.variables().name()
+ };
+
+ } else {
+ config = {
+ element: $scope.element,
+ // extent: $scope.extent,
+ filter: $scope.filterDefault,
+ colorScale: $scope.colorScale,
+ dimension: $scope.dimension,
+ reduced: $scope.reduced,
+ chartGroup: constants.groups.histogram.nonInteractive,
+ variable: $scope.window.variables().name(),
+ callback: $scope.initExistingFilters
+ };
+ drawFunction = $scope.drawDefault;
+ }
+
+ $scope.element.ready(function() {
+ $timeout(function() {
+ drawFunction(config);
+ $scope.chart.render();
+ if(config.callback) {
+ config.callback($scope.chart);
+ }
+ initDropdown();
+ });
+ });
+
+ $scope.deregisters = [];
+
+ function setResize() {
+ function setSize() {
+ $scope.size = angular.copy($scope.window.size());
+ }
+
+ var resizeUnbind = $scope.$on('gridster-item-transition-end', function(item) {
+ function gridSizeSame() {
+ return _.isEqual($scope.size, $scope.window.size());
+ }
+ if(!gridSizeSame()) {
+ renderWithNewDimensions();
+ }
+ });
+
+ setSize();
+ $scope.deregisters.push(resizeUnbind);
+ }
+
+ function renderWithNewDimensions() {
+ function setSize() {
+ $scope.size = angular.copy($scope.window.size());
+ }
+ var width = $scope.getWidth($scope.element),
+ height = $scope.getHeight($scope.element);
+
+ $scope.chart.width(width);
+ $scope.chart.height(height);
+ $scope.chart.redraw();
+ // $scope.chart.render();
+
+ setSize();
+ }
+
+ function setRerender() {
+ var reRenderUnbind = $rootScope.$on('window-handler.rerender', function(event, winHandler, config) {
+ if( winHandler == $scope.window.handler() ) {
+
+ $timeout(function() {
+ if($scope.isSpecial()) {
+ // $scope.chart.group($scope.filterSOMSpecial($scope.reduced));
+ } else {
+ // $scope.chart.group($scope.reduced);
+ $scope.chart.group($scope.filterDefault($scope.reduced));
+ }
+ $scope.chart.redraw();
+ });
+ }
+ });
+ $scope.deregisters.push(reRenderUnbind);
+ }
+
+ function setResizeElement() {
+ var renderThr = _.debounce(function() {
+ renderWithNewDimensions();
+ }, 150, { leading: false, trailing: true });
+
+ var resizeUnbind = $scope.$on('gridster-resized', function(sizes, gridster) {
+ var isVisible = _.contains($injector.get('WindowHandler').getVisible(), $scope.window.handler());
+ if(!isVisible) { return; }
+ renderThr();
+ });
+ }
+
+ function setRedraw() {
+ var redrawUnbind = $rootScope.$on('window-handler.redraw', function(event, winHandler) {
+ if( winHandler == $scope.window.handler() ) {
+ $timeout( function() {
+ $scope.chart.redraw();
+ });
+ }
+ });
+ $scope.deregisters.push(redrawUnbind);
+ }
+
+ setResize();
+ setRerender();
+ setRedraw();
+ setResizeElement();
+
+ $scope.$on('$destroy', function() {
+ console.log("destroying boxplot for", $scope.window.variables().name());
+ _.each($scope.deregisters, function(unbindFn) {
+ unbindFn();
+ });
+
+ // remove chart
+ dc.deregisterChart($scope.chart, constants.groups.histogram.nonInteractive);
+ $scope.chart.resetSvg();
+
+ $scope.groupInst.decrement();
+ if($scope.isSpecial()) {
+ $scope.totalDimensionInst.decrement();
+ }
+ $scope.dimensionInst.decrement();
+ });
+
+ ele.on('$destroy', function() {
+ $scope.$destroy();
+ });
+
+ }
+
+ return {
+ scope: false,
+ restrict: 'C',
+ controller: 'BoxplotController',
+ link: {
+ post: postLink
+ }
+ };
+
+});
\ No newline at end of file
diff --git a/src/app/vis/plotting/plotting.js b/src/app/vis/plotting/plotting.js
index a89287a8..97abc709 100644
--- a/src/app/vis/plotting/plotting.js
+++ b/src/app/vis/plotting/plotting.js
@@ -8,6 +8,7 @@ angular.module('plotter.vis.plotting',
'plotter.vis.plotting.profile-histogram',
'plotter.vis.plotting.regression',
'plotter.vis.plotting.classedbarchart',
+ 'plotter.vis.plotting.boxplot',
'services.dimensions',
'services.dataset',
'services.variable',
@@ -51,6 +52,10 @@ angular.module('plotter.vis.plotting',
callFn = that.drawRegression;
break;
+ case 'boxplot':
+ callFn = that.drawBoxplot;
+ break;
+
default:
throw new Error('Undefined plot type');
}
@@ -305,4 +310,48 @@ angular.module('plotter.vis.plotting',
.extra({ source: config.source });
};
+ this.drawBoxplot = function(config, windowHandler) {
+ var draw = function(config, windowHandler) {
+ var gridWindow = windowHandler.add();
+
+ gridWindow
+ .figure('pl-boxplot')
+ .variables(config.variable)
+ .extra({ somSpecial: config.somSpecial });
+ };
+
+ var defer = $q.defer();
+
+ var promises = [];
+ var plottingDataPromise = DatasetFactory.getVariableData([config.variable], windowHandler);
+ promises.push(plottingDataPromise);
+
+ if( config.somSpecial ) {
+ // need from primary as well
+ var primaryHandler = windowHandler.getService().getPrimary();
+ var primaryPromise = DatasetFactory.getVariableData([config.variable], primaryHandler);
+ promises.push(primaryPromise);
+ }
+
+ var dim = DimensionService.getPrimary();
+ var samp = dim.getSampleDimension();
+
+ $q.all(promises).then(function successFn(res) {
+ // draw the figure
+ NotifyService.closeModal();
+ draw(config, windowHandler);
+ defer.resolve();
+ },
+ function errorFn(variable) {
+ NotifyService.closeModal();
+
+ var title = 'Variable ' + variable + ' could not be loaded\n',
+ message = 'Please check the selected combination is valid for the selected datasets.',
+ level = 'error';
+ NotifyService.addTransient(title, message, level);
+ defer.reject();
+ });
+ return defer.promise;
+ };
+
});
\ No newline at end of file
diff --git a/src/app/vis/plotting/plotting.less b/src/app/vis/plotting/plotting.less
index ba8a864c..6158ad69 100644
--- a/src/app/vis/plotting/plotting.less
+++ b/src/app/vis/plotting/plotting.less
@@ -63,7 +63,7 @@ svg text.label {
}
-.pl-histogram, .pl-heatmap, .pl-somplane, .pl-scatterplot, .pl-profile-histogram, .pl-classed-bar-chart {
+.pl-histogram, .pl-heatmap, .pl-somplane, .pl-scatterplot, .pl-profile-histogram, .pl-classed-bar-chart, .pl-boxplot {
position: absolute;
width: 100%;
height: 100%;
diff --git a/src/app/vis/vis.js b/src/app/vis/vis.js
index bcdeb423..4bb6b654 100644
--- a/src/app/vis/vis.js
+++ b/src/app/vis/vis.js
@@ -265,6 +265,15 @@ angular.module('plotter.vis', [
}, winHandler);
}
+ function postBoxplot(variables) {
+ _.each(variables, function(v) {
+ PlotService.drawBoxplot({
+ variable: v,
+ somSpecial: false
+ }, winHandler);
+ });
+ }
+
switch (config.type) {
case 'histogram':
postHistogram(config.selection);
@@ -277,8 +286,11 @@ angular.module('plotter.vis', [
case 'heatmap':
postHeatmap(config.selection);
break;
- }
+ case 'boxplot':
+ postBoxplot(config.selection);
+ break;
+ }
});
};
diff --git a/src/common/services-dimensions.js b/src/common/services-dimensions.js
index 48c31834..69e46073 100644
--- a/src/common/services-dimensions.js
+++ b/src/common/services-dimensions.js
@@ -140,8 +140,6 @@ angular.module('services.dimensions', [
return dimensions[key];
};
-
-
this.getVariableBMUDimension = function() {
var creationFn = function(d) {
return {
@@ -730,6 +728,39 @@ angular.module('services.dimensions', [
return dimensionGroup;
};
+ this.getReducedBoxplot = function(dimensionGroup, variable) {
+ var reduceAdd = function(p, v) {
+ var variableVal = +v.variables[variable.name()];
+ if (_.isNaN(variableVal) || variableVal == constants.nanValue) {
+ // pass
+ } else {
+ p.splice(d3.bisectLeft(p, variableVal), 0, variableVal);
+ }
+ return p;
+ };
+
+ var reduceRemove = function(p, v) {
+ var variableVal = +v.variables[variable.name()];
+ if (_.isNaN(variableVal) || variableVal == constants.nanValue) {
+ // pass
+ } else {
+ p.splice(_.indexOf(p, variableVal, true), 1);
+ }
+ return p;
+ };
+
+ var reduceInitial = function() {
+ var p = [];
+ return p;
+ };
+ dimensionGroup.reduce({
+ add: reduceAdd,
+ remove: reduceRemove,
+ initial: reduceInitial
+ });
+ return dimensionGroup;
+ };
+
this.getReducedGroupHistoDistributions = function(dimensionGroup, variable) {
var getKey = function(samp) {
return [samp.originalDataset || samp.dataset, samp.sampleid].join("|");