“This is the 8th day of my participation in the First Challenge 2022. For details: First Challenge 2022”

preface

If you look at my 2022 Front-end technology Trends: Don’t you come in and see how to earn it a small target? If you are interested in this article, you should know that we have used Echarts plug-in in the project to show the requirements of visual charts, and the effect is quite good. React uses Echarts as a plugin and encapsulates reusable graphics components for use in groups.

Demand background

Because of our company is the architectural design industry, at ordinary times is quite sensitive to the data content, especially at the end of the data of the report, but also seriously, the traditional form and not intuitive to provide data which some expressive force, so we need to use the visual chart to show the report data, implement the data of self explanation and deliver accurate data information, Thus saving people’s thinking time.

The requirement is to display the data queried according to the query conditions with line chart, bar chart, pie chart, rising sun chart and rectangular tree chart respectively. When the line chart of the same block needs to be displayed in linkage. The so-called linkage means that when the mouse clicks legend of one line chart, the corresponding line of another line chart will also be displayed or hidden. Here we will focus on this interaction, other abnormal interaction will not expand on, after all, not all companies are our company (interaction design people fan confidence that the interaction experience is good, in fact, a chicken feather).

Let’s take a look at the UI design diagram and keep our requirements tradition as shown above:

The requirements summary above is the design diagram, give me the implementation of the design diagram, plus the interaction I want on the line. All right, we’re just getting paid to do it. Just flex the code.

The development of

The stack is React+TypeScript+Echarts. Therefore, other technology stack partners refer to use, if necessary, according to the need to consider the use of Vue and packaging ideas.

The installation

NPM I ECharts or YARN Add EchartsCopy the code

The basic use

Now that it is installed, let’s use a small chestnut to demonstrate how to use it:

Reference component:

import * as echarts from 'echarts';
Copy the code

Page use:

import React, {useEffect, useRef} from 'react'; import * as echarts from 'echarts'; Const constructor */ const EchartsExample = () => {const lineRef = useRef<any>(null); const myChartRef = useRef<any>(null); useEffect(() => { initChart(); } []); /** */ const initChart = () => {myChartref.current = echarts.init(lineref.current); let option: any = { xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] }, yAxis: { type: 'value' }, series: [ { data: [150, 230, 224, 218, 135, 147, 260], type: 'line' } ] }; myChartRef.current && myChartRef.current.setOption(option, true); } return ( <div> <div ref={lineRef} style={{width: 600, height: 400}}></div> </div> ) } export default EchartsExample;Copy the code

Effect:

This is the simplest way to use it: xAxis represents X-axis data, yAxis represents Y-axis data, and series.data represents broken line data. For basic usage of other diagrams, refer to the diagram above, which ensures that you can see the diagram as long as you paste in the Echarts example code to replace options.

Packaging ideas

Packaging components takes a lot of thought and is not limited to existing requirements. Requirements change all the time, so we need to pay attention to the extensibility of the code when packaging, so that we do not appear to be passive when requirements change, rather than kill the existing code and start from scratch. All right, so let’s start wrapping things up.

Page node:

return (
    <Spin spinning={loading}>
        {
            ObjTest.isNotEmptyArray(dataSource)
                ?
                <div ref={chartRef} style={{width, height, backgroundColor}}></div>
                :
                <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
        }
    </Spin>
)
Copy the code

As you can see, not only did we define the Echarts container, but we also took into account the loading animation Spin when querying for data and the Empty display when there is no data, so that the component we encapsulated felt better and didn’t have a blank area and a scene where the data was not being loaded.

Pass parameters:

interface ChartProps { dataSource: any; // Source data loading: Boolean; // Load identifier width? : number; // Graph width height? : number; // Graph height backgroundColor? : string; // Graph background color fontSize? : number; / / font size beginTimeVo? : any; // Start time endTimeVo? : any; // End time isWeekOrMonth? : number; // is the X-axis displayed week or month legendSelected? : any; // Select the collection isShowDataZoom? : boolean; // Whether to display the X-axis scaling bar isShowLegend? : boolean; // Is the legend gridTop displayed? : string; IsAllChecked? : boolean; // Is selectedMode selected for all? : boolean; // legend Select mode onChange? : (beginDate: string, endBegDate: string, endDate: string) => void; // Zoom bar zoom event onLegendSelect? : (name: string, selected: any) => void; // legend Select or deselect the event onClickItem? : (item: any, type: number, index: number) => void; // Click onClickGlobalArea? : (bool: boolean) => void; // Click global}Copy the code

It can be seen that fields that may need to be changed are passed into the component as parameters. Only the dataSource dataSource and loading data mark are required. Fields such as chart size, width and height, and legend related show and hide are passed as optional parameters. In this way, we can meet the needs of different styles and functionality on different pages. It is possible to pass all of its fields as parameters, but that is redundant, so you can see that we only pass some of the most frequently needed fields as parameters, and some of the less frequently used fields are still written to death in the component.

Interactive logic:

Move the X-axis zoom slider event:

myChartRef.current.on('dataZoom', updatePosition); /** ** @param e */ const updatePosition = (e: any) => {let xAxis: any = myChartRef.current.getModel().option.dataZoom[0]; let startIndex: number = xAxis.startValue; let endIndex: number = xAxis.endValue; let beginDate: string = timeVos && timeVos[startIndex] ? timeVos[startIndex]['beginTime'] : ''; let endBegDate: string = timeVos && timeVos[endIndex] ? timeVos[endIndex]['beginTime'] : ''; let endDate: string = timeVos && timeVos[endIndex] ? timeVos[endIndex]['endTime'] : ''; if (timerRef.current) { clearTimeout(timerRef.current); } timerRef.current = setTimeout(() => { onChange(beginDate, endBegDate, endDate); }}, 1000)Copy the code

It can be seen that we can get the start and end serial numbers of the slider by moving the scale slider on the X axis, and then obtain the exact start and end time by using the serial number. After that, we can expose the parent component through the onChange method, and then do the logical processing. The code also shows that when we slide the slider, instead of executing it immediately, we add an anti-shake process to prevent the page from being frozen by repeated processing.

Special logic:

myChartRef.current.off('showTip'); myChartRef.current.on('showTip', function (params) { const {dataIndex} = params; onClickItem(timeVos[dataIndex], 3, dataIndex); }); myChartRef.current.getZr().off('click'); myChartRef.current.getZr().on('click', function (params) { flag = ! flag; if (flag) { option.tooltip.triggerOn = 'click'; }else { option.tooltip.triggerOn = 'mousemove'; } myChartRef.current.setOption(option); onClickGlobalArea(flag); });Copy the code

When we use the stack line chart, we move the mouse to the point of the line is to be able to get to the part of the data, if there is no accurate moved to the line point don’t always get data, regardless of supply and demand in line didn’t enter the line points to get points in the X axis starting time and ending time to do other logical querying data. After querying the API document, we chose to use the showTip event with the Tooltip attribute to achieve accurate data for mouse movements. The global click event getZr().on(‘click’, function() {}) is used to lock the pointer and stop moving the mouse, i.e. to lock the data for viewing.

A complete package

Ok, it is necessary to say that the encapsulation of Echarts components is the above points, which are the real requirements of our actual project. Here we only take the line diagram component to show the complete code, and the complete code of other components can be viewed after the warehouse is built and uploaded. The complete code of line chart component is as follows:

import React, {useEffect, useRef, useState} from 'react';
import {Spin} from 'antd';
import * as echarts from 'echarts';
import {ObjTest} from "../../../util/common";

interface ChartProps {
    dataSource: any; // 源数据
    loading: boolean; // 加载标识
    width?: number; // 图表宽度
    height?: number; // 图表高度
    backgroundColor: string; // 图表背景颜色
    fontSize?: number; // 字号
    beginTimeVo?: any; // 开始时间
    endTimeVo?: any; // 结束时间
    isWeekOrMonth?: number; // X轴显示周还是月
    legendSelected?: any; // 图例选中集合
    isShowDataZoom?: boolean; // 是否显示X轴缩放条
    isShowLegend?: boolean; // 是否显示图例
    gridTop?: string; // 图表内容距离图例高度
    isAllChecked?: boolean; // 是否全选标识
    isLockEchart: boolean; // 是否锁住图表
    selectedMode?: boolean; // legend选择模式
    onChange?: (beginDate: string, endBegDate: string, endDate: string) => void; // 缩放条缩放事件
    onLegendSelect?: (name: string, selected: any) => void; // legend图例选中或取消事件
    onClickItem?: (item: any, type: number, index: number) => void; // 点击X轴指示器
    onClickGlobalArea?: (bool: boolean) => void; // 点击全局区域
}


/**
 * 折线图表组件
 * @props
 * @constructor
 */
const LineChart = (props: ChartProps) => {
    const {
        dataSource,
        loading = true,
        width = 510,
        height = 220,
        backgroundColor = #fff,
        fontSize = 10,
        beginTimeVo = {},
        endTimeVo = {},
        legendSelected = {},
        isShowDataZoom = true,
        isShowLegend = true,
        gridTop = '15%',
        isWeekOrMonth = 1,
        isAllChecked = false,
        isLockEchart = false,
        onChange = (beginDate: string, endBegDate: string, endDate: string) => {},
        onLegendSelect = (name: string, selected: any) => {},
        onClickItem = (item: any, type: number, index: number) => {},
        onClickGlobalArea = (bool: boolean) => {}
    } = props;
    const {
        timeVos = [],
        names = [],
        questionDetails = [],
        beginTimeVo: beginTimeObj = {},
        endTimeVo: endTimeObj = {}
    } = dataSource || {};
    const lineRef = useRef<any>(null);
    const myChartRef = useRef<any>(null);
    const timerRef = useRef<any>(null);

    useEffect(() => {
        initChart();
    }, [dataSource, isLockEchart]);


    /**
     * 初始化Echarts
     */
    const initChart = () => {
        myChartRef.current = echarts.init(lineRef.current);
        let timeVosList: any[] = JSON.parse(JSON.stringify(timeVos)) || [];
        timeVosList.forEach(v => v.week = isWeekOrMonth === 1 ? `${v.week}\n${v.xtime}` : `${v.xtime}`);
        let flag: boolean = isLockEchart;
        
        const xAxisData: string[] = timeVosList && timeVosList.map(v => v.week);
        let seriesData: any[] = [];
        if (questionDetails && questionDetails.length > 0) {
            seriesData = questionDetails.map((v: any, index: number) => {
                let questions: any[] = [...v.questions].map(item => {
                    if (item) {
                        return item.toFixed(0);
                    }
                    return item;
                });
                return {
                    ...v,
                    name: v.name,
                    type: 'bar',
                    stack: v.stack,
                    itemStyle: {
                        borderRadius: [4, 4, 0, 0],
                    },
                    barWidth: 12,
                    data: questions,
                }
            });

        }
        let option: any = {
            tooltip: {
                show: true,
                trigger: 'axis',
                triggerOn: flag ? 'click' : 'mousemove',
                appendToBody: true,
                backgroundColor: '#546E7A',
                borderColor: '#546E7A',
                padding: [2, 5],
                position: function (point, params, dom, rect, size) {
                    return [point[0] - 75, 20];
                },
                textStyle: {
                    color: '#fff',
                    fontSize: 8
                },
                formatter: function () {
                    return '单击锁定数据至左下角';
                }
            },
            legend: {
                data: names,
                itemWidth: 20,
                itemHeight: 12,
                itemGap: 5,
                top: 0,
                left: '3%',
                orient: 'horizontal',
                inactiveColor: '#888',
                textStyle: {
                    color: 'rgba(255, 255, 255, .8)'
                },
                selected: !isAllChecked && !selectedMode ? legendSelected['合计'] ? legendSelected : {
                    ...legendSelected,
                    '合计': false
                } : legendSelected,
            },
            grid: {
                top: gridTop,
                left: '3%',
                right: '8%',
                bottom: 30,
                containLabel: true
            },
            dataZoom: [
                {
                    type: 'slider',
                    show: isShowDataZoom,
                    height: 20,
                    left: 10,
                    right: 20,
                    bottom: 5,
                    startValue: begTime,
                    endValue: endTime,
                    rangeMode: ['value'],
                }
            ],
            xAxis: [
                {
                    type: 'category',
                    data: xAxisData,
                    boundaryGap: false,
                    name: isWeekOrMonth === 1 ? '周' : '月',
                    nameTextStyle: {
                        color: '#607D8B',
                        verticalAlign: 'middle'
                    },
                    axisPointer: {
                        show: true,
                        type: 'shadow',
                        shadowStyle: {
                            color: 'rgba(245, 245, 245, 0.1)'
                        },
                        label: {
                            show: false
                        }
                    },
                    axisLabel: {
                        lineHeight: 16,
                        color: '#607D8B',
                        fontSize: 10
                    }
                }
            ],
            yAxis: [
                {
                    type: 'value',
                    offset: 10,
                    axisLabel: {
                        color: '#607D8B',
                        fontSize: 10
                    },
                    splitLine: {
                        lineStyle: {
                            type: 'dashed',
                            color: 'rgba(255, 255, 255, .06)'
                        }
                    }
                }
            ],
            series: seriesData
        };
        myChartRef.current && myChartRef.current.setOption(option, true);
        myChartRef.current.on('dataZoom', updatePosition);
        myChartRef.current.off('legendselectchanged');
        myChartRef.current.on('legendselectchanged', function (params: any) {
            const {name, selected} = params;
            setLegendSelectedBak(selected);
        });
        myChartRef.current.off('showTip');
        myChartRef.current.on('showTip', function (params) {
            const {dataIndex} = params;
            onClickItem(timeVos[dataIndex], 3, dataIndex);
        });
        myChartRef.current.getZr().off('click');
        myChartRef.current.getZr().on('click', function (params) {
            flag = !flag;
            if (flag) {
                option.tooltip.triggerOn = 'click';
            }else {
                option.tooltip.triggerOn = 'mousemove';
            }
            myChartRef.current.setOption(option);
            onClickGlobalArea(flag);
        });
    }


    /**
     * 移动缩放滑块
     * @param e
     */
    const updatePosition = (e: any) => {
        let xAxis: any = myChartRef.current.getModel().option.dataZoom[0];
        let startIndex: number = xAxis.startValue;
        let endIndex: number = xAxis.endValue;
        let beginDate: string = timeVos && timeVos[startIndex] ? timeVos[startIndex]['beginTime'] : '';
        let endBegDate: string = timeVos && timeVos[endIndex] ? timeVos[endIndex]['beginTime'] : '';
        let endDate: string = timeVos && timeVos[endIndex] ? timeVos[endIndex]['endTime'] : '';
        if (timerRef.current) { clearTimeout(timerRef.current); }
        timerRef.current = setTimeout(() => {
            onChange(beginDate, endBegDate, endDate);
        }, 1000)
    }


    return (
        <Spin spinning={loading}>
            {
                ObjTest.isNotEmptyArray(dataSource)
                    ?
                    <div ref={lineRef} style={{width, height, backgroundColor}}></div>
                    :
                    <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
            }
        </Spin>
    )
}


export default LineChart;
Copy the code

Actually the packaging or defective, such as the back-end transmission fields, different back-end set field must be different, so there is no perfect in this reuse, but behind the son we encapsulate rectangular tree graph is considered, we define a dictionary to convert, parent components that can be accurate field into the perfect show; Here, the line chart, bar chart and rising sun chart are always the same person at the back end of my connection, so there is no situation of defining different fields, but friends must take this into account when encapsulating, otherwise someone who takes over the code may inevitably be greeted by the family, then it will be very awkward.

In this way, I will post the wrapping code of rectangle tree graph. As for the wrapping code of bar graph, pie chart and rising sun graph, you can try to wrap it by yourself or refer to it after I build the warehouse. Rectangular tree diagram package complete code:

import React, {useEffect, useRef, useState} from 'react'; import {Empty, Spin} from 'antd'; import * as echarts from 'echarts'; import {ObjTest} from ".. /.. /.. /util/common"; interface TreeMapChartProps { dataSource: any[]; // Source data mapping? : any; // Loading: Boolean; // Load the id type? : number; // 0: task group; 1: personnel width? : number; // Graph width height? : number; } /** * const TreeMapChart = (props: TreeMapChartProps) => { const {dataSource, loading, type, width = 512, height = 255, mapping = {name: 'userGroupName', value: 'value'}} = props; const treeMapRef = useRef<any>(null); const myChartRef = useRef<any>(null); useEffect(() => { if (treeMapRef.current) { initChart(); } }, [dataSource]); /** */ const initChart = () => {myChartref.current = echarts.init(treemapref.current); let seriesData: any[] = (dataSource || []).map((v: any, ind: number) => { return { ... v, value: v[mapping.value], name: v[mapping.name], } }); let option: any = { series: [ { name: 'All', type: 'treemap', width: '100%', height: '100%', roam: true, breadcrumb: { show: true }, data: seriesData, label: { offset: [0, 5], lineHeight: 14, fontSize: 10, formatter: '{b}\n{c}' } } ] }; myChartRef.current && myChartRef.current.setOption(option, true); } return ( <Spin spinning={loading}> { ObjTest.isNotEmptyArray(dataSource) ? <div ref={treeMapRef} style={{width, height}}></div> : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /> } </Spin> ) } export default TreeMapChart;Copy the code

Usage:

Component package, it must be used, otherwise it is a waste of time;

Import LineChart from "./component/LineChart"; import TreeMapChart from "./component/TreeMapChart"; 2) Use components: // LineChart dataSource={workHoursData} loading={loading} width={710} height={350} beginTimeVo={beginTimeVo} endTimeVo={endTimeVo} isLockEchart={echart1Bool} onChange={(beginDate: string, endBegDate: string, endDate: string) => { let days = getDaysBetween(beginDate, endBegDate); if (days > 60) { setFieldsValue({weekOrMonth: 2}); } else { setFieldsValue({weekOrMonth: 1}); } setFieldsValue({periodEnum: '', beginTime: beginDate, endTime: endDate}); setBeginTimeVo({}); setEndTimeVo({}); queryProjectData(); }} onClickItem={(item: any, type: number, index: number) => onClickItemHandle(item, type, index)} onClickGlobalArea={(bool: Boolean) => setEchart1Bool(bool)} /> // <TreeMapChart dataSource={hours} type={0} width={710} height={350} mapping={{name: 'projectName',value: 'valueStr'}} loading={loading} />Copy the code

The effect

I originally wanted to record the screen for my friends to see the effect, but because the project has some contents that can’t be displayed, I can’t record the screen to show. Take a look at the picture, though a little regretful, there are things I can’t show, so sorry.

Past wonderful articles

  • How can I change the default DatePicker week selector in Antd?
  • How to encapsulate the Vue watermark component and how to use it in React?
  • The most powerful rich text editor? TinyMCE Series [3]
  • The most powerful rich text editor? TinyMCE Series [2]
  • The most powerful rich text editor? TinyMCE Series of articles [1]
  • React project to implement a Checkbox style effect component
  • 2022 First update: 3 Progress bar effects in front end projects
  • Front-end Tech Trends 2022: You don’t come in and see how to earn it a small target?
  • If ancient have programmers writing summaries, probably is such | 2021 year-end summary
  • Front-end development specifications in front end team practice
  • Javascript high-level constructors
  • Javascript high-level inheritance
  • Recursion and closure

After the language

Guys, if you find this article helpful, click 👍 or ➕. In addition, if there are questions or do not understand the part of this article, you are welcome to comment on the comment section, we discuss together.