Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better tooltips for charts; ability to change formats for numbers and dates (WIP) #2468

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions client/app/lib/value-format.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import moment from 'moment/moment';
import numeral from 'numeral';
import _ from 'underscore';

numeral.options.scalePercentBy100 = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By default, for formats like 0[.]00% numeral will multiply value by 100 - so 0.5 will become 50%. This option disables this behavior so 0.5 will become 0.5%, and 50 - 50%.


// eslint-disable-next-line
const urlPattern = /(^|[\s\n]|<br\/?>)((?:https?|ftp):\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|])/gi;

Expand Down
2 changes: 1 addition & 1 deletion client/app/services/query-result.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ function QueryResultService($resource, $timeout, $q) {
const series = {};

this.getData().forEach((row) => {
let point = {};
let point = { $raw: row };
let seriesName;
let xValue = 0;
const yValues = {};
Expand Down
102 changes: 86 additions & 16 deletions client/app/visualizations/chart/chart-editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
<li ng-class="{active: currentTab == 'series'}" ng-if="options.globalSeriesType != 'custom'">
<a ng-click="changeTab('series')">Series</a>
</li>
<li ng-class="{active: currentTab == 'formatting'}" ng-if="options.globalSeriesType != 'custom'">
<a ng-click="changeTab('formatting')">Formatting</a>
</li>
</ul>
<div ng-if="currentTab == 'general'" class="m-t-10 m-b-10">
<div class="form-group">
Expand Down Expand Up @@ -135,24 +138,26 @@
</div>
</div>

<div class="form-group" ng-if="options.globalSeriesType == 'custom'">
<label class="control-label">Custom code</label>
<textarea ng-model="options.customCode" class="form-control v-resizable" rows="10">
</textarea>
</div>
<div ng-if="options.globalSeriesType == 'custom'">
<div class="form-group">
<label class="control-label">Custom code</label>
<textarea ng-model="options.customCode" class="form-control v-resizable" rows="10">
</textarea>
</div>

<div class="checkbox" ng-if="options.globalSeriesType == 'custom'">
<label>
<input type="checkbox" ng-model="options.enableConsoleLogs">
<i class="input-helper"></i> Show errors in the console
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="options.enableConsoleLogs">
<i class="input-helper"></i> Show errors in the console
</label>
</div>

<div class="checkbox" ng-if="options.globalSeriesType == 'custom'">
<label>
<input type="checkbox" ng-model="options.autoRedraw">
<i class="input-helper"></i> Auto update graph
</label>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="options.autoRedraw">
<i class="input-helper"></i> Auto update graph
</label>
</div>
</div>

<div ng-if="currentTab == 'xAxis'" class="m-t-10 m-b-10">
Expand Down Expand Up @@ -267,4 +272,69 @@ <h4>{{$index == 0 ? 'Left' : 'Right'}} Y Axis</h4>
</tbody>
</table>
</div>

<div ng-if="currentTab == 'formatting'" class="m-t-10 m-b-10">
<div class="form-group">
<label for="chart-editor-number-format">
Number format
<span class="m-l-5"
uib-popover-html="'Format <a href=&quot;http://numeraljs.com/&quot; target=&quot;_blank&quot;>specs.</a>'"
popover-trigger="'click outsideClick'"><i class="fa fa-question-circle"></i></span>
</label>
<input class="form-control" ng-model="options.numberFormat" ng-model-options="{ allowInvalid: true, debounce: 200 }"
id="chart-editor-number-format">
</div>

<div class="form-group">
<label for="chart-editor-percent-format">
Percent format
<span class="m-l-5"
uib-popover-html="'Format <a href=&quot;http://numeraljs.com/&quot; target=&quot;_blank&quot;>specs.</a>'"
popover-trigger="'click outsideClick'"><i class="fa fa-question-circle"></i></span>
</label>
<input class="form-control" ng-model="options.percentFormat" ng-model-options="{ allowInvalid: true, debounce: 200 }"
id="chart-editor-percent-format">
</div>

<div class="form-group">
<label for="chart-editor-datetime-format">
Date/Time format
<span class="m-l-5"
uib-popover-html="'Format <a href=&quot;http://momentjs.com/docs/#/displaying/format/&quot; target=&quot;_blank&quot;>specs.</a>'"
popover-trigger="'click outsideClick'"><i class="fa fa-question-circle"></i></span>
</label>
<input class="form-control" ng-model="options.dateTimeFormat" ng-model-options="{ allowInvalid: true, debounce: 200 }"
id="chart-editor-datetime-format">
</div>

<div class="form-group">
<label for="chart-editor-text">Data labels</label>
<input class="form-control" ng-model="options.textFormat" ng-model-options="{ allowInvalid: true, debounce: 200 }"
id="chart-editor-text" placeholder="(auto)">
</div>

<div class="form-group">
<label for="chart-editor-tooltip-header">Tooltip header</label>
<input class="form-control" ng-model="options.tooltipHeader" ng-model-options="{ allowInvalid: true, debounce: 200 }"
id="chart-editor-tooltip-header" placeholder="(none)">
</div>
<div class="form-group">
<label for="chart-editor-tooltip-line">Tooltip line</label>
<input class="form-control" ng-model="options.tooltipLine" ng-model-options="{ allowInvalid: true, debounce: 200 }"
id="chart-editor-tooltip-line">
</div>
<div class="form-group">
<label for="chart-editor-tooltip-footer">Tooltip footer</label>
<input class="form-control" ng-model="options.tooltipFooter" ng-model-options="{ allowInvalid: true, debounce: 200 }"
id="chart-editor-tooltip-footer" placeholder="(none)">
</div>

<div class="form-group">
<label class="text-muted" style="font-weight: normal; cursor: pointer;"
uib-popover-html="templateHint"
popover-trigger="'click outsideClick'" popover-placement="top-left">
Format specs <i class="fa fa-question-circle m-l-5"></i>
</label>
</div>
</div>
</div>
68 changes: 49 additions & 19 deletions client/app/visualizations/chart/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,35 @@
import {
some, extend, has, partial, intersection, without, contains, isUndefined,
some, extend, defaults, has, partial, intersection, without, contains, isUndefined,
sortBy, each, pluck, keys, difference,
} from 'underscore';
import template from './chart.html';
import editorTemplate from './chart-editor.html';

const DEFAULT_OPTIONS = {
globalSeriesType: 'column',
sortX: true,
legend: { enabled: true },
yAxis: [{ type: 'linear' }, { type: 'linear', opposite: true }],
xAxis: { type: 'datetime', labels: { enabled: true } },
error_y: { type: 'data', visible: true },
series: { stacking: null, error_y: { type: 'data', visible: true } },
seriesOptions: {},
columnMapping: {},

numberFormat: '0,0[.]00000',
percentFormat: '0[.]00%',
dateTimeFormat: 'DD/MM/YYYY HH:mm',
textFormat: '', // default: combination of {{ @@yPercent }} ({{ @@y }} ± {{ @@yError }})
tooltipHeader: '<b>{{ @@x }}</b>',
tooltipLine: '<b><font color="{{ @@color }}">{{ @@name }}</font></b>: {{ @@label }})',
tooltipFooter: '',

defaultColumns: 3,
defaultRows: 8,
minColumns: 1,
minRows: 5,
};

function ChartRenderer() {
return {
restrict: 'E',
Expand Down Expand Up @@ -33,7 +58,7 @@ function ChartRenderer() {

function reloadChart() {
reloadData();
$scope.plotlyOptions = $scope.options;
$scope.plotlyOptions = extend({}, DEFAULT_OPTIONS, $scope.options);
}

$scope.$watch('options', reloadChart, true);
Expand Down Expand Up @@ -238,6 +263,27 @@ function ChartEditor(ColorPalette, clientConfig) {
}
});
}

scope.$watch('options', () => {
if (scope.options) {
// For existing visualization - set default options
defaults(scope.options, DEFAULT_OPTIONS);
}
});

scope.templateHint = `
<div class="p-b-5">Use special names to access additional properties:</div>
<div><code>{{ @@x }}</code> x-value;</div>
<div><code>{{ @@name }}</code> series name;</div>
<div class="m-t-5"><u>Tooltip lines only:</u></div>
<div><code>{{ @@color }}</code> color of current point;</div>
<div><code>{{ @@y }}</code> y-value;</div>
<div><code>{{ @@yPercent }}</code> relative y-value;</div>
<div><code>{{ @@yError }}</code> y deviation;</div>
<div><code>{{ @@label }}</code> formatted "Data label".</div>
<div class="p-t-5">Also, all query result columns can be referenced using
<code class="text-nowrap">{{ column_name }}</code> syntax.</div>
`;
},
};
}
Expand All @@ -257,28 +303,12 @@ export default function init(ngModule) {
const renderTemplate = '<chart-renderer options="visualization.options" query-result="queryResult"></chart-renderer>';
const editTemplate = '<chart-editor options="visualization.options" query-result="queryResult"></chart-editor>';

const defaultOptions = {
globalSeriesType: 'column',
sortX: true,
legend: { enabled: true },
yAxis: [{ type: 'linear' }, { type: 'linear', opposite: true }],
xAxis: { type: 'datetime', labels: { enabled: true } },
error_y: { type: 'data', visible: true },
series: { stacking: null, error_y: { type: 'data', visible: true } },
seriesOptions: {},
columnMapping: {},
defaultColumns: 3,
defaultRows: 8,
minColumns: 1,
minRows: 5,
};

VisualizationProvider.registerVisualization({
type: 'CHART',
name: 'Chart',
renderTemplate,
editorTemplate: editTemplate,
defaultOptions,
defaultOptions: DEFAULT_OPTIONS,
});
});
}
48 changes: 46 additions & 2 deletions client/app/visualizations/chart/plotly/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { each, debounce, isArray, isObject } from 'underscore';
import $ from 'jquery';

import Plotly from 'plotly.js/lib/core';
import bar from 'plotly.js/lib/bar';
import pie from 'plotly.js/lib/pie';
import histogram from 'plotly.js/lib/histogram';
import box from 'plotly.js/lib/box';

import './plotly.less';

import {
ColorPalette,
prepareData,
Expand All @@ -14,14 +17,17 @@ import {
updateDimensions,
updateData,
normalizeValue,
prepareTooltipPoints,
calculateTooltipPosition,
renderTooltipContents,
} from './utils';

Plotly.register([bar, pie, histogram, box]);
Plotly.setPlotConfig({
modeBarButtonsToRemove: ['sendDataToCloud'],
modeBarButtonsToRemove: ['sendDataToCloud', 'hoverClosestCartesian', 'hoverCompareCartesian', 'toggleSpikelines'],
});

const PlotlyChart = () => ({
const PlotlyChart = $sanitize => ({
restrict: 'E',
template: '<div class="plotly-chart-container" resize-event="handleResize()"></div>',
scope: {
Expand All @@ -34,6 +40,10 @@ const PlotlyChart = () => ({
let layout = {};
let data = [];

const tooltip = $('<div>')
.addClass('plotly-chart-tooltip')
.appendTo('body');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we render our own tooltip... I can see how this can become a maintenance nightmare, chasing various Plotly edge cases 🤔

What do we lose if we go with Plotly's builtin tooltip?

Copy link
Collaborator Author

@kravets-levko kravets-levko Apr 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried a lot of settings, and noticed only two edge cases: bar charts with stacking enabled, and pie charts. for both we just need to compute position for tooltip in a different way (for pie we already receive position in event data).

Own tooltip allows to show all data in a single place (like in Highcharts) - x-axis value, all y values, etc. With default tooltip, we can customize text for y-values but not for x, and (if you remember) Plotly shows tooltip for each series for the same x value , and one more tooltip for x value (which also has a different style).

If you're afraid that it may be a nightmare - of course, I can simplify it, but we will lose some formatting and styling.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Past experience shows that there are always unexpected scenarios with charts :) Once we merge and deploy this, we're committed to it. But I don't think we need this distraction right now (having to maintain and fix issues related to the tooltip) -- we have more interesting stuff to do.

So let's shelf this for now, and implement something simpler:

  • First step: expose the ability to set hoverformat for the X and Y axes.
  • Second step: feature to set the hovertext property using data from the current point.

While it's not as complete as what you implemented here, it will serve most of the cases.


const updateChartDimensions = () => {
if (updateDimensions(layout, plotlyElement, calculateMargins(plotlyElement))) {
Plotly.relayout(plotlyElement, layout);
Expand Down Expand Up @@ -64,6 +74,36 @@ const PlotlyChart = () => ({
});

plotlyElement.on('plotly_afterplot', updateChartDimensions);

plotlyElement.on('plotly_hover', (hoverData) => {
const points = prepareTooltipPoints(hoverData.points, data);
if (points.length > 0) {
const bounds = plotlyElement.getBoundingClientRect();
const offsetLeft = bounds.left + window.scrollX;
const offsetTop = bounds.top + window.scrollY;


const { left, top } = calculateTooltipPosition(points, scope.options);
tooltip
.css({
left: Math.round(left + offsetLeft) + 'px',
top: Math.round(top + offsetTop) + 'px',
})
.html($sanitize(renderTooltipContents(points, data, scope.options)))
.show();

const tooltipBounds = tooltip[0].getBoundingClientRect();
if (tooltipBounds.top < 0) {
tooltip
.css({
top: Math.round(top + offsetTop - tooltipBounds.top) + 'px',
});
}
}
});
plotlyElement.on('plotly_unhover', () => {
tooltip.hide();
});
}
update();

Expand All @@ -79,6 +119,10 @@ const PlotlyChart = () => ({
}, true);

scope.handleResize = debounce(updateChartDimensions, 50);

scope.$on('$destroy', () => {
tooltip.remove();
});
},
});

Expand Down
38 changes: 38 additions & 0 deletions client/app/visualizations/chart/plotly/plotly.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.plotly-chart-tooltip {
pointer-events: none;
display: none;
position: absolute;
z-index: 999999;
transform: translate(-50%, -100%);
margin: -10px 0 0 0;

background: #fff;
border: 1px solid #ccc;
color: #333;
left: 100px;
top: 100px;
padding: 10px 15px;
border-radius: 5px;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);

&:after {
content: '';
font-size: 1px;
display: block;
width: 10px;
height: 10px;
position: absolute;
left: 50%;
bottom: 0;
border: inherit;
border-top: 0;
border-left: 0;
background: inherit;
transform-origin: 50% 50%;
transform: translate(-50%, 50%) rotate(45deg);
}

> div {
white-space: nowrap;
}
}
Loading