We are using @wuba/react-native-echarts to display eCharts in our react native app. Problem that we noticed is that on some devices with Android, charts are glitchy (image below). Here is a code:
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 seriesBounds = useMemo(() => {
// get data ranges for each datasource using each unit
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
);
// determine value ranges for each unit
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)
),
};
// if min is much smaller than max (or other way around) then we need to increase it
// to avoid "squashing" the chart on the smaller side of the chart
// this is done by making sure min/max are separated by no more than 1 order of magnitude
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);
}
// get ratios between min and max values
const ratios = new List(rangesByUnit)
.Select((rbu) => (rbu.min === 0 ? 0 : rbu.max / rbu.min))
.ToArray();
// we need to find which unit has zero most centered
const minDistanceToOne = new List(ratios)
.Where((r) => r! < 0)
.Min((r) => Math.abs(r - 1));
// get ratio for this unit so we can normalize others
const minRatio = ratios.filter(
(r) => Math.abs(r - 1) === minDistanceToOne
)[0];
// normalize all units to same ratio between min and max,
// causing zero to be at the same place on the chart
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 * ~~(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,
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.map((y) => y.name).indexOf(t(unitStyles.category)),
showSymbol: false,
zlevel: 9,
z: 9,
};
}),
[data, yAxis, t]
);
useEffect(() => {
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: `${~~((yAxis!.length + 1) / 2) * 70 || 20}px`,
right: `${~~(yAxis!.length / 2) * 70 || 20}px`,
},
};
let chart: any;
if (skiaRef.current) {
chart = echarts.init(skiaRef.current, "light", {
renderer: "svg",
useDirtyRect: true,
width: Dimensions.get("window").width - 28,
height: 400,
});
chart.setOption(options);
}
return () => chart?.dispose();
}, []);
return (
<View style={{ backgroundColor: "white" }}>
<SkiaChart ref={skiaRef} />
</View>
);
}
Issue on Redmi K30 and some other MIUI and samsung devices
On some devices (for instance iOS and some androids) charts looks good. We tried with some different packages versions but without results. Additionally, when I used standard example from https://wuba.github.io/react-native-echarts/docs/getting-started/write-a-simple-line-chart page it works just fine, no glich so I suspect it might be an issue from xAxis/yAxis const.