UI: Add HTTP Requests Bar Chart Tooltip (#6972)

* initialize tooltip

* style tooltip

* show date in tooltip

* show tooltip on hover

* style tooltip

* add hover padding for when bar is very short

* add tooltip test and format tooltip date

* revert to using real data

* update comment about binding the tooltip to shadowBars

* remove d3array

* use double colons for pseudo elements

* use elementId in bars-container id name to prevent clashing

* use Object.freeze to eliminate linting error
This commit is contained in:
Noelle Daley 2019-06-25 15:36:33 -07:00 committed by GitHub
parent 68f3b90978
commit 4c9dec60b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 97 additions and 15 deletions

View File

@ -3,6 +3,7 @@ import d3 from 'd3-selection';
import d3Scale from 'd3-scale';
import d3Axis from 'd3-axis';
import d3TimeFormat from 'd3-time-format';
import d3Tip from 'd3-tip';
import { assign } from '@ember/polyfills';
import { computed } from '@ember/object';
import { run } from '@ember/runloop';
@ -27,13 +28,12 @@ import { task, waitForEvent } from 'ember-concurrency';
*/
const HEIGHT = 240;
const HOVER_PADDING = 12;
export default Component.extend({
classNames: ['http-requests-bar-chart-container'],
counters: null,
/* eslint-disable ember/avoid-leaking-state-in-ember-objects */
margin: { top: 24, right: 16, bottom: 24, left: 16 },
margin: Object.freeze({ top: 24, right: 16, bottom: 24, left: 16 }),
padding: 0.04,
width: 0,
height() {
@ -84,10 +84,24 @@ export default Component.extend({
},
renderBarChart() {
const { margin, width, xScale, yScale, parsedCounters } = this;
const { margin, width, xScale, yScale, parsedCounters, elementId } = this;
const height = this.height();
const barChartSVG = d3.select('.http-requests-bar-chart');
const barsContainer = d3.select('#bars-container');
const barsContainer = d3.select(`#bars-container-${elementId}`);
// initialize the tooltip
const tip = d3Tip()
.attr('class', 'd3-tooltip')
.offset([HOVER_PADDING / 2, 0])
.html(function(d) {
const formatter = d3TimeFormat.utcFormat('%B %Y');
return `
<p class="date">${formatter(d.start_time)}</p>
<p>${Intl.NumberFormat().format(d.total)} Requests</p>
`;
});
barChartSVG.call(tip);
// render the chart
d3.select('.http-requests-bar-chart')
@ -127,8 +141,6 @@ export default Component.extend({
// render the bars
const bars = barsContainer.selectAll('.bar').data(parsedCounters, c => +c.start_time);
bars.exit().remove();
const barsEnter = bars
.enter()
.append('rect')
@ -138,9 +150,35 @@ export default Component.extend({
.merge(barsEnter)
.attr('width', xScale.bandwidth())
.attr('height', counter => height - yScale(counter.total))
// the offset between each bar
.attr('x', counter => xScale(counter.start_time))
.attr('y', counter => yScale(counter.total));
bars.exit().remove();
// render transparent bars and bind the tooltip to them since we cannot
// bind the tooltip to the actual bars. this is because the bars are
// within a clipPath & you cannot bind DOM events to non-display elements.
const shadowBarsContainer = d3.select('.shadow-bars');
const shadowBars = shadowBarsContainer.selectAll('.bar').data(parsedCounters, c => +c.start_time);
const shadowBarsEnter = shadowBars
.enter()
.append('rect')
.attr('class', 'bar')
.on('mouseenter', tip.show)
.on('mouseleave', tip.hide);
shadowBars
.merge(shadowBarsEnter)
.attr('width', xScale.bandwidth())
.attr('height', counter => height - yScale(counter.total) + HOVER_PADDING)
.attr('x', counter => xScale(counter.start_time))
.attr('y', counter => yScale(counter.total) - HOVER_PADDING)
.attr('fill', 'transparent')
.attr('stroke', 'transparent');
shadowBars.exit().remove();
},
updateDimensions() {

View File

@ -37,3 +37,30 @@
}
}
}
.d3-tooltip {
line-height: 1.25;
padding: $spacing-s;
background: $grey;
color: $ui-gray-010;
border-radius: 2px;
}
/* Creates a small triangle extender for the tooltip */
.d3-tooltip::after {
box-sizing: border-box;
display: inline;
font-size: 10px;
width: 100%;
color: $grey;
content: '\25BC';
position: absolute;
text-align: center;
}
/* Style northward tooltips differently */
.d3-tooltip.n::after {
margin-top: -4px;
top: 100%;
left: 0;
}

View File

@ -10,9 +10,10 @@
<g class="gridlines"></g>
<g class="x-axis"></g>
<g class="y-axis"></g>
<g clip-path="url(#bars-container)">
<g clip-path="url(#bars-container-{{this.elementId}})">
<rect x="0" y="0" width="100%" height="100%" style="fill: url(#bg-gradient)"></rect>
<clipPath id="bars-container">
<clipPath id="bars-container-{{this.elementId}}">
</clipPath>
</g>
<g class="shadow-bars"></g>
</svg>

View File

@ -57,6 +57,7 @@
"d3-scale": "^1.0.7",
"d3-selection": "^1.3.0",
"d3-time-format": "^2.1.1",
"d3-tip": "^0.9.1",
"date-fns": "^1.29.0",
"deepmerge": "^2.1.1",
"doctoc": "^1.4.0",
@ -158,6 +159,5 @@
"lib/replication",
"lib/kmip"
]
},
"dependencies": {}
}
}

View File

@ -1,6 +1,6 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { render, triggerEvent } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
const COUNTERS = [
@ -25,7 +25,7 @@ module('Integration | Component | http-requests-bar-chart', function(hooks) {
test('it renders the correct number of bars, ticks, and gridlines', async function(assert) {
await render(hbs`<HttpRequestsBarChart @counters={{counters}}/>`);
assert.equal(this.element.querySelectorAll('.bar').length, 3);
assert.equal(this.element.querySelectorAll('.bar').length, 6, 'it renders the bars and shadow bars');
assert.equal(this.element.querySelectorAll('.tick').length, 9), 'it renders the ticks and gridlines';
});
@ -43,4 +43,12 @@ module('Integration | Component | http-requests-bar-chart', function(hooks) {
'y axis ticks should round to the nearest thousand'
);
});
test('it renders a tooltip', async function(assert) {
await render(hbs`<HttpRequestsBarChart @counters={{counters}}/>`);
await triggerEvent('.shadow-bars>.bar', 'mouseenter');
const tooltipLabel = document.querySelector('.d3-tooltip .date');
assert.equal(tooltipLabel.textContent, 'April 2019', 'it shows the tooltip with the formatted date');
});
});

View File

@ -6486,7 +6486,7 @@ d3-axis@^1.0.8:
resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9"
integrity sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==
d3-collection@1:
d3-collection@1, d3-collection@^1.0.4:
version "1.0.7"
resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e"
integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==
@ -6538,6 +6538,14 @@ d3-time@1:
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.11.tgz#1d831a3e25cd189eb256c17770a666368762bbce"
integrity sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw==
d3-tip@^0.9.1:
version "0.9.1"
resolved "https://registry.yarnpkg.com/d3-tip/-/d3-tip-0.9.1.tgz#84e6d331c4e6650d80c5228a07e41820609ab64b"
integrity sha512-EVBfG9d+HnjIoyVXfhpytWxlF59JaobwizqMX9EBXtsFmJytjwHeYiUs74ldHQjE7S9vzfKTx2LCtvUrIbuFYg==
dependencies:
d3-collection "^1.0.4"
d3-selection "^1.3.0"
dag-map@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/dag-map/-/dag-map-2.0.2.tgz#9714b472de82a1843de2fba9b6876938cab44c68"