-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[edge-curve] First full featured version
Details: - Fixes broken shaders - Adds indexParallelEdgesIndex to help drawing parallel edges as curves with varying curvatures - Adds curved edges canvas label renderer - Adds more examples on interactions, labels and parallel edges
- Loading branch information
Showing
11 changed files
with
112,909 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import { Attributes } from "graphology-types"; | ||
import { Settings } from "sigma/settings"; | ||
import { NodeDisplayData, PartialButFor } from "sigma/types"; | ||
|
||
import { CurvedEdgeDisplayData, DEFAULT_EDGE_CURVATURE } from "./utils.ts"; | ||
|
||
interface Point { | ||
x: number; | ||
y: number; | ||
} | ||
|
||
function getCurvePoint(t: number, p0: Point, p1: Point, p2: Point): Point { | ||
const x = (1 - t) ** 2 * p0.x + 2 * (1 - t) * t * p1.x + t ** 2 * p2.x; | ||
const y = (1 - t) ** 2 * p0.y + 2 * (1 - t) * t * p1.y + t ** 2 * p2.y; | ||
return { x, y }; | ||
} | ||
|
||
function getCurveLength(p0: Point, p1: Point, p2: Point): number { | ||
const steps = 20; | ||
let length = 0; | ||
let lastPoint = p0; | ||
for (let i = 0; i < steps; i++) { | ||
const point = getCurvePoint((i + 1) / steps, p0, p1, p2); | ||
length += Math.sqrt((lastPoint.x - point.x) ** 2 + (lastPoint.y - point.y) ** 2); | ||
lastPoint = point; | ||
} | ||
|
||
return length; | ||
} | ||
|
||
export function drawCurvedEdgeLabel< | ||
N extends Attributes = Attributes, | ||
E extends Attributes = Attributes, | ||
G extends Attributes = Attributes, | ||
>( | ||
context: CanvasRenderingContext2D, | ||
edgeData: PartialButFor<CurvedEdgeDisplayData, "label" | "color" | "size" | "curvature">, | ||
sourceData: PartialButFor<NodeDisplayData, "x" | "y" | "size">, | ||
targetData: PartialButFor<NodeDisplayData, "x" | "y" | "size">, | ||
settings: Settings<N, E, G>, | ||
): void { | ||
const size = settings.edgeLabelSize, | ||
curvature = edgeData.curvature || DEFAULT_EDGE_CURVATURE, | ||
font = settings.edgeLabelFont, | ||
weight = settings.edgeLabelWeight, | ||
color = settings.edgeLabelColor.attribute | ||
? edgeData[settings.edgeLabelColor.attribute] || settings.edgeLabelColor.color || "#000" | ||
: settings.edgeLabelColor.color; | ||
|
||
let label = edgeData.label; | ||
|
||
if (!label) return; | ||
|
||
context.fillStyle = color; | ||
context.font = `${weight} ${size}px ${font}`; | ||
|
||
// Computing positions without considering nodes sizes: | ||
let sourceX = sourceData.x; | ||
let sourceY = sourceData.y; | ||
let targetX = targetData.x; | ||
let targetY = targetData.y; | ||
const centerX = (sourceX + targetX) / 2; | ||
const centerY = (sourceY + targetY) / 2; | ||
const diffX = targetX - sourceX; | ||
const diffY = targetY - sourceY; | ||
const diff = Math.sqrt(diffX ** 2 + diffY ** 2); | ||
// Anchor point: | ||
let anchorX = centerX + diffY * curvature; | ||
let anchorY = centerY - diffX * curvature; | ||
|
||
// Adapt curve points to edge thickness: | ||
const offset = edgeData.size * 0.7 + 5; | ||
const sourceOffsetVector = { | ||
x: anchorY - sourceY, | ||
y: -(anchorX - sourceX), | ||
}; | ||
const sourceOffsetVectorLength = Math.sqrt(sourceOffsetVector.x ** 2 + sourceOffsetVector.y ** 2); | ||
const targetOffsetVector = { | ||
x: targetY - anchorY, | ||
y: -(targetX - anchorX), | ||
}; | ||
const targetOffsetVectorLength = Math.sqrt(targetOffsetVector.x ** 2 + targetOffsetVector.y ** 2); | ||
sourceX += (offset * sourceOffsetVector.x) / sourceOffsetVectorLength; | ||
sourceY += (offset * sourceOffsetVector.y) / sourceOffsetVectorLength; | ||
targetX += (offset * targetOffsetVector.x) / targetOffsetVectorLength; | ||
targetY += (offset * targetOffsetVector.y) / targetOffsetVectorLength; | ||
// For anchor, the vector is simpler, so it is inlined: | ||
anchorX += (offset * diffY) / diff; | ||
anchorY -= (offset * diffX) / diff; | ||
|
||
// Compute curve length: | ||
const anchorPoint = { x: anchorX, y: anchorY }; | ||
const sourcePoint = { x: sourceX, y: sourceY }; | ||
const targetPoint = { x: targetX, y: targetY }; | ||
const curveLength = getCurveLength(sourcePoint, anchorPoint, targetPoint); | ||
|
||
if (curveLength < sourceData.size + targetData.size) return; | ||
|
||
// Handling ellipsis | ||
let textLength = context.measureText(label).width; | ||
const availableTextLength = curveLength - sourceData.size - targetData.size; | ||
if (textLength > availableTextLength) { | ||
const ellipsis = "…"; | ||
label = label + ellipsis; | ||
textLength = context.measureText(label).width; | ||
|
||
while (textLength > availableTextLength && label.length > 1) { | ||
label = label.slice(0, -2) + ellipsis; | ||
textLength = context.measureText(label).width; | ||
} | ||
|
||
if (label.length < 4) return; | ||
} | ||
|
||
// Measure each character: | ||
const charactersLengthCache: Record<string, number> = {}; | ||
for (let i = 0, length = label.length; i < length; i++) { | ||
const character = label[i]; | ||
|
||
if (!charactersLengthCache[character]) { | ||
charactersLengthCache[character] = context.measureText(character).width * (1 + curvature * 0.35); | ||
} | ||
} | ||
|
||
// Draw each character: | ||
let t = 0.5 - textLength / curveLength / 2; | ||
for (let i = 0, length = label.length; i < length; i++) { | ||
const character = label[i]; | ||
const point = getCurvePoint(t, sourcePoint, anchorPoint, targetPoint); | ||
|
||
const tangentX = 2 * (1 - t) * (anchorX - sourceX) + 2 * t * (targetX - anchorX); | ||
const tangentY = 2 * (1 - t) * (anchorY - sourceY) + 2 * t * (targetY - anchorY); | ||
const angle = Math.atan2(tangentY, tangentX); | ||
|
||
context.save(); | ||
context.translate(point.x, point.y); | ||
context.rotate(angle); | ||
|
||
// Dessiner le caractère | ||
context.fillText(character, 0, 0); | ||
|
||
context.restore(); | ||
|
||
t += charactersLengthCache[character] / curveLength; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { Meta, StoryObj } from "@storybook/html"; | ||
import Graph from "graphology"; | ||
import Sigma from "sigma"; | ||
import { EdgeDisplayData, NodeDisplayData } from "sigma/types"; | ||
|
||
import EdgeCurveProgram from "../index.ts"; | ||
import data from "./data/les-miserables.json"; | ||
import "./stage.css"; | ||
|
||
const createStage = () => { | ||
const stage = document.createElement("div"); | ||
stage.classList.add("stage"); | ||
|
||
const graph = new Graph(); | ||
graph.import(data); | ||
|
||
let state: { type: "idle" } | { type: "hovered"; edge: string; source: string; target: string } = { type: "idle" }; | ||
const sigma = new Sigma(graph, stage, { | ||
allowInvalidContainer: true, | ||
enableEdgeEvents: true, | ||
defaultEdgeType: "curve", | ||
zIndex: true, | ||
edgeProgramClasses: { | ||
curve: EdgeCurveProgram, | ||
}, | ||
edgeReducer: (edge, attributes) => { | ||
const res: Partial<EdgeDisplayData> = { ...attributes }; | ||
|
||
if (state.type === "hovered") { | ||
if (edge === state.edge) { | ||
res.size = (res.size || 1) * 1.5; | ||
res.zIndex = 1; | ||
} else { | ||
res.color = "#f0f0f0"; | ||
res.zIndex = 0; | ||
} | ||
} | ||
|
||
return res; | ||
}, | ||
nodeReducer: (node, attributes) => { | ||
const res: Partial<NodeDisplayData> = { ...attributes }; | ||
|
||
if (state.type === "hovered") { | ||
if (node === state.source || node === state.target) { | ||
res.highlighted = true; | ||
res.zIndex = 1; | ||
} else { | ||
res.label = undefined; | ||
res.zIndex = 0; | ||
} | ||
} | ||
|
||
return res; | ||
}, | ||
}); | ||
|
||
sigma.on("enterEdge", ({ edge }) => { | ||
state = { type: "hovered", edge, source: graph.source(edge), target: graph.target(edge) }; | ||
sigma.refresh(); | ||
}); | ||
sigma.on("leaveEdge", () => { | ||
state = { type: "idle" }; | ||
sigma.refresh(); | ||
}); | ||
|
||
return stage; | ||
}; | ||
|
||
const meta: Meta<typeof createStage> = { | ||
title: "edge-curve", | ||
render: () => createStage(), | ||
parameters: { | ||
layout: "fullscreen", | ||
}, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof createStage>; | ||
|
||
export const InteractionsExample: Story = { | ||
name: "Interactions", | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { Meta, StoryObj } from "@storybook/html"; | ||
import { MultiGraph } from "graphology"; | ||
import Sigma from "sigma"; | ||
import { EdgeArrowProgram } from "sigma/rendering"; | ||
|
||
import EdgeCurveProgram from "../index.ts"; | ||
import "./stage.css"; | ||
|
||
const createStage = () => { | ||
const stage = document.createElement("div"); | ||
stage.classList.add("stage"); | ||
|
||
// Create a graph, with various parallel edges: | ||
const graph = new MultiGraph(); | ||
|
||
graph.addNode("a", { x: 0, y: 0, size: 10, label: "Alexandra" }); | ||
graph.addNode("b", { x: 1, y: -1, size: 20, label: "Bastian" }); | ||
graph.addNode("c", { x: 3, y: -2, size: 10, label: "Charles" }); | ||
graph.addNode("d", { x: 1, y: -3, size: 10, label: "Dorothea" }); | ||
graph.addNode("e", { x: 3, y: -4, size: 20, label: "Ernestine" }); | ||
graph.addNode("f", { x: 4, y: -5, size: 10, label: "Fabian" }); | ||
|
||
graph.addEdge("a", "b", { forceLabel: true, size: 2, label: "works with" }); | ||
graph.addEdge("b", "c", { forceLabel: true, label: "works with", type: "curved", curvature: 0.5 }); | ||
graph.addEdge("b", "d", { forceLabel: true, label: "works with" }); | ||
graph.addEdge("c", "b", { forceLabel: true, size: 3, label: "works with", type: "curved" }); | ||
graph.addEdge("c", "e", { forceLabel: true, size: 3, label: "works with" }); | ||
graph.addEdge("d", "c", { forceLabel: true, label: "works with", type: "curved", curvature: 0.1 }); | ||
graph.addEdge("d", "e", { forceLabel: true, label: "works with", type: "curved", curvature: 1 }); | ||
graph.addEdge("e", "d", { forceLabel: true, size: 2, label: "works with", type: "curved" }); | ||
graph.addEdge("f", "e", { forceLabel: true, label: "works with", type: "curved" }); | ||
|
||
new Sigma(graph, stage, { | ||
allowInvalidContainer: true, | ||
defaultEdgeType: "straight", | ||
renderEdgeLabels: true, | ||
edgeProgramClasses: { | ||
straight: EdgeArrowProgram, | ||
curved: EdgeCurveProgram, | ||
}, | ||
}); | ||
|
||
return stage; | ||
}; | ||
|
||
const meta: Meta<typeof createStage> = { | ||
title: "edge-curve", | ||
render: () => createStage(), | ||
parameters: { | ||
layout: "fullscreen", | ||
}, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof createStage>; | ||
|
||
export const LabelsExample: Story = { | ||
name: "Labels", | ||
}; |
Oops, something went wrong.