| /* | |
| * 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. | |
| */ | |
| import * as zrUtil from 'zrender/src/core/util'; | |
| import Model from '../../model/Model'; | |
| import {isNameSpecified} from '../../util/model'; | |
| import ComponentModel from '../../model/Component'; | |
| import { | |
| ComponentOption, | |
| BoxLayoutOptionMixin, | |
| BorderOptionMixin, | |
| ColorString, | |
| LabelOption, | |
| LayoutOrient, | |
| CommonTooltipOption, | |
| ItemStyleOption, | |
| LineStyleOption | |
| } from '../../util/types'; | |
| import { Dictionary } from 'zrender/src/core/types'; | |
| import GlobalModel from '../../model/Global'; | |
| import { ItemStyleProps } from '../../model/mixin/itemStyle'; | |
| import { LineStyleProps } from './../../model/mixin/lineStyle'; | |
| import {PathStyleProps} from 'zrender/src/graphic/Path'; | |
| type LegendDefaultSelectorOptionsProps = { | |
| type: string; | |
| title: string; | |
| }; | |
| const getDefaultSelectorOptions = function (ecModel: GlobalModel, type: string): LegendDefaultSelectorOptionsProps { | |
| if (type === 'all') { | |
| return { | |
| type: 'all', | |
| title: ecModel.getLocaleModel().get(['legend', 'selector', 'all']) | |
| }; | |
| } | |
| else if (type === 'inverse') { | |
| return { | |
| type: 'inverse', | |
| title: ecModel.getLocaleModel().get(['legend', 'selector', 'inverse']) | |
| }; | |
| } | |
| }; | |
| type SelectorType = 'all' | 'inverse'; | |
| export interface LegendSelectorButtonOption { | |
| type?: SelectorType | |
| title?: string | |
| } | |
| /** | |
| * T: the type to be extended | |
| * ET: extended type for keys of T | |
| * ST: special type for T to be extended | |
| */ | |
| type ExtendPropertyType<T, ET, ST extends { [key in keyof T]: any }> = { | |
| [key in keyof T]: key extends keyof ST ? T[key] | ET | ST[key] : T[key] | ET | |
| }; | |
| export interface LegendItemStyleOption extends ExtendPropertyType<ItemStyleOption, 'inherit', { | |
| borderWidth: 'auto' | |
| }> {} | |
| export interface LegendLineStyleOption extends ExtendPropertyType<LineStyleOption, 'inherit', { | |
| width: 'auto' | |
| }> { | |
| inactiveColor?: ColorString | |
| inactiveWidth?: number | |
| } | |
| export interface LegendStyleOption { | |
| /** | |
| * Icon of the legend items. | |
| * @default 'roundRect' | |
| */ | |
| icon?: string | |
| /** | |
| * Color when legend item is not selected | |
| */ | |
| inactiveColor?: ColorString | |
| /** | |
| * Border color when legend item is not selected | |
| */ | |
| inactiveBorderColor?: ColorString | |
| /** | |
| * Border color when legend item is not selected | |
| */ | |
| inactiveBorderWidth?: number | 'auto' | |
| /** | |
| * Legend label formatter | |
| */ | |
| formatter?: string | ((name: string) => string) | |
| itemStyle?: LegendItemStyleOption | |
| lineStyle?: LegendLineStyleOption | |
| textStyle?: LabelOption | |
| symbolRotate?: number | 'inherit' | |
| /** | |
| * @deprecated | |
| */ | |
| symbolKeepAspect?: boolean | |
| } | |
| interface DataItem extends LegendStyleOption { | |
| name?: string | |
| icon?: string | |
| textStyle?: LabelOption | |
| // TODO: TYPE tooltip | |
| tooltip?: unknown | |
| } | |
| export interface LegendTooltipFormatterParams { | |
| componentType: 'legend' | |
| legendIndex: number | |
| name: string | |
| $vars: ['name'] | |
| } | |
| export interface LegendIconParams { | |
| itemWidth: number | |
| itemHeight: number | |
| /** | |
| * symbolType is from legend.icon, legend.data.icon, or series visual | |
| */ | |
| icon: string | |
| iconRotate: number | 'inherit' | |
| symbolKeepAspect: boolean | |
| itemStyle: PathStyleProps | |
| lineStyle: LineStyleProps | |
| } | |
| export interface LegendSymbolStyleOption { | |
| itemStyle?: ItemStyleProps | |
| lineStyle?: LineStyleProps | |
| } | |
| export interface LegendOption extends ComponentOption, LegendStyleOption, | |
| BoxLayoutOptionMixin, BorderOptionMixin | |
| { | |
| mainType?: 'legend' | |
| show?: boolean | |
| orient?: LayoutOrient | |
| align?: 'auto' | 'left' | 'right' | |
| backgroundColor?: ColorString | |
| /** | |
| * Border radius of background rect | |
| * @default 0 | |
| */ | |
| borderRadius?: number | number[] | |
| /** | |
| * Padding between legend item and border. | |
| * Support to be a single number or an array. | |
| * @default 5 | |
| */ | |
| padding?: number | number[] | |
| /** | |
| * Gap between each legend item. | |
| * @default 10 | |
| */ | |
| itemGap?: number | |
| /** | |
| * Width of legend symbol | |
| */ | |
| itemWidth?: number | |
| /** | |
| * Height of legend symbol | |
| */ | |
| itemHeight?: number | |
| selectedMode?: boolean | 'single' | 'multiple' | |
| /** | |
| * selected map of each item. Default to be selected if item is not in the map | |
| */ | |
| selected?: Dictionary<boolean> | |
| /** | |
| * Buttons for all select or inverse select. | |
| * @example | |
| * selector: [{type: 'all or inverse', title: xxx}] | |
| * selector: true | |
| * selector: ['all', 'inverse'] | |
| */ | |
| selector?: (LegendSelectorButtonOption | SelectorType)[] | boolean | |
| selectorLabel?: LabelOption | |
| emphasis?: { | |
| selectorLabel?: LabelOption | |
| } | |
| /** | |
| * Position of selector buttons. | |
| */ | |
| selectorPosition?: 'auto' | 'start' | 'end' | |
| /** | |
| * Gap between each selector button | |
| */ | |
| selectorItemGap?: number | |
| /** | |
| * Gap between selector buttons group and legend main items. | |
| */ | |
| selectorButtonGap?: number | |
| data?: (string | DataItem)[] | |
| /** | |
| * Tooltip option | |
| */ | |
| tooltip?: CommonTooltipOption<LegendTooltipFormatterParams> | |
| } | |
| class LegendModel<Ops extends LegendOption = LegendOption> extends ComponentModel<Ops> { | |
| static type = 'legend.plain'; | |
| type = LegendModel.type; | |
| static readonly dependencies = ['series']; | |
| readonly layoutMode = { | |
| type: 'box', | |
| // legend.width/height are maxWidth/maxHeight actually, | |
| // whereas real width/height is calculated by its content. | |
| // (Setting {left: 10, right: 10} does not make sense). | |
| // So consider the case: | |
| // `setOption({legend: {left: 10});` | |
| // then `setOption({legend: {right: 10});` | |
| // The previous `left` should be cleared by setting `ignoreSize`. | |
| ignoreSize: true | |
| } as const; | |
| private _data: Model<DataItem>[]; | |
| private _availableNames: string[]; | |
| init(option: Ops, parentModel: Model, ecModel: GlobalModel) { | |
| this.mergeDefaultAndTheme(option, ecModel); | |
| option.selected = option.selected || {}; | |
| this._updateSelector(option); | |
| } | |
| mergeOption(option: Ops, ecModel: GlobalModel) { | |
| super.mergeOption(option, ecModel); | |
| this._updateSelector(option); | |
| } | |
| _updateSelector(option: Ops) { | |
| let selector = option.selector; | |
| const {ecModel} = this; | |
| if (selector === true) { | |
| selector = option.selector = ['all', 'inverse']; | |
| } | |
| if (zrUtil.isArray(selector)) { | |
| zrUtil.each(selector, function (item, index) { | |
| zrUtil.isString(item) && (item = {type: item}); | |
| (selector as LegendSelectorButtonOption[])[index] = zrUtil.merge( | |
| item, getDefaultSelectorOptions(ecModel, item.type) | |
| ); | |
| }); | |
| } | |
| } | |
| optionUpdated() { | |
| this._updateData(this.ecModel); | |
| const legendData = this._data; | |
| // If selectedMode is single, try to select one | |
| if (legendData[0] && this.get('selectedMode') === 'single') { | |
| let hasSelected = false; | |
| // If has any selected in option.selected | |
| for (let i = 0; i < legendData.length; i++) { | |
| const name = legendData[i].get('name'); | |
| if (this.isSelected(name)) { | |
| // Force to unselect others | |
| this.select(name); | |
| hasSelected = true; | |
| break; | |
| } | |
| } | |
| // Try select the first if selectedMode is single | |
| !hasSelected && this.select(legendData[0].get('name')); | |
| } | |
| } | |
| _updateData(ecModel: GlobalModel) { | |
| let potentialData: string[] = []; | |
| let availableNames: string[] = []; | |
| ecModel.eachRawSeries(function (seriesModel) { | |
| const seriesName = seriesModel.name; | |
| availableNames.push(seriesName); | |
| let isPotential; | |
| if (seriesModel.legendVisualProvider) { | |
| const provider = seriesModel.legendVisualProvider; | |
| const names = provider.getAllNames(); | |
| if (!ecModel.isSeriesFiltered(seriesModel)) { | |
| availableNames = availableNames.concat(names); | |
| } | |
| if (names.length) { | |
| potentialData = potentialData.concat(names); | |
| } | |
| else { | |
| isPotential = true; | |
| } | |
| } | |
| else { | |
| isPotential = true; | |
| } | |
| if (isPotential && isNameSpecified(seriesModel)) { | |
| potentialData.push(seriesModel.name); | |
| } | |
| }); | |
| /** | |
| * @type {Array.<string>} | |
| * @private | |
| */ | |
| this._availableNames = availableNames; | |
| // If legend.data is not specified in option, use availableNames as data, | |
| // which is convenient for user preparing option. | |
| const rawData = this.get('data') || potentialData; | |
| const legendNameMap = zrUtil.createHashMap(); | |
| const legendData = zrUtil.map(rawData, function (dataItem) { | |
| // Can be string or number | |
| if (zrUtil.isString(dataItem) || zrUtil.isNumber(dataItem)) { | |
| dataItem = { | |
| name: dataItem as string | |
| }; | |
| } | |
| if (legendNameMap.get(dataItem.name)) { | |
| // remove legend name duplicate | |
| return null; | |
| } | |
| legendNameMap.set(dataItem.name, true); | |
| return new Model(dataItem, this, this.ecModel); | |
| }, this); | |
| /** | |
| * @type {Array.<module:echarts/model/Model>} | |
| * @private | |
| */ | |
| this._data = zrUtil.filter(legendData, item => !!item); | |
| } | |
| getData() { | |
| return this._data; | |
| } | |
| select(name: string) { | |
| const selected = this.option.selected; | |
| const selectedMode = this.get('selectedMode'); | |
| if (selectedMode === 'single') { | |
| const data = this._data; | |
| zrUtil.each(data, function (dataItem) { | |
| selected[dataItem.get('name')] = false; | |
| }); | |
| } | |
| selected[name] = true; | |
| } | |
| unSelect(name: string) { | |
| if (this.get('selectedMode') !== 'single') { | |
| this.option.selected[name] = false; | |
| } | |
| } | |
| toggleSelected(name: string) { | |
| const selected = this.option.selected; | |
| // Default is true | |
| if (!selected.hasOwnProperty(name)) { | |
| selected[name] = true; | |
| } | |
| this[selected[name] ? 'unSelect' : 'select'](name); | |
| } | |
| allSelect() { | |
| const data = this._data; | |
| const selected = this.option.selected; | |
| zrUtil.each(data, function (dataItem) { | |
| selected[dataItem.get('name', true)] = true; | |
| }); | |
| } | |
| inverseSelect() { | |
| const data = this._data; | |
| const selected = this.option.selected; | |
| zrUtil.each(data, function (dataItem) { | |
| const name = dataItem.get('name', true); | |
| // Initially, default value is true | |
| if (!selected.hasOwnProperty(name)) { | |
| selected[name] = true; | |
| } | |
| selected[name] = !selected[name]; | |
| }); | |
| } | |
| isSelected(name: string) { | |
| const selected = this.option.selected; | |
| return !(selected.hasOwnProperty(name) && !selected[name]) | |
| && zrUtil.indexOf(this._availableNames, name) >= 0; | |
| } | |
| getOrient(): {index: 0, name: 'horizontal'} | |
| getOrient(): {index: 1, name: 'vertical'} | |
| getOrient() { | |
| return this.get('orient') === 'vertical' | |
| ? {index: 1, name: 'vertical'} | |
| : {index: 0, name: 'horizontal'}; | |
| } | |
| static defaultOption: LegendOption = { | |
| // zlevel: 0, | |
| z: 4, | |
| show: true, | |
| orient: 'horizontal', | |
| left: 'center', | |
| // right: 'center', | |
| top: 0, | |
| // bottom: null, | |
| align: 'auto', | |
| backgroundColor: 'rgba(0,0,0,0)', | |
| borderColor: '#ccc', | |
| borderRadius: 0, | |
| borderWidth: 0, | |
| padding: 5, | |
| itemGap: 10, | |
| itemWidth: 25, | |
| itemHeight: 14, | |
| symbolRotate: 'inherit', | |
| symbolKeepAspect: true, | |
| inactiveColor: '#ccc', | |
| inactiveBorderColor: '#ccc', | |
| inactiveBorderWidth: 'auto', | |
| itemStyle: { | |
| color: 'inherit', | |
| opacity: 'inherit', | |
| borderColor: 'inherit', | |
| borderWidth: 'auto', | |
| borderCap: 'inherit', | |
| borderJoin: 'inherit', | |
| borderDashOffset: 'inherit', | |
| borderMiterLimit: 'inherit' | |
| }, | |
| lineStyle: { | |
| width: 'auto', | |
| color: 'inherit', | |
| inactiveColor: '#ccc', | |
| inactiveWidth: 2, | |
| opacity: 'inherit', | |
| type: 'inherit', | |
| cap: 'inherit', | |
| join: 'inherit', | |
| dashOffset: 'inherit', | |
| miterLimit: 'inherit' | |
| }, | |
| textStyle: { | |
| color: '#333' | |
| }, | |
| selectedMode: true, | |
| selector: false, | |
| selectorLabel: { | |
| show: true, | |
| borderRadius: 10, | |
| padding: [3, 5, 3, 5], | |
| fontSize: 12, | |
| fontFamily: 'sans-serif', | |
| color: '#666', | |
| borderWidth: 1, | |
| borderColor: '#666' | |
| }, | |
| emphasis: { | |
| selectorLabel: { | |
| show: true, | |
| color: '#eee', | |
| backgroundColor: '#666' | |
| } | |
| }, | |
| selectorPosition: 'auto', | |
| selectorItemGap: 7, | |
| selectorButtonGap: 10, | |
| tooltip: { | |
| show: false | |
| } | |
| }; | |
| } | |
| export default LegendModel; |