|  | /* | 
|  | * 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 ExtensionAPI from '../core/ExtensionAPI'; | 
|  | import GlobalModel from '../model/Global'; | 
|  | import Model from '../model/Model'; | 
|  | import SeriesModel from '../model/Series'; | 
|  | import {makeInner} from '../util/model'; | 
|  | import {Dictionary, DecalObject, InnerDecalObject, AriaOption} from '../util/types'; | 
|  | import {LocaleOption} from '../core/locale'; | 
|  | import { getDecalFromPalette } from '../model/mixin/palette'; | 
|  | import type {TitleOption} from '../component/title/install'; | 
|  |  | 
|  | const DEFAULT_OPTION: AriaOption = { | 
|  | label: { | 
|  | enabled: true | 
|  | }, | 
|  | decal: { | 
|  | show: false | 
|  | } | 
|  | }; | 
|  |  | 
|  | const inner = makeInner<{scope: object}, SeriesModel>(); | 
|  |  | 
|  | const decalPaletteScope: Dictionary<DecalObject> = {}; | 
|  |  | 
|  | type SeriesTypes = keyof LocaleOption['series']['typeNames']; | 
|  |  | 
|  | export default function ariaVisual(ecModel: GlobalModel, api: ExtensionAPI) { | 
|  | const ariaModel: Model<AriaOption> = ecModel.getModel('aria'); | 
|  |  | 
|  | // See "area enabled" detection code in `GlobalModel.ts`. | 
|  | if (!ariaModel.get('enabled')) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | const defaultOption = zrUtil.clone(DEFAULT_OPTION); | 
|  | zrUtil.merge(defaultOption.label, ecModel.getLocaleModel().get('aria'), false); | 
|  | zrUtil.merge(ariaModel.option, defaultOption, false); | 
|  |  | 
|  | setDecal(); | 
|  | setLabel(); | 
|  |  | 
|  | function setDecal() { | 
|  | const decalModel = ariaModel.getModel('decal'); | 
|  |  | 
|  | const useDecal = decalModel.get('show'); | 
|  | if (useDecal) { | 
|  | // Each type of series use one scope. | 
|  | // Pie and funnel are using different scopes. | 
|  | const paletteScopeGroupByType = zrUtil.createHashMap<object>(); | 
|  | ecModel.eachSeries((seriesModel: SeriesModel) => { | 
|  | if (seriesModel.isColorBySeries()) { | 
|  | return; | 
|  | } | 
|  | let decalScope = paletteScopeGroupByType.get(seriesModel.type); | 
|  | if (!decalScope) { | 
|  | decalScope = {}; | 
|  | paletteScopeGroupByType.set(seriesModel.type, decalScope); | 
|  | } | 
|  | inner(seriesModel).scope = decalScope; | 
|  | }); | 
|  |  | 
|  | ecModel.eachRawSeries((seriesModel: SeriesModel) => { | 
|  | if (ecModel.isSeriesFiltered(seriesModel)) { | 
|  | return; | 
|  | } | 
|  | if (zrUtil.isFunction(seriesModel.enableAriaDecal)) { | 
|  | // Let series define how to use decal palette on data | 
|  | seriesModel.enableAriaDecal(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | const data = seriesModel.getData(); | 
|  |  | 
|  | if (!seriesModel.isColorBySeries()) { | 
|  | const dataAll = seriesModel.getRawData(); | 
|  | const idxMap: Dictionary<number> = {}; | 
|  | const decalScope = inner(seriesModel).scope; | 
|  |  | 
|  | data.each(function (idx) { | 
|  | const rawIdx = data.getRawIndex(idx); | 
|  | idxMap[rawIdx] = idx; | 
|  | }); | 
|  |  | 
|  | const dataCount = dataAll.count(); | 
|  | dataAll.each(rawIdx => { | 
|  | const idx = idxMap[rawIdx]; | 
|  | const name = dataAll.getName(rawIdx) || (rawIdx + ''); | 
|  | const paletteDecal = getDecalFromPalette( | 
|  | seriesModel.ecModel, | 
|  | name, | 
|  | decalScope, | 
|  | dataCount | 
|  | ); | 
|  | const specifiedDecal = data.getItemVisual(idx, 'decal'); | 
|  | data.setItemVisual(idx, 'decal', mergeDecal(specifiedDecal, paletteDecal)); | 
|  | }); | 
|  | } | 
|  | else { | 
|  | const paletteDecal = getDecalFromPalette( | 
|  | seriesModel.ecModel, | 
|  | seriesModel.name, | 
|  | decalPaletteScope, | 
|  | ecModel.getSeriesCount() | 
|  | ); | 
|  | const specifiedDecal = data.getVisual('decal'); | 
|  | data.setVisual('decal', mergeDecal(specifiedDecal, paletteDecal)); | 
|  | } | 
|  |  | 
|  | function mergeDecal(specifiedDecal: DecalObject, paletteDecal: DecalObject): DecalObject { | 
|  | // Merge decal from palette to decal from itemStyle. | 
|  | // User do not need to specify all of the decal props. | 
|  | const resultDecal = specifiedDecal | 
|  | ? zrUtil.extend(zrUtil.extend({}, paletteDecal), specifiedDecal) | 
|  | : paletteDecal; | 
|  | (resultDecal as InnerDecalObject).dirty = true; | 
|  | return resultDecal; | 
|  | } | 
|  | }); | 
|  | } | 
|  | } | 
|  |  | 
|  | function setLabel() { | 
|  | const labelLocale = ecModel.getLocaleModel().get('aria'); | 
|  | const labelModel = ariaModel.getModel('label'); | 
|  | labelModel.option = zrUtil.defaults(labelModel.option, labelLocale); | 
|  |  | 
|  | if (!labelModel.get('enabled')) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | const dom = api.getZr().dom; | 
|  | if (labelModel.get('description')) { | 
|  | dom.setAttribute('aria-label', labelModel.get('description')); | 
|  | return; | 
|  | } | 
|  |  | 
|  | const seriesCnt = ecModel.getSeriesCount(); | 
|  | const maxDataCnt = labelModel.get(['data', 'maxCount']) || 10; | 
|  | const maxSeriesCnt = labelModel.get(['series', 'maxCount']) || 10; | 
|  | const displaySeriesCnt = Math.min(seriesCnt, maxSeriesCnt); | 
|  |  | 
|  | let ariaLabel; | 
|  | if (seriesCnt < 1) { | 
|  | // No series, no aria label | 
|  | return; | 
|  | } | 
|  | else { | 
|  | const title = getTitle(); | 
|  | if (title) { | 
|  | const withTitle = labelModel.get(['general', 'withTitle']); | 
|  | ariaLabel = replace(withTitle, { | 
|  | title: title | 
|  | }); | 
|  | } | 
|  | else { | 
|  | ariaLabel = labelModel.get(['general', 'withoutTitle']); | 
|  | } | 
|  |  | 
|  | const seriesLabels: string[] = []; | 
|  | const prefix = seriesCnt > 1 | 
|  | ? labelModel.get(['series', 'multiple', 'prefix']) | 
|  | : labelModel.get(['series', 'single', 'prefix']); | 
|  | ariaLabel += replace(prefix, { seriesCount: seriesCnt }); | 
|  |  | 
|  | ecModel.eachSeries(function (seriesModel, idx) { | 
|  | if (idx < displaySeriesCnt) { | 
|  | let seriesLabel; | 
|  |  | 
|  | const seriesName = seriesModel.get('name'); | 
|  | const withName = seriesName ? 'withName' : 'withoutName'; | 
|  | seriesLabel = seriesCnt > 1 | 
|  | ? labelModel.get(['series', 'multiple', withName]) | 
|  | : labelModel.get(['series', 'single', withName]); | 
|  |  | 
|  | seriesLabel = replace(seriesLabel, { | 
|  | seriesId: seriesModel.seriesIndex, | 
|  | seriesName: seriesModel.get('name'), | 
|  | seriesType: getSeriesTypeName(seriesModel.subType as SeriesTypes) | 
|  | }); | 
|  |  | 
|  | const data = seriesModel.getData(); | 
|  | if (data.count() > maxDataCnt) { | 
|  | // Show part of data | 
|  | const partialLabel = labelModel.get(['data', 'partialData']); | 
|  | seriesLabel += replace(partialLabel, { | 
|  | displayCnt: maxDataCnt | 
|  | }); | 
|  | } | 
|  | else { | 
|  | seriesLabel += labelModel.get(['data', 'allData']); | 
|  | } | 
|  |  | 
|  | const middleSeparator = labelModel.get(['data', 'separator', 'middle']); | 
|  | const endSeparator = labelModel.get(['data', 'separator', 'end']); | 
|  | const dataLabels = []; | 
|  | for (let i = 0; i < data.count(); i++) { | 
|  | if (i < maxDataCnt) { | 
|  | const name = data.getName(i); | 
|  | const value = data.getValues(i); | 
|  | const dataLabel = labelModel.get(['data', name ? 'withName' : 'withoutName']); | 
|  | dataLabels.push( | 
|  | replace(dataLabel, { | 
|  | name: name, | 
|  | value: value.join(middleSeparator) | 
|  | }) | 
|  | ); | 
|  | } | 
|  | } | 
|  | seriesLabel += dataLabels.join(middleSeparator) + endSeparator; | 
|  |  | 
|  | seriesLabels.push(seriesLabel); | 
|  | } | 
|  | }); | 
|  |  | 
|  | const separatorModel = labelModel.getModel(['series', 'multiple', 'separator']); | 
|  | const middleSeparator = separatorModel.get('middle'); | 
|  | const endSeparator = separatorModel.get('end'); | 
|  | ariaLabel += seriesLabels.join(middleSeparator) + endSeparator; | 
|  |  | 
|  | dom.setAttribute('aria-label', ariaLabel); | 
|  | } | 
|  | } | 
|  |  | 
|  | function replace(str: string, keyValues: object) { | 
|  | if (!zrUtil.isString(str)) { | 
|  | return str; | 
|  | } | 
|  |  | 
|  | let result = str; | 
|  | zrUtil.each(keyValues, function (value: string, key: string) { | 
|  | result = result.replace( | 
|  | new RegExp('\\{\\s*' + key + '\\s*\\}', 'g'), | 
|  | value | 
|  | ); | 
|  | }); | 
|  | return result; | 
|  | } | 
|  |  | 
|  | function getTitle() { | 
|  | let title = ecModel.get('title') as TitleOption | TitleOption[]; | 
|  | if (title && (title as TitleOption[]).length) { | 
|  | title = (title as TitleOption[])[0]; | 
|  | } | 
|  | return title && (title as TitleOption).text; | 
|  | } | 
|  |  | 
|  | function getSeriesTypeName(type: SeriesTypes) { | 
|  | return ecModel.getLocaleModel().get(['series', 'typeNames'])[type] || '自定义图'; | 
|  | } | 
|  | } |