Merge branch 'master' into gh-pages

This commit is contained in:
Rufus Pollock 2012-06-05 11:15:39 +01:00
commit 0ce9ed366e
16 changed files with 795 additions and 482 deletions

View File

@ -67,7 +67,7 @@
<script type="text/javascript" src="../src/widget.pager.js"></script>
<script type="text/javascript" src="../src/widget.queryeditor.js"></script>
<script type="text/javascript" src="../src/widget.filtereditor.js"></script>
<script type="text/javascript" src="../src/widget.facetviewer.js"></script>
<script type="text/javascript" src="../src/widget.fields.js"></script>
<script type="text/javascript" src="../src/view.multiview.js"></script>
<!-- non-library javascript specific to this demo -->
@ -84,22 +84,38 @@
<ul class="nav pull-right">
<li class="dropdown">
<a data-toggle="dropdown" class="dropdown-toggle">
Import <b class="caret"></b></a>
<ul class="dropdown-menu js-import">
Load Data <b class="caret"></b></a>
<ul class="dropdown-menu js-load">
<li>
<a data-toggle="modal" href=".js-import-dialog-url">Import from URL</a>
<a href="#" class="js-load-dialog-url" data-type="datahub" data-help="The link to the Dataset Data Resource on the DataHub to load from - note that the resource must have its Data API enabled">Load from the DataHub</a>
</li>
<li>
<a data-toggle="modal" href=".js-import-dialog-file">Import from File</a>
<a href="#" class="js-load-dialog-url" data-type="csv" data-help="Provide the link to the CSV file online">Load from CSV online</a>
</li>
<li>
<a href="#" class="js-load-dialog-url" data-type="excel" data-help="Provide the link to the Excel file online">Load from Excel online</a>
</li>
<li>
<a href="#" class="js-load-dialog-url" data-type="elasticsearch" data-help="Provide the link to the ElasticSearch endpoint (either an index or a type/table">Load from ElasticSearch</a>
</li>
<li class="divider"></li>
<li>
<a data-toggle="modal" href=".js-load-dialog-file">Load from CSV on disk</a>
</li>
</ul>
</li>
<li>
<a href=".js-share-and-embed-dialog" data-toggle="modal">
<a href=".js-share-and-embed-dialog" data-toggle="modal">
Share and Embed
<i class="icon-share icon-white"></i>
</a>
</li>
<li>
<a href=".js-settings" data-toggle="modal">
Settings
<i class="icon-cog icon-white" style="margin-top: 1px;"></i>
</a>
</li>
</ul>
</div>
</div>
@ -127,7 +143,7 @@
<li>Data grid</li>
<li>Data editing including programmatic data transformation in javascript</li>
<li>Visualizations includes graphs and maps</li>
<li>Import and export from a variety of sources including online sources such as online Excel and CSV files, Google docs and
<li>Load and export from a variety of sources including online sources such as online Excel and CSV files, Google docs and
the <a href="http://datahub.io/">DataHub</a> and offline sources like CSV files on your local machine.</li>
<li>Use online or offline - because the app is built in pure javascript and html you can use it anywhere there's a modern web browser. Using offline is as easy and downloading this web page to your local machine.</li>
</ul>
@ -149,41 +165,29 @@
</div>
<!-- modals for menus -->
<div class="modal fade in js-import-dialog-url" style="display: none;">
<div class="modal fade in js-load-dialog-url" style="display: none;">
<div class="modal-header">
<a class="close" data-dismiss="modal">×</a>
<h3>Import from URL</h3>
<h3>Load from URL</h3>
</div>
<div class="modal-body">
<form class="js-import-url form-horizontal">
<form class="js-load-url">
<div class="control-group">
<label class="control-label">URL</label>
<div class="controls">
<input type="text" name="source" class="input-xlarge" />
<input type="text" name="source" class="span5" placeholder="URL to data source" />
<p class="help-block"></p>
<input name="backend_type" style="display: none;" />
</div>
</div>
<div class="control-group">
<label class="control-label">Type of data</label>
<div class="controls">
<select name="backend_type">
<option value="csv">CSV</option>
<option vlaue="excel">Excel</option>
<option value="gdocs">Google Spreadsheet</option>
<option value="elasticsearch">ElasticSearch</option>
</select>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Import &raquo;</button>
</div>
<button type="submit" class="btn btn-primary">Load &raquo;</button>
</form>
</div>
</div>
<div class="modal fade in js-import-dialog-file" style="display: none;">
<div class="modal fade in js-load-dialog-file" style="display: none;">
<div class="modal-header">
<a class="close" data-dismiss="modal">×</a>
<h3>Import from File</h3>
<h3>Load from File</h3>
</div>
<div class="modal-body">
<form class="form-horizontal">
@ -213,7 +217,7 @@
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Import &raquo;</button>
<button type="submit" class="btn btn-primary">Load &raquo;</button>
</div>
</form>
</div>
@ -231,6 +235,27 @@
<textarea class="view-embed" style="width: 100%; height: 200px;"></textarea>
</div>
</div>
<div class="modal fade in js-settings" style="display: none;">
<div class="modal-header">
<a class="close" data-dismiss="modal">×</a>
<h3>Settings</h3>
</div>
<div class="modal-body">
<form class="form-horizontal">
<div class="control-group">
<label class="control-label">DataHub API Key</label>
<div class="controls">
<input type="text" name="datahub_api_key" value="" />
<p class="help-block"><strong>Getting your API key:</strong> Register/Login to <a href="http://datahub.io/">http://datahub.io/</a> and then visit your user home page (click on the link at the top right). On your home page your API key is located at the top of the page in the section showing your main user details.</p>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save &raquo;</button>
</div>
</form>
</div>
</div>
</div>
</body>
</html>

View File

@ -6,8 +6,10 @@ jQuery(function($) {
var ExplorerApp = Backbone.View.extend({
events: {
'submit form.js-import-url': '_onImportURL',
'submit .js-import-dialog-file form': '_onImportFile'
'click .nav .js-load-dialog-url': '_onLoadURLDialog',
'submit form.js-load-url': '_onLoadURL',
'submit .js-load-dialog-file form': '_onLoadFile',
'submit .js-settings form': '_onSettingsSave'
},
initialize: function() {
@ -45,6 +47,7 @@ var ExplorerApp = Backbone.View.extend({
if (dataset) {
this.createExplorer(dataset, state);
}
this._initializeSettings();
},
viewHome: function() {
@ -153,33 +156,52 @@ var ExplorerApp = Backbone.View.extend({
setupLoader: function(callback) {
// pre-populate webstore load form with an example url
var demoUrl = 'http://thedatahub.org/api/data/b9aae52b-b082-4159-b46f-7bb9c158d013';
$('form.js-import-url input[name="source"]').val(demoUrl);
$('form.js-load-url input[name="source"]').val(demoUrl);
},
_onImportURL: function(e) {
_onLoadURLDialog: function(e) {
e.preventDefault();
$('.modal.js-import-dialog-url').modal('hide');
var $link = $(e.target);
var $modal = $('.modal.js-load-dialog-url');
$modal.find('h3').text($link.text());
$modal.modal('show');
$modal.find('input[name="source"]').val('');
$modal.find('input[name="backend_type"]').val($link.attr('data-type'));
$modal.find('.help-block').text($link.attr('data-help'));
},
_onLoadURL: function(e) {
e.preventDefault();
$('.modal.js-load-dialog-url').modal('hide');
var $form = $(e.target);
var source = $form.find('input[name="source"]').val();
var datasetInfo = {
id: 'my-dataset',
url: source,
webstore_url: source
url: source
};
var type = $form.find('select[name="backend_type"]').val();
var type = $form.find('input[name="backend_type"]').val();
if (type === 'csv' || type === 'excel') {
datasetInfo.format = type;
type = 'dataproxy';
}
if (type === 'datahub') {
// have a full resource url so convert to data API
if (source.indexOf('dataset') != -1) {
var parts = source.split('/');
datasetInfo.url = parts[0] + '/' + parts[1] + '/' + parts[2] + '/api/data/' + parts[parts.length-1];
}
type = 'elasticsearch';
}
console.log(datasetInfo.url);
var dataset = new recline.Model.Dataset(datasetInfo, type);
this.createExplorer(dataset);
},
_onImportFile: function(e) {
_onLoadFile: function(e) {
var self = this;
e.preventDefault();
var $form = $(e.target);
$('.modal.js-import-dialog-file').modal('hide');
$('.modal.js-load-dialog-file').modal('hide');
var $file = $form.find('input[type="file"]')[0];
var file = $file.files[0];
var options = {
@ -192,13 +214,34 @@ var ExplorerApp = Backbone.View.extend({
},
options
);
},
_getSettings: function() {
var settings = localStorage.getItem('dataexplorer.settings');
settings = JSON.parse(settings) || {};
return settings;
},
_initializeSettings: function() {
var settings = this._getSettings();
$('.modal.js-settings form input[name="datahub_api_key"]').val(settings.datahubApiKey);
},
_onSettingsSave: function(e) {
var self = this;
e.preventDefault();
var $form = $(e.target);
$('.modal.js-settings').modal('hide');
var datahubKey = $form.find('input[name="datahub_api_key"]').val();
var settings = this._getSettings();
settings.datahubApiKey = datahubKey;
localStorage.setItem('dataexplorer.settings', JSON.stringify(settings));
}
});
// provide a demonstration in memory dataset
function localDataset() {
var dataset = Fixture.getDataset();
dataset.queryState.addFacet('country');
return dataset;
}

View File

@ -1,10 +1,17 @@
.recline-data-explorer .data-view-container {
display: block;
clear: both;
}
.recline-data-explorer .data-view-sidebar {
float: right;
margin-left: 8px;
}
.recline-data-explorer .header .navigation {
margin-bottom: 8px;
}
.recline-data-explorer .header .navigation,
.recline-data-explorer .header .navigation li,
.recline-data-explorer .header .pagination,
.recline-data-explorer .header .pagination form
{
@ -13,8 +20,6 @@
.recline-data-explorer .header .navigation {
float: left;
margin-left: 0;
padding-left: 0;
}
.recline-data-explorer .header .menu-right {
@ -97,6 +102,43 @@
display: inline;
}
/**********************************************************
* Fields Widget
*********************************************************/
.recline-fields-view {
width: 200px;
}
.recline-fields-view .fields-list {
padding: 0;
}
.recline-fields-view .fields-list .accordion-heading a,
.recline-fields-view .fields-list .accordion-heading h4 {
display: inline;
}
.recline-fields-view .fields-list .accordion-heading h4 {
margin-left: 10px;
}
.recline-fields-view .clear {
clear: both;
}
.recline-fields-view .facet-items {
list-style-type: none;
margin-left: 0;
}
.recline-fields-view .facet-item .term {
font-weight: bold;
}
.recline-fields-view .facet-item .count {
}
/**********************************************************
* Notifications
*********************************************************/

View File

@ -31,11 +31,15 @@ Recline has dependencies on some third-party libraries, notably JQuery and Backb
Optional dependencies:
* JQuery Mustache (required for all views)
* [Mustache.js](https://github.com/janl/mustache.js/) &gt;= 0.5.0-dev (required for all views)
* [JQuery Flot](http://code.google.com/p/flot/) >= 0.7 (required for for graph view)
* [Leaflet](http://leaflet.cloudmade.com/) >= 0.3.1 (required for map view
* [Verite Timeline](https://github.com/VeriteCo/Timeline/) as of 2012-05-02
* [Bootstrap](http://twitter.github.com/bootstrap/) &gt;= v2.0 (default option for CSS and UI JS but you can use your own)
If you grab the full zipball for Recline this will include all of the relevant
dependencies in the vendor directory.
### Example
Here is an example of the page setup for an app using every Recline component:

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
// @param metadata: (optional) dataset metadata - see recline.Model.Dataset.
// If not defined (or id not provided) id will be autogenerated.
my.createDataset = function(data, fields, metadata) {
var wrapper = new my.DataWrapper(data, fields);
var wrapper = new my.Store(data, fields);
var backend = new my.Backbone();
var dataset = new recline.Model.Dataset(metadata, backend);
dataset._dataCache = wrapper;
@ -29,7 +29,13 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
// Turn a simple array of JS objects into a mini data-store with
// functionality like querying, faceting, updating (by ID) and deleting (by
// ID).
my.DataWrapper = function(data, fields) {
//
// @param data list of hashes for each record/row in the data ({key:
// value, key: value})
// @param fields (optional) list of field hashes (each hash defining a field
// as per recline.Model.Field). If fields not specified they will be taken
// from the data.
my.Store = function(data, fields) {
var self = this;
this.data = data;
if (fields) {

View File

@ -111,6 +111,31 @@ my.Dataset = Backbone.Model.extend({
return data;
},
// Get a summary for each field in the form of a `Facet`.
//
// @return null as this is async function. Provides deferred/promise interface.
getFieldsSummary: function() {
var self = this;
var query = new my.Query();
query.set({size: 0});
this.fields.each(function(field) {
query.addFacet(field.id);
});
var dfd = $.Deferred();
this.backend.query(this, query.toJSON()).done(function(queryResult) {
if (queryResult.facets) {
_.each(queryResult.facets, function(facetResult, facetId) {
facetResult.id = facetId;
var facet = new my.Facet(facetResult);
// TODO: probably want replace rather than reset (i.e. just replace the facet with this id)
self.fields.get(facetId).facets.reset(facet);
});
}
dfd.resolve(queryResult);
});
return dfd.promise();
},
// ### _backendFromString(backendString)
//
// See backend argument to initialize for details
@ -190,13 +215,22 @@ my.Record = Backbone.Model.extend({
// For the provided Field get the corresponding rendered computed data value
// for this record.
getFieldValue: function(field) {
val = this.getFieldValueUnrendered(field);
if (field.renderer) {
val = field.renderer(val, field, this.toJSON());
}
return val;
},
// ### getFieldValueUnrendered
//
// For the provided Field get the corresponding computed data value
// for this record.
getFieldValueUnrendered: function(field) {
var val = this.get(field.id);
if (field.deriver) {
val = field.deriver(val, field, this);
}
if (field.renderer) {
val = field.renderer(val, field, this);
}
return val;
},
@ -233,9 +267,9 @@ my.RecordList = Backbone.Collection.extend({
// Following additional instance properties:
//
// @property {Function} renderer: a function to render the data for this field.
// Signature: function(value, field, doc) where value is the value of this
// Signature: function(value, field, record) where value is the value of this
// cell, field is corresponding field object and record is the record
// object. Note that implementing functions can ignore arguments (e.g.
// object (as simple JS object). Note that implementing functions can ignore arguments (e.g.
// function(value) would be a valid formatter function).
//
// @property {Function} deriver: a function to derive/compute the value of data
@ -282,6 +316,7 @@ my.Field = Backbone.Model.extend({
if (!this.renderer) {
this.renderer = this.defaultRenderers[this.get('type')];
}
this.facets = new my.FacetList();
},
defaultRenderers: {
object: function(val, field, doc) {

View File

@ -68,12 +68,26 @@ my.SlickGrid = Backbone.View.extend({
// We need all columns, even the hidden ones, to show on the column picker
var columns = [];
// custom formatter as default one escapes html
// plus this way we distinguish between rendering/formatting and computed value (so e.g. sort still works ...)
// row = row index, cell = cell index, value = value, columnDef = column definition, dataContext = full row values
var formatter = function(row, cell, value, columnDef, dataContext) {
var field = self.model.fields.get(columnDef.id);
if (field.renderer) {
return field.renderer(value, field, dataContext);
} else {
return value;
}
}
_.each(this.model.fields.toJSON(),function(field){
var column = {id:field['id'],
name:field['label'],
field:field['id'],
sortable: true,
minWidth: 80};
var column = {
id:field['id'],
name:field['label'],
field:field['id'],
sortable: true,
minWidth: 80,
formatter: formatter
};
var widthInfo = _.find(self.state.get('columnsWidth'),function(c){return c.column == field.id});
if (widthInfo){
@ -113,7 +127,7 @@ my.SlickGrid = Backbone.View.extend({
this.model.currentRecords.each(function(doc){
var row = {};
self.model.fields.each(function(field){
row[field.id] = doc.getFieldValue(field);
row[field.id] = doc.getFieldValueUnrendered(field);
});
data.push(row);
});

View File

@ -92,6 +92,14 @@ my.Timeline = Backbone.View.extend({
out.timeline.date.push(tlEntry);
}
});
// if no entries create a placeholder entry to prevent Timeline crashing with error
if (out.timeline.date.length === 0) {
var tlEntry = {
"startDate": '2000,1,1',
"headline": 'No data to show!'
};
out.timeline.date.push(tlEntry);
}
return out;
},

View File

@ -75,21 +75,26 @@ my.MultiView = Backbone.View.extend({
<div class="alert-messages"></div> \
\
<div class="header"> \
<ul class="navigation"> \
<div class="navigation"> \
<div class="btn-group" data-toggle="buttons-radio"> \
{{#views}} \
<li><a href="#{{id}}" data-view="{{id}}" class="btn">{{label}}</a> \
<a href="#{{id}}" data-view="{{id}}" class="btn">{{label}}</a> \
{{/views}} \
</ul> \
</div> \
</div> \
<div class="recline-results-info"> \
Results found <span class="doc-count">{{docCount}}</span> \
</div> \
<div class="menu-right"> \
<a href="#" class="btn" data-action="filters">Filters</a> \
<a href="#" class="btn" data-action="facets">Facets</a> \
<div class="btn-group" data-toggle="buttons-checkbox"> \
<a href="#" class="btn" data-action="filters">Filters</a> \
<a href="#" class="btn active" data-action="fields">Fields</a> \
</div> \
</div> \
<div class="query-editor-here" style="display:inline;"></div> \
<div class="clearfix"></div> \
</div> \
<div class="data-view-sidebar"></div> \
<div class="data-view-container"></div> \
</div> \
',
@ -212,19 +217,17 @@ my.MultiView = Backbone.View.extend({
});
this.$filterEditor = filterEditor.el;
this.el.find('.header').append(filterEditor.el);
var facetViewer = new recline.View.FacetViewer({
var fieldsView = new recline.View.Fields({
model: this.model
});
this.$facetViewer = facetViewer.el;
this.el.find('.header').append(facetViewer.el);
this.$fieldsView = fieldsView.el;
this.el.find('.data-view-sidebar').append(fieldsView.el);
},
updateNav: function(pageName) {
this.el.find('.navigation li').removeClass('active');
this.el.find('.navigation li a').removeClass('disabled');
var $el = this.el.find('.navigation li a[data-view="' + pageName + '"]');
$el.parent().addClass('active');
$el.addClass('disabled');
this.el.find('.navigation a').removeClass('active');
var $el = this.el.find('.navigation a[data-view="' + pageName + '"]');
$el.addClass('active');
// show the specific page
_.each(this.pageViews, function(view, idx) {
if (view.id === pageName) {
@ -241,9 +244,9 @@ my.MultiView = Backbone.View.extend({
e.preventDefault();
var action = $(e.target).attr('data-action');
if (action === 'filters') {
this.$filterEditor.show();
} else if (action === 'facets') {
this.$facetViewer.show();
this.$filterEditor.toggle();
} else if (action === 'fields') {
this.$fieldsView.toggle();
}
},

93
src/widget.fields.js Normal file
View File

@ -0,0 +1,93 @@
/*jshint multistr:true */
// Field Info
//
// For each field
//
// Id / Label / type / format
// Editor -- to change type (and possibly format)
// Editor for show/hide ...
// Summaries of fields
//
// Top values / number empty
// If number: max, min average ...
// Box to boot transform editor ...
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
(function($, my) {
my.Fields = Backbone.View.extend({
className: 'recline-fields-view',
template: ' \
<div class="accordion fields-list well"> \
{{#fields}} \
<div class="accordion-group field"> \
<div class="accordion-heading"> \
<h4> \
{{label}} \
<small> \
<i class="icon-file" title="Field type"></i> {{type}} \
<a class="accordion-toggle" data-toggle="collapse" href="#collapse{{id}}"> &raquo; \
</a> \
</small> \
</h4> \
</div> \
<div id="collapse{{id}}" class="accordion-body collapse in"> \
<div class="accordion-inner"> \
{{#facets}} \
<div class="facet-summary" data-facet="{{id}}"> \
<ul class="facet-items"> \
{{#terms}} \
<li class="facet-item"><span class="term">{{term}}</span> <span class="count">[{{count}}]</span></li> \
{{/terms}} \
</ul> \
</div> \
{{/facets}} \
<div class="clear"></div> \
</div> \
</div> \
</div> \
{{/fields}} \
</div> \
',
events: {
},
initialize: function(model) {
var self = this;
this.el = $(this.el);
_.bindAll(this, 'render');
this.model.fields.bind('all', function() {
self.model.fields.each(function(field) {
field.facets.bind('all', self.render);
});
// fields can get reset or changed in which case we need to recalculate
self.model.getFieldsSummary();
self.render();
});
this.render();
},
render: function() {
var self = this;
var tmplData = {
fields: []
};
this.model.fields.each(function(field) {
var out = field.toJSON();
out.facets = field.facets.toJSON();
tmplData.fields.push(out);
});
var templated = Mustache.render(this.template, tmplData);
this.el.html(templated);
this.el.find('.collapse').collapse('hide');
}
});
})(jQuery, recline.View);

View File

@ -1,6 +1,6 @@
(function ($) {
module("Backend Memory - DataWrapper");
module("Backend Memory - Store");
var memoryData = [
{id: 0, x: 1, y: 2, z: 3, country: 'DE', label: 'first'}
@ -13,7 +13,7 @@ var memoryData = [
var _wrapData = function() {
var dataCopy = $.extend(true, [], memoryData);
return new recline.Backend.Memory.DataWrapper(dataCopy);
return new recline.Backend.Memory.Store(dataCopy);
}
test('basics', function () {

View File

@ -49,7 +49,7 @@
<script type="text/javascript" src="../src/widget.pager.js"></script>
<script type="text/javascript" src="../src/widget.queryeditor.js"></script>
<script type="text/javascript" src="../src/widget.filtereditor.js"></script>
<script type="text/javascript" src="../src/widget.facetviewer.js"></script>
<script type="text/javascript" src="../src/widget.fields.js"></script>
<script type="text/javascript" src="../src/view.multiview.js"></script>
<script type="text/javascript" src="view-grid.test.js"></script>

View File

@ -125,6 +125,21 @@ test('Dataset _prepareQuery', function () {
deepEqual(out, exp);
});
test('Dataset getFieldsSummary', function () {
var dataset = Fixture.getDataset();
dataset.getFieldsSummary().done(function() {
var countryField = dataset.fields.get('country');
var facet = countryField.facets.models[0];
equal(facet.get('terms').length, 3);
var exp = [
{ count: 3, term: 'UK' },
{ count: 2, term: 'DE' },
{ count: 1, term: 'US' }
];
deepEqual(facet.get('terms'), exp);
});
});
// =================================
// Query

View File

@ -65,7 +65,7 @@ test('renderers', function () {
var dataset = Fixture.getDataset();
dataset.fields.get('country').renderer = function(val, field, doc){
return 'Country: ' + val;
return '<a href="abc">Country: ' + val + '</a>';
};
var deriver = function(val, field, doc){
@ -73,7 +73,6 @@ test('renderers', function () {
}
dataset.fields.add(new recline.Model.Field({id:'computed'},{deriver:deriver}));
var view = new recline.View.SlickGrid({
model: dataset
});
@ -84,6 +83,7 @@ test('renderers', function () {
view.grid.init();
equal($(view.grid.getCellNode(0,view.grid.getColumnIndex('country'))).text(),'Country: DE');
equal($(view.grid.getCellNode(0,view.grid.getColumnIndex('country'))).html(),'<a href="abc">Country: DE</a>');
equal($(view.grid.getCellNode(0,view.grid.getColumnIndex('computed'))).text(),'10');
view.remove();
});

View File

@ -61,9 +61,9 @@ test('initialize state', function () {
// check the correct view is visible
var css = explorer.el.find('.navigation a[data-view="graph"]').attr('class').split(' ');
ok(_.contains(css, 'disabled'), css);
ok(_.contains(css, 'active'), css);
var css = explorer.el.find('.navigation a[data-view="grid"]').attr('class').split(' ');
ok(!(_.contains(css, 'disabled')), css);
ok(!(_.contains(css, 'active')), css);
// check pass through of view config
deepEqual(explorer.state.get('view-grid')['hiddenFields'], ['x']);