ScatterPlotControl

Source

EditView.js
import React from 'react';
import PropTypes from 'prop-types';

import style from 'PVWStyle/ReactWidgets/ScatterPlotControl.mcss';

import SvgIconWidget from '../SvgIconWidget';

import ExtremesBest from '../../../../svg/PwfPresets/ExtremesBest.svg';
import HighestBest from '../../../../svg/PwfPresets/HighestBest.svg';
import LowestBest from '../../../../svg/PwfPresets/LowestBest.svg';
import MiddleBest from '../../../../svg/PwfPresets/MiddleBest.svg';

import points from './icons/points.png';
import sphere from './icons/sphere.png';
import blackEdgedCircle from './icons/black-edge-circle.png';
import plainCircle from './icons/circle.png';
import blackEdgedSquare from './icons/black-edge-square.png';
import square from './icons/square.png';

const REPRESENTATIONS = [
{ key: 'Plain square', icon: square },
{ key: 'Black-edged square', icon: blackEdgedSquare },
{ key: 'Plain circle', icon: plainCircle },
{ key: 'Black-edged circle', icon: blackEdgedCircle },
{ key: 'Sphere', icon: sphere },
];

// Filled from props.
const ACTIVE_SCORE_MAPPING = {
unselected: -999,
};

/* eslint-disable react/jsx-no-bind */
// Without a third parameter, this function assumes the second parameter is
// an object contains keys and values to update in the original object.
function applyChange(original, key, value) {
if (value === undefined) {
return Object.assign({}, original, key);
}
return Object.assign({}, original, { [key]: value });
}

export default function EditView(props) {
const {
model,
colorMaps,
getScalarRange,
scores,
activeScores,
onActiveScoresChange,
onChange,
} = props;
const spriteGroup = [];
const axesGroup = [];
const colorGroup = [];
const sizeGroup = [];
const opacityGroup = [];
const addOnGroup = [];
const scoreMap = {
unselected: '#CCCCCC',
};

scores.forEach((scoreObj) => {
scoreMap[scoreObj.name] = scoreObj.color;
ACTIVE_SCORE_MAPPING[scoreObj.name] = scoreObj.index;
});
// unselected index is one beyond the scores indices.
ACTIVE_SCORE_MAPPING.unselected = scores.length;

const functionPresets = [
{ icon: HighestBest, key: 'HighestBest' },
{ icon: LowestBest, key: 'LowestBest' },
{ icon: MiddleBest, key: 'MiddleBest' },
{ icon: ExtremesBest, key: 'ExtremesBest' },
];

const toggleActiveScore = (label) => () => {
const score = ACTIVE_SCORE_MAPPING[label];
const scoreIdx = activeScores.indexOf(score);
if (scoreIdx < 0) {
activeScores.push(score);
activeScores.sort((a, b) => a - b);
} else {
activeScores.splice(scoreIdx, 1);
}
onActiveScoresChange(activeScores);
};

//
// Representation
//
spriteGroup.push(
<section className={style.topSpaceProperty} key="sprite-group-use-sprites">
<label>Points</label>
<div
title="Points"
className={
model.usePointSprites === false
? style.selectedSpriteRepContainer
: style.spriteRepContainer
}
onClick={(e) =>
onChange(
applyChange(model, {
usePointSprites: false,
pointRepresentation: '',
})
)
}
>
<img alt="Points" src={points} className={style.spriteRepButton} />
</div>
<label style={{ marginLeft: 30 }}>Sprites</label>
{REPRESENTATIONS.map((repr) => (
<div
key={repr.key}
title={repr.key}
className={
model.pointRepresentation === repr.key && model.usePointSprites
? style.selectedSpriteRepContainer
: style.spriteRepContainer
}
onClick={(e) =>
onChange(
applyChange(model, {
usePointSprites: true,
pointRepresentation: repr.key,
})
)
}
>
<img
alt={repr.key}
src={`${repr.icon}`}
className={style.spriteRepButton}
/>
</div>
))}
</section>
);

/* eslint-disable react/jsx-curly-spacing */
//
// Axes
//
axesGroup.push(
<section className={style.titleGroup} key="axes-group-title">
<label>Axes</label>
</section>
);

if (model.hasMesh) {
axesGroup.push(
<section className={style.property} key="axes-group">
<label>Show mesh</label>
<input
type="checkbox"
style={{ marginTop: 5, flex: 'none' }}
checked={model.showMesh}
onChange={(e) =>
onChange(applyChange(model, 'showMesh', !model.showMesh))
}
/>
</section>
);
}
if (!model.showMesh) {
axesGroup.push(
<section className={style.property} key="axes-group-x">
<label>X</label>
<select
value={model.x}
onChange={(e) => onChange(applyChange(model, 'x', e.target.value))}
>
{model.arrayList.map((txt) => (
<option key={txt} value={txt}>
{txt}
</option>
))}
</select>
</section>
);

axesGroup.push(
<section className={style.property} key="axes-group-y">
<label>Y</label>
<select
value={model.y}
onChange={(e) => onChange(applyChange(model, 'y', e.target.value))}
>
{model.arrayList.map((txt) => (
<option key={txt} value={txt}>
{txt}
</option>
))}
</select>
</section>
);

axesGroup.push(
<section className={style.property} key="axes-group-z">
<label>Z</label>
<select
value={model.z}
onChange={(e) => onChange(applyChange(model, 'z', e.target.value))}
>
{model.arrayList.map((txt) => (
<option key={txt} value={txt}>
{txt}
</option>
))}
</select>
</section>
);
}

//
// Color
//
colorGroup.push(
<section className={style.titleGroup} key="color-group-title">
<label>Color</label>
</section>
);

colorGroup.push(
<section className={style.property} key="color-group-array">
<label>Color By</label>
<select
value={model.colorBy}
onChange={(e) =>
onChange(applyChange(model, 'colorBy', e.target.value))
}
>
<option value="user selection">Active Annotation</option>
{model.arrayList.map((txt) => (
<option key={txt} value={txt}>
{txt}
</option>
))}
</select>
</section>
);

if (model.colorBy !== 'user selection' && colorMaps) {
colorGroup.push(
<div key="color-group-main-options">
<section className={style.property}>
<label>Emphasis</label>
<div className={style.pwfFunctionContainer}>
{Object.keys(colorMaps).map((name) => (
<div key={name} className={style.pwfButtonContainer}>
<img
height="20px"
width="55px"
title={name}
alt={name}
src={`data:image/png;base64,${colorMaps[name]}`}
className={
model.colorMapName === name
? style.selectedPresetButton
: style.presetButton
}
onClick={(e) =>
onChange(applyChange(model, 'colorMapName', name))
}
/>
</div>
))}
</div>
</section>
<section className={style.property}>
<label>Legend</label>
<div className={style.pwfFunctionContainer}>
<img
height="25px"
alt="Color Legend"
style={{ flex: '1 0 auto', width: 25 }}
src={`data:image/png;base64,${colorMaps[model.colorMapName]}`}
/>
</div>
</section>
<section className={style.property}>
<label />
<div style={{ top: '-7px' }} className={style.pwfFunctionContainer}>
<span className={style.leftLabel}>
{getScalarRange(model.colorBy)[0].toFixed(3)}
</span>
<span className={style.rightLabel}>
{getScalarRange(model.colorBy)[1].toFixed(3)}
</span>
</div>
</section>
</div>
);
} else if (model.colorBy === 'user selection') {
// FIXME: Perhaps we can refactor this section to remove some of the duplication
colorGroup.push(
<section className={style.property} key="color-group-user-sel-options">
<label>Legend</label>
<div className={style.pwfFunctionContainer}>
{Object.keys(ACTIVE_SCORE_MAPPING).map((scoreName, i) => (
<div
key={i}
className={
activeScores.indexOf(ACTIVE_SCORE_MAPPING[scoreName]) >= 0
? style.colorLegendPatch
: style.inactiveColorLegendPatch
}
onClick={toggleActiveScore(scoreName)}
style={{
background: scoreMap[scoreName],
float: 'left',
}}
>
<span className={style.colorLegendPatchText}>{scoreName}</span>
</div>
))}
</div>
</section>
);
}

//
// Size
//
sizeGroup.push(
<section className={style.titleGroup} key="size-group-title">
<label>Size</label>
</section>
);

if (model.usePointSprites) {
sizeGroup.push(
<section key="sprite-size-by" className={style.property}>
<label>Size By</label>
<select
value={model.pointSizeBy}
onChange={(e) =>
onChange(applyChange(model, 'pointSizeBy', e.target.value))
}
>
<option key="" value="">
Constant (no array)
</option>
<option key="user selection" value="user selection">
Active Annotation
</option>
{model.arrayList.map((txt) => (
<option key={txt} value={txt}>
{txt}
</option>
))}
</select>
</section>
);

if (model.pointSizeBy === '') {
sizeGroup.push(
<section key="sprite-const" className={style.property}>
<label>Constant&nbsp;Size</label>
<input
type="number"
min="1"
max="10"
value={model.constantPointSize}
onChange={(e) =>
onChange(applyChange(model, 'constantPointSize', e.target.value))
}
/>
</section>
);
} else {
// Sizing by some array
sizeGroup.push(
<section key="sprite-size-range" className={style.property}>
<label>Size Range</label>
<input
key="sprite-size-min"
type="number"
min="0.05"
max="5"
step="0.05"
style={{ marginRight: 5 }}
value={model.pointSizeMin}
onChange={(e) =>
onChange(applyChange(model, 'pointSizeMin', e.target.value))
}
/>
<input
key="sprite-size-max"
type="number"
min="1"
max="5"
step="0.05"
value={model.pointSizeMax}
onChange={(e) =>
onChange(applyChange(model, 'pointSizeMax', e.target.value))
}
/>
</section>
);

// Prevent showing presets in case of user selection
if (model.pointSizeBy !== 'user selection') {
sizeGroup.push(
<section key="sprite-size-function" className={style.property}>
<label>Size</label>
<div className={style.pwfFunctionContainer}>
{functionPresets.map((p) => (
<div
key={p.key}
title={p.key}
className={style.pwfButtonContainer}
>
<SvgIconWidget
icon={p.icon}
className={
model.pointSizeFunction === p.key
? style.selectedPwfButton
: style.pwfButton
}
onClick={(e) =>
onChange(applyChange(model, 'pointSizeFunction', p.key))
}
/>
</div>
))}
</div>
</section>
);
}
}
} else {
// no sprites, just regular points
sizeGroup.push(
<section className={style.property} key="size-group-point-size">
<label>Constant&nbsp;Size</label>
<input
type="number"
min="1"
max="10"
value={model.pointSize}
onChange={(e) =>
onChange(applyChange(model, 'pointSize', e.target.value))
}
/>
</section>
);
}

//
// Opacity
//
if (model.usePointSprites && model.colorBy !== 'user selection') {
opacityGroup.push(
<section className={style.titleGroup} key="opacity-group-title">
<label>Opacity</label>
</section>
);

opacityGroup.push(
<section key="sprite-opacity" className={style.property}>
<label>Opacity By</label>
<select
value={model.opacityBy}
onChange={(e) =>
onChange(applyChange(model, 'opacityBy', e.target.value))
}
>
<option key="" value="">
No transparency
</option>
{model.arrayList.map((txt) => (
<option key={txt} value={txt}>
{txt}
</option>
))}
</select>
</section>
);

opacityGroup.push(
<section key="sprite-opacity-function" className={style.property}>
<label>Mapping</label>
<div className={style.pwfFunctionContainer}>
{functionPresets.map((p) => (
<div key={p.key} title={p.key} className={style.pwfButtonContainer}>
<SvgIconWidget
icon={p.icon}
className={
model.opacityFunction === p.key
? style.selectedPwfButton
: style.pwfButton
}
onClick={(e) =>
onChange(applyChange(model, 'opacityFunction', p.key))
}
/>
</div>
))}
</div>
</section>
);
}

if (model.enableStats) {
addOnGroup.push(
<section className={style.property} key="misc-group-render-stats">
<label style={{ marginTop: 20 }}>Show Render Stats</label>
<input
type="checkbox"
style={{ marginTop: 30, flex: 'none' }}
checked={model.showRenderStats}
onChange={(e) =>
onChange(
applyChange(model, 'showRenderStats', !model.showRenderStats)
)
}
/>
</section>
);
}

return (
<div className={style.container}>
{spriteGroup}
{axesGroup}
{colorGroup}
{sizeGroup}
{opacityGroup}
{addOnGroup}
</div>
);
}

EditView.propTypes = {
model: PropTypes.object,
colorMaps: PropTypes.object,
getScalarRange: PropTypes.func,
scores: PropTypes.array,
activeScores: PropTypes.array,
onActiveScoresChange: PropTypes.func,
onChange: PropTypes.func,
// toggleEditMode: PropTypes.func,
};

EditView.defaultProps = {
model: undefined,
colorMaps: undefined,
getScalarRange: undefined,
scores: undefined,
activeScores: undefined,
onActiveScoresChange: undefined,
onChange: undefined,
};
LegendView.js
import React from 'react';
import PropTypes from 'prop-types';

import style from 'PVWStyle/ReactWidgets/ScatterPlotControl.mcss';

export default function LegendView(props) {
const { model } = props;
const spriteLegend = [];
/* eslint-disable react/jsx-curly-spacing */
if (model.usePointSprites) {
// Point size
spriteLegend.push(
<section className={style.property}>
<label>Point Size</label>
<span>{model.pointSize}</span>
</section>
);

// Representation type
spriteLegend.push(
<section className={style.property}>
<label>Representation</label>
<span>{model.pointRepresentation}</span>
</section>
);

// Size mapping
spriteLegend.push(
<section className={style.property}>
<label>Size by</label>
<span>{model.pointSizeBy ? model.pointSizeBy : 'Constant'}</span>
<table className={style.propertyTable}>
<tbody>
<tr>
<td>
<label>Range</label>
</td>
<td title="Min radius">{model.pointSizeMin}</td>
<td title="Max radius">{model.pointSizeMax}</td>
</tr>
</tbody>
</table>
</section>
);

// Constant size for sprite
if (!model.pointSizeBy) {
spriteLegend.push(
<section className={style.property}>
<label>Constant&nbsp;Radius</label>
<span>{model.constantPointSize}</span>
</section>
);
}

// Opacity
spriteLegend.push(
<section className={style.property}>
<label>Opacity</label>
<span>{model.opacityBy ? model.opacityBy : 'Constant'}</span>
</section>
);
}
return (
<div className={style.container}>
<section className={style.property}>
<label>Axes</label>
<table className={style.propertyTable}>
<tbody>
<tr>
<td>
<label>X</label>
</td>
<td>
<span>{model.x}</span>
</td>
</tr>
<tr>
<td>
<label>Y</label>
</td>
<td>
<span>{model.y}</span>
</td>
</tr>
<tr>
<td>
<label>Z</label>
</td>
<td>
<span>{model.z}</span>
</td>
</tr>
</tbody>
</table>
</section>
{spriteLegend}
<section className={style.property}>
<div>
<center>{model.colorBy}</center>
</div>
</section>
</div>
);
}

LegendView.propTypes = {
model: PropTypes.object,
// toggleEditMode: PropTypes.func,
};

LegendView.defaultProps = {
model: undefined,
// toggleEditMode: undefined,
};
index.js
import React from 'react';
import PropTypes from 'prop-types';

import style from 'PVWStyle/ReactWidgets/ScatterPlotControl.mcss';

import LegendView from './LegendView';
import EditView from './EditView';

export default class ScatterPlotControl extends React.Component {
constructor(props) {
super(props);

this.state = {
editMode: true,
};

// Auto bind method
this.toggleEditMode = () => {
const editMode = !this.state.editMode;
this.setState({ editMode });
};

this.onChange = (model) => {
this.props.manager.updateModel(model);
this.forceUpdate();
};
}

render() {
if (!this.props.manager || !this.props.manager.getModel()) {
return null;
}
const model = this.props.manager.getModel();

if (this.state.editMode) {
return (
<div className={style.viewport}>
<EditView
model={model}
colorMaps={this.props.manager.getColorMaps()}
getScalarRange={(arrayName) =>
this.props.manager.getScalarRange(arrayName)
}
scores={this.props.manager.getProvider().getScores()}
activeScores={this.props.activeScores}
onActiveScoresChange={this.props.onActiveScoresChange}
toggleEditMode={this.toggleEditMode}
onChange={this.onChange}
/>
</div>
);
}
return (
<div className={style.viewport}>
<LegendView model={model} toggleEditMode={this.toggleEditMode} />
</div>
);
}
}

ScatterPlotControl.propTypes = {
manager: PropTypes.object,
activeScores: PropTypes.array,
onActiveScoresChange: PropTypes.func,
};

ScatterPlotControl.defaultProps = {
manager: undefined,
activeScores: undefined,
onActiveScoresChange: undefined,
};