VisActor / VChart

VChart, more than just a cross-platform charting library, but also an expressive data storyteller.
https://www.visactor.io/vchart
MIT License
895 stars 58 forks source link

[Bug] there has update error when use `customMark` to customize components #2928

Closed kkxxkk2019 closed 1 month ago

kkxxkk2019 commented 1 month ago

Version

lastest

Link to Minimal Reproduction

none

Steps to Reproduce


import { AbstractComponent } from '@visactor/vrender-components';
import type { Dict } from '@visactor/vutils';
import { isEmpty, last } from '@visactor/vutils';
import { Factory } from '@visactor/vgrammar-core';
import type { IGraphic } from '@visactor/vrender-core';
import { createText } from '@visactor/vrender-core';

export class SeriesLabelComponent extends AbstractComponent<Required<any>> {
  name = 'seriesLabel';
  protected render() {
    this.removeAllChild();
    const { data, layout, label, line = {} } = this.attribute as any;
    if (isEmpty(data)) {
      return;
    }

    const labelStyleMap = label?.styleMap ?? {};
    const adjustedPoints: Dict<any> = {};
    const filteredData = (data as any[]).filter(datum => labelStyleMap[datum.id]?.visible !== false);
    const lineHeight = (label?.style?.fontSize ?? 12) * 1.5; // 进行扰动,将重叠的文本抖开
    const createAndAddTextGraphic = (datum: any, textPoint: any, labelStyleMap: any) => {
      const { label: text, color, textAlign, textBaseline, id } = datum;
      const formatMethod = labelStyleMap[id]?.formatMethod ?? this.attribute.label?.formatMethod;
      const textGraphic = createText({
        text: formatMethod
          ? formatMethod(text, datum.datum, {
              series: datum.series
            })
          : text,
        ...this.attribute.label?.style,
        ...textPoint,
        textAlign,
        textBaseline,
        fill: this.attribute.label?.style?.fill ?? color,
        ...labelStyleMap[id]?.style
      });
      textGraphic.name = 'series-label-text';
      textGraphic.id = id;
      this.add(textGraphic);

      return textGraphic;
    };

    ['start', 'end'].forEach(position => {
      const posData = filteredData.filter(datum => datum.position === position);

      posData.forEach((datum, index) => {
        const textPoint = {
          x: datum.point.x + (label?.space ?? 8) * (datum.textAlign === 'start' ? 1 : -1),
          y: datum.point.y
        };
        createAndAddTextGraphic(datum, textPoint, labelStyleMap);
        adjustedPoints[datum.id] = textPoint;
      });
    });
  }
}

Factory.registerGraphicComponent(
  'seriesLabel',
  (attrs: Required<any>) => new SeriesLabelComponent(attrs) as unknown as IGraphic
);

 const spec1 = {
    direction: 'vertical',
    type: 'common',
    color: ['#00295C', '#2568BD', '#9F9F9F', '#C5C5C5', '#00B0F0', '#4BCFFF', '#C2C2C2', '#D7D7D7'],
    series: [
      {
        type: 'bar',
        stack: true,
        direction: 'vertical',
        bar: {
          style: {
            stroke: '',
            lineWidth: 1
          },
          state: {
            hover: {
              stroke: '#000',
              lineWidth: 1
            }
          }
        },
        barBackground: {
          style: {
            stroke: '',
            lineWidth: 1
          }
        },
        label: {
          visible: true,
          position: 'inside',
          style: {
            lineHeight: '100%',
            fontSize: 16,
            fontWeight: 'bold'
          },
          overlap: {
            strategy: []
          },
          smartInvert: true,
          interactive: true
        },
        totalLabel: {
          visible: true,
          position: 'top',
          overlap: false,
          clampForce: false,
          formatConfig: {
            fixed: 0,
            content: 'value'
          },
          style: {
            lineHeight: '100%',
            lineWidth: 1,
            fill: '#1F2329',
            stroke: '#ffffff',
            fontSize: 16,
            fontWeight: 'bold'
          },
          interactive: true
        },
        seriesLabel: {
          visible: true,
          position: 'end',
          label: {
            style: {
              lineHeight: '100%',
              lineWidth: 1,
              stroke: '#ffffff',
              fontSize: 16,
              fontWeight: 'bold'
            },
            space: 10
          }
        },
        xField: '_editor_dimension_field',
        yField: '_editor_value_field',
        dataId: '0',
        id: 'series-0',
        EDITOR_SERIES_DATA_KEY: 'a',
        seriesField: '_editor_type_field'
      },
      {
        type: 'bar',
        stack: true,
        direction: 'vertical',
        bar: {
          style: {
            stroke: '',
            lineWidth: 1
          },
          state: {
            hover: {
              stroke: '#000',
              lineWidth: 1
            }
          }
        },
        barBackground: {
          style: {
            stroke: '',
            lineWidth: 1
          }
        },
        label: {
          visible: true,
          position: 'inside',
          style: {
            lineHeight: '100%',
            fontSize: 16,
            fontWeight: 'bold'
          },
          overlap: {
            strategy: []
          },
          smartInvert: true,
          interactive: true
        },
        totalLabel: {
          visible: true,
          position: 'top',
          overlap: false,
          clampForce: false,
          formatConfig: {
            fixed: 0,
            content: 'value'
          },
          style: {
            lineHeight: '100%',
            lineWidth: 1,
            fill: '#1F2329',
            stroke: '#ffffff',
            fontSize: 16,
            fontWeight: 'bold'
          },
          interactive: true
        },
        seriesLabel: {
          visible: true,
          position: 'end',
          label: {
            style: {
              lineHeight: '100%',
              lineWidth: 1,
              stroke: '#ffffff',
              fontSize: 16,
              fontWeight: 'bold'
            },
            space: 10
          }
        },
        xField: '_editor_dimension_field',
        yField: '_editor_value_field',
        dataId: '1',
        id: 'series-1',
        EDITOR_SERIES_DATA_KEY: 'b',
        seriesField: '_editor_type_field'
      }
    ],
    legends: {
      id: 'legend-discrete',
      visible: false,
      autoPage: false,
      position: 'start',
      interactive: false,
      item: {
        label: {
          style: {
            fill: '#1F2329',
            fontSize: 16
          }
        }
      }
    },
    region: [
      {
        id: 'region-0'
      }
    ],
    tooltip: {
      visible: true
    },
    axes: [
      {
        orient: 'left',
        id: 'axis-left',
        type: 'linear',
        label: {
          autoLimit: false,
          style: {
            fill: '#1F2329',
            fontSize: 16
          },
          formatMethod: null
        },
        domainLine: {
          visible: true,
          style: {
            stroke: '#000000'
          }
        },
        tick: {
          visible: true,
          style: {
            stroke: '#000000'
          }
        },
        grid: {
          visible: false,
          style: {
            stroke: '#bbbfc4',
            pickStrokeBuffer: 2
          }
        },
        autoIndent: false,
        maxWidth: null,
        maxHeight: null
      },
      {
        orient: 'bottom',
        id: 'axis-bottom',
        type: 'band',
        label: {
          autoLimit: false,
          style: {
            fill: '#1F2329',
            fontSize: 16
          }
        },
        domainLine: {
          visible: true,
          style: {
            stroke: '#000000'
          },
          onZero: true
        },
        tick: {
          visible: true,
          style: {
            stroke: '#000000'
          }
        },
        grid: {
          visible: false,
          style: {
            stroke: '#bbbfc4',
            pickStrokeBuffer: 2
          }
        },
        autoIndent: false,
        maxWidth: null,
        maxHeight: null,
        trimPadding: false,
        paddingInner: [0.2, 0],
        paddingOuter: [0.2, 0]
      }
    ],
    data: [
      {
        id: '0',
        sourceKey: 'a',
        values: [
          {
            _editor_dimension_field: 'x1',
            _editor_value_field: 20,
            _editor_type_field: 'a'
          },
          {
            _editor_dimension_field: 'x2',
            _editor_value_field: 23,
            _editor_type_field: 'a'
          },
          {
            _editor_dimension_field: 'x3',
            _editor_value_field: 26,
            _editor_type_field: 'a'
          }
        ]
      },
      {
        id: '1',
        sourceKey: 'b',
        values: [
          {
            _editor_dimension_field: 'x1',
            _editor_value_field: 20,
            _editor_type_field: 'b'
          },
          {
            _editor_dimension_field: 'x2',
            _editor_value_field: 24,
            _editor_type_field: 'b'
          },
          {
            _editor_dimension_field: 'x3',
            _editor_value_field: 29,
            _editor_type_field: 'b'
          }
        ]
      }
    ],
    markLine: [
      {
        id: '3062c9c7-cfff-4b16-910d-e338a4477179',
        interactive: true,
        name: 'total-diff-line',
        type: 'type-step',
        coordinates: [
          {
            _editor_dimension_field: 'x1',
            _editor_value_field: 40,
            _editor_type_field: 'b',
            refRelativeSeriesId: 'series-1'
          },
          {
            _editor_dimension_field: 'x2',
            _editor_value_field: 47,
            _editor_type_field: 'b',
            refRelativeSeriesId: 'series-1'
          }
        ],
        connectDirection: 'top',
        expandDistance: '10.273972602739725%',
        line: {
          style: {
            stroke: '#000',
            lineWidth: 1,
            pickStrokeBuffer: 10,
            lineDash: [0],
            cornerRadius: 4
          }
        },
        label: {
          position: 'middle',
          text: '18%',
          labelBackground: {
            style: {
              fill: '#fff',
              fillOpacity: 1,
              stroke: '#000',
              lineWidth: 1,
              cornerRadius: 4
            }
          },
          style: {
            fill: '#1F2329',
            fontSize: 16,
            fontWeight: 'bold'
          },
          pickable: true,
          childrenPickable: false
        },
        endSymbol: {
          size: 12,
          refX: -4,
          symbolType:
            'M -0.0625 -0.3167 C -0.0625 -0.2821 -0.0346 -0.2542 0 -0.2542 C 0.0346 -0.2542 0.0625 -0.2821 0.0625 -0.3167 C 0.0625 -0.3167 -0.0625 -0.3167 -0.0625 -0.3167 Z M 0.0442 -0.4025 C 0.0196 -0.4271 -0.0196 -0.4271 -0.0442 -0.4025 C -0.0442 -0.4025 -0.4421 -0.0046 -0.4421 -0.0046 C -0.4662 0.0196 -0.4662 0.0592 -0.4421 0.0838 C -0.4175 0.1079 -0.3779 0.1079 -0.3538 0.0838 C -0.3538 0.0838 0 -0.27 0 -0.27 C 0 -0.27 0.3538 0.0838 0.3538 0.0838 C 0.3779 0.1079 0.4175 0.1079 0.4421 0.0838 C 0.4662 0.0592 0.4662 0.0196 0.4421 -0.0046 C 0.4421 -0.0046 0.0442 -0.4025 0.0442 -0.4025 Z M 0.0625 -0.3167 C 0.0625 -0.3167 0.0625 -0.3583 0.0625 -0.3583 C 0.0625 -0.3583 -0.0625 -0.3583 -0.0625 -0.3583 C -0.0625 -0.3583 -0.0625 -0.3167 -0.0625 -0.3167 C -0.0625 -0.3167 0.0625 -0.3167 0.0625 -0.3167 Z',
          style: {
            fill: '#000',
            lineWidth: 0,
            stroke: null
          }
        },
        _originValue_: [40, 47],
        zIndex: 510,
        coordinatesOffset: [
          {
            x: 0,
            y: -26.000000000000014
          },
          {
            x: 0,
            y: -26.000000000000007
          }
        ]
      },
      {
        id: 'be65e8ee-26c8-4c99-aa0f-33171ba5458e',
        interactive: true,
        name: 'growth-line',
        coordinates: [
          {
            _editor_dimension_field: 'x2',
            _editor_value_field: 47,
            _editor_type_field: 'b',
            refRelativeSeriesId: 'series-1'
          },
          {
            _editor_dimension_field: 'x3',
            _editor_value_field: 55,
            _editor_type_field: 'b',
            refRelativeSeriesId: 'series-1'
          }
        ],
        line: {
          style: {
            stroke: '#000',
            lineWidth: 1,
            pickStrokeBuffer: 10,
            lineDash: [0]
          }
        },
        label: {
          position: 'middle',
          text: '17%',
          labelBackground: {
            style: {
              fill: '#fff',
              fillOpacity: 1,
              stroke: '#000',
              lineWidth: 1,
              cornerRadius: 4
            },
            padding: {
              top: 2,
              bottom: 2,
              right: 4,
              left: 4
            }
          },
          style: {
            fill: '#1F2329',
            fontSize: 16,
            fontWeight: 'bold',
            fontStyle: 'normal'
          },
          pickable: true,
          childrenPickable: false
        },
        endSymbol: {
          size: 12,
          refX: -4,
          symbolType:
            'M -0.0625 -0.3167 C -0.0625 -0.2821 -0.0346 -0.2542 0 -0.2542 C 0.0346 -0.2542 0.0625 -0.2821 0.0625 -0.3167 C 0.0625 -0.3167 -0.0625 -0.3167 -0.0625 -0.3167 Z M 0.0442 -0.4025 C 0.0196 -0.4271 -0.0196 -0.4271 -0.0442 -0.4025 C -0.0442 -0.4025 -0.4421 -0.0046 -0.4421 -0.0046 C -0.4662 0.0196 -0.4662 0.0592 -0.4421 0.0838 C -0.4175 0.1079 -0.3779 0.1079 -0.3538 0.0838 C -0.3538 0.0838 0 -0.27 0 -0.27 C 0 -0.27 0.3538 0.0838 0.3538 0.0838 C 0.3779 0.1079 0.4175 0.1079 0.4421 0.0838 C 0.4662 0.0592 0.4662 0.0196 0.4421 -0.0046 C 0.4421 -0.0046 0.0442 -0.4025 0.0442 -0.4025 Z M 0.0625 -0.3167 C 0.0625 -0.3167 0.0625 -0.3583 0.0625 -0.3583 C 0.0625 -0.3583 -0.0625 -0.3583 -0.0625 -0.3583 C -0.0625 -0.3583 -0.0625 -0.3167 -0.0625 -0.3167 C -0.0625 -0.3167 0.0625 -0.3167 0.0625 -0.3167 Z',
          style: {
            fill: '#000',
            lineWidth: 0,
            stroke: null
          },
          visible: true
        },
        coordinatesOffset: [
          {
            x: 0,
            y: '-30%'
          },
          {
            x: 0,
            y: '-30%'
          }
        ],
        _originValue_: [47, 55],
        zIndex: 510,
        startSymbol: {
          visible: false,
          symbolType: 'triangle',
          size: 10,
          style: {
            fill: '#606773',
            stroke: null,
            lineWidth: 0
          }
        },
        expression: null
      }
    ],
    markArea: [],
    labelLayout: 'region',
    customMark: [
      {
        type: 'component',
        componentType: 'seriesLabel',
        interactive: false,
        style: {
          // id: '8124d358-3d47-4187-b301-a81853f09b56',
          position: 'end',
          label: {
            space: 10,
            style: {
              lineHeight: '100%',
              lineWidth: 1,
              stroke: '#ffffff',
              fontSize: 16,
              fontWeight: 'bold'
            }
          },
          data: (datum: any, ctx: any) => {
            const vchart = ctx.vchart;
            const chart = vchart.getChart();
            const series = chart.getAllSeries()[0] as ICartesianSeries;
            const isTransposed = series.direction === 'horizontal';
            const bandAxisHelper = (isTransposed ? series.getYAxisHelper() : series.getXAxisHelper()) as any;
            const bandAxisScale = bandAxisHelper.getScale?.(0) as any;
            const isBandAxisInverse = bandAxisHelper.isInverse();
            const dimensionField = series.getDimensionField()[0];
            const seriesField = series.getSeriesField() as string;
            const seriesTypes = chart
              .getAllSeries()
              .map((s: ICartesianSeries) => s.type)
              .filter((value: string, index: number, self: string[]) => {
                return self.indexOf(value) === index;
              });
            const labelData: any[] = [];

            // 系列标签目前不支持不同系列的组合图
            const pos = 'end';
            const targetValue = last(bandAxisScale.domain());
            const region = series.getRegion();
            const { x: regionStartX, y: regionStartY } = region.getLayoutStartPoint();
            let i = 0;
            chart.getAllSeries().forEach((s: ICartesianSeries) => {
              const mark = s.getMarkInName(seriesTypes[0] === 'waterfall' ? 'bar' : seriesTypes[0]);
              const vgrammarElements = mark.getProduct().elements;
              vgrammarElements.forEach((element: any) => {
                const data = element.data.find((datum: any) => datum[dimensionField] === targetValue);
                if (data) {
                  const graphItem = element.getGraphicItem();
                  const graphBounds = graphItem.AABBBounds;

                  const point = {
                    x: isTransposed
                      ? (graphBounds.x1 + graphBounds.x2) / 2
                      : isBandAxisInverse
                      ? graphBounds.x1
                      : graphBounds.x2,
                    y: isTransposed
                      ? isBandAxisInverse
                        ? graphBounds.y2
                        : graphBounds.y1
                      : (graphBounds.y1 + graphBounds.y2) / 2
                  };
                  const color = graphItem.attribute.fill;

                  const textAlign = isTransposed ? 'center' : isBandAxisInverse ? 'end' : 'start';
                  const textBaseline = isTransposed ? (isBandAxisInverse ? 'top' : 'bottom') : 'middle';

                  const labelValue = data[seriesField] ?? data[dimensionField];

                  labelData.push({
                    point: {
                      x: point.x + regionStartX,
                      y: point.y + regionStartY
                    },
                    label: labelValue,
                    color,
                    textAlign,
                    textBaseline,
                    series: s,
                    datum: data,
                    id: `${pos}-${i}`,
                    position: pos
                  });
                  i++;
                }
              });
            });

            return labelData;
          }
        }
      }
    ],
    width: 640,
    height: 360,
    background: 'transparent'
  };

  const vchart = new VChart(spec1, { dom: CONTAINER_ID, animation: false });
  vchart.renderSync();
  vchart.updateSpecSync(spec1, false, false);

Current Behavior

image

Expected Behavior

fix it

Environment

- OS:
- Browser:
- Framework:

Any additional comments?

No response

xile611 commented 1 month ago

label的 updateSpec 导致更新类型检测为 remake: true remake的时候,rect 图元进入到reuse逻辑,但是图形被删掉了 建议实现的时候,增加数据和图形的判断

if (data && element.getGraphicItem()) {
}