LabelManager.ts 18.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/

20 21 22 23 24 25
// TODO: move labels out of viewport.

import {
    Text as ZRText,
    BoundingRect,
    getECData,
P
pissang 已提交
26 27 28
    Polyline,
    updateProps,
    initProps
29
} from '../util/graphic';
30 31 32 33 34 35
import ExtensionAPI from '../ExtensionAPI';
import {
    ZRTextAlign,
    ZRTextVerticalAlign,
    LabelLayoutOption,
    LabelLayoutOptionCallback,
P
pissang 已提交
36
    LabelLayoutOptionCallbackParams,
37
    LabelLineOption,
38 39
    Dictionary,
    ECElement
40 41
} from '../util/types';
import { parsePercent } from '../util/number';
42
import ChartView from '../view/Chart';
P
pissang 已提交
43
import Element, { ElementTextConfig } from 'zrender/src/Element';
44 45
import { RectLike } from 'zrender/src/core/BoundingRect';
import Transformable from 'zrender/src/core/Transformable';
P
pissang 已提交
46
import { updateLabelLinePoints, setLabelLineStyle, getLabelLineStatesModels } from './labelGuideHelper';
P
pissang 已提交
47 48
import SeriesModel from '../model/Series';
import { makeInner } from '../util/model';
49
import { retrieve2, each, keys, isFunction, filter, indexOf } from 'zrender/src/core/util';
P
pissang 已提交
50
import { PathStyleProps } from 'zrender/src/graphic/Path';
P
pissang 已提交
51
import Model from '../model/Model';
52
import { prepareLayoutList, hideOverlap, shiftLayoutOnX, shiftLayoutOnY } from './labelLayoutHelper';
53

54
interface LabelDesc {
55
    label: ZRText
P
pissang 已提交
56
    labelLine: Polyline
57

P
pissang 已提交
58
    seriesModel: SeriesModel
59
    dataIndex: number
P
pissang 已提交
60
    dataType: string
61

62
    layoutOption: LabelLayoutOptionCallback | LabelLayoutOption
63
    computedLayoutOption: LabelLayoutOption
64 65 66 67 68

    hostRect: RectLike
    priority: number

    defaultAttr: SavedLabelAttr
69 70 71
}

interface SavedLabelAttr {
72
    ignore: boolean
73
    labelGuideIgnore: boolean
74

75 76 77
    x: number
    y: number
    rotation: number
78

79 80 81 82 83
    style: {
        align: ZRTextAlign
        verticalAlign: ZRTextVerticalAlign
        width: number
        height: number
P
pissang 已提交
84
        fontSize: number | string
85 86 87 88

        x: number
        y: number
    }
89

90 91
    cursor: string

92 93 94
    // Configuration in attached element
    attachedPos: ElementTextConfig['position']
    attachedRot: ElementTextConfig['rotation']
95

96 97 98
    rect: RectLike
}

99 100 101 102 103 104 105 106 107 108 109
function cloneArr(points: number[][]) {
    if (points) {
        const newPoints = [];
        for (let i = 0; i < points.length; i++) {
            newPoints.push(points[i].slice());
        }
        return newPoints;
    }
}

function prepareLayoutCallbackParams(labelItem: LabelDesc, hostEl?: Element): LabelLayoutOptionCallbackParams {
110 111
    const labelAttr = labelItem.defaultAttr;
    const label = labelItem.label;
112
    const labelLine = hostEl && hostEl.getTextGuideLine();
113
    return {
114
        dataIndex: labelItem.dataIndex,
P
pissang 已提交
115
        dataType: labelItem.dataType,
P
pissang 已提交
116
        seriesIndex: labelItem.seriesModel.seriesIndex,
117 118 119
        text: labelItem.label.style.text,
        rect: labelItem.hostRect,
        labelRect: labelAttr.rect,
120 121
        // x: labelAttr.x,
        // y: labelAttr.y,
122
        align: label.style.align,
123 124
        verticalAlign: label.style.verticalAlign,
        labelLinePoints: cloneArr(labelLine && labelLine.shape.points)
125 126 127
    };
}

128
const LABEL_OPTION_TO_STYLE_KEYS = ['align', 'verticalAlign', 'width', 'height', 'fontSize'] as const;
129

130
const dummyTransformable = new Transformable();
131

132
const labelLayoutInnerStore = makeInner<{
P
pissang 已提交
133 134 135 136
    oldLayout: {
        x: number,
        y: number,
        rotation: number
137 138 139 140 141 142 143 144 145 146
    },
    oldLayoutSelect?: {
        x?: number,
        y?: number,
        rotation?: number
    },
    oldLayoutEmphasis?: {
        x?: number,
        y?: number,
        rotation?: number
147 148
    },

149
    needsUpdateLabelLine?: boolean
P
pissang 已提交
150 151 152 153 154 155 156 157
}, ZRText>();

const labelLineAnimationStore = makeInner<{
    oldLayout: {
        points: number[][]
    }
}, Polyline>();

P
pissang 已提交
158 159 160 161 162
type LabelLineOptionMixin = {
    labelLine: LabelLineOption,
    emphasis: { labelLine: LabelLineOption }
};

163 164 165 166 167 168 169 170 171 172 173
function extendWithKeys(target: Dictionary<any>, source: Dictionary<any>, keys: string[]) {
    for (let i = 0; i < keys.length; i++) {
        const key = keys[i];
        if (source[key] != null) {
            target[key] = source[key];
        }
    }
}

const LABEL_LAYOUT_PROPS = ['x', 'y', 'rotation'];

174
class LabelManager {
175

176
    private _labelList: LabelDesc[] = [];
P
pissang 已提交
177
    private _chartViewList: ChartView[] = [];
178 179 180 181 182

    constructor() {}

    clearLabels() {
        this._labelList = [];
P
pissang 已提交
183
        this._chartViewList = [];
184 185 186 187 188
    }

    /**
     * Add label to manager
     */
189
    private _addLabel(
190
        dataIndex: number,
P
pissang 已提交
191
        dataType: string,
P
pissang 已提交
192
        seriesModel: SeriesModel,
193
        label: ZRText,
194
        layoutOption: LabelDesc['layoutOption']
195
    ) {
196 197 198 199
        const labelStyle = label.style;
        const hostEl = label.__hostTarget;
        const textConfig = hostEl.textConfig || {};

200
        // TODO: If label is in other state.
201 202 203 204
        const labelTransform = label.getComputedTransform();
        const labelRect = label.getBoundingRect().plain();
        BoundingRect.applyTransform(labelRect, labelRect, labelTransform);

205 206 207 208 209 210 211 212 213
        if (labelTransform) {
            dummyTransformable.setLocalTransform(labelTransform);
        }
        else {
            // Identity transform.
            dummyTransformable.x = dummyTransformable.y = dummyTransformable.rotation =
                dummyTransformable.originX = dummyTransformable.originY = 0;
            dummyTransformable.scaleX = dummyTransformable.scaleY = 1;
        }
214 215 216 217 218 219 220 221 222

        const host = label.__hostTarget;
        let hostRect;
        if (host) {
            hostRect = host.getBoundingRect().plain();
            const transform = host.getComputedTransform();
            BoundingRect.applyTransform(hostRect, hostRect, transform);
        }

223 224
        const labelGuide = hostRect && host.getTextGuideLine();

225
        this._labelList.push({
226
            label,
P
pissang 已提交
227
            labelLine: labelGuide,
228

P
pissang 已提交
229
            seriesModel,
230
            dataIndex,
P
pissang 已提交
231
            dataType,
232

233
            layoutOption,
234
            computedLayoutOption: null,
235

236 237 238 239 240 241 242 243 244
            hostRect,

            // Label with lower priority will be hidden when overlapped
            // Use rect size as default priority
            priority: hostRect ? hostRect.width * hostRect.height : 0,

            // Save default label attributes.
            // For restore if developers want get back to default value in callback.
            defaultAttr: {
245
                ignore: label.ignore,
246
                labelGuideIgnore: labelGuide && labelGuide.ignore,
247

248 249 250 251 252 253
                x: dummyTransformable.x,
                y: dummyTransformable.y,
                rotation: dummyTransformable.rotation,

                rect: labelRect,

254 255 256 257 258 259 260
                style: {
                    x: labelStyle.x,
                    y: labelStyle.y,

                    align: labelStyle.align,
                    verticalAlign: labelStyle.verticalAlign,
                    width: labelStyle.width,
261 262 263
                    height: labelStyle.height,

                    fontSize: labelStyle.fontSize
264
                },
265

266 267
                cursor: label.cursor,

268 269 270
                attachedPos: textConfig.position,
                attachedRot: textConfig.rotation
            }
271 272 273 274
        });
    }

    addLabelsOfSeries(chartView: ChartView) {
P
pissang 已提交
275 276
        this._chartViewList.push(chartView);

277
        const seriesModel = chartView.__model;
278

279
        const layoutOption = seriesModel.get('labelLayout');
280 281 282 283

        /**
         * Ignore layouting if it's not specified anything.
         */
284
        if (!(isFunction(layoutOption) || keys(layoutOption).length)) {
285 286 287
            return;
        }

288 289 290 291 292 293 294
        chartView.group.traverse((child) => {
            if (child.ignore) {
                return true;    // Stop traverse descendants.
            }

            // Only support label being hosted on graphic elements.
            const textEl = child.getTextContent();
P
pissang 已提交
295 296
            const ecData = getECData(child);
            const dataIndex = ecData.dataIndex;
P
pissang 已提交
297
            // Can only attach the text on the element with dataIndex
298
            if (textEl && dataIndex != null && !(textEl as ECElement).disableLabelLayout) {
P
pissang 已提交
299
                this._addLabel(dataIndex, ecData.dataType, seriesModel, textEl, layoutOption);
300 301 302 303 304 305 306
            }
        });
    }

    updateLayoutConfig(api: ExtensionAPI) {
        const width = api.getWidth();
        const height = api.getHeight();
P
pissang 已提交
307 308 309 310 311 312

        function createDragHandler(el: Element, labelLineModel: Model) {
            return function () {
                updateLabelLinePoints(el, labelLineModel);
            };
        }
313 314 315 316
        for (let i = 0; i < this._labelList.length; i++) {
            const labelItem = this._labelList[i];
            const label = labelItem.label;
            const hostEl = label.__hostTarget;
317
            const defaultLabelAttr = labelItem.defaultAttr;
318
            let layoutOption;
319
            // TODO A global layout option?
320 321
            if (typeof labelItem.layoutOption === 'function') {
                layoutOption = labelItem.layoutOption(
322
                    prepareLayoutCallbackParams(labelItem, hostEl)
323 324 325 326 327 328 329
                );
            }
            else {
                layoutOption = labelItem.layoutOption;
            }

            layoutOption = layoutOption || {};
330
            labelItem.computedLayoutOption = layoutOption;
P
pissang 已提交
331

332
            const degreeToRadian = Math.PI / 180;
333 334
            if (hostEl) {
                hostEl.setTextConfig({
335 336
                    // Force to set local false.
                    local: false,
337 338 339 340
                    // Ignore position and rotation config on the host el if x or y is changed.
                    position: (layoutOption.x != null || layoutOption.y != null)
                        ? null : defaultLabelAttr.attachedPos,
                    // Ignore rotation config on the host el if rotation is changed.
341 342
                    rotation: layoutOption.rotate != null
                        ? layoutOption.rotate * degreeToRadian : defaultLabelAttr.attachedRot,
343 344 345
                    offset: [layoutOption.dx || 0, layoutOption.dy || 0]
                });
            }
346
            let needsUpdateLabelLine = false;
347 348 349
            if (layoutOption.x != null) {
                // TODO width of chart view.
                label.x = parsePercent(layoutOption.x, width);
350
                label.setStyle('x', 0);  // Ignore movement in style. TODO: origin.
351
                needsUpdateLabelLine = true;
352 353 354 355 356
            }
            else {
                label.x = defaultLabelAttr.x;
                label.setStyle('x', defaultLabelAttr.style.x);
            }
357

358 359 360 361
            if (layoutOption.y != null) {
                // TODO height of chart view.
                label.y = parsePercent(layoutOption.y, height);
                label.setStyle('y', 0);  // Ignore movement in style.
362
                needsUpdateLabelLine = true;
363 364 365 366 367
            }
            else {
                label.y = defaultLabelAttr.y;
                label.setStyle('y', defaultLabelAttr.style.y);
            }
368 369 370 371 372 373 374 375

            if (layoutOption.labelLinePoints) {
                const guideLine = hostEl.getTextGuideLine();
                if (guideLine) {
                    guideLine.setShape({ points: layoutOption.labelLinePoints });
                    // Not update
                    needsUpdateLabelLine = false;
                }
376
            }
377

378 379 380
            const labelLayoutStore = labelLayoutInnerStore(label);
            labelLayoutStore.needsUpdateLabelLine = needsUpdateLabelLine;

381 382
            label.rotation = layoutOption.rotate != null
                ? layoutOption.rotate * degreeToRadian : defaultLabelAttr.rotation;
383 384 385

            for (let k = 0; k < LABEL_OPTION_TO_STYLE_KEYS.length; k++) {
                const key = LABEL_OPTION_TO_STYLE_KEYS[k];
P
pissang 已提交
386
                label.setStyle(key, layoutOption[key] != null ? layoutOption[key] : defaultLabelAttr.style[key]);
387 388
            }

389

P
pissang 已提交
390 391 392 393 394 395 396 397 398 399 400 401
            if (layoutOption.draggable) {
                label.draggable = true;
                label.cursor = 'move';
                if (hostEl) {
                    const data = labelItem.seriesModel.getData(labelItem.dataType);
                    const itemModel = data.getItemModel<LabelLineOptionMixin>(labelItem.dataIndex);
                    label.on('drag', createDragHandler(hostEl, itemModel.getModel('labelLine')));
                }
            }
            else {
                // TODO Other drag functions?
                label.off('drag');
402
                label.cursor = defaultLabelAttr.cursor;
P
pissang 已提交
403
            }
404 405 406
        }
    }

407 408 409
    layout(api: ExtensionAPI) {
        const width = api.getWidth();
        const height = api.getHeight();
410

411 412 413 414 415 416
        const labelList = prepareLayoutList(this._labelList);
        const labelsNeedsAdjustOnX = filter(labelList, function (item) {
            return item.layoutOption.moveOverlap === 'shift-x';
        });
        const labelsNeedsAdjustOnY = filter(labelList, function (item) {
            return item.layoutOption.moveOverlap === 'shift-y';
417 418
        });

419 420
        shiftLayoutOnX(labelsNeedsAdjustOnX, 0, width);
        shiftLayoutOnY(labelsNeedsAdjustOnY, 0, height);
421

422 423 424
        const labelsNeedsHideOverlap = filter(labelList, function (item) {
            return item.layoutOption.hideOverlap;
        });
425

426
        hideOverlap(labelsNeedsHideOverlap);
427
    }
P
pissang 已提交
428

P
pissang 已提交
429 430 431 432 433
    /**
     * Process all labels. Not only labels with layoutOption.
     */
    processLabelsOverall() {
        each(this._chartViewList, (chartView) => {
P
pissang 已提交
434
            const seriesModel = chartView.__model;
P
pissang 已提交
435
            const ignoreLabelLineUpdate = chartView.ignoreLabelLineUpdate;
436
            const animationEnabled = seriesModel.isAnimationEnabled();
P
pissang 已提交
437

438 439 440 441
            chartView.group.traverse((child) => {
                if (child.ignore) {
                    return true;    // Stop traverse descendants.
                }
P
pissang 已提交
442

443 444 445
                let needsUpdateLabelLine = !ignoreLabelLineUpdate;
                const label = child.getTextContent();
                if (!needsUpdateLabelLine && label) {
446
                    needsUpdateLabelLine = labelLayoutInnerStore(label).needsUpdateLabelLine;
447 448
                }
                if (needsUpdateLabelLine) {
P
pissang 已提交
449
                    this._updateLabelLine(child, seriesModel);
450
                }
451

452
                if (animationEnabled) {
P
pissang 已提交
453
                    this._animateLabels(child, seriesModel);
454 455
                }
            });
P
pissang 已提交
456 457
        });
    }
P
pissang 已提交
458 459 460 461 462 463 464 465 466 467

    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);
P
pissang 已提交
468
            const itemModel = data.getItemModel<LabelLineOptionMixin>(dataIndex);
P
pissang 已提交
469 470 471 472 473 474 475 476 477

            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');

P
pissang 已提交
478
            setLabelLineStyle(el, getLabelLineStatesModels(itemModel), defaultStyle);
P
pissang 已提交
479 480 481 482 483 484 485 486 487

            updateLabelLinePoints(el, labelLineModel);
        }
    }

    private _animateLabels(el: Element, seriesModel: SeriesModel) {
        const textEl = el.getTextContent();
        const guideLine = el.getTextGuideLine();
        // Animate
488
        if (textEl && !textEl.ignore && !textEl.invisible && !(el as ECElement).disableLabelAnimation) {
489
            const layoutStore = labelLayoutInnerStore(textEl);
P
pissang 已提交
490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506
            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);
507 508 509 510 511 512 513 514 515 516 517

                // Make sure the animation from is in the right status.
                const prevStates = el.prevStates;
                if (prevStates) {
                    if (indexOf(prevStates, 'select') >= 0) {
                        textEl.attr(layoutStore.oldLayoutSelect);
                    }
                    if (indexOf(prevStates, 'emphasis') >= 0) {
                        textEl.attr(layoutStore.oldLayoutEmphasis);
                    }
                }
P
pissang 已提交
518 519 520
                updateProps(textEl, newProps, seriesModel);
            }
            layoutStore.oldLayout = newProps;
521 522 523 524 525 526 527 528 529 530 531 532

            if (textEl.states.select) {
                const layoutSelect = layoutStore.oldLayoutSelect = {};
                extendWithKeys(layoutSelect, newProps, LABEL_LAYOUT_PROPS);
                extendWithKeys(layoutSelect, textEl.states.select, LABEL_LAYOUT_PROPS);
            }

            if (textEl.states.emphasis) {
                const layoutEmphasis = layoutStore.oldLayoutEmphasis = {};
                extendWithKeys(layoutEmphasis, newProps, LABEL_LAYOUT_PROPS);
                extendWithKeys(layoutEmphasis, textEl.states.emphasis, LABEL_LAYOUT_PROPS);
            }
P
pissang 已提交
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555
        }

        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;
        }
    }
556 557 558 559
}


export default LabelManager;