skeleton code for scatterfacet

git-svn-id: http://google-refine.googlecode.com/svn/trunk@453 7d457c2a-affb-35e4-300a-418c747d4874
This commit is contained in:
Stefano Mazzocchi 2010-04-12 19:22:49 +00:00
parent e154a7e02d
commit 7ab1acd801
15 changed files with 601 additions and 11 deletions

View File

@ -13,6 +13,7 @@ import com.metaweb.gridworks.Jsonizable;
import com.metaweb.gridworks.browsing.facets.Facet; import com.metaweb.gridworks.browsing.facets.Facet;
import com.metaweb.gridworks.browsing.facets.ListFacet; import com.metaweb.gridworks.browsing.facets.ListFacet;
import com.metaweb.gridworks.browsing.facets.RangeFacet; import com.metaweb.gridworks.browsing.facets.RangeFacet;
import com.metaweb.gridworks.browsing.facets.ScatterplotFacet;
import com.metaweb.gridworks.browsing.facets.TextSearchFacet; import com.metaweb.gridworks.browsing.facets.TextSearchFacet;
import com.metaweb.gridworks.browsing.filters.RowFilter; import com.metaweb.gridworks.browsing.filters.RowFilter;
import com.metaweb.gridworks.model.Project; import com.metaweb.gridworks.model.Project;
@ -66,6 +67,8 @@ public class Engine implements Jsonizable {
facet = new ListFacet(); facet = new ListFacet();
} else if ("range".equals(type)) { } else if ("range".equals(type)) {
facet = new RangeFacet(); facet = new RangeFacet();
} else if ("scatterplot".equals(type)) {
facet = new ScatterplotFacet();
} else if ("text".equals(type)) { } else if ("text".equals(type)) {
facet = new TextSearchFacet(); facet = new TextSearchFacet();
} }

View File

@ -50,6 +50,7 @@ public class ScatterplotCharter {
private static final double px = 0.5f; private static final double px = 0.5f;
boolean process = true; boolean process = true;
boolean smoothed = false;
int width = 50; int width = 50;
int height = 50; int height = 50;
@ -68,7 +69,7 @@ public class ScatterplotCharter {
BufferedImage image; BufferedImage image;
Graphics2D g2; Graphics2D g2;
public DrawingRowVisitor(Project project, JSONObject o) throws JSONException { public DrawingRowVisitor(Project project, JSONObject o) throws JSONException {
String col_x_name = o.getString("cx"); String col_x_name = o.getString("cx");
Column column_x = project.columnModel.getColumnByName(col_x_name); Column column_x = project.columnModel.getColumnByName(col_x_name);
@ -135,7 +136,7 @@ public class ScatterplotCharter {
{ {
double xv = ((Number) cellx.value).doubleValue(); double xv = ((Number) cellx.value).doubleValue();
double yv = ((Number) celly.value).doubleValue(); double yv = ((Number) celly.value).doubleValue();
double x = (xv - min_x) * w / max_x; double x = (xv - min_x) * w / max_x;
double y = (yv - min_y) * h / max_y; double y = (yv - min_y) * h / max_y;
g2.fill(new Rectangle2D.Double(x, y, px, px)); g2.fill(new Rectangle2D.Double(x, y, px, px));

View File

@ -0,0 +1,234 @@
package com.metaweb.gridworks.browsing.facets;
import java.util.Properties;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONWriter;
import com.metaweb.gridworks.browsing.FilteredRows;
import com.metaweb.gridworks.browsing.filters.ExpressionNumberComparisonRowFilter;
import com.metaweb.gridworks.browsing.filters.RowFilter;
import com.metaweb.gridworks.expr.Evaluable;
import com.metaweb.gridworks.expr.MetaParser;
import com.metaweb.gridworks.expr.ParsingException;
import com.metaweb.gridworks.model.Column;
import com.metaweb.gridworks.model.Project;
import com.metaweb.gridworks.util.JSONUtilities;
public class ScatterplotFacet implements Facet {
/*
* Configuration, from the client side
*/
protected String _name; // name of facet
protected String _expression; // expression to compute numeric value(s) per row
protected String _columnName; // column to base expression on, if any
protected String _mode; // "range", MIN, MAX
protected double _from; // the numeric selection
protected double _to;
protected boolean _selectNumeric; // whether the numeric selection applies, default true
protected boolean _selectNonNumeric;
protected boolean _selectBlank;
protected boolean _selectError;
/*
* Derived configuration data
*/
protected int _cellIndex;
protected Evaluable _eval;
protected String _errorMessage;
protected boolean _selected; // false if we're certain that all rows will match
// and there isn't any filtering to do
/*
* Computed data, to return to the client side
*/
protected double _min;
protected double _max;
protected double _step;
protected int[] _baseBins;
protected int[] _bins;
protected int _numericCount;
protected int _nonNumericCount;
protected int _blankCount;
protected int _errorCount;
public ScatterplotFacet() {
}
private static final String MIN = "min";
private static final String MAX = "max";
private static final String TO = "to";
private static final String FROM = "from";
public void write(JSONWriter writer, Properties options)
throws JSONException {
writer.object();
writer.key("name"); writer.value(_name);
writer.key("expression"); writer.value(_expression);
writer.key("columnName"); writer.value(_columnName);
writer.key("mode"); writer.value(_mode);
if (_errorMessage != null) {
writer.key("error"); writer.value(_errorMessage);
} else {
if (!Double.isInfinite(_min) && !Double.isInfinite(_max)) {
writer.key(MIN); writer.value(_min);
writer.key(MAX); writer.value(_max);
writer.key("step"); writer.value(_step);
writer.key("bins"); writer.array();
for (int b : _bins) {
writer.value(b);
}
writer.endArray();
writer.key("baseBins"); writer.array();
for (int b : _baseBins) {
writer.value(b);
}
writer.endArray();
if (MIN.equals(_mode)) {
writer.key(FROM); writer.value(_from);
} else if (MAX.equals(_mode)) {
writer.key(TO); writer.value(_to);
} else {
writer.key(FROM); writer.value(_from);
writer.key(TO); writer.value(_to);
}
}
writer.key("numericCount"); writer.value(_numericCount);
writer.key("nonNumericCount"); writer.value(_nonNumericCount);
writer.key("blankCount"); writer.value(_blankCount);
writer.key("errorCount"); writer.value(_errorCount);
}
writer.endObject();
}
public void initializeFromJSON(Project project, JSONObject o) throws Exception {
_name = o.getString("name");
_expression = o.getString("expression");
_columnName = o.getString("columnName");
if (_columnName.length() > 0) {
Column column = project.columnModel.getColumnByName(_columnName);
if (column != null) {
_cellIndex = column.getCellIndex();
} else {
_errorMessage = "No column named " + _columnName;
}
} else {
_cellIndex = -1;
}
try {
_eval = MetaParser.parse(_expression);
} catch (ParsingException e) {
_errorMessage = e.getMessage();
}
_mode = o.getString("mode");
if (MIN.equals(_mode)) {
if (o.has(FROM)) {
_from = o.getDouble(FROM);
_selected = true;
}
} else if (MAX.equals(_mode)) {
if (o.has(TO)) {
_to = o.getDouble(TO);
_selected = true;
}
} else {
if (o.has(FROM) && o.has(TO)) {
_from = o.getDouble(FROM);
_to = o.getDouble(TO);
_selected = true;
}
}
_selectNumeric = JSONUtilities.getBoolean(o, "selectNumeric", true);
_selectNonNumeric = JSONUtilities.getBoolean(o, "selectNonNumeric", true);
_selectBlank = JSONUtilities.getBoolean(o, "selectBlank", true);
_selectError = JSONUtilities.getBoolean(o, "selectError", true);
if (!_selectNumeric || !_selectNonNumeric || !_selectBlank || !_selectError) {
_selected = true;
}
}
public RowFilter getRowFilter() {
if (_eval != null && _errorMessage == null && _selected) {
if (MIN.equals(_mode)) {
return new ExpressionNumberComparisonRowFilter(
_eval, _columnName, _cellIndex, _selectNumeric, _selectNonNumeric, _selectBlank, _selectError) {
protected boolean checkValue(double d) {
return d >= _from;
};
};
} else if (MAX.equals(_mode)) {
return new ExpressionNumberComparisonRowFilter(
_eval, _columnName, _cellIndex, _selectNumeric, _selectNonNumeric, _selectBlank, _selectError) {
protected boolean checkValue(double d) {
return d < _to;
};
};
} else {
return new ExpressionNumberComparisonRowFilter(
_eval, _columnName, _cellIndex, _selectNumeric, _selectNonNumeric, _selectBlank, _selectError) {
protected boolean checkValue(double d) {
return d >= _from && d < _to;
};
};
}
} else {
return null;
}
}
public void computeChoices(Project project, FilteredRows filteredRows) {
if (_eval != null && _errorMessage == null) {
Column column = project.columnModel.getColumnByCellIndex(_cellIndex);
String key = "numeric-bin:" + _expression;
NumericBinIndex index = (NumericBinIndex) column.getPrecompute(key);
if (index == null) {
index = new NumericBinIndex(project, _columnName, _cellIndex, _eval);
column.setPrecompute(key, index);
}
_min = index.getMin();
_max = index.getMax();
_step = index.getStep();
_baseBins = index.getBins();
if (_selected) {
_from = Math.max(_from, _min);
_to = Math.min(_to, _max);
} else {
_from = _min;
_to = _max;
}
ExpressionNumericRowBinner binner =
new ExpressionNumericRowBinner(_eval, _columnName, _cellIndex, index);
filteredRows.accept(project, binner);
_bins = binner.bins;
_numericCount = binner.numericCount;
_nonNumericCount = binner.nonNumericCount;
_blankCount = binner.blankCount;
_errorCount = binner.errorCount;
}
}
}

View File

@ -40,6 +40,7 @@ public class GetScatterplotCommand extends Command {
//Gridworks.log("drawn scatterplot in " + (System.currentTimeMillis() - start) + "ms"); //Gridworks.log("drawn scatterplot in " + (System.currentTimeMillis() - start) + "ms");
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace();
respondException(response, e); respondException(response, e);
} }
} }

File diff suppressed because one or more lines are too long

View File

@ -46,17 +46,37 @@ ScatterplotDialog.prototype._renderMatrix = function(columns) {
for (var i = 0; i < columns.length; i++) { for (var i = 0; i < columns.length; i++) {
var tr = table.insertRow(table.rows.length); var tr = table.insertRow(table.rows.length);
for (var j = 0; j < i; j++) { for (var j = 0; j < i; j++) {
var url = "/command/get-scatterplot?" + $.param({ var cx = columns[i];
var cy = columns[j];
var plotter_params = {
'cx' : cx.name,
'cy' : cy.name,
'w' : 20,
'h' : 20
};
var params = {
project: theProject.id, project: theProject.id,
engine: JSON.stringify(ui.browsingEngine.getJSON()), engine: JSON.stringify(ui.browsingEngine.getJSON()),
plotter: JSON.stringify({ plotter: JSON.stringify(plotter_params)
'cx' : columns[i].name, }
'cy' : columns[j].name, var url = "/command/get-scatterplot?" + $.param(params);
'w' : 20, var name = cx.name + '(x) vs. ' + cy.name + '(y)';
'h' : 20 var cell = $(tr.insertCell(j));
}) var link = $('<a href="javascript:{}"></a>').attr("title",name).click(function() {
ui.browsingEngine.addFacet(
"scatterplot",
{
"name" : name,
"x_column" : cx.name,
"y_column" : cy.name,
"expression" : "value",
"mode" : "scatterplot"
}
);
//self._dismiss();
}); });
$(tr.insertCell(j)).html('<img class="scatterplot" title="' + columns[i].name + ' vs. ' + columns[j].name + '" src="' + url + '" />'); var plot = $('<img src="' + url + '" />').addClass("scatterplot").appendTo(link);
link.appendTo(cell);
} }
$(tr.insertCell(i)).text(columns[i]); $(tr.insertCell(i)).text(columns[i]);
for (var j = i + 1; j < columns.length; j++) { for (var j = i + 1; j < columns.length; j++) {

View File

@ -0,0 +1,156 @@
function ScatterplotFacet(div, config, options) {
this._div = div;
this._config = config;
this._options = options;
this._from_x = ("from_x" in this._config) ? this._config.from_x : null;
this._to_x = ("to_x" in this._config) ? this._config.to_x : null;
this._from_y = ("from_y" in this._config) ? this._config.from_y : null;
this._to_y = ("to_y" in this._config) ? this._config.to_y : null;
this._error = false;
this._initializedUI = false;
}
ScatterplotFacet.prototype.reset = function() {
// TODO
};
ScatterplotFacet.reconstruct = function(div, uiState) {
return new ScatterplotFacet(div, uiState.c, uiState.o);
};
ScatterplotFacet.prototype.getUIState = function() {
var json = {
c: this.getJSON(),
o: this._options
};
return json;
};
ScatterplotFacet.prototype.getJSON = function() {
var o = {
type: "scatterplot",
name: this._config.name,
mode: this._config.mode,
expression: this._config.expression,
x_column : this._config.x_column,
y_column : this._config.y_column,
};
return o;
};
ScatterplotFacet.prototype.hasSelection = function() {
// TODO
};
ScatterplotFacet.prototype._initializeUI = function() {
var self = this;
var container = this._div.empty();
var headerDiv = $('<div></div>').addClass("facet-title").appendTo(container);
$('<span></span>').text(this._config.name).appendTo(headerDiv);
var resetButton = $('<a href="javascript:{}"></a>').addClass("facet-choice-link").text("reset").click(function() {
self.reset();
self._updateRest();
}).prependTo(headerDiv);
var removeButton = $('<img>')
.attr("src", "images/close.png")
.attr("title", "Remove this facet")
.addClass("facet-choice-link")
.click(function() {
self._remove();
}).prependTo(headerDiv);
var bodyDiv = $('<div></div>').addClass("facet-scatterplot-body").appendTo(container);
this._messageDiv = $('<div>').text("Loading...").addClass("facet-scatterplot-message").appendTo(bodyDiv);
this._plotDiv = $('<div>').addClass("facet-scatterplot-plot").appendTo(bodyDiv);
this._statusDiv = $('<div>').addClass("facet-scatterplot-status").appendTo(bodyDiv);
this._plot = new ScatterplotWidget(this._plotDiv, { binColors: [ "#ccccff", "#6666ff" ] });
};
ScatterplotFacet.prototype.updateState = function(data) {
if ("min" in data && "max" in data) {
this._error = false;
this._config.min = data.min;
this._config.max = data.max;
this._config.step = data.step;
this._baseBins = data.baseBins;
this._bins = data.bins;
switch (this._config.mode) {
case "min":
this._from = Math.max(data.from, this._config.min);
break;
case "max":
this._to = Math.min(data.to, this._config.max);
break;
default:
this._from = Math.max(data.from, this._config.min);
if ("to" in data) {
this._to = Math.min(data.to, this._config.max);
} else {
this._to = data.max;
}
}
this._numericCount = data.numericCount;
this._nonNumericCount = data.nonNumericCount;
this._blankCount = data.blankCount;
this._errorCount = data.errorCount;
} else {
this._error = true;
this._errorMessage = "error" in data ? data.error : "Unknown error.";
}
this.render();
};
ScatterplotFacet.prototype.render = function() {
if (!this._initializedUI) {
this._initializeUI();
this._initializedUI = true;
}
if (this._error) {
this._messageDiv.text(this._errorMessage).show();
this._sliderDiv.hide();
this._histogramDiv.hide();
this._statusDiv.hide();
this._otherChoicesDiv.hide();
return;
}
this._messageDiv.hide();
this._plotDiv.show();
this._statusDiv.show();
this._plot.update(
this._config.min,
this._config.max,
this._config.step,
[ this._baseBins, this._bins ],
this._from,
this._to
);
};
ScatterplotFacet.prototype._remove = function() {
ui.browsingEngine.removeFacet(this);
this._div = null;
this._config = null;
this._data = null;
};
ScatterplotFacet.prototype._updateRest = function() {
Gridworks.update({ engineChanged: true });
};

View File

@ -15,6 +15,9 @@ function BrowsingEngine(div, facetConfigs) {
case "range": case "range":
facet = RangeFacet.reconstruct(elmt, facetConfig); facet = RangeFacet.reconstruct(elmt, facetConfig);
break; break;
case "scatterplot":
facet = ScatterplotFacet.reconstruct(elmt, facetConfig);
break;
case "text": case "text":
facet = TextSearchFacet.reconstruct(elmt, facetConfig); facet = TextSearchFacet.reconstruct(elmt, facetConfig);
break; break;
@ -111,6 +114,9 @@ BrowsingEngine.prototype.addFacet = function(type, config, options) {
case "range": case "range":
facet = new RangeFacet(elmt, config, options); facet = new RangeFacet(elmt, config, options);
break; break;
case "scatterplot":
facet = new ScatterplotFacet(elmt, config, options);
break;
case "text": case "text":
facet = new TextSearchFacet(elmt, config, options); facet = new TextSearchFacet(elmt, config, options);
break; break;

View File

@ -0,0 +1,160 @@
function ScatterplotWidget(elmt, options) {
this._elmt = elmt;
this._options = options;
this._range = null;
this._binMatrix = null;
this._highlight = null;
this._initializeUI();
}
ScatterplotWidget.prototype.highlight = function(from, to) {
this._highlight = { from: from, to: to };
this._update();
};
ScatterplotWidget.prototype.update = function(min, max, step, binMatrix, from, to) {
if (typeof min == "undefined" || typeof binMatrix == "undefined" || binMatrix.length === 0 || binMatrix[0].length === 0) {
this._range = null;
this._binMatrix = null;
this._highlight = null;
this._elmt.hide();
} else {
this._range = { min: min, max: max, step: step };
this._binMatrix = binMatrix;
this._peak = 0;
for (var r = 0; r < binMatrix.length; r++) {
var row = binMatrix[r];
for (var c = 0; c < row.length; c++) {
this._peak = Math.max(this._peak, row[c]);
}
}
if (typeof from != "undefined" && typeof to != "undefined") {
this._highlight = { from: from, to: to };
}
this._update();
}
};
ScatterplotWidget.prototype._update = function() {
if (this._binMatrix !== null) {
if (this._highlight !== null) {
this._highlight.from = Math.max(this._highlight.from, this._range.min);
this._highlight.to = Math.min(this._highlight.to, this._range.max);
}
this._elmt.show();
this._resize();
this._render();
}
};
ScatterplotWidget.prototype._initializeUI = function() {
this._elmt
.empty()
.hide()
.addClass("scatterplot-widget")
.html(
'<canvas bind="canvas"></canvas>'
);
this._elmts = DOM.bind(this._elmt);
};
ScatterplotWidget.prototype._resize = function() {
this._elmts.canvas.attr("height", "height" in this._options ? this._options.height : 50);
this._elmts.canvas.attr("width", this._elmts.canvas.width());
};
ScatterplotWidget.prototype._render = function() {
var self = this;
var options = this._options;
var canvas = this._elmts.canvas[0];
var ctx = canvas.getContext('2d');
ctx.fillStyle = "white";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(0, canvas.height);
ctx.scale(1, -1);
var stepPixels = canvas.width / this._binMatrix[0].length;
var stepScale = stepPixels / this._range.step;
/*
* Draw axis
*/
ctx.save();
ctx.strokeStyle = "emptyBinColor" in options ? options.emptyBinColor : "#faa";
ctx.lineWidth = 1;
ctx.moveTo(0, 0);
ctx.lineTo(canvas.width, 0);
ctx.stroke();
ctx.restore();
/*
* Draw bins
*/
var makeColor = function(i) {
var n = Math.floor(15 * (self._binMatrix.length - i) / self._binMatrix.length);
var h = n.toString(16);
return "#" + h + h + h;
};
var renderRow = function(row, color) {
ctx.save();
ctx.lineWidth = 0;
ctx.fillStyle = color;
for (var c = 0; c < row.length; c++) {
var x = self._range.min + c * self._range.step;
var y = row[c];
if (y > 0) {
var left = c * stepPixels;
var width = Math.ceil(stepPixels);
var height = Math.ceil(y * canvas.height / self._peak);
ctx.fillRect(left, 0, width, height);
}
}
ctx.restore();
};
for (var r = 0; r < this._binMatrix.length; r++) {
renderRow(
this._binMatrix[r],
"binColors" in options && r < options.binColors.length ?
options.binColors[r] :
makeColor(r)
);
}
/*
* Draw highlight
*/
if (this._highlight !== null) {
ctx.fillStyle = "rgba(192,192,192, 0.5)";
ctx.globalCompositeOperation = "source-over";
if (this._highlight.from > this._range.min) {
ctx.fillRect(
0,
0,
(this._highlight.from - this._range.min) * stepScale,
canvas.height
);
}
if (this._highlight.to < this._range.max) {
ctx.fillRect(
(this._highlight.to - this._range.min) * stepScale,
0,
canvas.width - (this._highlight.to - this._range.min) * stepScale,
canvas.height
);
}
}
ctx.restore();
};

View File

@ -0,0 +1,9 @@
.scatterplot-widget {
margin: 0;
padding: 0;
position: relative;
}
.scatterplot-widget canvas {
width: 100%;
}