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; }