line-graph-component.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  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 default 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. let 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. let newPoint;
  205. const pt = this.myRef.createSVGPoint();
  206. pt.x = event.clientX;
  207. pt.y = event.clientY;
  208. const svgP = pt.matrixTransform(this.myRef.getScreenCTM().inverse());
  209. const points = this.state.points;
  210. const padding = this.padding/2;
  211. if( svgP.x < (padding) ||
  212. svgP.x > (this.width+(padding)) ||
  213. svgP.y > (this.height+(padding)) ||
  214. svgP.y < (this.padding/2)) {
  215. return;
  216. }
  217. newPoint = this.unNormalizePoint(Vec2.create(svgP.x, svgP.y));
  218. points.push(newPoint);
  219. points.sort((a, b) => {
  220. if(a[0] === b[0]){
  221. if(a[0] === 0){
  222. return a[1]-b[1];
  223. }
  224. if(a[1] === 1){
  225. return b[1]-a[1];
  226. }
  227. return a[1]-b[1];
  228. }
  229. return a[0] - b[0];
  230. });
  231. this.setState({points})
  232. this.change(points);
  233. }
  234. private deletePoint = (i: number) => (event: any) => {
  235. if(i===0 || i===this.state.points.length-1){ return; }
  236. const points = this.state.points.filter((_,j) => j !== i);
  237. points.sort((a, b) => {
  238. if(a[0] === b[0]){
  239. if(a[0] === 0){
  240. return a[1]-b[1];
  241. }
  242. if(a[1] === 1){
  243. return b[1]-a[1];
  244. }
  245. return a[1]-b[1];
  246. }
  247. return a[0] - b[0];
  248. });
  249. this.setState({points});
  250. this.change(points);
  251. event.stopPropagation();
  252. }
  253. private handleLeave() {
  254. if(this.selected === undefined) {
  255. return;
  256. }
  257. document.addEventListener('mousemove', this.handleDrag, true);
  258. document.addEventListener('mouseup', this.handlePointUpdate, true);
  259. }
  260. private handleEnter() {
  261. document.removeEventListener('mousemove', this.handleDrag, true);
  262. document.removeEventListener('mouseup', this.handlePointUpdate, true);
  263. }
  264. private normalizePoint(point: Vec2) {
  265. const min = this.padding/2;
  266. const maxX = this.width+min;
  267. const maxY = this.height+min;
  268. const normalizedX = (point[0]*(maxX-min))+min;
  269. const normalizedY = (point[1]*(maxY-min))+min;
  270. const reverseY = (this.height+this.padding)-normalizedY;
  271. const newPoint = Vec2.create(normalizedX, reverseY);
  272. return newPoint;
  273. }
  274. private unNormalizePoint(point: Vec2) {
  275. const min = this.padding/2;
  276. const maxX = this.width+min;
  277. const maxY = this.height+min;
  278. const unNormalizedX = (point[0]-min)/(maxX-min);
  279. // we have to take into account that we reversed y when we first normalized it.
  280. const unNormalizedY = ((this.height+this.padding)-point[1]-min)/(maxY-min);
  281. return Vec2.create(unNormalizedX, unNormalizedY);
  282. }
  283. private refCallBack(element: any) {
  284. if(element){
  285. this.myRef = element;
  286. }
  287. }
  288. private renderPoints() {
  289. const points: any[] = [];
  290. let point: Vec2;
  291. for (let i = 0; i < this.state.points.length; i++){
  292. if(i !== 0 && i !== this.state.points.length-1){
  293. point = this.normalizePoint(this.state.points[i]);
  294. points.push(<PointComponent
  295. key={i}
  296. id={i}
  297. x={point[0]}
  298. y={point[1]}
  299. nX={this.state.points[i][0]}
  300. nY={this.state.points[i][1]}
  301. selected={false}
  302. delete={this.deletePoint}
  303. onmouseover={this.props.onHover}
  304. onmousedown={this.handleMouseDown(i)}
  305. onclick={this.handleClick(i)}
  306. />);
  307. }
  308. }
  309. return points;
  310. }
  311. private renderLines() {
  312. const points: Vec2[] = [];
  313. let lines = [];
  314. let min: number;
  315. let maxX: number;
  316. let maxY: number;
  317. let normalizedX: number;
  318. let normalizedY: number;
  319. let reverseY: number;
  320. for(const point of this.state.points){
  321. min = this.padding/2;
  322. maxX = this.width+min;
  323. maxY = this.height+min;
  324. normalizedX = (point[0]*(maxX-min))+min;
  325. normalizedY = (point[1]*(maxY-min))+min;
  326. reverseY = this.height+this.padding-normalizedY;
  327. points.push(Vec2.create(normalizedX, reverseY));
  328. }
  329. const data = points;
  330. const size = data.length;
  331. for (let i=0; i<size-1;i++){
  332. const x1 = data[i][0];
  333. const y1 = data[i][1];
  334. const x2 = data[i+1][0];
  335. const y2 = data[i+1][1];
  336. lines.push(<line key={`lineOf${i}`} x1={x1} x2={x2} y1={y1} y2={y2} stroke="#cec9ba" strokeWidth="5"/>)
  337. }
  338. return lines;
  339. }
  340. }