VChart icon indicating copy to clipboard operation
VChart copied to clipboard

[Bug] 条形图动画更新后,出现空白渲染

Open skie1997 opened this issue 5 months ago • 0 comments

Version

2.0.1

Link to Minimal Reproduction

vscreen

Steps to Reproduce

const spec = {
  type: 'bar',
  theme: {},
  xField: ['10002'],
  yField: ['OCxMNY4j86cy', '10001'],
  direction: 'horizontal',
  sortDataByAxis: true,
  seriesField: '30001',
  padding: {
    left: 6,
    right: 6,
    top: 6,
    bottom: 6
  },
  labelLayout: 'region',
  data: [
    {
      id: 'data',
      values: [
        {
          '10001': 'Profit',
          '10002': 20,
          '10003': 'XU0wVG43cGL7',
          '30001': '2023',
          OCxMNY4j86cy: 'Labels',
          XU0wVG43cGL7: 20,
          vGTOlXqnCrc3: '2023'
        },
        {
          '10001': 'Profit',
          '10002': 44,
          '10003': 'XU0wVG43cGL7',
          '30001': '2022',
          OCxMNY4j86cy: 'Labels',
          XU0wVG43cGL7: 44,
          vGTOlXqnCrc3: '2022'
        },
        {
          '10001': 'Profit',
          '10002': 15,
          '10003': 'XU0wVG43cGL7',
          '30001': '2023',
          OCxMNY4j86cy: 'Tables',
          XU0wVG43cGL7: 15,
          vGTOlXqnCrc3: '2023'
        },
        {
          '10001': 'Profit',
          '10002': 20,
          '10003': 'XU0wVG43cGL7',
          '30001': '2022',
          OCxMNY4j86cy: 'Tables',
          XU0wVG43cGL7: 20,
          vGTOlXqnCrc3: '2022'
        },
        {
          '10001': 'Profit',
          '10002': 50,
          '10003': 'XU0wVG43cGL7',
          '30001': '2023',
          OCxMNY4j86cy: 'Storage',
          XU0wVG43cGL7: 50,
          vGTOlXqnCrc3: '2023'
        },
        {
          '10001': 'Profit',
          '10002': 65,
          '10003': 'XU0wVG43cGL7',
          '30001': '2022',
          OCxMNY4j86cy: 'Storage',
          XU0wVG43cGL7: 65,
          vGTOlXqnCrc3: '2022'
        },
        {
          '10001': 'Profit',
          '10002': 15,
          '10003': 'XU0wVG43cGL7',
          '30001': '2023',
          OCxMNY4j86cy: 'Furn',
          XU0wVG43cGL7: 15,
          vGTOlXqnCrc3: '2023'
        },
        {
          '10001': 'Profit',
          '10002': 40,
          '10003': 'XU0wVG43cGL7',
          '30001': '2022',
          OCxMNY4j86cy: 'Furn',
          XU0wVG43cGL7: 40,
          vGTOlXqnCrc3: '2022'
        },
        {
          '10001': 'Profit',
          '10002': 57,
          '10003': 'XU0wVG43cGL7',
          '30001': '2023',
          OCxMNY4j86cy: 'Art',
          XU0wVG43cGL7: 57,
          vGTOlXqnCrc3: '2023'
        },
        {
          '10001': 'Profit',
          '10002': 35,
          '10003': 'XU0wVG43cGL7',
          '30001': '2022',
          OCxMNY4j86cy: 'Art',
          XU0wVG43cGL7: 35,
          vGTOlXqnCrc3: '2022'
        }
      ],
      fields: {
        '10001': {
          alias: '指标名称'
        },
        '10002': {
          alias: '指标值'
        },
        '30001': {
          alias: '图例项',
          domain: null,
          sortIndex: null,
          lockStatisticsByDomain: true
        },
        OCxMNY4j86cy: {
          alias: 'Product',
          domain: ['Art', 'Furn', 'Labels', 'Storage', 'Tables'],
          sortIndex: 0,
          lockStatisticsByDomain: true
        },
        vGTOlXqnCrc3: {
          alias: 'Year'
        },
        XU0wVG43cGL7: {
          alias: 'Profit'
        }
      }
    }
  ],
  stackInverse: true,
  axes: [
    {
      type: 'band',
      tick: {
        style: {
          strokeOpacity: 0.2
        }
      },
      grid: {
        visible: false,
        style: {
          zIndex: 150,
          stroke: '#FFFFFF',
          lineWidth: 1,
          lineDash: []
        }
      },
      orient: 'left',
      visible: true,
      domainLine: {
        visible: false,
        style: {
          lineWidth: 1,
          stroke: '#d5d7e2'
        }
      },
      title: {
        visible: false,
        space: 5,
        text: '',
        style: {
          fontSize: 12,
          fill: 'rgba(255,255,255,0.5)',
          fontFamily: 'D-DIN',
          fontWeight: 'normal'
        }
      },
      maxHeight: null,
      autoIndent: false,
      sampling: false,
      zIndex: 200,
      label: {
        visible: true,
        space: 8,
        style: {
          fontSize: 12,
          fill: 'rgba(255,255,255,0.7)',
          angle: 0,
          fontFamily: 'D-DIN',
          fontWeight: 'normal',
          direction: 'horizontal',
          maxLineWidth: 174
        },
        autoHide: true,
        autoHideMethod: 'greedy',
        flush: true,
        lastVisible: true,
        autoHideSeparation: 4
      },
      hover: true,
      background: {
        visible: true,
        state: {
          hover: {
            fillOpacity: 0.08,
            fill: '#141414'
          },
          hover_reverse: {
            fillOpacity: 0.08,
            fill: '#141414'
          }
        }
      },
      paddingInner: 0.13,
      paddingOuter: 0.13,
      ticks: true,
      maxWidth: 180
    },
    {
      type: 'linear',
      tick: {
        size: 4,
        visible: true
      },
      niceType: 'rough',
      zIndex: 200,
      grid: {
        visible: true
      },
      orient: 'bottom',
      visible: true,
      domainLine: {
        visible: false,
        style: {
          lineWidth: 1,
          stroke: '#d5d7e2'
        }
      },
      title: {
        visible: false,
        text: '',
        space: 8,
        style: {
          fontSize: 12,
          fill: 'rgba(255,255,255,0.5)',
          fontFamily: 'D-DIN',
          fontWeight: 'normal'
        }
      },
      autoIndent: false,
      sampling: false,
      label: {
        visible: true,
        space: 4,
        flush: true,
        padding: 0,
        style: {
          fontSize: 12,
          maxLineWidth: 174,
          fill: 'rgba(255,255,255,0.7)',
          angle: 0,
          fontFamily: 'D-DIN',
          fontWeight: 'normal',
          dy: 0,
          direction: 'horizontal'
        },
        autoHide: true,
        autoHideMethod: 'greedy',
        autoHideSeparation: 4,
        rotateAngle: [null],
        labelOverlap: 'custom',
        tighten: false
      },
      background: {
        visible: true,
        state: {
          hover: {
            fillOpacity: 0.08,
            fill: '#141414'
          },
          hover_reverse: {
            fillOpacity: 0.08,
            fill: '#141414'
          }
        }
      },
      innerOffset: {
        top: 0
      },
      zero: true,
      nice: true,
      paddingInner: 0.13,
      paddingOuter: 0.13,
      ticks: true
    },
    {
      type: 'linear',
      tick: {
        size: 4,
        visible: true
      },
      niceType: 'rough',
      zIndex: 200,
      grid: {
        visible: false
      },
      orient: 'top',
      visible: true,
      domainLine: {
        visible: false
      },
      title: {
        visible: false
      },
      autoIndent: false,
      sampling: false,
      label: {
        visible: true,
        space: 4,
        flush: true,
        padding: 0,
        style: {
          visible: false,
          fontSize: 12,
          maxLineWidth: 174,
          fill: 'rgba(255,255,255,0.7)',
          angle: 0,
          fontFamily: 'D-DIN',
          fontWeight: 'normal',
          dy: 0,
          direction: 'horizontal'
        },
        autoHide: true,
        autoHideMethod: 'greedy',
        autoHideSeparation: 4,
        rotateAngle: [null],
        labelOverlap: 'custom',
        tighten: false
      },
      background: {
        visible: true,
        state: {
          hover: {
            fillOpacity: 0.08,
            fill: '#141414'
          },
          hover_reverse: {
            fillOpacity: 0.08,
            fill: '#141414'
          }
        }
      },
      innerOffset: {
        top: 0
      },
      zero: true,
      nice: true,
      paddingInner: 0.13,
      paddingOuter: 0.13,
      ticks: true
    }
  ],
  color: {
    field: '30001',
    type: 'ordinal',
    range: ['rgb(0,110,255)', 'rgb(0,229,229)'],
    specified: {},
    domain: ['2023', '2022']
  },
  colorGradient: {
    type: 'linear',
    x0: {
      field: '30001',
      type: 'ordinal',
      range: [0, 0]
    },
    y0: {
      field: '30001',
      type: 'ordinal',
      range: [1, 1]
    },
    x1: {
      field: '30001',
      type: 'ordinal',
      range: [1, 1]
    },
    y1: {
      field: '30001',
      type: 'ordinal',
      range: [1, 1]
    },
    stops: [
      {
        offset: 0,
        color: {
          field: '30001',
          type: 'ordinal',
          range: ['rgba(0,110,255,0.2)', 'rgba(0,229,229,0.2)']
        }
      },
      {
        offset: 1,
        color: {
          field: '30001',
          type: 'ordinal',
          range: ['rgb(0,110,255)', 'rgb(0,229,229)']
        }
      }
    ]
  },
  legends: [
    {
      type: 'discrete',
      visible: true,
      id: 'legend-discrete',
      orient: 'top',
      position: 'end',
      layoutType: 'normal',
      maxRow: 1,
      title: {
        textStyle: {
          fontSize: 12,
          fill: 'rgba(255,255,255,0.7)'
        }
      },
      layoutLevel: 70,
      item: {
        focus: false,
        focusIconStyle: {
          size: 14
        },
        maxWidth: 400,
        spaceRow: 0,
        spaceCol: 0,
        padding: {
          left: 10,
          right: 0,
          top: 0,
          bottom: 5
        },
        background: {
          visible: false,
          style: {
            fillOpacity: 0.001
          }
        },
        label: {
          style: {
            fontSize: 12,
            fill: 'rgba(255,255,255,0.7)',
            fontFamily: 'D-DIN',
            fontWeight: 'normal'
          },
          state: {
            unSelected: {
              fillOpacity: 0.2
            }
          }
        },
        shape: {
          style: {
            lineWidth: 0,
            symbolType: 'square',
            size: 12,
            fillOpacity: 1,
            width: 12,
            height: 7.416
          }
        }
      },
      pager: {
        layout: 'horizontal',
        padding: 0,
        textStyle: {
          fill: 'rgba(255,255,255,0.7)'
        },
        space: 0,
        handler: {
          preShape: 'triangleLeft',
          nextShape: 'triangleRight',
          style: {
            fill: 'rgba(255,255,255,0.7)',
            cursor: 'pointer',
            fillOpaity: 1
          },
          state: {
            disable: {
              fill: 'rgba(255,255,255,0.7)',
              cursor: 'not-allowed',
              fillOpacity: 0.45
            }
          }
        }
      },
      alignSelf: 'end',
      padding: {
        left: 10,
        right: 0,
        top: 0,
        bottom: 12
      }
    }
  ],
  label: {
    visible: false,
    offset: 3,
    overlap: {
      hideOnHit: true,
      avoidBaseMark: false,
      strategy: [
        {
          type: 'position',
          position: []
        }
      ],
      clampForce: true
    },
    style: {
      fontSize: 12,
      fontFamily: 'D-DIN',
      fontWeight: 'normal',
      zIndex: 400,
      lineHeight: '100%',
      boundsPadding: [1, 0, 0, 0],
      fill: 'rgba(255,255,255,1)',
      strokeOpacity: 0
    },
    position: 'outside',
    smartInvert: false,
    fontWeight: 'normal',
    animation: false,
    avoidBaseMark: true
  },
  tooltip: {
    visible: true,
    renderMode: 'canvas',
    mark: {
      visible: true
    },
    style: {
      panel: {
        padding: {
          top: 5,
          bottom: 10,
          left: 10,
          right: 10
        },
        backgroundColor: 'rgba(8, 28, 48, 0.95)',
        border: {
          color: '#CFCFCF',
          width: 0,
          radius: 2
        },
        shadow: {
          x: 0,
          y: 4,
          blur: 12,
          spread: 0,
          color: 'rgba(0, 0, 0, 0.2)'
        }
      },
      titleLabel: {
        fontSize: 14,
        fontColor: '#FFF',
        fontWeight: 'bold',
        fontFamily: 'D-DIN',
        align: 'left',
        lineHeight: 18
      },
      keyLabel: {
        fontSize: 12,
        fontColor: 'rgba(255,255,255,0.65)',
        fontWeight: 'normal',
        fontFamily: 'SourceHanSansCN-Normal',
        align: 'left',
        lineHeight: 18
      },
      valueLabel: {
        fontSize: 12,
        fontColor: '#FFF',
        fontWeight: 'normal',
        fontFamily: 'D-DIN',
        align: 'right',
        lineHeight: 18
      },
      shape: {
        size: 10,
        spacing: 10,
        shapeLineWidth: 0
      },
      spaceRow: 10
    },
    dimension: {
      visible: true
    }
  },
  hover: {
    enable: true
  },
  select: {
    enable: true
  },
  bar: {
    state: {
      hover: {
        cursor: 'pointer',
        fillOpacity: 0.8,
        stroke: '#58595B',
        lineWidth: 1,
        zIndex: 500
      },
      selected: {
        cursor: 'pointer',
        fillOpacity: 1,
        stroke: '#58595B',
        lineWidth: 1
      },
      selected_reverse: {
        fillOpacity: 0.3,
        lineWidth: 0.3
      }
    },
    style: {
      cornerRadius: 0,
      fill: {
        gradient: 'linear',
        stops: [
          {
            offset: 0
          },
          {
            offset: 1
          }
        ]
      },
      lineWidth: 2,
      stroke: {
        gradient: 'linear',
        stops: [
          {
            offset: 0
          },
          {
            offset: 1
          }
        ]
      }
    }
  },
  region: [
    {
      clip: true
    }
  ],
  background: 'rgba(0, 0, 0, 1)',
  markLine: [],
  animation: true,
  crosshair: {
    yField: {
      visible: true,
      line: {
        type: 'rect',
        style: {
          fillOpacity: 1,
          fill: 'rgba(80,156,255,0.1)'
        }
      }
    },
    gridZIndex: 100,
    xField: {
      line: {
        style: {
          fillOpacity: 1,
          fill: 'rgba(80,156,255,0.1)'
        }
      },
      visible: false
    }
  },
  morph: {
    enable: false
  },
  fillOpacity: {
    fillOpacity: 1
  },
  axesPadding: true,
  plotLayout: {
    clip: false
  },
  scales: [
    {
      id: 'gradientFillStop0_data',
      type: 'ordinal',
      range: [
        'rgba(0,110,255,0.2)',
        'rgba(0,229,229,0.2)',
        'rgba(46,85,234,0.2)',
        'rgba(184,231,254,0.2)',
        'rgba(0,214,137,0.2)',
        'rgba(183,249,245,0.2)',
        'rgba(251,204,113,0.2)',
        'rgba(244,110,80,0.2)'
      ],
      domain: ['2023', '2022']
    },
    {
      id: 'gradientFillStop1_data',
      type: 'ordinal',
      range: [
        'rgb(0,110,255)',
        'rgb(0,229,229)',
        'rgb(46,85,234)',
        'rgb(184,231,254)',
        'rgb(0,214,137)',
        'rgb(183,249,245)',
        'rgb(251,204,113)',
        'rgb(244,110,80)'
      ],
      domain: ['2023', '2022']
    },
    {
      id: 'gradientStrokeStop0_data',
      type: 'ordinal',
      range: [
        'rgba(51, 139, 255, 0.2)',
        'rgba(25, 255, 255, 0.2)',
        'rgba(92, 123, 239, 0.2)',
        'rgba(234, 248, 255, 0.2)',
        'rgba(10, 255, 167, 0.2)',
        'rgba(230, 253, 252, 0.2)',
        'rgba(252, 222, 163, 0.2)',
        'rgba(247, 150, 128, 0.2)'
      ],
      domain: ['2023', '2022']
    },
    {
      id: 'gradientStrokeStop1_data',
      type: 'ordinal',
      range: [
        'rgba(51, 139, 255, 1)',
        'rgba(25, 255, 255, 1)',
        'rgba(92, 123, 239, 1)',
        'rgba(234, 248, 255, 1)',
        'rgba(10, 255, 167, 1)',
        'rgba(230, 253, 252, 1)',
        'rgba(252, 222, 163, 1)',
        'rgba(247, 150, 128, 1)'
      ],
      domain: ['2023', '2022']
    },
    {
      id: 'gradientX1_data',
      type: 'ordinal',
      range: [1, 1, 1, 1, 1, 1, 1, 1],
      domain: ['2023', '2022']
    },
    {
      id: 'gradientY1_data',
      type: 'ordinal',
      range: [0, 0, 0, 0, 0, 0, 0, 0],
      domain: ['2023', '2022']
    }
  ],
  barWidth: '60%',
  barBackground: {
    fieldLevel: 1,
    visible: false,
    interactive: false,
    style: {
      cornerRadius: 0,
      fill: 'rgba(255,255,255,1)',
      fillOpacity: 0.1
    }
  },
  animationAppear: {
    bar: {
      type: 'growWidthIn',
      oneByOne: false,
      controlOptions: {
        immediatelyApply: true
      },
      easing: 'circInOut',
      duration: 1000
    }
  },
  animationNormal: {
    bar: [
      {
        type: 'growWidthIn',
        oneByOne: false,
        controlOptions: {
          immediatelyApply: false
        },
        startTime: 5000,
        easing: 'circInOut',
        duration: 1000,
        delayAfter: 6000,
        loop: true
      },
      {
        loop: true,
        startTime: 5000,
        delay: 1000,
        delayAfter: 5000,
        duration: 1000,
        easing: 'circInOut',
        customParameters: {
          isHorizontal: true,
          attribute: {
            fill: {
              gradient: 'linear',
              x0: 0,
              x1: 1,
              y1: 0,
              y0: 0,
              stops: [
                {
                  offset: 0,
                  color: 'rgba(255,255,255,0)'
                },
                {
                  offset: 1,
                  color: 'rgba(255,255,255,0.3)'
                }
              ]
            },
            blur: 0,
            shadowColor: 'rgba(0,0,0,0)'
          }
        }
      }
    ]
  },
  animationEnter: {
    bar: {
      easing: 'circInOut',
      duration: 1000
    }
  },
  animationUpdate: {
    bar: {
      easing: 'circInOut',
      duration: 1000
    }
  },
  animationExit: {},
  hash: 'e89bf72b22f7068b85e9ce618e06ce66',
  line: {
    style: {
      stroke: {
        gradient: 'linear',
        stops: [
          {
            offset: 0
          },
          {
            offset: 1
          }
        ]
      }
    }
  },
  width: 400,
  height: 225
};

const getAnimationSpec = spec => {
  if (!spec) {
    return {};
  }
  return {
    animation: spec.animation,
    animationAppear: spec.animationAppear,
    animationUpdate: spec.animationUpdate,
    animationEnter: spec.animationEnter,
    animationExit: spec.animationExit
  };
};

const chartUpdate = (chartInstance, preChartSpec, curSpec) => {
  const preAnimationSpec = getAnimationSpec(preChartSpec);
  const curAnimationSpec = getAnimationSpec(curSpec);
  const reAnimate = JSON.stringify(preAnimationSpec) !== JSON.stringify(curAnimationSpec);
  // VChart图表分批updateSpec, 流程待图表库底层优化
  // step1. update除data外的spec
  // step2. updateData

  const specWithoutDataChange = {
    ...curSpec,
    data: preChartSpec?.data ?? [
      {
        id: 'data',
        values: []
      }
    ]
  };
  const curData = curSpec.data;
  chartInstance.updateSpec(specWithoutDataChange, false, undefined); // remake, 无动画
  chartInstance.updateFullDataSync(curData, true);
};

const vchart = new VChart(spec, { dom: CONTAINER_ID });
vchart.renderSync();

vchart.updateSpec(spec);
vchart.reRunNormalAnimation();

setTimeout(() => {
  chartUpdate(vchart, spec, spec);
  vchart.reRunNormalAnimation()
}, 2000);


// Just for the convenience of console debugging, DO NOT COPY!
window['vchart'] = vchart;

Current Behavior

Image

Expected Behavior

两次数据更新前,不出现空白帧

Environment

- OS:
- Browser:
- Framework:

Any additional comments?

No response

skie1997 avatar Jul 28 '25 07:07 skie1997