export function shapeViewModel()

in packages/charts/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts [295:519]


export function shapeViewModel(
  textMeasure: TextMeasure,
  spec: PartitionSpec,
  style: PartitionStyle,
  chartDimensions: Size,
  rawTextGetter: RawTextGetter,
  valueGetter: ValueGetterFunction,
  tree: HierarchyOfArrays,
  backgroundStyle: BackgroundStyle,
  panelModel: PartitionSmallMultiplesModel,
): ShapeViewModel {
  const {
    layout,
    layers,
    topGroove,
    valueFormatter: specifiedValueFormatter,
    percentFormatter: specifiedPercentFormatter,
    fillOutside,
    clockwiseSectors,
    maxRowCount,
    specialFirstInnermostSector,
    animation,
  } = spec;
  const { emptySizeRatio, outerSizeRatio, linkLabel, minFontSize, sectorLineWidth, sectorLineStroke, fillLabel } =
    style;
  const { width, height } = chartDimensions;
  const { marginLeftPx, marginTopPx, panel } = panelModel;

  const treemapLayout = isTreemap(layout);
  const mosaicLayout = isMosaic(layout);
  const sunburstLayout = isSunburst(layout);
  const icicleLayout = isIcicle(layout);
  const flameLayout = isFlame(layout);
  const simpleLinear = isSimpleLinear(layout, fillLabel, layers);
  const waffleLayout = isWaffle(layout);

  const diskCenter = isSunburst(layout)
    ? {
        x: marginLeftPx + panel.innerWidth / 2,
        y: marginTopPx + panel.innerHeight / 2,
      }
    : {
        x: marginLeftPx,
        y: marginTopPx,
      };

  // use the smaller of the two sizes, as a circle fits into a square
  const circleMaximumSize = Math.min(
    panel.innerWidth,
    panel.innerHeight - (panel.title.length > 0 ? panel.fontSize * 2 : 0),
  );
  const outerRadius: Radius = Math.min(outerSizeRatio * circleMaximumSize, circleMaximumSize - sectorLineWidth) / 2;
  // don't render anything if the total, the width or height is not positive
  if (!(width > 0) || !(height > 0) || tree.length === 0 || outerRadius <= 0) {
    return nullShapeViewModel(layout, style, diskCenter);
  }

  const longestPath = (entry?: ArrayEntry): number => {
    const [, node] = entry ?? [];
    if (!node) return NaN; // should never happen
    const { children, path } = node;
    return children.length > 0 ? children.reduce((p, n) => Math.max(p, longestPath(n)), 0) : path.length;
  };

  const maxDepth = longestPath(tree[0]) - 2; // don't include the root node
  const childNodes = rawChildNodes(
    layout,
    tree,
    topGroove,
    panel.innerWidth,
    panel.innerHeight,
    clockwiseSectors,
    specialFirstInnermostSector,
    maxDepth,
  );

  const shownChildNodes = childNodes.filter((n: Part) => {
    const layerIndex = entryValue(n.node).depth - 1;
    const layer = layers[layerIndex];
    return !layer || !layer.showAccessor || layer.showAccessor(entryKey(n.node));
  });

  const innerRadius: Radius = outerRadius - (1 - emptySizeRatio) * outerRadius;
  const treeHeight = shownChildNodes.reduce((p: number, n: Part) => Math.max(p, entryValue(n.node).depth), 0); // 1: pie, 2: two-ring donut etc.
  const ringThickness = (outerRadius - innerRadius) / treeHeight;
  const partToShapeFn = partToShapeTreeNode(!sunburstLayout, innerRadius, ringThickness);
  const quadViewModel = makeQuadViewModel(
    shownChildNodes.slice(1).map(partToShapeFn),
    layers,
    sectorLineWidth,
    sectorLineStroke,
    panelModel.smAccessorValue,
    panelModel.index,
    panelModel.innerIndex,
    fillLabel,
    backgroundStyle,
  );

  // fill text
  const roomCondition = (n: ShapeTreeNode) => {
    const diff = n.x1 - n.x0;
    return sunburstLayout
      ? (diff < 0 ? TAU + diff : diff) * ringSectorMiddleRadius(n) > Math.max(minFontSize, linkLabel.maximumSection)
      : n.x1 - n.x0 > minFontSize && n.y1px - n.y0px > minFontSize;
  };

  const nodesWithRoom = quadViewModel.filter(roomCondition);
  const outsideFillNodes = fillOutside && sunburstLayout ? nodesWithRoom : [];

  const textFillOrigins = nodesWithRoom.map(sunburstLayout ? sectorFillOrigins(fillOutside) : rectangleFillOrigins);

  const valueFormatter = valueGetter === percentValueGetter ? specifiedPercentFormatter : specifiedValueFormatter;

  const getRowSets = sunburstLayout
    ? fillTextLayout(
        ringSectorConstruction(spec, style, innerRadius, ringThickness),
        getSectorRowGeometry,
        inSectorRotation(style.horizontalTextEnforcer, style.horizontalTextAngleThreshold),
      )
    : simpleLinear || waffleLayout
      ? () => [] // no multirow layout needed for simpleLinear partitions; no text at all for waffles
      : fillTextLayout(
          rectangleConstruction(treeHeight, treemapLayout || mosaicLayout ? topGroove : null),
          getRectangleRowGeometry,
          () => 0,
        );

  const rowSets: RowSet[] = getRowSets(
    textMeasure,
    rawTextGetter,
    valueGetter,
    valueFormatter,
    nodesWithRoom,
    style,
    layers,
    textFillOrigins,
    maxRowCount,
    !sunburstLayout,
    !(treemapLayout || mosaicLayout),
  );

  // whiskers (ie. just lines, no text) for fill text outside the outer radius
  const outsideLinksViewModel = makeOutsideLinksViewModel(outsideFillNodes, rowSets, linkLabel.radiusPadding);

  // linked text
  const currentY = [-height, -height, -height, -height];

  const nodesWithoutRoom =
    fillOutside || treemapLayout || mosaicLayout || icicleLayout || flameLayout || waffleLayout
      ? [] // outsideFillNodes and linkLabels are in inherent conflict due to very likely overlaps
      : quadViewModel.filter((n: ShapeTreeNode) => {
          const id = nodeId(n);
          const foundInFillText = rowSets.find((r: RowSet) => r.id === id);
          // successful text render if found, and has some row(s)
          return !(foundInFillText && foundInFillText.rows.length > 0);
        });
  const maxLinkedLabelTextLength = style.linkLabel.maxTextLength;
  const linkLabelViewModels = linkTextLayout(
    panel.innerWidth,
    panel.innerHeight,
    textMeasure,
    style,
    nodesWithoutRoom,
    currentY,
    outerRadius,
    rawTextGetter,
    valueGetter,
    valueFormatter,
    maxLinkedLabelTextLength,
    {
      x: width * panelModel.left + panel.innerWidth / 2,
      y: height * panelModel.top + panel.innerHeight / 2,
    },
    backgroundStyle,
  );

  const pickQuads: PickFunction = sunburstLayout
    ? (x, y) => {
        return quadViewModel.filter(({ x0, y0px, x1, y1px }) => {
          const angleX = (Math.atan2(y, x) + TAU / 4 + TAU) % TAU;
          const yPx = Math.hypot(x, y);
          return x0 <= angleX && angleX <= x1 && y0px <= yPx && yPx <= y1px;
        });
      }
    : (x, y, { currentFocusX0, currentFocusX1 }) => {
        return quadViewModel.filter(({ x0, y0px, x1, y1px }) => {
          const scale = width / (currentFocusX1 - currentFocusX0);
          const fx0 = Math.max((x0 - currentFocusX0) * scale, 0);
          const fx1 = Math.min((x1 - currentFocusX0) * scale, width);
          return fx0 <= x && x < fx1 && y0px < y && y <= y1px;
        });
      };

  // combined viewModel
  return {
    layout,
    smAccessorValue: panelModel.smAccessorValue,
    index: panelModel.index,
    innerIndex: panelModel.innerIndex,
    width: panelModel.width,
    height: panelModel.height,
    top: panelModel.top,
    left: panelModel.left,
    innerRowCount: panelModel.innerRowCount,
    innerColumnCount: panelModel.innerColumnCount,
    innerRowIndex: panelModel.innerRowIndex,
    innerColumnIndex: panelModel.innerColumnIndex,
    marginLeftPx: panelModel.marginLeftPx,
    marginTopPx: panelModel.marginTopPx,
    panel: {
      ...panelModel.panel,
    },
    style,
    layers,
    diskCenter,
    quadViewModel,
    rowSets,
    linkLabelViewModels,
    outsideLinksViewModel,
    pickQuads,
    outerRadius,
    chartDimensions,
    animation,
  };
}