feature #33535 [WebProfilerBundle] Assign automatic colors to custom Stopwatch categories (javiereguiluz)
This PR was merged into the 4.4 branch.
Discussion
----------
[WebProfilerBundle] Assign automatic colors to custom Stopwatch categories
| Q | A
| ------------- | ---
| Branch? | 4.4
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | #33514
| License | MIT
| Doc PR | not needed
### Before
![image](https://user-images.githubusercontent.com/73419/64624345-d2907000-d3ea-11e9-9320-5b316768273d.png)
### After
![image](https://user-images.githubusercontent.com/73419/64624358-d6bc8d80-d3ea-11e9-875d-99396782d95a.png)
- - - - -
I'd appreciate reviews from JavaScript experts. Thanks!
Commits
-------
329a74fe47
[WebProfilerBundle] Assign automatic colors to custom Stopwatch categories
This commit is contained in:
commit
bed6511c26
@ -1,15 +1,3 @@
|
||||
/* Variables */
|
||||
|
||||
.sf-profiler-timeline {
|
||||
--color-default: #777;
|
||||
--color-section: #999;
|
||||
--color-event-listener: #00B8F5;
|
||||
--color-template: #66CC00;
|
||||
--color-doctrine: #FF6633;
|
||||
--color-messenger-middleware: #BDB81E;
|
||||
--color-controller-argument-value-resolver: #8c5de6;
|
||||
}
|
||||
|
||||
/* Legend */
|
||||
|
||||
.sf-profiler-timeline .legends .timeline-category {
|
||||
@ -31,14 +19,6 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.sf-profiler-timeline .legends .{{ classnames.default|raw }} { border-color: var(--color-default); }
|
||||
.sf-profiler-timeline .legends .{{ classnames.section|raw }} { border-color: var(--color-section); }
|
||||
.sf-profiler-timeline .legends .{{ classnames.event_listener|raw }} { border-color: var(--color-event-listener); }
|
||||
.sf-profiler-timeline .legends .{{ classnames.template|raw }} { border-color: var(--color-template); }
|
||||
.sf-profiler-timeline .legends .{{ classnames.doctrine|raw }} { border-color: var(--color-doctrine); }
|
||||
.sf-profiler-timeline .legends .{{ classnames['messenger.middleware']|raw }} { border-color: var(--color-messenger-middleware); }
|
||||
.sf-profiler-timeline .legends .{{ classnames['controller.argument_value_resolver']|raw }} { border-color: var(--color-controller-argument-value-resolver); }
|
||||
|
||||
.timeline-graph {
|
||||
margin: 1em 0;
|
||||
width: 100%;
|
||||
@ -82,24 +62,3 @@
|
||||
.timeline-graph .timeline-period {
|
||||
stroke-width: 0;
|
||||
}
|
||||
.timeline-graph .{{ classnames.default|raw }} .timeline-period {
|
||||
fill: var(--color-default);
|
||||
}
|
||||
.timeline-graph .{{ classnames.section|raw }} .timeline-period {
|
||||
fill: var(--color-section);
|
||||
}
|
||||
.timeline-graph .{{ classnames.event_listener|raw }} .timeline-period {
|
||||
fill: var(--color-event-listener);
|
||||
}
|
||||
.timeline-graph .{{ classnames.template|raw }} .timeline-period {
|
||||
fill: var(--color-template);
|
||||
}
|
||||
.timeline-graph .{{ classnames.doctrine|raw }} .timeline-period {
|
||||
fill: var(--color-doctrine);
|
||||
}
|
||||
.timeline-graph .{{ classnames['messenger.middleware']|raw }} .timeline-period {
|
||||
fill: var(--color-messenger-middleware);
|
||||
}
|
||||
.timeline-graph .{{ classnames['controller.argument_value_resolver']|raw }} .timeline-period {
|
||||
fill: var(--color-controller-argument-value-resolver);
|
||||
}
|
||||
|
@ -2,16 +2,6 @@
|
||||
|
||||
{% import _self as helper %}
|
||||
|
||||
{% set classnames = {
|
||||
'default': 'timeline-category-default',
|
||||
'section': 'timeline-category-section',
|
||||
'event_listener': 'timeline-category-event-listener',
|
||||
'template': 'timeline-category-template',
|
||||
'doctrine': 'timeline-category-doctrine',
|
||||
'messenger.middleware': 'timeline-category-messenger-middleware',
|
||||
'controller.argument_value_resolver': 'timeline-category-controller-argument-value-resolver',
|
||||
} %}
|
||||
|
||||
{% block toolbar %}
|
||||
{% set has_time_events = collector.events|length > 0 %}
|
||||
{% set total_time = has_time_events ? '%.0f'|format(collector.duration) : 'n/a' %}
|
||||
@ -128,7 +118,7 @@
|
||||
</h3>
|
||||
{% endif %}
|
||||
|
||||
{{ helper.display_timeline(token, classnames, collector.events, collector.events.__section__.origin) }}
|
||||
{{ helper.display_timeline(token, collector.events, collector.events.__section__.origin) }}
|
||||
|
||||
{% if profile.children|length %}
|
||||
<p class="help">Note: sections with a striped background correspond to sub-requests.</p>
|
||||
@ -142,7 +132,7 @@
|
||||
<small>{{ events.__section__.duration }} ms</small>
|
||||
</h4>
|
||||
|
||||
{{ helper.display_timeline(child.token, classnames, events, collector.events.__section__.origin) }}
|
||||
{{ helper.display_timeline(child.token, events, collector.events.__section__.origin) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@ -154,7 +144,7 @@
|
||||
</defs>
|
||||
</svg>
|
||||
<style type="text/css">
|
||||
{% include '@WebProfiler/Collector/time.css.twig' with classnames %}
|
||||
{% include '@WebProfiler/Collector/time.css.twig' %}
|
||||
</style>
|
||||
<script>
|
||||
{% include '@WebProfiler/Collector/time.js' %}
|
||||
@ -202,16 +192,19 @@
|
||||
{% endautoescape %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro display_timeline(token, classnames, events, origin) %}
|
||||
{% macro display_timeline(token, events, origin) %}
|
||||
{% import _self as helper %}
|
||||
<div class="sf-profiler-timeline">
|
||||
<div id="legend-{{ token }}" class="legends"></div>
|
||||
<svg id="timeline-{{ token }}" class="timeline-graph"></svg>
|
||||
<script>{% autoescape 'js' %}
|
||||
window.addEventListener('load', function onLoad() {
|
||||
const theme = new Theme();
|
||||
|
||||
new TimelineEngine(
|
||||
theme,
|
||||
new SvgRenderer(document.getElementById('timeline-{{ token }}')),
|
||||
new Legend(document.getElementById('legend-{{ token }}'), {{ classnames|json_encode|raw }}),
|
||||
new Legend(document.getElementById('legend-{{ token }}'), theme),
|
||||
document.getElementById('threshold'),
|
||||
{{ helper.dump_request_data(token, events, origin) }}
|
||||
);
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
class TimelineEngine {
|
||||
/**
|
||||
* @param {Theme} theme
|
||||
* @param {Renderer} renderer
|
||||
* @param {Legend} legend
|
||||
* @param {Element} threshold
|
||||
@ -9,7 +10,8 @@ class TimelineEngine {
|
||||
* @param {Number} eventHeight
|
||||
* @param {Number} horizontalMargin
|
||||
*/
|
||||
constructor(renderer, legend, threshold, request, eventHeight = 36, horizontalMargin = 10) {
|
||||
constructor(theme, renderer, legend, threshold, request, eventHeight = 36, horizontalMargin = 10) {
|
||||
this.theme = theme;
|
||||
this.renderer = renderer;
|
||||
this.legend = legend;
|
||||
this.threshold = threshold;
|
||||
@ -81,7 +83,7 @@ class TimelineEngine {
|
||||
const lines = periods.map(period => this.createPeriod(period, category));
|
||||
const label = this.createLabel(this.getShortName(name), duration, memory, periods[0]);
|
||||
const title = this.renderer.createTitle(name);
|
||||
const group = this.renderer.group([title, border, label].concat(lines), this.legend.getClassname(event.category));
|
||||
const group = this.renderer.group([title, border, label].concat(lines), this.theme.getCategoryColor(event.category));
|
||||
|
||||
event.elements = Object.assign(event.elements || {}, { group, label, border });
|
||||
|
||||
@ -100,7 +102,7 @@ class TimelineEngine {
|
||||
}
|
||||
|
||||
createPeriod(period, category) {
|
||||
const timeline = this.renderer.createPath(null, 'timeline-period');
|
||||
const timeline = this.renderer.createPath(null, 'timeline-period', this.theme.getCategoryColor(category));
|
||||
|
||||
period.draw = category === 'section' ? this.renderer.setSectionLine : this.renderer.setPeriodLine;
|
||||
period.elements = Object.assign(period.elements || {}, { timeline });
|
||||
@ -213,14 +215,14 @@ class TimelineEngine {
|
||||
}
|
||||
|
||||
class Legend {
|
||||
constructor(element, classnames) {
|
||||
constructor(element, theme) {
|
||||
this.element = element;
|
||||
this.classnames = classnames;
|
||||
this.theme = theme;
|
||||
|
||||
this.toggle = this.toggle.bind(this);
|
||||
this.createCategory = this.createCategory.bind(this);
|
||||
|
||||
this.categories = Array.from(Object.keys(classnames)).map(this.createCategory);
|
||||
this.categories = Array.from(this.theme.getDefaultCategories()).map(this.createCategory);
|
||||
}
|
||||
|
||||
add(category) {
|
||||
@ -229,8 +231,8 @@ class Legend {
|
||||
|
||||
createCategory(category) {
|
||||
const element = document.createElement('button');
|
||||
|
||||
element.className = `timeline-category ${this.getClassname(category)} active`;
|
||||
element.className = `timeline-category active`;
|
||||
element.style.borderColor = this.theme.getCategoryColor(category);
|
||||
element.innerText = category;
|
||||
element.value = category;
|
||||
element.type = 'button';
|
||||
@ -390,13 +392,17 @@ class SvgRenderer {
|
||||
return element;
|
||||
}
|
||||
|
||||
createPath(path = null, className = null) {
|
||||
createPath(path = null, className = null, color = null) {
|
||||
const element = this.create('path', className);
|
||||
|
||||
if (path) {
|
||||
element.setAttribute('d', path);
|
||||
}
|
||||
|
||||
if (color) {
|
||||
element.setAttribute('fill', color);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
@ -410,3 +416,55 @@ class SvgRenderer {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
class Theme {
|
||||
constructor(element) {
|
||||
this.reservedCategoryColors = {
|
||||
'default': '#777',
|
||||
'section': '#999',
|
||||
'event_listener': '#00b8f5',
|
||||
'template': '#66cc00',
|
||||
'doctrine': '#ff6633',
|
||||
'messenger_middleware': '#bdb81e',
|
||||
'controller.argument_value_resolver': '#8c5de6',
|
||||
};
|
||||
|
||||
this.customCategoryColors = [
|
||||
'#dbab09', // dark yellow
|
||||
'#ea4aaa', // pink
|
||||
'#964b00', // brown
|
||||
'#22863a', // dark green
|
||||
'#0366d6', // dark blue
|
||||
'#17a2b8', // teal
|
||||
];
|
||||
|
||||
this.getCategoryColor = this.getCategoryColor.bind(this);
|
||||
this.getDefaultCategories = this.getDefaultCategories.bind(this);
|
||||
}
|
||||
|
||||
getDefaultCategories() {
|
||||
return Object.keys(this.reservedCategoryColors);
|
||||
}
|
||||
|
||||
getCategoryColor(category) {
|
||||
return this.reservedCategoryColors[category] || this.getRandomColor(category);
|
||||
}
|
||||
|
||||
getRandomColor(category) {
|
||||
// instead of pure randomness, colors are assigned deterministically based on the
|
||||
// category name, to ensure that each custom category always displays the same color
|
||||
return this.customCategoryColors[this.hash(category) % this.customCategoryColors.length];
|
||||
}
|
||||
|
||||
// copied from https://github.com/darkskyapp/string-hash
|
||||
hash(string) {
|
||||
var hash = 5381;
|
||||
var i = string.length;
|
||||
|
||||
while(i) {
|
||||
hash = (hash * 33) ^ string.charCodeAt(--i);
|
||||
}
|
||||
|
||||
return hash >>> 0;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user