You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by sh...@apache.org on 2022/01/05 09:55:29 UTC

[echarts] 01/01: feat(tooltip): add valueFormatter

This is an automated email from the ASF dual-hosted git repository.

shenyi pushed a commit to branch tooltip-value-formatter
in repository https://gitbox.apache.org/repos/asf/echarts.git

commit 4c7d9a4ab57e6da1a7ce89d494ab73a1a7cc1883
Author: pissang <bm...@gmail.com>
AuthorDate: Wed Jan 5 17:54:03 2022 +0800

    feat(tooltip): add valueFormatter
---
 src/component/tooltip/TooltipView.ts   |  24 ++-
 src/component/tooltip/tooltipMarkup.ts | 320 +++++++++++++++------------------
 src/model/mixin/dataFormat.ts          |  13 +-
 src/util/types.ts                      |   7 +
 test/tooltip-valueFormatter.html       |  96 ++++++++++
 5 files changed, 266 insertions(+), 194 deletions(-)

diff --git a/src/component/tooltip/TooltipView.ts b/src/component/tooltip/TooltipView.ts
index bc1e3a7..fc69ecf 100644
--- a/src/component/tooltip/TooltipView.ts
+++ b/src/component/tooltip/TooltipView.ts
@@ -16,7 +16,7 @@
 * specific language governing permissions and limitations
 * under the License.
 */
-import { bind, each, clone, trim, isString, isFunction, isArray, isObject } from 'zrender/src/core/util';
+import { bind, each, clone, trim, isString, isFunction, isArray, isObject, extend } from 'zrender/src/core/util';
 import env from 'zrender/src/core/env';
 import TooltipHTMLContent from './TooltipHTMLContent';
 import TooltipRichContent from './TooltipRichContent';
@@ -585,11 +585,16 @@ class TooltipView extends ComponentView {
                     const seriesTooltipResult = normalizeTooltipFormatResult(
                         series.formatTooltip(dataIndex, true, null)
                     );
-                    if (seriesTooltipResult.markupFragment) {
-                        axisSectionMarkup.blocks.push(seriesTooltipResult.markupFragment);
+                    const frag = seriesTooltipResult.frag;
+                    if (frag) {
+                        const valueFormatter = buildTooltipModel(
+                            [series as Model<TooltipableOption>],
+                            globalTooltipModel
+                        ).get('valueFormatter');
+                        axisSectionMarkup.blocks.push(valueFormatter ? extend({ valueFormatter }, frag) : frag);
                     }
-                    if (seriesTooltipResult.markupText) {
-                        markupTextArrLegacy.push(seriesTooltipResult.markupText);
+                    if (seriesTooltipResult.text) {
+                        markupTextArrLegacy.push(seriesTooltipResult.text);
                     }
                     cbParamsList.push(cbParams);
                 });
@@ -682,16 +687,17 @@ class TooltipView extends ComponentView {
             dataModel.formatTooltip(dataIndex, false, dataType)
         );
         const orderMode = tooltipModel.get('order');
-        const markupText = seriesTooltipResult.markupFragment
-            ? buildTooltipMarkup(
-                seriesTooltipResult.markupFragment,
+        const valueFormatter = tooltipModel.get('valueFormatter');
+        const frag = seriesTooltipResult.frag;
+        const markupText = frag ? buildTooltipMarkup(
+                valueFormatter ? extend({ valueFormatter }, frag) : frag,
                 markupStyleCreator,
                 renderMode,
                 orderMode,
                 ecModel.get('useUTC'),
                 tooltipModel.get('textStyle')
             )
-            : seriesTooltipResult.markupText;
+            : seriesTooltipResult.text;
 
         const asyncTicket = 'item_' + dataModel.name + '_' + dataIndex;
 
diff --git a/src/component/tooltip/tooltipMarkup.ts b/src/component/tooltip/tooltipMarkup.ts
index 2e88c1d..03aeab0 100644
--- a/src/component/tooltip/tooltipMarkup.ts
+++ b/src/component/tooltip/tooltipMarkup.ts
@@ -19,7 +19,7 @@
 
 import {
     Dictionary, TooltipRenderMode, ColorString,
-    TooltipOrderMode, DimensionType
+    TooltipOrderMode, DimensionType, CommonTooltipOption, OptionDataValue
 } from '../../util/types';
 import {
     TooltipMarkerType, getTooltipMarker, encodeHTML,
@@ -88,8 +88,8 @@ function getTooltipTextStyle(
 type GapLevel = number;
 // See `TooltipMarkupLayoutIntent['innerGapLevel']`.
 // (value from UI design)
-const HTML_GAPS: { [key in GapLevel]: number } = [0, 10, 20, 30];
-const RICH_TEXT_GAPS: { [key in GapLevel]: string } = ['', '\n', '\n\n', '\n\n\n'];
+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
@@ -128,7 +128,6 @@ export type TooltipMarkupBlockFragment =
 interface TooltipMarkupBlock {
     // Use to make comparison when `sortBlocks: true`.
     sortParam?: unknown;
-    __gapLevelBetweenSubBlocks?: number;
 }
 
 export interface TooltipMarkupSection extends TooltipMarkupBlock {
@@ -141,6 +140,8 @@ export interface TooltipMarkupSection extends TooltipMarkupBlock {
     blocks?: TooltipMarkupBlockFragment[];
     // Enable to sort blocks when making final html or richText.
     sortBlocks?: boolean;
+
+    valueFormatter?: CommonTooltipOption<unknown>['valueFormatter']
 }
 
 export interface TooltipMarkupNameValueBlock extends TooltipMarkupBlock {
@@ -161,6 +162,8 @@ export interface TooltipMarkupNameValueBlock extends TooltipMarkupBlock {
     // null/undefined/NaN/''... (displayed as '-').
     noName?: boolean;
     noValue?: boolean;
+
+    valueFormatter?: CommonTooltipOption<unknown>['valueFormatter']
 }
 
 /**
@@ -180,10 +183,7 @@ export function createTooltipMarkup(type: TooltipMarkupBlockFragment['type'], op
 // Can be null/undefined, which means generate nothing markup text.
 type MarkupText = string;
 interface TooltipMarkupFragmentBuilder {
-    planLayout(
-        fragment: TooltipMarkupBlockFragment
-    ): void;
-    build(
+    (
         ctx: TooltipMarkupBuildContext,
         fragment: TooltipMarkupBlockFragment,
         topMarginForOuterGap: number,
@@ -191,167 +191,51 @@ interface TooltipMarkupFragmentBuilder {
     ): MarkupText;
 }
 
-function getBuilder(fragment: TooltipMarkupBlockFragment): TooltipMarkupFragmentBuilder {
-    return hasOwn(builderMap, fragment.type) && builderMap[fragment.type];
+function isSectionFragment(frag: TooltipMarkupBlockFragment): frag is TooltipMarkupSection {
+    return frag.type === 'section';
 }
 
-const builderMap: { [key in TooltipMarkupBlockFragment['type']]: TooltipMarkupFragmentBuilder } = {
-
-    /**
-     * A `section` block is like:
-     * ```
-     * header
-     * subBlock
-     * subBlock
-     * ...
-     * ```
-     */
-    section: {
-        planLayout: function (fragment: TooltipMarkupSection) {
-            const subBlockLen = fragment.blocks.length;
-            const thisBlockHasInnerGap = subBlockLen > 1 || (subBlockLen > 0 && !fragment.noHeader);
-
-            let thisGapLevelBetweenSubBlocks = 0;
-            each(fragment.blocks, function (subBlock) {
-                getBuilder(subBlock).planLayout(subBlock);
-                const subGapLevel = subBlock.__gapLevelBetweenSubBlocks;
-
-                // 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 >= thisGapLevelBetweenSubBlocks) {
-                    thisGapLevelBetweenSubBlocks = subGapLevel + (
-                        (
-                            thisBlockHasInnerGap && (
-                                // 0 always can not be readable gap level.
-                                !subGapLevel
-                                // If no header, always keep the sub gap level. Otherwise
-                                // look weird in case `multipleSeries`.
-                                || (subBlock.type === 'section' && !subBlock.noHeader)
-                            )
-                        ) ? 1 : 0
-                    );
-                }
-            });
-            fragment.__gapLevelBetweenSubBlocks = thisGapLevelBetweenSubBlocks;
-        },
-
-        build(
-            ctx,
-            fragment: TooltipMarkupSection,
-            topMarginForOuterGap,
-            toolTipTextStyle
-        ): string {
-            const noHeader = fragment.noHeader;
-            const gaps = getGap(fragment);
-
-            const subMarkupText = buildSubBlocks(
-                ctx,
-                fragment,
-                noHeader ? topMarginForOuterGap : gaps.html,
-                toolTipTextStyle
-            );
-
-            if (noHeader) {
-                return subMarkupText;
-            }
+function getBuilder(frag: TooltipMarkupBlockFragment): TooltipMarkupFragmentBuilder {
+    return isSectionFragment(frag) ? buildSection : buildNameValue;
+}
 
-            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 getBlockGapLevel(frag: TooltipMarkupBlockFragment) {
+    if (isSectionFragment(frag)) {
+        let thisGapLevelBetweenSubBlocks = 0;
+        const subBlockLen = frag.blocks.length;
+        const thisBlockHasInnerGap = 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 >= thisGapLevelBetweenSubBlocks) {
+                thisGapLevelBetweenSubBlocks = subGapLevel + (
+                    (
+                        thisBlockHasInnerGap && (
+                            // 0 always can not be readable gap level.
+                            !subGapLevel
+                            // If no header, always keep the sub gap level. Otherwise
+                            // look weird in case `multipleSeries`.
+                            || (subBlock.type === 'section' && !subBlock.noHeader)
+                        )
+                    ) ? 1 : 0
                 );
             }
-        }
-    },
-
-    /**
-     * A `nameValue` block is like:
-     * ```
-     * marker  name  value
-     * ```
-     */
-    nameValue: {
-        planLayout: function (fragment: TooltipMarkupNameValueBlock) {
-            fragment.__gapLevelBetweenSubBlocks = 0;
-        },
-
-        build(ctx, fragment: TooltipMarkupNameValueBlock, topMarginForOuterGap, toolTipTextStyle) {
-            const renderMode = ctx.renderMode;
-            const noName = fragment.noName;
-            const noValue = fragment.noValue;
-            const noMarker = !fragment.markerType;
-            const name = fragment.name;
-            const value = fragment.value;
-            const useUTC = ctx.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
-                ? []
-                : (isArray(value)
-                    ? map(value, (val, idx) => makeValueReadable(
-                        val, isArray(valueTypeOption) ? valueTypeOption[idx] : valueTypeOption, useUTC
-                    ))
-                    : [makeValueReadable(
-                        value, isArray(valueTypeOption) ? valueTypeOption[0] : valueTypeOption, useUTC
-                    )]
-                );
-            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
-                );
-        }
+        });
     }
-};
-
+    return 0;
+}
 
-function buildSubBlocks(
+function buildSection(
     ctx: TooltipMarkupBuildContext,
     fragment: TooltipMarkupSection,
     topMarginForOuterGap: number,
-    tooltipTextStyle: TooltipOption['textStyle']
-): MarkupText {
+    toolTipTextStyle: TooltipOption['textStyle']
+) {
+    const noHeader = fragment.noHeader;
+
+    const gaps = getGap(getBlockGapLevel(fragment));
+
     const subMarkupTextList: string[] = [];
     let subBlocks = fragment.blocks || [];
     assert(!subBlocks || isArray(subBlocks));
@@ -371,25 +255,102 @@ function buildSubBlocks(
         }
     }
 
-    const gaps = getGap(fragment);
     each(subBlocks, function (subBlock, idx) {
-        const subMarkupText = getBuilder(subBlock).build(
-            ctx,
+        const valueFormatter = fragment.valueFormatter;
+        const subMarkupText = getBuilder(subBlock)(
+            // Inherit valueFormatter
+            valueFormatter ? extend(extend({}, ctx), { valueFormatter }) : ctx,
             subBlock,
             idx > 0 ? gaps.html : 0,
-            tooltipTextStyle
+            toolTipTextStyle
         );
         subMarkupText != null && subMarkupTextList.push(subMarkupText);
     });
 
-    if (!subMarkupTextList.length) {
+    const subMarkupText = ctx.renderMode === 'richText'
+        ? subMarkupTextList.join(gaps.richText)
+        : wrapBlockHTML(
+            subMarkupTextList.join(''),
+            topMarginForOuterGap
+        );
+
+    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;
     }
 
-    return ctx.renderMode === 'richText'
-        ? subMarkupTextList.join(gaps.richText)
+    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(
-            subMarkupTextList.join(''),
+            (noMarker ? '' : markerStr)
+            + (noName ? '' : wrapInlineNameHTML(readableName, !noMarker, nameStyle as string))
+            + (noValue ? '' : wrapInlineValueHTML(
+                readableValueList, valueAlignRight, valueCloseToMarker, valueStyle as string
+            )),
             topMarginForOuterGap
         );
 }
@@ -399,6 +360,8 @@ interface TooltipMarkupBuildContext {
     renderMode: TooltipRenderMode;
     orderMode: TooltipOrderMode;
     markupStyleCreator: TooltipMarkupStyleCreator;
+
+    valueFormatter: CommonTooltipOption<unknown>['valueFormatter']
 }
 
 /**
@@ -417,25 +380,24 @@ export function buildTooltipMarkup(
     }
 
     const builder = getBuilder(fragment);
-    builder.planLayout(fragment);
     const ctx: TooltipMarkupBuildContext = {
         useUTC: useUTC,
         renderMode: renderMode,
         orderMode: orderMode,
-        markupStyleCreator: markupStyleCreator
+        markupStyleCreator: markupStyleCreator,
+        valueFormatter: fragment.valueFormatter
     };
-    return builder.build(ctx, fragment, 0, toolTipTextStyle);
+    return builder(ctx, fragment, 0, toolTipTextStyle);
 }
 
 
-function getGap(fragment: TooltipMarkupBlock): {
+function getGap(gapLevel: number): {
     html: number;
     richText: string
 } {
-    const gapLevelBetweenSubBlocks = fragment.__gapLevelBetweenSubBlocks;
     return {
-        html: HTML_GAPS[gapLevelBetweenSubBlocks],
-        richText: RICH_TEXT_GAPS[gapLevelBetweenSubBlocks]
+        html: HTML_GAPS[gapLevel],
+        richText: RICH_TEXT_GAPS[gapLevel]
     };
 }
 
@@ -462,7 +424,7 @@ function wrapInlineNameHTML(
 }
 
 function wrapInlineValueHTML(
-    valueList: string[],
+    valueList: string | string[],
     alignRight: boolean,
     valueCloseToMarker: boolean,
     style: string
@@ -470,6 +432,7 @@ function wrapInlineValueHTML(
     // 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.
@@ -484,7 +447,7 @@ function wrapInlineNameRichText(ctx: TooltipMarkupBuildContext, name: string, st
 
 function wrapInlineValueRichText(
     ctx: TooltipMarkupBuildContext,
-    valueList: string[],
+    values: string | string[],
     alignRight: boolean,
     valueCloseToMarker: boolean,
     style: RichTextStyle
@@ -493,7 +456,10 @@ function wrapInlineValueRichText(
     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(valueList.join('  '), styles);
+    return ctx.markupStyleCreator.wrapRichTextStyle(
+        isArray(values) ? values.join('  ') : values,
+        styles
+    );
 }
 
 
diff --git a/src/model/mixin/dataFormat.ts b/src/model/mixin/dataFormat.ts
index 87cf12b..9671354 100644
--- a/src/model/mixin/dataFormat.ts
+++ b/src/model/mixin/dataFormat.ts
@@ -220,14 +220,11 @@ type TooltipFormatResult =
 /**
  * For backward compat, normalize the return from `formatTooltip`.
  */
-export function normalizeTooltipFormatResult(
-    result: TooltipFormatResult
-    // markersExisting: Dictionary<ColorString>
-): {
+export function normalizeTooltipFormatResult(result: TooltipFormatResult): {
     // If `markupFragment` exists, `markupText` should be ignored.
-    markupFragment: TooltipMarkupBlockFragment;
+    frag: TooltipMarkupBlockFragment;
     // Can be `null`/`undefined`, means no tooltip.
-    markupText: string;
+    text: string;
     // Merged with `markersExisting`.
     // markers: Dictionary<ColorString>;
 } {
@@ -256,8 +253,8 @@ export function normalizeTooltipFormatResult(
     }
 
     return {
-        markupText: markupText,
+        text: markupText,
         // markers: markers || markersExisting,
-        markupFragment: markupFragment
+        frag: markupFragment
     };
 }
diff --git a/src/util/types.ts b/src/util/types.ts
index 95e5b50..893de66 100644
--- a/src/util/types.ts
+++ b/src/util/types.ts
@@ -1253,6 +1253,13 @@ export interface CommonTooltipOption<FormatterParams> {
     alwaysShowContent?: boolean
 
     formatter?: string | TooltipFormatterCallback<FormatterParams>
+
+    /**
+     * Formatter of value.
+     *
+     * Will be ignored if tooltip.formatter is specified.
+     */
+    valueFormatter?: (value: OptionDataValue | OptionDataValue[]) => string
     /**
      * Absolution pixel [x, y] array. Or relative percent string [x, y] array.
      * If trigger is 'item'. position can be set to 'inside' / 'top' / 'left' / 'right' / 'bottom',
diff --git a/test/tooltip-valueFormatter.html b/test/tooltip-valueFormatter.html
new file mode 100644
index 0000000..3d6f9e1
--- /dev/null
+++ b/test/tooltip-valueFormatter.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+
+<html>
+    <head>
+        <meta charset="utf-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <script src="lib/simpleRequire.js"></script>
+        <script src="lib/config.js"></script>
+        <script src="lib/jquery.min.js"></script>
+        <script src="lib/facePrint.js"></script>
+        <script src="lib/testHelper.js"></script>
+        <!-- <script src="ut/lib/canteen.js"></script> -->
+        <link rel="stylesheet" href="lib/reset.css" />
+    </head>
+    <body>
+        <style>
+        </style>
+
+
+
+        <div id="main0"></div>
+
+
+
+
+
+
+        <script>
+        require([
+            'echarts'
+        ], function (echarts) {
+            var option;
+
+            option = {
+                animation: false,
+                tooltip: {
+                    position: 'top',
+                    valueFormatter: (val) => val + '%'
+                },
+                xAxis: {
+                    type: 'category',
+                    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+                },
+                yAxis: {
+                    type: 'value'
+                },
+                series: [
+                    {
+                        data: [50, 80, 70, 20, 30, 10, 100],
+                        type: 'bar',
+                        showBackground: true
+                    }
+                ]
+            };
+
+            var chart = testHelper.create(echarts, 'main0', {
+                title: [
+                    'Basic valueFormatter of item tooltip'
+                ],
+                option: option
+                // height: 300,
+                // buttons: [{text: 'btn-txt', onclick: function () {}}],
+                // recordCanvas: true,
+            });
+
+            chart.dispatchAction({
+                type: 'showTip',
+                dataIndex: 2,
+                seriesIndex: 0
+            });
+        });
+        </script>
+
+
+    </body>
+</html>
+

---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org