提交 b0d4a358 编写于 作者: 1 100pah

feature: [geo]

(1) Support component focus blur.
The option `emphasis.focus` can be specified to enable this feature. `emphasis.focus` only supports the value `'self'`.
The option `emphasis.focusScope` is not supported in component. That is, the focus scope can be only a single component itself. All of the elements in this component will be blurred.
Added `Component['focusBlurEnabled']` to enable component blur.
(2) Support component hover link when highlight with a given name.
There probably be multiple elements share one region name while those region elements has no common ancestor. hover link enables them to be highlighted together.
Implementation: if a component implements `Component['findHighDownDispatchers']`,  dispatcher elements by a given name will be found and this feature is enabled.
(3) Support component highlight/downplay be triggered by `dispatchAction`.
Implementation: if a component implements `Component['findHighDownDispatchers']`,  dispatcher elements by a given name will be found and this feature is enabled.
(4) Some refactor.
上级 9c2cb8eb
......@@ -18,7 +18,7 @@
*/
import * as zrUtil from 'zrender/src/core/util';
import SymbolDraw from '../helper/SymbolDraw';
import SymbolDraw, { ListForSymbolDraw } from '../helper/SymbolDraw';
import LineDraw from '../helper/LineDraw';
import RoamController, { RoamControllerHost } from '../../component/helper/RoamController';
import * as roamHelper from '../../component/helper/roamHelper';
......@@ -105,7 +105,7 @@ class GraphView extends ChartView {
adjustEdge(seriesModel.getGraph(), getNodeGlobalScale(seriesModel));
const data = seriesModel.getData();
symbolDraw.updateData(data);
symbolDraw.updateData(data as ListForSymbolDraw);
const edgeData = seriesModel.getEdgeData();
// TODO: TYPE
......
......@@ -32,7 +32,8 @@ import {
ZRStyleProps,
StatesOptionMixin,
BlurScope,
DisplayState
DisplayState,
DefaultEmphasisFocus
} from '../../util/types';
import { CoordinateSystemClipArea } from '../../coord/CoordinateSystem';
import Model from '../../model/Model';
......@@ -94,7 +95,7 @@ interface SymbolDrawStateOption {
export interface SymbolDrawItemModelOption extends SymbolOptionMixin<object>,
StatesOptionMixin<SymbolDrawStateOption, {
emphasis?: {
focus?: string
focus?: DefaultEmphasisFocus
scale?: boolean
}
}>,
......@@ -111,7 +112,7 @@ export interface SymbolDrawSeriesScope {
blurItemStyle?: ZRStyleProps
selectItemStyle?: ZRStyleProps
focus?: string
focus?: DefaultEmphasisFocus
blurScope?: BlurScope
symbolRotate?: ScatterSeriesOption['symbolRotate']
......@@ -148,7 +149,7 @@ function makeSeriesScope(data: List): SymbolDrawSeriesScope {
};
}
type ListForSymbolDraw = List<Model<SymbolDrawItemModelOption & AnimationOptionMixin>>;
export type ListForSymbolDraw = List<Model<SymbolDrawItemModelOption & AnimationOptionMixin>>;
class SymbolDraw {
group = new graphic.Group();
......
......@@ -146,12 +146,12 @@ class SunburstPiece extends graphic.Sector {
const focus = emphasisModel.get('focus');
const focusDataIndices: number[] = focus === 'ancestor'
? node.getAncestorsIndices()
: focus === 'descendant' ? node.getDescendantIndices() : null;
const focusOrIndices =
focus === 'ancestor' ? node.getAncestorsIndices()
: focus === 'descendant' ? node.getDescendantIndices()
: focus;
enableHoverEmphasis(this, focusDataIndices || focus, emphasisModel.get('blurScope'));
enableHoverEmphasis(this, focusOrIndices, emphasisModel.get('blurScope'));
}
_updateLabel(
......
......@@ -803,9 +803,10 @@ function renderNode(
const focus = nodeModel.get(['emphasis', 'focus']);
const blurScope = nodeModel.get(['emphasis', 'blurScope']);
const focusDataIndices: number[] = focus === 'ancestor'
? thisNode.getAncestorsIndices()
: focus === 'descendant' ? thisNode.getDescendantIndices() : null;
const focusOrIndices =
focus === 'ancestor' ? thisNode.getAncestorsIndices()
: focus === 'descendant' ? thisNode.getDescendantIndices()
: focus;
// No children, render content.
if (isParent) {
......@@ -820,7 +821,7 @@ function renderNode(
// Only for enabling highlight/downplay.
data.setItemGraphicEl(thisNode.dataIndex, bg);
enableHoverFocus(bg, focusDataIndices || focus, blurScope);
enableHoverFocus(bg, focusOrIndices, blurScope);
}
}
else {
......@@ -834,7 +835,7 @@ function renderNode(
// Only for enabling highlight/downplay.
data.setItemGraphicEl(thisNode.dataIndex, group);
enableHoverFocus(group, focusDataIndices || focus, blurScope);
enableHoverFocus(group, focusOrIndices, blurScope);
}
return group;
......
......@@ -26,6 +26,7 @@ import GeoModel from '../../coord/geo/GeoModel';
import { Payload, ZRElementEvent, ECEventData } from '../../util/types';
import { getECData } from '../../util/innerStore';
import { findEventDispatcher } from '../../util/event';
import Element from 'zrender/src/Element';
class GeoView extends ComponentView {
......@@ -38,6 +39,8 @@ class GeoView extends ComponentView {
private _model: GeoModel;
focusBlurEnabled = true;
init(ecModel: GlobalModel, api: ExtensionAPI) {
const mapDraw = new MapDraw(api);
this._mapDraw = mapDraw;
......@@ -94,6 +97,10 @@ class GeoView extends ComponentView {
});
}
findHighDownDispatchers(name: string): Element[] {
return this._mapDraw && this._mapDraw.findHighDownDispatchers(name, this._model);
}
dispose(): void {
this._mapDraw && this._mapDraw.remove();
}
......
......@@ -22,14 +22,19 @@ import RoamController from './RoamController';
import * as roamHelper from '../../component/helper/roamHelper';
import {onIrrelevantElement} from '../../component/helper/cursorHelper';
import * as graphic from '../../util/graphic';
import { enableHoverEmphasis, DISPLAY_STATES } from '../../util/states';
import {
enableHoverEmphasis,
DISPLAY_STATES,
enableComponentHighDownFeatures,
setDefaultStateProxy
} from '../../util/states';
import geoSourceManager from '../../coord/geo/geoSourceManager';
import {getUID} from '../../util/component';
import ExtensionAPI from '../../core/ExtensionAPI';
import GeoModel, { GeoCommonOptionMixin, GeoItemStyleOption } from '../../coord/geo/GeoModel';
import MapSeries from '../../chart/map/MapSeries';
import GlobalModel from '../../model/Global';
import { Payload, ECElement, LineStyleOption } from '../../util/types';
import { Payload, ECElement, LineStyleOption, InnerFocus } from '../../util/types';
import GeoView from '../geo/GeoView';
import MapView from '../../chart/map/MapView';
import Geo from '../../coord/geo/Geo';
......@@ -40,7 +45,7 @@ import { createOrUpdatePatternFromDecal } from '../../util/decal';
import { ViewCoordSysTransformInfoPart } from '../../coord/View';
import { GeoSVGGraphicRecord, GeoSVGResource } from '../../coord/geo/GeoSVGResource';
import Displayable from 'zrender/src/graphic/Displayable';
import { ElementTextConfig } from 'zrender/src/Element';
import Element, { ElementTextConfig } from 'zrender/src/Element';
import List from '../../data/List';
import { GeoJSONRegion } from '../../coord/geo/Region';
import { RegionGraphic } from '../../coord/geo/geoTypes';
......@@ -102,11 +107,13 @@ class MapDraw {
private _regionsGroup: RegionsGroup;
private _regionsGroupByName: zrUtil.HashMap<RegionsGroup>;
private _svgMapName: string;
private _svgGroup: graphic.Group;
private _svgRegionGraphics: GeoSVGGraphicRecord['regionGraphics'];
private _svgGraphicRecord: GeoSVGGraphicRecord;
constructor(api: ExtensionAPI) {
......@@ -189,7 +196,7 @@ class MapDraw {
}
private _buildGeoJSON(viewBuildCtx: ViewBuildContext): void {
const nameMap = zrUtil.createHashMap<RegionsGroup>();
const nameMap = this._regionsGroupByName = zrUtil.createHashMap<RegionsGroup>();
const regionsGroup = this._regionsGroup;
const transformInfoRaw = viewBuildCtx.transformInfoRaw;
......@@ -255,7 +262,7 @@ class MapDraw {
const regionGraphic: RegionGraphic = {
name: region.name,
el: compoundPath,
styleOptionKey: 'itemStyle',
optionStyleEnabled: true,
stateTrigger: regionGroup,
eventTrigger: regionGroup,
useLabel: true
......@@ -281,19 +288,50 @@ class MapDraw {
this._useSVG(mapName);
}
zrUtil.each(this._svgRegionGraphics, function (regionGraphic) {
let focusSelf = false;
zrUtil.each(this._svgGraphicRecord.regionGraphics, function (regionGraphic) {
// Note that we also allow different elements have the same name.
// For example, a glyph of a city and the label of the city have
// the same name and their tooltip info can be defined in a single
// region option.
this._resetSingleRegionGraphic(
const focus = this._resetSingleRegionGraphic(
viewBuildCtx, regionGraphic, [0, 0], 'inside',
// We do not know how the SVG like so we'd better not to change z2.
// Otherwise it might bring some unexpected result. For example,
// an area hovered that make some inner city can not be clicked.
true
);
if (focus === 'self') {
focusSelf = true;
}
}, this);
// It's a little complicated to support blurring the entire geoSVG in series-map.
// So do not suport it until some requirements come.
// At present, in series-map, only regions can be blurred.
if (focusSelf && viewBuildCtx.isGeo) {
const blurStyle = (viewBuildCtx.mapOrGeoModel as GeoModel).getModel(['blur', 'itemStyle']).getItemStyle();
// Only suport `opacity` here. Because not sure that other props are suitable for
// all of the elements generated by SVG (especially for Text/TSpan/Image/... ).
const opacity = blurStyle.opacity;
this._svgGraphicRecord.root.traverse(el => {
if (!el.isGroup) {
// PENDING: clear those settings to SVG elements when `_freeSVG`.
// (Currently it happen not to be needed.)
setDefaultStateProxy(el as Displayable);
const style = (el as Displayable).ensureState('blur').style || {};
// Do not overwrite the region style that already set from region option.
if (style.opacity == null && opacity != null) {
style.opacity = opacity;
}
// If opacity not set, but `ensureState('blur').style` set, there will
// be default opacity.
// Enable `stateTransition` (animation).
(el as Displayable).ensureState('emphasis');
}
});
}
}
private _resetSingleRegionGraphic(
......@@ -302,48 +340,21 @@ class MapDraw {
labelXY: number[],
labelPosition: ElementTextConfig['position'],
noZ2EmphasisLift: boolean
): void {
): InnerFocus {
const regionName = regionGraphic.name;
const mapOrGeoModel = viewBuildCtx.mapOrGeoModel;
const data = viewBuildCtx.data;
const isVisualEncodedByVisualMap = viewBuildCtx.isVisualEncodedByVisualMap;
const isGeo = viewBuildCtx.isGeo;
const dataIdx = data ? data.indexOfName(regionName) : null;
const regionModel = mapOrGeoModel.getRegionModel(regionName);
const styles = makeStyleForRegion(regionGraphic.styleOptionKey, regionModel);
// Use the itemStyle in data if has data
if (styles && styles.styleOptionKey === 'itemStyle' && data) {
// Only visual color of each item will be used. It can be encoded by visualMap
// But visual color of series is used in symbol drawing
// Visual color for each series is for the symbol draw
const style = data.getItemVisual(dataIdx, 'style');
const decal = data.getItemVisual(dataIdx, 'decal');
if (isVisualEncodedByVisualMap && style.fill) {
styles.normal.fill = style.fill;
}
if (decal) {
styles.normal.decal = createOrUpdatePatternFromDecal(decal, viewBuildCtx.api);
}
}
// PENDING: SVG text, tspan and image can be named but not supporeted
// to be styled by region option yet.
if (styles && regionGraphic.el instanceof graphic.Path) {
regionGraphic.el.setStyle(styles.normal);
regionGraphic.el.style.strokeNoScale = true;
regionGraphic.el.ensureState('emphasis').style = styles.emphasis;
regionGraphic.el.ensureState('select').style = styles.select;
regionGraphic.el.ensureState('blur').style = styles.blur;
}
applyOptionStyleForRegion(viewBuildCtx, regionGraphic, dataIdx, regionModel);
if (regionGraphic.el instanceof Displayable) {
regionGraphic.el.culling = true;
}
if (noZ2EmphasisLift) {
(regionGraphic.el as ECElement).z2EmphasisLift = 0;
}
......@@ -455,25 +466,53 @@ class MapDraw {
});
}
if (regionGraphic.stateTrigger) {
let focus;
const stateTrigger = regionGraphic.stateTrigger;
if (stateTrigger) {
// @ts-ignore FIXME:TS fix the "compatible with each other"?
regionGraphic.stateTrigger.highDownSilentOnTouch = !!mapOrGeoModel.get('selectedMode');
stateTrigger.highDownSilentOnTouch = !!mapOrGeoModel.get('selectedMode');
// @ts-ignore FIXME:TS fix the "compatible with each other"?
const emphasisModel = regionModel.getModel('emphasis');
focus = emphasisModel.get('focus');
enableHoverEmphasis(
regionGraphic.stateTrigger, emphasisModel.get('focus'), emphasisModel.get('blurScope')
stateTrigger, focus, emphasisModel.get('blurScope')
);
if (isGeo) {
enableComponentHighDownFeatures(stateTrigger, mapOrGeoModel as GeoModel, regionName);
}
}
return focus;
}
remove(): void {
this._regionsGroup.removeAll();
this._regionsGroupByName = null;
this._svgGroup.removeAll();
this._controller.dispose();
this._freeSVG();
this._controller.dispose();
this._controllerHost = null;
}
findHighDownDispatchers(name: string, geoModel: GeoModel): Element[] {
if (name == null) {
return [];
}
const geo = geoModel.coordinateSystem;
if (geo.resourceType === 'geoJSON') {
const regionsGroupByName = this._regionsGroupByName;
if (regionsGroupByName) {
const regionGroup = regionsGroupByName.get(name);
return regionGroup ? [regionGroup] : [];
}
}
else if (geo.resourceType === 'geoSVG') {
return this._svgGraphicRecord.regionElementMap.get(name) || [];
}
}
private _svgResourceChanged(mapName: string): boolean {
return this._svgMapName !== mapName;
}
......@@ -483,7 +522,7 @@ class MapDraw {
if (resource && resource.type === 'geoSVG') {
const svgGraphic = (resource as GeoSVGResource).useGraphic(this.uid);
this._svgGroup.add(svgGraphic.root);
this._svgRegionGraphics = svgGraphic.regionGraphics;
this._svgGraphicRecord = svgGraphic;
this._svgMapName = mapName;
}
}
......@@ -493,11 +532,12 @@ class MapDraw {
if (mapName == null) {
return;
}
const resource = geoSourceManager.getGeoResource(mapName);
if (resource && resource.type === 'geoSVG') {
(resource as GeoSVGResource).freeGraphic(this.uid);
}
this._svgRegionGraphics = null;
this._svgGraphicRecord = null;
this._svgGroup.removeAll();
this._svgMapName = null;
}
......@@ -599,8 +639,10 @@ function labelTextAfterUpdate(this: graphic.Text) {
m[3] /= scaleY;
}
function makeStyleForRegion(
styleOptionKey: RegionGraphic['styleOptionKey'],
function applyOptionStyleForRegion(
viewBuildCtx: ViewBuildContext,
regionGraphic: RegionGraphic,
dataIndex: number,
regionModel: Model<
GeoStyleableOption & {
emphasis?: GeoStyleableOption;
......@@ -608,48 +650,59 @@ function makeStyleForRegion(
blur?: GeoStyleableOption;
}
>
): {
styleOptionKey: 'itemStyle';
normal: ItemStyleProps;
emphasis: ItemStyleProps;
select: ItemStyleProps;
blur: ItemStyleProps;
} | {
styleOptionKey: 'lineStyle';
normal: LineStyleProps;
emphasis: LineStyleProps;
select: LineStyleProps;
blur: LineStyleProps;
} {
if (!styleOptionKey) {
): void {
if (
!regionGraphic.optionStyleEnabled
|| !(regionGraphic.el instanceof Displayable)
) {
return;
}
const normalStyleModel = regionModel.getModel(styleOptionKey);
const emphasisStyleModel = regionModel.getModel(['emphasis', styleOptionKey]);
const blurStyleModel = regionModel.getModel(['blur', styleOptionKey]);
const selectStyleModel = regionModel.getModel(['select', styleOptionKey]);
// All of the path are using `itemStyle`, becuase
// (1) Some SVG also use fill on polyline (The different between
// polyline and polygon is "open" or "close" but not fill or not).
// (2) For the common props like opacity, if some use itemStyle
// and some use `lineStyle`, it might confuse users.
// (3) Most SVG use <path>, where can not detect wether draw a "line"
// or a filled shape, so use `itemStyle` for <path>.
const normalStyleModel = regionModel.getModel('itemStyle');
const emphasisStyleModel = regionModel.getModel(['emphasis', 'itemStyle']);
const blurStyleModel = regionModel.getModel(['blur', 'itemStyle']);
const selectStyleModel = regionModel.getModel(['select', 'itemStyle']);
// NOTE: DONT use 'style' in visual when drawing map.
// This component is used for drawing underlying map for both geo component and map series.
if (styleOptionKey === 'itemStyle') {
return {
styleOptionKey,
normal: getFixedItemStyle(normalStyleModel),
emphasis: getFixedItemStyle(emphasisStyleModel),
select: getFixedItemStyle(selectStyleModel),
blur: getFixedItemStyle(blurStyleModel)
};
}
else if (styleOptionKey === 'lineStyle') {
return {
styleOptionKey,
normal: normalStyleModel.getLineStyle(),
emphasis: emphasisStyleModel.getLineStyle(),
select: selectStyleModel.getLineStyle(),
blur: blurStyleModel.getLineStyle()
};
const normalStyle = getFixedItemStyle(normalStyleModel);
const emphasisStyle = getFixedItemStyle(emphasisStyleModel);
const selectStyle = getFixedItemStyle(selectStyleModel);
const blurStyle = getFixedItemStyle(blurStyleModel);
// Update the itemStyle if has data visual
const data = viewBuildCtx.data;
if (data) {
// Only visual color of each item will be used. It can be encoded by visualMap
// But visual color of series is used in symbol drawing
// Visual color for each series is for the symbol draw
const style = data.getItemVisual(dataIndex, 'style');
const decal = data.getItemVisual(dataIndex, 'decal');
if (viewBuildCtx.isVisualEncodedByVisualMap && style.fill) {
normalStyle.fill = style.fill;
}
if (decal) {
normalStyle.decal = createOrUpdatePatternFromDecal(decal, viewBuildCtx.api);
}
}
// SVG text, tspan and image can be named but not supporeted
// to be styled by region option yet.
regionGraphic.el.setStyle(normalStyle);
regionGraphic.el.style.strokeNoScale = true;
regionGraphic.el.ensureState('emphasis').style = emphasisStyle;
regionGraphic.el.ensureState('select').style = selectStyle;
regionGraphic.el.ensureState('blur').style = blurStyle;
}
export default MapDraw;
......
......@@ -319,8 +319,6 @@ class TooltipView extends ComponentView {
el.y = payload.y;
el.update();
getECData(el).tooltipConfig = {
componentMainType: null,
componentIndex: null,
name: null,
option: payload.tooltip
};
......@@ -707,7 +705,8 @@ class TooltipView extends ComponentView {
el: ECElement,
dispatchAction: ExtensionAPI['dispatchAction']
) {
const tooltipConfig = getECData(el).tooltipConfig;
const ecData = getECData(el);
const tooltipConfig = ecData.tooltipConfig;
let tooltipOpt = tooltipConfig.option;
if (zrUtil.isString(tooltipOpt)) {
const content = tooltipOpt;
......@@ -719,7 +718,7 @@ class TooltipView extends ComponentView {
}
const tooltipModelCascade = [tooltipOpt] as TooltipModelOptionCascade[];
const cmpt = this._ecModel.getComponent(tooltipConfig.componentMainType, tooltipConfig.componentIndex);
const cmpt = this._ecModel.getComponent(ecData.componentMainType, ecData.componentIndex);
if (cmpt) {
tooltipModelCascade.push(cmpt as Model<TooltipableOption>);
}
......
......@@ -20,33 +20,30 @@
import { parseSVG, makeViewBoxTransform, SVGNodeTagLower } from 'zrender/src/tool/parseSVG';
import Group from 'zrender/src/graphic/Group';
import Rect from 'zrender/src/graphic/shape/Rect';
import {assert, createHashMap, HashMap, hasOwn} from 'zrender/src/core/util';
import {assert, createHashMap, HashMap} from 'zrender/src/core/util';
import BoundingRect from 'zrender/src/core/BoundingRect';
import { GeoResource, GeoSVGGraphicRoot, GeoSVGSourceInput, RegionGraphic } from './geoTypes';
import { parseXML } from 'zrender/src/tool/parseXML';
import { GeoSVGRegion } from './Region';
import Element from 'zrender/src/Element';
type RegionName = string;
export interface GeoSVGGraphicRecord {
root: Group;
boundingRect: BoundingRect;
regionGraphics: RegionGraphic[];
// A name may correspond to multiple graphics.
regionElementMap: HashMap<Element[], RegionName>;
}
const REGION_AVAILABLE_SVG_TAG_MAP = createHashMap<number, SVGNodeTagLower>([
'rect', 'circle', 'line', 'ellipse', 'polygon', 'polyline', 'text', 'tspan', 'path'
]);
const STYLE_OPTION_KEY = createHashMap<'itemStyle' | 'lineStyle', SVGNodeTagLower>({
'rect': 'itemStyle',
'circle': 'itemStyle',
'line': 'lineStyle',
'ellipse': 'itemStyle',
'polygon': 'itemStyle',
'polyline': 'lineStyle',
// 'image': '?', // TODO
// 'text': '?', // TODO
// 'tspan': '?', // TODO
'path': 'itemStyle'
});
const OPTION_STYLE_ENABLED_TAG_MAP = createHashMap<number, SVGNodeTagLower>([
'rect', 'circle', 'line', 'ellipse', 'polygon', 'polyline', 'path'
]);
const LABEL_HOST_MAP = createHashMap<number, SVGNodeTagLower>([
'rect', 'circle', 'line', 'ellipse', 'polygon', 'polyline', 'path'
]);
......@@ -231,25 +228,30 @@ function buildGraphic(
(root as GeoSVGGraphicRoot).isGeoSVGGraphicRoot = true;
const regionGraphics = [] as GeoSVGGraphicRecord['regionGraphics'];
const regionElementMap = createHashMap<Element[], RegionName>();
const named = result.named;
for (let i = 0; i < named.length; i++) {
const namedItem = named[i];
const svgNodeTagLower = namedItem.svgNodeTagLower;
if (REGION_AVAILABLE_SVG_TAG_MAP.get(svgNodeTagLower) != null) {
const styleOptionKey = STYLE_OPTION_KEY.get(svgNodeTagLower);
const optionStyleEnabled = OPTION_STYLE_ENABLED_TAG_MAP.get(svgNodeTagLower);
const el = namedItem.el;
const name = namedItem.name;
regionGraphics.push({
name: namedItem.name,
name: name,
el: el,
styleOptionKey: styleOptionKey,
stateTrigger: styleOptionKey != null ? el : null,
optionStyleEnabled: optionStyleEnabled != null,
stateTrigger: optionStyleEnabled != null ? el : null,
// text/tspan/image do not suport style but support event.
eventTrigger: el,
useLabel: LABEL_HOST_MAP.get(svgNodeTagLower) != null
});
const els = regionElementMap.get(name) || regionElementMap.set(name, []);
els.push(el);
// Only named element has silent: false, other elements should
// act as background and has no user interaction.
el.silent = false;
......@@ -262,7 +264,7 @@ function buildGraphic(
}
}
return { root, boundingRect, regionGraphics };
return { root, boundingRect, regionGraphics, regionElementMap: regionElementMap };
}
......
......@@ -155,6 +155,6 @@ export type RegionGraphic = {
eventTrigger: Element;
// Whether to set label on `el.textContent`.
useLabel: boolean;
// Use this key to obtain style config in echarts option.
styleOptionKey: 'itemStyle' | 'lineStyle';
// Whether to be enabled to set style via in echarts option.
optionStyleEnabled: boolean;
};
......@@ -41,8 +41,7 @@ import {
isHighDownDispatcher,
HOVER_STATE_EMPHASIS,
HOVER_STATE_BLUR,
blurSeries,
blurSeriesFromPayload,
blurSeriesFromHighlightPayload,
toggleSelectionFromPayload,
updateSeriesElementSelection,
getAllSelectedIndices,
......@@ -60,7 +59,11 @@ import {
enterSelect,
leaveSelect,
enterBlur,
allLeaveBlur
allLeaveBlur,
findComponentHighDownDispatchers,
blurComponent,
handleGlobalMouseOverForHighDown,
handleGlboalMouseOutForHighDown
} from '../util/states';
import * as modelUtil from '../util/model';
import {throttle} from '../util/throttle';
......@@ -1480,9 +1483,26 @@ class ECharts extends Eventful<ECEventDefinition> {
// If dispatchAction before setOption, do nothing.
ecModel && ecModel.eachComponent(condition, function (model) {
if (!excludeSeriesIdMap || excludeSeriesIdMap.get(model.id) == null) {
if (isHighDownPayload(payload) && !payload.notBlur) {
if (model instanceof SeriesModel) {
blurSeriesFromPayload(model, payload, ecIns._api);
if (isHighDownPayload(payload)) {
if (payload.type === HIGHLIGHT_ACTION_TYPE) {
if (model instanceof SeriesModel) {
!payload.notBlur && blurSeriesFromHighlightPayload(model, payload, ecIns._api);
}
else {
const { focusSelf, dispatchers } = findComponentHighDownDispatchers(
model.mainType, model.componentIndex, payload.name, ecIns._api
);
if (focusSelf && !payload.notBlur) {
blurComponent(model.mainType, model.componentIndex, ecIns._api);
}
// PENDING:
// Whether to put this "enter emphasis" code in `ComponentView`,
// which will be the same as `ChartView` but might be not necessary
// and will be far from this logic.
if (dispatchers) {
each(dispatchers, dispatcher => enterEmphasis(dispatcher));
}
}
}
}
else if (isSelectChangePayload(payload)) {
......@@ -1780,7 +1800,7 @@ class ECharts extends Eventful<ECEventDefinition> {
let eventObj: ECActionEvent;
const isSelectChange = isSelectChangePayload(payload);
const isStatusChange = isHighDownPayload(payload) || isSelectChange;
const isHighDown = isHighDownPayload(payload);
each(payloads, (batchItem) => {
// Action can specify the event by return it.
......@@ -1792,11 +1812,16 @@ class ECharts extends Eventful<ECEventDefinition> {
eventObjBatch.push(eventObj);
// light update does not perform data process, layout and visual.
if (isStatusChange) {
// method, payload, mainType, subType
if (isHighDown) {
const { queryOptionMap, mainTypeSpecified } = modelUtil.preParseFinder(payload as ModelFinder);
const componentMainType = mainTypeSpecified ? queryOptionMap.keys()[0] : 'series';
updateDirectly(this, updateMethod, batchItem as Payload, componentMainType);
markStatusToUpdate(this);
}
else if (isSelectChange) {
// At present `dispatchAction({ type: 'select', ... })` is not supported on components.
// geo still use 'geoselect'.
updateDirectly(this, updateMethod, batchItem as Payload, 'series');
// Mark status to update
markStatusToUpdate(this);
}
else if (cptType) {
......@@ -1804,7 +1829,7 @@ class ECharts extends Eventful<ECEventDefinition> {
}
});
if (updateMethod !== 'none' && !isStatusChange && !cptType) {
if (updateMethod !== 'none' && !isHighDown && !isSelectChange && !cptType) {
// Still dirty
if (this[OPTION_UPDATED_KEY]) {
prepare(this);
......@@ -1900,24 +1925,14 @@ class ECharts extends Eventful<ECEventDefinition> {
const el = e.target;
const dispatcher = findEventDispatcher(el, isHighDownDispatcher);
if (dispatcher) {
const ecData = getECData(dispatcher);
// Try blur all in the related series. Then emphasis the hoverred.
// TODO. progressive mode.
blurSeries(
ecData.seriesIndex, ecData.focus, ecData.blurScope, ecIns._api
);
enterEmphasisWhenMouseOver(dispatcher, e);
handleGlobalMouseOverForHighDown(dispatcher, e, ecIns._api);
markStatusToUpdate(ecIns);
}
}).on('mouseout', function (e) {
const el = e.target;
const dispatcher = findEventDispatcher(el, isHighDownDispatcher);
if (dispatcher) {
allLeaveBlur(ecIns._api);
leaveEmphasisWhenMouseOut(dispatcher, e);
handleGlboalMouseOutForHighDown(dispatcher, e, ecIns._api);
markStatusToUpdate(ecIns);
}
}).on('click', function (e) {
......
......@@ -846,9 +846,10 @@ export function setTooltipConfig(opt: {
});
}
getECData(opt.el).tooltipConfig = {
componentMainType: mainType,
componentIndex: componentIndex,
const ecData = getECData(opt.el);
ecData.componentMainType = mainType;
ecData.componentIndex = componentIndex;
ecData.tooltipConfig = {
name: itemName,
option: defaults({
content: itemName,
......
......@@ -34,14 +34,18 @@ export interface ECData {
dataType?: SeriesDataType;
focus?: InnerFocus;
blurScope?: BlurScope;
// Required by `tooltipConfig` and `focus`.
componentMainType?: ComponentMainType;
componentIndex?: number;
componentHighDownName?: string;
// To make a tooltipConfig, seach `setTooltipConfig`.
// Used to find component tooltip option, which is used as
// the parent of tooltipConfig.option for cascading.
// If not provided, do not use component as its parent.
// (Set manatary to make developers not to forget them).
tooltipConfig?: {
// To make a tooltipConfig, seach `setTooltipConfig`.
// Used to find component tooltip option, which is used as
// the parent of tooltipConfig.option for cascading.
// If not provided, do not use component as its parent.
// (Set manatary to make developers not to forget them).
componentMainType: ComponentMainType;
componentIndex: number;
// Target item name to locate tooltip.
name: string;
option: ComponentItemTooltipOption<unknown>;
......
......@@ -35,9 +35,10 @@ import {
Payload,
ZRColor,
HighlightPayload,
DownplayPayload
DownplayPayload,
ComponentMainType
} from './types';
import { extend, indexOf, isArrayLike, isObject, keys, isArray, each } from 'zrender/src/core/util';
import { extend, indexOf, isArrayLike, isObject, keys, isArray, each, assert } from 'zrender/src/core/util';
import { getECData } from './innerStore';
import * as colorTool from 'zrender/src/tool/color';
import List from '../data/List';
......@@ -47,6 +48,8 @@ import { queryDataIndex, makeInner } from './model';
import Path, { PathStyleProps } from 'zrender/src/graphic/Path';
import GlobalModel from '../model/Global';
import ExtensionAPI from '../core/ExtensionAPI';
import ComponentModel from '../model/Component';
// Reserve 0 as default.
let _highlightNextDigit = 1;
......@@ -223,7 +226,7 @@ function createEmphasisDefaultState(
stateName: 'emphasis',
targetStates: string[],
state: Displayable['states'][number]
) {
): DisplayableState {
const hasSelect = targetStates && indexOf(targetStates, 'select') >= 0;
let cloned = false;
if (el instanceof Path) {
......@@ -270,7 +273,7 @@ function createSelectDefaultState(
el: Displayable,
stateName: 'select',
state: Displayable['states'][number]
) {
): DisplayableState {
// const hasSelect = indexOf(el.currentStates, stateName) >= 0;
if (state) {
// TODO Share with textContent?
......@@ -287,7 +290,7 @@ function createBlurDefaultState(
el: Displayable,
stateName: 'blur',
state: Displayable['states'][number]
) {
): DisplayableState {
const hasBlur = indexOf(el.currentStates, stateName) >= 0;
const currentOpacity = el.style.opacity;
......@@ -482,16 +485,35 @@ export function blurSeries(
});
}
export function blurSeriesFromPayload(
seriesModel: SeriesModel,
payload: Payload,
export function blurComponent(
componentMainType: ComponentMainType,
componentIndex: number,
api: ExtensionAPI
) {
if (!isHighDownPayload(payload)) {
if (componentMainType == null || componentIndex == null) {
return;
}
const componentModel = api.getModel().getComponent(componentMainType, componentIndex);
if (!componentModel) {
return;
}
const isHighlight = payload.type === HIGHLIGHT_ACTION_TYPE;
const view = api.getViewOfComponentModel(componentModel);
if (!view || !view.focusBlurEnabled) {
return;
}
view.group.traverse(function (child) {
singleEnterBlur(child);
});
}
export function blurSeriesFromHighlightPayload(
seriesModel: SeriesModel,
payload: HighlightPayload,
api: ExtensionAPI
) {
const seriesIndex = seriesModel.seriesIndex;
const data = seriesModel.getData(payload.dataType);
let dataIndex = queryDataIndex(data, payload);
......@@ -507,25 +529,133 @@ export function blurSeriesFromPayload(
}
}
if (isHighlight) {
if (el) {
const ecData = getECData(el);
blurSeries(
seriesIndex, ecData.focus, ecData.blurScope, api
);
if (el) {
const ecData = getECData(el);
blurSeries(
seriesIndex, ecData.focus, ecData.blurScope, api
);
}
else {
// If there is no element put on the data. Try getting it from raw option
// TODO Should put it on seriesModel?
const focus = seriesModel.get(['emphasis', 'focus']);
const blurScope = seriesModel.get(['emphasis', 'blurScope']);
if (focus != null) {
blurSeries(seriesIndex, focus, blurScope, api);
}
else {
// If there is no element put on the data. Try getting it from raw option
// TODO Should put it on seriesModel?
const focus = seriesModel.get(['emphasis', 'focus']);
const blurScope = seriesModel.get(['emphasis', 'blurScope']);
if (focus != null) {
blurSeries(seriesIndex, focus, blurScope, api);
}
}
}
export function findComponentHighDownDispatchers(
componentMainType: ComponentMainType,
componentIndex: number,
name: string,
api: ExtensionAPI
): {
focusSelf: boolean;
// If return null/undefined, do not support this feature.
dispatchers: Element[];
} {
const ret = {
focusSelf: false,
dispatchers: null as Element[]
};
if (componentMainType == null
|| componentMainType === 'series'
|| componentIndex == null
|| name == null
) {
return ret;
}
const componentModel = api.getModel().getComponent(componentMainType, componentIndex);
if (!componentModel) {
return ret;
}
const view = api.getViewOfComponentModel(componentModel);
if (!view || !view.findHighDownDispatchers) {
return ret;
}
const dispatchers = view.findHighDownDispatchers(name);
// At presnet, the component (like Geo) only blur inside itself.
// So we do not use `blurScope` in component.
let focusSelf: boolean;
for (let i = 0; i < dispatchers.length; i++) {
if (__DEV__) {
assert(isHighDownDispatcher(dispatchers[i]));
}
if (getECData(dispatchers[i]).focus === 'self') {
focusSelf = true;
break;
}
}
return { focusSelf, dispatchers };
}
export function handleGlobalMouseOverForHighDown(
dispatcher: Element,
e: ElementEvent,
api: ExtensionAPI
): void {
if (__DEV__) {
assert(isHighDownDispatcher(dispatcher));
}
const ecData = getECData(dispatcher);
const { dispatchers, focusSelf } = findComponentHighDownDispatchers(
ecData.componentMainType, ecData.componentIndex, ecData.componentHighDownName, api
);
// If `findHighDownDispatchers` is supported on the component,
// highlight/downplay elements with the same name.
if (dispatchers) {
if (focusSelf) {
blurComponent(ecData.componentMainType, ecData.componentIndex, api);
}
each(dispatchers, dispatcher => enterEmphasisWhenMouseOver(dispatcher, e));
}
else {
// Try blur all in the related series. Then emphasis the hoverred.
// TODO. progressive mode.
blurSeries(ecData.seriesIndex, ecData.focus, ecData.blurScope, api);
if (ecData.focus === 'self') {
blurComponent(ecData.componentMainType, ecData.componentIndex, api);
}
// Other than series, component that not support `findHighDownDispatcher` will
// also use it. But in this case, highlight/downplay are only supported in
// mouse hover but not in dispatchAction.
enterEmphasisWhenMouseOver(dispatcher, e);
}
}
export function handleGlboalMouseOutForHighDown(
dispatcher: Element,
e: ElementEvent,
api: ExtensionAPI
): void {
if (__DEV__) {
assert(isHighDownDispatcher(dispatcher));
}
allLeaveBlur(api);
const ecData = getECData(dispatcher);
const { dispatchers } = findComponentHighDownDispatchers(
ecData.componentMainType, ecData.componentIndex, ecData.componentHighDownName, api
);
if (dispatchers) {
each(dispatchers, dispatcher => leaveEmphasisWhenMouseOut(dispatcher, e));
}
else {
leaveEmphasisWhenMouseOut(dispatcher, e);
}
}
export function toggleSelectionFromPayload(
seriesModel: SeriesModel,
payload: Payload,
......@@ -679,6 +809,22 @@ export function isHighDownDispatcher(el: Element): boolean {
return !!(el && (el as ExtendedDisplayable).__highDownDispatcher);
}
/**
* Enable component highlight/downplay features:
* + hover link (within the same name)
* + focus blur in component
*/
export function enableComponentHighDownFeatures(
el: Element,
componentModel: ComponentModel,
componentHighDownName: string
): void {
const ecData = getECData(el);
ecData.componentMainType = componentModel.mainType;
ecData.componentIndex = componentModel.componentIndex;
ecData.componentHighDownName = componentHighDownName;
}
/**
* Support hightlight/downplay record on each elements.
* For the case: hover highlight/downplay (legend, visualMap, ...) and
......
......@@ -1484,7 +1484,7 @@ export type BlurScope = 'coordinateSystem' | 'series' | 'global';
* can be array of data indices.
* Or may be an dictionary if have different types of data like in graph.
*/
export type InnerFocus = string | ArrayLike<number> | Dictionary<ArrayLike<number>>;
export type InnerFocus = DefaultEmphasisFocus | ArrayLike<number> | Dictionary<ArrayLike<number>>;
export interface DefaultExtraStateOpts {
emphasis: any
......
......@@ -32,7 +32,7 @@ interface ComponentView {
* Implement it if needed.
*/
updateTransform?(
seriesModel: ComponentModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload
model: ComponentModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload
): void | {update: true};
/**
......@@ -42,6 +42,22 @@ interface ComponentView {
filterForExposedEvent(
eventType: string, query: EventQueryItem, targetEl: Element, packedEvent: ECActionEvent | ECElementEvent
): boolean;
/**
* Find dispatchers for highlight/downplay by name.
* If this methods provided, hover link (within the same name) is enabled in component.
* That is, in component, a name can correspond to multiple dispatchers.
* Those dispatchers can have no common ancestor.
* The highlight/downplay state change will be applied on the
* dispatchers and their descendents.
*
* @return Must return an array but not null/undefined.
*/
findHighDownDispatchers?(
name: string
): Element[];
focusBlurEnabled?: boolean;
}
class ComponentView {
......
<!DOCTYPE html>
<!--
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.
-->
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="lib/esl.js"></script>
<script src="lib/config.js"></script>
<script src="lib/jquery.min.js"></script>
<script src="lib/facePrint.js"></script>
<script src="lib/testHelper.js"></script>
<!-- <script src="ut/lib/canteen.js"></script> -->
<link rel="stylesheet" href="lib/reset.css" />
</head>
<body>
<style>
</style>
<div id="main_geo_svg_organ"></div>
<!-- <div id="main_geo_svg_organ1"></div> -->
<script>
function listenAndPrintEvent(chart) {
if (!chart) {
return;
}
const out = {
};
chart.on('geoselectchanged', function (params) {
out.geoselectechanged = {
allSelected: params.allSelected
};
console.log('geoselectechanged', params);
chart.__testHelper.updateInfo(out, 'event');
});
chart.on('selectchanged', function (params) {
out.selectechanged = {
selected: params.selected
};
console.log('selectechanged', params);
chart.__testHelper.updateInfo(out, 'event');
});
chart.on('click', function (params) {
out.click = {
componentIndex: params.componentIndex,
componentType: params.componentType,
geoIndex: params.geoIndex,
name: params.name
};
console.log('click', params);
chart.__testHelper.updateInfo(out, 'event');
});
}
</script>
<script>
require(['echarts'/*, 'map/js/china' */], function (echarts) {
var option;
$.ajax({
url: '../../vis-data/map/svg/organ/Veins_Medical_Diagram_clip_art.svg',
dataType: 'text'
}).done(function (svg) {
echarts.registerMap('seatmap', {
svg: svg
});
option = {
tooltip: {
},
geo: {
map: 'seatmap',
roam: true,
selectedMode: 'multiple',
// height: 100,
// zoom: 1.5
emphasis: {
focus: 'self',
itemStyle: {
// color: null
},
label: {
show: false,
textBorderColor: '#fff',
textBorderWidth: 2
}
},
blur: {
// itemStyle: {
// opacity: 0.3
// }
},
select: {
itemStyle: {
color: '#b50205'
},
label: {
show: false,
textBorderColor: '#fff',
textBorderWidth: 2
}
}
}
};
var chart = testHelper.create(echarts, 'main_geo_svg_organ', {
title: [
'pure geo component with svg resource',
'click seat: check **allSelected** correct.'
],
option: option,
info: {},
infoKey: 'event',
height: 500
// buttons: [{text: 'btn-txt', onclick: function () {}}],
// recordCanvas: true,
});
listenAndPrintEvent(chart);
if (chart) {
chart.on('highlight', function () {
console.log('agsd');
});
}
});
});
</script>
<!--
<script>
require(['echarts'/*, 'map/js/china' */], function (echarts) {
var option;
$.ajax({
url: '../../vis-data/map/svg/organ/Veins_Medical_Diagram_clip_art.svg',
dataType: 'text'
}).done(function (svg) {
echarts.registerMap('seatmap', {
svg: svg
});
option = {
tooltip: {
},
series: {
type: 'map',
map: 'seatmap',
roam: true,
selectedMode: 'multiple',
// height: 100,
// zoom: 1.5
emphasis: {
focus: 'self',
label: {
textBorderColor: '#fff',
textBorderWidth: 2
}
},
select: {
itemStyle: {
color: '#b50205'
},
label: {
show: false,
textBorderColor: '#fff',
textBorderWidth: 2
}
}
}
};
var chart = testHelper.create(echarts, 'main_geo_svg_organ1', {
title: [
'pure geo component with svg resource',
'click seat: check **allSelected** correct.'
],
option: option,
info: {},
infoKey: 'event',
height: 500
// buttons: [{text: 'btn-txt', onclick: function () {}}],
// recordCanvas: true,
});
listenAndPrintEvent(chart);
if (chart) {
chart.on('highlight', function () {
console.log('agsd');
});
}
});
});
</script> -->
</body>
</html>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册