From 4a58520482a798bdc9dedfb775a4a616f969920f Mon Sep 17 00:00:00 2001 From: Noelle Daley Date: Tue, 25 Jun 2019 15:36:33 -0700 Subject: [PATCH] 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 --- ui/app/components/http-requests-bar-chart.js | 54 ++++++++++++++++--- .../components/http-requests-bar-chart.scss | 27 ++++++++++ .../components/http-requests-bar-chart.hbs | 5 +- ui/package.json | 4 +- .../http-requests-bar-chart-test.js | 12 ++++- ui/yarn.lock | 10 +++- 6 files changed, 97 insertions(+), 15 deletions(-) diff --git a/ui/app/components/http-requests-bar-chart.js b/ui/app/components/http-requests-bar-chart.js index 35328ac98b..d53fb21e5c 100644 --- a/ui/app/components/http-requests-bar-chart.js +++ b/ui/app/components/http-requests-bar-chart.js @@ -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 ` +

${formatter(d.start_time)}

+

${Intl.NumberFormat().format(d.total)} Requests

+ `; + }); + + 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() { diff --git a/ui/app/styles/components/http-requests-bar-chart.scss b/ui/app/styles/components/http-requests-bar-chart.scss index d2dded3e06..2c62196684 100644 --- a/ui/app/styles/components/http-requests-bar-chart.scss +++ b/ui/app/styles/components/http-requests-bar-chart.scss @@ -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; +} diff --git a/ui/app/templates/components/http-requests-bar-chart.hbs b/ui/app/templates/components/http-requests-bar-chart.hbs index 583b8e73aa..59458a68bd 100644 --- a/ui/app/templates/components/http-requests-bar-chart.hbs +++ b/ui/app/templates/components/http-requests-bar-chart.hbs @@ -10,9 +10,10 @@ - + - + + diff --git a/ui/package.json b/ui/package.json index 0d8a603cb9..2ddefe70fb 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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": {} + } } diff --git a/ui/tests/integration/components/http-requests-bar-chart-test.js b/ui/tests/integration/components/http-requests-bar-chart-test.js index fdf815570b..7bb07564cb 100644 --- a/ui/tests/integration/components/http-requests-bar-chart-test.js +++ b/ui/tests/integration/components/http-requests-bar-chart-test.js @@ -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``); - 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``); + 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'); + }); }); diff --git a/ui/yarn.lock b/ui/yarn.lock index 628f82fb04..5da92fdd5e 100644 --- a/ui/yarn.lock +++ b/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"