| /* |
| * 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 {retrieveRawValue} from '../data/helper/dataProvider'; |
| 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, SeriesOption} 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 diferrent 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 (typeof seriesModel.enableAriaDecal === 'function') { |
| // 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 dataLabels = []; |
| for (let i = 0; i < data.count(); i++) { |
| if (i < maxDataCnt) { |
| const name = data.getName(i); |
| const value = retrieveRawValue(data, i); |
| const dataLabel = labelModel.get(['data', name ? 'withName' : 'withoutName']); |
| dataLabels.push( |
| replace(dataLabel, { |
| name: name, |
| value: value |
| }) |
| ); |
| } |
| } |
| const middleSeparator = labelModel.get(['data', 'separator', 'middle']); |
| const endSeparator = labelModel.get(['data', 'separator', 'end']); |
| 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 (typeof str !== 'string') { |
| 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] || '自定义图'; |
| } |
| } |