Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable Live-Edits of any Property of any Plot #837

Merged
merged 6 commits into from
Mar 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ Right now the following callback events are supported:
4. `Click` - Triggers when Image pane is clicked on, has a parameter:
- `image_coord` - dictionary with the fields `x` and `y` for the click coordinates in the coordinate frame of the possibly zoomed/panned image (*not* the enclosing pane).

##### Plot Parameters
Use the top-right *edit*-Button to inspect all parameters used for plot in the respective window.
The visdom client supports dynamic change of plot parameters as well. Just change one of the listed parameters, the plot will be altered on-the-fly.
Click the button again to close the property list.
<p align="center"><img align="center" src="https://user-images.githubusercontent.com/19650074/156751970-0915757d-8bf0-4a6d-a510-1d34a918e47a.gif" width="400" /></p>

### Environments
<p align="center"><img align="center" src="docs/images/environment.png" width="300" /></p>

Expand Down
156 changes: 156 additions & 0 deletions js/AbstractPropertiesList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* Copyright 2017-present, The Visdom Authors
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import React from 'react';

class MyRef {
JackUrb marked this conversation as resolved.
Show resolved Hide resolved
constructor() {
this._ref = null;
}

getRef() {
return this._ref;
}

setRef = ref => {
this._ref = ref;
};
}

class EditablePropertyText extends React.Component {
constructor(props) {
super(props);
this.textInput = new MyRef();
this.state = {
propsValue: props.value,
actualValue: props.value,
isEdited: false,
};
}

handleChange = event => {
let newValue = event.target.value;
if (this.props.validateHandler && !this.props.validateHandler(newValue)) {
event.preventDefault();
} else {
this.setState({
actualValue: newValue,
});
}
};

handleKeyPress = event => {
if (event.key === 'Enter') {
let ref = this.textInput.getRef();
if (ref) ref.blur(); // Blur invokes submit
}
};

onBlur = () => {
this.setState({ isEdited: false }, () => {
if (this.props.submitHandler) {
this.props.submitHandler(this.state.actualValue);
}
});
};

onFocus = () => {
this.setState({
isEdited: true,
});
};

UNSAFE_componentWillReceiveProps(nextProps) {
if (this.state.propsValue !== nextProps.value || !this.state.isEdited) {
let newState = this.state.isEdited
? {
propsValue: nextProps.value,
}
: {
propsValue: nextProps.value,
actualValue: nextProps.value,
};
this.setState(newState);
}
}

render() {
return (
<input
type="text"
ref={this.textInput.setRef}
value={this.state.actualValue}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
onBlur={this.onBlur}
onFocus={this.onFocus}
/>
);
}
}

class AbstractPropertiesList extends React.Component {
updateValue = (propId, value) => {};

renderPropertyValue = (prop, propId) => {
switch (prop.type) {
case 'text':
return (
<EditablePropertyText
value={prop.value}
submitHandler={value => this.updateValue(propId, value)}
/>
);
case 'number':
return (
<EditablePropertyText
value={prop.value}
submitHandler={value => this.updateValue(propId, value)}
validateHandler={value => value.match(/^[0-9]*([.][0-9]*)?$/i)}
/>
);
case 'button':
return (
<button
className="btn btn-sm"
onClick={() => this.updateValue(propId, 'clicked')}
>
{prop.value}
</button>
);
case 'checkbox':
return (
<label className="checkbox-inline">
<input
type="checkbox"
checked={prop.value}
onChange={() => this.updateValue(propId, !prop.value)}
/>
&nbsp;
</label>
);
case 'select':
return (
<select
className="form-control"
onChange={event => this.updateValue(propId, event.target.value)}
value={prop.value}
>
{prop.values.map((name, id) => (
<option key={id} value={id}>
{name}
</option>
))}
</select>
);
}
};
}

module.exports = AbstractPropertiesList;
112 changes: 111 additions & 1 deletion js/Pane.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,20 @@
*/

import React from 'react';
const AbstractPropertiesList = require('./AbstractPropertiesList');
var classNames = require('classnames');

class Pane extends React.Component {
_windowRef = null;
_barRef = null;

constructor(props) {
super(props);
this.state = {
propertyListShown: false,
};
}

close = () => {
this.props.onClose(this.props.id);
};
Expand All @@ -28,6 +36,10 @@ class Pane extends React.Component {
}
};

togglePropertyList = () => {
this.setState(state => ({ propertyListShown: !state.propertyListShown }));
};

reset = () => {
if (this.props.handleReset) {
this.props.handleReset();
Expand Down Expand Up @@ -66,7 +78,7 @@ class Pane extends React.Component {
};
};

shouldComponentUpdate(nextProps) {
shouldComponentUpdate(nextProps, nextState) {
if (this.props.contentID !== nextProps.contentID) {
return true;
} else if (this.props.h !== nextProps.h || this.props.w !== nextProps.w) {
Expand All @@ -75,6 +87,8 @@ class Pane extends React.Component {
return true;
} else if (this.props.isFocused !== nextProps.isFocused) {
return true;
} else if (this.state.propertyListShown !== nextState.propertyListShown) {
return true;
}

return false;
Expand Down Expand Up @@ -114,13 +128,109 @@ class Pane extends React.Component {
>
&#10226;
</button>
{this.props.enablePropertyList &&
this.props.content &&
typeof this.props.content == 'object' &&
this.props.content.data && (
<button
title="properties"
onClick={this.togglePropertyList}
className={
this.state.propertyListShown
? 'pull-right active'
: 'pull-right'
}
>
<span className="glyphicon glyphicon-tags" />
</button>
)}
<div>{this.props.title}</div>
</div>
<div className="content">{this.props.children}</div>
<div className="widgets">{this.props.widgets}</div>
{this.state.propertyListShown && (
<div className="attachedWindow">
{this.props.content.data.map((data, dataId) => [
<b>Data[{dataId}] Properties</b>,
<PropertyList
keylist={'data[' + dataId + ']'}
content={data}
title={'Data[' + dataId + ']'}
/>,
<hr />,
])}
<b>Layout Properties</b>
<PropertyList
keylist="layout"
content={this.props.content.layout}
title="Layout"
/>
</div>
)}
</div>
);
}
}

class PropertyList extends AbstractPropertiesList {
_windowRef = null;
_barRef = null;

// updates the property of the window dynamically
// note: this.props refers in this content to the Components directly responsible
// to the key, e.g. EditablePropertyText object from AbstractPropertiesList
updateValue = (key, value) => {
this.props.content[key] = value;
JackUrb marked this conversation as resolved.
Show resolved Hide resolved
};

render() {
// create for each element of props.content a representation in the PropertyList
let props = Object.entries(this.props.content).map(([key_local, value]) => {
// append key for multi-level objects
var keylist = this.props.keylist
? Array.isArray(this.props.keylist)
? this.props.keylist.concat([key_local])
: [this.props.keylist, key_local]
: [key_local];
var key_string =
keylist.length > 1 ? keylist.slice(1).join('.') : keylist[0];

// map value type to property type
if (typeof value == 'number') var type = 'number';
else if (typeof value == 'boolean') var type = 'checkbox';
else if (typeof value == 'string') var type = 'text';
else if (Array.isArray(value)) return [];
else if (value && typeof value === 'object')
return <PropertyList content={value} keylist={keylist} />;
else return [];

// list new property as part of a table
return (
<tr key={key_string}>
<td className="table-properties-name">{key_string}</td>
<td className="table-properties-value">
{this.renderPropertyValue(
{
name: key_string,
type: type,
value: value,
},
key_local
)}
</td>
</tr>
);
});

// only first PropertyList in recursion should create a table-tag
if (!Array.isArray(this.props.keylist))
return (
<table className="table table-bordered table-condensed table-properties">
{props}
</table>
);
else return props;
}
}

module.exports = Pane;
1 change: 1 addition & 0 deletions js/PlotPane.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class PlotPane extends React.Component {
{...this.props}
handleDownload={this.handleDownload}
ref={ref => (this._paneRef = ref)}
enablePropertyList
>
<div
id={this.props.contentID}
Expand Down
Loading