From 7afcf0bb68a3269b17b9b38641a523dfa1b182f5 Mon Sep 17 00:00:00 2001 From: David Sehnal <dsehnal@users.noreply.github.com> Date: Fri, 16 Dec 2022 20:02:46 +0100 Subject: [PATCH] Show histogram in direct volume control point settings (#666) --- CHANGELOG.md | 2 + .../line-graph/line-graph-component.tsx | 46 ++++++++++++++----- src/mol-plugin-ui/controls/parameters.tsx | 29 +++++++++--- src/mol-repr/volume/direct-volume.ts | 4 +- src/mol-util/param-definition.ts | 9 ++-- 5 files changed, 68 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8ec51e80..8ebfd0619 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Note that since we don't clearly distinguish between a public and private interf ## [Unreleased] +- Show histogram in direct volume control point settings + ## [v3.27.0] - 2022-12-15 diff --git a/src/mol-plugin-ui/controls/line-graph/line-graph-component.tsx b/src/mol-plugin-ui/controls/line-graph/line-graph-component.tsx index 654d6733e..e7a90cfb6 100644 --- a/src/mol-plugin-ui/controls/line-graph/line-graph-component.tsx +++ b/src/mol-plugin-ui/controls/line-graph/line-graph-component.tsx @@ -1,12 +1,15 @@ /** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Paul Luna <paulluna0215@gmail.com> + * @author David Sehnal <david.sehnal@gmail.com> */ import { PointComponent } from './point-component'; import * as React from 'react'; import { Vec2 } from '../../../mol-math/linear-algebra'; +import { Grid } from '../../../mol-model/volume'; +import { arrayMax } from '../../../mol-util/array'; interface LineGraphComponentState { points: Vec2[], @@ -76,6 +79,7 @@ export class LineGraphComponent extends React.Component<any, LineGraphComponentS public render() { const points = this.renderPoints(); const lines = this.renderLines(); + const histogram = this.renderHistogram(); return ([ <div key="LineGraph"> @@ -93,6 +97,7 @@ export class LineGraphComponent extends React.Component<any, LineGraphComponentS onDoubleClick={this.handleDoubleClick}> <g stroke="black" fill="black"> + {histogram} {lines} {points} </g> @@ -297,11 +302,11 @@ export class LineGraphComponent extends React.Component<any, LineGraphComponentS } private normalizePoint(point: Vec2) { - const min = this.padding / 2; - const maxX = this.width + min; - const maxY = this.height + min; - const normalizedX = (point[0] * (maxX - min)) + min; - const normalizedY = (point[1] * (maxY - min)) + min; + const offset = this.padding / 2; + const maxX = this.width + offset; + const maxY = this.height + offset; + const normalizedX = (point[0] * (maxX - offset)) + offset; + const normalizedY = (point[1] * (maxY - offset)) + offset; const reverseY = (this.height + this.padding) - normalizedY; const newPoint = Vec2.create(normalizedX, reverseY); return newPoint; @@ -325,6 +330,24 @@ export class LineGraphComponent extends React.Component<any, LineGraphComponentS } } + private renderHistogram() { + if (!this.props.volume) return null; + + const histogram = Grid.getHistogram(this.props.volume.grid, 40); + const bars = []; + const N = histogram.counts.length; + const w = this.width / N; + const offset = this.padding / 2; + const max = arrayMax(histogram.counts) || 1; + for (let i = 0; i < N; i++) { + const x = this.width * i / (N - 1) + offset; + const y1 = this.height + offset; + const y2 = this.height * (1 - histogram.counts[i] / max) + offset; + bars.push(<line key={`histogram${i}`} x1={x} x2={x} y1={y1} y2={y2} stroke="#ded9ca" strokeWidth={w} />); + } + return bars; + } + private renderPoints() { const points: any[] = []; let point: Vec2; @@ -352,19 +375,18 @@ export class LineGraphComponent extends React.Component<any, LineGraphComponentS private renderLines() { const points: Vec2[] = []; const lines = []; - let min: number; let maxX: number; let maxY: number; let normalizedX: number; let normalizedY: number; let reverseY: number; + const o = this.padding / 2; for (const point of this.state.points) { - min = this.padding / 2; - maxX = this.width + min; - maxY = this.height + min; - normalizedX = (point[0] * (maxX - min)) + min; - normalizedY = (point[1] * (maxY - min)) + min; + maxX = this.width + o; + maxY = this.height + this.padding; + normalizedX = (point[0] * (maxX - o)) + o; + normalizedY = (point[1] * (maxY - o)) + o; reverseY = this.height + this.padding - normalizedY; points.push(Vec2.create(normalizedX, reverseY)); } diff --git a/src/mol-plugin-ui/controls/parameters.tsx b/src/mol-plugin-ui/controls/parameters.tsx index 1d7cf96da..3030981e2 100644 --- a/src/mol-plugin-ui/controls/parameters.tsx +++ b/src/mol-plugin-ui/controls/parameters.tsx @@ -7,6 +7,7 @@ import * as React from 'react'; import { Mat4, Vec2, Vec3 } from '../../mol-math/linear-algebra'; +import { Volume } from '../../mol-model/volume'; import { Script } from '../../mol-script/script'; import { Asset } from '../../mol-util/assets'; import { Color } from '../../mol-util/color'; @@ -306,17 +307,32 @@ export class LineGraphControl extends React.PureComponent<ParamProps<PD.LineGrap message: `${this.props.param.defaultValue.length} points`, }; + + private pointToLabel(point?: Vec2) { + if (!point) return ''; + + const volume = this.props.param.getVolume?.() as Volume; + if (volume) { + const { min, max, mean, sigma } = volume.grid.stats; + const v = min + (max - min) * point[0]; + const s = (v - mean) / sigma; + return `(${v.toFixed(2)} | ${s.toFixed(2)}Ď, ${point[1].toFixed(2)})`; + } else { + return `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})`; + } + } + onHover = (point?: Vec2) => { this.setState({ isOverPoint: !this.state.isOverPoint }); if (point) { - this.setState({ message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})` }); - return; + this.setState({ message: this.pointToLabel(point) }); + } else { + this.setState({ message: `${this.props.value.length} points` }); } - this.setState({ message: `${this.props.value.length} points` }); }; onDrag = (point: Vec2) => { - this.setState({ message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})` }); + this.setState({ message: this.pointToLabel(point) }); }; onChange = (value: PD.LineGraph['defaultValue']) => { @@ -332,9 +348,10 @@ export class LineGraphControl extends React.PureComponent<ParamProps<PD.LineGrap const label = this.props.param.label || camelCaseToWords(this.props.name); return <> <ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{`${this.state.message}`}</button>} /> - <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}> + <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none', marginTop: 1 }}> <LineGraphComponent - data={this.props.param.defaultValue} + data={this.props.value} + volume={this.props.param.getVolume?.()} onChange={this.onChange} onHover={this.onHover} onDrag={this.onDrag} /> diff --git a/src/mol-repr/volume/direct-volume.ts b/src/mol-repr/volume/direct-volume.ts index 72d384088..24a69b3a2 100644 --- a/src/mol-repr/volume/direct-volume.ts +++ b/src/mol-repr/volume/direct-volume.ts @@ -129,7 +129,9 @@ export const DirectVolumeParams = { }; export type DirectVolumeParams = typeof DirectVolumeParams export function getDirectVolumeParams(ctx: ThemeRegistryContext, volume: Volume) { - return PD.clone(DirectVolumeParams); + const params = PD.clone(DirectVolumeParams); + params.controlPoints.getVolume = () => volume; + return params; } export type DirectVolumeProps = PD.Values<DirectVolumeParams> diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts index 5516f9097..59aee99dd 100644 --- a/src/mol-util/param-definition.ts +++ b/src/mol-util/param-definition.ts @@ -216,10 +216,13 @@ export namespace ParamDefinition { } export interface LineGraph extends Base<Vec2Data[]> { - type: 'line-graph' + type: 'line-graph', + getVolume?: () => unknown } - export function LineGraph(defaultValue: Vec2Data[], info?: Info): LineGraph { - return setInfo<LineGraph>({ type: 'line-graph', defaultValue }, info); + export function LineGraph(defaultValue: Vec2Data[], info?: Info & { getVolume?: (binCount?: number) => unknown }): LineGraph { + const ret = setInfo<LineGraph>({ type: 'line-graph', defaultValue }, info); + if (info?.getVolume) ret.getVolume = info.getVolume; + return ret; } export interface Group<T> extends Base<T> { -- GitLab