Talk about Echarts

Echarts is a ready-to-use chart library that takes charts as a starting point and customizes them by configuring the components within the chart. Most commonly used charts are built in. With a little configuration, the user can easily get the graph you want without having to worry about mapping and rendering the graph data.

And because Echarts does so much for you, it’s hard to do when you want to control the chart in more detail.

For example, Echarts only provides us with the relationship between a component and a family of mapping figures (lines, columns, and other data mapping figures) and the relationship between a component and a coordinate system. Components are imperceptive to each other, as are the more subtle units within a component. You can’t blame Echarts for that. Echarts was originally designed to help users create charts faster.

Label Indicates the label occlusion problem

As the requirements continue to rise, ECharts’ control of tiny units becomes problematic case by case.

demand

Solve the problem of lable label occlusion in line graph and bar graph. Because the chart finally needs to be converted into pictures into PDF for download. Therefore, the data must be clearly displayed through the Lable label.

thinking

The label position of the same item in different groups of data cannot be obtained simultaneously because labels cannot be sensed between labels. Therefore, if you want to control the position relationship between labels, the best way is to analyze the data data column before configuring the chart, identify the data that may lead to label overlap, and shift a certain distance in a certain direction when configuring series.data.label.

The solution is there, but there are still the following problems to be solved.

  1. How to form the position relationship of the same column of labels and how to label them.
  2. How to determine whether labels overlap
  3. Determine the direction and displacement of the overlapping label
  4. If it is guaranteed that the label after displacement will not overlap again

To solve the problem

How to form the position relationship of the same column of labels and how to label them.

Create a ChartMap class to store the relationship between the current data and the data in the same column, and record coordinates, positions, and shifts. There is no comparison here, only record, and not only can record the position of the label, as long as it is associated with the original data can be recorded, depending on personal needs.

Each chart corresponds to a ChartMap instance. A ChartMap instance contains a mapData for storing relational data. The create method is used to create a mapping object for the data and store it in mapData

/** ChartMap Id */
let _id = 0;
/** * To record the relationship between echarts data *@class ChartMap
 * @constructor Initialize data *@param {Array} Data stores copies of the original data column *@param {Object} MapData Indicates the mapped data *@param {String} Name the name *@param {Number} _id  Id
 * @method Create Creates the mapData object */
class ChartMap {

  constructor(data, name = "Chart") {
    /** Stores a copy of the original column */
    this.data = JSON.parse(JSON.stringify(data));
    /** Mapped data */
    this.mapData = {};
    /** ChartMap name */
    this.name = name == "Chart" ? name + _id : name;
    /** ChartMap Id */
    this._id = _id;
    _id += 1;
  }
  /** * create a map object and store it in mapData *@param {Number|String} In what row is x located, the coordinate position of x *@param {Number|String} What column y is in (which group in the intolerant data set), and the coordinate position of y *@returns * /
  create(x, y) {
    if (this.mapData[`${x}.${y}`]) return;
    this.mapData[`${x}.${y}`] = {
      /** Original data column */
      originList: this.data[x].data,
      /** Original data */
      originData: Number(this.data[x].data[y]),
      /** Use the raw data to mark the label position, used to calculate the offset */
      labelPosition: Number(this.data[x].data[y]),
      /** The set of points that need to be evaluated with the current point */
      compareMap: {},
      /** The point object closest to the current point in absolute position */
      NearestPoint:null./** The nearest absolute distance from the current point */
      NearestDistance:0./** The distance to be moved in the x direction */
      offSetx: 0./** The distance to be shifted in the y direction of the current point */
      offsetY: 0./** The position of the current point in the same group */
      sort:0./** The x-coordinate of the current point */
      x,
      /** The y coordinate of the current point */y, }; }}Copy the code

Initialize the ChartMap instance to build the relationship of the same column data that needs to be compared (x is different from Y, collectively referred to as same column data below)

/** * Serialize echarts data columns to fix echarts label occlusion *@param {String} EchartType Type of chart: line line bar (mandatory) *@param {Array} SeriesArr Echarts Data column (mandatory) *@param {Object} LabelOption Label Additional configuration (Optional) *@param {Number} AxisMax coordinate system range (optional) *@param {Number} JudgeCoefficient coefficient to calculate the trigger occlusion condition (optional) *@param {Number} FixCoefficient, coefficient of corrected distance after occlusion (optional) */
function fixEchartsLabel(
  echartType,
  seriesArr,
  labelOption,
  AxisMax,
  judgeCoefficient = 2.5,
  fixCoefficient = 0.6
) {
  if (Object.prototype.toString.call(seriesArr) ! = ="[object Array]")
    return new Error("The fixEchartsLabel function needs to pass in an Echarts data column.");
  if (seriesArr.length <= 1) return seriesArr;
 
  // How many groups of data x
  const dataRowLength = seriesArr.length;
  // How many y are there in each set of data
  let lengerColLength = 0;
  for (let i = 0; i < seriesArr.length; i++) {
    if (Object.prototype.toString.call(seriesArr[i].data) ! = ="[object Array]")
      continue;
    if(seriesArr[i].data.length > lengerColLength) { lengerColLength = seriesArr[i].data.length; }}// Initialize the coordinates
  let x = 0;
  let y = 0;
  // Save the mapping point for the final calculation of the assignment
  const chartMap = new ChartMap(seriesArr);
  // Get the maximum value
  let maxValue = 0;
  // Create a mapping relationship with the column data
  while (x <= dataRowLength && y < lengerColLength) {
    if (x + 1 > dataRowLength) {
      y += 1;
      x = 0;
      continue;
    }
    // Save the current point
    chartMap.create(x, y);
    const cur = chartMap.mapData[`${x}.${y}`];
    maxValue = cur.originData > maxValue ? cur.originData : maxValue;
    // Store the same group of zero time data, used for non-bar graphs to add sort to the same value
    const temp = [];
    // The nearest distance
    let NearestDistance = Infinity;
    // The current point establishes the relationship with the same column data
    for (let i = 0; i < dataRowLength; i++) {
      if (i == x) continue;
      // save the comparison point, do not worry about repeating, create does the same judgment
      chartMap.create(i, y);
      const target = chartMap.mapData[`${i}.${y}`];
      // all the points in the same column are added to the comparison list
      cur.compareMap[`${i}.${y}`] = target;
      // Determine the current position of the sort, non-bar graph exclude points with the same value
      if(cur.originData > target.originData &&  temp.indexOf(target.originData) == -1){
        cur.sort += 1;
        temp.push(target.originData)
      } else if (echartType == 'bar' && cur.originData == target.originData && target.x > cur.x ){
        target.sort += 1;
      }
      // Bind the nearest point
      const distance = Math.abs(target.originData - cur.originData);
      if( distance < NearestDistance ){
        cur.NearestPoint = target;
        cur.NearestDistance = distance;
        NearestDistance = distance;
      }
    }
    x += 1;
  }
  console.log(maxValue,chartMap)
  AxisMax calculates the base displacement of the offset
  AxisMax = AxisMax ? AxisMax : maxValue;
  // Returns the computed data column
  return calcLableOffset(
    chartMap.data,
    chartMap.mapData,
    labelOption,
    judgeCoefficient,
    fixCoefficient,
    AxisMax
  );
}
Copy the code

How to determine whether labels overlap

/** * Calculates the echarts label shift *@param {Array} SeriesArr Echarts data column *@param {Object} ChartMap Echarts Data map *@param {Object} LabelOption Label Additional configuration item *@param {Number} JudgeCoefficient coefficient, which calculates the condition for triggering occlusion *@param {Number} FixCoefficient correction coefficient, the coefficient of corrected distance after occlusion *@return {Object} Result The computed echarts data column */
function calcLableOffset(seriesArr, chartMap, labelOption, judgeCoefficient, fixCoefficient, AxisMax) {
  // Calculate the trigger value.
  const judgeDistance = +((+AxisMax * judgeCoefficient) / 100).toFixed(2);
  // Calculate the correction value.
  const fixDistance = +((+AxisMax * fixCoefficient) / 100).toFixed(2);
  // Loop data map to determine occlusion, displacement and displacement distance
  for (let cPoint in chartMap) {
    const cur = chartMap[cPoint];
    // The displacement distance is in the order of base distance * the current point in the same column
    // 5 times critical value is used as the safe distance, greater than 5 times critical value will not be shifted
    const offsetY = cur.NearestDistance > 5 * judgeDistance ? 0 : -fixDistance * cur.sort;
    cur.offsetY = offsetY;
    // Rebuild the data column
    seriesArr[cur.x].data[cur.y] = {
      value: cur.originData,
      label: {
        ...labelOption,
        position: 'top'.offset: [0, offsetY],
        show: true,}}; }return seriesArr;
}
Copy the code

Determine the direction and displacement of the overlapping label

Here’s my final solution

Here, the displacement direction is uniformly upward, and the displacement distance is calculated according to the sequence of the position of the current point.

Displacement distance = is the nearest comparison point beyond the safe distance? No displacement: (The basic displacement record obtained by calculation * the size position of the current point in the same column data)

The displacement distance is controlled by three elements

  1. The uppercase position of the current point in the same column.
  2. The maximum value of coordinate system and fixCoefficient correction are used to calculate the displacement distance of foundation.
  3. Whether the current point is farther than the safe distance from the nearest point in the column. Beyond the safe distance, the label doesn’t have to be shifted anymore.

In fact, there is no judgment of occlusion here. Another method is used to solve the problem of occlusion and label displacement and then occlusion. The specific thinking process is as follows.

If it is guaranteed that the label after displacement will not overlap again

This problem has bothered me for a long time. Since the displacement direction designed here is one-way, there will be many recursive comparisons when determining whether the position after moving is covered or not, and the final label position is often much larger than the source point.

Therefore, a clever method is used to calculate the position of label, sorting the data in the same column by size. The moving distance is in order of the size of the same column as the base distance *, which actually hashes the position of the label in the y direction. The larger the data is compared to the group, the larger the spread.

So, hey! That satisfies the demand. But that still leaves two problems:

  1. Some points obviously do not need to be compared, and labels are also shifted without displacement
  2. Sometimes the label moves out of the diagram and becomes invisible.

My solution;

  1. The distance of the nearest point is recorded for each data point, and a safe range is set, with no displacement greater than the safe range
  2. Given a maximum coordinate value. This value is used to calculate the trigger distance and the displacement basic distance. This value can also be used as the maximum value of the coordinate system, which can be set during external configuration of the chart.

The renderings are as follows

Gitee address

conclusion

This article mainly records my thinking and problem-solving process. Hope through this article, can give you daily development work to bring some help.

Since the project I took over was two years ago, Echarts was used in the project, but the requirements shown in the original chart became more and more complicated in daily maintenance, so LET’s take a look at ANTV-G2 here.

If you know G2, you can use the official solution as long as you configure the layout attribute of the label in G2, which is very useful. At the same time, you can customize the layout scheme is very flexible.

When I find out G2, I will introduce and share it with you if I have time.