blob: baa1e8fdd1a4a83e36f96082274cff59aa5e0872 [file] [log] [blame]
/*
* 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] || '自定义图';
}
}