Added histograms to range slider facets.

git-svn-id: http://google-refine.googlecode.com/svn/trunk@25 7d457c2a-affb-35e4-300a-418c747d4874
This commit is contained in:
David Huynh 2010-02-03 00:20:42 +00:00
parent 00696a96fc
commit ed5eae83af
12 changed files with 240 additions and 50 deletions

View File

@ -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<Double> allValues = new ArrayList<Double>();
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<Double> allValues) {
min = Math.min(min, v);
max = Math.max(max, v);
allValues.add(v);
}
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -9,9 +9,11 @@ public class MassCellChange implements Change {
private static final long serialVersionUID = -933571199802688027L;
protected CellChange[] _cellChanges;
protected int _commonCellIndex;
public MassCellChange(List<CellChange> cellChanges) {
public MassCellChange(List<CellChange> 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();
}
}
}
}

View File

@ -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<String, Object> _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<String, Object>();
}
_precomputes.put(key, value);
}
}

View File

@ -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);

View File

@ -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 ...",

View File

@ -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;
}
};
@ -72,20 +72,21 @@ RangeFacet.prototype._initializeUI = function() {
var bodyDiv = $('<div></div>').addClass("facet-range-body").appendTo(container);
this._histogramDiv = $('<div></div>').addClass("facet-range-histogram").appendTo(bodyDiv);
this._sliderDiv = $('<div></div>').addClass("facet-range-slider").appendTo(bodyDiv);
this._statusDiv = $('<div></div>').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();
$('<img />').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();
};

View File

@ -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;
}