Make writing a habit together! This is the second day of my participation in the “Gold Digging Day New Plan · April More text challenge”. Click here for more details.

This article will describe the implementation of drag line graph ideas and part of the code to share, if you want to directly see the code please click here

demand

Recently I met a requirement to make a visual chart page, which has a line chart. It can be said that the implementation of this page is relatively complex. The specific requirements are as follows:

  1. Draggable dot
  2. The dot needs to stay on the line during the drag
  3. Each dot needs to be marked

Since the whole project is using the Echart chart plug-in, I searched the case on the official website, found an official drag-and-drop example, and debugged the case to see if it could meet the requirements of the product. The official example found was to use a GraphicComponent to implement this draggable point (and to override the fold point in the original fold diagram by hierarchically listening for the onMousemove event to achieve draggable points in the layer).

Here’s the problem. If I had implemented the draggable point using the GraphicComponent, I’d have solved the draggable problem, but how do I keep the line? Now I was in trouble again.

But the method is always more than difficult, and finally realized the requirements of the product, we first look at the finished product effect, of course, in the realization of the process also stepped on a lot of pits 🤡🤡

implementation

Let’s do this in code first, create a div element and pass it to echart.init. (This need not say more, no students, I put the link here to get started quickly)

Creating a draw container

<template>
  <div ref="lineChartDom" :style="{width: '780px',height:'200px'}"></div>
</template>
Copy the code

Create virtual data file

// useData.ts
import { ref } from 'vue';
import { ChartDataItem } from './types';

export default function useData() {
  const data = ref<ChartDataItem[]>([]);
  function func(x: number) :number {
    x /= 60;
    return Math.sin(x) * 10 + 10;
  }

  for (let i = 0; i <= 800; i += 0.1) {
    data.value.push([i, func(i)]);
  }
  return { data };
}
Copy the code

After virtualizing the Data, we need to provide Echart with the Option property object to render the chart. Next, we create the useoption. ts file to update the Data and get the Option property.

// useOption.ts
import { EChartsOption, ChartDataItem } from './types';
import {ref} from "vue";

export default function useOption() {
  let option = ref<EChartsOption>({
    height: 120.grid: {
      show: false
    },
    xAxis: {
      type: 'value'.axisLine: {
        show: false
      },
      axisTick: {
        show: false
      },
      splitLine: {
        show: false}},yAxis: {
      type: 'value'.splitLine: {
        show: false
      },
      axisTick: {
        show: false.length: 1
      },
      axisLine: {
        show: false}},series: [{data: [].type: 'line'.color: '#5470c6'.smooth: true.showSymbol: false.lineStyle: {
          color: '#5470c6'
        },
        zlevel: 0}}]);const updateData = (data: ChartDataItem[]) = > {
    // @ts-ignore
    option.value.series[0].data = data;
  };
  return {
    option,
    updateData
  };
}
Copy the code

Now just render the diagram to the view, create the usechart. ts file, and deal with the layer rendering part.

Much of the subsequent logic is written in this file

// useChart.ts
import * as echarts from 'echarts/core';

import {
    EChartsOption,
} from './types';
import {DatasetComponent, GraphicComponent, GridComponent, TooltipComponent} from 'echarts/components';
import {CanvasRenderer} from 'echarts/renderers'

echarts.use([GraphicComponent, GridComponent, DatasetComponent, TooltipComponent, CanvasRenderer]);
export default function useChart() {
    / / chart instance
    let Chart: any;
    const initCart = (lineChartDom: HTMLElement, option: EChartsOption) = > {
        Chart = echarts.init(lineChartDom);
        Chart.setOption(option);
    };
    return {
        initCart
    };
}
Copy the code
<template> <div> <div id="lineChartDom" ref="lineChartDom" :style="{ width: '780px', height: '200px' }"></div> </div> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue' import useChart from './useChart'; import useInitData from './useData'; import useOption from './useOption'; import { EChartsOption } from './types' const lineChartDom = ref<HTMLElement>(); const { data } = useInitData(); const { option, updateData } = useOption(); const { createChart } = useChart(); onMounted(() => { updateData(data.value) createChart(lineChartDom.value! , (option.value as EChartsOption)); }); </script> <style scoped></style>Copy the code

After following the steps above, you should see the following effect in your view

Add draggable dots

EChart provides a variety of mouse event types, such as click, Mousedown, Mousemove… Such events can be monitored in Echart.

If you’re smart, you’ll notice that with mouse events, we can add draggable dots depending on where we click. Yes!

In useechart. ts, add a listener to the mouseup event,

Why not the Click event?

Since the click event is triggered during mouseup, what we need to implement is the drag effect when the mouse is held down. The dots are then rendered on mouseup. Read on!

All mouse events in EChart contain the parameter Params, which is an object containing data information about the click graph, in the following format:

type EventParams = {
  // The name of the component to which the currently clicked graphic element belongs,
  // The value can be 'series', 'markLine', 'markPoint', 'timeLine', etc.
  componentType: string;
  // Series type. The values can be: 'line', 'bar', 'pie', etc. Makes sense when componentType is 'series'.
  seriesType: string;
  // Series index in the passed option.series. Makes sense when componentType is 'series'.
  seriesIndex: number;
  // Series name. Makes sense when componentType is 'series'.
  seriesName: string;
  // Data name, category name
  name: string;
  // The index of the data in the passed data array
  dataIndex: number;
  // The raw data item passed in
  data: Object;
  // Sankey and graph contain both nodeData and edgeData.
  // dataType will be 'node' or 'edge', indicating whether the current click is on node or edge.
  // Most other charts have only one type of data, dataType is meaningless.
  dataType: string;
  // The data value passed in
  value: number | Array;
  // The color of the data graph. Makes sense when componentType is 'series'.
  color: string;
};
Copy the code

We determine the position of the user click based on the event parameter Params.componentType. If the value of Params.componentType is series, it indicates that the user is currently clicking the position on the polyline.

In addition to knowing where the user clicked, we need to be able to obtain the user’s current online data through the params.data property. With data, we can use the echart.convertToPixel method to get the converted Canvas coordinate system.

Draw the dot

Echart allows users to draw native graphic element components (option.graphic)

Graphic apis related documents echarts.apache.org/zh/option.h…

We support multiple dot data in the chart. Before drawing dots, we need to create an array to store the data set of these dots. Id is the unique identification of each dot.

export interface MaskItemType {
  id: string;
  data: ChartDataItem;
}

// Dot data
let sourceDotPoints = ref<MaskItemType[]>([]);
Copy the code

The usemark. ts file is created in the same directory to obtain the ID of the current dot. The ID is used as the label for subsequent display.

// useMark.ts
import { ref } from 'vue';
import { MarkType } from './types';

export default function useMark() {
  // Assign labels
  let marks = ref<MarkType[]>(['h1'.'h2'.'h3'.'h4'.'h5']);
  const getMark = (): MarkType= > {
    if(marks.value.length ! = =0) {
      return marks.value.shift() as MarkType;
    } else {
      throw Error('Labels have been assigned'); }};const returnMark = (mark: MarkType) = > {
    return marks.value.unshift(mark);
  };
  return {
    getMark,
    returnMark
  };
}
Copy the code

Create the drawAllDot method in useChart to draw dots in the diagram.

const drawAllDot = () = > {
  Chart.setOption({
    graphic: echarts.util.map(sourceDotPoints.value, function (item, dataIndex) {
      return {
        id: item.id,
        type: 'circle'.position: Chart.convertToPixel('grid', item.data),
        shape: { r: 10 / 2 },
        invisible: false.draggable: false.style: {
          fill: '#ffffff'.stroke: '#33cccc'
        },
        z: 100}; })}); };Copy the code
// useChart createChart function
Chart.on('mouseup'.function (params: EventParamsType) {
    if (params.componentType === 'series') {
        // The constraint can have only 5 elements
        if (sourceDotPoints.value.length < 5) {
            // Get the tag and add the dot- tag to the graphic element to avoid subsequent conflicts with the label ID
            const id: MarkType = getMark();
            let dotObj: MaskItemType = {
                id: `dot-${id}`.data: params.data
            };
            // Add to maskPoint and paintsourceDotPoints.value.push(dotObj); drawAllDot(); }}})Copy the code

Points along the line

I mentioned above that we couldn’t keep the dot on the polyline if we used the GraphicComponent drag-and-drop. So I thought is there any trick to make the user think that the dot on the current mouse is the dot being dragged?

I went through the case again and found this example of a line chart, where when we move the mouse over the chart there’s a little dot, and here you can imagine that we can use this feature point to make the user think that this is the point being dragged. Smoke screen

Its corresponding attribute is option. The grid. Tooltip. The trigger attribute is equal to the axis, display.

Add a mouseDown event to remove the clicked dot when the user clicks on it, and then if the user releases the mouse over the polyline, it triggers the draw dot event written above to create a drag effect.

Chart.on('mousedown'.function (params: EventParamsType) {
    if (params.componentType === 'series') {
      //
    } else if (params.componentType === 'graphic') {
      console.log('Dot is clicked.');
      let cur: ChartEventTargetType = params.event.target as ChartEventTargetType;
      if (cur === null) return;
      letoption = Chart! .getOption();let id = cur.id;
      // Delete dot and label
      option.graphic = {
        id: id,
        $action: 'remove'
      };
      Chart.setOption(option);
      // Remove the dot from the record
      let index = sourceDotPoints.value.findIndex((item) = > (item.id = id));
      .value.splice(index, 1);
      // Return the tag
      let newId: MarkType = cur.id.replace('dot-'.' ') asMarkType; returnMark(newId); drawAllDot(); }});Copy the code

implementationlabelThe label

The idea of a label is the same as drawing dots, using the same graphic text element.

There were a lot of pits here 🙃🙃

When the mouse moves over the dot, the ID of the current dot is displayed. But here you don’t need to use the data that holds the Label, just a Label graph.

Note that putting all labels in an array (like dots) will cause multiple renders to remove labels, which can cause page stashing

/ / label data
let sourceLabelPoints = ref<MaskItemType[]>([]);
Copy the code

So the question is, how do I render the text?

Add two methods to show and hide label images.

Define the ID of a label graph,

const markLabelId:string = 'markLabelId';
Copy the code

ShowLabel: Displays the label image, passing in the corresponding labelID and display coordinate system.

const showLabel = (id: string, x: number, y: number, z: number) = > {
  let newId = id.replace('dot'.'label');
  console.log(id, x, y, z);
  Chart.setOption({
    graphic: {
      id: markLabelId,
      type: 'text'.$action: 'replace'.x: x - 7.y: y - 30.z: 9999.invisible: false.draggable: false.shape: {
        width: 40.height: 20
      },
      style: {
        text: newId.replace('label-'.' '),
        fill: '#95a5a6'.lineWidth: 1.font: '14px Fira Sans, sans-serif'
      },
      transition: 'style'.zlevel: 999}}); };Copy the code

HiddenLabel: Used to hide label images, graphic images have an invisible attribute, which is used to set whether the image is visible.

/ / hide the label
const hiddenLabel = (id: string) :void= > {
  letoption = Chart! .getOption(); option.graphic = [ {id: markLabelId,
      $action: 'replace'.type: 'text'.invisible: false}];console.log(option);
  Chart.setOption(option);
};
Copy the code

Modify the drawAllDot method to add a hover in and out event over the dot image. Corresponds to the show/hidden method above.

Add onMouseOver in the graph. When the event is triggered, the ID of the current dot and the corresponding coordinate system information are passed to the showLabel method. The onmouseout method corresponds to the hiddenLabel method, and the corresponding ID is passed to hide the corresponding label element.

const drawAllDot = () = > {
  Chart.setOption({
    graphic: echarts.util.map(sourceDotPoints.value, function (item, dataIndex) {
      return {
        id: item.id,
        type: 'circle'.position: Chart.convertToPixel('grid', item.data),
        shape: { r: 10 / 2 },
        invisible: false.draggable: false.style: {
          fill: '#ffffff'.stroke: '#33cccc'
        },
        z: 100.onmouseover: function (e: MouseEvent) {
          / / to render the label
          let target: ChartEventTargetType = e.target as ChartEventTargetType;
          console.log('target', target);
          showLabel(target.id, target.x, target.y, target.z);
        },
        onmouseout: function () {
          / / clear the label
          hiddenLabel(this.id); }}; })}); };Copy the code

After configuring the dot shapes, you need to add information in the mousedown method. When the dot is clicked, the label is in a display state, and we will use the hiddenLabel method to hide it.

Chart.on('mousedown'.function (params: EventParamsType) {
  if (params.componentType === 'series') {
    //
  } else if (params.componentType === 'graphic') {
    console.log('Dot is clicked.');
    let cur: ChartEventTargetType = params.event.target as ChartEventTargetType;
    if (cur === null) return;
    letoption = Chart! .getOption();let id = cur.id;
    // Delete dot and label
    option.graphic = {
      id: id,
      $action: 'remove'
    };
    hiddenLabel(id);
    Chart.setOption(option);
    // Remove the label from the record
    let index = sourceDotPoints.value.findIndex((item) = > (item.id === id));
    sourceDotPoints.value.splice(index, 1);
    // Return the tag
    let newId: MarkType = cur.id.replace('dot-'.' ') asMarkType; returnMark(newId); }});Copy the code

Ok, now we have the renderings, and I have put the example of this article on GITHUB here, if you want to see the full code, you can go there.

conclusion

How to say, this requirement is not very difficult and not very simple on the whole, but I also have some small problems to solve in the process of implementation. I tried all kinds of methods over and over again and finally figured it out.