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:
parent
68f3b90978
commit
4c9dec60b7
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
10
ui/yarn.lock
10
ui/yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue