Show value in tooltip as percentage in vertical bar chart percentage mode (#37326) (#38650)

Before that the value was just displayed als a number
This commit is contained in:
Matthias Wilhelm 2019-06-11 16:30:56 +02:00 committed by GitHub
parent fa02b72b31
commit 68d1eac274
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 584 additions and 40 deletions

View file

@ -49,7 +49,12 @@ export function PointSeriesTooltipFormatterProvider($compile, $rootScope) {
}
if (datum.y) {
const value = datum.yScale ? datum.yScale * datum.y : datum.y;
addDetail(currentSeries.label, currentSeries.yAxisFormatter(value));
if(event.isPercentageMode) {
const valueInPercent = Math.round(value * 10000) / 100;
addDetail(currentSeries.label, `${valueInPercent.toFixed(2)} %`);
} else {
addDetail(currentSeries.label, currentSeries.yAxisFormatter(value));
}
}
if (datum.z) {
addDetail(currentSeries.zLabel, currentSeries.zAxisFormatter(datum.z));

View file

@ -0,0 +1,153 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Vislib event responses dispatcher - for heatmap return valid data for a heatmap popover 1`] = `
Object {
"color": undefined,
"config": Object {
"get": [Function],
},
"data": Object {
"series": Array [
Object {
"id": "1",
"label": "Thursday",
"rawId": "Thursday-col-2-1",
"values": Array [],
},
Object {
"id": "1",
"label": "Friday",
"rawId": "Friday-col-2-1",
"values": Array [],
},
Object {
"id": "1",
"label": "Tuesday",
"rawId": "Tuesday-col-2-1",
"values": Array [],
},
Object {
"id": "1",
"label": "Wednesday",
"rawId": "Wednesday-col-2-1",
"values": Array [],
},
Object {
"id": "1",
"label": "Saturday",
"rawId": "Saturday-col-2-1",
"values": Array [],
},
],
},
"datum": Object {
"extraMetrics": Array [],
"parent": Object {
"accessor": "col-0-2",
"column": 0,
"params": Object {},
"title": "day_of_week: Descending",
},
"series": "Thursday",
"seriesId": "Thursday-col-2-1",
"x": "Men's Shoes",
"y": 43,
},
"e": Object {
"target": Object {
"nearestViewportElement": Object {
"__data__": Object {
"series": Array [
Object {
"id": "1",
"label": "Thursday",
"rawId": "Thursday-col-2-1",
"values": Array [],
},
Object {
"id": "1",
"label": "Friday",
"rawId": "Friday-col-2-1",
"values": Array [],
},
Object {
"id": "1",
"label": "Tuesday",
"rawId": "Tuesday-col-2-1",
"values": Array [],
},
Object {
"id": "1",
"label": "Wednesday",
"rawId": "Wednesday-col-2-1",
"values": Array [],
},
Object {
"id": "1",
"label": "Saturday",
"rawId": "Saturday-col-2-1",
"values": Array [],
},
],
},
},
},
},
"handler": Object {
"data": Object {},
"visConfig": Object {
"get": [Function],
},
},
"isPercentageMode": false,
"label": "Thursday",
"point": Object {
"extraMetrics": Array [],
"parent": Object {
"accessor": "col-0-2",
"column": 0,
"params": Object {},
"title": "day_of_week: Descending",
},
"series": "Thursday",
"seriesId": "Thursday-col-2-1",
"x": "Men's Shoes",
"y": 43,
},
"pointIndex": 0,
"series": Array [
Object {
"id": "1",
"label": "Thursday",
"rawId": "Thursday-col-2-1",
"values": Array [],
},
Object {
"id": "1",
"label": "Friday",
"rawId": "Friday-col-2-1",
"values": Array [],
},
Object {
"id": "1",
"label": "Tuesday",
"rawId": "Tuesday-col-2-1",
"values": Array [],
},
Object {
"id": "1",
"label": "Wednesday",
"rawId": "Wednesday-col-2-1",
"values": Array [],
},
Object {
"id": "1",
"label": "Saturday",
"rawId": "Saturday-col-2-1",
"values": Array [],
},
],
"slices": undefined,
"value": 43,
}
`;

View file

@ -0,0 +1,58 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import mockDispatchDataD3 from './fixtures/dispatch_heatmap_d3.json';
jest.mock('d3', () => ({
event: {
target: {
nearestViewportElement: {
__data__: mockDispatchDataD3,
},
},
},
}));
import { Dispatch } from '../../lib/dispatch';
import mockdataPoint from './fixtures/dispatch_heatmap_data_point.json';
import mockConfigPercentage from './fixtures/dispatch_heatmap_config.json';
jest.mock('ui/chrome', () => ({
getUiSettingsClient: () => ({
get: () => '',
}),
addBasePath: () => {},
}));
function getHandlerMock(config = {}, data = {}) {
return {
visConfig: { get: (id, fallback) => config[id] || fallback },
data,
};
}
describe('Vislib event responses dispatcher - for heatmap', () => {
test('return valid data for a heatmap popover', () => {
// this is mainly a test that isPercentageMode doesn't fail with other data than vertical barcharts
const dataPoint = mockdataPoint;
const handlerMock = getHandlerMock(mockConfigPercentage);
const dispatch = new Dispatch(handlerMock);
const actual = dispatch.eventResponse(dataPoint, 0);
expect(actual).toMatchSnapshot();
});
});

View file

@ -0,0 +1,67 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import mockDispatchDataD3 from './fixtures/dispatch_bar_chart_d3.json';
jest.mock('d3', () => ({
event: {
target: {
nearestViewportElement: {
__data__: mockDispatchDataD3,
},
},
},
}));
import { Dispatch } from '../../lib/dispatch';
import mockdataPoint from './fixtures/dispatch_bar_chart_data_point.json';
import mockConfigPercentage from './fixtures/dispatch_bar_chart_config_percentage.json';
import mockConfigNormal from './fixtures/dispatch_bar_chart_config_normal.json';
jest.mock('ui/chrome', () => ({
getUiSettingsClient: () => ({
get: () => '',
}),
addBasePath: () => {},
}));
function getHandlerMock(config = {}, data = {}) {
return {
visConfig: { get: (id, fallback) => config[id] || fallback },
data,
};
}
describe('Vislib event responses dispatcher', () => {
test('return data for a vertical bars popover in percentage mode', () => {
const dataPoint = mockdataPoint;
const handlerMock = getHandlerMock(mockConfigPercentage);
const dispatch = new Dispatch(handlerMock);
const actual = dispatch.eventResponse(dataPoint, 0);
expect(actual.isPercentageMode).toBeTruthy();
});
test('return data for a vertical bars popover in normal mode', () => {
const dataPoint = mockdataPoint;
const handlerMock = getHandlerMock(mockConfigNormal);
const dispatch = new Dispatch(handlerMock);
const actual = dispatch.eventResponse(dataPoint, 0);
expect(actual.isPercentageMode).toBeFalsy();
});
});

View file

@ -0,0 +1,31 @@
{
"seriesParams": [
{
"data": {
"id": "1",
"label": "Count"
},
"drawLinesBetweenPoints": true,
"interpolate": "cardinal",
"mode": "stacked",
"show": "true",
"showCircles": true,
"type": "histogram",
"valueAxis": "ValueAxis-1"
}
],
"valueAxes": [
{
"id": "ValueAxis-1",
"name": "LeftAxis-1",
"position": "left",
"scale": {
"type": "linear",
"mode": "normal"
},
"show": true,
"style": {},
"type": "value"
}
]
}

View file

@ -0,0 +1,40 @@
{
"seriesParams": [
{
"data": {
"id": "1",
"label": "Count"
},
"drawLinesBetweenPoints": true,
"interpolate": "cardinal",
"mode": "stacked",
"show": "true",
"showCircles": true,
"type": "histogram",
"valueAxis": "ValueAxis-1"
}
],
"valueAxes": [
{
"id": "ValueAxis-1",
"labels": {
"show": true,
"rotate": 0,
"filter": false,
"truncate": 100
},
"name": "LeftAxis-1",
"position": "left",
"scale": {
"type": "linear",
"mode": "percentage"
},
"show": true,
"style": {},
"title": {
"text": "Count"
},
"type": "value"
}
]
}

View file

@ -0,0 +1,19 @@
{
"series": [
{
"id": "1",
"rawId": "Late Aircraft Delay-col-2-1",
"label": "Late Aircraft Delay"
},
{
"id": "1",
"rawId": "No Delay-col-2-1",
"label": "No Delay"
},
{
"id": "1",
"rawId": "NAS Delay-col-2-1",
"label": "NAS Delay"
}
]
}

View file

@ -0,0 +1,9 @@
{
"parent": {
"accessor": "col-1-3",
"column": 1,
"params": {}
},
"series": "No Delay",
"seriesId": "No Delay-col-2-1"
}

View file

@ -0,0 +1,82 @@
{
"data": {
},
"_values": {
"addLegend": true,
"addTooltip": true,
"colorSchema": "Greens",
"colorsNumber": 4,
"colorsRange": [],
"type": "point_series",
"valueAxes": [
{
"id": "ValueAxis-1",
"labels": {
"color": "black",
"overwriteColor": false,
"rotate": 0,
"show": false
},
"scale": {
"defaultYExtents": false,
"type": "linear"
},
"show": false,
"type": "value"
}
],
"chartTitle": {},
"mode": "normal",
"tooltip": {
"show": true
},
"categoryAxes": [
{
"id": "CategoryAxis-1",
"type": "category",
"labels": {},
"scale": {},
"title": {
"text": "products.category.keyword: Descending"
},
"style": {
"rangePadding": 0,
"rangeOuterPadding": 0
}
},
{
"id": "CategoryAxis-2",
"type": "category",
"position": "left",
"values": [
"Thursday",
"Friday",
"Tuesday",
"Wednesday",
"Saturday"
],
"scale": {
"inverted": true
},
"labels": {
"filter": false
},
"style": {
"rangePadding": 0,
"rangeOuterPadding": 0
},
"title": {
"text": ""
}
}
],
"charts": [
{
"type": "point_series",
"series":[],
"width": 610,
"height": 124
}
]
}
}

View file

@ -0,0 +1,34 @@
{
"series": [
{
"id": "1",
"rawId": "Thursday-col-2-1",
"label": "Thursday",
"values": []
},
{
"id": "1",
"rawId": "Friday-col-2-1",
"label": "Friday",
"values": []
},
{
"id": "1",
"rawId": "Tuesday-col-2-1",
"label": "Tuesday",
"values": []
},
{
"id": "1",
"rawId": "Wednesday-col-2-1",
"label": "Wednesday",
"values": []
},
{
"id": "1",
"rawId": "Saturday-col-2-1",
"label": "Saturday",
"values": []
}
]
}

View file

@ -0,0 +1,13 @@
{
"x": "Men's Shoes",
"y": 43,
"extraMetrics": [],
"parent": {
"accessor": "col-0-2",
"column": 0,
"title": "day_of_week: Descending",
"params": {}
},
"series": "Thursday",
"seriesId": "Thursday-col-2-1"
}

View file

@ -21,6 +21,9 @@ import d3 from 'd3';
import { get } from 'lodash';
import $ from 'jquery';
import { SimpleEmitter } from '../../utils/simple_emitter';
import chrome from 'ui/chrome';
const config = chrome.getUiSettingsClient();
/**
* Handles event responses
@ -29,7 +32,6 @@ import { SimpleEmitter } from '../../utils/simple_emitter';
* @constructor
* @param handler {Object} Reference to Handler Class Object
*/
export class Dispatch extends SimpleEmitter {
constructor(handler) {
super();
@ -37,7 +39,6 @@ export class Dispatch extends SimpleEmitter {
this._listeners = {};
}
_pieClickResponse(data) {
const points = [];
@ -76,8 +77,9 @@ export class Dispatch extends SimpleEmitter {
clickEventResponse(d, props = {}) {
let isSlices = props.isSlices;
if (isSlices === undefined) {
const _data = d3.event.target.nearestViewportElement ?
d3.event.target.nearestViewportElement.__data__ : d3.event.target.__data__;
const _data = d3.event.target.nearestViewportElement
? d3.event.target.nearestViewportElement.__data__
: d3.event.target.__data__;
isSlices = !!(_data && _data.slices);
}
@ -89,6 +91,38 @@ export class Dispatch extends SimpleEmitter {
};
}
/**
* Determine whether rendering a series is configured in percentage mode
* Used to display a value percentage formatted in it's popover
*
* @param rawId {string} The rawId of series to check
* @param series {Array} Array of all series data
* @param visConfig {VisConfig}
* @returns {Boolean}
*/
_isSeriesInPercentageMode(rawId, series, visConfig) {
if (!rawId || !Array.isArray(series) || !visConfig) {
return false;
}
//find the primary id by the rawId, that id is used in the config's seriesParams
const { id } = series.find(series => series.rawId === rawId);
if (!id) {
return false;
}
//find the matching seriesParams of the series, to get the id of the valueAxis
const seriesParams = visConfig.get('seriesParams', []);
const { valueAxis: valueAxisId } = seriesParams.find(param => param.data.id === id) || {};
if (!valueAxisId) {
return false;
}
const usedValueAxis = visConfig
.get('valueAxes', [])
.find(valueAxis => valueAxis.id === valueAxisId);
return get(usedValueAxis, 'scale.mode') === 'percentage';
}
/**
* Response to hover events
*
@ -100,29 +134,33 @@ export class Dispatch extends SimpleEmitter {
*/
eventResponse(d, i) {
const datum = d._input || d;
const data = d3.event.target.nearestViewportElement ?
d3.event.target.nearestViewportElement.__data__ : d3.event.target.__data__;
const label = d.label ? d.label : (d.series || 'Count');
const data = d3.event.target.nearestViewportElement
? d3.event.target.nearestViewportElement.__data__
: d3.event.target.__data__;
const label = d.label ? d.label : d.series || 'Count';
const isSeries = !!(data && data.series);
const isSlices = !!(data && data.slices);
const series = isSeries ? data.series : undefined;
const slices = isSlices ? data.slices : undefined;
const handler = this.handler;
const color = get(handler, 'data.color');
const config = handler && handler.visConfig;
const isPercentageMode = this._isSeriesInPercentageMode(d.seriesId, series, config);
const eventData = {
value: d.y,
point: datum,
datum: datum,
label: label,
datum,
label,
color: color ? color(label) : undefined,
pointIndex: i,
series: series,
slices: slices,
config: handler && handler.visConfig,
data: data,
series,
slices,
config,
data,
e: d3.event,
handler: handler
handler,
isPercentageMode,
};
return eventData;
@ -168,8 +206,7 @@ export class Dispatch extends SimpleEmitter {
self.addMousePointer.call(this, arguments);
}
const dimmingOpacity = self.handler.visConfig.get('dimmingOpacity');
self.handler.highlight.call(this, $el, dimmingOpacity);
self.handler.highlight.call(this, $el);
self.emit('hover', self.eventResponse(d, i));
}
@ -202,14 +239,9 @@ export class Dispatch extends SimpleEmitter {
* @returns {Function}
*/
addClickEvent() {
const self = this;
const addEvent = this.addEvent;
const onClick = (d) => this.emit('click', this.clickEventResponse(d));
function click(d) {
self.emit('click', self.clickEventResponse(d));
}
return addEvent('click', click);
return this.addEvent('click', onClick);
}
/**
@ -263,14 +295,16 @@ export class Dispatch extends SimpleEmitter {
* @param element {d3.Selection}
* @method highlight
*/
highlight(element, dimmingOpacity) {
highlight(element) {
const label = this.getAttribute('data-label');
if (!label) return;
$(element).parent().find('[data-label]')
.css('opacity', 1)//Opacity 1 is needed to avoid the css application
const dimming = config.get('visualization:dimmingOpacity');
$(element)
.parent()
.find('[data-label]')
.css('opacity', 1) //Opacity 1 is needed to avoid the css application
.not((els, el) => String($(el).data('label')) === label)
.css('opacity', justifyOpacity(dimmingOpacity));
.css('opacity', justifyOpacity(dimming));
}
/**
@ -305,32 +339,32 @@ export class Dispatch extends SimpleEmitter {
}
brush.on('brushend', function brushEnd() {
// Assumes data is selected at the chart level
// In this case, the number of data objects should always be 1
const data = d3.select(this).data()[0];
const isTimeSeries = (data.ordered && data.ordered.date);
const isTimeSeries = data.ordered && data.ordered.date;
// Allows for brushing on d3.scale.ordinal()
const selected = xScale.domain().filter(function (d) {
return (brush.extent()[0] <= xScale(d)) && (xScale(d) <= brush.extent()[1]);
});
const selected = xScale.domain().filter((d) =>
brush.extent()[0] <= xScale(d) && xScale(d) <= brush.extent()[1]
);
const range = isTimeSeries ? brush.extent() : selected;
return self.emit('brush', {
range: range,
range,
config: visConfig,
e: d3.event,
data: data
data,
});
});
// if `addBrushing` is true, add brush canvas
if (self.listenerCount('brush')) {
const rect = svg.insert('g', 'g')
const rect = svg
.insert('g', 'g')
.attr('class', 'brush')
.call(brush)
.call(function (brushG) {
.call(brushG => {
// hijack the brush start event to filter out right/middle clicks
const brushHandler = brushG.on('mousedown.brush');
if (!brushHandler) return; // touch events in use
@ -355,9 +389,8 @@ function validBrushClick(event) {
return event.button === 0;
}
function justifyOpacity(opacity) {
const decimalNumber = parseFloat(opacity, 10);
const fallbackOpacity = 0.5;
return (0 <= decimalNumber && decimalNumber <= 1) ? decimalNumber : fallbackOpacity;
return 0 <= decimalNumber && decimalNumber <= 1 ? decimalNumber : fallbackOpacity;
}