| /* | 
 | * 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; |