line-graph-component.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. /**
  2. * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Paul Luna <paulluna0215@gmail.com>
  5. */
  6. import { PointComponent } from './point-component';
  7. import * as React from 'react';
  8. import { Vec2 } from '../../../mol-math/linear-algebra';
  9. interface LineGraphComponentState {
  10. points: Vec2[],
  11. copyPoint: any,
  12. canSelectMultiple: boolean,
  13. }
  14. export class LineGraphComponent extends React.Component<any, LineGraphComponentState> {
  15. private myRef: any;
  16. private height: number;
  17. private width: number;
  18. private padding: number;
  19. private updatedX: number;
  20. private updatedY: number;
  21. private selected?: number[];
  22. private ghostPoints: SVGElement[];
  23. private gElement: SVGElement;
  24. private namespace: string;
  25. constructor(props: any) {
  26. super(props);
  27. this.myRef = React.createRef();
  28. this.state = {
  29. points: [
  30. Vec2.create(0, 0),
  31. Vec2.create(1, 0)
  32. ],
  33. copyPoint: undefined,
  34. canSelectMultiple: false,
  35. };
  36. this.height = 400;
  37. this.width = 600;
  38. this.padding = 70;
  39. this.selected = undefined;
  40. this.ghostPoints = [];
  41. this.namespace = 'http://www.w3.org/2000/svg';
  42. for (const point of this.props.data) {
  43. this.state.points.push(point);
  44. }
  45. this.state.points.sort((a, b) => {
  46. if (a[0] === b[0]) {
  47. if (a[0] === 0) {
  48. return a[1] - b[1];
  49. }
  50. if (a[1] === 1) {
  51. return b[1] - a[1];
  52. }
  53. return a[1] - b[1];
  54. }
  55. return a[0] - b[0];
  56. });
  57. this.handleDrag = this.handleDrag.bind(this);
  58. this.handleMultipleDrag = this.handleMultipleDrag.bind(this);
  59. this.handleDoubleClick = this.handleDoubleClick.bind(this);
  60. this.refCallBack = this.refCallBack.bind(this);
  61. this.handlePointUpdate = this.handlePointUpdate.bind(this);
  62. this.change = this.change.bind(this);
  63. this.handleKeyUp = this.handleKeyUp.bind(this);
  64. this.handleLeave = this.handleLeave.bind(this);
  65. this.handleEnter = this.handleEnter.bind(this);
  66. }
  67. public render() {
  68. const points = this.renderPoints();
  69. const lines = this.renderLines();
  70. return ([
  71. <div key="LineGraph">
  72. <svg
  73. className="msp-canvas"
  74. ref={this.refCallBack}
  75. viewBox={`0 0 ${this.width + this.padding} ${this.height + this.padding}`}
  76. onMouseMove={this.handleDrag}
  77. onMouseUp={this.handlePointUpdate}
  78. onMouseLeave={this.handleLeave}
  79. onMouseEnter={this.handleEnter}
  80. tabIndex={0}
  81. onKeyDown={this.handleKeyDown}
  82. onKeyUp={this.handleKeyUp}
  83. onDoubleClick={this.handleDoubleClick}>
  84. <g stroke="black" fill="black">
  85. {lines}
  86. {points}
  87. </g>
  88. <g className="ghost-points" stroke="black" fill="black">
  89. </g>
  90. </svg>
  91. </div>,
  92. <div key="modal" id="modal-root" />
  93. ]);
  94. }
  95. componentDidMount() {
  96. this.gElement = document.getElementsByClassName('ghost-points')[0] as SVGElement;
  97. }
  98. private change(points: Vec2[]) {
  99. const copyPoints = points.slice();
  100. copyPoints.shift();
  101. copyPoints.pop();
  102. this.props.onChange(copyPoints);
  103. }
  104. private handleKeyDown = (event: any) => {
  105. // TODO: set canSelectMultiple = true
  106. }
  107. private handleKeyUp = (event: any) => {
  108. // TODO: SET canSelectMultiple = fasle
  109. }
  110. private handleClick = (id: number) => (event: any) => {
  111. // TODO: add point to selected array
  112. }
  113. private handleMouseDown = (id: number) => (event: any) => {
  114. if (id === 0 || id === this.state.points.length - 1) {
  115. return;
  116. }
  117. if (this.state.canSelectMultiple) {
  118. return;
  119. }
  120. const copyPoint: Vec2 = this.normalizePoint(Vec2.create(this.state.points[id][0], this.state.points[id][1]));
  121. this.ghostPoints.push(document.createElementNS(this.namespace, 'circle') as SVGElement);
  122. this.ghostPoints[0].setAttribute('r', '10');
  123. this.ghostPoints[0].setAttribute('fill', 'orange');
  124. this.ghostPoints[0].setAttribute('cx', `${copyPoint[0]}`);
  125. this.ghostPoints[0].setAttribute('cy', `${copyPoint[1]}`);
  126. this.ghostPoints[0].setAttribute('style', 'display: none');
  127. this.gElement.appendChild(this.ghostPoints[0]);
  128. this.updatedX = copyPoint[0];
  129. this.updatedY = copyPoint[1];
  130. this.selected = [id];
  131. }
  132. private handleDrag(event: any) {
  133. if (this.selected === undefined) {
  134. return;
  135. }
  136. const pt = this.myRef.createSVGPoint();
  137. let updatedCopyPoint;
  138. const padding = this.padding / 2;
  139. pt.x = event.clientX;
  140. pt.y = event.clientY;
  141. const svgP = pt.matrixTransform(this.myRef.getScreenCTM().inverse());
  142. updatedCopyPoint = Vec2.create(svgP.x, svgP.y);
  143. if ((svgP.x < (padding) || svgP.x > (this.width + (padding))) && (svgP.y > (this.height + (padding)) || svgP.y < (padding))) {
  144. updatedCopyPoint = Vec2.create(this.updatedX, this.updatedY);
  145. } else if (svgP.x < padding) {
  146. updatedCopyPoint = Vec2.create(padding, svgP.y);
  147. } else if (svgP.x > (this.width + (padding))) {
  148. updatedCopyPoint = Vec2.create(this.width + padding, svgP.y);
  149. } else if (svgP.y > (this.height + (padding))) {
  150. updatedCopyPoint = Vec2.create(svgP.x, this.height + padding);
  151. } else if (svgP.y < (padding)) {
  152. updatedCopyPoint = Vec2.create(svgP.x, padding);
  153. } else {
  154. updatedCopyPoint = Vec2.create(svgP.x, svgP.y);
  155. }
  156. this.updatedX = updatedCopyPoint[0];
  157. this.updatedY = updatedCopyPoint[1];
  158. const unNormalizePoint = this.unNormalizePoint(updatedCopyPoint);
  159. this.ghostPoints[0].setAttribute('style', 'display: visible');
  160. this.ghostPoints[0].setAttribute('cx', `${updatedCopyPoint[0]}`);
  161. this.ghostPoints[0].setAttribute('cy', `${updatedCopyPoint[1]}`);
  162. this.props.onDrag(unNormalizePoint);
  163. }
  164. private handleMultipleDrag() {
  165. // TODO
  166. }
  167. private handlePointUpdate(event: any) {
  168. const selected = this.selected;
  169. if (this.state.canSelectMultiple) {
  170. return;
  171. }
  172. if (selected === undefined || selected[0] === 0 || selected[0] === this.state.points.length - 1) {
  173. this.setState({
  174. copyPoint: undefined,
  175. });
  176. return;
  177. }
  178. this.selected = undefined;
  179. const updatedPoint = this.unNormalizePoint(Vec2.create(this.updatedX, this.updatedY));
  180. const points = this.state.points.filter((_, i) => i !== selected[0]);
  181. points.push(updatedPoint);;
  182. points.sort((a, b) => {
  183. if (a[0] === b[0]) {
  184. if (a[0] === 0) {
  185. return a[1] - b[1];
  186. }
  187. if (a[1] === 1) {
  188. return b[1] - a[1];
  189. }
  190. return a[1] - b[1];
  191. }
  192. return a[0] - b[0];
  193. });
  194. this.setState({
  195. points,
  196. });
  197. this.change(points);
  198. this.gElement.innerHTML = '';
  199. this.ghostPoints = [];
  200. document.removeEventListener('mousemove', this.handleDrag, true);
  201. document.removeEventListener('mouseup', this.handlePointUpdate, true);
  202. }
  203. private handleDoubleClick(event: any) {
  204. const pt = this.myRef.createSVGPoint();
  205. pt.x = event.clientX;
  206. pt.y = event.clientY;
  207. const svgP = pt.matrixTransform(this.myRef.getScreenCTM().inverse());
  208. const points = this.state.points;
  209. const padding = this.padding / 2;
  210. if (svgP.x < (padding) ||
  211. svgP.x > (this.width + (padding)) ||
  212. svgP.y > (this.height + (padding)) ||
  213. svgP.y < (this.padding / 2)) {
  214. return;
  215. }
  216. const newPoint = this.unNormalizePoint(Vec2.create(svgP.x, svgP.y));
  217. points.push(newPoint);
  218. points.sort((a, b) => {
  219. if (a[0] === b[0]) {
  220. if (a[0] === 0) {
  221. return a[1] - b[1];
  222. }
  223. if (a[1] === 1) {
  224. return b[1] - a[1];
  225. }
  226. return a[1] - b[1];
  227. }
  228. return a[0] - b[0];
  229. });
  230. this.setState({ points });
  231. this.change(points);
  232. }
  233. private deletePoint = (i: number) => (event: any) => {
  234. if (i === 0 || i === this.state.points.length - 1) { return; }
  235. const points = this.state.points.filter((_, j) => j !== i);
  236. points.sort((a, b) => {
  237. if (a[0] === b[0]) {
  238. if (a[0] === 0) {
  239. return a[1] - b[1];
  240. }
  241. if (a[1] === 1) {
  242. return b[1] - a[1];
  243. }
  244. return a[1] - b[1];
  245. }
  246. return a[0] - b[0];
  247. });
  248. this.setState({ points });
  249. this.change(points);
  250. event.stopPropagation();
  251. }
  252. private handleLeave() {
  253. if (this.selected === undefined) {
  254. return;
  255. }
  256. document.addEventListener('mousemove', this.handleDrag, true);
  257. document.addEventListener('mouseup', this.handlePointUpdate, true);
  258. }
  259. private handleEnter() {
  260. document.removeEventListener('mousemove', this.handleDrag, true);
  261. document.removeEventListener('mouseup', this.handlePointUpdate, true);
  262. }
  263. private normalizePoint(point: Vec2) {
  264. const min = this.padding / 2;
  265. const maxX = this.width + min;
  266. const maxY = this.height + min;
  267. const normalizedX = (point[0] * (maxX - min)) + min;
  268. const normalizedY = (point[1] * (maxY - min)) + min;
  269. const reverseY = (this.height + this.padding) - normalizedY;
  270. const newPoint = Vec2.create(normalizedX, reverseY);
  271. return newPoint;
  272. }
  273. private unNormalizePoint(point: Vec2) {
  274. const min = this.padding / 2;
  275. const maxX = this.width + min;
  276. const maxY = this.height + min;
  277. const unNormalizedX = (point[0] - min) / (maxX - min);
  278. // we have to take into account that we reversed y when we first normalized it.
  279. const unNormalizedY = ((this.height + this.padding) - point[1] - min) / (maxY - min);
  280. return Vec2.create(unNormalizedX, unNormalizedY);
  281. }
  282. private refCallBack(element: any) {
  283. if (element) {
  284. this.myRef = element;
  285. }
  286. }
  287. private renderPoints() {
  288. const points: any[] = [];
  289. let point: Vec2;
  290. for (let i = 0; i < this.state.points.length; i++) {
  291. if (i !== 0 && i !== this.state.points.length - 1) {
  292. point = this.normalizePoint(this.state.points[i]);
  293. points.push(<PointComponent
  294. key={i}
  295. id={i}
  296. x={point[0]}
  297. y={point[1]}
  298. nX={this.state.points[i][0]}
  299. nY={this.state.points[i][1]}
  300. selected={false}
  301. delete={this.deletePoint}
  302. onmouseover={this.props.onHover}
  303. onmousedown={this.handleMouseDown(i)}
  304. onclick={this.handleClick(i)}
  305. />);
  306. }
  307. }
  308. return points;
  309. }
  310. private renderLines() {
  311. const points: Vec2[] = [];
  312. const lines = [];
  313. let min: number;
  314. let maxX: number;
  315. let maxY: number;
  316. let normalizedX: number;
  317. let normalizedY: number;
  318. let reverseY: number;
  319. for (const point of this.state.points) {
  320. min = this.padding / 2;
  321. maxX = this.width + min;
  322. maxY = this.height + min;
  323. normalizedX = (point[0] * (maxX - min)) + min;
  324. normalizedY = (point[1] * (maxY - min)) + min;
  325. reverseY = this.height + this.padding - normalizedY;
  326. points.push(Vec2.create(normalizedX, reverseY));
  327. }
  328. const data = points;
  329. const size = data.length;
  330. for (let i = 0; i < size - 1;i++) {
  331. const x1 = data[i][0];
  332. const y1 = data[i][1];
  333. const x2 = data[i + 1][0];
  334. const y2 = data[i + 1][1];
  335. lines.push(<line key={`lineOf${i}`} x1={x1} x2={x2} y1={y1} y2={y2} stroke="#cec9ba" strokeWidth="5"/>);
  336. }
  337. return lines;
  338. }
  339. }