diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/editors/point_series.html b/src/legacy/core_plugins/kbn_vislib_vis_types/public/editors/point_series.html
index 751c75f9f2cd4..47079b44e1453 100644
--- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/editors/point_series.html
+++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/editors/point_series.html
@@ -64,7 +64,19 @@
+
+
-
+
\ No newline at end of file
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js
index 88ee50e023a49..07ddbcb941fdc 100644
--- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js
+++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js
@@ -98,6 +98,9 @@ export default function PointSeriesVisType(Private) {
legendPosition: 'right',
times: [],
addTimeMarker: false,
+ labels: {
+ show: false,
+ }
},
},
editorConfig: {
diff --git a/src/legacy/ui/public/vislib/_index.scss b/src/legacy/ui/public/vislib/_index.scss
index f8045b7cf5d35..4e4ba8175444d 100644
--- a/src/legacy/ui/public/vislib/_index.scss
+++ b/src/legacy/ui/public/vislib/_index.scss
@@ -1,3 +1,5 @@
@import './variables';
@import './lib/index';
+
+@import './visualizations/point_series/index';
diff --git a/src/legacy/ui/public/vislib/lib/axis/axis_config.js b/src/legacy/ui/public/vislib/lib/axis/axis_config.js
index c19cb4de09713..7c49d7d39d2a9 100644
--- a/src/legacy/ui/public/vislib/lib/axis/axis_config.js
+++ b/src/legacy/ui/public/vislib/lib/axis/axis_config.js
@@ -61,9 +61,13 @@ const defaults = {
title: {
text: '',
elSelector: '.visAxis__column--{pos} .axis-div',
- }
+ },
+ padForLabels: 0,
};
+const padForLabelsX = 40;
+const padForLabelsY = 15;
+
const categoryDefaults = {
type: 'category',
position: 'bottom',
@@ -159,6 +163,10 @@ export class AxisConfig {
this._values.scale.inverted = _.get(axisConfigArgs, 'scale.inverted', true);
}
+ if (chartConfig.get('labels.show', false) && !isCategoryAxis) {
+ this._values.padForLabels = isHorizontal ? padForLabelsX : padForLabelsY;
+ }
+
let offset;
let stacked = true;
switch (this.get('scale.mode')) {
diff --git a/src/legacy/ui/public/vislib/lib/axis/axis_scale.js b/src/legacy/ui/public/vislib/lib/axis/axis_scale.js
index 807e2adbf3324..836d27441d1ae 100644
--- a/src/legacy/ui/public/vislib/lib/axis/axis_scale.js
+++ b/src/legacy/ui/public/vislib/lib/axis/axis_scale.js
@@ -168,6 +168,21 @@ export class AxisScale {
return [Math.min(0, min), Math.max(0, max)];
}
+ getDomain(length) {
+ const domain = this.getExtents();
+ const pad = this.axisConfig.get('padForLabels');
+ if (pad > 0 && this.canApplyNice()) {
+ const domainLength = domain[1] - domain[0];
+ const valuePerPixel = domainLength / length;
+ const padValue = valuePerPixel * pad;
+ if (domain[0] < 0) {
+ domain[0] -= padValue;
+ }
+ domain[1] += padValue;
+ }
+ return domain;
+ }
+
getRange(length) {
if (this.axisConfig.isHorizontal()) {
return !this.axisConfig.get('scale.inverted') ? [0, length] : [length, 0];
@@ -212,7 +227,7 @@ export class AxisScale {
getScale(length) {
const config = this.axisConfig;
const scale = this.getD3Scale(config.getScaleType());
- const domain = this.getExtents();
+ const domain = this.getDomain(length);
const range = this.getRange(length);
const padding = config.get('style.rangePadding');
const outerPadding = config.get('style.rangeOuterPadding');
diff --git a/src/legacy/ui/public/vislib/visualizations/point_series/_index.scss b/src/legacy/ui/public/vislib/visualizations/point_series/_index.scss
new file mode 100644
index 0000000000000..53fce786ecc15
--- /dev/null
+++ b/src/legacy/ui/public/vislib/visualizations/point_series/_index.scss
@@ -0,0 +1 @@
+@import './labels';
diff --git a/src/legacy/ui/public/vislib/visualizations/point_series/_labels.scss b/src/legacy/ui/public/vislib/visualizations/point_series/_labels.scss
new file mode 100644
index 0000000000000..8bcd17fd55ddf
--- /dev/null
+++ b/src/legacy/ui/public/vislib/visualizations/point_series/_labels.scss
@@ -0,0 +1,20 @@
+$visColumnChartBarLabelDarkColor: #000; // EUI doesn't yet have a variable for fully black in all themes;
+$visColumnChartBarLabelLightColor: $euiColorGhost;
+
+.visColumnChart__barLabel {
+ font-size: 8pt;
+ pointer-events: none;
+}
+
+.visColumnChart__barLabel--stack {
+ dominant-baseline: central;
+ text-anchor: middle;
+}
+
+.visColumnChart__bar-label--dark {
+ fill: $visColumnChartBarLabelDarkColor;
+}
+
+.visColumnChart__bar-label--light {
+ fill: $visColumnChartBarLabelLightColor;
+}
diff --git a/src/legacy/ui/public/vislib/visualizations/point_series/column_chart.js b/src/legacy/ui/public/vislib/visualizations/point_series/column_chart.js
index 6e0dd2851df84..d6c1d2b86f158 100644
--- a/src/legacy/ui/public/vislib/visualizations/point_series/column_chart.js
+++ b/src/legacy/ui/public/vislib/visualizations/point_series/column_chart.js
@@ -18,14 +18,16 @@
*/
import _ from 'lodash';
+import d3 from 'd3';
+import { isColorDark } from '@elastic/eui/lib/services';
import { PointSeries } from './_point_series';
-
const defaults = {
mode: 'normal',
showTooltip: true,
color: undefined,
fillColor: undefined,
+ showLabel: true,
};
/**
@@ -58,6 +60,7 @@ export class ColumnChart extends PointSeries {
constructor(handler, chartEl, chartData, seriesConfigArgs) {
super(handler, chartEl, chartData, seriesConfigArgs);
this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults);
+ this.labelOptions = _.defaults(handler.visConfig.get('labels', {}), defaults.showLabel);
}
addBars(svg, data) {
@@ -124,8 +127,10 @@ export class ColumnChart extends PointSeries {
const yScale = this.getValueAxis().getScale();
const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal();
const isTimeScale = this.getCategoryAxis().axisConfig.isTimeDomain();
+ const isLabels = this.labelOptions.show;
const yMin = yScale.domain()[0];
const gutterSpacingPercentage = 0.15;
+ const chartData = this.chartData;
const groupCount = this.getGroupedCount();
const groupNum = this.getGroupedNum(this.chartData);
let barWidth;
@@ -154,6 +159,22 @@ export class ColumnChart extends PointSeries {
return yScale(d.y0 + d.y);
}
+ function labelX(d, i) {
+ return x(d, i) + widthFunc(d, i) / 2;
+ }
+
+ function labelY(d) {
+ return y(d) + heightFunc(d) / 2;
+ }
+
+ function labelDisplay(d, i) {
+ if (isHorizontal && this.getBBox().width > widthFunc(d, i)) return 'none';
+ if (!isHorizontal && this.getBBox().width > heightFunc(d)) return 'none';
+ if (isHorizontal && this.getBBox().height > heightFunc(d)) return 'none';
+ if (!isHorizontal && this.getBBox().height > widthFunc(d, i)) return 'none';
+ return 'block';
+ }
+
function widthFunc(d, i) {
if (isTimeScale) {
return datumWidth(barWidth, d, bars.data()[i + 1], xScale, gutterWidth, groupCount);
@@ -167,10 +188,13 @@ export class ColumnChart extends PointSeries {
if (d.y0 === 0 && yMin > 0) {
return yScale(yMin) - yScale(d.y);
}
-
return Math.abs(yScale(d.y0) - yScale(d.y0 + d.y));
}
+ function formatValue(d) {
+ return chartData.yAxisFormatter(d.y);
+ }
+
// update
bars
.attr('x', isHorizontal ? x : y)
@@ -178,6 +202,34 @@ export class ColumnChart extends PointSeries {
.attr('y', isHorizontal ? y : x)
.attr('height', isHorizontal ? heightFunc : widthFunc);
+ const layer = d3.select(bars[0].parentNode);
+ const barLabels = layer.selectAll('text').data(chartData.values.filter(function (d) {
+ return !_.isNull(d.y);
+ }));
+
+ if (isLabels) {
+ const colorFunc = this.handler.data.getColorFunc();
+ const d3Color = d3.rgb(colorFunc(chartData.label));
+ let labelClass;
+ if (isColorDark(d3Color.r, d3Color.g, d3Color.b)) {
+ labelClass = 'visColumnChart__bar-label--light';
+ } else {
+ labelClass = 'visColumnChart__bar-label--dark';
+ }
+
+ barLabels
+ .enter()
+ .append('text')
+ .text(formatValue)
+ .attr('class', `visColumnChart__barLabel visColumnChart__barLabel--stack ${labelClass}`)
+ .attr('x', isHorizontal ? labelX : labelY)
+ .attr('y', isHorizontal ? labelY : labelX)
+
+ // display must apply last, because labelDisplay decision it based
+ // on text bounding box which depends on actual applied style.
+ .attr('display', labelDisplay);
+ }
+
return bars;
}
@@ -191,12 +243,14 @@ export class ColumnChart extends PointSeries {
addGroupedBars(bars) {
const xScale = this.getCategoryAxis().getScale();
const yScale = this.getValueAxis().getScale();
+ const chartData = this.chartData;
const groupCount = this.getGroupedCount();
const groupNum = this.getGroupedNum(this.chartData);
const gutterSpacingPercentage = 0.15;
const isTimeScale = this.getCategoryAxis().axisConfig.isTimeDomain();
const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal();
const isLogScale = this.getValueAxis().axisConfig.isLogScale();
+ const isLabels = this.labelOptions.show;
let barWidth;
let gutterWidth;
@@ -220,10 +274,29 @@ export class ColumnChart extends PointSeries {
if ((isHorizontal && d.y < 0) || (!isHorizontal && d.y > 0)) {
return yScale(0);
}
-
return yScale(d.y);
}
+ function labelX(d, i) {
+ return x(d, i) + widthFunc(d, i) / 2;
+ }
+
+ function labelY(d) {
+ if (isHorizontal) {
+ return d.y >= 0 ? y(d) - 4 : y(d) + heightFunc(d) + this.getBBox().height;
+ }
+ return d.y >= 0 ? y(d) + heightFunc(d) + 4 : y(d) - this.getBBox().width - 4;
+ }
+
+ function labelDisplay(d, i) {
+ if (isHorizontal && this.getBBox().width > widthFunc(d, i)) {
+ return 'none';
+ }
+ if (!isHorizontal && this.getBBox().height > widthFunc(d)) {
+ return 'none';
+ }
+ return 'block';
+ }
function widthFunc(d, i) {
if (isTimeScale) {
return datumWidth(barWidth, d, bars.data()[i + 1], xScale, gutterWidth, groupCount);
@@ -236,6 +309,10 @@ export class ColumnChart extends PointSeries {
return Math.abs(yScale(baseValue) - yScale(d.y));
}
+ function formatValue(d) {
+ return chartData.yAxisFormatter(d.y);
+ }
+
// update
bars
.attr('x', isHorizontal ? x : y)
@@ -243,6 +320,33 @@ export class ColumnChart extends PointSeries {
.attr('y', isHorizontal ? y : x)
.attr('height', isHorizontal ? heightFunc : widthFunc);
+ const layer = d3.select(bars[0].parentNode);
+ const barLabels = layer.selectAll('text').data(chartData.values.filter(function (d) {
+ return !_.isNull(d.y);
+ }));
+
+ barLabels
+ .exit()
+ .remove();
+
+ if (isLabels) {
+ const labelColor = this.handler.data.getColorFunc()(chartData.label);
+
+ barLabels
+ .enter()
+ .append('text')
+ .text(formatValue)
+ .attr('class', 'visColumnChart__barLabel')
+ .attr('x', isHorizontal ? labelX : labelY)
+ .attr('y', isHorizontal ? labelY : labelX)
+ .attr('dominant-baseline', isHorizontal ? 'auto' : 'central')
+ .attr('text-anchor', isHorizontal ? 'middle' : 'start')
+ .attr('fill', labelColor)
+
+ // display must apply last, because labelDisplay decision it based
+ // on text bounding box which depends on actual applied style.
+ .attr('display', labelDisplay);
+ }
return bars;
}