<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[248910] trunk/Tools</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/248910">248910</a></dd>
<dt>Author</dt> <dd>jbedard@apple.com</dd>
<dt>Date</dt> <dd>2019-08-20 11:55:34 -0700 (Tue, 20 Aug 2019)</dd>
</dl>
<h3>Log Message</h3>
<pre>results.webkit.org: Add ToolTips
https://bugs.webkit.org/show_bug.cgi?id=200801
Rubber-stamped by Aakash Jain.
When dots or scale labels are hovered over, we should display a tool tip with additional information about
The specific element.
* resultsdbpy/resultsdbpy/view/static/css/tooltip.css: Added.
(.tooltip): Add class for ToolTip text box.
(.tooltip-arrow-up): Add class for ToolTip arrow pointing up.
(.tooltip-arrow-down): Add class for ToolTip arrow pointing down.
* resultsdbpy/resultsdbpy/view/static/js/commit.js:
(_CommitBank.prototype.commitsDuringUuid): Return a list of commits which were the trunk of their respective
repositories at a given time.
* resultsdbpy/resultsdbpy/view/static/js/timeline.js:
(xAxisFromScale): Add callbacks triggered when the mouse enters or leaves elements in the scale canvas.
(TimelineFromEndpoint.render): Add callbacks triggered when the mouse enters or leaves dot elements.
* resultsdbpy/resultsdbpy/view/static/js/tooltip.js: Added.
(isPointInElement): Given an element and a point, return true if that point is within the bounds of the element.
(_ToolTip):
(_ToolTip.prototype.set): Set the content and location of the ToolTip.
(_ToolTip.prototype.toString): Return the html needed to render the ToolTip.
(_ToolTip.prototype.unset): Clear and hide the ToolTip.
(_ToolTip.prototype.isIn): Check if a given point is contained within the ToolTip.
* resultsdbpy/resultsdbpy/view/static/library/js/components/TimelineComponents.js:
(Timeline.CanvasSeriesComponent): Convert onHover events to onEnter/onLeave events. Add toolTips points to both
dot and scale elements.
* resultsdbpy/resultsdbpy/view/templates/search.html: Add ToolTip.
* resultsdbpy/resultsdbpy/view/templates/suite_results.html: Ditto.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkToolsChangeLog">trunk/Tools/ChangeLog</a></li>
<li><a href="#trunkToolsresultsdbpyresultsdbpyviewstaticjscommitjs">trunk/Tools/resultsdbpy/resultsdbpy/view/static/js/commit.js</a></li>
<li><a href="#trunkToolsresultsdbpyresultsdbpyviewstaticjstimelinejs">trunk/Tools/resultsdbpy/resultsdbpy/view/static/js/timeline.js</a></li>
<li><a href="#trunkToolsresultsdbpyresultsdbpyviewstaticlibraryjscomponentsTimelineComponentsjs">trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/components/TimelineComponents.js</a></li>
<li><a href="#trunkToolsresultsdbpyresultsdbpyviewtemplatessearchhtml">trunk/Tools/resultsdbpy/resultsdbpy/view/templates/search.html</a></li>
<li><a href="#trunkToolsresultsdbpyresultsdbpyviewtemplatessuite_resultshtml">trunk/Tools/resultsdbpy/resultsdbpy/view/templates/suite_results.html</a></li>
</ul>
<h3>Added Paths</h3>
<ul>
<li><a href="#trunkToolsresultsdbpyresultsdbpyviewstaticcsstooltipcss">trunk/Tools/resultsdbpy/resultsdbpy/view/static/css/tooltip.css</a></li>
<li><a href="#trunkToolsresultsdbpyresultsdbpyviewstaticjstooltipjs">trunk/Tools/resultsdbpy/resultsdbpy/view/static/js/tooltip.js</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkToolsChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Tools/ChangeLog (248909 => 248910)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/ChangeLog 2019-08-20 18:46:36 UTC (rev 248909)
+++ trunk/Tools/ChangeLog 2019-08-20 18:55:34 UTC (rev 248910)
</span><span class="lines">@@ -1,3 +1,36 @@
</span><ins>+2019-08-20 Jonathan Bedard <jbedard@apple.com>
+
+ results.webkit.org: Add ToolTips
+ https://bugs.webkit.org/show_bug.cgi?id=200801
+
+ Rubber-stamped by Aakash Jain.
+
+ When dots or scale labels are hovered over, we should display a tool tip with additional information about
+ The specific element.
+
+ * resultsdbpy/resultsdbpy/view/static/css/tooltip.css: Added.
+ (.tooltip): Add class for ToolTip text box.
+ (.tooltip-arrow-up): Add class for ToolTip arrow pointing up.
+ (.tooltip-arrow-down): Add class for ToolTip arrow pointing down.
+ * resultsdbpy/resultsdbpy/view/static/js/commit.js:
+ (_CommitBank.prototype.commitsDuringUuid): Return a list of commits which were the trunk of their respective
+ repositories at a given time.
+ * resultsdbpy/resultsdbpy/view/static/js/timeline.js:
+ (xAxisFromScale): Add callbacks triggered when the mouse enters or leaves elements in the scale canvas.
+ (TimelineFromEndpoint.render): Add callbacks triggered when the mouse enters or leaves dot elements.
+ * resultsdbpy/resultsdbpy/view/static/js/tooltip.js: Added.
+ (isPointInElement): Given an element and a point, return true if that point is within the bounds of the element.
+ (_ToolTip):
+ (_ToolTip.prototype.set): Set the content and location of the ToolTip.
+ (_ToolTip.prototype.toString): Return the html needed to render the ToolTip.
+ (_ToolTip.prototype.unset): Clear and hide the ToolTip.
+ (_ToolTip.prototype.isIn): Check if a given point is contained within the ToolTip.
+ * resultsdbpy/resultsdbpy/view/static/library/js/components/TimelineComponents.js:
+ (Timeline.CanvasSeriesComponent): Convert onHover events to onEnter/onLeave events. Add toolTips points to both
+ dot and scale elements.
+ * resultsdbpy/resultsdbpy/view/templates/search.html: Add ToolTip.
+ * resultsdbpy/resultsdbpy/view/templates/suite_results.html: Ditto.
+
</ins><span class="cx"> 2019-08-20 Justin Michaud <justin_michaud@apple.com>
</span><span class="cx">
</span><span class="cx"> Fix InBounds speculation of typed array PutByVal and add extra step to integer range optimization to search for equality relationships on the RHS value
</span></span></pre></div>
<a id="trunkToolsresultsdbpyresultsdbpyviewstaticcsstooltipcss"></a>
<div class="addfile"><h4>Added: trunk/Tools/resultsdbpy/resultsdbpy/view/static/css/tooltip.css (0 => 248910)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/resultsdbpy/resultsdbpy/view/static/css/tooltip.css (rev 0)
+++ trunk/Tools/resultsdbpy/resultsdbpy/view/static/css/tooltip.css 2019-08-20 18:55:34 UTC (rev 248910)
</span><span class="lines">@@ -0,0 +1,58 @@
</span><ins>+/*
+ * Copyright (C) 2019 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.tooltip {
+ z-index: 50;
+ position: absolute;
+ opacity: 80%;
+ width: 0;
+ height: 0;
+ border-style: solid;
+ border-width: 15px;
+ -webkit-background-clip: padding-box;
+ background-clip: padding-box;
+}
+
+.tooltip.arrow-up {
+ border-color: transparent transparent #cccd transparent;
+}
+
+.tooltip.arrow-down {
+ border-color: #cccd transparent transparent transparent;
+}
+
+.tooltip-content {
+ z-index: 50;
+ position: absolute;
+ -webkit-backdrop-filter: blur(10px) brightness(88%);
+ backdrop-filter: blur(10px) brightness(88%);
+ color: var(--boldInverseColor);
+ border: 1px solid #ccc;
+ border-radius: 5px;
+ padding: 5px;
+ font-size: var(--smallSize);
+ list-style: none;
+ max-width: 600px;
+}
</ins></span></pre></div>
<a id="trunkToolsresultsdbpyresultsdbpyviewstaticjscommitjs"></a>
<div class="modfile"><h4>Modified: trunk/Tools/resultsdbpy/resultsdbpy/view/static/js/commit.js (248909 => 248910)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/resultsdbpy/resultsdbpy/view/static/js/commit.js 2019-08-20 18:46:36 UTC (rev 248909)
+++ trunk/Tools/resultsdbpy/resultsdbpy/view/static/js/commit.js 2019-08-20 18:55:34 UTC (rev 248910)
</span><span class="lines">@@ -162,6 +162,48 @@
</span><span class="cx"> }
</span><span class="cx"> return null;
</span><span class="cx"> }
</span><ins>+ commitsDuringUuid(uuid) {
+ let commits = [];
+ let begin = 0;
+ let end = this.commits.length - 1;
+ let index = this.commits.length - 1;
+ while (begin <= end) {
+ const mid = Math.ceil((begin + end) / 2);
+ const candidate = this.commits[mid];
+ if (candidate.uuid === uuid) {
+ commits.push(candidate);
+ index = mid - 1;
+ break;
+ }
+ if (candidate.uuid < uuid)
+ begin = mid + 1;
+ else
+ end = mid - 1;
+ }
+
+ let repositories = new Set();
+ if (commits.length)
+ repositories.add(commits[0].repository_id);
+
+ while (index >= 0) {
+ if (repositories.has(this.commits[index].repository_id)) {
+ --index;
+ continue;
+ }
+ if (this._repositories.length && !this._repositories.has(this.commits[index].repository_id)) {
+ --index;
+ continue;
+ }
+
+ commits.push(this.commits[index]);
+ repositories.add(this.commits[index].repository_id);
+ if (repositories.length == this._repositories.length)
+ break;
+
+ --index;
+ }
+ return commits.sort(function(a, b) {return a.repository_id.localeCompare(b.repository_id)});
+ }
</ins><span class="cx"> _loadSiblings(commit) {
</span><span class="cx"> const query = paramsToQuery({
</span><span class="cx"> branch: [commit.branch],
</span></span></pre></div>
<a id="trunkToolsresultsdbpyresultsdbpyviewstaticjstimelinejs"></a>
<div class="modfile"><h4>Modified: trunk/Tools/resultsdbpy/resultsdbpy/view/static/js/timeline.js (248909 => 248910)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/resultsdbpy/resultsdbpy/view/static/js/timeline.js 2019-08-20 18:46:36 UTC (rev 248909)
+++ trunk/Tools/resultsdbpy/resultsdbpy/view/static/js/timeline.js 2019-08-20 18:55:34 UTC (rev 248910)
</span><span class="lines">@@ -23,9 +23,10 @@
</span><span class="cx">
</span><span class="cx"> import {CommitBank} from '/assets/js/commit.js';
</span><span class="cx"> import {Configuration} from '/assets/js/configuration.js';
</span><del>-import {deepCompare, ErrorDisplay, paramsToQuery, queryToParams} from '/assets/js/common.js';
</del><ins>+import {deepCompare, ErrorDisplay, escapeHTML, paramsToQuery, queryToParams} from '/assets/js/common.js';
+import {ToolTip} from '/assets/js/tooltip.js';
+import {Timeline} from '/library/js/components/TimelineComponents.js';
</ins><span class="cx"> import {DOM, EventStream, REF, FP} from '/library/js/Ref.js';
</span><del>-import {Timeline} from '/library/js/components/TimelineComponents.js';
</del><span class="cx">
</span><span class="cx">
</span><span class="cx"> const DEFAULT_LIMIT = 100;
</span><span class="lines">@@ -219,6 +220,25 @@
</span><span class="cx"> const query = paramsToQuery(params);
</span><span class="cx"> window.open(`/commit?${query}`, '_blank');
</span><span class="cx"> },
</span><ins>+ onScaleEnter: (node, event, canvas) => {
+ const scrollDelta = document.documentElement.scrollTop || document.body.scrollTop;
+ ToolTip.set(
+ `<div class="content">
+ Time: ${new Date(node.label.timestamp * 1000).toLocaleString()}<br>
+ Repository: ${node.label.repository_id}<br>
+ Branch: ${node.label.branch}<br>
+ Committer: ${node.label.committer}
+ ${node.label.message ? `<br><div>${escapeHTML(node.label.message.split('\n')[0])}</div>` : ''}
+ </div>`,
+ node.tipPoints.map((point) => {
+ return {x: canvas.x + point.x, y: canvas.y + scrollDelta + point.y};
+ })
+ );
+ },
+ onScaleLeave: (event, canvas) => {
+ if (!ToolTip.isIn({x: event.x, y: event.y}))
+ ToolTip.unset();
+ },
</ins><span class="cx"> // Per the birthday paradox, 10% change of collision with 7.7 million commits with 12 character commits
</span><span class="cx"> getLabelFunc: (commit) => {return commit ? commit.id.substring(0,12) : '?';},
</span><span class="cx"> getScaleFunc: (commit) => commit.uuid,
</span><span class="lines">@@ -570,6 +590,48 @@
</span><span class="cx"> }
</span><span class="cx"> }
</span><span class="cx">
</span><ins>+ function onDotEnterFactory(configuration) {
+ return (data, event, canvas) => {
+ const scrollDelta = document.documentElement.scrollTop || document.body.scrollTop;
+ ToolTip.set(
+ `<div class="content">
+ ${data.start_time ? `<a href="/urls/build?${paramsToQuery(function () {
+ let buildParams = configuration.toParams();
+ buildParams['suite'] = [self.suite];
+ buildParams['uuid'] = [data.uuid];
+ buildParams['after_time'] = [data.start_time];
+ buildParams['before_time'] = [data.start_time];
+ if (branch)
+ buildParams['branch'] = branch;
+ return buildParams;
+ } ())}" target="_blank">Test run</a> @ ${new Date(data.start_time * 1000).toLocaleString()}<br>` : ''}
+ Commits: ${CommitBank.commitsDuringUuid(data.uuid).map((commit) => {
+ let params = {
+ branch: commit.branch ? [commit.branch] : branch,
+ uuid: [commit.uuid],
+ }
+ if (!params.branch)
+ delete params.branch;
+ const query = paramsToQuery(params);
+ return `<a href="/commit/info?${query}" target="_blank">${commit.id.substring(0,12)}</a>`;
+ }).join(', ')}
+ <br>
+
+ ${data.expected ? `Expected: ${data.expected}<br>` : ''}
+ ${data.actual ? `Actual: ${data.actual}<br>` : ''}
+ </div>`,
+ data.tipPoints.map((point) => {
+ return {x: canvas.x + point.x, y: canvas.y + scrollDelta + point.y};
+ })
+ );
+ }
+ }
+
+ function onDotLeave(event, canvas) {
+ if (!ToolTip.isIn({x: event.pageX, y: event.pageY}))
+ ToolTip.unset();
+ }
+
</ins><span class="cx"> function exporterFactory(data) {
</span><span class="cx"> return (updateFunction) => {
</span><span class="cx"> self.updates.push((scale) => {updateFunction(data, scale);});
</span><span class="lines">@@ -624,6 +686,8 @@
</span><span class="cx"> renderFactory: options.renderFactory,
</span><span class="cx"> exporter: options.exporter,
</span><span class="cx"> onDotClick: onDotClickFactory(config),
</span><ins>+ onDotEnter: onDotEnterFactory(config),
+ onDotLeave: onDotLeave,
</ins><span class="cx"> exporter: exporterFactory(resultsForConfig),
</span><span class="cx"> }));
</span><span class="cx">
</span><span class="lines">@@ -638,6 +702,8 @@
</span><span class="cx"> renderFactory: options.renderFactory,
</span><span class="cx"> exporter: options.exporter,
</span><span class="cx"> onDotClick: onDotClickFactory(sdkConfig),
</span><ins>+ onDotEnter: onDotEnterFactory(sdkConfig),
+ onDotLeave: onDotLeave,
</ins><span class="cx"> exporter: exporterFactory(resultsByKey[sdkConfig.toKey()]),
</span><span class="cx"> })));
</span><span class="cx"> });
</span><span class="lines">@@ -663,6 +729,8 @@
</span><span class="cx"> compareFunc: options.compareFunc,
</span><span class="cx"> renderFactory: options.renderFactory,
</span><span class="cx"> onDotClick: onDotClickFactory(configuration),
</span><ins>+ onDotEnter: onDotEnterFactory(configuration),
+ onDotLeave: onDotLeave,
</ins><span class="cx"> exporter: exporterFactory(allResults),
</span><span class="cx"> })),
</span><span class="cx"> ...collapsedTimelines
</span></span></pre></div>
<a id="trunkToolsresultsdbpyresultsdbpyviewstaticjstooltipjs"></a>
<div class="addfile"><h4>Added: trunk/Tools/resultsdbpy/resultsdbpy/view/static/js/tooltip.js (0 => 248910)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/resultsdbpy/resultsdbpy/view/static/js/tooltip.js (rev 0)
+++ trunk/Tools/resultsdbpy/resultsdbpy/view/static/js/tooltip.js 2019-08-20 18:55:34 UTC (rev 248910)
</span><span class="lines">@@ -0,0 +1,144 @@
</span><ins>+// Copyright (C) 2019 Apple Inc. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+// THE POSSIBILITY OF SUCH DAMAGE.
+
+import {DOM, REF} from '/library/js/Ref.js';
+
+function isPointInElement(element, point)
+{
+ if (!element || element.style.display == 'none')
+ return false;
+ const bounds = element.getBoundingClientRect();
+ return point.x >= bounds.left && point.x <= bounds.right && point.y >= bounds.top && point.y <= bounds.bottom;
+}
+
+class _ToolTip {
+ constructor() {
+ this.ref = null;
+ this.arrow = null;
+ }
+ toString() {
+ const self = this;
+ this.ref = REF.createRef({
+ state: {content: null, points: null},
+ onElementMount: (element) => {
+ element.addEventListener('mouseleave', (event) => {
+ if (!isPointInElement(self.arrow.element, event))
+ this.unset()
+ });
+ },
+ onStateUpdate: (element, stateDiff, state) => {
+ if (stateDiff.content) {
+ DOM.inject(element, stateDiff.content);
+ element.style.display = null;
+ }
+ if (!state.content && !element.style.display) {
+ element.style.display = 'none';
+ DOM.inject(element, '');
+ }
+ if (stateDiff.points) {
+ element.style.left = '0px';
+ element.style.top = '0px';
+
+ const upperPoint = stateDiff.points.length > 1 && stateDiff.points[0].y > stateDiff.points[1].y ? stateDiff.points[1] : stateDiff.points[0];
+ const lowerPoint = stateDiff.points.length > 1 && stateDiff.points[1].y > stateDiff.points[0].y ? stateDiff.points[1] : stateDiff.points[0];
+ const scrollDelta = document.documentElement.scrollTop || document.body.scrollTop;
+ const bounds = element.getBoundingClientRect();
+ const viewportWitdh = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
+ const viewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
+
+ // Make an effort to place the tooltip in the center of the viewport.
+ let direction = 'down';
+ let tipY = upperPoint.y - 8 - bounds.height;
+ let point = upperPoint;
+ if (tipY < scrollDelta || tipY + bounds.height + (lowerPoint.y - upperPoint.y) / 2 < scrollDelta + viewportHeight / 2) {
+ direction = 'up';
+ tipY = lowerPoint.y + 16;
+ point = lowerPoint;
+ }
+ element.style.top = `${tipY}px`;
+
+ let tipX = point.x - bounds.width / 2;
+ if (tipX + bounds.width > viewportWitdh)
+ tipX = viewportWitdh - bounds.width;
+ if (tipX < 0)
+ tipX = 0;
+ element.style.left = `${tipX}px`;
+
+ self.arrow.setState({direction: direction, location: point});
+ }
+ },
+ });
+ this.arrow = REF.createRef({
+ state: {direction: null, location: null},
+ onElementMount: (element) => {
+ element.addEventListener('mouseleave', (event) => {
+ if (!isPointInElement(self.ref.element, event))
+ this.unset()
+ });
+ },
+ onStateUpdate: (element, stateDiff, state) => {
+ if (!state.direction || !state.location) {
+ element.style.display = 'none';
+ return;
+ }
+
+ element.classList = [`tooltip arrow-${state.direction}`];
+ element.style.left = `${state.location.x - 15}px`;
+ if (state.direction == 'down')
+ element.style.top = `${state.location.y - 8}px`;
+ else
+ element.style.top = `${state.location.y - 13}px`;
+ element.style.display = null;
+ },
+
+ });
+ return `<div class="tooltip arrow-up" ref="${this.arrow}"></div>
+ <div class="tooltip-content" ref="${this.ref}">
+ </div>`;
+ }
+ set(content, points) {
+ if (!this.ref) {
+ console.error('Cannot set ToolTip content, no tooltip on the page');
+ return;
+ }
+ if (!points || points.length == 0) {
+ console.error('Tool tips require a location');
+ return;
+ }
+
+ this.ref.setState({content: content, points: points});
+ }
+ unset() {
+ if (this.ref)
+ this.ref.setState({content: null, points: null});
+ if (this.arrow)
+ this.arrow.setState({direction: null, points: null});
+ }
+ isIn(point) {
+ return isPointInElement(this.ref.element, point) || isPointInElement(this.arrow.element, point);
+ }
+}
+
+const ToolTip = new _ToolTip();
+
+export {ToolTip};
</ins></span></pre></div>
<a id="trunkToolsresultsdbpyresultsdbpyviewstaticlibraryjscomponentsTimelineComponentsjs"></a>
<div class="modfile"><h4>Modified: trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/components/TimelineComponents.js (248909 => 248910)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/components/TimelineComponents.js 2019-08-20 18:46:36 UTC (rev 248909)
+++ trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/components/TimelineComponents.js 2019-08-20 18:55:34 UTC (rev 248910)
</span><span class="lines">@@ -223,7 +223,8 @@
</span><span class="cx"> const getScale = typeof option.getScaleFunc === "function" ? option.getScaleFunc : (a) => a;
</span><span class="cx"> const comp = typeof option.compareFunc === "function" ? option.compareFunc : (a, b) => a - b;
</span><span class="cx"> const onDotClick = typeof option.onDotClick === "function" ? option.onDotClick : null;
</span><del>- const onDotHover = typeof option.onDotHover === "function" ? option.onDotHover : null;
</del><ins>+ const onDotEnter = typeof option.onDotEnter === "function" ? option.onDotEnter : null;
+ const onDotLeave = typeof option.onDotLeave === "function" ? option.onDotLeave : null;
</ins><span class="cx"> const tagHeight = defaultFontSize;
</span><span class="cx"> const height = option.height ? option.height : 2 * radius + tagHeight;
</span><span class="cx"> const colorBatchRender = new ColorBatchRender();
</span><span class="lines">@@ -360,8 +361,26 @@
</span><span class="cx"> };
</span><span class="cx">
</span><span class="cx"> return ListProviderReceiver((updateContainerWidth, onContainerScroll, onResize) => {
</span><ins>+ const mouseMove = (e) => {
+ let dots = getMouseEventTirggerDots(e, canvasRef.state.scrollLeft, canvasRef.element);
+ if (dots.length) {
+ if (onDotEnter) {
+ dots[0].tipPoints = [
+ {x: dots[0]._dotCenter.x, y: dots[0]._dotCenter.y - 3 * radius / 2},
+ {x: dots[0]._dotCenter.x, y: dots[0]._dotCenter.y + radius / 2},
+ ];
+ onDotEnter(dots[0], e, canvasRef.element.getBoundingClientRect());
+ }
+ canvasRef.element.style.cursor = "pointer";
+ } else {
+ if (onDotLeave)
+ onDotLeave(e, canvasRef.element.getBoundingClientRect());
+ canvasRef.element.style.cursor = "default";
+ }
+ }
</ins><span class="cx"> const onScrollAction = (e) => {
</span><span class="cx"> canvasRef.setState({scrollLeft: e.target.scrollLeft / getDevicePixelRatio()});
</span><ins>+ mouseMove(e);
</ins><span class="cx"> };
</span><span class="cx"> const onResizeAction = (width) => {
</span><span class="cx"> canvasRef.setState({width: width});
</span><span class="lines">@@ -386,17 +405,10 @@
</span><span class="cx"> });
</span><span class="cx"> }
</span><span class="cx">
</span><del>- if (onDotClick || onDotHover) {
- element.addEventListener('mousemove', (e) => {
- let dots = getMouseEventTirggerDots(e, canvasRef.state.scrollLeft, element);
- if (dots.length) {
- if (onDotHover)
- onDotHover(dots[0], e);
- element.style.cursor = "pointer";
- } else
- element.style.cursor = "default";
- });
- }
</del><ins>+ if (onDotClick || onDotEnter || onDotLeave)
+ element.addEventListener('mousemove', mouseMove);
+ if (onDotLeave)
+ element.addEventListener('mouseleave', (e) => onDotLeave(e, element.getBoundingClientRect()));
</ins><span class="cx">
</span><span class="cx"> createInsertionObservers(element, (entries) => {
</span><span class="cx"> canvasRef.setState({onScreen: entries[0].isIntersecting});
</span><span class="lines">@@ -533,7 +545,8 @@
</span><span class="cx"> const getScaleKey = typeof option.getScaleFunc === "function" ? option.getScaleFunc : (a) => a;
</span><span class="cx"> const comp = typeof option.compareFunc === "function" ? option.compareFunc : (a, b) => a - b;
</span><span class="cx"> const onScaleClick = typeof option.onScaleClick === "function" ? option.onScaleClick : null;
</span><del>- const onScaleHover = typeof option.onScaleHover === "function" ? option.onScaleHover : null;
</del><ins>+ const onScaleEnter = typeof option.onScaleEnter === "function" ? option.onScaleEnter : null;
+ const onScaleLeave = typeof option.onScaleLeave === "function" ? option.onScaleLeave : null;
</ins><span class="cx"> const sortData = option.sortData === true ? option.sortData : false;
</span><span class="cx"> const getLabel = typeof option.getLabelFunc === "function" ? option.getLabelFunc : (a) => a;
</span><span class="cx"> const isTop = typeof option.isTop === "boolean" ? option.isTop : false;
</span><span class="lines">@@ -626,8 +639,9 @@
</span><span class="cx"> const getMouseEventTirggerScales = (e, scrollLeft, element) => {
</span><span class="cx"> const {x, y} = getMousePosInCanvas(e, element);
</span><span class="cx"> return onScreenScales.filter(scale => {
</span><del>- const width = scale.label.toString().length * fontSizeNumber / 2;
- const height = scale.label.toString().length * fontSizeNumber / 2 * sqrt3;
</del><ins>+ const labelLength = getLabel(scale.label).length;
+ const width = labelLength * fontSizeNumber / 2;
+ const height = labelLength * fontSizeNumber / 2 * sqrt3;
</ins><span class="cx"> const point1 = {
</span><span class="cx"> x: scale._tagTop.x - scrollLeft - (isTop ? fontSizeNumber / 2 * sqrt3 : 0),
</span><span class="cx"> y: scale._tagTop.y + (fontSizeNumber / 2 + scaleTagLineHeight) * (isTop ? -1 : 1),
</span><span class="lines">@@ -725,8 +739,30 @@
</span><span class="cx">
</span><span class="cx"> return {
</span><span class="cx"> series: ListProviderReceiver((updateContainerWidth, onContainerScroll, onResize) => {
</span><ins>+ const mouseMove = (e) => {
+ let scales = getMouseEventTirggerScales(e, canvasRef.state.scrollLeft, canvasRef.element);
+ if (scales.length) {
+ if (onScaleEnter) {
+ const labelLength = getLabel(scales[0].label).length;
+ scales[0].tipPoints = [{
+ x: scales[0]._tagTop.x - canvasRef.state.scrollLeft,
+ y: scales[0]._tagTop.y + scaleTagLineHeight * (isTop ? -1 : 0),
+ }, {
+ x: scales[0]._tagTop.x - canvasRef.state.scrollLeft + labelLength * fontSizeNumber / 3 - scaleTagLineHeight * (isTop ? 1 : .25),
+ y: scales[0]._tagTop.y + (labelLength * fontSizeNumber / 2 * sqrt3) * (isTop ? -1 : 1) + scaleTagLineHeight * (isTop ? 1 : 0),
+ }];
+ onScaleEnter(scales[0], e, canvasRef.element.getBoundingClientRect());
+ }
+ canvasRef.element.style.cursor = "pointer";
+ } else {
+ if (onScaleEnter)
+ onScaleLeave(e, canvasRef.element.getBoundingClientRect());
+ canvasRef.element.style.cursor = "default";
+ }
+ }
</ins><span class="cx"> const onScrollAction = (e) => {
</span><span class="cx"> canvasRef.setState({scrollLeft: e.target.scrollLeft / getDevicePixelRatio()});
</span><ins>+ mouseMove(e);
</ins><span class="cx"> };
</span><span class="cx"> const onResizeAction = (width) => {
</span><span class="cx"> canvasRef.setState({width: width});
</span><span class="lines">@@ -750,18 +786,10 @@
</span><span class="cx"> });
</span><span class="cx"> }
</span><span class="cx">
</span><del>- if (onScaleClick || onScaleHover) {
- element.addEventListener('mousemove', (e) => {
- let scales = getMouseEventTirggerScales(e, canvasRef.state.scrollLeft, element);
- if (scales.length) {
- if (onScaleHover)
- onScaleHover(scales[0], e);
- element.style.cursor = "pointer";
- } else {
- element.style.cursor = "default";
- }
- });
- }
</del><ins>+ if (onScaleClick || onScaleEnter || onScaleLeave)
+ element.addEventListener('mousemove', mouseMove);
+ if (onScaleLeave)
+ element.addEventListener('mouseleave', (e) => onScaleLeave(e, element.getBoundingClientRect()));
</ins><span class="cx"> },
</span><span class="cx"> onElementUnmount: (element) => {
</span><span class="cx"> onContainerScroll.stopAction(onScrollAction);
</span></span></pre></div>
<a id="trunkToolsresultsdbpyresultsdbpyviewtemplatessearchhtml"></a>
<div class="modfile"><h4>Modified: trunk/Tools/resultsdbpy/resultsdbpy/view/templates/search.html (248909 => 248910)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/resultsdbpy/resultsdbpy/view/templates/search.html 2019-08-20 18:46:36 UTC (rev 248909)
+++ trunk/Tools/resultsdbpy/resultsdbpy/view/templates/search.html 2019-08-20 18:55:34 UTC (rev 248910)
</span><span class="lines">@@ -28,6 +28,7 @@
</span><span class="cx"> <link rel="stylesheet" type="text/css" href="assets/css/drawer.css">
</span><span class="cx"> <link rel="stylesheet" type="text/css" href="assets/css/search.css">
</span><span class="cx"> <link rel="stylesheet" type="text/css" href="assets/css/timeline.css">
</span><ins>+<link rel="stylesheet" type="text/css" href="assets/css/tooltip.css">
</ins><span class="cx">
</span><span class="cx"> <script type="module">
</span><span class="cx"> import {CommitBank} from '/assets/js/commit.js';
</span><span class="lines">@@ -34,9 +35,10 @@
</span><span class="cx"> import {deepCompare, ErrorDisplay, queryToParams, paramsToQuery} from '/assets/js/common.js';
</span><span class="cx"> import {Configuration} from '/assets/js/configuration.js';
</span><span class="cx"> import {Drawer, BranchSelector, ConfigurationSelectors, LimitSlider} from '/assets/js/drawer.js';
</span><del>-import {DOM, REF} from '/library/js/Ref.js';
</del><span class="cx"> import {SearchBar} from '/assets/js/search.js';
</span><span class="cx"> import {Legend, TimelineFromEndpoint} from '/assets/js/timeline.js';
</span><ins>+import {ToolTip} from '/assets/js/tooltip.js';
+import {DOM, REF} from '/library/js/Ref.js';
</ins><span class="cx">
</span><span class="cx"> const DEFAULT_LIMIT = 100;
</span><span class="cx"> const SUITES = JSON.parse('{{ suites|safe }}');
</span><span class="lines">@@ -215,7 +217,8 @@
</span><span class="cx">
</span><span class="cx"> DOM.inject(
</span><span class="cx"> document.getElementById('app'),
</span><del>- `${Drawer([
</del><ins>+ `${ToolTip}
+ ${Drawer([
</ins><span class="cx"> LimitSlider(() => {view.reload()}),
</span><span class="cx"> BranchSelector(() => {
</span><span class="cx"> CommitBank.reload();
</span></span></pre></div>
<a id="trunkToolsresultsdbpyresultsdbpyviewtemplatessuite_resultshtml"></a>
<div class="modfile"><h4>Modified: trunk/Tools/resultsdbpy/resultsdbpy/view/templates/suite_results.html (248909 => 248910)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/resultsdbpy/resultsdbpy/view/templates/suite_results.html 2019-08-20 18:46:36 UTC (rev 248909)
+++ trunk/Tools/resultsdbpy/resultsdbpy/view/templates/suite_results.html 2019-08-20 18:55:34 UTC (rev 248910)
</span><span class="lines">@@ -28,6 +28,7 @@
</span><span class="cx"> <link rel="stylesheet" type="text/css" href="assets/css/drawer.css">
</span><span class="cx"> <link rel="stylesheet" type="text/css" href="assets/css/search.css">
</span><span class="cx"> <link rel="stylesheet" type="text/css" href="assets/css/timeline.css">
</span><ins>+<link rel="stylesheet" type="text/css" href="assets/css/tooltip.css">
</ins><span class="cx">
</span><span class="cx"> <script type="module">
</span><span class="cx"> import {CommitBank} from '/assets/js/commit.js';
</span><span class="lines">@@ -34,8 +35,9 @@
</span><span class="cx"> import {deepCompare, ErrorDisplay, queryToParams, paramsToQuery} from '/assets/js/common.js';
</span><span class="cx"> import {Configuration} from '/assets/js/configuration.js';
</span><span class="cx"> import {Drawer, BranchSelector, ConfigurationSelectors, LimitSlider} from '/assets/js/drawer.js';
</span><ins>+import {Legend, TimelineFromEndpoint} from '/assets/js/timeline.js';
+import {ToolTip} from '/assets/js/tooltip.js';
</ins><span class="cx"> import {DOM, REF} from '/library/js/Ref.js';
</span><del>-import {Legend, TimelineFromEndpoint} from '/assets/js/timeline.js';
</del><span class="cx">
</span><span class="cx"> const DEFAULT_LIMIT = 100;
</span><span class="cx"> const SUITES = JSON.parse('{{ suites|safe }}');
</span><span class="lines">@@ -159,7 +161,8 @@
</span><span class="cx">
</span><span class="cx"> let view = new MainView();
</span><span class="cx">
</span><del>-DOM.inject(document.getElementById('app'), `${Drawer([
</del><ins>+DOM.inject(document.getElementById('app'), `${ToolTip}
+${Drawer([
</ins><span class="cx"> LimitSlider(() => {view.reload()}),
</span><span class="cx"> BranchSelector(() => {
</span><span class="cx"> CommitBank.reload();
</span></span></pre>
</div>
</div>
</body>
</html>