diff --git a/src/main/java/com/metaweb/gridworks/browsing/Engine.java b/src/main/java/com/metaweb/gridworks/browsing/Engine.java index f5fd1bb7f..5a6f249e4 100644 --- a/src/main/java/com/metaweb/gridworks/browsing/Engine.java +++ b/src/main/java/com/metaweb/gridworks/browsing/Engine.java @@ -13,6 +13,7 @@ import com.metaweb.gridworks.Jsonizable; import com.metaweb.gridworks.browsing.facets.Facet; import com.metaweb.gridworks.browsing.facets.ListFacet; 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.filters.RowFilter; import com.metaweb.gridworks.model.Project; @@ -66,6 +67,8 @@ public class Engine implements Jsonizable { facet = new ListFacet(); } else if ("range".equals(type)) { facet = new RangeFacet(); + } else if ("scatterplot".equals(type)) { + facet = new ScatterplotFacet(); } else if ("text".equals(type)) { facet = new TextSearchFacet(); } diff --git a/src/main/java/com/metaweb/gridworks/browsing/charting/ScatterplotCharter.java b/src/main/java/com/metaweb/gridworks/browsing/charting/ScatterplotCharter.java index 36caa391d..3bcabb204 100644 --- a/src/main/java/com/metaweb/gridworks/browsing/charting/ScatterplotCharter.java +++ b/src/main/java/com/metaweb/gridworks/browsing/charting/ScatterplotCharter.java @@ -50,6 +50,7 @@ public class ScatterplotCharter { private static final double px = 0.5f; boolean process = true; + boolean smoothed = false; int width = 50; int height = 50; @@ -68,7 +69,7 @@ public class ScatterplotCharter { BufferedImage image; Graphics2D g2; - + public DrawingRowVisitor(Project project, JSONObject o) throws JSONException { String col_x_name = o.getString("cx"); Column column_x = project.columnModel.getColumnByName(col_x_name); @@ -135,7 +136,7 @@ public class ScatterplotCharter { { double xv = ((Number) cellx.value).doubleValue(); double yv = ((Number) celly.value).doubleValue(); - + double x = (xv - min_x) * w / max_x; double y = (yv - min_y) * h / max_y; g2.fill(new Rectangle2D.Double(x, y, px, px)); diff --git a/src/main/java/com/metaweb/gridworks/browsing/facets/ScatterplotFacet.java b/src/main/java/com/metaweb/gridworks/browsing/facets/ScatterplotFacet.java new file mode 100644 index 000000000..b154c0498 --- /dev/null +++ b/src/main/java/com/metaweb/gridworks/browsing/facets/ScatterplotFacet.java @@ -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; + } + } +} diff --git a/src/main/java/com/metaweb/gridworks/commands/info/GetScatterplotCommand.java b/src/main/java/com/metaweb/gridworks/commands/info/GetScatterplotCommand.java index 9b95cc2ee..1cd0e0d4b 100644 --- a/src/main/java/com/metaweb/gridworks/commands/info/GetScatterplotCommand.java +++ b/src/main/java/com/metaweb/gridworks/commands/info/GetScatterplotCommand.java @@ -40,6 +40,7 @@ public class GetScatterplotCommand extends Command { //Gridworks.log("drawn scatterplot in " + (System.currentTimeMillis() - start) + "ms"); } catch (Exception e) { + e.printStackTrace(); respondException(response, e); } } diff --git a/src/main/webapp/project.html b/src/main/webapp/project.html index a6c21785c..569575883 100644 --- a/src/main/webapp/project.html +++ b/src/main/webapp/project.html @@ -1 +1 @@ - Freebase Gridworks
starting up ...
\ No newline at end of file + Freebase Gridworks
starting up ...
\ No newline at end of file diff --git a/src/main/webapp/scripts/dialogs/scatterplot-dialog.js b/src/main/webapp/scripts/dialogs/scatterplot-dialog.js index 9ccf67a4c..ebebb4135 100644 --- a/src/main/webapp/scripts/dialogs/scatterplot-dialog.js +++ b/src/main/webapp/scripts/dialogs/scatterplot-dialog.js @@ -46,17 +46,37 @@ ScatterplotDialog.prototype._renderMatrix = function(columns) { for (var i = 0; i < columns.length; i++) { var tr = table.insertRow(table.rows.length); 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, engine: JSON.stringify(ui.browsingEngine.getJSON()), - plotter: JSON.stringify({ - 'cx' : columns[i].name, - 'cy' : columns[j].name, - 'w' : 20, - 'h' : 20 - }) + plotter: JSON.stringify(plotter_params) + } + var url = "/command/get-scatterplot?" + $.param(params); + var name = cx.name + '(x) vs. ' + cy.name + '(y)'; + var cell = $(tr.insertCell(j)); + var link = $('').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(''); + var plot = $('').addClass("scatterplot").appendTo(link); + link.appendTo(cell); } $(tr.insertCell(i)).text(columns[i]); for (var j = i + 1; j < columns.length; j++) { diff --git a/src/main/webapp/scripts/facets/scatterplot-facet.js b/src/main/webapp/scripts/facets/scatterplot-facet.js new file mode 100644 index 000000000..192703bcd --- /dev/null +++ b/src/main/webapp/scripts/facets/scatterplot-facet.js @@ -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 = $('
').addClass("facet-title").appendTo(container); + $('').text(this._config.name).appendTo(headerDiv); + + var resetButton = $('').addClass("facet-choice-link").text("reset").click(function() { + self.reset(); + self._updateRest(); + }).prependTo(headerDiv); + + var removeButton = $('') + .attr("src", "images/close.png") + .attr("title", "Remove this facet") + .addClass("facet-choice-link") + .click(function() { + self._remove(); + }).prependTo(headerDiv); + + var bodyDiv = $('
').addClass("facet-scatterplot-body").appendTo(container); + + this._messageDiv = $('
').text("Loading...").addClass("facet-scatterplot-message").appendTo(bodyDiv); + this._plotDiv = $('
').addClass("facet-scatterplot-plot").appendTo(bodyDiv); + this._statusDiv = $('
').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 }); +}; diff --git a/src/main/webapp/scripts/project/browsing-engine.js b/src/main/webapp/scripts/project/browsing-engine.js index 55a333a85..ca2ca255a 100644 --- a/src/main/webapp/scripts/project/browsing-engine.js +++ b/src/main/webapp/scripts/project/browsing-engine.js @@ -15,6 +15,9 @@ function BrowsingEngine(div, facetConfigs) { case "range": facet = RangeFacet.reconstruct(elmt, facetConfig); break; + case "scatterplot": + facet = ScatterplotFacet.reconstruct(elmt, facetConfig); + break; case "text": facet = TextSearchFacet.reconstruct(elmt, facetConfig); break; @@ -111,6 +114,9 @@ BrowsingEngine.prototype.addFacet = function(type, config, options) { case "range": facet = new RangeFacet(elmt, config, options); break; + case "scatterplot": + facet = new ScatterplotFacet(elmt, config, options); + break; case "text": facet = new TextSearchFacet(elmt, config, options); break; diff --git a/src/main/webapp/scripts/util/histogram-widget.js b/src/main/webapp/scripts/widgets/histogram-widget.js similarity index 100% rename from src/main/webapp/scripts/util/histogram-widget.js rename to src/main/webapp/scripts/widgets/histogram-widget.js diff --git a/src/main/webapp/scripts/project/history-widget.js b/src/main/webapp/scripts/widgets/history-widget.js similarity index 100% rename from src/main/webapp/scripts/project/history-widget.js rename to src/main/webapp/scripts/widgets/history-widget.js diff --git a/src/main/webapp/scripts/project/process-widget.js b/src/main/webapp/scripts/widgets/process-widget.js similarity index 100% rename from src/main/webapp/scripts/project/process-widget.js rename to src/main/webapp/scripts/widgets/process-widget.js diff --git a/src/main/webapp/scripts/widgets/scatterplot-widget.js b/src/main/webapp/scripts/widgets/scatterplot-widget.js new file mode 100644 index 000000000..9898cbb08 --- /dev/null +++ b/src/main/webapp/scripts/widgets/scatterplot-widget.js @@ -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( + '' + ); + + 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(); +}; diff --git a/src/main/webapp/styles/util/histogram-widget.css b/src/main/webapp/styles/widgets/histogram-widget.css similarity index 100% rename from src/main/webapp/styles/util/histogram-widget.css rename to src/main/webapp/styles/widgets/histogram-widget.css diff --git a/src/main/webapp/styles/project/history.css b/src/main/webapp/styles/widgets/history.css similarity index 100% rename from src/main/webapp/styles/project/history.css rename to src/main/webapp/styles/widgets/history.css diff --git a/src/main/webapp/styles/widgets/scatterplot-widget.css b/src/main/webapp/styles/widgets/scatterplot-widget.css new file mode 100644 index 000000000..4e863c5fd --- /dev/null +++ b/src/main/webapp/styles/widgets/scatterplot-widget.css @@ -0,0 +1,9 @@ +.scatterplot-widget { + margin: 0; + padding: 0; + position: relative; +} + +.scatterplot-widget canvas { + width: 100%; +}