You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native中@wuba/react-native-echarts线性图表部分安卓设备UI故障

React Native @wuba/react-native-echarts 安卓设备UI故障修复方案

问题背景

使用@wuba/react-native-echarts组件展示线性图表时,Redmi K30(MIUI)、部分三星等安卓设备出现图表UI故障(元素错位、渲染混乱),但iOS及其他安卓设备显示正常。更换依赖版本无效,官方基础示例可正常运行,问题锁定在自定义轴配置及相关计算逻辑。

核心问题分析

  1. useEffect依赖缺失:图表初始化逻辑未监听yAxis、series等关键配置的变化,导致图表可能使用过期配置渲染。
  2. 多Y轴偏移计算兼容性:offset: 80 * ~~(i / 2)的整数取整计算在部分安卓SVG渲染环境下引发布局错位。
  3. Grid边距精度问题:通过整数取整计算的Gridleft/right边距,在不同像素密度设备上适配性差。
  4. 时间轴数据格式兼容性:直接传入Date对象作为X轴数据,部分安卓设备的ECharts解析存在兼容性问题。
  5. useDirtyRect配置冲突:启用useDirtyRect可能在部分安卓设备上触发渲染bug。

修复后的完整代码

import { List } from "linqts";
import { SVGRenderer, SkiaChart } from "@wuba/react-native-echarts";
import { useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Dimensions } from "react-native";
import { roundUpToNearestLog } from "../../utils/number-utils";
import { Unit } from "../../utils/units";
import * as echarts from "echarts/core";
import { View } from "../Themed";

echarts.use([SVGRenderer]);

export interface ChartStyles {
  category: string;
  opacity: number;
  interval: number | undefined;
}

const defaultUnitChartStyle: ChartStyles = {
  category: "common.value",
  opacity: 1,
  interval: undefined,
};

export const unitChartStyles: { [name in Unit]: ChartStyles } = {
  [Unit.none]: {
    ...defaultUnitChartStyle,
  },
  [Unit.euro]: {
    ...defaultUnitChartStyle,
    category: "common.price",
    opacity: 0.3,
  },
  [Unit.watt]: {
    ...defaultUnitChartStyle,
    category: "common.power",
  },
  [Unit.amper]: {
    ...defaultUnitChartStyle,
    category: "common.current",
  },
  [Unit.volt]: {
    ...defaultUnitChartStyle,
    category: "common.voltage",
  },
  [Unit.wattHour]: {
    ...defaultUnitChartStyle,
    category: "common.energy",
  },
  [Unit.percent]: {
    ...defaultUnitChartStyle,
    category: "common.percentage",
  },
  [Unit.state]: {
    ...defaultUnitChartStyle,
    category: "common.state",
    interval: 1,
  },
};

export function getUnitStyles(unit: Unit | string | undefined): ChartStyles {
  return unitChartStyles[unit as Unit] || unitChartStyles[Unit.none];
}

function yAxisFormatter(value: number, unit: string | undefined): string {
  if (unit === Unit.state) {
    return Boolean(value).toString();
  }
  return `${
    Math.abs(value) < 1 && value !== 0 ? value.toFixed(1) : value.toFixed(0)
  } ${unit ?? ""}`;
}

export interface TimeChartPoint {
  date: Date;
  value: number;
}

export interface TimeChartData {
  color?: string;
  unit?: Unit | string;
  name?: string;
  points: TimeChartPoint[];
  step?: boolean | "start" | "middle" | "end";
  type?: "line" | "bar";
  showBackground?: boolean;
}

export interface TimeChartProps {
  data: TimeChartData[];
}

export function TimeChart({ data }: TimeChartProps) {
  const { t } = useTranslation();
  const skiaRef = useRef<any>(null);
  const chartRef = useRef<any>(null);

  const seriesBounds = useMemo(() => {
    const rangesByUnitAndSerie = data.map((d) => ({
      unit: d.unit,
      min:
        d.points.length === 0
          ? 0
          : d.points
              .map((p) => p.value)
              .reduce((prev, curr) => Math.min(prev, curr)),
      max:
        d.points.length === 0
          ? 0
          : d.points
              .map((p) => p.value)
              .reduce((prev, curr) => Math.max(prev, curr)),
    }));
    const rangesByUnitGrouped = new List(rangesByUnitAndSerie).GroupBy(
      (r) => r.unit ?? "",
      (e) => e
    );

    const rangesByUnit = [];
    for (const key in rangesByUnitGrouped) {
      const unitRange = {
        unit: key,
        min: roundUpToNearestLog(
          new List(rangesByUnitGrouped[key]).Min((r) => r.min)
        ),
        max: roundUpToNearestLog(
          new List(rangesByUnitGrouped[key]).Max((r) => r.max)
        ),
      };

      const orderDifference =
        Math.log10(Math.abs(unitRange.min || 1)) -
        Math.log10(Math.abs(unitRange.max || 1));

      if (orderDifference > 1) {
        unitRange.max =
          unitRange.max * Math.pow(10, Math.floor(orderDifference));
      } else if (orderDifference < -1) {
        unitRange.min =
          unitRange.min * Math.pow(10, Math.floor(-orderDifference));
      }

      if (key === Unit.state) {
        unitRange.min = +false;
        unitRange.max = +true;
      }

      rangesByUnit.push(unitRange);
    }

    const ratios = new List(rangesByUnit)
      .Select((rbu) => (rbu.min === 0 ? 0 : rbu.max / rbu.min))
      .ToArray();

    const negativeRatios = ratios.filter(r => r < 0);
    let minRatio: number | undefined;
    if (negativeRatios.length > 0) {
      const minDistanceToOne = new List(negativeRatios)
        .Min((r) => Math.abs(r - 1));
      minRatio = negativeRatios.find(r => Math.abs(r - 1) === minDistanceToOne);
    }

    rangesByUnit
      .filter((x) => x.unit !== Unit.state)
      .forEach((r) => {
        r.min =
          minRatio !== undefined ? r.max / minRatio : r.min > 0 ? 0 : r.min;
      });

    return rangesByUnit;
  }, [data]);

  const yAxis = useMemo(
    () =>
      data
        .filter((x, i, a) => a.map((y) => y.unit).indexOf(x.unit) === i)
        .map((x, i) => {
          const unitStyles = getUnitStyles(x.unit);
          return {
            name: t(unitStyles.category),
            type: "value",
            max: seriesBounds.filter((s) => s.unit === x.unit)[0]?.max,
            min: seriesBounds.filter((s) => s.unit === x.unit)[0]?.min,
            axisLabel: {
              formatter: (value: number) => yAxisFormatter(value, x.unit),
            },
            axisLine: {
              show: true,
            },
            splitLine: {
              show: false,
            },
            interval: unitStyles.interval,
            axisTick: {
              show: true,
              inside: true,
            },
            showGrid: false,
            position: i % 2 ? "right" : "left",
            offset: 80 * Math.floor(i / 2),
          };
        }),
    [data, t, seriesBounds]
  );

  const series = useMemo(
    () =>
      data.map((x, i) => {
        const unitStyles = getUnitStyles(x.unit);
        return {
          name: `${x.name ?? i} ${
            (x.unit as Unit) && x.unit !== Unit.state ? `[${x.unit}]` : ""
          }`,
          data: x.points.map((point) => [
            point.date.getTime(),
            x.unit !== Unit.state ? point.value : Boolean(point.value),
          ]),
          color: x.color,
          itemStyle: {
            opacity: unitStyles.opacity,
          },
          step: x.step,
          type: x.type ?? "line",
          areaStyle: {
            color: x.showBackground ? x.color : "transparent",
            opacity: 0.3,
          },
          yAxisIndex: yAxis.findIndex(y => y.name === t(unitStyles.category)),
          showSymbol: false,
          zlevel: 9,
          z: 9,
        };
      }),
    [data, yAxis, t]
  );

  useEffect(() => {
    if (!skiaRef.current) return;

    if (chartRef.current) {
      chartRef.current.dispose();
    }

    const windowWidth = Dimensions.get("window").width;
    const options = {
      tooltip: {
        trigger: "axis",
      },
      xAxis: {
        type: "time",
        axisLabel: {
          hideOverlap: false,
        },
        axisTick: {
          show: true,
        },
        splitLine: {
          show: true,
        },
        boundaryGap: false,
      },
      yAxis: yAxis,
      series: series,
      grid: {
        left: `${Math.max(20, Math.floor((yAxis.length + 1) / 2) * 70)}px`,
        right: `${Math.max(20, Math.floor(yAxis.length / 2) * 70)}px`,
      },
    };

    chartRef.current = echarts.init(skiaRef.current, "light", {
      renderer: "svg",
      useDirtyRect: false,
      width: windowWidth - 28,
      height: 400,
    });
    chartRef.current.setOption(options);

    return () => chartRef.current?.dispose();
  }, [yAxis, series]);

  return (
    <View style={{ backgroundColor: "white" }}>
      <SkiaChart ref={skiaRef} />
    </View>
  );
}

关键修复点说明

  1. 补全useEffect依赖:添加yAxis、series作为依赖项,确保配置变化时图表重新初始化。
  2. 时间轴数据格式转换:将Date对象转为时间戳,解决部分安卓设备的时间解析问题。
  3. 禁用useDirtyRect:关闭可能引发渲染异常的脏矩形优化。
  4. 优化Y轴索引查找:用findIndex替代map+indexOf,提升性能同时避免潜在的匹配问题。
  5. Grid边距保底处理:用Math.max(20, ...)确保最小边距,避免布局溢出。

内容的提问来源于stack exchange,提问作者PoloMonola

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 09:47:05