Skip to content

Commit

Permalink
add simple network example, update install links
Browse files Browse the repository at this point in the history
  • Loading branch information
venkatesh-sivaraman committed May 23, 2024
1 parent 0c518d9 commit 776347b
Show file tree
Hide file tree
Showing 16 changed files with 4,290 additions and 86 deletions.
18 changes: 13 additions & 5 deletions docs/_pages/01-quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,26 @@ D3.js users looking for additional flexibility and simpler canvas support.

## Installation

TBD update directions and urls
To install Counterpoint using NPM:

Importing Counterpoint in ES6 JavaScript is simple:
```bash
npm install -S counterpoint-vis
```

Then import in your code:

```javascript
import { Mark, Attribute, ... } from 'counterpoint-vis';
```

To import Counterpoint in vanilla JavaScript, you can use similar syntax but
To import Counterpoint in a JavaScript module, you can use similar syntax but
refer to the full library URL:

```javascript
import { Mark, Attribute } from 'https://cdn.jsdelivr.net/npm/counterpoint-vis@latest/dist/counterpoint-vis.es.js';
```

or use [dynamic import syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import#):
or use [dynamic import syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import#) in vanilla JS:

```javascript
import(
Expand Down Expand Up @@ -336,4 +340,8 @@ From here, you can check out further documentation to learn about [how to use at
[add and remove marks dynamically]({{ site.baseurl }}/pages/04-staging),
[make your canvases non-visually accessible]({{ site.baseurl }}/pages/06-accessible-navigation), and more.

We've also provided some more complete examples (TODO).
We've also provided some more complete examples:

* An [animated scatter plot of the Cars dataset]({{ site.baseurl }}/2024/04/30/cars) showing cars over time
* A [network/embedding visualization of VIS paper citations]({{ site.baseurl }}/2024/04/30/citations)
* A [keyboard-navigable version of the Gapminder chart]({{ site.baseurl }}/2024/04/30/gapminder-accessible)
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Counterpoint **is not**:
modules from D3 such as `d3-zoom`, `d3-axis`, and others in combination with
Counterpoint's data structures.

Ready to get started? Head over to the [quickstart]({% link _pages/01-quickstart.md %})
Ready to get started? Head over to the [quickstart]({{ site.baseurl }}/pages/01-quickstart)
to install and learn the basics.

Or, check out this demo chart showing worldwide GDP, life expectancy, and population
Expand Down
141 changes: 61 additions & 80 deletions examples/network/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,67 @@
Mark,
AttributeRecompute,
PositionMap,
Attribute,
Animator,
interpolateTo,
curveLinear,
} from 'counterpoint-vis';
import * as d3 from 'd3';
import { onDestroy, onMount } from 'svelte';
let canvas: HTMLCanvasElement;
let width = 1000;
let height = 500;
const simulationWidth = 2000;
const simulationHeight = 700;
const gutter = 100;
let height = 600;
const simulationWidth = 1000;
const simulationHeight = 600;
const pointSize = 3;
let numPoints = 1000;
let hoveredID: number | null = null;
// create randomly-positioned graph nodes
let nodes = new Array(numPoints).fill(0).map((_, i) => ({
x: Math.random() * simulationWidth,
y: Math.random() * simulationHeight,
}));
// create a mark render group, which contains information about how these nodes will be drawn.
// each node will be represented as a Mark attribute with position, color, and radius
// attributes.
let markSet = new MarkRenderGroup(
nodes.map(
(n, i) =>
new Mark(i, {
x: {
value: n.x,
transform: (v: number) => {
let x = xOffset.get() + v;
return ((x + gutter) % (simulationWidth + gutter * 2)) - gutter;
},
},
x: n.x,
y: n.y,
color: {
value: `hsl(${Math.random() * 360}, 90%, 70%)`,
recompute: AttributeRecompute.WHEN_UPDATED,
},
radius: pointSize,
// computed function for radius so that only the hovered point will be large
radius: () => (hoveredID == i ? pointSize * 2 : pointSize),
alpha: 1.0,
})
)
).configure({
animationDuration: 2000,
// configure defaults for animation
animationDuration: 1000,
animationCurve: curveEaseInOut,
// the hit-test function determines whether a given location falls inside
// a mark or not, which will be used for mouseover interactions
hitTest(mark, location) {
return (
Math.sqrt(
Math.pow(mark.attr('x') - location[0], 2.0) +
Math.pow(mark.attr('y') - location[1], 2.0)
) <=
mark.attr('radius') * 2
);
},
});
let xOffset = new Attribute(simulationWidth + gutter);
let positionMap = new PositionMap().add(markSet);
// a position map helps us reverse-lookup marks by position so we can check
// what node was hovered on
let positionMap = new PositionMap({
maximumHitTestDistance: pointSize * 4,
}).add(markSet);
// use the adj() method to represent node edges (these are random for demo purposes)
markSet.forEach((m) =>
m.adj(
'neighbors',
Expand All @@ -62,7 +76,13 @@
.filter((n, i) => n.id > m.id && (i == 0 || Math.random() < 0.5))
)
);
let ticker = new Ticker([markSet, xOffset]).onChange(draw);
// the ticker makes sure our draw function will get called whenever the marks change
let ticker = new Ticker(markSet).onChange(() => {
// since the marks may have moved, tell the position map that its positions aren't valid anymore
positionMap.invalidate();
draw();
});
function draw() {
if (!!canvas) {
Expand All @@ -72,13 +92,9 @@
ctx.resetTransform();
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
ctx.fillStyle = '#225';
ctx.fillRect(0, 0, canvas.clientWidth, canvas.clientHeight);
ctx.translate(0, -(simulationHeight - height) * 0.5);
ctx.strokeStyle = '#bbb';
ctx.lineWidth = 1.0;
ctx.globalAlpha = 0.3;
let xo = xOffset.get();
// Draw edges
markSet.stage!.forEach((mark) => {
let x = mark.attr('x');
Expand All @@ -96,25 +112,12 @@
ctx?.lineTo(nx, ny);
});
[...mark.adj('neighbors'), ...mark.sourceMarks()].forEach(
(neighbor) => {
// start an animation on radius
if (
!mark.changed('radius') &&
neighbor.attr('radius') > pointSize * 1.2
) {
pulsateMark(
mark,
neighbor.attributes['radius'].future() * 0.95
);
}
}
);
ctx?.stroke();
ctx?.closePath();
});
ctx.globalAlpha = 1.0;
// Draw node circles
markSet.stage!.forEach((mark) => {
ctx?.save();
let x = mark.attr('x');
Expand All @@ -134,34 +137,16 @@
}
}
// use a d3 force simulation to optimize the layout
let simulation: d3.Simulation<{ x: number; y: number }, undefined> | null =
null;
let currentTick = 0;
async function pan() {
await xOffset
.animate(
new Animator(
interpolateTo(-gutter),
((simulationWidth + gutter) / 1000) * 20000,
curveLinear
)
)
.wait();
xOffset.set(simulationWidth + gutter);
pan();
}
async function pulsateMark(mark: Mark, radius: number) {
try {
await mark.animateTo('radius', radius, { duration: 200 }).wait('radius');
mark.animateTo('radius', pointSize, { duration: 500 });
} catch (e) {}
}
onMount(() => {
pan();
if (!!simulation) simulation.stop();
// for the d3 simulation, this defines the forces that will push the nodes
// to the final positions, including pulling connected nodes closer, centering
// the graph, preventing collisions, and generally repelling nodes
simulation = d3
.forceSimulation(nodes)
.force(
Expand All @@ -174,33 +159,27 @@
)
.flat()
)
.distance(simulationHeight * 0.1)
.distance(pointSize * 10)
.strength(2)
)
.force(
'center',
d3.forceCenter(simulationWidth / 2, simulationHeight / 2)
d3.forceCenter(simulationWidth / 2, simulationHeight / 2).strength(0.1)
)
.force('x', d3.forceX(simulationWidth * 0.5).strength(0.01))
.force('y', d3.forceY(simulationHeight * 0.5).strength(0.01))
.force('collide', d3.forceCollide(pointSize * 2))
.force('repel', d3.forceManyBody().strength(-2))
.force('repel', d3.forceManyBody().strength(-1))
.alphaDecay(0.01)
.on('tick', () => {
currentTick += 1;
if (!markSet.changed('x')) {
if (!markSet.changed('x'))
markSet
.animateTo('x', (_, i) => nodes[i].x)
.animateTo('y', (_, i) => nodes[i].y);
nodes.forEach((n) => {
n.x +=
Math.random() * simulationHeight * 0.2 - simulationHeight * 0.1;
n.y +=
Math.random() * simulationHeight * 0.2 - simulationHeight * 0.1;
});
}
if (currentTick % 100 == 0) {
simulation?.alphaDecay(0);
}
});
// handle view resizing
new ResizeObserver(() => {
if (!canvas) return;
console.log('resizing', canvas.offsetWidth);
Expand All @@ -211,28 +190,30 @@
draw();
}).observe(canvas);
});
// clean up when the view is deleted
onDestroy(() => {
ticker.stop();
simulation?.stop();
});
function mouseover(e: MouseEvent) {
// find the mark the user is hovering on and increase its radius
let pos = [
e.clientX - canvas.getBoundingClientRect().left,
e.clientY - canvas.getBoundingClientRect().top,
];
positionMap.invalidate();
let closestMarks = positionMap.marksNear(pos, pointSize * 5);
if (closestMarks.length >= 1) {
if (!closestMarks[0].changed('radius'))
pulsateMark(closestMarks[0], pointSize * 2.5);
let closestMark = positionMap.hitTest(pos);
if ((closestMark?.id ?? null) !== hoveredID) {
hoveredID = closestMark?.id ?? null;
markSet.animate('radius', { duration: 200 });
}
}
</script>

<main>
<canvas
style="width: 100%; height: 500px;"
style="width: {width}px; height: {height}px;"
bind:this={canvas}
on:mousemove={mouseover}
/>
Expand Down
24 changes: 24 additions & 0 deletions examples/network_website_header/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
3 changes: 3 additions & 0 deletions examples/network_website_header/.vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}
11 changes: 11 additions & 0 deletions examples/network_website_header/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Scatterplot Example

This example demonstrates using basic animations for marks with `x`, `y`, and `color` properties.

## Setup

```bash
cd tests/scatterplot
npm install
npm run dev
```
13 changes: 13 additions & 0 deletions examples/network_website_header/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Svelte + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
Loading

0 comments on commit 776347b

Please sign in to comment.