From ed5eae83afca98c0812e8c96f873065ecb781304 Mon Sep 17 00:00:00 2001 From: David Huynh Date: Wed, 3 Feb 2010 00:20:42 +0000 Subject: [PATCH] Added histograms to range slider facets. git-svn-id: http://google-refine.googlecode.com/svn/trunk@25 7d457c2a-affb-35e4-300a-418c747d4874 --- .../browsing/facets/NumericBinIndex.java | 88 ++++++++++++++++++ .../gridlock/browsing/facets/RangeFacet.java | 51 ++++++++--- .../commands/ApproveNewReconcileCommand.java | 2 +- .../commands/ApproveReconcileCommand.java | 2 +- .../commands/DiscardReconcileCommand.java | 2 +- .../commands/DoTextTransformCommand.java | 2 +- .../gridlock/history/MassCellChange.java | 14 ++- .../com/metaweb/gridlock/model/Column.java | 24 +++++ .../gridlock/process/ReconProcess.java | 2 +- .../webapp/scripts/project/data-table-view.js | 6 ++ .../webapp/scripts/project/range-facet.js | 90 +++++++++++++------ src/main/webapp/styles/browsing.css | 7 +- 12 files changed, 240 insertions(+), 50 deletions(-) create mode 100644 src/main/java/com/metaweb/gridlock/browsing/facets/NumericBinIndex.java diff --git a/src/main/java/com/metaweb/gridlock/browsing/facets/NumericBinIndex.java b/src/main/java/com/metaweb/gridlock/browsing/facets/NumericBinIndex.java new file mode 100644 index 000000000..8a6685e13 --- /dev/null +++ b/src/main/java/com/metaweb/gridlock/browsing/facets/NumericBinIndex.java @@ -0,0 +1,88 @@ +package com.metaweb.gridlock.browsing.facets; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import com.metaweb.gridlock.expr.Evaluable; +import com.metaweb.gridlock.model.Cell; +import com.metaweb.gridlock.model.Project; +import com.metaweb.gridlock.model.Row; + +public class NumericBinIndex { + public double min; + public double max; + public double step; + public int[] bins; + + public NumericBinIndex(Project project, int cellIndex, Evaluable eval) { + Properties bindings = new Properties(); + + min = Double.POSITIVE_INFINITY; + max = Double.NEGATIVE_INFINITY; + + List allValues = new ArrayList(); + for (int i = 0; i < project.rows.size(); i++) { + Row row = project.rows.get(i); + + if (cellIndex < row.cells.size()) { + Cell cell = row.cells.get(cellIndex); + if (cell != null) { + bindings.put("project", project); + bindings.put("cell", cell); + bindings.put("value", cell.value); + + Object value = eval.evaluate(bindings); + if (value != null) { + if (value.getClass().isArray()) { + Object[] a = (Object[]) value; + for (Object v : a) { + if (v instanceof Number) { + processValue(((Number) v).doubleValue(), allValues); + } + } + } else if (value instanceof Number) { + processValue(((Number) value).doubleValue(), allValues); + } + } + } + } + } + + if (min >= max) { + step = 0; + bins = new int[0]; + return; + } + + double diff = max - min; + if (diff > 10) { + step = 1; + while (step * 100 < diff) { + step *= 10; + } + } else { + step = 1; + while (step * 100 > diff) { + step /= 10; + } + } + + min = Math.floor(min / step) * step; + max = Math.ceil(max / step) * step; + + int binCount = 1 + (int) Math.ceil((max - min) / step); + + bins = new int[binCount]; + for (double d : allValues) { + int bin = (int) Math.round((d - min) / step); + bins[bin]++; + } + } + + protected void processValue(double v, List allValues) { + min = Math.min(min, v); + max = Math.max(max, v); + allValues.add(v); + } +} diff --git a/src/main/java/com/metaweb/gridlock/browsing/facets/RangeFacet.java b/src/main/java/com/metaweb/gridlock/browsing/facets/RangeFacet.java index 5fea5e4d0..8657618a4 100644 --- a/src/main/java/com/metaweb/gridlock/browsing/facets/RangeFacet.java +++ b/src/main/java/com/metaweb/gridlock/browsing/facets/RangeFacet.java @@ -11,6 +11,7 @@ import com.metaweb.gridlock.browsing.filters.ExpressionNumberComparisonRowFilter import com.metaweb.gridlock.browsing.filters.RowFilter; import com.metaweb.gridlock.expr.Evaluable; import com.metaweb.gridlock.expr.Parser; +import com.metaweb.gridlock.model.Column; import com.metaweb.gridlock.model.Project; public class RangeFacet implements Facet { @@ -22,6 +23,11 @@ public class RangeFacet implements Facet { protected String _mode; protected double _min; protected double _max; + protected double _step; + protected int[] _bins; + + protected double _from; + protected double _to; public RangeFacet() { } @@ -34,15 +40,24 @@ public class RangeFacet implements Facet { writer.key("name"); writer.value(_name); writer.key("expression"); writer.value(_expression); writer.key("cellIndex"); writer.value(_cellIndex); + 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("mode"); writer.value(_mode); if ("min".equals(_mode)) { - writer.key("min"); writer.value(_min); + writer.key("from"); writer.value(_from); } else if ("max".equals(_mode)) { - writer.key("max"); writer.value(_max); + writer.key("to"); writer.value(_to); } else { - writer.key("min"); writer.value(_min); - writer.key("max"); writer.value(_max); + writer.key("from"); writer.value(_from); + writer.key("to"); writer.value(_to); } writer.endObject(); } @@ -57,12 +72,12 @@ public class RangeFacet implements Facet { _mode = o.getString("mode"); if ("min".equals(_mode)) { - _min = o.getDouble("min"); + _from = o.getDouble("from"); } else if ("max".equals(_mode)) { - _max = o.getDouble("max"); + _to = o.getDouble("to"); } else { - _min = o.getDouble("min"); - _max = o.getDouble("max"); + _from = o.getDouble("from"); + _to = o.getDouble("to"); } } @@ -71,19 +86,19 @@ public class RangeFacet implements Facet { if ("min".equals(_mode)) { return new ExpressionNumberComparisonRowFilter(_eval, _cellIndex) { protected boolean checkValue(double d) { - return d >= _min; + return d >= _from; }; }; } else if ("max".equals(_mode)) { return new ExpressionNumberComparisonRowFilter(_eval, _cellIndex) { protected boolean checkValue(double d) { - return d <= _max; + return d <= _to; }; }; } else { return new ExpressionNumberComparisonRowFilter(_eval, _cellIndex) { protected boolean checkValue(double d) { - return d >= _min && d <= _max; + return d >= _from && d <= _to; }; }; } @@ -91,6 +106,18 @@ public class RangeFacet implements Facet { @Override public void computeChoices(Project project, FilteredRows filteredRows) { - // nothing to do + Column column = project.columnModel.getColumnByCellIndex(_cellIndex); + + String key = "numeric-bin:" + _expression; + NumericBinIndex index = (NumericBinIndex) column.getPrecompute(key); + if (index == null) { + index = new NumericBinIndex(project, _cellIndex, _eval); + column.setPrecompute(key, index); + } + + _min = index.min; + _max = index.max; + _step = index.step; + _bins = index.bins; } } diff --git a/src/main/java/com/metaweb/gridlock/commands/ApproveNewReconcileCommand.java b/src/main/java/com/metaweb/gridlock/commands/ApproveNewReconcileCommand.java index ee1c67bea..5dc8da078 100644 --- a/src/main/java/com/metaweb/gridlock/commands/ApproveNewReconcileCommand.java +++ b/src/main/java/com/metaweb/gridlock/commands/ApproveNewReconcileCommand.java @@ -71,7 +71,7 @@ public class ApproveNewReconcileCommand extends Command { } }.init(cellIndex, cellChanges)); - MassCellChange massCellChange = new MassCellChange(cellChanges); + MassCellChange massCellChange = new MassCellChange(cellChanges, cellIndex); HistoryEntry historyEntry = new HistoryEntry( project, "Approve new topics for " + columnName, massCellChange); diff --git a/src/main/java/com/metaweb/gridlock/commands/ApproveReconcileCommand.java b/src/main/java/com/metaweb/gridlock/commands/ApproveReconcileCommand.java index 0c19f0053..b44201ad5 100644 --- a/src/main/java/com/metaweb/gridlock/commands/ApproveReconcileCommand.java +++ b/src/main/java/com/metaweb/gridlock/commands/ApproveReconcileCommand.java @@ -71,7 +71,7 @@ public class ApproveReconcileCommand extends Command { } }.init(cellIndex, cellChanges)); - MassCellChange massCellChange = new MassCellChange(cellChanges); + MassCellChange massCellChange = new MassCellChange(cellChanges, cellIndex); HistoryEntry historyEntry = new HistoryEntry( project, "Approve best recon candidates for " + columnName, massCellChange); diff --git a/src/main/java/com/metaweb/gridlock/commands/DiscardReconcileCommand.java b/src/main/java/com/metaweb/gridlock/commands/DiscardReconcileCommand.java index ed632e5f0..f99ad43f3 100644 --- a/src/main/java/com/metaweb/gridlock/commands/DiscardReconcileCommand.java +++ b/src/main/java/com/metaweb/gridlock/commands/DiscardReconcileCommand.java @@ -67,7 +67,7 @@ public class DiscardReconcileCommand extends Command { } }.init(cellIndex, cellChanges)); - MassCellChange massCellChange = new MassCellChange(cellChanges); + MassCellChange massCellChange = new MassCellChange(cellChanges, cellIndex); HistoryEntry historyEntry = new HistoryEntry( project, "Discard recon results for " + columnName, massCellChange); diff --git a/src/main/java/com/metaweb/gridlock/commands/DoTextTransformCommand.java b/src/main/java/com/metaweb/gridlock/commands/DoTextTransformCommand.java index 9632e69c3..a19ea2173 100644 --- a/src/main/java/com/metaweb/gridlock/commands/DoTextTransformCommand.java +++ b/src/main/java/com/metaweb/gridlock/commands/DoTextTransformCommand.java @@ -85,7 +85,7 @@ public class DoTextTransformCommand extends Command { } }.init(cellIndex, bindings, cellChanges, eval)); - MassCellChange massCellChange = new MassCellChange(cellChanges); + MassCellChange massCellChange = new MassCellChange(cellChanges, cellIndex); HistoryEntry historyEntry = new HistoryEntry( project, "Text transform on " + columnName + ": " + expression, massCellChange); diff --git a/src/main/java/com/metaweb/gridlock/history/MassCellChange.java b/src/main/java/com/metaweb/gridlock/history/MassCellChange.java index e57dd2291..5003c4e6d 100644 --- a/src/main/java/com/metaweb/gridlock/history/MassCellChange.java +++ b/src/main/java/com/metaweb/gridlock/history/MassCellChange.java @@ -8,10 +8,12 @@ import com.metaweb.gridlock.model.Row; public class MassCellChange implements Change { private static final long serialVersionUID = -933571199802688027L; - protected CellChange[] _cellChanges; + protected CellChange[] _cellChanges; + protected int _commonCellIndex; - public MassCellChange(List cellChanges) { + public MassCellChange(List cellChanges, int commonCellIndex) { _cellChanges = new CellChange[cellChanges.size()]; + _commonCellIndex = commonCellIndex; cellChanges.toArray(_cellChanges); } @@ -23,6 +25,10 @@ public class MassCellChange implements Change { for (CellChange cellChange : _cellChanges) { rows.get(cellChange.row).cells.set(cellChange.column, cellChange.newCell); } + + if (_commonCellIndex >= 0) { + project.columnModel.getColumnByCellIndex(_commonCellIndex).clearPrecomputes(); + } } } @@ -34,6 +40,10 @@ public class MassCellChange implements Change { for (CellChange cellChange : _cellChanges) { rows.get(cellChange.row).cells.set(cellChange.column, cellChange.oldCell); } + + if (_commonCellIndex >= 0) { + project.columnModel.getColumnByCellIndex(_commonCellIndex).clearPrecomputes(); + } } } } diff --git a/src/main/java/com/metaweb/gridlock/model/Column.java b/src/main/java/com/metaweb/gridlock/model/Column.java index d933d0d33..8d8f48422 100644 --- a/src/main/java/com/metaweb/gridlock/model/Column.java +++ b/src/main/java/com/metaweb/gridlock/model/Column.java @@ -1,6 +1,8 @@ package com.metaweb.gridlock.model; import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; import java.util.Properties; import org.json.JSONException; @@ -15,6 +17,8 @@ public class Column implements Serializable, Jsonizable { public String headerLabel; public Class valueType; + transient protected Map _precomputes; + @Override public void write(JSONWriter writer, Properties options) throws JSONException { @@ -25,4 +29,24 @@ public class Column implements Serializable, Jsonizable { writer.key("valueType"); writer.value(valueType == null ? null : valueType.getSimpleName()); writer.endObject(); } + + public void clearPrecomputes() { + if (_precomputes != null) { + _precomputes.clear(); + } + } + + public Object getPrecompute(String key) { + if (_precomputes != null) { + return _precomputes.get(key); + } + return null; + } + + public void setPrecompute(String key, Object value) { + if (_precomputes == null) { + _precomputes = new HashMap(); + } + _precomputes.put(key, value); + } } diff --git a/src/main/java/com/metaweb/gridlock/process/ReconProcess.java b/src/main/java/com/metaweb/gridlock/process/ReconProcess.java index ac74dce87..7666663f3 100644 --- a/src/main/java/com/metaweb/gridlock/process/ReconProcess.java +++ b/src/main/java/com/metaweb/gridlock/process/ReconProcess.java @@ -95,7 +95,7 @@ public class ReconProcess extends LongRunningProcess implements Runnable { } } - MassCellChange massCellChange = new MassCellChange(cellChanges); + MassCellChange massCellChange = new MassCellChange(cellChanges, _cellIndex); HistoryEntry historyEntry = new HistoryEntry(_project, _description, massCellChange); _project.history.addEntry(historyEntry); diff --git a/src/main/webapp/scripts/project/data-table-view.js b/src/main/webapp/scripts/project/data-table-view.js index 2851c9ef9..455c6bc79 100644 --- a/src/main/webapp/scripts/project/data-table-view.js +++ b/src/main/webapp/scripts/project/data-table-view.js @@ -240,6 +240,7 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm MenuSystem.createAndShowStandardMenu([ { label: "Filter", + tooltip: "Filter rows by this column's cell content or characteristics", submenu: [ { "heading" : "On Cell Content" }, { @@ -303,6 +304,9 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm "name" : column.headerLabel + ": judgment", "cellIndex" : column.cellIndex, "expression" : "cell.recon.judgment" + }, + { + "scroll" : false } ); } @@ -415,6 +419,7 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm }, { label: "Collapse/Expand", + tooltip: "Collapse/expand columns to make viewing the data more convenient", submenu: [ { label: "Collapse This Column", @@ -475,6 +480,7 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm }, { label: "Reconcile", + tooltip: "Match this column's cells to topics on Freebase", submenu: [ { label: "Start Reconciling ...", diff --git a/src/main/webapp/scripts/project/range-facet.js b/src/main/webapp/scripts/project/range-facet.js index a369b0d08..acb1df51a 100644 --- a/src/main/webapp/scripts/project/range-facet.js +++ b/src/main/webapp/scripts/project/range-facet.js @@ -12,14 +12,14 @@ function RangeFacet(div, config, options) { RangeFacet.prototype._setDefaults = function() { switch (this._config.mode) { case "min": - this._min = this._config.min; + this._from = this._config.min; break; case "max": - this._max = this._config.max; + this._to = this._config.max; break; default: - this._min = this._config.min; - this._max = this._config.max; + this._from = this._config.min; + this._to = this._config.max; } }; @@ -29,18 +29,18 @@ RangeFacet.prototype.getJSON = function() { switch (this._config.mode) { case "min": - o.min = this._min; + o.from = this._from; break; case "max": - o.max = this._max; + o.to = this._to; break; default: - o.min = this._min; - if (this._max == this._config.max) { + o.from = this._from; + if (this._to == this._config.max) { // pretend to be open-ended o.mode = "min"; } else { - o.max = this._max; + o.to = this._to; } } @@ -50,12 +50,12 @@ RangeFacet.prototype.getJSON = function() { RangeFacet.prototype.hasSelection = function() { switch (this._config.mode) { case "min": - return this._min > this._config.min; + return this._from > this._config.min; case "max": - return this._max < this._config.max; + return this._to < this._config.max; default: - return this._min > this._config.min || - this._max < this._config.max; + return this._from > this._config.min || + this._to < this._config.max; } }; @@ -71,21 +71,22 @@ RangeFacet.prototype._initializeUI = function() { }).prependTo(headerDiv); var bodyDiv = $('
').addClass("facet-range-body").appendTo(container); - + + this._histogramDiv = $('
').addClass("facet-range-histogram").appendTo(bodyDiv); this._sliderDiv = $('
').addClass("facet-range-slider").appendTo(bodyDiv); this._statusDiv = $('
').addClass("facet-range-status").appendTo(bodyDiv); var onSlide = function(event, ui) { switch (self._config.mode) { case "min": - self._min = ui.value; + self._from = ui.value; break; case "max": - self._max = ui.value; + self._to = ui.value; break; default: - self._min = ui.values[0]; - self._max = ui.values[1]; + self._from = ui.values[0]; + self._to = ui.values[1]; } self._setRangeIndicators(); self._scheduleUpdate(); @@ -104,15 +105,15 @@ RangeFacet.prototype._initializeUI = function() { switch (this._config.mode) { case "min": sliderConfig.range = "max"; - sliderConfig.value = this._min; + sliderConfig.value = this._from; break; case "max": sliderConfig.range = "min"; - sliderConfig.value = this._max; + sliderConfig.value = this._to; break; default: sliderConfig.range = true; - sliderConfig.values = [ this._min, this._max ]; + sliderConfig.values = [ this._from, this._to ]; } this._sliderDiv.slider(sliderConfig); this._setRangeIndicators(); @@ -122,29 +123,36 @@ RangeFacet.prototype._setRangeIndicators = function() { var text; switch (this._config.mode) { case "min": - text = "At least " + this._min; + text = "At least " + this._from; break; case "max": - text = "At most " + this._max; + text = "At most " + this._to; break; default: - text = this._min + " to " + this._max; + text = this._from + " to " + this._to; } this._statusDiv.text(text); }; RangeFacet.prototype.updateState = function(data) { + this._config.min = data.min; + this._config.max = data.max; + this._config.step = data.step; + this._bins = data.bins; + switch (this._config.mode) { case "min": - this._min = data.min; + this._from = Math.max(data.from, this._config.min); break; case "max": - this._max = data.max; + this._to = Math.min(data.to, this._config.max); break; default: - this._min = data.min; - if ("max" in data) { - this._max = data.max; + 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; } } @@ -152,6 +160,30 @@ RangeFacet.prototype.updateState = function(data) { }; RangeFacet.prototype.render = function() { + this._sliderDiv.slider("option", "min", this._config.min); + this._sliderDiv.slider("option", "max", this._config.max); + this._sliderDiv.slider("option", "step", this._config.step); + + var max = 0; + for (var i = 0; i < this._bins.length; i++) { + max = Math.max(max, this._bins[i]); + } + if (max == 0) { + this._histogramDiv.hide(); + } else { + var a = []; + for (var i = 0; i < this._bins.length; i++) { + a.push(Math.round(100 * this._bins[i] / max)); + } + + this._histogramDiv.empty().show(); + $('').attr("src", + "http://chart.apis.google.com/chart?cht=ls&chs=" + + this._histogramDiv[0].offsetWidth + + "x50&chd=t:" + a.join(",") + ).appendTo(this._histogramDiv); + } + this._setRangeIndicators(); }; diff --git a/src/main/webapp/styles/browsing.css b/src/main/webapp/styles/browsing.css index 79ef7419c..be125dad3 100644 --- a/src/main/webapp/styles/browsing.css +++ b/src/main/webapp/styles/browsing.css @@ -69,12 +69,15 @@ a.facet-choice-link:hover { .facet-range-body { border: 1px solid #ccc; - padding: 5px; + padding: 15px; +} +.facet-range-histogram { + margin-bottom: 10px; } .facet-range-slider { } .facet-range-status { - margin: 5px 0px; + margin-top: 5px; text-align: center; }