echarts tooltipMarkup 源码
echarts tooltipMarkup 代码
文件路径:/src/component/tooltip/tooltipMarkup.ts
/*
* 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 {
Dictionary, TooltipRenderMode, ColorString,
TooltipOrderMode, DimensionType, CommonTooltipOption, OptionDataValue
} from '../../util/types';
import {
TooltipMarkerType, getTooltipMarker, encodeHTML,
makeValueReadable, convertToColorString
} from '../../util/format';
import { isString, each, hasOwn, isArray, map, assert, extend } from 'zrender/src/core/util';
import { SortOrderComparator } from '../../data/helper/dataValueHelper';
import SeriesModel from '../../model/Series';
import { getRandomIdBase } from '../../util/number';
import Model from '../../model/Model';
import { TooltipOption } from './TooltipModel';
type RichTextStyle = {
fontSize: number | string,
fill: string,
fontWeight?: number | string
};
type TextStyle = string | RichTextStyle;
const TOOLTIP_LINE_HEIGHT_CSS = 'line-height:1';
// TODO: more textStyle option
function getTooltipTextStyle(
textStyle: TooltipOption['textStyle'],
renderMode: TooltipRenderMode
): {
nameStyle: TextStyle
valueStyle: TextStyle
} {
const nameFontColor = textStyle.color || '#6e7079';
const nameFontSize = textStyle.fontSize || 12;
const nameFontWeight = textStyle.fontWeight || '400';
const valueFontColor = textStyle.color || '#464646';
const valueFontSize = textStyle.fontSize || 14;
const valueFontWeight = textStyle.fontWeight || '900';
if (renderMode === 'html') {
// `textStyle` is probably from user input, should be encoded to reduce security risk.
return {
// eslint-disable-next-line max-len
nameStyle: `font-size:${encodeHTML(nameFontSize + '')}px;color:${encodeHTML(nameFontColor)};font-weight:${encodeHTML(nameFontWeight + '')}`,
// eslint-disable-next-line max-len
valueStyle: `font-size:${encodeHTML(valueFontSize + '')}px;color:${encodeHTML(valueFontColor)};font-weight:${encodeHTML(valueFontWeight + '')}`
};
}
else {
return {
nameStyle: {
fontSize: nameFontSize,
fill: nameFontColor,
fontWeight: nameFontWeight
},
valueStyle: {
fontSize: valueFontSize,
fill: valueFontColor,
fontWeight: valueFontWeight
}
};
}
}
// 0: no gap in this block.
// 1: has max gap in level 1 in this block.
// ...
type GapLevel = number;
// See `TooltipMarkupLayoutIntent['innerGapLevel']`.
// (value from UI design)
const HTML_GAPS: Record<GapLevel, number> = [0, 10, 20, 30];
const RICH_TEXT_GAPS: Record<GapLevel, string> = ['', '\n', '\n\n', '\n\n\n'];
/**
* This is an abstract layer to insulate the upper usage of tooltip content
* from the different backends according to different `renderMode` ('html' or 'richText').
* With the help of the abstract layer, it does not need to consider how to create and
* assemble html or richText snippets when making tooltip content.
*
* @usage
*
* ```ts
* class XxxSeriesModel {
* formatTooltip(
* dataIndex: number,
* multipleSeries: boolean,
* dataType: string
* ) {
* ...
* return createTooltipMarkup('section', {
* header: header,
* blocks: [
* createTooltipMarkup('nameValue', {
* name: name,
* value: value,
* noValue: value == null
* })
* ]
* });
* }
* }
* ```
*/
export type TooltipMarkupBlockFragment =
TooltipMarkupSection
| TooltipMarkupNameValueBlock;
interface TooltipMarkupBlock {
// Use to make comparison when `sortBlocks: true`.
sortParam?: unknown;
}
export interface TooltipMarkupSection extends TooltipMarkupBlock {
type: 'section';
header?: unknown;
// If `noHeader` is `true`, do not display header.
// Otherwise, always display it even if it is
// null/undefined/NaN/''... (displayed as '-').
noHeader?: boolean;
blocks?: TooltipMarkupBlockFragment[];
// Enable to sort blocks when making final html or richText.
sortBlocks?: boolean;
valueFormatter?: CommonTooltipOption<unknown>['valueFormatter']
}
export interface TooltipMarkupNameValueBlock extends TooltipMarkupBlock {
type: 'nameValue';
// If `!markerType`, tooltip marker is not used.
markerType?: TooltipMarkerType;
markerColor?: ColorString;
name?: string;
// Also support value is `[121, 555, 94.2]`.
value?: unknown | unknown[];
// If not specified, treat value as normal string or numeric.
// If needs to display formatted time, set as 'time'.
// If needs to display original string with numeric guessing, set as 'ordinal'.
// If both `value` and `valueType` are array, each valueType[i] cooresponds to value[i].
valueType?: DimensionType | DimensionType[];
// If `noName` or `noValue` is `true`, do not display name or value.
// Otherwise, always display them even if they are
// null/undefined/NaN/''... (displayed as '-').
noName?: boolean;
noValue?: boolean;
valueFormatter?: CommonTooltipOption<unknown>['valueFormatter']
}
/**
* Create tooltip markup by this function, we can get TS type check.
*/
// eslint-disable-next-line max-len
export function createTooltipMarkup(type: 'section', option: Omit<TooltipMarkupSection, 'type'>): TooltipMarkupSection;
// eslint-disable-next-line max-len
export function createTooltipMarkup(type: 'nameValue', option: Omit<TooltipMarkupNameValueBlock, 'type'>): TooltipMarkupNameValueBlock;
// eslint-disable-next-line max-len
export function createTooltipMarkup(type: TooltipMarkupBlockFragment['type'], option: Omit<TooltipMarkupBlockFragment, 'type'>): TooltipMarkupBlockFragment {
(option as TooltipMarkupBlockFragment).type = type;
return option as TooltipMarkupBlockFragment;
}
// Can be null/undefined, which means generate nothing markup text.
type MarkupText = string;
interface TooltipMarkupFragmentBuilder {
(
ctx: TooltipMarkupBuildContext,
fragment: TooltipMarkupBlockFragment,
topMarginForOuterGap: number,
toolTipTextStyle: TooltipOption['textStyle']
): MarkupText;
}
function isSectionFragment(frag: TooltipMarkupBlockFragment): frag is TooltipMarkupSection {
return frag.type === 'section';
}
function getBuilder(frag: TooltipMarkupBlockFragment): TooltipMarkupFragmentBuilder {
return isSectionFragment(frag) ? buildSection : buildNameValue;
}
function getBlockGapLevel(frag: TooltipMarkupBlockFragment) {
if (isSectionFragment(frag)) {
let gapLevel = 0;
const subBlockLen = frag.blocks.length;
const hasInnerGap = subBlockLen > 1 || (subBlockLen > 0 && !frag.noHeader);
each(frag.blocks, function (subBlock) {
const subGapLevel = getBlockGapLevel(subBlock);
// If the some of the sub-blocks have some gaps (like 10px) inside, this block
// should use a larger gap (like 20px) to distinguish those sub-blocks.
if (subGapLevel >= gapLevel) {
gapLevel = subGapLevel + (
+(
hasInnerGap && (
// 0 always can not be readable gap level.
!subGapLevel
// If no header, always keep the sub gap level. Otherwise
// look weird in case `multipleSeries`.
|| (isSectionFragment(subBlock) && !subBlock.noHeader)
)
)
);
}
});
return gapLevel;
}
return 0;
}
function buildSection(
ctx: TooltipMarkupBuildContext,
fragment: TooltipMarkupSection,
topMarginForOuterGap: number,
toolTipTextStyle: TooltipOption['textStyle']
) {
const noHeader = fragment.noHeader;
const gaps = getGap(getBlockGapLevel(fragment));
const subMarkupTextList: string[] = [];
let subBlocks = fragment.blocks || [];
assert(!subBlocks || isArray(subBlocks));
subBlocks = subBlocks || [];
const orderMode = ctx.orderMode;
if (fragment.sortBlocks && orderMode) {
subBlocks = subBlocks.slice();
const orderMap = { valueAsc: 'asc', valueDesc: 'desc' } as const;
if (hasOwn(orderMap, orderMode)) {
const comparator = new SortOrderComparator(orderMap[orderMode as 'valueAsc' | 'valueDesc'], null);
subBlocks.sort((a, b) => comparator.evaluate(a.sortParam, b.sortParam));
}
// FIXME 'seriesDesc' necessary?
else if (orderMode === 'seriesDesc') {
subBlocks.reverse();
}
}
each(subBlocks, function (subBlock, idx) {
const valueFormatter = fragment.valueFormatter;
const subMarkupText = getBuilder(subBlock)(
// Inherit valueFormatter
valueFormatter ? extend(extend({}, ctx), { valueFormatter }) : ctx,
subBlock,
idx > 0 ? gaps.html : 0,
toolTipTextStyle
);
subMarkupText != null && subMarkupTextList.push(subMarkupText);
});
const subMarkupText = ctx.renderMode === 'richText'
? subMarkupTextList.join(gaps.richText)
: wrapBlockHTML(
subMarkupTextList.join(''),
noHeader ? topMarginForOuterGap : gaps.html
);
if (noHeader) {
return subMarkupText;
}
const displayableHeader = makeValueReadable(fragment.header, 'ordinal', ctx.useUTC);
const {nameStyle} = getTooltipTextStyle(toolTipTextStyle, ctx.renderMode);
if (ctx.renderMode === 'richText') {
return wrapInlineNameRichText(ctx, displayableHeader, nameStyle as RichTextStyle) + gaps.richText
+ subMarkupText;
}
else {
return wrapBlockHTML(
`<div style="${nameStyle};${TOOLTIP_LINE_HEIGHT_CSS};">`
+ encodeHTML(displayableHeader)
+ '</div>'
+ subMarkupText,
topMarginForOuterGap
);
}
}
function buildNameValue(
ctx: TooltipMarkupBuildContext,
fragment: TooltipMarkupNameValueBlock,
topMarginForOuterGap: number,
toolTipTextStyle: TooltipOption['textStyle']
) {
const renderMode = ctx.renderMode;
const noName = fragment.noName;
const noValue = fragment.noValue;
const noMarker = !fragment.markerType;
const name = fragment.name;
const useUTC = ctx.useUTC;
const valueFormatter = fragment.valueFormatter || ctx.valueFormatter || ((value) => {
value = isArray(value) ? value : [value];
return map(value as unknown[], (val, idx) => makeValueReadable(
val, isArray(valueTypeOption) ? valueTypeOption[idx] : valueTypeOption, useUTC
));
});
if (noName && noValue) {
return;
}
const markerStr = noMarker
? ''
: ctx.markupStyleCreator.makeTooltipMarker(
fragment.markerType,
fragment.markerColor || '#333',
renderMode
);
const readableName = noName
? ''
: makeValueReadable(name, 'ordinal', useUTC);
const valueTypeOption = fragment.valueType;
const readableValueList = noValue ? [] : valueFormatter(fragment.value as OptionDataValue);
const valueAlignRight = !noMarker || !noName;
// It little weird if only value next to marker but far from marker.
const valueCloseToMarker = !noMarker && noName;
const {nameStyle, valueStyle} = getTooltipTextStyle(toolTipTextStyle, renderMode);
return renderMode === 'richText'
? (
(noMarker ? '' : markerStr)
+ (noName ? '' : wrapInlineNameRichText(ctx, readableName, nameStyle as RichTextStyle))
// Value has commas inside, so use ' ' as delimiter for multiple values.
+ (noValue ? '' : wrapInlineValueRichText(
ctx, readableValueList, valueAlignRight, valueCloseToMarker, valueStyle as RichTextStyle
))
)
: wrapBlockHTML(
(noMarker ? '' : markerStr)
+ (noName ? '' : wrapInlineNameHTML(readableName, !noMarker, nameStyle as string))
+ (noValue ? '' : wrapInlineValueHTML(
readableValueList, valueAlignRight, valueCloseToMarker, valueStyle as string
)),
topMarginForOuterGap
);
}
interface TooltipMarkupBuildContext {
useUTC: boolean;
renderMode: TooltipRenderMode;
orderMode: TooltipOrderMode;
markupStyleCreator: TooltipMarkupStyleCreator;
valueFormatter: CommonTooltipOption<unknown>['valueFormatter']
}
/**
* @return markupText. null/undefined means no content.
*/
export function buildTooltipMarkup(
fragment: TooltipMarkupBlockFragment,
markupStyleCreator: TooltipMarkupStyleCreator,
renderMode: TooltipRenderMode,
orderMode: TooltipOrderMode,
useUTC: boolean,
toolTipTextStyle: TooltipOption['textStyle']
): MarkupText {
if (!fragment) {
return;
}
const builder = getBuilder(fragment);
const ctx: TooltipMarkupBuildContext = {
useUTC: useUTC,
renderMode: renderMode,
orderMode: orderMode,
markupStyleCreator: markupStyleCreator,
valueFormatter: fragment.valueFormatter
};
return builder(ctx, fragment, 0, toolTipTextStyle);
}
function getGap(gapLevel: number): {
html: number;
richText: string
} {
return {
html: HTML_GAPS[gapLevel],
richText: RICH_TEXT_GAPS[gapLevel]
};
}
function wrapBlockHTML(
encodedContent: string,
topGap: number
): string {
const clearfix = '<div style="clear:both"></div>';
const marginCSS = `margin: ${topGap}px 0 0`;
return `<div style="${marginCSS};${TOOLTIP_LINE_HEIGHT_CSS};">`
+ encodedContent + clearfix
+ '</div>';
}
function wrapInlineNameHTML(
name: string,
leftHasMarker: boolean,
style: string
): string {
const marginCss = leftHasMarker ? 'margin-left:2px' : '';
return `<span style="${style};${marginCss}">`
+ encodeHTML(name)
+ '</span>';
}
function wrapInlineValueHTML(
valueList: string | string[],
alignRight: boolean,
valueCloseToMarker: boolean,
style: string
): string {
// Do not too close to marker, considering there are multiple values separated by spaces.
const paddingStr = valueCloseToMarker ? '10px' : '20px';
const alignCSS = alignRight ? `float:right;margin-left:${paddingStr}` : '';
valueList = isArray(valueList) ? valueList : [valueList];
return (
`<span style="${alignCSS};${style}">`
// Value has commas inside, so use ' ' as delimiter for multiple values.
+ map(valueList, value => encodeHTML(value)).join(' ')
+ '</span>'
);
}
function wrapInlineNameRichText(ctx: TooltipMarkupBuildContext, name: string, style: RichTextStyle): string {
return ctx.markupStyleCreator.wrapRichTextStyle(name, style as Dictionary<unknown>);
}
function wrapInlineValueRichText(
ctx: TooltipMarkupBuildContext,
values: string | string[],
alignRight: boolean,
valueCloseToMarker: boolean,
style: RichTextStyle
): string {
const styles: Dictionary<unknown>[] = [style];
const paddingLeft = valueCloseToMarker ? 10 : 20;
alignRight && styles.push({ padding: [0, 0, 0, paddingLeft], align: 'right' });
// Value has commas inside, so use ' ' as delimiter for multiple values.
return ctx.markupStyleCreator.wrapRichTextStyle(
isArray(values) ? values.join(' ') : values,
styles
);
}
export function retrieveVisualColorForTooltipMarker(
series: SeriesModel,
dataIndex: number
): ColorString {
const style = series.getData().getItemVisual(dataIndex, 'style');
const color = style[series.visualDrawType];
return convertToColorString(color);
}
export function getPaddingFromTooltipModel(
model: Model<TooltipOption>,
renderMode: TooltipRenderMode
): number | number[] {
const padding = model.get('padding');
return padding != null
? padding
// We give slightly different to look pretty.
: renderMode === 'richText'
? [8, 10]
: 10;
}
/**
* The major feature is generate styles for `renderMode: 'richText'`.
* But it also serves `renderMode: 'html'` to provide
* "renderMode-independent" API.
*/
export class TooltipMarkupStyleCreator {
readonly richTextStyles: Dictionary<Dictionary<unknown>> = {};
// Notice that "generate a style name" usuall happens repeatly when mouse moving and
// displaying a tooltip. So we put the `_nextStyleNameId` as a member of each creator
// rather than static shared by all creators (which will cause it increase to fast).
private _nextStyleNameId: number = getRandomIdBase();
private _generateStyleName() {
return '__EC_aUTo_' + this._nextStyleNameId++;
}
makeTooltipMarker(
markerType: TooltipMarkerType,
colorStr: ColorString,
renderMode: TooltipRenderMode
): string {
const markerId = renderMode === 'richText'
? this._generateStyleName()
: null;
const marker = getTooltipMarker({
color: colorStr,
type: markerType,
renderMode,
markerId: markerId
});
if (isString(marker)) {
return marker;
}
else {
if (__DEV__) {
assert(markerId);
}
this.richTextStyles[markerId] = marker.style;
return marker.content;
}
}
/**
* @usage
* ```ts
* const styledText = markupStyleCreator.wrapRichTextStyle([
* // The styles will be auto merged.
* {
* fontSize: 12,
* color: 'blue'
* },
* {
* padding: 20
* }
* ]);
* ```
*/
wrapRichTextStyle(text: string, styles: Dictionary<unknown> | Dictionary<unknown>[]): string {
const finalStl = {};
if (isArray(styles)) {
each(styles, stl => extend(finalStl, stl));
}
else {
extend(finalStl, styles);
}
const styleName = this._generateStyleName();
this.richTextStyles[styleName] = finalStl;
return `{${styleName}|${text}}`;
}
}
相关信息
相关文章
0
赞
热门推荐
-
2、 - 优质文章
-
3、 gate.io
-
8、 golang
-
9、 openharmony
-
10、 Vue中input框自动聚焦