Source code for esptest.iperf_utility.line_chart

from typing import TYPE_CHECKING

import esptest.common.compat_typing as t

from ..common.decorators import enhance_import_error_message
from ..logger import get_logger

if TYPE_CHECKING:
    from pyecharts import options as opts

XVarType: t.TypeAlias = t.Union[int, float, str]
YVarType: t.TypeAlias = t.Union[
    t.Dict[str, t.Union[int, float, None]],
    t.Dict[str, int],
    t.Dict[str, float],
    t.Dict[str, None],
    int,
    float,
    None,
    object,  # for type hinting
]
logger = get_logger('iperf-util')


def _calculate_adjacent_diffs(series_data: t.Sequence[t.Union[int, float, None]]) -> t.List[t.Union[int, float]]:
    """Calculate adjacent differences for a series, handling None values."""
    diffs: t.List[t.Union[int, float]] = []
    last_valid_value: t.Optional[t.Union[int, float]] = None

    for i, current_value in enumerate(series_data):
        if i == 0:
            diffs.append(0)
            if current_value is not None:
                last_valid_value = current_value
            continue

        if current_value is None:
            diffs.append(0)
            continue

        if last_valid_value is not None:
            diff = current_value - last_valid_value
            diffs.append(diff)
            last_valid_value = current_value
        else:
            diffs.append(0)
            last_valid_value = current_value
    return diffs


@enhance_import_error_message('please install pyecharts or "pip install esp-test-utils[all]"')
def _create_tooltip_options(
    show_diff_tooltip: bool, y_data: t.Sequence[YVarType], y_names: t.List[str]
) -> 'opts.TooltipOpts':
    """
    create tooltip for auto calculate diff value by mouseover
    """

    import pyecharts.options as opts
    from pyecharts.commons.utils import JsCode

    # Create tooltip with diff display
    if show_diff_tooltip:
        assert y_data and y_names, 'y_data and y_names must be provided'
        all_diffs = []
        assert all(isinstance(y, dict) for y in y_data)
        for name in y_names:
            values: t.Sequence[t.Union[int, float, None]] = [y[name] for y in y_data]  # type: ignore
            all_diffs.append(_calculate_adjacent_diffs(values))
        formatter = f"""
            function(params) {{
                var alldiffs = {all_diffs};
                var tooltip = '<div style="padding: 10px;">';
                tooltip += '<b>X: ' + params[0].axisValue + '</b><br/>';
                if (Array.isArray(params)) {{
                    for (var i = 0; i < params.length; i++) {{
                        var param = params[i];
                        var seriesIndex = param.seriesIndex;
                        var seriesName = param.seriesName;
                        var seriesColor = param.color || '#2E86DE';
                        var diffs = alldiffs[seriesIndex];
                        var dataIndex = param.dataIndex;
                        var yValue = Array.isArray(param.value) ? param.value[1] : param.value;

                        if (yValue === null || yValue === undefined) {{
                            tooltip += '<span style="color: ' + seriesColor + ';">●</span> ' + seriesName;
                            tooltip += ': <b>N/A</b><br/>';
                            continue;
                        }}

                        var diff = diffs[dataIndex];
                        var diffStr = diff >= 0 ? '+' + diff.toFixed(2) : diff.toFixed(2);
                        var diffColor = diff >= 0 ? '#52c41a' : '#f5222d';
                        var diffIcon = diff >= 0 ? '▲' : '▼';

                        tooltip += '<span style="color: ' + seriesColor + ';">●</span> ' + seriesName + ': <b>';
                        tooltip += '<span style="display:inline-block;min-width:70px;">';
                        tooltip += yValue.toFixed(2) + '</span></b> ';

                        if (dataIndex > 0 && diff !== 0) {{
                            tooltip += '<span style="color: ' + diffColor + ';">' + diffIcon + '</span> ';
                            tooltip += '<span style="color:' + diffColor + '"><b>' + diffStr + '</b></span>';
                        }}
                        tooltip += '<br/>';
                    }}
                }}
                tooltip += '</div>';
                return tooltip;
            }}
        """
        tooltip_opts_obj = opts.TooltipOpts(
            trigger='axis',
            axis_pointer_type='cross',
            background_color='rgba(245, 245, 245, 0.9)',
            border_width=1,
            border_color='#ccc',
            textstyle_opts=opts.TextStyleOpts(color='#000'),
            formatter=JsCode(formatter),
            is_confine=True,
            extra_css_text='box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);',
        )
    else:
        tooltip_opts_obj = opts.TooltipOpts(trigger='axis')
    return tooltip_opts_obj


@enhance_import_error_message('please install pyecharts or "pip install esp-test-utils[all]"')
def _create_axis_options(
    x_type: str, x_label: str, y_label: str, x_scale: bool = True, y_scale: bool = True
) -> t.Tuple['opts.AxisOpts', 'opts.AxisOpts']:
    """
    create axis and yaxis options
    """
    import pyecharts.options as opts

    xaxis_opts = opts.AxisOpts(
        type_=x_type,
        name=x_label,
        is_scale=x_scale,
        boundary_gap=['3%', '3%'],
        axistick_opts=opts.AxisTickOpts(is_align_with_label=True),
        splitline_opts=opts.SplitLineOpts(is_show=True),
    )

    yaxis_opts = opts.AxisOpts(
        type_='value',
        name=y_label,
        is_scale=y_scale,
        axistick_opts=opts.AxisTickOpts(is_show=True),
        splitline_opts=opts.SplitLineOpts(is_show=True),
    )

    return xaxis_opts, yaxis_opts


[docs] @enhance_import_error_message('please install pyecharts or "pip install esp-test-utils[all]"') def draw_line_chart_basic( # pylint: disable=too-many-positional-arguments,too-many-arguments,too-many-locals file_name: str, title: str, y_data: t.Sequence[YVarType], x_data: t.Optional[t.Sequence[XVarType]] = None, x_label: str = 'x', y_label: str = 'y', x_scale: bool = True, y_scale: bool = True, show_diff_tooltip: bool = False, ) -> None: """Draw line chart and save to file. Args: file_name (str): line chart render file name title (str): title of the chart y_data (Sequence[Dict[str, float]]): list of chart data, format eg: [{'y1': 1, 'y2': 1}, {'y1': 2, 'y2': 1}] x_data (Optional[Sequence[float]], optional): x data. Defaults to "range(len(y_data))". x_label (str, optional): x label name. Defaults to 'x'. y_label (str, optional): y label name. Defaults to 'y'. x_scale (bool, optional): x scale. Defaults to True. y_scale (bool, optional): y scale name. Defaults to True. show_diff_tooltip (bool, optional): show diff in tooltip. Defaults to False. """ # pylint: disable=too-many-arguments import pyecharts.options as opts from pyecharts.charts import Line line = Line() assert len(y_data) > 0 if not x_data: x_data = list(range(len(y_data))) assert len(x_data) == len(y_data) x_type = 'value' for x in x_data: if isinstance(x, str): x_type = 'category' break if x_type == 'category': x_data = list(map(str, x_data)) line.add_xaxis(x_data) # Convert simple list to dict format if needed if isinstance(y_data[0], (int, float, type(None))): y_data = [{'y': var} for var in y_data] # type: ignore # Collect all series data and calculate diffs assert isinstance(y_data[0], dict) y_names: t.List[str] = list(y_data[0].keys()) for name in y_names: legend = name _data: t.Sequence[t.Union[int, float, None]] = [y[name] for y in y_data] # type: ignore # Remove None values before calculating max/min, as pyecharts supports # Calculate diffs for this series _data_except_none = [y for y in _data if y is not None] if all((isinstance(y, (int, float)) for y in _data_except_none)): # show max/min legend += f' (max: {max(_data_except_none)}, min: {min(_data_except_none)})' line.add_yaxis(legend, _data, is_connect_nones=True, is_smooth=True) # create axis xaxis_opts, yaxis_opts = _create_axis_options(x_type, x_label, y_label, x_scale, y_scale) # add global line.set_global_opts( datazoom_opts=opts.DataZoomOpts(range_start=0, range_end=100), title_opts=opts.TitleOpts(title=title, pos_left='center'), legend_opts=opts.LegendOpts(pos_top='10%', pos_left='right', orient='vertical'), tooltip_opts=_create_tooltip_options(show_diff_tooltip, y_data, y_names), xaxis_opts=xaxis_opts, yaxis_opts=yaxis_opts, toolbox_opts=opts.ToolboxOpts( is_show=True, pos_right='5%', feature={ 'saveAsImage': {'title': 'Save as image'}, 'restore': {'title': 'Restore'}, 'dataZoom': {'title': {'zoom': 'Zoom', 'back': 'Restore zoom'}}, 'dataView': {'title': 'Data view', 'lang': ['Data view', 'Close', 'Refresh']}, }, ), ) line.render(file_name)