added timeline facet (like the numeric binning facet but working on dates instead of numbers and with date-specific binning logic)

git-svn-id: http://google-refine.googlecode.com/svn/trunk@1234 7d457c2a-affb-35e4-300a-418c747d4874
This commit is contained in:
Stefano Mazzocchi 2010-08-31 17:57:54 +00:00
parent 37099b4c9f
commit 5d788c9260
12 changed files with 1108 additions and 4 deletions

View File

@ -15,6 +15,7 @@ import com.google.gridworks.browsing.facets.ListFacet;
import com.google.gridworks.browsing.facets.RangeFacet;
import com.google.gridworks.browsing.facets.ScatterplotFacet;
import com.google.gridworks.browsing.facets.TextSearchFacet;
import com.google.gridworks.browsing.facets.TimeRangeFacet;
import com.google.gridworks.browsing.util.ConjunctiveFilteredRecords;
import com.google.gridworks.browsing.util.ConjunctiveFilteredRows;
import com.google.gridworks.browsing.util.FilteredRecordsAsFilteredRows;
@ -154,6 +155,8 @@ public class Engine implements Jsonizable {
facet = new ListFacet();
} else if ("range".equals(type)) {
facet = new RangeFacet();
} else if ("timerange".equals(type)) {
facet = new TimeRangeFacet();
} else if ("scatterplot".equals(type)) {
facet = new ScatterplotFacet();
} else if ("text".equals(type)) {

View File

@ -72,10 +72,10 @@ public class RangeFacet implements Facet {
public RangeFacet() {
}
private static final String MIN = "min";
private static final String MAX = "max";
private static final String TO = "to";
private static final String FROM = "from";
protected static final String MIN = "min";
protected static final String MAX = "max";
protected static final String TO = "to";
protected static final String FROM = "from";
public void write(JSONWriter writer, Properties options)
throws JSONException {

View File

@ -0,0 +1,201 @@
package com.google.gridworks.browsing.facets;
import java.util.Properties;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONWriter;
import com.google.gridworks.browsing.FilteredRecords;
import com.google.gridworks.browsing.FilteredRows;
import com.google.gridworks.browsing.RowFilter;
import com.google.gridworks.browsing.filters.ExpressionTimeComparisonRowFilter;
import com.google.gridworks.browsing.util.ExpressionTimeValueBinner;
import com.google.gridworks.browsing.util.RowEvaluable;
import com.google.gridworks.browsing.util.TimeBinIndex;
import com.google.gridworks.browsing.util.TimeBinRecordIndex;
import com.google.gridworks.browsing.util.TimeBinRowIndex;
import com.google.gridworks.expr.MetaParser;
import com.google.gridworks.expr.ParsingException;
import com.google.gridworks.model.Column;
import com.google.gridworks.model.Project;
import com.google.gridworks.util.JSONUtilities;
public class TimeRangeFacet extends RangeFacet {
protected boolean _selectTime; // whether the time selection applies, default true
protected boolean _selectNonTime;
protected int _baseTimeCount;
protected int _baseNonTimeCount;
protected int _timeCount;
protected int _nonTimeCount;
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);
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();
writer.key(FROM); writer.value(_from);
writer.key(TO); writer.value(_to);
}
writer.key("baseTimeCount"); writer.value(_baseTimeCount);
writer.key("baseNonTimeCount"); writer.value(_baseNonTimeCount);
writer.key("baseBlankCount"); writer.value(_baseBlankCount);
writer.key("baseErrorCount"); writer.value(_baseErrorCount);
writer.key("timeCount"); writer.value(_timeCount);
writer.key("nonTimeCount"); writer.value(_nonTimeCount);
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();
}
if (o.has(FROM) || o.has(TO)) {
_from = o.has(FROM) ? o.getDouble(FROM) : _min;
_to = o.has(TO) ? o.getDouble(TO) : _max;
_selected = true;
}
_selectTime = JSONUtilities.getBoolean(o, "selectTime", true);
_selectNonTime = JSONUtilities.getBoolean(o, "selectNonTime", true);
_selectBlank = JSONUtilities.getBoolean(o, "selectBlank", true);
_selectError = JSONUtilities.getBoolean(o, "selectError", true);
if (!_selectTime || !_selectNonTime || !_selectBlank || !_selectError) {
_selected = true;
}
}
public RowFilter getRowFilter(Project project) {
if (_eval != null && _errorMessage == null && _selected) {
return new ExpressionTimeComparisonRowFilter(
getRowEvaluable(project), _selectTime, _selectNonTime, _selectBlank, _selectError) {
protected boolean checkValue(long t) {
return t >= _from && t < _to;
};
};
} else {
return null;
}
}
public void computeChoices(Project project, FilteredRows filteredRows) {
if (_eval != null && _errorMessage == null) {
RowEvaluable rowEvaluable = getRowEvaluable(project);
Column column = project.columnModel.getColumnByCellIndex(_cellIndex);
String key = "time-bin:row-based:" + _expression;
TimeBinIndex index = (TimeBinIndex) column.getPrecompute(key);
if (index == null) {
index = new TimeBinRowIndex(project, rowEvaluable);
column.setPrecompute(key, index);
}
retrieveDataFromBaseBinIndex(index);
ExpressionTimeValueBinner binner = new ExpressionTimeValueBinner(rowEvaluable, index);
filteredRows.accept(project, binner);
retrieveDataFromBinner(binner);
}
}
public void computeChoices(Project project, FilteredRecords filteredRecords) {
if (_eval != null && _errorMessage == null) {
RowEvaluable rowEvaluable = getRowEvaluable(project);
Column column = project.columnModel.getColumnByCellIndex(_cellIndex);
String key = "time-bin:record-based:" + _expression;
TimeBinIndex index = (TimeBinIndex) column.getPrecompute(key);
if (index == null) {
index = new TimeBinRecordIndex(project, rowEvaluable);
column.setPrecompute(key, index);
}
retrieveDataFromBaseBinIndex(index);
ExpressionTimeValueBinner binner = new ExpressionTimeValueBinner(rowEvaluable, index);
filteredRecords.accept(project, binner);
retrieveDataFromBinner(binner);
}
}
protected void retrieveDataFromBaseBinIndex(TimeBinIndex index) {
_min = index.getMin();
_max = index.getMax();
_step = index.getStep();
_baseBins = index.getBins();
_baseTimeCount = index.getTimeRowCount();
_baseNonTimeCount = index.getNonTimeRowCount();
_baseBlankCount = index.getBlankRowCount();
_baseErrorCount = index.getErrorRowCount();
if (_selected) {
_from = Math.max(_from, _min);
_to = Math.min(_to, _max);
} else {
_from = _min;
_to = _max;
}
}
protected void retrieveDataFromBinner(ExpressionTimeValueBinner binner) {
_bins = binner.bins;
_timeCount = binner.timeCount;
_nonTimeCount = binner.nonTimeCount;
_blankCount = binner.blankCount;
_errorCount = binner.errorCount;
}
}

View File

@ -0,0 +1,52 @@
package com.google.gridworks.browsing.filters;
import java.util.Date;
import com.google.gridworks.browsing.util.RowEvaluable;
import com.google.gridworks.expr.ExpressionUtils;
/**
* Judge if a row matches by evaluating a given expression on the row, based on a particular
* column, and checking the result. It's a match if the result satisfies some time comparisons,
* or if the result is not a time or blank or error and we want non-time or blank or error
* values.
*/
abstract public class ExpressionTimeComparisonRowFilter extends ExpressionNumberComparisonRowFilter {
final protected boolean _selectTime;
final protected boolean _selectNonTime;
public ExpressionTimeComparisonRowFilter(
RowEvaluable rowEvaluable,
boolean selectTime,
boolean selectNonTime,
boolean selectBlank,
boolean selectError
) {
super(rowEvaluable, selectTime, selectNonTime, selectBlank, selectError);
_selectTime = selectTime;
_selectNonTime = selectNonTime;
}
protected boolean checkValue(Object v) {
if (ExpressionUtils.isError(v)) {
return _selectError;
} else if (ExpressionUtils.isNonBlankData(v)) {
if (v instanceof Date) {
long time = ((Date) v).getTime();
return _selectTime && checkValue(time);
} else {
return _selectNonTime;
}
} else {
return _selectBlank;
}
}
// not really needed for operation, just to make extending the abstract class possible
protected boolean checkValue(double d) {
return false;
}
abstract protected boolean checkValue(long d);
}

View File

@ -0,0 +1,146 @@
package com.google.gridworks.browsing.util;
import java.util.Collection;
import java.util.Date;
import java.util.Properties;
import com.google.gridworks.browsing.RecordVisitor;
import com.google.gridworks.browsing.RowVisitor;
import com.google.gridworks.expr.ExpressionUtils;
import com.google.gridworks.model.Project;
import com.google.gridworks.model.Record;
import com.google.gridworks.model.Row;
/**
* Visit matched rows or records and slot them into bins based on the date computed
* from a given expression.
*/
public class ExpressionTimeValueBinner implements RowVisitor, RecordVisitor {
/*
* Configuration
*/
final protected RowEvaluable _rowEvaluable;
final protected TimeBinIndex _index; // base bins
/*
* Computed results
*/
final public int[] bins;
public int timeCount;
public int nonTimeCount;
public int blankCount;
public int errorCount;
/*
* Scratchpad variables
*/
protected boolean hasError;
protected boolean hasBlank;
protected boolean hasTime;
protected boolean hasNonTime;
public ExpressionTimeValueBinner(RowEvaluable rowEvaluable, TimeBinIndex index) {
_rowEvaluable = rowEvaluable;
_index = index;
bins = new int[_index.getBins().length];
}
@Override
public void start(Project project) {
// nothing to do
}
@Override
public void end(Project project) {
// nothing to do
}
@Override
public boolean visit(Project project, int rowIndex, Row row) {
resetFlags();
Properties bindings = ExpressionUtils.createBindings(project);
processRow(project, rowIndex, row, bindings);
updateCounts();
return false;
}
@Override
public boolean visit(Project project, Record record) {
resetFlags();
Properties bindings = ExpressionUtils.createBindings(project);
for (int r = record.fromRowIndex; r < record.toRowIndex; r++) {
processRow(project, r, project.rows.get(r), bindings);
}
updateCounts();
return false;
}
protected void resetFlags() {
hasError = false;
hasBlank = false;
hasTime = false;
hasNonTime = false;
}
protected void updateCounts() {
if (hasError) {
errorCount++;
}
if (hasBlank) {
blankCount++;
}
if (hasTime) {
timeCount++;
}
if (hasNonTime) {
nonTimeCount++;
}
}
protected void processRow(Project project, int rowIndex, Row row, Properties bindings) {
Object value = _rowEvaluable.eval(project, rowIndex, row, bindings);
if (value != null) {
if (value.getClass().isArray()) {
Object[] a = (Object[]) value;
for (Object v : a) {
processValue(v);
}
return;
} else if (value instanceof Collection<?>) {
for (Object v : ExpressionUtils.toObjectCollection(value)) {
processValue(v);
}
return;
} // else, fall through
}
processValue(value);
}
protected void processValue(Object value) {
if (ExpressionUtils.isError(value)) {
hasError = true;
} else if (ExpressionUtils.isNonBlankData(value)) {
if (value instanceof Date) {
long t = ((Date) value).getTime();
hasTime = true;
int bin = (int) Math.floor((t - _index.getMin()) / _index.getStep());
if (bin >= 0 && bin < bins.length) { // as a precaution
bins[bin]++;
}
} else {
hasNonTime = true;
}
} else {
hasBlank = true;
}
}
}

View File

@ -0,0 +1,218 @@
package com.google.gridworks.browsing.util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import com.google.gridworks.expr.ExpressionUtils;
import com.google.gridworks.model.Project;
import com.google.gridworks.model.Row;
/**
* A utility class for computing the base bins that form the base histograms of
* temporal range facets. It evaluates an expression on all the rows of a project to
* get temporal values, determines how many bins to distribute those values in, and
* bins the rows accordingly.
*
* This class processes all rows rather than just the filtered rows because it
* needs to compute the base bins of a temporal range facet, which remain unchanged
* as the user interacts with the facet.
*/
abstract public class TimeBinIndex {
protected int _totalValueCount;
protected int _timeValueCount;
protected long _min;
protected long _max;
protected long _step;
protected int[] _bins;
protected int _timeRowCount;
protected int _nonTimeRowCount;
protected int _blankRowCount;
protected int _errorRowCount;
protected boolean _hasError = false;
protected boolean _hasNonTime = false;
protected boolean _hasTime = false;
protected boolean _hasBlank = false;
protected long[] steps = {
1, // msec
1000, // sec
1000*60, // min
1000*60*60, // hour
1000*60*60*24, // day
1000*60*60*24*7, // week
1000*2629746, // month (average Gregorian year / 12)
1000*31556952, // year (average Gregorian year)
1000*31556952*10, // decade
1000*31556952*100, // century
1000*31556952*1000, // millennium
};
abstract protected void iterate(Project project, RowEvaluable rowEvaluable, List<Long> allValues);
public TimeBinIndex(Project project, RowEvaluable rowEvaluable) {
_min = Long.MAX_VALUE;
_max = Long.MIN_VALUE;
List<Long> allValues = new ArrayList<Long>();
iterate(project, rowEvaluable, allValues);
_timeValueCount = allValues.size();
if (_min >= _max) {
_step = 1;
_min = Math.min(_min, _max);
_max = _step;
_bins = new int[1];
return;
}
long diff = _max - _min;
for (int i = 0; i < steps.length; i++) {
_step = steps[i];
if (diff / _step <= 100) break;
}
_bins = new int[(int) (diff / _step) + 1];
for (long d : allValues) {
int bin = (int) Math.max((d - _min) / _step,0);
_bins[bin]++;
}
}
public boolean isTemporal() {
return _timeValueCount > _totalValueCount / 2;
}
public long getMin() {
return _min;
}
public long getMax() {
return _max;
}
public long getStep() {
return _step;
}
public int[] getBins() {
return _bins;
}
public int getTimeRowCount() {
return _timeRowCount;
}
public int getNonTimeRowCount() {
return _nonTimeRowCount;
}
public int getBlankRowCount() {
return _blankRowCount;
}
public int getErrorRowCount() {
return _errorRowCount;
}
protected void processRow(
Project project,
RowEvaluable rowEvaluable,
List<Long> allValues,
int rowIndex,
Row row,
Properties bindings
) {
Object value = rowEvaluable.eval(project, rowIndex, row, bindings);
if (ExpressionUtils.isError(value)) {
_hasError = true;
} else if (ExpressionUtils.isNonBlankData(value)) {
if (value.getClass().isArray()) {
Object[] a = (Object[]) value;
for (Object v : a) {
_totalValueCount++;
if (ExpressionUtils.isError(v)) {
_hasError = true;
} else if (ExpressionUtils.isNonBlankData(v)) {
if (v instanceof Date) {
_hasTime = true;
processValue(((Date) v).getTime(), allValues);
} else {
_hasNonTime = true;
}
} else {
_hasBlank = true;
}
}
} else if (value instanceof Collection<?>) {
for (Object v : ExpressionUtils.toObjectCollection(value)) {
_totalValueCount++;
if (ExpressionUtils.isError(v)) {
_hasError = true;
} else if (ExpressionUtils.isNonBlankData(v)) {
if (v instanceof Date) {
_hasTime = true;
processValue(((Date) v).getTime(), allValues);
} else {
_hasNonTime = true;
}
} else {
_hasBlank = true;
}
}
} else {
_totalValueCount++;
if (value instanceof Date) {
_hasTime = true;
processValue(((Date) value).getTime(), allValues);
} else {
_hasNonTime = true;
}
}
} else {
_hasBlank = true;
}
}
protected void preprocessing() {
_hasBlank = false;
_hasError = false;
_hasNonTime = false;
_hasTime = false;
}
protected void postprocessing() {
if (_hasError) {
_errorRowCount++;
}
if (_hasBlank) {
_blankRowCount++;
}
if (_hasTime) {
_timeRowCount++;
}
if (_hasNonTime) {
_nonTimeRowCount++;
}
}
protected void processValue(long v, List<Long> allValues) {
_min = Math.min(_min, v);
_max = Math.max(_max, v);
allValues.add(v);
}
}

View File

@ -0,0 +1,38 @@
package com.google.gridworks.browsing.util;
import java.util.List;
import java.util.Properties;
import com.google.gridworks.expr.ExpressionUtils;
import com.google.gridworks.model.Project;
import com.google.gridworks.model.Record;
import com.google.gridworks.model.Row;
public class TimeBinRecordIndex extends TimeBinIndex {
public TimeBinRecordIndex(Project project, RowEvaluable rowEvaluable) {
super(project, rowEvaluable);
}
@Override
protected void iterate(Project project, RowEvaluable rowEvaluable, List<Long> allValues) {
Properties bindings = ExpressionUtils.createBindings(project);
int count = project.recordModel.getRecordCount();
for (int r = 0; r < count; r++) {
Record record = project.recordModel.getRecord(r);
preprocessing();
for (int i = record.fromRowIndex; i < record.toRowIndex; i++) {
Row row = project.rows.get(i);
processRow(project, rowEvaluable, allValues, i, row, bindings);
}
postprocessing();
}
}
}

View File

@ -0,0 +1,32 @@
package com.google.gridworks.browsing.util;
import java.util.List;
import java.util.Properties;
import com.google.gridworks.expr.ExpressionUtils;
import com.google.gridworks.model.Project;
import com.google.gridworks.model.Row;
public class TimeBinRowIndex extends TimeBinIndex {
public TimeBinRowIndex(Project project, RowEvaluable rowEvaluable) {
super(project, rowEvaluable);
}
@Override
protected void iterate(Project project, RowEvaluable rowEvaluable, List<Long> allValues) {
Properties bindings = ExpressionUtils.createBindings(project);
for (int i = 0; i < project.rows.size(); i++) {
Row row = project.rows.get(i);
preprocessing();
processRow(project, rowEvaluable, allValues, i, row, bindings);
postprocessing();
}
}
}

View File

@ -208,6 +208,7 @@ function init() {
"scripts/facets/list-facet.js",
"scripts/facets/range-facet.js",
"scripts/facets/timerange-facet.js",
"scripts/facets/scatterplot-facet.js",
"scripts/facets/text-search-facet.js",

View File

@ -0,0 +1,392 @@
function TimeRangeFacet(div, config, options) {
this._div = div;
this._config = config;
this._options = options;
this._from = ("from" in this._config) ? this._config.from : null;
this._to = ("to" in this._config) ? this._config.to : null;
this._step = ("step" in this._config) ? this._config.step : null;
this._selectTime = ("selectTime" in this._config) ? this._config.selectTime : true;
this._selectNonTime = ("selectNonTime" in this._config) ? this._config.selectNonTime : true;
this._selectBlank = ("selectBlank" in this._config) ? this._config.selectBlank : true;
this._selectError = ("selectError" in this._config) ? this._config.selectError : true;
this._baseTimeCount = 0;
this._baseNonTimeCount = 0;
this._baseBlankCount = 0;
this._baseErrorCount = 0;
this._TimeCount = 0;
this._nonTimeCount = 0;
this._blankCount = 0;
this._errorCount = 0;
this._error = false;
this._initializedUI = false;
}
TimeRangeFacet.prototype.reset = function() {
this._from = this._config.min;
this._to = this._config.max;
this._sliderWidget.update(
this._config.min,
this._config.max,
this._config.step,
this._from,
this._to
);
this._selectTime = true;
this._selectNonTime = true;
this._selectBlank = true;
this._selectError = true;
this._setRangeIndicators();
};
TimeRangeFacet.reconstruct = function(div, uiState) {
return new TimeRangeFacet(div, uiState.c, uiState.o);
};
TimeRangeFacet.prototype.dispose = function() {
};
TimeRangeFacet.prototype.getUIState = function() {
var json = {
c: this.getJSON(),
o: this._options
};
return json;
};
TimeRangeFacet.prototype.getJSON = function() {
var o = {
type: "timerange",
name: this._config.name,
expression: this._config.expression,
columnName: this._config.columnName,
selectTime: this._selectTime,
selectNonTime: this._selectNonTime,
selectBlank: this._selectBlank,
selectError: this._selectError
};
if (this._from !== null) {
o.from = this._from;
}
if (this._to !== null) {
o.to = this._to;
}
return o;
};
TimeRangeFacet.prototype.hasSelection = function() {
if (!this._selectTime || !this._selectNonTime || !this._selectBlank || !this._selectError) {
return true;
}
return (this._from !== null && (!this._initializedUI || this._from > this._config.min)) ||
(this._to !== null && (!this._initializedUI || this._to < this._config.max));
};
TimeRangeFacet.prototype._initializeUI = function() {
var self = this;
this._div
.empty()
.show()
.html(
'<div class="facet-title" bind="headerDiv">' +
'<div class="grid-layout layout-tightest layout-full"><table><tr>' +
'<td width="1%"><a href="javascript:{}" title="Remove this facet" class="facet-title-remove" bind="removeButton">&nbsp;</a></td>' +
'<td>' +
'<a href="javascript:{}" class="facet-choice-link" bind="resetButton">reset</a>' +
'<a href="javascript:{}" class="facet-choice-link" bind="changeButton">change</a>' +
'<span bind="facetTitle"></span>' +
'</td>' +
'</tr></table></div>' +
'</div>' +
'<div class="facet-expression" bind="expressionDiv"></div>' +
'<div class="facet-range-body">' +
'<div class="facet-range-message" bind="messageDiv">Loading...</div>' +
'<div class="facet-range-slider" bind="sliderWidgetDiv">' +
'<div class="facet-range-histogram" bind="histogramDiv"></div>' +
'</div>' +
'<div class="facet-range-status" bind="statusDiv"></div>' +
'<div class="facet-range-other-choices" bind="otherChoicesDiv"></div>' +
'</div>'
);
this._elmts = DOM.bind(this._div);
this._elmts.facetTitle.text(this._config.name);
this._elmts.changeButton.attr("title","Current Expression: " + this._config.expression).click(function() {
self._elmts.expressionDiv.slideToggle(100);
});
this._elmts.expressionDiv.text(this._config.expression).click(function() {
self._editExpression();
}).hide();
this._elmts.resetButton.click(function() {
self.reset();
self._updateRest();
});
this._elmts.removeButton.click(function() {
self._remove();
});
this._histogram = new HistogramWidget(this._elmts.histogramDiv, { binColors: [ "#ccccff", "#6666ff" ] });
this._sliderWidget = new SliderWidget(this._elmts.sliderWidgetDiv);
this._elmts.sliderWidgetDiv.bind("slide", function(evt, data) {
self._from = data.from;
self._to = data.to;
self._setRangeIndicators();
}).bind("stop", function(evt, data) {
self._from = data.from;
self._to = data.to;
self._selectTime = true;
self._updateRest();
});
};
TimeRangeFacet.prototype._renderOtherChoices = function() {
var self = this;
var container = this._elmts.otherChoicesDiv.empty();
if (this._baseNonTimeCount === 0 && this._baseBlankCount === 0 && this._baseErrorCount === 0) {
return;
}
var facet_id = this._div.attr("id");
var choices = $('<div>').addClass("facet-range-choices");
// ----------------- time -----------------
var timeCheck = $('<input type="checkbox" />').attr("id",facet_id + "-time").appendTo(choices).change(function() {
self._selectTime = !self._selectTime;
self._updateRest();
});
if (this._selectTime) timeCheck.attr("checked","checked");
var timeLabel = $('<label>').attr("for", facet_id + "-time").appendTo(choices);
$('<span>').text("Time ").addClass("facet-choice-label").appendTo(timeLabel);
$('<br>').appendTo(timeLabel);
$('<span>').text(this._timeCount).addClass("facet-choice-count").appendTo(timeLabel);
// ----------------- non-Time -----------------
var nonTimeCheck = $('<input type="checkbox" />').attr("id",facet_id + "-non-time").appendTo(choices).change(function() {
self._selectNonTime = !self._selectNonTime;
self._updateRest();
});
if (this._selectNonTime) nonTimeCheck.attr("checked","checked");
var nonTimeLabel = $('<label>').attr("for", facet_id + "-non-time").appendTo(choices);
$('<span>').text("Non-Time ").addClass("facet-choice-label").appendTo(nonTimeLabel);
$('<br>').appendTo(nonTimeLabel);
$('<span>').text(this._nonTimeCount).addClass("facet-choice-count").appendTo(nonTimeLabel);
if (this._baseNonTimeCount === 0) nonTimeCheck.removeAttr("checked");
// ----------------- blank -----------------
var blankCheck = $('<input type="checkbox" />').attr("id",facet_id + "-blank").appendTo(choices).change(function() {
self._selectBlank = !self._selectBlank;
self._updateRest();
});
if (this._selectBlank) blankCheck.attr("checked","checked");
var blankLabel = $('<label>').attr("for", facet_id + "-blank").appendTo(choices);
$('<span>').text("Blank ").addClass("facet-choice-label").appendTo(blankLabel);
$('<br>').appendTo(blankLabel);
$('<span>').text(this._blankCount).addClass("facet-choice-count").appendTo(blankLabel);
if (this._baseBlankCount === 0) blankCheck.removeAttr("checked");
// ----------------- error -----------------
var errorCheck = $('<input type="checkbox" />').attr("id",facet_id + "-error").appendTo(choices).change(function() {
self._selectError = !self._selectError;
self._updateRest();
});
if (this._selectError) errorCheck.attr("checked","checked");
var errorLabel = $('<label>').attr("for", facet_id + "-error").appendTo(choices);
$('<span>').text("Error ").addClass("facet-choice-label").appendTo(errorLabel);
$('<br>').appendTo(errorLabel);
$('<span>').text(this._errorCount).addClass("facet-choice-count").appendTo(errorLabel);
if (this._baseErrorCount === 0) errorCheck.removeAttr("checked");
// --------------------------
choices.buttonset().appendTo(container);
};
TimeRangeFacet.prototype.steps = [
1, // msec
1000, // sec
1000*60, // min
1000*60*60, // hour
1000*60*60*24, // day
1000*60*60*24*7, // week
1000*2629746, // month (average Gregorian year / 12)
1000*31556952, // year (average Gregorian year)
1000*31556952*10, // decade
1000*31556952*100, // century
1000*31556952*1000, // millennium
];
TimeRangeFacet.prototype._setRangeIndicators = function() {
var fromDate = new Date(this._from);
var toDate = new Date(this._to);
if (this._step > 2629746000) { // > month
var format = "yyyy";
this._elmts.statusDiv.html(fromDate.toString(format) + " &mdash; " + toDate.toString(format));
} else if (this.step > 3600000) { // > hour
var format = "yyyy-MM-dd";
this._elmts.statusDiv.html(fromDate.toString(format) + " &mdash; " + toDate.toString(format));
} else {
var format = "HH:mm:ss";
this._elmts.statusDiv.html("<b style='margin-right: 4em'>" + fromDate.toString("yyyy-MM-dd") + "</b> " + fromDate.toString(format) + " &mdash; " + toDate.toString(format));
}
};
TimeRangeFacet.prototype._addCommas = function(nStr) {
nStr += '';
x = nStr.split('.');
x1 = x[0];
x2 = x.length > 1 ? '.' + x[1] : '';
var rgx = /(\d+)(\d{3})/;
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + ',' + '$2');
}
return x1 + x2;
};
TimeRangeFacet.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._baseTimeCount = data.baseTimeCount;
this._baseNonTimeCount = data.baseNonTimeCount;
this._baseBlankCount = data.baseBlankCount;
this._baseErrorCount = data.baseErrorCount;
this._timeCount = data.timeCount;
this._nonTimeCount = data.nonTimeCount;
this._blankCount = data.blankCount;
this._errorCount = data.errorCount;
} else {
this._error = true;
this._errorMessage = "error" in data ? data.error : "Unknown error.";
}
this.render();
};
TimeRangeFacet.prototype.render = function() {
if (!this._initializedUI) {
this._initializeUI();
this._initializedUI = true;
}
if (this._error) {
this._elmts.messageDiv.text(this._errorMessage).show();
this._elmts.sliderWidgetDiv.hide();
this._elmts.histogramDiv.hide();
this._elmts.statusDiv.hide();
this._elmts.otherChoicesDiv.hide();
return;
}
this._elmts.messageDiv.hide();
this._elmts.sliderWidgetDiv.show();
this._elmts.histogramDiv.show();
this._elmts.statusDiv.show();
this._elmts.otherChoicesDiv.show();
this._sliderWidget.update(
this._config.min,
this._config.max,
this._config.step,
this._from,
this._to
);
this._histogram.update(
this._config.min,
this._config.max,
this._config.step,
[ this._baseBins, this._bins ]
);
this._setRangeIndicators();
this._renderOtherChoices();
};
TimeRangeFacet.prototype._remove = function() {
ui.browsingEngine.removeFacet(this);
this._div = null;
this._config = null;
this._data = null;
};
TimeRangeFacet.prototype._updateRest = function() {
Gridworks.update({ engineChanged: true });
};
TimeRangeFacet.prototype._editExpression = function() {
var self = this;
var title = (this._config.columnName) ?
("Edit Facet's Expression based on Column " + this._config.columnName) :
"Edit Facet's Expression";
var column = Gridworks.columnNameToColumn(this._config.columnName);
var o = DataTableView.sampleVisibleRows(column);
new ExpressionPreviewDialog(
title,
column ? column.cellIndex : -1,
o.rowIndices,
o.values,
this._config.expression,
function(expr) {
if (expr != self._config.expression) {
self._config.expression = expr;
self._elmts.expressionDiv.text(self._config.expression);
self.reset();
self._from = null;
self._to = null;
self._updateRest();
}
}
);
};

View File

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

View File

@ -49,6 +49,21 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
);
}
},
{
id: "core/time-facet",
label: "Timeline Facet",
click: function() {
ui.browsingEngine.addFacet(
"timerange",
{
"name": column.name,
"columnName": column.name,
"expression": "value",
"mode": "range"
}
);
}
},
{
id: "core/scatterplot-facet",
label: "Scatterplot Facet",