Skip to content

Commit

Permalink
Merge pull request #551 from Kitware/girder-plugin
Browse files Browse the repository at this point in the history
Transfer `girder-candela` plugin from Girder codebase
  • Loading branch information
waxlamp committed Oct 5, 2018
2 parents 8876904 + a463663 commit 51d7b2b
Show file tree
Hide file tree
Showing 14 changed files with 631 additions and 0 deletions.
25 changes: 25 additions & 0 deletions python/girder_plugin/girder_candela/__init__.py
@@ -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)
2 changes: 2 additions & 0 deletions python/girder_plugin/girder_candela/web_client/main.js
@@ -0,0 +1,2 @@
// Extends and overrides API
import './views/ItemView';
28 changes: 28 additions & 0 deletions python/girder_plugin/girder_candela/web_client/package.json
@@ -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"
}
}
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
@@ -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;

0 comments on commit 51d7b2b

Please sign in to comment.