From dae5a2b495ce5e8642f85fa175c40e5d059a1966 Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 3 Jun 2020 21:06:57 +0800 Subject: [PATCH] feat: add labelLine for all series --- src/chart/funnel/FunnelSeries.ts | 10 +- src/chart/funnel/FunnelView.ts | 41 ++++---- src/chart/pie/PieSeries.ts | 10 +- src/chart/pie/PieView.ts | 27 ++--- src/echarts.ts | 5 +- src/label/LabelManager.ts | 172 +++++++++++++++++++------------ src/label/labelGuideHelper.ts | 113 +++++++++++++++++--- src/util/types.ts | 4 +- src/view/Chart.ts | 6 ++ test/labelLine.html | 124 ++++++++++++++++++++++ test/pie-alignTo.html | 16 ++- 11 files changed, 383 insertions(+), 145 deletions(-) create mode 100644 test/labelLine.html diff --git a/src/chart/funnel/FunnelSeries.ts b/src/chart/funnel/FunnelSeries.ts index a54d96712..3e3c98f72 100644 --- a/src/chart/funnel/FunnelSeries.ts +++ b/src/chart/funnel/FunnelSeries.ts @@ -28,7 +28,7 @@ import { BoxLayoutOptionMixin, HorizontalAlign, LabelOption, - LabelGuideLineOption, + LabelLineOption, ItemStyleOption, OptionDataValueNumeric } from '../../util/types'; @@ -51,12 +51,12 @@ export interface FunnelDataItemOption { height?: number | string } label?: FunnelLabelOption - labelLine?: LabelGuideLineOption + labelLine?: LabelLineOption emphasis?: { itemStyle?: ItemStyleOption label?: FunnelLabelOption - labelLine?: LabelGuideLineOption + labelLine?: LabelLineOption } } @@ -80,12 +80,12 @@ export interface FunnelSeriesOption funnelAlign?: HorizontalAlign label?: FunnelLabelOption - labelLine?: LabelGuideLineOption + labelLine?: LabelLineOption itemStyle?: ItemStyleOption emphasis?: { label?: FunnelLabelOption - labelLine?: LabelGuideLineOption + labelLine?: LabelLineOption itemStyle?: ItemStyleOption } diff --git a/src/chart/funnel/FunnelView.ts b/src/chart/funnel/FunnelView.ts index 9f6ab671e..a93299e5c 100644 --- a/src/chart/funnel/FunnelView.ts +++ b/src/chart/funnel/FunnelView.ts @@ -25,30 +25,30 @@ import ExtensionAPI from '../../ExtensionAPI'; import List from '../../data/List'; import { ColorString, LabelOption } from '../../util/types'; import Model from '../../model/Model'; +import { setLabelLineStyle } from '../../label/labelGuideHelper'; const opacityAccessPath = ['itemStyle', 'opacity'] as const; /** * Piece of pie including Sector, Label, LabelLine */ -class FunnelPiece extends graphic.Group { +class FunnelPiece extends graphic.Polygon { constructor(data: List, idx: number) { super(); - const polygon = new graphic.Polygon(); + const polygon = this; const labelLine = new graphic.Polyline(); const text = new graphic.Text(); - this.add(polygon); - this.add(labelLine); polygon.setTextContent(text); + this.setTextGuideLine(labelLine); this.updateData(data, idx, true); } updateData(data: List, idx: number, firstCreate?: boolean) { - const polygon = this.childAt(0) as graphic.Polygon; + const polygon = this; const seriesModel = data.hostModel; const itemModel = data.getItemModel(idx); @@ -92,8 +92,8 @@ class FunnelPiece extends graphic.Group { } _updateLabel(data: List, idx: number) { - const polygon = this.childAt(0); - const labelLine = this.childAt(1) as graphic.Polyline; + const polygon = this; + const labelLine = this.getTextGuideLine(); const labelText = polygon.getTextContent(); const seriesModel = data.hostModel; @@ -131,11 +131,9 @@ class FunnelPiece extends graphic.Group { outsideFill: visualColor }); - graphic.updateProps(labelLine, { - shape: { - points: labelLayout.linePoints || labelLayout.linePoints - } - }, seriesModel, idx); + labelLine.setShape({ + points: labelLayout.linePoints || labelLayout.linePoints + }); // Make sure update style on labelText after setLabelStyle. // Because setLabelStyle will replace a new style on it. @@ -153,18 +151,15 @@ class FunnelPiece extends graphic.Group { z2: 10 }); - labelLine.ignore = !labelLineModel.get('show'); - const labelLineEmphasisState = labelLine.ensureState('emphasis'); - labelLineEmphasisState.ignore = !labelLineHoverModel.get('show'); - - // Default use item visual color - labelLine.setStyle({ + setLabelLineStyle(polygon, { + normal: labelLineModel, + emphasis: labelLineHoverModel + }, { + // Default use item visual color stroke: visualColor + }, { + autoCalculate: false }); - labelLine.setStyle(labelLineModel.getModel('lineStyle').getLineStyle()); - - const lineEmphasisState = labelLine.ensureState('emphasis'); - lineEmphasisState.style = labelLineHoverModel.getModel('lineStyle').getLineStyle(); } } @@ -174,6 +169,8 @@ class FunnelView extends ChartView { private _data: List; + ignoreLabelLineUpdate = true; + render(seriesModel: FunnelSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) { const data = seriesModel.getData(); const oldData = this._data; diff --git a/src/chart/pie/PieSeries.ts b/src/chart/pie/PieSeries.ts index e1cfa912d..4b7fcc823 100644 --- a/src/chart/pie/PieSeries.ts +++ b/src/chart/pie/PieSeries.ts @@ -30,7 +30,7 @@ import { SeriesOption, CallbackDataParams, CircleLayoutOptionMixin, - LabelGuideLineOption, + LabelLineOption, ItemStyleOption, LabelOption, BoxLayoutOptionMixin, @@ -57,12 +57,12 @@ export interface PieDataItemOption extends itemStyle?: ItemStyleOption label?: PieLabelOption - labelLine?: LabelGuideLineOption + labelLine?: LabelLineOption emphasis?: { itemStyle?: ItemStyleOption label?: PieLabelOption - labelLine?: LabelGuideLineOption + labelLine?: LabelLineOption } } export interface PieSeriesOption extends @@ -81,7 +81,7 @@ export interface PieSeriesOption extends // TODO: TYPE Color Callback itemStyle?: ItemStyleOption label?: PieLabelOption - labelLine?: LabelGuideLineOption + labelLine?: LabelLineOption clockwise?: boolean startAngle?: number @@ -99,7 +99,7 @@ export interface PieSeriesOption extends emphasis?: { itemStyle?: ItemStyleOption label?: PieLabelOption - labelLine?: LabelGuideLineOption + labelLine?: LabelLineOption } animationType?: 'expansion' | 'scale' diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts index 6241d524a..54b73ba2f 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -28,6 +28,7 @@ import { Payload, ColorString, ECElement } from '../../util/types'; import List from '../../data/List'; import PieSeriesModel, {PieDataItemOption} from './PieSeries'; import labelLayout from './labelLayout'; +import { setLabelLineStyle } from '../../label/labelGuideHelper'; function updateDataSelected( this: PiePiece, @@ -160,13 +161,11 @@ class PiePiece extends graphic.Sector { private _updateLabel(seriesModel: PieSeriesModel, data: List, idx: number): void { const sector = this; - const labelLine = sector.getTextGuideLine(); const labelText = sector.getTextContent(); const itemModel = data.getItemModel(idx); const labelTextEmphasisState = labelText.ensureState('emphasis'); - const labelLineEmphasisState = labelLine.ensureState('emphasis'); const labelModel = itemModel.getModel('label'); const labelHoverModel = itemModel.getModel(['emphasis', 'label']); @@ -209,25 +208,15 @@ class PiePiece extends graphic.Sector { labelText.ignore = !labelModel.get('show'); labelTextEmphasisState.ignore = !labelHoverModel.get('show'); - labelLine.ignore = !labelLineModel.get('show'); - labelLineEmphasisState.ignore = !labelLineHoverModel.get('show'); - // Default use item visual color - labelLine.setStyle({ + setLabelLineStyle(this, { + normal: labelLineModel, + emphasis: labelLineHoverModel + }, { stroke: visualColor, opacity: style && style.opacity - }); - labelLine.setStyle(labelLineModel.getModel('lineStyle').getLineStyle()); - - const lineEmphasisState = labelLine.ensureState('emphasis'); - lineEmphasisState.style = labelLineHoverModel.getModel('lineStyle').getLineStyle(); - - let smooth = labelLineModel.get('smooth'); - if (smooth && smooth === true) { - smooth = 0.3; - } - labelLine.setShape({ - smooth: smooth as number + }, { + autoCalculate: false }); } } @@ -238,6 +227,8 @@ class PieView extends ChartView { static type = 'pie'; + ignoreLabelLineUpdate = true; + private _sectorGroup: graphic.Group; private _data: List; diff --git a/src/echarts.ts b/src/echarts.ts index 485e058e3..d4d32c87f 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1097,7 +1097,7 @@ class ECharts extends Eventful { const labelManager = this._labelManager; labelManager.updateLayoutConfig(this._api); labelManager.layout(); - labelManager.animateLabels(); + labelManager.processLabelsOverall(); } appendData(params: { @@ -1727,14 +1727,13 @@ class ECharts extends Eventful { // Add labels. labelManager.addLabelsOfSeries(chartView); - }); scheduler.unfinished = unfinished || scheduler.unfinished; labelManager.updateLayoutConfig(api); labelManager.layout(); - labelManager.animateLabels(); + labelManager.processLabelsOverall(); ecModel.eachSeries(function (seriesModel) { const chartView = ecIns._chartsMap[seriesModel.__viewId]; diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index 9c50a06bf..bb3246fcd 100644 --- a/src/label/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -36,17 +36,19 @@ import { ZRTextVerticalAlign, LabelLayoutOption, LabelLayoutOptionCallback, - LabelLayoutOptionCallbackParams + LabelLayoutOptionCallbackParams, + LabelLineOption } from '../util/types'; import { parsePercent } from '../util/number'; import ChartView from '../view/Chart'; -import { ElementTextConfig } from 'zrender/src/Element'; +import Element, { ElementTextConfig } from 'zrender/src/Element'; import { RectLike } from 'zrender/src/core/BoundingRect'; import Transformable from 'zrender/src/core/Transformable'; -import { updateLabelGuideLine } from './labelGuideHelper'; +import { updateLabelLinePoints, setLabelLineStyle } from './labelGuideHelper'; import SeriesModel from '../model/Series'; import { makeInner } from '../util/model'; -import { retrieve2, guid, each, keys } from 'zrender/src/core/util'; +import { retrieve2, each, keys } from 'zrender/src/core/util'; +import { PathStyleProps } from 'zrender/src/graphic/Path'; interface DisplayedLabelItem { label: ZRText @@ -59,7 +61,7 @@ interface DisplayedLabelItem { interface LabelLayoutDesc { label: ZRText - labelGuide: Polyline + labelLine: Polyline seriesModel: SeriesModel dataIndex: number @@ -186,7 +188,7 @@ class LabelManager { this._labelList.push({ label, - labelGuide: labelGuide, + labelLine: labelGuide, seriesModel, dataIndex, @@ -228,7 +230,6 @@ class LabelManager { attachedRot: textConfig.rotation } }); - } addLabelsOfSeries(chartView: ChartView) { @@ -253,6 +254,7 @@ class LabelManager { // Only support label being hosted on graphic elements. const textEl = child.getTextContent(); const dataIndex = getECData(child).dataIndex; + // Can only attach the text on the element with dataIndex if (textEl && dataIndex != null) { this._addLabel(dataIndex, seriesModel, textEl, layoutOption); } @@ -382,18 +384,18 @@ class LabelManager { } } - const labelGuide = labelItem.labelGuide; + const labelLine = labelItem.labelLine; // TODO Callback to determine if this overlap should be handled? if (overlapped && labelItem.layoutOption && (labelItem.layoutOption as LabelLayoutOption).overlap === 'hidden' ) { label.hide(); - labelGuide && labelGuide.hide(); + labelLine && labelLine.hide(); } else { label.attr('ignore', labelItem.defaultAttr.ignore); - labelGuide && labelGuide.attr('ignore', labelItem.defaultAttr.labelGuideIgnore); + labelLine && labelLine.attr('ignore', labelItem.defaultAttr.labelGuideIgnore); displayedLabels.push({ label, @@ -405,79 +407,115 @@ class LabelManager { }); } - updateLabelGuideLine( - label, - globalRect, - label.__hostTarget, - labelItem.hostRect, - labelItem.seriesModel.getModel(['labelLine']) - ); } } - animateLabels() { - each(this._chartViewList, function (chartView) { + /** + * Process all labels. Not only labels with layoutOption. + */ + processLabelsOverall() { + each(this._chartViewList, (chartView) => { const seriesModel = chartView.__model; - if (!seriesModel.isAnimationEnabled()) { - return; - } + const animationEnabled = seriesModel.isAnimationEnabled(); + const ignoreLabelLineUpdate = chartView.ignoreLabelLineUpdate; chartView.group.traverse((child) => { if (child.ignore) { return true; // Stop traverse descendants. } - // Only support label being hosted on graphic elements. - const textEl = child.getTextContent(); - const guideLine = child.getTextGuideLine(); - - if (textEl && !textEl.ignore && !textEl.invisible) { - const layoutStore = labelAnimationStore(textEl); - const oldLayout = layoutStore.oldLayout; - const newProps = { - x: textEl.x, - y: textEl.y, - rotation: textEl.rotation - }; - if (!oldLayout) { - textEl.attr(newProps); - const oldOpacity = retrieve2(textEl.style.opacity, 1); - // Fade in animation - textEl.style.opacity = 0; - initProps(textEl, { - style: { opacity: oldOpacity } - }, seriesModel); - } - else { - textEl.attr(oldLayout); - updateProps(textEl, newProps, seriesModel); - } - layoutStore.oldLayout = newProps; + if (!ignoreLabelLineUpdate) { + this._updateLabelLine(child, seriesModel); } - if (guideLine && !guideLine.ignore && !guideLine.invisible) { - const layoutStore = labelLineAnimationStore(guideLine); - const oldLayout = layoutStore.oldLayout; - const newLayout = { points: guideLine.shape.points }; - if (!oldLayout) { - guideLine.setShape(newLayout); - guideLine.style.strokePercent = 0; - initProps(guideLine, { - style: { strokePercent: 1 } - }, seriesModel); - } - else { - guideLine.attr({ shape: oldLayout }); - updateProps(guideLine, { - shape: newLayout - }, seriesModel); - } - - layoutStore.oldLayout = newLayout; + if (animationEnabled) { + this._animateLabels(child, seriesModel); } }); }); } + + private _updateLabelLine(el: Element, seriesModel: SeriesModel) { + // Only support label being hosted on graphic elements. + const textEl = el.getTextContent(); + // Update label line style. + const ecData = getECData(el); + const dataIndex = ecData.dataIndex; + + if (textEl && dataIndex != null) { + const data = seriesModel.getData(ecData.dataType); + const itemModel = data.getItemModel<{ + labelLine: LabelLineOption, + emphasis: { labelLine: LabelLineOption } + }>(dataIndex); + + const defaultStyle: PathStyleProps = {}; + const visualStyle = data.getItemVisual(dataIndex, 'style'); + const visualType = data.getVisual('drawType'); + // Default to be same with main color + defaultStyle.stroke = visualStyle[visualType]; + + const labelLineModel = itemModel.getModel('labelLine'); + + setLabelLineStyle(el, { + normal: labelLineModel, + emphasis: itemModel.getModel(['emphasis', 'labelLine']) + }, defaultStyle); + + + updateLabelLinePoints(el, labelLineModel); + } + } + + private _animateLabels(el: Element, seriesModel: SeriesModel) { + const textEl = el.getTextContent(); + const guideLine = el.getTextGuideLine(); + // Animate + if (textEl && !textEl.ignore && !textEl.invisible) { + const layoutStore = labelAnimationStore(textEl); + const oldLayout = layoutStore.oldLayout; + const newProps = { + x: textEl.x, + y: textEl.y, + rotation: textEl.rotation + }; + if (!oldLayout) { + textEl.attr(newProps); + const oldOpacity = retrieve2(textEl.style.opacity, 1); + // Fade in animation + textEl.style.opacity = 0; + initProps(textEl, { + style: { opacity: oldOpacity } + }, seriesModel); + } + else { + textEl.attr(oldLayout); + updateProps(textEl, newProps, seriesModel); + } + layoutStore.oldLayout = newProps; + } + + if (guideLine && !guideLine.ignore && !guideLine.invisible) { + const layoutStore = labelLineAnimationStore(guideLine); + const oldLayout = layoutStore.oldLayout; + const newLayout = { points: guideLine.shape.points }; + if (!oldLayout) { + guideLine.setShape(newLayout); + guideLine.style.strokePercent = 0; + initProps(guideLine, { + style: { strokePercent: 1 } + }, seriesModel); + } + else { + guideLine.attr({ shape: oldLayout }); + updateProps(guideLine, { + shape: newLayout + }, seriesModel); + } + + layoutStore.oldLayout = newLayout; + } + } } diff --git a/src/label/labelGuideHelper.ts b/src/label/labelGuideHelper.ts index ddeba604c..c3cbc90fd 100644 --- a/src/label/labelGuideHelper.ts +++ b/src/label/labelGuideHelper.ts @@ -20,19 +20,24 @@ import { Text as ZRText, Point, - Path + Path, + Polyline } from '../util/graphic'; import PathProxy from 'zrender/src/core/PathProxy'; import { RectLike } from 'zrender/src/core/BoundingRect'; import { normalizeRadian } from 'zrender/src/contain/util'; import { cubicProjectPoint, quadraticProjectPoint } from 'zrender/src/core/curve'; import Element from 'zrender/src/Element'; -import { LabelGuideLineOption } from '../util/types'; +import { extend, defaults, retrieve2 } from 'zrender/src/core/util'; +import { LabelLineOption } from '../util/types'; import Model from '../model/Model'; +import { invert } from 'zrender/src/core/matrix'; const PI2 = Math.PI * 2; const CMD = PathProxy.CMD; +const STATES = ['normal', 'emphasis'] as const; + const DEFAULT_SEARCH_SPACE = ['top', 'right', 'bottom', 'left'] as const; type CandidatePosition = typeof DEFAULT_SEARCH_SPACE[number]; @@ -331,50 +336,58 @@ const dir2 = new Point(); * @param target * @param targetRect */ -export function updateLabelGuideLine( - label: ZRText, - labelRect: RectLike, +export function updateLabelLinePoints( target: Element, - targetRect: RectLike, - labelLineModel: Model + labelLineModel: Model ) { if (!target) { return; } const labelLine = target.getTextGuideLine(); + const label = target.getTextContent(); // Needs to create text guide in each charts. - if (!labelLine) { + if (!(label && labelLine)) { return; } const labelGuideConfig = target.textGuideLineConfig || {}; - if (!labelGuideConfig.autoCalculate) { - return; - } const points = [[0, 0], [0, 0], [0, 0]]; const searchSpace = labelGuideConfig.candidates || DEFAULT_SEARCH_SPACE; + const labelRect = label.getBoundingRect().clone(); + labelRect.applyTransform(label.getComputedTransform()); let minDist = Infinity; const anchorPoint = labelGuideConfig && labelGuideConfig.anchor; + const targetTransform = target.getComputedTransform(); + const targetInversedTransform = invert([], targetTransform); + const len = labelLineModel.get('length2') || 0; + if (anchorPoint) { pt2.copy(anchorPoint); } for (let i = 0; i < searchSpace.length; i++) { const candidate = searchSpace[i]; getCandidateAnchor(candidate, 0, labelRect, pt0, dir); - Point.scaleAndAdd(pt1, pt0, dir, labelGuideConfig.len == null ? 15 : labelGuideConfig.len); + Point.scaleAndAdd(pt1, pt0, dir, len); + + // Transform to target coord space. + pt1.transform(targetInversedTransform); const dist = anchorPoint ? anchorPoint.distance(pt1) : (target instanceof Path ? nearestPointOnPath(pt1, target.path, pt2) - : nearestPointOnRect(pt1, targetRect, pt2)); + : nearestPointOnRect(pt1, target.getBoundingRect(), pt2)); // TODO pt2 is in the path if (dist < minDist) { minDist = dist; + // Transform back to global space. + pt1.transform(targetTransform); + pt2.transform(targetTransform); + pt2.toArray(points[0]); pt1.toArray(points[1]); pt0.toArray(points[2]); @@ -440,4 +453,76 @@ export function limitTurnAngle(linePoints: number[][], minTurnAngle: number) { tmpProjPoint.toArray(linePoints[1]); } -} \ No newline at end of file +} + + +type LabelLineModel = Model; +/** + * Create a label line if necessary and set it's style. + */ +export function setLabelLineStyle( + targetEl: Element, + statesModels: Record, + defaultStyle?: Polyline['style'], + defaultConfig?: Element['textGuideLineConfig'] +) { + let labelLine = targetEl.getTextGuideLine(); + const label = targetEl.getTextContent(); + if (!label) { + // Not show label line if there is no label. + if (labelLine) { + targetEl.removeTextGuideLine(); + } + return; + } + + const normalModel = statesModels.normal; + const showNormal = normalModel.get('show'); + const labelShowNormal = label.ignore; + + for (let i = 0; i < STATES.length; i++) { + const stateName = STATES[i]; + const stateModel = statesModels[stateName]; + const isNormal = stateName === 'normal'; + if (stateModel) { + const stateShow = stateModel.get('show'); + const isLabelIgnored = isNormal + ? labelShowNormal + : retrieve2(label.states && label.states[stateName].ignore, labelShowNormal); + if (isLabelIgnored // Not show when label is not shown in this state. + || !retrieve2(stateShow, showNormal) // Use normal state by default if not set. + ) { + const stateObj = isNormal ? labelLine : (labelLine && labelLine.states.normal); + if (stateObj) { + stateObj.ignore = true; + } + continue; + } + // Create labelLine if not exists + if (!labelLine) { + labelLine = new Polyline(); + targetEl.setTextGuideLine(labelLine); + } + + const stateObj = isNormal ? labelLine : labelLine.ensureState(stateName); + // Make sure display. + stateObj.ignore = false; + // Set smooth + let smooth = stateModel.get('smooth'); + if (smooth && smooth === true) { + smooth = 0.4; + } + stateObj.shape = stateObj.shape || {}; + (stateObj.shape as Polyline['shape']).smooth = smooth as number; + + const styleObj = stateModel.getModel('lineStyle').getLineStyle(); + isNormal ? labelLine.useStyle(styleObj) : stateObj.style = styleObj; + } + } + + if (labelLine) { + defaults(labelLine.style, defaultStyle); + // Not fill. + labelLine.style.fill = null; + } +} diff --git a/src/util/types.ts b/src/util/types.ts index 75ae6e313..994aab83f 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -799,7 +799,7 @@ export interface LineLabelOption extends Omit + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + diff --git a/test/pie-alignTo.html b/test/pie-alignTo.html index 969fde27b..db08612c2 100644 --- a/test/pie-alignTo.html +++ b/test/pie-alignTo.html @@ -68,7 +68,7 @@ under the License. type: 'pie', radius: '50%', data: data, - animation: false, + labelLine: { length2: 15 }, @@ -84,7 +84,7 @@ under the License. type: 'pie', radius: '50%', data: data, - animation: false, + labelLine: { length2: 15 }, @@ -101,7 +101,7 @@ under the License. type: 'pie', radius: '50%', data: data, - animation: false, + labelLine: { length2: 15 }, @@ -119,7 +119,7 @@ under the License. radius: '25%', center: ['50%', '50%'], data: data, - animation: false, + labelLine: { length2: 15 }, @@ -136,7 +136,7 @@ under the License. radius: '25%', center: ['50%', '50%'], data: data, - animation: false, + labelLine: { length2: 15 }, @@ -154,7 +154,7 @@ under the License. radius: '25%', center: ['50%', '50%'], data: data, - animation: false, + labelLine: { length2: 15 }, @@ -171,7 +171,7 @@ under the License. radius: '25%', center: ['50%', '50%'], data: data, - animation: false, + labelLine: { length2: 15 }, @@ -251,8 +251,6 @@ under the License. }); }); - - -- GitLab