states.ts 16.8 KB
Newer Older
1 2 3 4 5 6 7 8
import ZRText from 'zrender/src/graphic/Text';
import { Dictionary } from 'zrender/src/core/types';
import LRU from 'zrender/src/core/LRU';
import Displayable, { DisplayableState } from 'zrender/src/graphic/Displayable';
import { PatternObject } from 'zrender/src/graphic/Pattern';
import { GradientObject } from 'zrender/src/graphic/Gradient';
import Element, { ElementEvent } from 'zrender/src/Element';
import Model from '../model/Model';
9 10
import { DisplayState, ECElement, ColorString, BlurScope, InnerFocus } from './types';
import { extend, indexOf, isArrayLike, isObject, keys } from 'zrender/src/core/util';
11 12 13 14 15 16
import {
    Z2_EMPHASIS_LIFT,
    getECData,
    _highlightKeyMap
} from './graphic';
import * as colorTool from 'zrender/src/tool/color';
17
import { EChartsType } from '../echarts';
18
import List from '../data/List';
19 20
import SeriesModel from '../model/Series';
import { CoordinateSystemMaster, CoordinateSystem } from '../coord/CoordinateSystem';
21 22 23 24

// Reserve 0 as default.
export let _highlightNextDigit = 1;

25 26 27 28
export const HOVER_STATE_NORMAL: 0 = 0;
export const HOVER_STATE_BLUR: 1 = 1;
export const HOVER_STATE_EMPHASIS: 2 = 2;

P
pissang 已提交
29 30 31
export const SPECIAL_STATES = ['emphasis', 'blur', 'select'] as const;
export const DISPLAY_STATES = ['normal', 'emphasis', 'blur', 'select'] as const;

32 33 34 35 36 37 38 39 40 41
type ExtendedProps = {
    __highByOuter: number

    __highDownSilentOnTouch: boolean

    __highDownDispatcher: boolean
};
type ExtendedElement = Element & ExtendedProps;
type ExtendedDisplayable = Displayable & ExtendedProps;

42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
function hasFillOrStroke(fillOrStroke: string | PatternObject | GradientObject) {
    return fillOrStroke != null && fillOrStroke !== 'none';
}
// Most lifted color are duplicated.
const liftedColorCache = new LRU<string>(100);
function liftColor(color: string): string {
    if (typeof color !== 'string') {
        return color;
    }
    let liftedColor = liftedColorCache.get(color);
    if (!liftedColor) {
        liftedColor = colorTool.lift(color, -0.1);
        liftedColorCache.put(color, liftedColor);
    }
    return liftedColor;
}

59 60 61 62 63 64 65
function triggerStateChange(el: ECElement, stateEnum: 0 | 1 | 2, stateName: DisplayState) {
    if ((el.hoverState || 0) !== stateEnum) {
        el.onStateChange && el.onStateChange(stateName);
    }
}

function singleEnterEmphasis(el: ECElement) {
66 67
    // Only mark the flag.
    // States will be applied in the echarts.ts in next frame.
68 69
    triggerStateChange(el, HOVER_STATE_EMPHASIS, 'emphasis');
    el.hoverState = HOVER_STATE_EMPHASIS;
70 71
}

72
function singleLeaveEmphasis(el: ECElement) {
73 74
    // Only mark the flag.
    // States will be applied in the echarts.ts in next frame.
75 76
    triggerStateChange(el, HOVER_STATE_NORMAL, 'normal');
    el.hoverState = HOVER_STATE_NORMAL;
77 78
}

79 80 81 82 83 84 85 86 87 88
function singleEnterBlur(el: ECElement) {
    triggerStateChange(el, HOVER_STATE_BLUR, 'blur');
    el.hoverState = HOVER_STATE_BLUR;
}

function singleLeaveBlur(el: ECElement) {
    triggerStateChange(el, HOVER_STATE_NORMAL, 'normal');
    el.hoverState = HOVER_STATE_NORMAL;
}

89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
function updateElementState<T>(
    el: ExtendedElement,
    updater: (this: void, el: Element, commonParam?: T) => void,
    commonParam?: T
) {
    updater(el, commonParam);
}

function traverseUpdateState<T>(
    el: ExtendedElement,
    updater: (this: void, el: Element, commonParam?: T) => void,
    commonParam?: T
) {
    updateElementState(el, updater, commonParam);
    el.isGroup && el.traverse(function (child: ExtendedElement) {
        updateElementState(child, updater, commonParam);
    });
}
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123

export function setStatesFlag(el: ECElement, stateName: DisplayState) {
    switch (stateName) {
        case 'emphasis':
            el.hoverState = HOVER_STATE_EMPHASIS;
            break;
        case 'normal':
            el.hoverState = HOVER_STATE_NORMAL;
            break;
        case 'blur':
            el.hoverState = HOVER_STATE_BLUR;
            break;
        case 'select':
            el.selected = true;
    }
}

124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
/**
 * If we reuse elements when rerender.
 * DONT forget to clearStates before we update the style and shape.
 * Or we may update on the wrong state instead of normal state.
 */
export function clearStates(el: Element) {
    if (el.isGroup) {
        el.traverse(function (child) {
            child.clearStates();
        });
    }
    else {
        el.clearStates();
    }
}
139 140 141 142 143 144 145

function createEmphasisDefaultState(
    el: Displayable,
    stateName: 'emphasis',
    state: Displayable['states'][number]
) {
    const hasEmphasis = indexOf(el.currentStates, stateName) >= 0;
146
    let cloned = false;
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
    if (!(el instanceof ZRText)) {
        const currentFill = el.style.fill;
        const currentStroke = el.style.stroke;
        if (currentFill || currentStroke) {
            let fromState: {
                fill: ColorString;
                stroke: ColorString;
            };
            if (!hasEmphasis) {
                fromState = { fill: currentFill, stroke: currentStroke };
                for (let i = 0; i < el.animators.length; i++) {
                    const animator = el.animators[i];
                    if (animator.__fromStateTransition
                        // Dont consider the animation to emphasis state.
                        && animator.__fromStateTransition.indexOf(stateName) < 0
                        && animator.targetName === 'style') {
                        animator.saveFinalToTarget(fromState, ['fill', 'stroke']);
164 165
                    }
                }
166 167 168 169 170 171 172 173 174 175 176 177 178 179
            }
            state = state || {};
            // Apply default color lift
            let emphasisStyle = state.style || {};
            if (!hasFillOrStroke(emphasisStyle.fill)) {
                cloned = true;
                // Not modify the original value.
                state = extend({}, state);
                emphasisStyle = extend({}, emphasisStyle);
                // Already being applied 'emphasis'. DON'T lift color multiple times.
                emphasisStyle.fill = hasEmphasis ? currentFill : liftColor(fromState.fill);
            }
            if (!hasFillOrStroke(emphasisStyle.stroke)) {
                if (!cloned) {
180 181 182
                    state = extend({}, state);
                    emphasisStyle = extend({}, emphasisStyle);
                }
183
                emphasisStyle.stroke = hasEmphasis ? currentStroke : liftColor(fromState.stroke);
184
            }
185
            state.style = emphasisStyle;
186
        }
187 188 189
    }
    if (state) {
        // TODO Share with textContent?
190 191 192 193 194 195 196
        if (state.z2 == null) {
            if (!cloned) {
                state = extend({}, state);
            }
            const z2EmphasisLift = (el as ECElement).z2EmphasisLift;
            state.z2 = el.z2 + (z2EmphasisLift != null ? z2EmphasisLift : Z2_EMPHASIS_LIFT);
        }
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
    }
    return state;
}

function createBlurDefaultState(
    el: Displayable,
    stateName: 'blur',
    state: Displayable['states'][number]
) {
    const hasBlur = indexOf(el.currentStates, stateName) >= 0;
    const currentOpacity = el.style.opacity;
    const fromState = {
        opacity: currentOpacity == null ? 1 : currentOpacity
    };
    if (!hasBlur) {
        for (let i = 0; i < el.animators.length; i++) {
            const animator = el.animators[i];
            if (animator.__fromStateTransition
                // Dont consider the animation to emphasis state.
                && animator.__fromStateTransition.indexOf(stateName) < 0
                && animator.targetName === 'style') {
                animator.saveFinalToTarget(fromState, ['opacity']);
            }
        }
    }

    state = state || {};
    let blurStyle = state.style || {};
    if (blurStyle.opacity == null) {
        // clone state
        state = extend({}, state);
        blurStyle = extend({
            // Already being applied 'emphasis'. DON'T mul opacity multiple times.
            opacity: hasBlur ? currentOpacity : (fromState.opacity * 0.1)
        }, blurStyle);
        state.style = blurStyle;
    }

    return state;
}

function elementStateProxy(this: Displayable, stateName: string): DisplayableState {
    const state = this.states[stateName];
    if (this.style) {
        if (stateName === 'emphasis') {
            return createEmphasisDefaultState(this, stateName, state);
        }
        else if (stateName === 'blur') {
            return createBlurDefaultState(this, stateName, state);
246 247 248 249 250 251 252 253 254
        }
    }
    return state;
}
/**FI
 * Set hover style (namely "emphasis style") of element.
 * @param el Should not be `zrender/graphic/Group`.
 * @param focus 'self' | 'selfInSeries' | 'series'
 */
255
export function setDefaultStateProxy(el: Displayable) {
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
    el.stateProxy = elementStateProxy;
    const textContent = el.getTextContent();
    const textGuide = el.getTextGuideLine();
    if (textContent) {
        textContent.stateProxy = elementStateProxy;
    }
    if (textGuide) {
        textGuide.stateProxy = elementStateProxy;
    }
}

export function enterEmphasisWhenMouseOver(el: Element, e: ElementEvent) {
    !shouldSilent(el, e)
        // "emphasis" event highlight has higher priority than mouse highlight.
        && !(el as ExtendedElement).__highByOuter
        && traverseUpdateState((el as ExtendedElement), singleEnterEmphasis);
}

export function leaveEmphasisWhenMouseOut(el: Element, e: ElementEvent) {
    !shouldSilent(el, e)
        // "emphasis" event highlight has higher priority than mouse highlight.
        && !(el as ExtendedElement).__highByOuter
        && traverseUpdateState((el as ExtendedElement), singleLeaveEmphasis);
}

export function enterEmphasis(el: Element, highlightDigit?: number) {
    (el as ExtendedElement).__highByOuter |= 1 << (highlightDigit || 0);
    traverseUpdateState((el as ExtendedElement), singleEnterEmphasis);
}

export function leaveEmphasis(el: Element, highlightDigit?: number) {
    !((el as ExtendedElement).__highByOuter &= ~(1 << (highlightDigit || 0)))
        && traverseUpdateState((el as ExtendedElement), singleLeaveEmphasis);
}

function shouldSilent(el: Element, e: ElementEvent) {
    return (el as ExtendedElement).__highDownSilentOnTouch && e.zrByTouch;
}

295 296
export function toggleSeriesBlurStates(
    targetSeriesIndex: number,
297
    focus: InnerFocus,
298 299 300 301 302
    blurScope: BlurScope,
    ecIns: EChartsType,
    isBlur: boolean
) {
    if (targetSeriesIndex == null) {
303 304 305
        return;
    }

306 307 308
    const model = ecIns.getModel();
    blurScope = blurScope || 'coordinateSystem';

309
    if (!focus || focus === 'none') {
310 311
        return;
    }
312

313 314 315 316 317 318 319
    function leaveBlur(data: List, dataIndices: ArrayLike<number>) {
        for (let i = 0; i < dataIndices.length; i++) {
            const itemEl = data.getItemGraphicEl(dataIndices[i]);
            itemEl && traverseUpdateState(itemEl as ExtendedElement, singleLeaveBlur);
        }
    }

320 321 322 323 324 325
    const targetSeriesModel = model.getSeriesByIndex(targetSeriesIndex);
    let targetCoordSys: CoordinateSystemMaster | CoordinateSystem = targetSeriesModel.coordinateSystem;
    if (targetCoordSys && (targetCoordSys as CoordinateSystem).master) {
        targetCoordSys = (targetCoordSys as CoordinateSystem).master;
    }

326
    model.eachSeries(function (seriesModel) {
327 328 329 330 331
        const sameSeries = targetSeriesModel === seriesModel;
        let coordSys: CoordinateSystemMaster | CoordinateSystem = seriesModel.coordinateSystem;
        if (coordSys && (coordSys as CoordinateSystem).master) {
            coordSys = (coordSys as CoordinateSystem).master;
        }
332 333 334
        const sameCoordSys = coordSys && targetCoordSys
            ? coordSys === targetCoordSys
            : sameSeries;   // If there is no coordinate system. use sameSeries instead.
335
        if (!(
336 337 338
            // Not blur other series if blurScope series
            blurScope === 'series' && !sameSeries
            // Not blur other coordinate system if blurScope is coordinateSystem
339
          || blurScope === 'coordinateSystem' && !sameCoordSys
340 341
            // Not blur self series if focus is series.
          || focus === 'series' && sameSeries
342 343 344 345
          // TODO blurScope: coordinate system
        )) {
            const view = ecIns.getViewOfSeriesModel(seriesModel);
            view.group.traverse(function (child) {
346
                isBlur ? singleEnterBlur(child) : singleLeaveBlur(child);
347
            });
348

349 350 351 352 353 354 355 356 357
            if (isBlur) {
                if (isArrayLike(focus)) {
                    leaveBlur(seriesModel.getData(), focus as ArrayLike<number>);
                }
                else if (isObject(focus)) {
                    const dataTypes = keys(focus);
                    for (let d = 0; d < dataTypes.length; d++) {
                        leaveBlur(seriesModel.getData(dataTypes[d]), focus[dataTypes[d]]);
                    }
358 359
                }
            }
360
        }
361 362 363
    });
}

364 365 366
/**
 * Enable the function that mouseover will trigger the emphasis state.
 *
367 368 369
 * NOTE:
 * This function should be used on the element with dataIndex, seriesIndex.
 *
370
 */
371
export function enableHoverEmphasis(el: Element, focus?: InnerFocus, blurScope?: BlurScope) {
372
    setAsHighDownDispatcher(el, true);
373
    traverseUpdateState(el as ExtendedElement, setDefaultStateProxy);
374 375 376 377

    enableHoverFocus(el, focus, blurScope);
}

378
export function enableHoverFocus(el: Element, focus: InnerFocus, blurScope: BlurScope) {
379 380
    if (focus != null) {
        const ecData = getECData(el);
381 382 383 384 385 386 387 388 389 390
        // TODO dataIndex may be set after this function. This check is not useful.
        // if (ecData.dataIndex == null) {
        //     if (__DEV__) {
        //         console.warn('focus can only been set on element with dataIndex');
        //     }
        // }
        // else {
        ecData.focus = focus;
        ecData.blurScope = blurScope;
        // }
391
    }
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
}

const OTHER_STATES = ['emphasis', 'blur', 'select'] as const;
const styleGetterMap: Dictionary<'getItemStyle' | 'getLineStyle' | 'getAreaStyle'> = {
    itemStyle: 'getItemStyle',
    lineStyle: 'getLineStyle',
    areaStyle: 'getAreaStyle'
};
/**
 * Set emphasis/blur/selected states of element.
 */
export function setStatesStylesFromModel(
    el: Displayable,
    itemModel: Model<Partial<Record<'emphasis' | 'blur' | 'select', any>>>,
    styleType?: string, // default itemStyle
    getterType?: 'getItemStyle' | 'getLineStyle' | 'getAreaStyle'
) {
    styleType = styleType || 'itemStyle';
    for (let i = 0; i < OTHER_STATES.length; i++) {
        const stateName = OTHER_STATES[i];
        const model = itemModel.getModel([stateName, styleType]);
        const state = el.ensureState(stateName);
        // Let it throw error if getterType is not found.
        state.style = model[getterType || styleGetterMap[styleType]]();
    }
}

419

420
/**
421 422
 * @parame el
 * @param el.highDownSilentOnTouch
423 424 425 426 427 428 429 430 431
 *        In touch device, mouseover event will be trigger on touchstart event
 *        (see module:zrender/dom/HandlerProxy). By this mechanism, we can
 *        conveniently use hoverStyle when tap on touch screen without additional
 *        code for compatibility.
 *        But if the chart/component has select feature, which usually also use
 *        hoverStyle, there might be conflict between 'select-highlight' and
 *        'hover-highlight' especially when roam is enabled (see geo for example).
 *        In this case, `highDownSilentOnTouch` should be used to disable
 *        hover-highlight on touch device.
432
 * @param asDispatcher If `false`, do not set as "highDownDispatcher".
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 469 470
 */
export function setAsHighDownDispatcher(el: Element, asDispatcher: boolean) {
    const disable = asDispatcher === false;
    const extendedEl = el as ExtendedElement;
    // Make `highDownSilentOnTouch` and `onStateChange` only work after
    // `setAsHighDownDispatcher` called. Avoid it is modified by user unexpectedly.
    if ((el as ECElement).highDownSilentOnTouch) {
        extendedEl.__highDownSilentOnTouch = (el as ECElement).highDownSilentOnTouch;
    }
    // Simple optimize, since this method might be
    // called for each elements of a group in some cases.
    if (!disable || extendedEl.__highDownDispatcher) {
        // Emphasis, normal can be triggered manually by API or other components like hover link.
        // el[method]('emphasis', onElementEmphasisEvent)[method]('normal', onElementNormalEvent);
        // Also keep previous record.
        extendedEl.__highByOuter = extendedEl.__highByOuter || 0;
        extendedEl.__highDownDispatcher = !disable;
    }
}

export function isHighDownDispatcher(el: Element): boolean {
    return !!(el && (el as ExtendedDisplayable).__highDownDispatcher);
}

/**
 * Support hightlight/downplay record on each elements.
 * For the case: hover highlight/downplay (legend, visualMap, ...) and
 * user triggerred hightlight/downplay should not conflict.
 * Only all of the highlightDigit cleared, return to normal.
 * @param {string} highlightKey
 * @return {number} highlightDigit
 */
export function getHighlightDigit(highlightKey: number) {
    let highlightDigit = _highlightKeyMap[highlightKey];
    if (highlightDigit == null && _highlightNextDigit <= 32) {
        highlightDigit = _highlightKeyMap[highlightKey] = _highlightNextDigit++;
    }
    return highlightDigit;
471
}