src/components/charts/stacked_column/stacked_column.vue (357 lines of code) (raw):
<script>
import merge from 'lodash/merge';
import {
defaultChartOptions,
grid,
gridWithSecondaryYAxis,
yAxis,
dataZoomAdjustments,
mergeSeriesToOptions,
generateBarSeries,
generateLineSeries,
} from '../../../utils/charts/config';
import {
LEGEND_LAYOUT_INLINE,
LEGEND_LAYOUT_TABLE,
LEGEND_AVERAGE_TEXT,
LEGEND_CURRENT_TEXT,
LEGEND_MIN_TEXT,
LEGEND_MAX_TEXT,
CHART_TYPE_LINE,
HEIGHT_AUTO_CLASSES,
CHART_DEFAULT_SERIES_STACK,
CHART_DEFAULT_SERIES_SECONDARY_STACK,
} from '../../../utils/charts/constants';
import { colorFromDefaultPalette } from '../../../utils/charts/theme';
import { stackedPresentationOptions } from '../../../utils/constants';
import TooltipDefaultFormat from '../shared/tooltip/tooltip_default_format/tooltip_default_format.vue';
import Chart from '../chart/chart.vue';
import ChartLegend from '../legend/legend.vue';
import ChartTooltip from '../shared/tooltip/tooltip.vue';
const yAxisDefaults = {
...yAxis,
nameLocation: 'center',
axisTick: {
show: false,
},
};
export default {
name: 'GlStackedColumnChart',
components: {
Chart,
ChartTooltip,
ChartLegend,
TooltipDefaultFormat,
},
inheritAttrs: false,
props: {
bars: {
type: Array,
required: false,
default: () => [],
},
lines: {
type: Array,
required: false,
default: () => [],
},
secondaryData: {
type: Array,
required: false,
default: () => [],
},
option: {
type: Object,
required: false,
default: () => ({}),
},
presentation: {
type: String,
required: false,
default: stackedPresentationOptions.stacked,
validator: (value) => Object.values(stackedPresentationOptions).indexOf(value) !== -1,
},
groupBy: {
type: Array,
required: true,
},
xAxisType: {
type: String,
required: true,
validator: (value) => ['value', 'category', 'time', 'log'].indexOf(value) !== -1,
},
xAxisTitle: {
type: String,
required: true,
},
yAxisTitle: {
type: String,
required: true,
},
secondaryDataTitle: {
type: String,
required: false,
default: '',
},
seriesNames: {
type: Array,
required: false,
default: () => [],
},
includeLegendAvgMax: {
type: Boolean,
required: false,
default: true,
},
legendAverageText: {
type: String,
required: false,
default: LEGEND_AVERAGE_TEXT,
},
legendMaxText: {
type: String,
required: false,
default: LEGEND_MAX_TEXT,
},
legendMinText: {
type: String,
required: false,
default: LEGEND_MIN_TEXT,
},
legendCurrentText: {
type: String,
required: false,
default: LEGEND_CURRENT_TEXT,
},
legendLayout: {
type: String,
required: false,
default: LEGEND_LAYOUT_INLINE,
validator(layout) {
return [LEGEND_LAYOUT_INLINE, LEGEND_LAYOUT_TABLE].indexOf(layout) !== -1;
},
},
/**
* Callback called when showing or refreshing a tooltip.
* **Deprecated:** Use slots `#tooltip-title`, `#tooltip-content` or `#tooltip-value`.
*
* @deprecated Use slots `#tooltip-title`, `#tooltip-content` or `#tooltip-value`.
*/
formatTooltipText: {
type: Function,
required: false,
default: null,
},
customPalette: {
type: Array,
required: false,
default: null,
},
/**
* Sets the chart's height in pixels. Set to `"auto"` to use the height of the container.
*/
height: {
type: [Number, String],
required: false,
default: null,
},
},
data() {
return {
chart: null,
compiledOptions: null,
};
},
computed: {
hasSecondaryAxis() {
return Boolean(this.secondaryData.length);
},
barSeries() {
return this.bars.map(({ name, data, stack }, index) => {
const color = this.getColor(index);
const seriesStack =
this.presentation === stackedPresentationOptions.stacked
? stack || CHART_DEFAULT_SERIES_STACK
: null;
return generateBarSeries({
stack: seriesStack,
name,
data,
color,
});
});
},
lineSeries() {
const offset = this.bars.length;
return this.lines.map(({ name, data }, index) => {
const color = this.getColor(offset + index);
return generateLineSeries({ name, data, color });
});
},
secondarySeries() {
const offset = this.bars.length + this.lines.length;
return this.secondaryData.map(({ name, data, type, stack }, index) => {
const color = this.getColor(offset + index);
const seriesStack =
this.presentation === stackedPresentationOptions.stacked
? stack || CHART_DEFAULT_SERIES_SECONDARY_STACK
: null;
return type === CHART_TYPE_LINE
? generateLineSeries({ color, name, data, yAxisIndex: 1 })
: generateBarSeries({
color,
name,
data,
stack: seriesStack,
yAxisIndex: 1,
});
});
},
series() {
return [...this.barSeries, ...this.lineSeries, ...this.secondarySeries];
},
options() {
const stackedColumnChartOptions = {
grid: this.hasSecondaryAxis ? gridWithSecondaryYAxis : grid,
xAxis: {
boundaryGap: true,
axisLabel: {
margin: 20,
verticalAlign: 'bottom',
},
axisLine: {
show: false,
},
axisPointer: {
type: 'none',
},
data: this.groupBy,
name: this.xAxisTitle,
type: this.xAxisType,
},
yAxis: [
{
...yAxisDefaults,
name: this.yAxisTitle,
},
{
...yAxisDefaults,
name: this.secondaryDataTitle,
show: this.hasSecondaryAxis,
},
],
};
// `formatTooltipText` is deprecated, these added options should be
// removed when `formatTooltipText` is removed.
const deprecatedTooltipFormatterOptions = {
xAxis: {
axisPointer: {
show: true,
label: {
formatter: this.formatTooltipText,
},
},
},
};
const mergedOptions = merge(
{},
defaultChartOptions,
stackedColumnChartOptions,
this.formatTooltipText ? deprecatedTooltipFormatterOptions : {},
this.option,
dataZoomAdjustments(this.option.dataZoom)
);
// All chart options can be merged but series
// needs to be handled specially
return mergeSeriesToOptions(mergedOptions, this.series);
},
legendStyle() {
return { paddingLeft: `${grid.left}px` };
},
seriesInfo() {
const compiledSeries = this.compiledOptions?.series || [];
return compiledSeries.reduce((acc, series, index) => {
acc.push({
name: series.name,
type: series.type,
color: this.getColor(index),
data: this.includeLegendAvgMax ? series.data.map((data) => data) : undefined,
yAxisIndex: series.yAxisIndex,
});
return acc;
}, []);
},
autoHeight() {
return this.height === 'auto';
},
},
methods: {
getColor(index) {
return this.customPalette ? this.customPalette?.[index] : colorFromDefaultPalette(index);
},
onCreated(chart) {
this.chart = chart;
this.$emit('created', chart);
},
onUpdated() {
this.compiledOptions = this.chart.getOption();
},
getTooltipTitle({ params }) {
if (!params) return '';
const options = this.chart.getOption();
const titleAxisName = options?.xAxis?.[0]?.name;
return titleAxisName ? `${params.value} (${titleAxisName})` : params.value;
},
getTooltipContent({ params }) {
if (!params) return {};
const tooltipContentEntries = params.seriesData
.toSorted((a, b) => b.seriesIndex - a.seriesIndex) // Invert stacking order so it matches chart (see https://github.com/apache/echarts/issues/14700)
.map(({ seriesName = '', value, borderColor }) => [
seriesName,
{ value, color: borderColor },
]);
return Object.fromEntries(tooltipContentEntries);
},
},
HEIGHT_AUTO_CLASSES,
};
</script>
<template>
<div class="gl-relative" :class="{ [$options.HEIGHT_AUTO_CLASSES]: autoHeight }">
<chart
v-bind="$attrs"
:class="{ 'gl-grow': autoHeight }"
:height="height"
:options="options"
v-on="$listeners"
@created="onCreated"
@updated="onUpdated"
/>
<chart-tooltip v-if="chart" :chart="chart" :use-default-tooltip-formatter="!formatTooltipText">
<template #title="scope">
<slot name="tooltip-title" v-bind="scope">{{ getTooltipTitle(scope) }}</slot>
</template>
<template #default="scope">
<slot name="tooltip-content" v-bind="scope">
<tooltip-default-format :tooltip-content="getTooltipContent(scope)">
<template v-if="$scopedSlots['tooltip-value']" #tooltip-value="valueScope">
<slot name="tooltip-value" v-bind="valueScope"></slot>
</template>
</tooltip-default-format>
</slot>
</template>
</chart-tooltip>
<chart-legend
v-if="compiledOptions"
:chart="chart"
:style="legendStyle"
:series-info="seriesInfo"
:text-style="compiledOptions.textStyle"
:min-text="legendMinText"
:max-text="legendMaxText"
:average-text="legendAverageText"
:current-text="legendCurrentText"
:layout="legendLayout"
/>
</div>
</template>