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,
};
}