LabelManager.ts 15.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 26 27
// TODO: move labels out of viewport.

import {
    OrientedBoundingRect,
    Text as ZRText,
    Point,
    BoundingRect,
    getECData,
P
pissang 已提交
28 29 30
    Polyline,
    updateProps,
    initProps
31
} from '../util/graphic';
32 33 34 35 36 37 38 39
import { MatrixArray } from 'zrender/src/core/matrix';
import ExtensionAPI from '../ExtensionAPI';
import {
    ZRTextAlign,
    ZRTextVerticalAlign,
    LabelLayoutOption,
    LabelLayoutOptionCallback,
    LabelLayoutOptionCallbackParams
40 41
} from '../util/types';
import { parsePercent } from '../util/number';
42
import ChartView from '../view/Chart';
P
pissang 已提交
43
import { ElementTextConfig } from 'zrender/src/Element';
44 45
import { RectLike } from 'zrender/src/core/BoundingRect';
import Transformable from 'zrender/src/core/Transformable';
46
import { updateLabelGuideLine } from './labelGuideHelper';
P
pissang 已提交
47 48 49
import SeriesModel from '../model/Series';
import { makeInner } from '../util/model';
import { retrieve2, guid, each } from 'zrender/src/core/util';
50 51 52 53 54 55 56 57 58 59

interface DisplayedLabelItem {
    label: ZRText
    rect: BoundingRect
    localRect: BoundingRect
    obb?: OrientedBoundingRect
    axisAligned: boolean
    transform: MatrixArray
}

60
interface LabelLayoutDesc {
61
    label: ZRText
62 63
    labelGuide: Polyline

P
pissang 已提交
64
    seriesModel: SeriesModel
65
    dataIndex: number
66

67 68 69 70
    layoutOption: LabelLayoutOptionCallback | LabelLayoutOption

    overlap: LabelLayoutOption['overlap']
    overlapMargin: LabelLayoutOption['overlapMargin']
71 72 73 74 75

    hostRect: RectLike
    priority: number

    defaultAttr: SavedLabelAttr
76 77 78
}

interface SavedLabelAttr {
79
    ignore: boolean
80
    labelGuideIgnore: boolean
81

82 83 84
    x: number
    y: number
    rotation: number
85

86 87 88 89 90 91 92 93 94
    style: {
        align: ZRTextAlign
        verticalAlign: ZRTextVerticalAlign
        width: number
        height: number

        x: number
        y: number
    }
95

96 97 98
    // Configuration in attached element
    attachedPos: ElementTextConfig['position']
    attachedRot: ElementTextConfig['rotation']
99

100 101 102 103 104 105
    rect: RectLike
}

function prepareLayoutCallbackParams(labelItem: LabelLayoutDesc): LabelLayoutOptionCallbackParams {
    const labelAttr = labelItem.defaultAttr;
    const label = labelItem.label;
106
    return {
107
        dataIndex: labelItem.dataIndex,
P
pissang 已提交
108
        seriesIndex: labelItem.seriesModel.seriesIndex,
109 110 111
        text: labelItem.label.style.text,
        rect: labelItem.hostRect,
        labelRect: labelAttr.rect,
112 113
        // x: labelAttr.x,
        // y: labelAttr.y,
114 115 116 117 118 119 120
        align: label.style.align,
        verticalAlign: label.style.verticalAlign
    };
}

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

121
const dummyTransformable = new Transformable();
122

P
pissang 已提交
123 124 125 126 127 128 129 130 131 132 133 134 135 136
const labelAnimationStore = makeInner<{
    oldLayout: {
        x: number,
        y: number,
        rotation: number
    }
}, ZRText>();

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

137
class LabelManager {
138

139
    private _labelList: LabelLayoutDesc[] = [];
P
pissang 已提交
140
    private _chartViewList: ChartView[] = [];
141 142 143 144 145

    constructor() {}

    clearLabels() {
        this._labelList = [];
P
pissang 已提交
146
        this._chartViewList = [];
147 148 149 150 151
    }

    /**
     * Add label to manager
     */
152 153
    addLabel(
        dataIndex: number,
P
pissang 已提交
154
        seriesModel: SeriesModel,
155 156 157
        label: ZRText,
        layoutOption: LabelLayoutDesc['layoutOption']
    ) {
158 159 160 161
        const labelStyle = label.style;
        const hostEl = label.__hostTarget;
        const textConfig = hostEl.textConfig || {};

162
        // TODO: If label is in other state.
163 164 165 166
        const labelTransform = label.getComputedTransform();
        const labelRect = label.getBoundingRect().plain();
        BoundingRect.applyTransform(labelRect, labelRect, labelTransform);

167 168 169 170 171 172 173 174 175
        if (labelTransform) {
            dummyTransformable.setLocalTransform(labelTransform);
        }
        else {
            // Identity transform.
            dummyTransformable.x = dummyTransformable.y = dummyTransformable.rotation =
                dummyTransformable.originX = dummyTransformable.originY = 0;
            dummyTransformable.scaleX = dummyTransformable.scaleY = 1;
        }
176 177 178 179 180 181 182 183 184

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

185 186
        const labelGuide = hostRect && host.getTextGuideLine();

187
        this._labelList.push({
188 189 190
            label,
            labelGuide: labelGuide,

P
pissang 已提交
191
            seriesModel,
192
            dataIndex,
193

194
            layoutOption,
195

196 197 198 199 200 201 202 203 204 205 206 207
            hostRect,

            overlap: 'hidden',
            overlapMargin: 0,

            // 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: {
208
                ignore: label.ignore,
209
                labelGuideIgnore: labelGuide && labelGuide.ignore,
210

211 212 213 214 215 216
                x: dummyTransformable.x,
                y: dummyTransformable.y,
                rotation: dummyTransformable.rotation,

                rect: labelRect,

217 218 219 220 221 222 223 224 225
                style: {
                    x: labelStyle.x,
                    y: labelStyle.y,

                    align: labelStyle.align,
                    verticalAlign: labelStyle.verticalAlign,
                    width: labelStyle.width,
                    height: labelStyle.height
                },
226 227 228 229

                attachedPos: textConfig.position,
                attachedRot: textConfig.rotation
            }
230
        });
231

232 233 234
    }

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

237 238 239 240 241 242 243 244 245 246 247
        const seriesModel = chartView.__model;
        const layoutOption = seriesModel.get('labelLayout');
        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 dataIndex = getECData(child).dataIndex;
            if (textEl && dataIndex != null) {
P
pissang 已提交
248
                this.addLabel(dataIndex, seriesModel, textEl, layoutOption);
249 250 251 252 253 254 255 256 257 258 259
            }
        });
    }

    updateLayoutConfig(api: ExtensionAPI) {
        const width = api.getWidth();
        const height = api.getHeight();
        for (let i = 0; i < this._labelList.length; i++) {
            const labelItem = this._labelList[i];
            const label = labelItem.label;
            const hostEl = label.__hostTarget;
260
            const defaultLabelAttr = labelItem.defaultAttr;
261
            let layoutOption;
262
            // TODO A global layout option?
263 264
            if (typeof labelItem.layoutOption === 'function') {
                layoutOption = labelItem.layoutOption(
265
                    prepareLayoutCallbackParams(labelItem)
266 267 268 269 270 271 272
                );
            }
            else {
                layoutOption = labelItem.layoutOption;
            }

            layoutOption = layoutOption || {};
P
pissang 已提交
273

274 275
            if (hostEl) {
                hostEl.setTextConfig({
276 277
                    // Force to set local false.
                    local: false,
278 279 280 281
                    // 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.
282
                    rotation: layoutOption.rotation != null ? layoutOption.rotation : defaultLabelAttr.attachedRot,
283 284 285
                    offset: [layoutOption.dx || 0, layoutOption.dy || 0]
                });
            }
286 287 288
            if (layoutOption.x != null) {
                // TODO width of chart view.
                label.x = parsePercent(layoutOption.x, width);
289
                label.setStyle('x', 0);  // Ignore movement in style. TODO: origin.
290 291 292 293 294
            }
            else {
                label.x = defaultLabelAttr.x;
                label.setStyle('x', defaultLabelAttr.style.x);
            }
295

296 297 298 299 300 301 302 303 304
            if (layoutOption.y != null) {
                // TODO height of chart view.
                label.y = parsePercent(layoutOption.y, height);
                label.setStyle('y', 0);  // Ignore movement in style.
            }
            else {
                label.y = defaultLabelAttr.y;
                label.setStyle('y', defaultLabelAttr.style.y);
            }
305 306 307 308 309 310

            label.rotation = layoutOption.rotation != null
                ? layoutOption.rotation : defaultLabelAttr.rotation;

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

            labelItem.overlap = layoutOption.overlap;
            labelItem.overlapMargin = layoutOption.overlapMargin;
316 317 318 319
        }
    }

    layout() {
320
        // TODO: sort by priority(area)
321 322 323 324 325
        const labelList = this._labelList;

        const displayedLabels: DisplayedLabelItem[] = [];
        const mvt = new Point();

326
        // TODO, render overflow visible first, put in the displayedLabels.
327 328 329 330
        labelList.sort(function (a, b) {
            return b.priority - a.priority;
        });

331 332
        for (let i = 0; i < labelList.length; i++) {
            const labelItem = labelList[i];
333 334 335 336
            if (labelItem.defaultAttr.ignore) {
                continue;
            }

337 338 339 340 341 342 343 344 345 346 347
            const label = labelItem.label;
            const transform = label.getComputedTransform();
            // NOTE: Get bounding rect after getComputedTransform, or label may not been updated by the host el.
            const localRect = label.getBoundingRect();
            const isAxisAligned = !transform || (transform[1] < 1e-5 && transform[2] < 1e-5);

            const globalRect = localRect.clone();
            globalRect.applyTransform(transform);

            let obb = isAxisAligned ? new OrientedBoundingRect(localRect, transform) : null;
            let overlapped = false;
348
            const overlapMargin = labelItem.overlapMargin || 0;
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
            const marginSqr = overlapMargin * overlapMargin;
            for (let j = 0; j < displayedLabels.length; j++) {
                const existsTextCfg = displayedLabels[j];
                // Fast rejection.
                if (!globalRect.intersect(existsTextCfg.rect, mvt) && mvt.lenSquare() > marginSqr) {
                    continue;
                }

                if (isAxisAligned && existsTextCfg.axisAligned) {   // Is overlapped
                    overlapped = true;
                    break;
                }

                if (!existsTextCfg.obb) { // If self is not axis aligned. But other is.
                    existsTextCfg.obb = new OrientedBoundingRect(existsTextCfg.localRect, existsTextCfg.transform);
                }

                if (!obb) { // If self is axis aligned. But other is not.
                    obb = new OrientedBoundingRect(localRect, transform);
                }

                if (obb.intersect(existsTextCfg.obb, mvt) || mvt.lenSquare() < marginSqr) {
                    overlapped = true;
                    break;
                }
            }

376
            const labelGuide = labelItem.labelGuide;
377
            // TODO Callback to determine if this overlap should be handled?
378
            if (overlapped) {
379
                label.hide();
380
                labelGuide && labelGuide.hide();
381 382
            }
            else {
383
                label.attr('ignore', labelItem.defaultAttr.ignore);
384
                labelGuide && labelGuide.attr('ignore', labelItem.defaultAttr.labelGuideIgnore);
385

386 387 388 389 390 391 392 393 394
                displayedLabels.push({
                    label,
                    rect: globalRect,
                    localRect,
                    obb,
                    axisAligned: isAxisAligned,
                    transform
                });
            }
395

396 397 398 399
            updateLabelGuideLine(
                label,
                globalRect,
                label.__hostTarget,
P
pissang 已提交
400 401
                labelItem.hostRect,
                labelItem.seriesModel.getModel(['labelLine'])
402 403
            );
        }
404
    }
P
pissang 已提交
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468

    animateLabels() {
        each(this._chartViewList, function (chartView) {
            const seriesModel = chartView.__model;
            if (!seriesModel.isAnimationEnabled()) {
                return;
            }

            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 (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;
                }
            });
        });
    }
469 470 471 472 473 474
}




export default LabelManager;