2 * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13 * or implied. See the License for the specific language governing
14 * permissions and limitations under the License.
17 import React from 'react';
18 import _template from 'lodash/template';
19 import _merge from 'lodash/merge';
22 import Common from '../../common/Common';
23 import Logger from '../../common/Logger';
24 import Popup from './components/popup/Popup';
29 export default class Diagram extends React.Component {
31 // ///////////////////////////////////////////////////////////////////////////////////////////////
34 * Construct React view.
35 * @param props properties.
36 * @param context context.
38 constructor(props, context) {
39 super(props, context);
41 this.application = Common.assertNotNull(props.application);
42 this.options = this.application.getOptions().diagram;
51 diagram: _template(require('./templates/diagram.html')),
52 lifeline: _template(require('./templates/lifeline.html')),
53 message: _template(require('./templates/message.html')),
54 occurrence: _template(require('./templates/occurrence.html')),
55 fragment: _template(require('./templates/fragment.html')),
56 title: _template(require('./templates/title.html')),
59 this.handleResize = this.handleResize.bind(this);
62 // ///////////////////////////////////////////////////////////////////////////////////////////////
69 this.svg.select('').text(n);
72 // ///////////////////////////////////////////////////////////////////////////////////////////////
75 * Get SVG from diagram.
79 const svg = this.svg.node().outerHTML;
80 return svg.replace('<svg ', '<svg xmlns="http://www.w3.org/2000/svg" ');
83 // ///////////////////////////////////////////////////////////////////////////////////////////////
86 * Select message by ID.
87 * @param id message ID.
90 const sel = this.svg.selectAll('g.asdcs-diagram-message-container');
91 sel.classed('asdcs-active', false);
92 sel.selectAll('rect.asdcs-diagram-message-bg').attr('filter', null);
94 const parent = this.svg.select(`g.asdcs-diagram-message-container[data-id="${id}"]`);
95 parent.classed('asdcs-active', true);
96 parent.selectAll('rect.asdcs-diagram-message-bg').attr('filter', 'url(#asdcsSvgHighlight)');
98 this._showNotesPopup(id);
101 // ///////////////////////////////////////////////////////////////////////////////////////////////
104 * Select lifeline by ID.
105 * @param id lifeline ID.
108 const sel = this.svg.selectAll('g.asdcs-diagram-lifeline-container');
109 sel.classed('asdcs-active', false);
110 sel.selectAll('rect').attr('filter', null);
112 const parent = this.svg.select(`g.asdcs-diagram-lifeline-container[data-id="${id}"]`);
113 parent.selectAll('rect').attr('filter', 'url(#asdcsSvgHighlight)');
114 parent.classed('asdcs-active', true);
118 // ///////////////////////////////////////////////////////////////////////////////////////////////
121 * Handle resize, including initial sizing.
125 const height = this.wrapper.offsetHeight;
126 const width = this.wrapper.offsetWidth;
127 if (this.state.height !== height || this.state.width !== width) {
128 this.setState({ height, width });
133 // ///////////////////////////////////////////////////////////////////////////////////////////////
136 * (Re)render diagram.
140 const model = this.application.getModel();
141 const modelJSON = model.unwrap();
142 const name = modelJSON.diagram.metadata.name;
143 const options = this.application.getOptions();
144 const titleHeight = options.diagram.title.height;
145 const titleClass = (titleHeight && titleHeight > 0) ? `height:${titleHeight}` : 'asdcs-hidden';
148 <div className="asdcs-diagram">
149 <div className={`asdcs-diagram-name ${titleClass}`}>{name}</div>
150 <div className="asdcs-diagram-svg" ref={(r) => { this.wrapper = r; }}></div>
151 <Popup visible={false} ref={(r) => { this.popup = r; }} />
156 // ///////////////////////////////////////////////////////////////////////////////////////////////
162 // ///////////////////////////////////////////////////////////////////////////////////////////////
167 componentDidMount() {
168 window.addEventListener('resize', this.handleResize);
178 // ///////////////////////////////////////////////////////////////////////////////////////////////
180 componentWillUnmount() {
181 window.removeEventListener('resize', this.handleResize);
184 // ///////////////////////////////////////////////////////////////////////////////////////////////
189 componentDidUpdate() {
193 // ///////////////////////////////////////////////////////////////////////////////////////////////
196 * Redraw SVG diagram. So far it's fast enough that it doesn't seem to matter whether
197 * it's completely redrawn.
202 const svgparams = _merge({}, this.options.svg);
203 this.wrapper.innerHTML = this.templates.diagram(svgparams);
204 this.svg = d3.select(this.wrapper).select('svg');
207 if (this.state.height === 0) {
209 // We'll get a resize event, and the height will be non-zero when it's actually time.
214 if (this.state.height && this.state.width) {
215 const margin = this.options.svg.margin;
218 const height = this.state.height + (margin * 2);
219 const width = this.state.width + (margin * 2);
220 const viewBox = `${x} ${y} ${width} ${height}`;
221 this.svg.attr('viewBox', viewBox);
225 // If we've already rendered, then save the current scale/translate so that we
226 // can reapply it after rendering.
228 const gContentSelection = this.svg.selectAll('g.asdcs-diagram-content');
229 if (gContentSelection.size() === 1) {
230 const transform = gContentSelection.attr('transform');
232 this.savedTransform = transform;
236 // Empty the document. We're starting again.
238 this.svg.selectAll('.asdcs-diagram-content').remove();
240 // Extract the model.
242 const model = this.application.getModel();
246 const modelJSON = model.unwrap();
248 // Extract dimension options.
250 const header = this.options.lifelines.header;
251 const spacing = this.options.lifelines.spacing;
253 // Make separate container elements so that we can control Z order.
255 const gContent = this.svg.append('g').attr('class', 'asdcs-diagram-content');
256 const gLifelines = gContent.append('g').attr('class', 'asdcs-diagram-lifelines');
257 const gCanvas = gContent.append('g').attr('class', 'asdcs-diagram-canvas');
258 gCanvas.append('g').attr('class', 'asdcs-diagram-occurrences');
259 gCanvas.append('g').attr('class', 'asdcs-diagram-fragments');
260 gCanvas.append('g').attr('class', 'asdcs-diagram-messages');
262 // Lifelines -----------------------------------------------------------------------------------
264 const actorsById = {};
265 const positionsByMessageId = {};
266 const lifelines = [];
267 for (const actor of modelJSON.diagram.lifelines) {
268 const x = (header.width / 2) + (lifelines.length * spacing.horizontal);
269 Diagram._processLifeline(actor, x);
270 lifelines.push({ x, actor });
271 actorsById[actor.id] = actor;
274 // Messages ------------------------------------------------------------------------------------
276 // Analyze occurrence information.
278 const occurrences = model.analyzeOccurrences();
279 const fragments = model.analyzeFragments();
280 let y = this.options.lifelines.header.height + spacing.vertical;
281 let messageIndex = 0;
282 for (const step of modelJSON.diagram.steps) {
284 positionsByMessageId[step.message.id] = positionsByMessageId[step.message.id] || {};
285 positionsByMessageId[step.message.id].y = y;
286 this._drawMessage(gCanvas, step.message, y, actorsById,
287 positionsByMessageId, ++messageIndex, occurrences, fragments);
289 y += spacing.vertical;
292 // ---------------------------------------------------------------------------------------------
294 // Draw the actual (dashed) lifelines in a background <g>.
296 this._drawLifelines(gLifelines, lifelines, y);
298 // Initialize mouse event handlers.
300 this._initMouseEvents(gLifelines, gCanvas);
304 const bb = gContent.node().getBBox();
305 this._initZoom(gContent, bb.width, bb.height);
308 // ///////////////////////////////////////////////////////////////////////////////////////////////
309 // ///////////////////////////////////////////////////////////////////////////////////////////////
310 // ///////////////////////////////////////////////////////////////////////////////////////////////
311 // ///////////////////////////////////////////////////////////////////////////////////////////////
312 // ///////////////////////////////////////////////////////////////////////////////////////////////
313 // ///////////////////////////////////////////////////////////////////////////////////////////////
314 // ///////////////////////////////////////////////////////////////////////////////////////////////
315 // ///////////////////////////////////////////////////////////////////////////////////////////////
316 // ///////////////////////////////////////////////////////////////////////////////////////////////
317 // ///////////////////////////////////////////////////////////////////////////////////////////////
320 * Draw message into SVG canvas.
321 * @param gCanvas container.
322 * @param message message to be rendered.
323 * @param y current y position.
324 * @param actorsById actor lookup.
325 * @param positionsByMessageId x- and y-position of each message.
326 * @param messageIndex where we are in the set of messages to be rendered.
327 * @param oData occurrences info.
328 * @param fData fragments info.
331 _drawMessage(gCanvas, message, y, actorsById, positionsByMessageId,
332 messageIndex, oData, fData) {
334 Common.assertNotNull(oData);
336 const request = message.type === 'request';
337 const fromActor = request ? actorsById[message.from] : actorsById[message.to];
338 const toActor = request ? actorsById[message.to] : actorsById[message.from];
341 Logger.warn(`Cannot draw message ${JSON.stringify(message)}: 'from' not found.`);
346 Logger.warn(`Cannot draw message ${JSON.stringify(message)}: 'to' not found.`);
350 // Occurrences. --------------------------------------------------------------------------------
352 if (message.occurrence) {
353 Logger.debug(`Found occurrence for ${message.name}: ${JSON.stringify(message.occurrence)}`);
355 const activeTo = Diagram._calcActive(oData, toActor.id);
356 this._drawOccurrence(gCanvas, oData, positionsByMessageId, fromActor, message.id);
357 this._drawOccurrence(gCanvas, oData, positionsByMessageId, toActor, message.id);
358 const activeFrom = Diagram._calcActive(oData, fromActor.id);
360 // Messages. -----------------------------------------------------------------------------------
362 const gMessages = gCanvas.select('g.asdcs-diagram-messages');
364 // Save positions for later.
366 const positions = positionsByMessageId[message.id];
367 positions.x0 = fromActor.x;
368 positions.x1 = toActor.x;
372 const leftToRight = fromActor.x < toActor.x;
373 const loopback = (message.to === message.from);
374 const x1 = this._calcMessageX(activeTo, toActor.x, true, leftToRight);
375 const x0 = loopback ? x1 : this._calcMessageX(activeFrom, fromActor.x, false, leftToRight);
382 messagePath = `M${x1},${y}`;
383 messagePath = `${messagePath} L${x1 + 200},${y}`;
384 messagePath = `${messagePath} L${x1 + 200},${y + 50}`;
385 messagePath = `${messagePath} L${x1},${y + 50}`;
388 // Between lifelines.
390 messagePath = `M${x0},${y}`;
391 messagePath = `${messagePath} L${x1},${y}`;
394 const styles = Diagram._getMessageStyles(message);
396 // Split message over lines.
398 const messageWithPrefix = `${messageIndex}. ${message.name}`;
399 const maxLines = this.options.messages.label.maxLines;
400 const wrapWords = this.options.messages.label.wrapWords;
401 const wrapLines = this.options.messages.label.wrapLines;
402 const messageLines = Common.tokenize(messageWithPrefix, wrapWords, wrapLines, maxLines);
404 const messageTxt = this.templates.message({
407 marker: styles.marker,
408 dasharray: styles.dasharray,
409 labels: messageLines,
416 const messageEl = Common.txt2dom(messageTxt);
417 const gMessage = gMessages.append('g');
418 Common.dom2svg(messageEl, gMessage);
420 // Set the background's bounding box to that of the text,
421 // so that they fit snugly.
423 const labelBB = gMessage.select('.asdcs-diagram-message-label').node().getBBox();
424 gMessage.select('.asdcs-diagram-message-label-bg')
425 .attr('x', labelBB.x)
426 .attr('y', labelBB.y)
427 .attr('height', labelBB.height)
428 .attr('width', labelBB.width);
430 // Fragments. ----------------------------------------------------------------------------------
432 const fragment = fData[message.id];
435 // It ends on this message.
437 this._drawFragment(gCanvas, fragment, positionsByMessageId);
442 // ///////////////////////////////////////////////////////////////////////////////////////////////
445 * Draw a single occurrence.
446 * @param gCanvas container.
447 * @param oData occurrence data.
448 * @param positionsByMessageId map of y positions by message ID.
449 * @param actor wrapper containing lifeline ID (.id), position (.x) and name (.name).
450 * @param messageId message identifier.
453 _drawOccurrence(gCanvas, oData, positionsByMessageId, actor, messageId) {
455 Common.assertType(oData, 'Object');
456 Common.assertType(positionsByMessageId, 'Object');
457 Common.assertType(actor, 'Object');
458 Common.assertType(messageId, 'String');
460 const gOccurrences = gCanvas.select('g.asdcs-diagram-occurrences');
462 const oOptions = this.options.lifelines.occurrences;
463 const oWidth = oOptions.width;
464 const oHalfWidth = oWidth / 2;
465 const oForeshortening = oOptions.foreshortening;
466 const oMarginTop = oOptions.marginTop;
467 const oMarginBottom = oOptions.marginBottom;
468 const o = oData[actor.id];
470 const active = Diagram._calcActive(oData, actor.id);
472 const x = (actor.x - oHalfWidth) + (active * oWidth);
473 const positions = positionsByMessageId[messageId];
474 const y = positions.y;
479 if (o.start[messageId]) {
481 // Starting, but drawing nothing until we find the end.
483 o.active.push(messageId);
486 } else if (active > 0) {
488 const startMessageId = o.stop[messageId];
489 if (startMessageId) {
491 // OK, it ends here. Draw the occurrence box.
494 const foreshorteningY = active * oForeshortening;
495 const startY = positionsByMessageId[startMessageId].y;
496 const height = ((oMarginTop + oMarginBottom) + (y - startY)) - (foreshorteningY * 2);
498 x: (actor.x - oHalfWidth) + ((active - 1) * oWidth),
499 y: ((startY - oMarginTop) + foreshorteningY),
504 const occurrenceTxt = this.templates.occurrence(oProps);
505 const occurrenceEl = Common.txt2dom(occurrenceTxt);
506 Common.dom2svg(occurrenceEl, gOccurrences.append('g'));
515 // Seems this is a singleton occurrence. We just draw a wee box around it.
517 const height = (oMarginTop + oMarginBottom);
518 const occurrenceProperties = { x, y: y - oMarginTop, height, width: oWidth };
519 const defaultTxt = this.templates.occurrence(occurrenceProperties);
520 const defaultEl = Common.txt2dom(defaultTxt);
521 Common.dom2svg(defaultEl, gOccurrences.append('g'));
526 // ///////////////////////////////////////////////////////////////////////////////////////////////
529 * Draw box(es) around fragment(s).
530 * @param gCanvas container.
531 * @param fragment fragment definition, corresponding to its final (stop) message.
532 * @param positionsByMessageId message dimensions.
535 _drawFragment(gCanvas, fragment, positionsByMessageId) {
537 const optFragments = this.options.fragments;
538 const gFragments = gCanvas.select('g.asdcs-diagram-fragments');
539 const p1 = positionsByMessageId[fragment.stop];
540 if (p1 && fragment.start && fragment.start.length > 0) {
542 for (const start of fragment.start) {
544 const message = this.application.getModel().getMessageById(start);
545 const bounds = this._calcFragmentBounds(message, fragment, positionsByMessageId);
548 const maxLines = this.options.fragments.label.maxLines;
549 const wrapWords = this.options.fragments.label.wrapWords;
550 const wrapLines = this.options.fragments.label.wrapLines;
551 const lines = Common.tokenize(message.fragment.guard, wrapWords, wrapLines, maxLines);
555 x: bounds.x0 - optFragments.leftMargin,
556 y: bounds.y0 - optFragments.topMargin,
557 height: (bounds.y1 - bounds.y0) + optFragments.heightMargin,
558 width: (bounds.x1 - bounds.x0) + optFragments.widthMargin,
559 operator: (message.fragment.operator || 'alt'),
563 const fragmentTxt = this.templates.fragment(params);
564 const fragmentEl = Common.txt2dom(fragmentTxt);
565 const gFragment = gFragments.append('g');
566 Common.dom2svg(fragmentEl, gFragment);
568 const labelBB = gFragment.select('.asdcs-diagram-fragment-guard').node().getBBox();
569 gFragment.select('.asdcs-diagram-fragment-guard-bg')
570 .attr('x', labelBB.x)
571 .attr('y', labelBB.y)
572 .attr('height', labelBB.height)
573 .attr('width', labelBB.width);
576 Logger.warn(`Bad fragment: ${JSON.stringify(fragment)}`);
582 // ///////////////////////////////////////////////////////////////////////////////////////////////
584 _calcFragmentBounds(startMessage, fragment, positionsByMessageId) {
586 const steps = this.application.getModel().unwrap().diagram.steps;
587 const bounds = { x0: 99999, x1: 0, y0: 99999, y1: 0 };
588 let foundStart = false;
589 let foundStop = false;
590 for (const step of steps) {
591 const message = step.message;
593 if (message.id === startMessage.id) {
596 if (foundStart && !foundStop) {
597 const positions = positionsByMessageId[message.id];
599 bounds.x0 = Math.min(bounds.x0, Math.min(positions.x0, positions.x1));
600 bounds.y0 = Math.min(bounds.y0, positions.y);
601 bounds.x1 = Math.max(bounds.x1, Math.max(positions.x0, positions.x1));
602 bounds.y1 = Math.max(bounds.y1, positions.y);
604 // This probably means it hasn't been recorded yet, which is fine, because
605 // we draw fragments from where they END.
610 if (message.id === fragment.stop) {
620 // ///////////////////////////////////////////////////////////////////////////////////////////////
623 * Draw all lifelines.
624 * @param gLifelines lifelines container.
625 * @param lifelines lifelines definitions.
629 _drawLifelines(gLifelines, lifelines, y) {
631 const maxLines = this.options.lifelines.header.maxLines;
632 const wrapWords = this.options.lifelines.header.wrapWords;
633 const wrapLines = this.options.lifelines.header.wrapLines;
635 for (const lifeline of lifelines) {
636 const lines = Common.tokenize(lifeline.actor.name, wrapWords, wrapLines, maxLines);
637 const lifelineTxt = this.templates.lifeline({
643 headerHeight: this.options.lifelines.header.height,
644 headerWidth: this.options.lifelines.header.width,
645 id: lifeline.actor.id,
648 const lifelineEl = Common.txt2dom(lifelineTxt);
649 Common.dom2svg(lifelineEl, gLifelines.append('g'));
653 // ///////////////////////////////////////////////////////////////////////////////////////////////
656 * Initialize all mouse events.
657 * @param gLifelines lifelines container.
658 * @param gCanvas top-level canvas container.
661 _initMouseEvents(gLifelines, gCanvas) {
664 const source = 'asdcs';
665 const origin = `${window.location.protocol}//${window.location.host}`;
668 gLifelines.selectAll('.asdcs-diagram-lifeline-selectable')
669 .on('mouseenter', function f() {
670 timer = setTimeout(() => {
671 self.application.selectLifeline(d3.select(this.parentNode).attr('data-id'));
674 .on('mouseleave', () => {
676 self.application.selectLifeline();
678 .on('click', function f() {
679 const id = d3.select(this.parentNode).attr('data-id');
680 window.postMessage({ source, id, type: 'lifeline' }, origin);
683 gLifelines.selectAll('.asdcs-diagram-lifeline-heading-box')
684 .on('mouseenter', function f() {
685 timer = setTimeout(() => {
686 self.application.selectLifeline(d3.select(this.parentNode).attr('data-id'));
689 .on('mouseleave', () => {
691 self.application.selectLifeline();
693 .on('click', function f() {
694 const id = d3.select(this.parentNode).attr('data-id');
695 window.postMessage({ source, id, type: 'lifelineHeader' }, origin);
698 gCanvas.selectAll('.asdcs-diagram-message-selectable')
699 .on('mouseenter', function f() {
700 self.events.message = { x: d3.event.pageX, y: d3.event.pageY };
701 timer = setTimeout(() => {
702 self.application.selectMessage(d3.select(this.parentNode).attr('data-id'));
705 .on('mouseleave', () => {
706 delete self.events.message;
708 self.application.selectMessage();
710 .on('click', function f() {
711 const id = d3.select(this.parentNode).attr('data-id');
712 window.postMessage({ source, id, type: 'message' }, origin);
717 // ///////////////////////////////////////////////////////////////////////////////////////////////
720 * Get CSS classes to be applied to a message, according to whether request/response
721 * or synchronous/asynchronous.
722 * @param message message being rendered.
723 * @returns CSS class name(s).
726 static _getMessageStyles(message) {
728 let marker = 'asdcsDiagramArrowSolid';
730 let css = 'asdcs-diagram-message';
731 if (message.type === 'request') {
732 css = `${css} asdcs-diagram-message-request`;
734 css = `${css} asdcs-diagram-message-response`;
735 marker = 'asdcsDiagramArrowOpen';
736 dasharray = '30, 10';
739 if (message.asynchronous) {
740 css = `${css} asdcs-diagram-message-asynchronous`;
741 marker = 'asdcsDiagramArrowOpen';
743 css = `${css} asdcs-diagram-message-synchronous`;
746 return { css, marker, dasharray };
749 // ///////////////////////////////////////////////////////////////////////////////////////////////
752 * Initialize or reinitialize zoom. This sets the initial zoom in the case of
753 * a re-rendering, and initializes the eventhandling in all cases.
755 * It does some fairly risky parsing of the 'transform' attribute, assuming that it
756 * can contain scale() and translate(). But only the zoom handler and us are writing
757 * the transform values, so that's probably OK.
759 * @param gContent container.
760 * @param width diagram width.
761 * @param height diagram height.
764 _initZoom(gContent, width, height) {
766 const zoomed = function zoomed() {
767 gContent.attr('transform',
768 `translate(${d3.event.translate})scale(${d3.event.scale})`);
771 const viewWidth = this.state.width || this.options.svg.width;
772 const viewHeight = this.state.height || this.options.svg.height;
773 const scaleMinimum = this.options.svg.scale.minimum;
774 const scaleWidth = viewWidth / width;
775 const scaleHeight = viewHeight / height;
777 let scale = scaleMinimum;
778 if (this.options.svg.scale.width) {
779 scale = Math.max(scale, scaleWidth);
781 if (this.options.svg.scale.height) {
782 scale = Math.min(scale, scaleHeight);
785 scale = Math.max(scale, scaleMinimum);
787 let translate = [0, 0];
788 if (this.savedTransform) {
789 const s = this.savedTransform;
790 const scaleStart = s.indexOf('scale(');
791 if (scaleStart !== -1) {
792 scale = parseFloat(s.substring(scaleStart + 6, s.length - 1));
794 const translateStart = s.indexOf('translate(');
795 if (translateStart !== -1) {
796 const spec = s.substring(translateStart + 10, s.indexOf(')', translateStart));
797 const tokens = spec.split(',');
798 translate = [parseFloat(tokens[0]), parseFloat(tokens[1])];
801 gContent.attr('transform', this.savedTransform);
803 gContent.attr('transform', `scale(${scale})`);
806 const zoom = d3.behavior.zoom()
808 .scaleExtent([scaleMinimum, 10])
809 .translate(translate)
814 // ///////////////////////////////////////////////////////////////////////////////////////////////
817 * Hide from the linter the fact that we're modifying the lifeline.
818 * @param lifeline to be updated with X position.
819 * @param x X position.
822 static _processLifeline(lifeline, x) {
823 const actor = lifeline;
824 actor.id = actor.id || actor.name;
828 // ///////////////////////////////////////////////////////////////////////////////////////////////
831 * Derive active occurrences for lifeline.
832 * @param oData occurrences data.
833 * @param lifelineId lifeline to be analyzed.
837 static _calcActive(oData, lifelineId) {
838 const o = oData[lifelineId];
841 active = o.active.length;
846 // ///////////////////////////////////////////////////////////////////////////////////////////////
849 * Derive the X position of an occurrence on a lifeline, taking into account how
850 * many occurrences are active.
851 * @param active active count.
852 * @param x lifeline X position; basis for offset.
853 * @param arrow whether this is the arrow (to) end.
854 * @param leftToRight whether this message goes left-to-right.
855 * @returns {*} calculated X position for occurrence left-hand side.
858 _calcMessageX(active, x, arrow, leftToRight) {
859 const width = this.options.lifelines.occurrences.width;
860 const halfWidth = width / 2;
861 const active0 = Math.max(0, active - 1);
862 let calculated = x + (active0 * width);
866 calculated -= halfWidth;
868 calculated += halfWidth;
871 // Start (NOT ARROW).
873 calculated += halfWidth;
875 calculated -= halfWidth;
882 // ///////////////////////////////////////////////////////////////////////////////////////////////
885 * Show popup upon hovering over a messages that has associated notes.
889 _showNotesPopup(id) {
892 const message = this.application.getModel().getMessageById(id);
893 if (message && message.notes && message.notes.length > 0 && this.events.message) {
894 this.popup.setState({
896 left: this.events.message.x - 50,
897 top: this.events.message.y + 20,
898 notes: message.notes[0],
902 this.popup.setState({ visible: false, notes: '' });
909 Diagram.propTypes = {
910 application: React.PropTypes.object.isRequired,