Awesome Front-End Animation R&D and Practice —— Take Data Dashboard as an Example

XuanHunXuanHun
21 min read

Introduction

Animation is very important in front-end products, as it can enhance user experience, improve the effect of information transmission, and shape brand image. In data products, well-designed animations can intuitively present complex data and information, highlight key data trends, leave a deep impression on users, and enhance brand recognition and reputation. VChart not only has basic chart drawing capabilities, but also provides flexible and rich animation capabilities.

This sharing will introduce how to precipitate animation design solutions from scenarios and further realize them through VChart and the underlying self-developed visualization system.

What is Visual Animation

VisActor: https://visactor.io

Visual animation is a form of expression that presents data, information or concepts through dynamic graphics, images and videos.

It has the following important characteristics and functions:

  • Visual representation: It transforms complex data or abstract concepts into easily understandable visual elements, allowing viewers to quickly obtain key information.

  • Enhanced attractiveness: Compared to static charts or text, animation can attract the attention of viewers and improve the effect of information transmission.

  • Storytelling: It can tell the process of information transmission according to certain logic and plots.

What is a data dashboard?

Definition

Data dashboard is an information display system that presents a large amount of data in a centralized and visual way on a large screen.

It has the following characteristics and functions:

  1. Visual display: It uses graphics, charts, maps and other visual elements to present data, which is easy to understand and interpret quickly.

  2. Large-scale data presentation: It can process and display massive and complex data.

  3. Real-time: It can update data in real time to reflect the latest information status.

Usage scenarios

Smart Government Affairs

New Energy

Wisdom Cultural Tourism

Financial Services

The significance of animation for big screens

It is undeniable that dynamic effects are more eye-catching than static effects.

Operating business data big screen: Start animation

Operational Business Data Dashboard: Turn Off Animation

The scene of large-screen static or reporting decides that it pays more attention to dynamic display. It is more vivid, more affinity , and what it conveys is no longer cold numbers, but the story behind the numbers.

The magnitude and complexity of data determine its need to support context information at different levels.

Dynamic: drawn dynamically by year

Static: Split the column by year

Visual Animation Design in Big Screen Scenarios

Animation Scope

Element level: Animation of the element itself

Page level: animation between different pages

Chart level: Animation when switching between charts

  1. The animation of graphic elements takes place from the moment the visual attributes of the graphic elements change to the end of the change. Due to the real-time monitoring and interactive filter data functions of the large screen, the attributes of the graphic elements will change at any time, and the animation of the graphic elements will be performed frequently.

  2. Chart switching animation depends on the morphing animation capabilities of the chart library. However, since the product function side does not open the function of switching animation types, there is no scene for the performance of chart switching animation.

  3. Page switching animation depends on the business logic of the large screen, which is implemented by the switching logic between the containers of the large screen pages and is not within the scope of VChart's capabilities.

In summary, this animation customization mainly focuses on the animation of elementary graph units within the chart.

The relationship between animation scopes

Animation Configuration

After deciding to use graphic animation as the main body for upgrading the animation effects, we conducted user-oriented animation configuration abstractions for different charts.

Primary structure: Entry, Normal, Update, and Exit

First, based on the chronological evolution, the state of a graphic element can be sequentially divided into three categories:

  • Entry: The state where a graphic element appears from nothing when the chart is initially drawn.

  • Normal: The static state of the chart after the graphic element is drawn.

  • Exit: The state where a graphic element disappears from something when the chart is destroyed.

Secondly, the update state is also a frequent state of a graphic element.

  • Update: When the data is regularly updated on the large screen and the attributes are changed interactively, the chart will be updated and redrawn.

Three types of large-screen configurations that trigger the update state:

Data is updated regularly.

Interactive filter data

Interactive style change

The above analysis shows that the normal state and the update state are the high-frequency states of the graph element.

Secondary structure: Normal state = rotation + atmosphere, Update = sequential update + general update

There are two main ideas for normal animation: cyclically executing a certain animation, and attaching additional graph elements to enhance the graph elements of the chart. We define the former as "rotation" and the latter as "atmosphere".

Carousel: Loop Highlight Chart Widgets

Atmosphere: Additional graphic

There are two main ideas for updating animation: playing it dynamically in chronological order and updating data or a certain configuration. We define the former as "timing update" and the latter as "general update".

Timestamp update:Controlled by time field, played in chronological order

General update: any update caused by configuration or data change

3-level structure: specific animation configurations for different charts

Chart ComponentAdmissionUpdateNormalcy
Timing updateGeneral UpdateSlideshowatmosphere
Column ChartSide-by-Side Column ChartStack Column ChartPercentage Column ChartGrowthGrowthMove InGrowthMove InGrowthMove InGroup highlighting.Streamer
Bar Chart, Side-by-Side Bar Chart, Stacked Bar Chart, Percentage Bar ChartGrowthGrowthDisplacementGrowthDisplacement.
Area ChartPercentage Area ChartLine ChartGrowthLoadingNoneGrowthGrowthLoading.Streamerripple
Pie ChartRose ChartRing ChartradialZoomNoneradialChange sizeChange the centerNone
scatter plotcircle viewGrowthZoomNoneGrowthGrowthZoomripple
Radar ChartradialZoomNoneGrowthNoneripple

Visual animation implementation in large-screen scenarios

Underlying principles

VRender Rendering Layer - Timing and Animation Atoms

The VRender layer is mainly composed of the Animate animation class and the Tick timer class.

Animate Class

  • Mounted on the graphical node, defining how the animation is executed.

  • The most important member is onUpdate, which receives the current animation execution ratio and updates the graphical node attributes based on the ratio.

/**
** Animation begins: Some preparatory work
*/
onStart() => void

/**
* Update status of map elements
* @param end Is the animation over?
* @param ratio The ratio of animation execution[0, 1]
* @param out Element Attributes
*/
onUpdate(end: boolean, ratio: number, out: any) => void

/**
* The animation is over.
*/ 
onEnd()

/**
** Interpolation tool function
* @param key 
* @param ratio The ratio of animation execution[0, 1]
* @param from Starting Attributes
* @param to Termination Attributes
*/
interpolateFunc(key: string, ratio: number, from: any, to: any, nextAttributes: any) => void

The Tick class

  • Used for controlling animation execution logic such as start, pause, etc.
// Set frame rate per second
setFPS: (fps: number) => void;
// Set the time interval for repeated execution
setInterval: (interval: number) => void;
// Start
start: (force?: boolean) => boolean;
// Pause
stop: () => boolean;
// resume
resume: () => boolean;

Case: Streamline Animation

Cylindrical chart carousel streamer animation

Stream animation of line chart rotation

Columnar Graph Glitter Animation:

Essentially, it involves adding a rectangular node to the columnar graph element, and achieving the glitter effect by adjusting the position of the rectangular node.

  onStart(): void {
    // Get the parent node (the graphic that needs to be loaded for animation)
    const root = this.target.attachShadow();
    // Get some attributes
    const isHorizontal = this.params?.isHorizontal ?? true;
    const sizeAttr = isHorizontal ? 'height' : 'width';
    const otherSizeAttr = isHorizontal ? 'width' : 'height';
    const size = this.target.AABBBounds[sizeAttr]();
    const y = isHorizontal ? 0 : this.target.AABBBounds.y1;

    // Initialize the Glow Rectangle
    const rect = application.graphicService.creator.rect({...some_attr})
  }


  protected onUpdate(end: boolean, ratio: number, out: Record<string, any>): void {
    const isHorizontal = this.params?.isHorizontal ?? true;
    const parentAttr = (this.target as any).attribute; // Get the parent node.
    if (isHorizontal) {
      const parentWidth = parentAttr.width ?? Math.abs(parentAttr.x1 - parentAttr.x) ?? 250;
      const streamLength = this.params?.streamLength ?? parentWidth;
      const maxLength = this.params?.attribute?.width ?? 60;
      // Start point, align the right endpoint of rect x with the left endpoint of parent.
      // If parent.x1 < parent.x, we need to move the rect property to the position of parent x1, because the initial rect.x = parent.x
      const startX = -maxLength;
      // Interpolated position
      const currentX = startX + (streamLength - startX) * ratio;
      // Position > 0
      const x = Math.max(currentX, 0);
      // Width Calculation
      const w = Math.min(Math.min(currentX + maxLength, maxLength), streamLength - currentX);
      // If the right endpoint of rect extends beyond the right endpoint of parent, adjust the width dynamically.
      const width = w + x > parentWidth ? Math.max(parentWidth - x, 0) : w;
      this.rect.setAttributes(...some_attr);
    } else {
      // ... Vertical movement
    }
  }

Streamline Animation of Line Chart:

Essentially, it involves adding a line node to the online graph element, achieving the streamline effect by adjusting the position of the line node.

In order to make the additional line node stick closely to the parent node, what needs to be done here is to split or duplicate a selected section of the line.

// ....Pre-initialize the polyline chart element

protected onUpdate(end: boolean, ratio: number, out: Record<string, any>): void {
  // For a polyline, you only need to get the points of the path to reproduce it. 
  // 1. Iterate over each segment and calculate the length of each segment of the polyline.
   for (let i = 1; i < points.length; i++) {
      totalLen += PointService.distancePP(points[i], points[i - 1]);
    }
    // Calculate the positions of the start and end points of the node based on the ratio
    const startLen = totalLen * ratio;
    const endLen = Math.min(startLen + this.params?.streamLength ?? 10, totalLen); 
    // Traverse each segment and find the node
   const  len = PointService.distancePP(points[i], points[i - 1]);
   const nextPoints.push(PointService.pointAtPP(points[i - 1], points[i], 1 - (lastLen + len - startLen) / len));

   // For a curve, split the Bezier curve according to the percentage.
   const startPercent = 1 - (lastLen + len - startLen) / len;
   let endPercent = 1 - (lastLen + len - endLen) / len;
   const [_, curve2] = divideCubic(curveItem as ICubicBezierCurve, startPercent);
   customPath.moveTo(curve2.p0.x, curve2.p0.y);
}

VGrammar Syntax Layer - Animation Triggering and Choreography

Triggering Timing

VGrammar divides the animation triggering timing into two types: passive triggering and active triggering.

Passive Triggering usually occurs when the state of a graphical element changes:

  • enter: Animation trigger when a new graphical element is added;

  • exit: Animation trigger when a graphical element is removed;

  • update: Animation trigger when the visual channel of a graphical element is updated;

  • state: Animation trigger when the interaction state of a graphical element changes. In the most common scenario of interaction and animation coordination, the animation is expressed as an interpolation with the change of interactive status, such as hover animation. For this animation state, we do additional processing in the underlying rendering library to avoid expensive data stream calculations and improve performance;

enter状态

update status

主动触发发生在任意时刻,在大屏的场景应用中体现在轮播动画中,因为轮播动画发生前,图元没有任何状态的改变

Pie Chart carousel animation: keep looping at regular intervals

Effect Encapsulation

Effect can be understood as the smallest executable unit of animation.

  1. Effect can be a packaged specific animation effect: It is based on the correct attributes and context of the graphic mapping after the dataflow is executed, and different animation types are encapsulated.

For example, for the growth animation of a bar chart, developers can simply specify type: 'growHeightIn' to make the bar chart elements in the same dimension grow upwards from the same starting point.

animationNormal: { // Animation trigger timing
  channel: { // Custom Visual Channel
    outerBorder: { // Border properties
      from: { distance: 0, strokeOpacity: 1 }, // Attributes at the start
      to: (...args) => { // Attributes at the end
        return {
          distance: 16,
          strokeOpacity: 0, 
          stroke: args[1].graphicItem.attribute.fill
        }
      }
    }
  },
}

T

Timeline scheduling

With the basic units of animation, the next step is to schedule the execution time of different animations.

VGrammar describes the execution logic of the corresponding graphical animation through animation configuration, and the schematic diagram of animation configuration is as follows:

The definition of Timeline in animation is:

$$timeline=$$

Elements in the timeline include:

  • timeslice: A slice of animation that describes a specific interpolated animation configuration, including specific animation effects, animation execution time, and other specific animation configurations. All timeslices in a timeline are connected end to end;

  • startTime: The start execution time of the animation, describing the time when the current timeline starts to execute the animation after being triggered;

  • duration: The execution duration of the animation timeline, describing the duration of the current timeline animation;

  • loop: A timeline can be set to loop, and all animation processes described by all timeslices included in it will be repeated.

The definition of Timeslice in animation is:

$$timeslice=$$

The elements of Timeslice include:

  • effect: The specific execution effect of the animation, describing the specific interpolation calculation logic of the visual channel attributes of the graphic elements. effect can be a packaged specific animation effect, or an animation configuration configured by the developer with the start and end states, describing the calculation logic of attribute interpolation of the animation;

  • duration: The execution duration of the animation slice;

  • delay: The waiting time before the animation slice is executed;

  • OneByOne: Describes the logic of successive execution of specific graphical elements within the corresponding graphic elements.

VChart Chart Layer - Status and Configuration Encapsulation

Since the VGrammar layer has already completed the encapsulation and arrangement of most of the animation effects, the VChart layer only needs to perform further encapsulation and attribute transmission based on the common logic of the chart library.

Additional trigger timing

VChart adds three new states - appear, disappear, and normal - to the previous states (enter, update, exit, and state). Because in business scenarios, in addition to when the state of the图形 changes, people obviously also hope to have uninterrupted animations in normal situations, as we mentioned in the previous review of large-screen animations.

Further encapsulation of effects

For some special animations, VChart needs to do additional processing.

For example, in the animation of the radar chart below, it is manifested as expanding according to the angle. However, since the radar chart is composed of scatter and ring area graphics, interpolation calculation alone cannot achieve synchronization when controlling their expansion. Therefore, the approach taken by VChart is to control the expansion of the upper-level graphics according to the angle, which allows the sub graphics to expand simultaneously.

红色部分为rootMark, 构成雷达图的点图元和面积图元皆为其子图元

// Configure type: grow on rootMark 
// How VGrammar layer handles it
{
   from: startAngle,
   to: endAngle
}

Set the starting and ending angles directly, and use VRender's interpolation update at the bottom.

Animation configuration encapsulation and transmission

The configuration finally passed on by VChart is as shown in the figure above:

  • The first level is the trigger state.

  • The second level is the type of chart element: different chart elements have different types.

  • The third level is the specific configuration of animation effects:

    • type represents the encapsulated animation type.

    • channel represents the animation type open to user customization, used for custom interpolation.

    • timeSlices represents the animation slices open to user definition, used for custom scheduling.

More VChart animation configuration tutorials: https://www.visactor.io/vchart/guide/tutorial_docs/Animation/Animation_Types

Large-screen Animation Implementation

Configuration and Combination of Animation Atoms

Animation atoms refer to the minimized configuration of graph element animation effects.

Different chart configurations have different methods. Choose which of the three configurations - type, channel, and timeSlices - to use based on the effect.

Here are different animation configuration cases:

Lower level: type configuration

All attribute calculations are built-in, and only the type needs to be specified.

Line chart entry: Loading animation


animationAppear: { // Trigger time
  bar: { // Element types
      type: 'clipIn', // Animation Type
      duration: 1000,  // Duration
      easing: 'linear' // Easing effect: uniform speed
  }
}

VChart Configuration

Pie Chart Entry: Radial Animation



animationAppear: { // Trigger time
  pie: { // Element type
      type: 'growAngleIn', // Animation type
      duration: 1000,  // Duration
      easing: 'circInOut' // Easing effect: ease in and ease out
  }
}

VChart configuration

Entry of Radar Chart: Zoom Animation



animationAppear: { // Triggering moment
  radar: { // Element Type
      type: 'grow', // Animation types
      duration: 1000,  // Duration
      easing: 'quintIn' // Easing effect: ease in
  }
}

VChart configuration

Intermediate: Channel Configuration

It is necessary to calculate the sequence of the animation of the graph elements and control the rotation order.

Pie chart: carousel - change position

Pie Chart: Carousel - Change Size

{
  channel: {
    x: {
      from: (...p) => p[1].graphicItem.attribute.x,
      to: (...p) => {
        const angle = (p[1].graphicItem.attribute.startAngle + p[1].graphicItem.attribute.endAngle) / 2
        return p[1].graphicItem.attribute.x + offset * Math.cos(angle)
      }
    },
    y: { .... },
    outRadius: { .... }
  },
  oneByOne: true, 
  duration: loopDuration,
  delayAfter: loopDuration + interval, // The previous animation line is aligned with the next animation line.
}, {
  channel: { 
    x: {
      from: (...p) => {
        const angle = (p[1].graphicItem.attribute.startAngle + p[1].graphicItem.attribute.endAngle) / 2
        return p[1].graphicItem.attribute.x + offset * Math.cos(angle)
      },
      to: (...p) => p[1].graphicItem.attribute.x,
    },
    y: { .... },
    outRadius: { .... }
    oneByOne: true, 
    duration: loopDuration,
    delayAfter: interval,
}

Advanced: timeSlices configuration

Further calculation of the animation sequence time for different groups is required.

Column Chart: Carousel - Group Highlights

channel_1: 
$$delay = duration * index $$
$$delayAfter = duration * (totalCount - index)$$

chennel_2:
$$delay = duration * (index + 1)$$
$$delayAfter = duration * (totalCount - index - 1)$$
timeSlices: [
  {
    effects: {
      channel: {
        fill: {
          to: fillColor
        },
        stroke: {
          to: strokeColor
        }
      },
      easing: ease
    },
    delay: (datum, element, context, global) => {
      const { count, index } = getGroupInfo(chartInstance, datum)
      if(count === 0) {
        return 0
      }
      return index * totalDuration / count
    },
    duration: (datum, element, context, global) => {
      const { count, index } = getGroupInfo(chartInstance, datum)
      if(count === 0) {
        return 1000
      }
      return totalDuration / count / 2
    }
  },
  {
    effects: {
      channel: {
        fill: {
          from: fillColor,
          to: (...p) => {
            return p[1].graphicItem.attribute.fill
          }
        },
        stroke: {
          from: strokeColor,
          to: (...p) => {
            return p[1].graphicItem.attribute.fill;
          }
        }
      },
      easing: ease
    },
    delayAfter: (datum, element, context, global) => {
      const { count, index } = getGroupInfo(chartInstance, datum)
      if(count === 0) {
        return 0
      }
      return (interval + atmoDuration) * 1000 + (count - index - 1) * totalDuration / count;
    },
    duration: (datum, element, context, global) => {
      const { count, index } = getGroupInfo(chartInstance, datum)
      if(count === 0) {
        return 1000
      }
      return totalDuration / count / 2
    }
  }
]

Animation Atomic Composition

Area Chart Loop Animation

Slideshow: Load + Atmosphere: Glow & Ripple

animationNormal: {
    "area": [{ // Area Chart Loading Animation
            "type": "clipIn", "oneByOne": false, "startTime": 5000, "easing": "circInOut", "loop": true, "duration": 1000,  // 动画相关配置 
            "delayAfter": 6000, // Arrange related configuration
            "controlOptions": { "immediatelyApply": false }
        },
        {   // Area Chart Atmosphere Animation
            "loop": true, "startTime": 5000, "duration": 1000, "easing": "circInOut"
            custom: StreamLight, // Required self-introduction
            "delay": 1000, // Arrangement of related configurations
            "delayAfter": 5000, // Arrangement of related configuration
    }],
    "point": [{
            "loop": true, "startTime": 5000, "easing": "circInOut",
            "delayAfter": 6000, // Arrange related configurations
            "duration": 1000, // Arrangement related configuration
            "channel": { "outerBorder": { "from": { "distance": 0, "strokeOpacity": 1 } }
            }
        }
    ]
  }
}

Timeline Animation

Timeline Control: Interactive Control of Data Updates

Timeline animation usually proceeds incrementally or decrementally with time data. The Player component provided by VChart allows for automatic, manual play/toggle control of such animations.

The player component supports functions such as forward, backward, and play, allowing users to interactively switch between chronological data.

{
    dataSpecs: dataset[] // Each "frame" of data constitutes an array, and the player automatically recognizes and invokes it.
}

The essence of the time series control Player is to fragment the data based on the time field, switch the data of different fragments, that is, call the updateData interface, to realize the redrawing of the chart.

Timestamp: Display the current time series

In order to let the audience know where they are in the current time series, text labels are often required.

By using the customMark provided by VChart and binding the dataId, it can change as the data changes.

The text label in the lower right corner can show the current year.

customMark: [{
  type: 'text',
  dataId: 'year',
  style: {
    text: datum => datum.year, // Change according to the changes in the data
    x: (datum, ctx) => {
      return ctx.vchart.getChart().getCanvasRect()?.width - 50
    },
    y: (datum, ctx) => {
      return ctx.vchart.getChart().getCanvasRect()?.height - 50
    }
  }}
],

Conflicts in Animation

Reasons for Conflicts

The relationship between large-screen chart rendering and VChart animation execution

The basic process of rendering charts on a large screen is to initially create an empty chart, and then update it using the updateSpec method.

There are three ways to update updateSpec:

  • Font loading is complete: This occurs a short time after initialization.

  • Automatically obtaining data: The latest data is obtained at regular intervals according to the user's configuration.

  • Configuration update: Users can switch chart configurations after the chart is rendered, and this process can occur at any time.

Possible conflicts:

  1. Conflicts between the exit and loop animations

  2. Conflicts between the exit and update animations

  3. Conflicts between the update and loop animations

Conflicts between the exit animation and loop animation

In the chart library's processing, if the exit animation and loop animation are triggered at the same time, the two animations will be executed in sequence.

The entry and loop are executed simultaneously, as if the entry has been executed twice, causing misunderstandings.

Solution:

Configuring startTime on the animation can delay its entry time, thereby avoiding the first loop animation and preventing conflicts with the appear animation.

animationNormal: {
  bar: {
      startTime: appear_duration,
      type: 'growHeightIn',
      duration: ....
      // ....
  }
}

Conflict between entry and update animations

The conflict between entry and update animations occurs when the font is loaded and the chart is re-rendered shortly after the first render, causing the entry animation to be immediately interrupted.

The appearance was disrupted by the update, as if the view was stuck, affecting the viewing experience.

Solution: Use setTimeout to defer the update animation time.

const delayTime =  
    this.getAppearDuration(preAnimationSpec) * 1000 - // The duration of the entry animation
    (Date.now() - this.chartAppearTimeStamp) // The interval between the current time and the start time of the entry animation
setTimeout(() => { this.updateChartSpec(newSpec)}, delayTime)

Conflicts between Updating Animations and Loop Animations

When the chart library performs an updateSpec, it determines whether to update the chart elements (reMake: false) or redraw the chart elements (reMake: true).

If the chart elements need to be redrawn, any previously attached animations will be interrupted, meaning that the loop animation will no longer play.

To address this issue of interrupted loop animations, the update process for charts has been modified: updateSpec -> specWithoutDataChange + updateData.

const specWithoutDataChange = {
  ...curSpec,
  data: preData
}
// ....
const curData = curSpec.data

// step1: Update the spec except for the data, ensure that other attributes are correctly mounted, and the animation is blocked.
this.chartInstance?.updateSpec(specWithoutDataChange, false, undefined, { reAnimate }) // Note, here needs to add protection?, there may be appear that the animation has not been completed, it is necessary to switch to the next page for the next update, then when setTimeout is executed, chartInstance may be released.

// step2: Updated data by using the updateData API, the animation is restarted.
this.chartInstance?.updateFullDataSync(curData, true, { reAnimate })

Quality Assurance

Large-screen side: Modular manual testing + scenario-based automatic testing to ensure the quality of application layer rendering.

For static products, improve the automatic test cases

Automated testing can be divided into two parts:

  1. Modular testing: Create screens for color themes,Graphics, labels, axes, legends,... respectively.

During actual operation:

  • Each screen should contain all functional cases of the corresponding module.

  • Update cases based on the on-call status.

  • Minor changes: Run through the test screens of the corresponding module before submitting the code.

  1. Scenario testing: Domain key large screen case entry (conducting multiple rounds of domain key large screen automated testing, which works well and can find many detailed issues).

During actual operation:

  • Update cases according to the on-call status.

  • For major changes, run through the domain's key dashboards fully before submitting code.

Strengthen self-testing for dynamic/interactive products.

  1. For dynamic products, set up cases for each animation effect of each chart type. After making changes, it is necessary to confirm the effect with designers and products in a timely manner.

  2. For interactive products:

Interactions can be divided into chart interactions and general interactions for large screens.

Chart interactions need to pay attention to whether the tooltip effect is correct.

General interactions for large screens need to pay attention to whether the transparency of chart parameters is correct. For example, in the following scenarios, chart parameters are relied on:

These boundary cases were not well understood before, and these boundary cases will be added later to ensure that they should be tested during self-testing.

Chart library side: Chart screenshot comparison + memory testing to ensure the rendering quality of a single chart

Issues caused by the VChart can be added as cases in the BugServer of the chart library's quality assurance platform. If there are any problems, they can be detected in time before the release of the chart library.

Specific implementation:

  1. Enter high-frequency cases of the big screen in the bugserver, and mark the test content and associated issues. Take screenshots for comparison every time before code merge / release.

Contact us

Finally, we sincerely welcome all friends interested in data visualization to participate in the open source construction of VisActor:

VChart: VChart official website, VChart Github (thanks for the Star)

VTable: VTable official website, VTable Github (thanks for the Star)

VMind: VMind official website, VMind Github (thanks for the Star)

Discordhttps://discord.gg/3wPyxVyH6m

Twiterhttps://twitter.com/xuanhun1

1
Subscribe to my newsletter

Read articles from XuanHun directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

XuanHun
XuanHun

I am a developer from VisActor( https://www.visactor.io/)