-
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.
Details: - Drafts new package @sigma/utils - Adds `fitNodesToViewport` and `getCameraStateToFitNodesToViewport` - Adds a new story to showcase `fitNodesToViewport`
- Loading branch information
Showing
13 changed files
with
331 additions
and
13 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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,64 @@ | ||
import { fitNodesToViewport } from "@sigma/utils"; | ||
import Graph from "graphology"; | ||
import louvain from "graphology-communities-louvain"; | ||
import iwanthue from "iwanthue"; | ||
import Sigma from "sigma"; | ||
|
||
import data from "../_data/data.json"; | ||
import { onStoryDown } from "../utils"; | ||
|
||
export default () => { | ||
const graph = new Graph(); | ||
graph.import(data); | ||
|
||
// Detect communities | ||
louvain.assign(graph, { nodeCommunityAttribute: "community" }); | ||
const communities = new Set<string>(); | ||
graph.forEachNode((_, attrs) => communities.add(attrs.community)); | ||
const communitiesArray = Array.from(communities); | ||
|
||
// Determine colors, and color each node accordingly | ||
const palette: Record<string, string> = iwanthue(communities.size).reduce( | ||
(iter, color, i) => ({ | ||
...iter, | ||
[communitiesArray[i]]: color, | ||
}), | ||
{}, | ||
); | ||
graph.forEachNode((node, attr) => graph.setNodeAttribute(node, "color", palette[attr.community])); | ||
|
||
// Retrieve some useful DOM elements | ||
const container = document.getElementById("sigma-container") as HTMLElement; | ||
|
||
// Instantiate sigma | ||
const renderer = new Sigma(graph, container); | ||
|
||
// Add buttons | ||
const buttonsContainer = document.createElement("div"); | ||
buttonsContainer.style.position = "absolute"; | ||
buttonsContainer.style.right = "10px"; | ||
buttonsContainer.style.bottom = "10px"; | ||
document.body.append(buttonsContainer); | ||
|
||
communitiesArray.forEach((community) => { | ||
const id = `cb-${community}`; | ||
const buttonContainer = document.createElement("div"); | ||
buttonContainer.innerHTML += ` | ||
<button id="${id}" style="color:${palette[community]};margin-top:3px">Community n°${community + 1}</button> | ||
`; | ||
buttonsContainer.append(buttonContainer); | ||
const button = buttonsContainer.querySelector(`#${id}`) as HTMLButtonElement; | ||
|
||
button.addEventListener("click", () => { | ||
fitNodesToViewport( | ||
renderer, | ||
graph.filterNodes((_, attr) => attr.community === community), | ||
{ animate: true }, | ||
); | ||
}); | ||
}); | ||
|
||
onStoryDown(() => { | ||
renderer?.kill(); | ||
}); | ||
}; |
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,14 @@ | ||
<style> | ||
html, | ||
body, | ||
#storybook-root, | ||
#sigma-container { | ||
width: 100%; | ||
height: 100%; | ||
margin: 0 !important; | ||
padding: 0 !important; | ||
overflow: hidden; | ||
font-family: sans-serif; | ||
} | ||
</style> | ||
<div id="sigma-container"></div> |
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,25 @@ | ||
import type { Meta, StoryObj } from "@storybook/web-components"; | ||
|
||
import FitNodesToViewportPlay from "./fit-nodes-to-viewport"; | ||
import FitNodesToViewportSource from "./fit-nodes-to-viewport?raw"; | ||
import template from "./index.html?raw"; | ||
|
||
const meta: Meta = { | ||
id: "utils", | ||
title: "utils", | ||
}; | ||
export default meta; | ||
|
||
type Story = StoryObj; | ||
|
||
export const FitNodesToViewport: Story = { | ||
name: "Fit nodes to viewport", | ||
render: () => template, | ||
play: FitNodesToViewportPlay, | ||
args: {}, | ||
parameters: { | ||
storySource: { | ||
source: FitNodesToViewportSource, | ||
}, | ||
}, | ||
}; |
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,2 @@ | ||
node_modules | ||
dist |
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,4 @@ | ||
.gitignore | ||
node_modules | ||
src | ||
tsconfig.json |
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,7 @@ | ||
# Sigma.js - Utils | ||
|
||
This package contains various utils functions to ease the use of [sigma.js](https://www.sigmajs.org/). | ||
|
||
### `fitNodesToViewport` and `getCameraStateToFitNodesToViewport` | ||
|
||
These functions allow moving a sigma instance's camera so that it fits to a given group of nodes. |
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,34 @@ | ||
{ | ||
"name": "@sigma/utils", | ||
"version": "3.0.0-beta.0", | ||
"description": "A set of utils functions to ease the use of sigma.js", | ||
"main": "dist/sigma-utils.cjs.js", | ||
"module": "dist/sigma-utils.esm.js", | ||
"files": [ | ||
"/dist" | ||
], | ||
"sideEffects": false, | ||
"homepage": "https://www.sigmajs.org", | ||
"bugs": "http://github.com/jacomyal/sigma.js/issues", | ||
"repository": { | ||
"type": "git", | ||
"url": "http://github.com/jacomyal/sigma.js.git" | ||
}, | ||
"preconstruct": { | ||
"entrypoints": [ | ||
"index.ts" | ||
] | ||
}, | ||
"peerDependencies": { | ||
"sigma": ">=3.0.0-beta.29" | ||
}, | ||
"license": "MIT", | ||
"exports": { | ||
".": { | ||
"module": "./dist/sigma-utils.esm.js", | ||
"import": "./dist/sigma-utils.cjs.mjs", | ||
"default": "./dist/sigma-utils.cjs.js" | ||
}, | ||
"./package.json": "./package.json" | ||
} | ||
} |
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,104 @@ | ||
import Sigma from "sigma"; | ||
import type { CameraState } from "sigma/types"; | ||
import { getCorrectionRatio } from "sigma/utils"; | ||
|
||
export type FitNodesToScreenOptions = { | ||
animate: boolean; | ||
}; | ||
export const DEFAULT_FIT_NODES_TO_SCREEN_OPTIONS: FitNodesToScreenOptions = { | ||
animate: true, | ||
}; | ||
|
||
/** | ||
* This function takes a Sigma instance and a list of nodes as input, and returns a CameraState so that the camera fits | ||
* the best to the given groups of nodes (i.e. the camera is as zoomed as possible while keeping all nodes on screen). | ||
* | ||
* @param sigma A Sigma instance | ||
* @param nodes A list of nodes IDs | ||
* @param opts An optional and partial FitNodesToScreenOptions object | ||
*/ | ||
export function getCameraStateToFitNodesToViewport( | ||
sigma: Sigma, | ||
nodes: string[], | ||
_opts: Partial<Omit<FitNodesToScreenOptions, "animate">> = {}, | ||
): CameraState { | ||
let minX = Infinity; | ||
let maxX = -Infinity; | ||
let minY = Infinity; | ||
let maxY = -Infinity; | ||
let groupMinX = Infinity; | ||
let groupMaxX = -Infinity; | ||
let groupMinY = Infinity; | ||
let groupMaxY = -Infinity; | ||
let groupFramedMinX = Infinity; | ||
let groupFramedMaxX = -Infinity; | ||
let groupFramedMinY = Infinity; | ||
let groupFramedMaxY = -Infinity; | ||
|
||
const group = new Set(nodes); | ||
const graph = sigma.getGraph(); | ||
graph.forEachNode((node, { x, y }) => { | ||
const data = sigma.getNodeDisplayData(node); | ||
if (!data) throw new Error(`getCameraStateToFitNodesToViewport: Node ${node} not found in sigma's graph.`); | ||
const { x: framedX, y: framedY } = data; | ||
|
||
minX = Math.min(minX, x); | ||
maxX = Math.max(maxX, x); | ||
minY = Math.min(minY, y); | ||
maxY = Math.max(maxY, y); | ||
if (group.has(node)) { | ||
groupMinX = Math.min(groupMinX, x); | ||
groupMaxX = Math.max(groupMaxX, x); | ||
groupMinY = Math.min(groupMinY, y); | ||
groupMaxY = Math.max(groupMaxY, y); | ||
groupFramedMinX = Math.min(groupFramedMinX, framedX); | ||
groupFramedMaxX = Math.max(groupFramedMaxX, framedX); | ||
groupFramedMinY = Math.min(groupFramedMinY, framedY); | ||
groupFramedMaxY = Math.max(groupFramedMaxY, framedY); | ||
} | ||
}); | ||
|
||
const groupCenterX = (groupFramedMinX + groupFramedMaxX) / 2; | ||
const groupCenterY = (groupFramedMinY + groupFramedMaxY) / 2; | ||
const groupWidth = groupMaxX - groupMinX || 1; | ||
const groupHeight = groupMaxY - groupMinY || 1; | ||
const graphWidth = maxX - minX || 1; | ||
const graphHeight = maxY - minY || 1; | ||
|
||
const { width, height } = sigma.getDimensions(); | ||
const correction = getCorrectionRatio({ width, height }, { width: graphWidth, height: graphHeight }); | ||
const ratio = | ||
((groupHeight / groupWidth < height / width ? groupWidth : groupHeight) / Math.max(graphWidth, graphHeight)) * | ||
correction; | ||
|
||
const camera = sigma.getCamera(); | ||
return { | ||
...camera.getState(), | ||
x: groupCenterX, | ||
y: groupCenterY, | ||
ratio, | ||
}; | ||
} | ||
|
||
/** | ||
* This function takes a Sigma instance and a list of nodes as input, and updates the camera so that the camera fits the | ||
* best to the given groups of nodes (i.e. the camera is as zoomed as possible while keeping all nodes on screen). | ||
* | ||
* @param sigma A Sigma instance | ||
* @param nodes A list of nodes IDs | ||
* @param opts An optional and partial FitNodesToScreenOptions object | ||
*/ | ||
export function fitNodesToViewport(sigma: Sigma, nodes: string[], opts: Partial<FitNodesToScreenOptions> = {}): void { | ||
const { animate } = { | ||
...DEFAULT_FIT_NODES_TO_SCREEN_OPTIONS, | ||
...opts, | ||
}; | ||
|
||
const camera = sigma.getCamera(); | ||
const newCameraState = getCameraStateToFitNodesToViewport(sigma, nodes, opts); | ||
if (animate) { | ||
camera.animate(newCameraState); | ||
} else { | ||
camera.setState(newCameraState); | ||
} | ||
} |
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 @@ | ||
export * from "./fitNodesToViewport"; |
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,23 @@ | ||
{ | ||
"extends": "../../tsconfig.base.json", | ||
"compilerOptions": { | ||
"target": "ESNext", | ||
"useDefineForClassFields": true, | ||
"lib": ["ES2020", "DOM", "DOM.Iterable"], | ||
"module": "ESNext", | ||
"skipLibCheck": true, | ||
"moduleResolution": "node", | ||
"allowSyntheticDefaultImports": true, | ||
"allowImportingTsExtensions": true, | ||
"resolveJsonModule": true, | ||
"isolatedModules": true, | ||
"noEmit": true, | ||
"strict": true, | ||
"noUnusedLocals": true, | ||
"noUnusedParameters": true, | ||
"noFallthroughCasesInSwitch": true, | ||
"declaration": true | ||
}, | ||
"include": ["src"], | ||
"exclude": ["src/**/__docs__", "src/**/__test__"] | ||
} |
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