blob: c625f246a3aa4ae99a6fea345bd3482a526afdd8 [file] [log] [blame]
<!DOCTYPE html>
<!--
Copyright 2015 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<link rel="import" href="/tracing/base/unit.html">
<link rel="import" href="/tracing/ui/base/deep_utils.html">
<link rel="import" href="/tracing/value/histogram.html">
<link rel="import" href="/tracing/value/ui/scalar_context_controller.html">
<script>
'use strict';
tr.exportTo('tr.v.ui', function() {
var emojiPrefix = String.fromCharCode(55357);
var Emoji = {
GRINNING_FACE: emojiPrefix + String.fromCharCode(56835),
NEUTRAL_FACE: emojiPrefix + String.fromCharCode(56848),
CONFOUNDED_FACE: emojiPrefix + String.fromCharCode(56854)
};
/**
* @param {undefined|tr.v.ScalarNumeric|tr.v.Histogram} value
* @param {Object=} opt_config
* @param {!tr.b.Range=} opt_config.customContextRange
* @param {boolean=} opt_config.rightAlign
* @param {!tr.b.Unit=} opt_config.unit
* @param {tr.v.Significance=} opt_config.significance
* @param {string=} opt_config.contextGroup
* @return {(string|!HTMLElement)}
*/
function createScalarSpan(value, opt_config) {
if (value === undefined)
return '';
var config = opt_config || {};
var ownerDocument = config.ownerDocument || document;
var span = ownerDocument.createElement('tr-v-ui-scalar-span');
var numericValue;
if (value instanceof tr.v.ScalarNumeric) {
span.value = value;
numericValue = value.value;
} else if (value instanceof tr.v.Histogram) {
numericValue = value.average;
if (numericValue === undefined)
return '';
span.setValueAndUnit(numericValue, value.unit);
} else {
var unit = config.unit;
if (unit === undefined) {
throw new Error(
'Unit must be provided in config when value is a number');
}
span.setValueAndUnit(value, unit);
numericValue = value;
}
if (config.context)
span.context = config.context;
if (config.customContextRange)
span.customContextRange = config.customContextRange;
if (config.rightAlign)
span.rightAlign = true;
if (config.significance !== undefined)
span.significance = config.significance;
if (config.contextGroup !== undefined)
span.contextGroup = config.contextGroup;
return span;
}
return {
Emoji: Emoji,
createScalarSpan: createScalarSpan
};
});
</script>
<dom-module id='tr-v-ui-scalar-span'>
<template>
<style>
:host {
display: block;
position: relative;
/* Limit the sparkline's negative z-index to the span only. */
isolation: isolate;
}
#content.right-align {
text-align: right;
position: relative;
display: block;
}
#significance.better, #content.better {
color: green;
}
#significance.worse, #content.worse {
color: red;
}
#sparkline {
width: 0%;
position: absolute;
bottom: 0;
display: none;
height: 100%;
background-color: hsla(216, 100%, 94.5%, .75);
border-color: hsl(216, 100%, 89%);
box-sizing: border-box;
z-index: -1;
}
#sparkline.positive {
border-right-style: solid;
/* The border width must be kept in sync with buildSparklineStyle_(). */
border-right-width: 1px;
}
#sparkline:not(.positive) {
border-left-style: solid;
/* The border width must be kept in sync with buildSparklineStyle_(). */
border-left-width: 1px;
}
#sparkline.better {
background-color: hsla(115, 100%, 93%, .75);
border-color: hsl(118, 60%, 80%);
}
#sparkline.worse {
background-color: hsla(0, 100%, 88%, .75);
border-color: hsl(0, 100%, 80%);
}
#warning {
margin-left: 4px;
font-size: 66%;
}
#significance {
font-family: "Noto Color Emoji", "Apple Color Emoji", "Segoe UI Emoji", Times, Symbola, Aegyptus, Code2000, Code2001, Code2002, Musica, serif, LastResort;
font-size: 13pt;
}
</style>
<span id="sparkline"></span>
<span id="content"></span>
<span id="significance"></span>
<span id="warning" style="display:none">&#9888;</span>
</template>
</dom-module>
<script>
'use strict';
Polymer({
is: 'tr-v-ui-scalar-span',
properties: {
/**
* String identifier for grouping scalar spans with common context (e.g.
* all scalar spans in a single table column would typically share a common
* context and, thus, have the same context group identifier). If falsy,
* the scalar span will NOT be associated with any context.
*/
contextGroup: {
type: String,
reflectToAttribute: true,
observer: 'contextGroupChanged_'
}
},
created: function() {
this.value_ = undefined;
this.unit_ = undefined;
// TODO(petrcermak): Merge this into the context controller.
this.context_ = undefined;
this.warning_ = undefined;
this.significance_ = tr.v.Significance.DONT_CARE;
// To avoid unnecessary DOM traversal, search for the context controller
// only when necessary (when the span is attached and has a context group).
this.shouldSearchForContextController_ = false;
this.lazyContextController_ = undefined;
this.onContextUpdated_ = this.onContextUpdated_.bind(this);
// The span can specify a custom context range, which will override the
// values from the context controller.
this.customContextRange_ = undefined;
},
get significance() {
return this.significance_;
},
set significance(s) {
this.significance_ = s;
this.updateContents_();
},
set contentTextDecoration(deco) {
this.$.content.style.textDecoration = deco;
},
get value() {
return this.value_;
},
set value(value) {
if (value instanceof tr.v.ScalarNumeric) {
this.value_ = value.value;
this.unit_ = value.unit;
} else {
this.value_ = value;
}
this.updateContents_();
if (this.hasContext_(this.contextGroup))
this.contextController_.onScalarSpanUpdated(this.contextGroup, this);
else
this.updateSparkline_();
},
get contextController_() {
if (this.shouldSearchForContextController_) {
this.lazyContextController_ =
tr.v.ui.getScalarContextControllerForElement(this);
this.shouldSearchForContextController_ = false;
}
return this.lazyContextController_;
},
hasContext_: function(contextGroup) {
// The ordering here is important. It ensures that we avoid a DOM traversal
// when the span doesn't have a context group.
return !!(contextGroup && this.contextController_);
},
contextGroupChanged_: function(newContextGroup, oldContextGroup) {
this.detachFromContextControllerIfPossible_(oldContextGroup);
if (!this.attachToContextControllerIfPossible_(newContextGroup)) {
// If the span failed to attach to a controller, it won't receive a
// context-updated event, so we trigger it manually.
this.onContextUpdated_();
}
},
attachToContextControllerIfPossible_: function(contextGroup) {
if (!this.hasContext_(contextGroup))
return false;
this.contextController_.addEventListener(
'context-updated', this.onContextUpdated_);
this.contextController_.onScalarSpanAdded(contextGroup, this);
return true;
},
detachFromContextControllerIfPossible_: function(contextGroup) {
if (!this.hasContext_(contextGroup))
return;
this.contextController_.removeEventListener(
'context-updated', this.onContextUpdated_);
this.contextController_.onScalarSpanRemoved(contextGroup, this);
},
attached: function() {
tr.b.Unit.addEventListener(
'display-mode-changed', this.updateContents_.bind(this));
this.shouldSearchForContextController_ = true;
this.attachToContextControllerIfPossible_(this.contextGroup);
},
detached: function() {
tr.b.Unit.removeEventListener(
'display-mode-changed', this.updateContents_.bind(this));
this.detachFromContextControllerIfPossible_(this.contextGroup);
this.shouldSearchForContextController_ = false;
this.lazyContextController_ = undefined;
},
onContextUpdated_: function() {
this.updateSparkline_();
},
get context() {
return this.context_;
},
set context(context) {
this.context_ = context;
this.updateContents_();
},
get unit() {
return this.unit_;
},
set unit(unit) {
this.unit_ = unit;
this.updateContents_();
this.updateSparkline_();
},
setValueAndUnit: function(value, unit) {
this.value_ = value;
this.unit_ = unit;
this.updateContents_();
},
get customContextRange() {
return this.customContextRange_;
},
set customContextRange(customContextRange) {
this.customContextRange_ = customContextRange;
this.updateSparkline_();
},
get rightAlign() {
return Polymer.dom(this.$.content).classList.contains('right-align');
},
set rightAlign(rightAlign) {
if (rightAlign)
Polymer.dom(this.$.content).classList.add('right-align');
else
Polymer.dom(this.$.content).classList.remove('right-align');
},
updateSparkline_: function() {
Polymer.dom(this.$.sparkline).classList.remove('positive');
Polymer.dom(this.$.sparkline).classList.remove('better');
Polymer.dom(this.$.sparkline).classList.remove('worse');
Polymer.dom(this.$.sparkline).classList.remove('same');
this.$.sparkline.style.display = 'none';
this.$.sparkline.style.left = '0';
this.$.sparkline.style.width = '0';
// Custom context range takes precedence over controller context range.
var range = this.customContextRange_;
if (!range && this.hasContext_(this.contextGroup)) {
var context = this.contextController_.getContext(this.contextGroup);
if (context)
range = context.range;
}
if (!range || range.isEmpty)
return;
var leftPoint = Math.min(range.min, 0);
var rightPoint = Math.max(range.max, 0);
var pointDistance = rightPoint - leftPoint;
if (pointDistance === 0) {
// This can happen, for example, when all spans within the context have
// zero values (so |range| is [0, 0]).
return;
}
// Draw the sparkline.
this.$.sparkline.style.display = 'block';
var left, width;
if (this.value > 0) {
width = Math.min(this.value, rightPoint);
left = -leftPoint;
Polymer.dom(this.$.sparkline).classList.add('positive');
} else if (this.value <= 0) {
width = -Math.max(this.value, leftPoint);
left = (-leftPoint) - width;
}
this.$.sparkline.style.left = this.buildSparklineStyle_(
left / pointDistance, false);
this.$.sparkline.style.width = this.buildSparklineStyle_(
width / pointDistance, true);
// Set the sparkline color (if applicable).
var changeClass = this.changeClassName_;
if (changeClass)
Polymer.dom(this.$.sparkline).classList.add(changeClass);
},
buildSparklineStyle_: function(ratio, isWidth) {
// To avoid visual glitches around the zero value bar, we subtract 1 pixel
// from the width of the element and multiply the remainder (100% - 1px) by
// |ratio|. The extra pixel is used for the sparkline border. This allows
// us to align zero sparklines with both positive and negative values:
//
// ::::::::::| +10 MiB
// :::::| +5 MiB
// | 0 MiB
// |::::: -5 MiB
// |:::::::::: -10 MiB
//
var position = 'calc(' + ratio + ' * (100% - 1px)';
if (isWidth)
position += ' + 1px'; // Extra pixel for sparkline border.
position += ')';
return position;
},
updateContents_: function() {
Polymer.dom(this.$.significance).textContent = '';
Polymer.dom(this.$.content).textContent = '';
Polymer.dom(this.$.content).classList.remove('better');
Polymer.dom(this.$.content).classList.remove('worse');
Polymer.dom(this.$.content).classList.remove('same');
Polymer.dom(this.$.significance).classList.remove('better');
Polymer.dom(this.$.significance).classList.remove('worse');
Polymer.dom(this.$.significance).classList.remove('same');
if (this.unit_ === undefined)
return;
this.$.content.title = '';
Polymer.dom(this.$.content).textContent =
this.unit_.format(this.value, this.context);
this.updateDelta_();
},
updateDelta_: function() {
if (!this.unit_.isDelta) {
this.$.significance.style.display = 'none';
return;
}
this.$.significance.style.display = '';
var changeClass = this.changeClassName_;
if (!changeClass)
return; // Not a delta or we don't care.
var emoji, title;
switch (changeClass) {
case 'better':
emoji = tr.v.ui.Emoji.GRINNING_FACE;
title = 'improvement';
break;
case 'worse':
emoji = tr.v.ui.Emoji.CONFOUNDED_FACE;
title = 'regression';
break;
case 'same':
emoji = tr.v.ui.Emoji.NEUTRAL_FACE;
title = 'no change';
break;
default:
throw new Error('Unknown change class: ' + changeClass);
}
// Set the content class separately from the significance class so that
// NEUTRAL_FACE is always a neutral color.
Polymer.dom(this.$.content).classList.add(changeClass);
switch (this.significance) {
case tr.v.Significance.DONT_CARE:
emoji = '';
changeClass = 'same';
break;
case tr.v.Significance.INSIGNIFICANT:
if (changeClass !== 'same')
title = 'insignificant ' + title;
emoji = tr.v.ui.Emoji.NEUTRAL_FACE;
changeClass = 'same';
break;
case tr.v.Significance.SIGNIFICANT:
if (changeClass === 'same')
throw new Error('How can no change be significant?');
title = 'significant ' + title;
break;
}
Polymer.dom(this.$.significance).textContent = emoji;
this.$.significance.title = title;
this.$.content.title = title;
Polymer.dom(this.$.significance).classList.add(changeClass);
},
get changeClassName_() {
if (!this.unit_ || !this.unit_.isDelta)
return undefined;
switch (this.unit_.improvementDirection) {
case tr.b.ImprovementDirection.DONT_CARE:
return undefined;
case tr.b.ImprovementDirection.BIGGER_IS_BETTER:
if (this.value === 0)
return 'same';
return this.value > 0 ? 'better' : 'worse';
case tr.b.ImprovementDirection.SMALLER_IS_BETTER:
if (this.value === 0)
return 'same';
return this.value < 0 ? 'better' : 'worse';
default:
throw new Error('Unknown improvement direction: ' +
this.unit_.improvementDirection);
}
},
get warning() {
return this.warning_;
},
set warning(warning) {
this.warning_ = warning;
var warningEl = this.$.warning;
if (this.warning_) {
warningEl.title = warning;
warningEl.style.display = '';
} else {
warningEl.title = '';
warningEl.style.display = 'none';
}
},
// tr-v-ui-time-stamp-span property
get timestamp() {
return this.value;
},
set timestamp(timestamp) {
if (timestamp instanceof tr.b.u.TimeStamp) {
this.value = timestamp;
return;
}
this.setValueAndUnit(timestamp, tr.b.u.Units.timeStampInMs);
},
// tr-v-ui-time-duration-span property
get duration() {
return this.value;
},
set duration(duration) {
if (duration instanceof tr.b.u.TimeDuration) {
this.value = duration;
return;
}
this.setValueAndUnit(duration, tr.b.u.Units.timeDurationInMs);
}
});
</script>