in src/app/views/canvas/editing_link.tsx [190:498]
private renderMarkPlaceholders() {
const manager = this.props.store.chartManager;
// Get the two glyphs
const props = this.props.link
.properties as Prototypes.Links.LinksProperties;
const lineMode: string = props.linkType;
let glyphs: {
glyph: Specification.Glyph;
glyphState: Specification.GlyphState;
coordinateSystem: Graphics.CoordinateSystem;
}[] = [];
switch (this.props.link.classID) {
case "links.through":
{
const plotSegmentClass = manager.getClassById(
props.linkThrough.plotSegment
) as Prototypes.PlotSegments.PlotSegmentClass;
const coordinateSystem = plotSegmentClass.getCoordinateSystem();
const facets = Prototypes.Links.facetRows(
manager.dataflow.getTable(plotSegmentClass.object.table),
plotSegmentClass.state.dataRowIndices,
props.linkThrough.facetExpressions.map((x) =>
manager.dataflow.cache.parse(x)
)
);
const glyph = getById(
manager.chart.glyphs,
plotSegmentClass.object.glyph
);
const rowToMarkState = new Map<string, Specification.GlyphState>();
for (
let i = 0;
i < plotSegmentClass.state.dataRowIndices.length;
i++
) {
rowToMarkState.set(
plotSegmentClass.state.dataRowIndices[i].join(","),
plotSegmentClass.state.glyphs[i]
);
}
let firstNonEmptyFacet = 0;
for (; firstNonEmptyFacet < facets.length; firstNonEmptyFacet++) {
if (facets[firstNonEmptyFacet].length >= 2) {
break;
}
}
if (firstNonEmptyFacet < facets.length) {
glyphs = [
{
glyph,
glyphState: rowToMarkState.get(
facets[firstNonEmptyFacet][0].join(",")
),
coordinateSystem,
},
{
glyph,
glyphState: rowToMarkState.get(
facets[firstNonEmptyFacet][1].join(",")
),
coordinateSystem,
},
];
}
}
break;
case "links.between":
{
const plotSegmentClasses = props.linkBetween.plotSegments.map(
(x) =>
manager.getClassById(
x
) as Prototypes.PlotSegments.PlotSegmentClass
);
const glyphObjects = plotSegmentClasses.map((x) =>
getById(manager.chart.glyphs, x.object.glyph)
);
glyphs = [
{
glyph: glyphObjects[0],
glyphState: plotSegmentClasses[0].state.glyphs[0],
coordinateSystem: plotSegmentClasses[0].getCoordinateSystem(),
},
{
glyph: glyphObjects[1],
glyphState: plotSegmentClasses[1].state.glyphs[0],
coordinateSystem: plotSegmentClasses[1].getCoordinateSystem(),
},
];
}
break;
case "links.table":
{
const plotSegmentClasses = props.linkTable.plotSegments.map(
(x) =>
manager.getClassById(
x
) as Prototypes.PlotSegments.PlotSegmentClass
);
const glyphObjects = plotSegmentClasses.map((x) =>
getById(manager.chart.glyphs, x.object.glyph)
);
const linkTable = this.props.store.chartManager.dataflow.getTable(
props.linkTable.table
);
const tables = plotSegmentClasses.map((plotSegmentClass) => {
const table = this.props.store.chartManager.dataflow.getTable(
plotSegmentClass.object.table
);
const id2RowGlyphIndex = new Map<string, [number[], number]>();
for (
let i = 0;
i < plotSegmentClass.state.dataRowIndices.length;
i++
) {
const rowIndex = plotSegmentClass.state.dataRowIndices[i];
const rowIDs = rowIndex.map((i) => table.getRow(i).id).join(",");
id2RowGlyphIndex.set(rowIDs, [rowIndex, i]);
}
return {
table,
id2RowGlyphIndex,
};
});
// Find the first links with nodes are exists in main table
const rowItem: Specification.DataRow = linkTable.rows.find(
(row) =>
row.source_id &&
tables[0].id2RowGlyphIndex.get(row.source_id.toString()) !=
undefined &&
row.target_id &&
tables[1].id2RowGlyphIndex.get(row.target_id.toString()) !=
undefined
);
if (rowItem) {
const [, i0] = tables[0].id2RowGlyphIndex.get(
rowItem.source_id?.toString()
);
const [, i1] = tables[1].id2RowGlyphIndex.get(
rowItem.target_id?.toString()
);
glyphs = [
{
glyph: glyphObjects[0],
glyphState: plotSegmentClasses[0].state.glyphs[i0],
coordinateSystem: plotSegmentClasses[0].getCoordinateSystem(),
},
{
glyph: glyphObjects[1],
glyphState: plotSegmentClasses[1].state.glyphs[i1],
coordinateSystem: plotSegmentClasses[1].getCoordinateSystem(),
},
];
} else {
glyphs = [];
}
}
break;
}
// Render mark anchor candidates
const elements = glyphs.map(
({ glyph, glyphState, coordinateSystem }, glyphIndex) => {
const anchorX = glyphState.marks[0].attributes.x as number;
const anchorY = glyphState.marks[0].attributes.y as number;
const offsetX = (glyphState.attributes.x as number) - anchorX;
const offsetY = (glyphState.attributes.y as number) - anchorY;
const marks = glyph.marks.map((element, elementIndex) => {
if (glyph.marks.length > 1 && element.classID == "mark.anchor") {
return null;
}
const markClass = manager.getMarkClass(
glyphState.marks[elementIndex]
);
const mode: "begin" | "end" = glyphIndex == 0 ? "begin" : "end";
let anchors = markClass.getLinkAnchors
? markClass.getLinkAnchors(mode)
: [];
anchors = anchors.filter((anchor) => {
if (lineMode == "line") {
return anchor.points.length == 1;
}
if (lineMode == "band") {
return anchor.points.length == 2;
}
});
return (
<g key={element._id}>
{anchors.map((anchor, index) => (
<g
className="anchor"
key={`m${index}`}
ref={(g) => {
if (g != null) {
this.markPlaceholders.set(g, {
mode,
markID: element._id,
anchor,
offsetX,
offsetY,
coordinateSystem,
});
}
}}
>
{this.renderAnchor(
coordinateSystem,
offsetX,
offsetY,
anchor
)}
</g>
))}
</g>
);
});
return <g key={glyphIndex}>{marks}</g>;
}
);
const currentAnchors = glyphs
.map(({ glyph, glyphState, coordinateSystem }, glyphIndex) => {
const anchorX = glyphState.marks[0].attributes.x as number;
const anchorY = glyphState.marks[0].attributes.y as number;
const offsetX = (glyphState.attributes.x as number) - anchorX;
const offsetY = (glyphState.attributes.y as number) - anchorY;
const anchor = glyphIndex == 0 ? props.anchor1 : props.anchor2;
const element = anchor[0].x.element;
if (!element) {
return null;
}
const elementState =
glyphState.marks[getIndexById(glyph.marks, element)];
const anchorDescription: Prototypes.LinkAnchor.Description = {
element,
points: anchor.map((a) => {
return {
x: elementState.attributes[a.x.attribute] as number,
xAttribute: a.x.attribute,
y: elementState.attributes[a.y.attribute] as number,
yAttribute: a.y.attribute,
direction: a.direction,
};
}),
};
return {
coordinateSystem,
offsetX,
offsetY,
anchor: anchorDescription,
};
})
.filter((anchor) => anchor != null);
let currentLinkElement: JSX.Element = null;
if (currentAnchors.length == 2) {
const path = Graphics.makePath();
const anchor1 = {
coordinateSystem: currentAnchors[0].coordinateSystem,
points: currentAnchors[0].anchor.points.map((p) => {
return {
x: p.x + currentAnchors[0].offsetX,
y: p.y + currentAnchors[0].offsetY,
direction: p.direction,
};
}),
curveness: this.props.link.properties.curveness as number,
};
const anchor2 = {
coordinateSystem: currentAnchors[1].coordinateSystem,
points: currentAnchors[1].anchor.points.map((p) => {
return {
x: p.x + currentAnchors[1].offsetX,
y: p.y + currentAnchors[1].offsetY,
direction: p.direction,
};
}),
curveness: this.props.link.properties.curveness as number,
};
Prototypes.Links.LinksClass.LinkPath(
path,
props.linkType,
props.interpolationType,
anchor1,
anchor2
);
const transform = `translate(${this.props.zoom.centerX},${this.props.zoom.centerY}) scale(${this.props.zoom.scale})`;
currentLinkElement = (
<g transform={transform}>
<path
d={renderSVGPath(path.path.cmds)}
className={`link-hint-${props.linkType}`}
/>
</g>
);
}
return (
<g>
{currentLinkElement}
{elements}
{currentAnchors.map(
({ coordinateSystem, offsetX, offsetY, anchor }, index) => (
<g className="anchor active" key={index}>
{this.renderAnchor(coordinateSystem, offsetX, offsetY, anchor)}
</g>
)
)}
</g>
);
}