VChart icon indicating copy to clipboard operation
VChart copied to clipboard

[Bug] 单值环形图更新后,循环动画异常

Open skie1997 opened this issue 4 months ago • 0 comments

Version

vscreen

Link to Minimal Reproduction

2.0.3

Steps to Reproduce

const spec = {
  "theme": {},
  "type": "circularProgress",
  "valueField": "measureValue",
  "radiusField": "measureValue",
  "padding": {
    "left": 6,
    "right": 6,
    "top": 6,
    "bottom": 6
  },
  "outerRadius": 0.92,
  "innerRadius": 0.8200000000000001,
  "cornerRadius": 20,
  "track": {
    "style": {
      "fill": "#434343",
      "fillOpacity": 0.29
    }
  },
  "categoryField": "name",
  "roundCap": false,
  "data": [
    {
      "id": "data",
      "values": [
        {
          "current": 0.8138999999999998,
          "name": "Sales Proportion",
          "emptyName": "",
          "percent": "-",
          "measureValue": 0.8138999999999998,
          "detail": "undefined"
        }
      ]
    }
  ],
  "progress": {
    "style": {
      "cornerRadius": 20,
      "fill": "#006EFF",
      "fillOpacity": 1
    },
    "state": {
      "selected": {}
    }
  },
  "axes": [
    {
      "type": "linear",
      "orient": "angle",
      "min": 0,
      "max": 1,
      "grid": {
        "visible": false
      },
      "label": {
        "visible": false,
        "autoHideSeparation": 4
      },
      "ticks": false
    }
  ],
  "indicator": {
    "pickable": false,
    "visible": true,
    "fixed": true,
    "title": {
      "space": 5,
      "visible": false,
      "style": {
        "visible": false,
        "text": "-",
        "fontSize": 12,
        "fill": "#FFF",
        "fontWeight": "normal",
        "fontFamily": "D-DIN",
        "pickable": false
      },
      "autoLimit": false
    },
    "content": [
      {
        "space": 5,
        "visible": true,
        "style": {
          "visible": true,
          "text": "81.39%",
          "fontSize": 28,
          "fill": "#FFF",
          "fontWeight": "bold",
          "fontFamily": "D-DIN-Bold",
          "pickable": false
        },
        "autoLimit": false
      },
      {
        "space": 5,
        "visible": true,
        "style": {
          "visible": true,
          "text": "Sales Proportion",
          "fontSize": 14,
          "fill": "rgba(255,255,255,0.7)",
          "fontWeight": "normal",
          "fontFamily": "D-DIN",
          "pickable": false
        },
        "autoLimit": false
      }
    ]
  },
  "region": [
    {
      "clip": true
    }
  ],
  "background": "rgba(0, 0, 0, 1)",
  "animation": true,
  "tooltip": {
    "visible": true,
    "renderMode": "canvas",
    "mark": {
      "visible": false
    },
    "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": false
    }
  },
  "crosshair": {
    "xField": {
      "line": {
        "style": {
          "fillOpacity": 1,
          "fill": "rgba(80,156,255,0.1)"
        }
      },
      "visible": false
    },
    "yField": {
      "line": {
        "style": {
          "fillOpacity": 1,
          "fill": "rgba(80,156,255,0.1)"
        }
      },
      "visible": false
    }
  },
  "morph": {
    "enable": false
  },
  "plotLayout": {
    "clip": false
  },
  "select": {
    "enable": true
  },
  "animationAppear": {
    "progress": {
      "loop": false,
      "duration": 1000,
      "easing": "linear"
    }
  },
  "animationNormal": {
    "progress": {
      "loop": true,
      "startTime": 1200,
      "delayAfter": 1200,
      "duration": 1000,
      "easing": "linear",
      "controlOptions": {
        "immediatelyApply": false
      },
      "channel": {
        startAngle: {
          from: (...args) => {
            const { startAngle, endAngle } = args[1].attribute
            const minAngle = Math.min(startAngle, endAngle)
            const maxAngle = Math.max(startAngle, endAngle)
            return minAngle
          },
          to: (...args) => {
            const { startAngle, endAngle } = args[1].attribute
            const minAngle = Math.min(startAngle, endAngle)
            const maxAngle = Math.max(startAngle, endAngle)
            return minAngle
          },
        },
        endAngle: {
          from: (...args) => {
            const { startAngle, endAngle } = args[1].attribute
            const minAngle = Math.min(startAngle, endAngle)
            const maxAngle = Math.max(startAngle, endAngle)
            return minAngle
          },
          to: (...args) => {
            const { startAngle, endAngle } = args[1].attribute
            const minAngle = Math.min(startAngle, endAngle)
            const maxAngle = Math.max(startAngle, endAngle)
            return maxAngle
          },
        },
      }
    }
  },
  "animationEnter": {
    "duration": 1000,
    "easing": "circInOut"
  },
  "animationUpdate": {
    "duration": 1000,
    "easing": "circInOut"
  },
  "animationExit": {},
  "hash": "96bb1fa575d942c43c517b6d7d66cdb2",
  "width": 400,
  "height": 225
}

const spec2 = {
  "theme": {},
  "type": "circularProgress",
  "valueField": "measureValue",
  "radiusField": "measureValue",
  "padding": {
    "left": 6,
    "right": 6,
    "top": 6,
    "bottom": 6
  },
  "outerRadius": 0.92,
  "innerRadius": 0.8200000000000001,
  "cornerRadius": 20,
  "track": {
    "style": {
      "fill": "#434343",
      "fillOpacity": 0.29
    }
  },
  "categoryField": "name",
  "roundCap": false,
  "data": [
    {
      "id": "data",
      "values": [
        {
          "current": 0.1,
          "name": "Sales Proportion",
          "emptyName": "",
          "percent": "-",
          "measureValue": 0.1,
          "detail": "undefined"
        }
      ]
    }
  ],
  "progress": {
    "style": {
      "cornerRadius": 20,
      "fill": "#006EFF",
      "fillOpacity": 1
    },
    "state": {
      "selected": {}
    }
  },
  "axes": [
    {
      "type": "linear",
      "orient": "angle",
      "min": 0,
      "max": 1,
      "grid": {
        "visible": false
      },
      "label": {
        "visible": false,
        "autoHideSeparation": 4
      },
      "ticks": false
    }
  ],
  "indicator": {
    "pickable": false,
    "visible": true,
    "fixed": true,
    "title": {
      "space": 5,
      "visible": false,
      "style": {
        "visible": false,
        "text": "-",
        "fontSize": 12,
        "fill": "#FFF",
        "fontWeight": "normal",
        "fontFamily": "D-DIN",
        "pickable": false
      },
      "autoLimit": false
    },
    "content": [
      {
        "space": 5,
        "visible": true,
        "style": {
          "visible": true,
          "text": "10.00%",
          "fontSize": 28,
          "fill": "#FFF",
          "fontWeight": "bold",
          "fontFamily": "D-DIN-Bold",
          "pickable": false
        },
        "autoLimit": false
      },
      {
        "space": 5,
        "visible": true,
        "style": {
          "visible": true,
          "text": "Sales Proportion",
          "fontSize": 14,
          "fill": "rgba(255,255,255,0.7)",
          "fontWeight": "normal",
          "fontFamily": "D-DIN",
          "pickable": false
        },
        "autoLimit": false
      }
    ]
  },
  "region": [
    {
      "clip": true
    }
  ],
  "background": "rgba(0, 0, 0, 1)",
  "animation": true,
  "tooltip": {
    "visible": true,
    "renderMode": "canvas",
    "mark": {
      "visible": false
    },
    "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": false
    }
  },
  "crosshair": {
    "xField": {
      "line": {
        "style": {
          "fillOpacity": 1,
          "fill": "rgba(80,156,255,0.1)"
        }
      },
      "visible": false
    },
    "yField": {
      "line": {
        "style": {
          "fillOpacity": 1,
          "fill": "rgba(80,156,255,0.1)"
        }
      },
      "visible": false
    }
  },
  "morph": {
    "enable": false
  },
  "plotLayout": {
    "clip": false
  },
  "select": {
    "enable": true
  },
  "animationAppear": {
    "progress": {
      "loop": false,
      "duration": 1000,
      "easing": "linear"
    }
  },
  "animationNormal": {
    "progress": {
      "loop": true,
      "startTime": 1200,
      "delayAfter": 1200,
      "duration": 1000,
      "easing": "linear",
      "controlOptions": {
        "immediatelyApply": false
      },
      "channel": {
        "channel": {
          startAngle: {
            from: (...args) => {
              const { startAngle, endAngle } = args[1].attribute
              const minAngle = Math.min(startAngle, endAngle)
              const maxAngle = Math.max(startAngle, endAngle)
              return minAngle
            },
            to: (...args) => {
              const { startAngle, endAngle } = args[1].attribute
              const minAngle = Math.min(startAngle, endAngle)
              const maxAngle = Math.max(startAngle, endAngle)
              return minAngle
            },
          },
          endAngle: {
            from: (...args) => {
              const { startAngle, endAngle } = args[1].attribute
              const minAngle = Math.min(startAngle, endAngle)
              const maxAngle = Math.max(startAngle, endAngle)
              return minAngle
            },
            to: (...args) => {
              const { startAngle, endAngle } = args[1].attribute
              const minAngle = Math.min(startAngle, endAngle)
              const maxAngle = Math.max(startAngle, endAngle)
              return maxAngle
            },
          },
        }
      }
    }
  },
  "animationEnter": {
    "duration": 1000,
    "easing": "circInOut"
  },
  "animationUpdate": {
    "duration": 1000,
    "easing": "circInOut"
  },
  "animationExit": {},
  "hash": "96bb1fa575d942c43c517b6d7d66cdb2",
  "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);
  chartInstance.updateFullDataSync(curData, true);
  chartInstance.reRunNormalAnimation()
};

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

setTimeout(() => {
  chartUpdate(vchart, spec, spec2)
}, 1000)

// 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 Aug 27 '25 08:08 skie1997