Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #551 from Kitware/girder-plugin
Transfer `girder-candela` plugin from Girder codebase
- Loading branch information
Showing
14 changed files
with
631 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
############################################################################### | ||
# Copyright Kitware Inc. | ||
# | ||
# Licensed under the Apache License, Version 2.0 ( the "License" ); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
############################################################################### | ||
|
||
from girder.plugin import getPlugin, GirderPlugin | ||
|
||
|
||
class CandelaPlugin(GirderPlugin): | ||
DISPLAY_NAME = 'Candela Visualization' | ||
CLIENT_SOURCE_PATH = 'web_client' | ||
|
||
def load(self, info): | ||
getPlugin('item_tasks').load(info) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
// Extends and overrides API | ||
import './views/ItemView'; |
28 changes: 28 additions & 0 deletions
28
python/girder_plugin/girder_candela/web_client/package.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
{ | ||
"name": "@girder/candela", | ||
"version": "0.2.0-alpha.1", | ||
"description": "Render Candela visualizations in the item page for CSV files.", | ||
"homepage": "http://girder.readthedocs.io/en/latest/plugins.html#candela-visualization", | ||
"bugs": { | ||
"url": "https://github.com/girder/girder/issues" | ||
}, | ||
"license": "Apache-2.0", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/girder/girder.git" | ||
}, | ||
"dependencies": { | ||
"@candela/treeheatmap": "~0.23.3", | ||
"@candela/vega": "~0.23.3", | ||
"vega-loader": "^3.0.0" | ||
}, | ||
"peerDependencies": { | ||
"@girder/item_tasks": "*" | ||
}, | ||
"girderPlugin": { | ||
"name": "candela", | ||
"main": "./main.js", | ||
"dependencies": ["item_tasks"], | ||
"webpack": "webpack.helper" | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
python/girder_plugin/girder_candela/web_client/stylesheets/candelaParameters.styl
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
.g-candela-container | ||
|
||
.g-candela-vis-setup | ||
max-width 650px | ||
|
||
.g-candela-panel-group | ||
position static | ||
width 100% | ||
margin 20px 0 0 | ||
overflow-y visible | ||
|
||
.g-candela-update-vis-container | ||
margin 10px 0 | ||
text-align right | ||
|
||
.g-candela-validation-failed-message | ||
padding 10px 0 |
12 changes: 12 additions & 0 deletions
12
python/girder_plugin/girder_candela/web_client/stylesheets/candelaWidget.styl
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
.g-item-candela-header | ||
background-color #f0f0f0 | ||
padding 5px 6px | ||
color #555 | ||
font-size 16px | ||
font-weight bold | ||
margin-top 18px | ||
|
||
select | ||
display inline | ||
width auto | ||
margin-left 25px |
9 changes: 9 additions & 0 deletions
9
python/girder_plugin/girder_candela/web_client/templates/candelaParameters.pug
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
.g-candela-vis-setup | ||
.g-candela-panel-group | ||
.g-candela-inputs-container | ||
.g-candela-update-vis-container | ||
.g-candela-validation-failed-message | ||
button.g-candela-update-vis.btn.btn-sm.btn-success | ||
i.icon-play | ||
| Update | ||
.g-candela-vis |
10 changes: 10 additions & 0 deletions
10
python/girder_plugin/girder_candela/web_client/templates/candelaWidget.pug
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
.g-item-candela | ||
.g-item-candela-header | ||
i.icon-chart-bar | ||
| Candela visualization | ||
select.form-control.g-item-candela-component | ||
each component in components | ||
option(value=component) | ||
= component | ||
.g-item-candela-container | ||
.g-item-candela-parameters |
179 changes: 179 additions & 0 deletions
179
python/girder_plugin/girder_candela/web_client/views/CandelaParametersView.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
import _ from 'underscore'; | ||
|
||
import WidgetModel from '@girder/item_tasks/models/WidgetModel'; | ||
import WidgetCollection from '@girder/item_tasks/collections/WidgetCollection'; | ||
import ControlsPanel from '@girder/item_tasks/views/ControlsPanel'; | ||
import View from 'girder/views/View'; | ||
|
||
import template from '../templates/candelaParameters.pug'; | ||
import '../stylesheets/candelaParameters.styl'; | ||
|
||
const CandelaParametersView = View.extend({ | ||
events: { | ||
'click .g-candela-update-vis': 'updateVisualization' | ||
}, | ||
|
||
initialize: function (settings) { | ||
this._inputWidgets = new WidgetCollection(); | ||
this._inputsPanel = new ControlsPanel({ | ||
title: 'Visualization options', | ||
collection: this._inputWidgets, | ||
parentView: this | ||
}); | ||
}, | ||
|
||
setData: function (data, columns) { | ||
this._data = data; | ||
this._columns = ['(none)', ...columns]; | ||
this._multiColumns = columns; | ||
this._numericColumns = ['(none)', ...columns.filter((column) => { | ||
const columnType = this._data.__types__[column]; | ||
return _.contains(['number', 'integer', 'date'], columnType); | ||
})]; | ||
this._multiNumericColumns = this._numericColumns.slice(1); | ||
this.render(); | ||
}, | ||
|
||
setComponent: function (component) { | ||
this._component = component; | ||
this.render(); | ||
}, | ||
|
||
render: function () { | ||
if (!this._data) { | ||
return this; | ||
} | ||
if (!this._component) { | ||
return this; | ||
} | ||
|
||
this._inputWidgets.reset(); | ||
|
||
this._inputWidgets.add(new WidgetModel({ | ||
type: 'integer', | ||
title: 'Width', | ||
id: 'width', | ||
min: 0, | ||
value: 400 | ||
})); | ||
this._inputWidgets.add(new WidgetModel({ | ||
type: 'integer', | ||
title: 'Height', | ||
id: 'height', | ||
min: 0, | ||
value: 400 | ||
})); | ||
|
||
// Build all the widget models from the vis spec | ||
_.each(this._component.options, (input) => { | ||
if (input.type === 'number') { | ||
this._inputWidgets.add(new WidgetModel({ | ||
type: 'number', | ||
title: input.name || input.id, | ||
id: input.id || input.name, | ||
description: input.description || '', | ||
value: input.default === undefined ? 0 : input.default | ||
})); | ||
} else if (input.type === 'boolean') { | ||
this._inputWidgets.add(new WidgetModel({ | ||
type: 'boolean', | ||
title: input.name || input.id, | ||
id: input.id || input.name, | ||
description: input.description || '', | ||
value: input.default === undefined ? false : input.default | ||
})); | ||
} else if (['string', 'string_list'].includes(input.type) && input.domain) { | ||
let type = input.type === 'string' ? 'string-enumeration' : 'string-enumeration-multiple'; | ||
let values = null; | ||
let value = null; | ||
if (_.isArray(input.domain)) { | ||
values = input.domain; | ||
if (input.type === 'string') { | ||
value = input.default === undefined ? input.domain[0] : input.default; | ||
} else { | ||
value = input.default === undefined ? [] : input.default; | ||
} | ||
} else { | ||
let numeric = !input.domain.fieldTypes.includes('string'); | ||
if (input.type === 'string') { | ||
values = numeric ? this._numericColumns : this._columns; | ||
value = '(none)'; | ||
} else { | ||
values = numeric ? this._multiNumericColumns : this._multiColumns; | ||
value = []; | ||
} | ||
} | ||
this._inputWidgets.add(new WidgetModel({ | ||
type: type, | ||
title: input.name || input.id, | ||
id: input.id || input.name, | ||
description: input.description || '', | ||
values: values, | ||
value: value | ||
})); | ||
} else if (input.type === 'string') { | ||
this._inputWidgets.add(new WidgetModel({ | ||
type: 'string', | ||
title: input.name || input.id, | ||
id: input.id || input.name, | ||
description: input.description || '', | ||
value: input.default === undefined ? '' : input.default | ||
})); | ||
} | ||
}); | ||
|
||
this.$el.html(template()); | ||
|
||
this._inputsPanel.setElement(this.$('.g-candela-inputs-container')).render(); | ||
|
||
return this; | ||
}, | ||
|
||
/** | ||
* Validates that all of the widgets are in a valid state. Displays any | ||
* invalid states. | ||
*/ | ||
validate: function () { | ||
let ok = true; | ||
const test = (model) => { | ||
if (!model.isValid()) { | ||
ok = false; | ||
} | ||
}; | ||
|
||
// Don't short-circuit; we want to highlight *all* invalid inputs | ||
this._inputWidgets.each(test); | ||
|
||
return ok; | ||
}, | ||
|
||
/** | ||
* Translates the WidgetCollection state for the input widgets into the | ||
* appropriate Candela options, then shows the visualization. | ||
*/ | ||
updateVisualization: function (e) { | ||
if (!this.validate()) { | ||
this.$('.g-candela-validation-failed-message').text( | ||
'One or more of your inputs or s is invalid, they are highlighted in red.'); | ||
return; | ||
} | ||
this.$('.g-candela-validation-failed-message').empty(); | ||
|
||
let inputs = {}; | ||
this._inputWidgets.each((model) => { | ||
if (model.value() !== '(none)') { | ||
inputs[model.id] = model.value(); | ||
} | ||
}); | ||
inputs.data = this._data; | ||
|
||
if (this.vis && this.vis.destroy) { | ||
this.vis.destroy(); | ||
} | ||
this.$('.g-candela-vis').empty(); | ||
let vis = new this._component(this.$('.g-candela-vis')[0], inputs); | ||
vis.render(); | ||
} | ||
}); | ||
|
||
export default CandelaParametersView; |
99 changes: 99 additions & 0 deletions
99
python/girder_plugin/girder_candela/web_client/views/CandelaWidget.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import _ from 'underscore'; | ||
|
||
import View from 'girder/views/View'; | ||
|
||
import * as vega from '@candela/vega'; | ||
import * as treeheatmap from '@candela/treeheatmap'; | ||
|
||
import { loader, read, inferTypes } from 'vega-loader'; | ||
|
||
import CandelaWidgetTemplate from '../templates/candelaWidget.pug'; | ||
import '../stylesheets/candelaWidget.styl'; | ||
|
||
import CandelaParametersView from './CandelaParametersView'; | ||
|
||
const components = Object.assign({}, vega, treeheatmap); | ||
|
||
var CandelaWidget = View.extend({ | ||
events: { | ||
'change .g-item-candela-component': 'updateComponent' | ||
}, | ||
|
||
initialize: function (settings) { | ||
this.item = settings.item; | ||
this.accessLevel = settings.accessLevel; | ||
this._components = _.keys(_.pick(components, (comp) => comp.options)); | ||
|
||
this.listenTo(this.item, 'change', function () { | ||
this.render(); | ||
}, this); | ||
|
||
this.parametersView = new CandelaParametersView({ | ||
component: components[this.$('.g-item-candela-component').val()], | ||
parentView: this | ||
}); | ||
|
||
this.loader = loader(); | ||
this.render(); | ||
}, | ||
|
||
updateComponent: function () { | ||
this.parametersView.setComponent(components[this.$('.g-item-candela-component').val()]); | ||
}, | ||
|
||
render: function () { | ||
let options = { | ||
type: null | ||
}; | ||
let name = this.item.get('name').toLowerCase(); | ||
if (name.endsWith('.csv')) { | ||
options.type = 'csv'; | ||
} else if (name.endsWith('.tsv') || name.endsWith('.tab')) { | ||
options.type = 'tsv'; | ||
} else { | ||
this.$('.g-item-candela').remove(); | ||
return this; | ||
} | ||
|
||
this.$el.html(CandelaWidgetTemplate({ | ||
components: this._components | ||
})); | ||
this.parametersView.setElement(this.$('.g-item-candela-parameters')); | ||
this.loader.load(this.item.downloadUrl()).then((data) => { | ||
data = read(data, options); | ||
let columns = Object.keys(data[0]); | ||
const types = inferTypes(data, columns); | ||
data.__types__ = types; | ||
|
||
// Vega has issues with empty-string fields and fields with dots, so rename those. | ||
let rename = []; | ||
_.each(data.__types__, (value, key) => { | ||
if (key === '') { | ||
rename.push({from: '', to: 'id'}); | ||
} else if (key.indexOf('.') >= 0) { | ||
rename.push({from: key, to: key.replace(/\./g, '_')}); | ||
} | ||
}); | ||
|
||
_.each(rename, (d) => { | ||
data.__types__[d.to] = data.__types__[d.from]; | ||
delete data.__types__[d.from]; | ||
_.each(data, (row) => { | ||
row[d.to] = row[d.from]; | ||
delete row[d.from]; | ||
}); | ||
}); | ||
|
||
columns = Object.keys(data[0]); | ||
|
||
this.parametersView.setData(data, columns); | ||
this.updateComponent(); | ||
|
||
return undefined; | ||
}); | ||
|
||
return this; | ||
} | ||
}); | ||
|
||
export default CandelaWidget; |
Oops, something went wrong.