UNPKG

molstar

Version:

A comprehensive macromolecular library.

321 lines (320 loc) 13 kB
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime"; /** * 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'; export class LineGraphComponent extends React.Component { constructor(props) { super(props); this.handleKeyDown = (event) => { // TODO: set canSelectMultiple = true }; this.handleKeyUp = (event) => { // TODO: SET canSelectMultiple = fasle }; this.handleClick = (id) => (event) => { // TODO: add point to selected array }; this.handleMouseDown = (id) => (event) => { if (id === 0 || id === this.state.points.length - 1) { return; } if (this.state.canSelectMultiple) { return; } const copyPoint = this.normalizePoint(Vec2.create(this.state.points[id][0], this.state.points[id][1])); this.ghostPoints.push(document.createElementNS(this.namespace, 'circle')); this.ghostPoints[0].setAttribute('r', '10'); this.ghostPoints[0].setAttribute('fill', 'orange'); this.ghostPoints[0].setAttribute('cx', `${copyPoint[0]}`); this.ghostPoints[0].setAttribute('cy', `${copyPoint[1]}`); this.ghostPoints[0].setAttribute('style', 'display: none'); this.gElement.appendChild(this.ghostPoints[0]); this.updatedX = copyPoint[0]; this.updatedY = copyPoint[1]; this.selected = [id]; }; this.deletePoint = (i) => (event) => { if (i === 0 || i === this.state.points.length - 1) { return; } const points = this.state.points.filter((_, j) => j !== i); points.sort((a, b) => { if (a[0] === b[0]) { if (a[0] === 0) { return a[1] - b[1]; } if (a[1] === 1) { return b[1] - a[1]; } return a[1] - b[1]; } return a[0] - b[0]; }); this.setState({ points }); this.change(points); event.stopPropagation(); }; this.myRef = React.createRef(); this.state = { points: [ Vec2.create(0, 0), Vec2.create(1, 0) ], copyPoint: undefined, canSelectMultiple: false, }; this.height = 400; this.width = 600; this.padding = 70; this.selected = undefined; this.ghostPoints = []; this.namespace = 'http://www.w3.org/2000/svg'; for (const point of this.props.data) { this.state.points.push(point); } this.state.points.sort((a, b) => { if (a[0] === b[0]) { if (a[0] === 0) { return a[1] - b[1]; } if (a[1] === 1) { return b[1] - a[1]; } return a[1] - b[1]; } return a[0] - b[0]; }); this.handleDrag = this.handleDrag.bind(this); this.handleMultipleDrag = this.handleMultipleDrag.bind(this); this.handleDoubleClick = this.handleDoubleClick.bind(this); this.refCallBack = this.refCallBack.bind(this); this.handlePointUpdate = this.handlePointUpdate.bind(this); this.change = this.change.bind(this); this.handleKeyUp = this.handleKeyUp.bind(this); this.handleLeave = this.handleLeave.bind(this); this.handleEnter = this.handleEnter.bind(this); } render() { const points = this.renderPoints(); const lines = this.renderLines(); const histogram = this.renderHistogram(); return ([ _jsx("div", { children: _jsxs("svg", { className: "msp-canvas", ref: this.refCallBack, viewBox: `0 0 ${this.width + this.padding} ${this.height + this.padding}`, onMouseMove: this.handleDrag, onMouseUp: this.handlePointUpdate, onMouseLeave: this.handleLeave, onMouseEnter: this.handleEnter, tabIndex: 0, onKeyDown: this.handleKeyDown, onKeyUp: this.handleKeyUp, onDoubleClick: this.handleDoubleClick, children: [_jsxs("g", { stroke: "black", fill: "black", children: [histogram, lines, points] }), _jsx("g", { className: "ghost-points", stroke: "black", fill: "black" })] }) }, "LineGraph"), _jsx("div", { id: "modal-root" }, "modal") ]); } componentDidMount() { this.gElement = document.getElementsByClassName('ghost-points')[0]; } change(points) { const copyPoints = points.slice(); copyPoints.shift(); copyPoints.pop(); this.props.onChange(copyPoints); } handleDrag(event) { if (this.selected === undefined) { return; } const pt = this.myRef.createSVGPoint(); let updatedCopyPoint; const padding = this.padding / 2; pt.x = event.clientX; pt.y = event.clientY; const svgP = pt.matrixTransform(this.myRef.getScreenCTM().inverse()); updatedCopyPoint = Vec2.create(svgP.x, svgP.y); if ((svgP.x < (padding) || svgP.x > (this.width + (padding))) && (svgP.y > (this.height + (padding)) || svgP.y < (padding))) { updatedCopyPoint = Vec2.create(this.updatedX, this.updatedY); } else if (svgP.x < padding) { updatedCopyPoint = Vec2.create(padding, svgP.y); } else if (svgP.x > (this.width + (padding))) { updatedCopyPoint = Vec2.create(this.width + padding, svgP.y); } else if (svgP.y > (this.height + (padding))) { updatedCopyPoint = Vec2.create(svgP.x, this.height + padding); } else if (svgP.y < (padding)) { updatedCopyPoint = Vec2.create(svgP.x, padding); } else { updatedCopyPoint = Vec2.create(svgP.x, svgP.y); } this.updatedX = updatedCopyPoint[0]; this.updatedY = updatedCopyPoint[1]; const unNormalizePoint = this.unNormalizePoint(updatedCopyPoint); this.ghostPoints[0].setAttribute('style', 'display: visible'); this.ghostPoints[0].setAttribute('cx', `${updatedCopyPoint[0]}`); this.ghostPoints[0].setAttribute('cy', `${updatedCopyPoint[1]}`); this.props.onDrag(unNormalizePoint); } handleMultipleDrag() { // TODO } handlePointUpdate(event) { const selected = this.selected; if (this.state.canSelectMultiple) { return; } if (selected === undefined || selected[0] === 0 || selected[0] === this.state.points.length - 1) { this.setState({ copyPoint: undefined, }); return; } this.selected = undefined; const updatedPoint = this.unNormalizePoint(Vec2.create(this.updatedX, this.updatedY)); const points = this.state.points.filter((_, i) => i !== selected[0]); points.push(updatedPoint); points.sort((a, b) => { if (a[0] === b[0]) { if (a[0] === 0) { return a[1] - b[1]; } if (a[1] === 1) { return b[1] - a[1]; } return a[1] - b[1]; } return a[0] - b[0]; }); this.setState({ points, }); this.change(points); this.gElement.innerHTML = ''; this.ghostPoints = []; document.removeEventListener('mousemove', this.handleDrag, true); document.removeEventListener('mouseup', this.handlePointUpdate, true); } handleDoubleClick(event) { const pt = this.myRef.createSVGPoint(); pt.x = event.clientX; pt.y = event.clientY; const svgP = pt.matrixTransform(this.myRef.getScreenCTM().inverse()); const points = this.state.points; const padding = this.padding / 2; if (svgP.x < (padding) || svgP.x > (this.width + (padding)) || svgP.y > (this.height + (padding)) || svgP.y < (this.padding / 2)) { return; } const newPoint = this.unNormalizePoint(Vec2.create(svgP.x, svgP.y)); points.push(newPoint); points.sort((a, b) => { if (a[0] === b[0]) { if (a[0] === 0) { return a[1] - b[1]; } if (a[1] === 1) { return b[1] - a[1]; } return a[1] - b[1]; } return a[0] - b[0]; }); this.setState({ points }); this.change(points); } handleLeave() { if (this.selected === undefined) { return; } document.addEventListener('mousemove', this.handleDrag, true); document.addEventListener('mouseup', this.handlePointUpdate, true); } handleEnter() { document.removeEventListener('mousemove', this.handleDrag, true); document.removeEventListener('mouseup', this.handlePointUpdate, true); } normalizePoint(point) { 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; } unNormalizePoint(point) { const min = this.padding / 2; const maxX = this.width + min; const maxY = this.height + min; const unNormalizedX = (point[0] - min) / (maxX - min); // we have to take into account that we reversed y when we first normalized it. const unNormalizedY = ((this.height + this.padding) - point[1] - min) / (maxY - min); return Vec2.create(unNormalizedX, unNormalizedY); } refCallBack(element) { if (element) { this.myRef = element; } } 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(_jsx("line", { x1: x, x2: x, y1: y1, y2: y2, stroke: "#ded9ca", strokeWidth: w }, `histogram${i}`)); } return bars; } renderPoints() { const points = []; let point; for (let i = 0; i < this.state.points.length; i++) { if (i !== 0 && i !== this.state.points.length - 1) { point = this.normalizePoint(this.state.points[i]); points.push(_jsx(PointComponent, { id: i, x: point[0], y: point[1], nX: this.state.points[i][0], nY: this.state.points[i][1], selected: false, delete: this.deletePoint, onmouseover: this.props.onHover, onmousedown: this.handleMouseDown(i), onclick: this.handleClick(i) }, i)); } } return points; } renderLines() { const points = []; const lines = []; let maxX; let maxY; let normalizedX; let normalizedY; let reverseY; const o = this.padding / 2; for (const point of this.state.points) { 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)); } const data = points; const size = data.length; for (let i = 0; i < size - 1; i++) { const x1 = data[i][0]; const y1 = data[i][1]; const x2 = data[i + 1][0]; const y2 = data[i + 1][1]; lines.push(_jsx("line", { x1: x1, x2: x2, y1: y1, y2: y2, stroke: "#cec9ba", strokeWidth: "5" }, `lineOf${i}`)); } return lines; } }