Skip to content

Commit

Permalink
populate docs
Browse files Browse the repository at this point in the history
  • Loading branch information
venkatesh-sivaraman committed Oct 7, 2024
1 parent ac92f6d commit cfd7f0b
Show file tree
Hide file tree
Showing 10 changed files with 1,305 additions and 43 deletions.
2 changes: 1 addition & 1 deletion docs/_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# You can create any custom variable you would like, and they will be accessible
# in the templates via {{ site.myvariable }}.

title: Counterpoint Documentation
title: Counterpoint
description: >- # this means to ignore newlines until "baseurl:"
Counterpoint is a JavaScript/TypeScript library to help people develop beautiful
large-scale animated data visualizations using HTML5 Canvas and WebGL.
Expand Down
5 changes: 2 additions & 3 deletions docs/_pages/01-quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,13 +243,12 @@ interpolate to the new values.
> Since the `draw()` function will get called about 60 times per second during animations, it's
> important to make sure it runs fast and doesn't perform any unnecessary
> calculations. Plus, you can configure Counterpoint to redraw only when
> needed, improving performance and saving energy. See [Optimizing Performance]({% link _pages/07-optimizing-performance.md %})
> to learn more.
> needed, improving performance and saving energy.
>
>
{: .block-tip }

To enable animations, we first have to create a **<a href="{{ site.baseurl }}/pages/03-animation-timing#triggering-render-updates-with-a-ticker">ticker</a>** to keep track of our
To enable animations, we first have to create a **<a href="{{ site.baseurl }}/pages/03-animation-timing#tickers">ticker</a>** to keep track of our
animations' timing. This `Ticker` instance will keep track of the render group(s)
we give it, and we pass it a function to call when the state of the render group
changes:
Expand Down
210 changes: 186 additions & 24 deletions docs/_pages/02-marks-and-rendergroups.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ Change</a> below.)

You can also pass an object of options to the attribute constructor, including
the attribute's value under either the `value` (for a static value) or `valueFn`
(for a function) keys. The options you can pass are listed in the
<a href="#Attribute-constructor">API Reference</a> below. For example:
(for a function) keys. For example:

```javascript
new Attribute({
Expand All @@ -69,6 +68,17 @@ new Attribute({
})
```

Below is the full list of options you can pass:

| Option | Description |
|:-------|:-----------:|
| `value` | The value of the attribute, if it is not updated using a dynamic function. |
| `valueFn` | A function that takes the attribute's compute argument and returns the attribute's value. This overrides a static `value` property value. |
| `computeArg` | An argument to be passed to the attribute's `valueFn` and `transform` functions. If `undefined`, the attribute itself is passed as the argument. (This only applies to attributes *not* being used inside `Mark` instances. Marks pass themselves as their attributes' compute arguments.) |
| `transform` | A function that transforms the value of the attribute before being returned. It should take the raw attribute value and (optionally) the attribute's compute argument, and return a transformed value. |
| <span id="Attribute-recompute"></span>`recompute` | Defines the behavior of the attribute's computation when specified using a value function. The default value of `AttributeRecompute.DEFAULT` causes the value function to be called every time `get()`, `compute()`, or `animate()` is called. If set to `AttributeRecompute.ALWAYS`, the value function is called every time the `advance()` method is called (i.e. every tick). If set to `AttributeRecompute.WHEN_UPDATED`, it will only be called when `compute()` or `animate()` is called. See <a href="#controlling-attribute-computation">Controlling Attribute Computation</a> for more details. |
| <span id="Attribute-cacheTransform"></span>`cacheTransform` | If `true`, specifies that the transformed value should be cached and reused when the raw value does not change (suitable when the transform function is fixed). If `false`, specifies that the transform should be rerun every time the value is requested - suitable when the transform function's behavior may change from frame to frame. When the value is cached, the transform can be updated by calling `updateTransform()` on the attribute. |

## Combining Marks in a Render Group

The `MarkRenderGroup` class allows you to manage animations and updates across
Expand Down Expand Up @@ -110,11 +120,9 @@ renderGroup.filter((mark) => mark.id % 2 == 0).animate('color');
```

You can dynamically add and remove marks from a render group using the `addMark`
and `deleteMark` methods. However, using these methods directly typically only
works well when there is no risk of overwriting marks with the same ID. For use
cases in which the same mark might need to be reused (e.g. animating connection
lines between points when hovering), it's best to use a `StageManager` (see
[Staging Marks]({{ site.baseurl }}/pages/04-staging)).
and `deleteMark` methods (which take a Mark as argument), or the `add` and `delete`
methods (which take an ID as argument and assume all marks have a unique ID).
For more details, see [Animating Mark Entry and Exit]({{ site.baseurl }}/pages/04-staging).

## Controlling Attribute Computation

Expand All @@ -123,7 +131,7 @@ expensive function to compute attribute values, Counterpoint allows you the
flexibility to decide how and when attribute computation should be performed.
Understanding when your value function will be run depends both on the options
listed below and the behavior of the ticker instance you are using (see
[Configuring Ticker Behavior]({{ site.baseurl }}/pages/07-optimizing-performance#configuring-ticker-behavior)).
[Configuring Ticker Behavior]({{ site.baseurl }}/pages/03-animation-timing#tickers)).

By default, all attributes with value functions are recomputed whenever their
`get()` or `advance()` methods are called. This likely means that when the canvas
Expand All @@ -149,26 +157,180 @@ of calls to potentially expensive functions:
does change, you can call `updateTransform` on the render group, mark, or
attribute.

## API Reference
## Representing Your Data

### Attribute
While Attributes represent potentially animatable properties of your marks, you
may also want to keep track of some fixed data for each mark or tie a mark to your
internal representation.

<h4 id="Attribute-constructor"><code>constructor(value | valueFn | options)</code></h4>
Let's say we are creating a network visualization. We may have an array of nodes
that we want to represent with marks. We can associate each mark with its corresponding
node using the `represented` property and the `representing` method:
```javascript
let marks = new MarkRenderGroup(
nodes.map((node) => new Mark(node.id, {
// populate attributes...
}).representing(node))
);
// later, get the represented node for a given mark:
let node = mark.represented;
```

Constructs an `Attribute` using either a single value, value function, or a
dictionary of options. If using an options dictionary, the available options are
listed below:
## Event Listeners

To help you organize your code, render groups and marks provide a lightweight
event dispatching and handling mechanism. This allows you to specify how marks
should visually respond to state changes independently of the state changes
themselves.

For example, let's say we want to implement different types of animations when
a user clicks each of the marks below.

<div>
<canvas id="event-listen-canvas" style="width: 400px; height: 300px; border: 1px solid #999;"></canvas>
<script>
import('https://cdn.jsdelivr.net/npm/counterpoint-vis@latest/dist/counterpoint-vis.es.js').then(({ Mark, Ticker, MarkRenderGroup, PositionMap }) => {
const canvas = document.getElementById("event-listen-canvas");
canvas.width = canvas.offsetWidth * window.devicePixelRatio;
canvas.height = canvas.offsetHeight * window.devicePixelRatio;
let scaleMark = new Mark('scale', {
x: 50,
y: 150,
w: 40,
h: 40,
rotation: 0,
alpha: 1.0,
color: 'salmon'
}).onEvent('click', async (mark) => {
await mark.animateTo('w', 60).animateTo('h', 60).wait(['w', 'h']);
mark.animateTo('w', 40).animateTo('h', 40);
});
let fadeMark = new Mark('fade', {
x: 150,
y: 150,
w: 40,
h: 40,
rotation: 0,
alpha: 1.0,
color: 'salmon'
}).onEvent('click', async (mark) => {
await mark.animateTo('alpha', 0.3).wait('alpha');
mark.animateTo('alpha', 1.0);
});
let rotateMark = new Mark('rotate', {
x: 250,
y: 150,
w: 40,
h: 40,
rotation: 0,
alpha: 1.0,
color: 'salmon'
}).onEvent('click', async (mark) => {
await mark.animateTo('rotation', mark.data('rotation') + 2 * Math.PI).wait('rotation');
mark.setAttr('rotation', 0);
});
let colorMark = new Mark('color', {
x: 350,
y: 150,
w: 40,
h: 40,
rotation: 0,
alpha: 1.0,
color: 'salmon'
}).onEvent('click', async (mark) => {
await mark.animateTo('color', 'steelblue').wait('color');
mark.animateTo('color', 'salmon');
});
let marks = new MarkRenderGroup([scaleMark, fadeMark, rotateMark, colorMark]).configure({
animationDuration: 500
});
let positionMap = new PositionMap().add(marks);

function draw() {
const ctx = canvas.getContext('2d');

// scaling for 2x devices
ctx.resetTransform();
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
ctx.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight);

marks.stage.forEach((mark) => {
let { x, y, w, h, rotation, alpha, color } = mark.get();
ctx.save();
ctx.fillStyle = color;
ctx.globalAlpha = alpha;
ctx.translate(x, y);
ctx.rotate(rotation);
ctx.fillRect(-w * 0.5, -h * 0.5, w, h);
ctx.font = "14pt 'Helvetica Neue'";
ctx.textAlign = 'center';
ctx.fillText(mark.id, 0, -h - 14);
ctx.restore();
});
}

let ticker = new Ticker(marks).onChange(draw);

canvas.addEventListener('click', (e) => {
let location = [
e.pageX - canvas.getBoundingClientRect().left,
e.pageY - canvas.getBoundingClientRect().top,
];
let nearest = positionMap.marksNear(location, 300);
if (nearest.length > 0) {
nearest[0].dispatch('click');
}
});

draw();
});
</script>
</div>

We can register each mark to listen for the 'click' event using the `onEvent`
method:

| Option | Description |
|:-------|:-----------:|
| `value` | The value of the attribute, if it is not updated using a dynamic function. |
| `valueFn` | A function that takes the attribute's compute argument and returns the attribute's value. This overrides a static `value` property value. |
| `computeArg` | An argument to be passed to the attribute's `valueFn` and `transform` functions. If `undefined`, the attribute itself is passed as the argument. (This only applies to attributes *not* being used inside `Mark` instances. Marks pass themselves as their attributes' compute arguments.) |
| `transform` | A function that transforms the value of the attribute before being returned. It should take the raw attribute value and (optionally) the attribute's compute argument, and return a transformed value. |
| <span id="Attribute-recompute"></span>`recompute` | Defines the behavior of the attribute's computation when specified using a value function. The default value of `AttributeRecompute.DEFAULT` causes the value function to be called every time `get()`, `compute()`, or `animate()` is called. If set to `AttributeRecompute.ALWAYS`, the value function is called every time the `advance()` method is called (i.e. every tick). If set to `AttributeRecompute.WHEN_UPDATED`, it will only be called when `compute()` or `animate()` is called. See <a href="#controlling-attribute-computation">Controlling Attribute Computation</a> for more details. |
| <span id="Attribute-cacheTransform"></span>`cacheTransform` | If `true`, specifies that the transformed value should be cached and reused when the raw value does not change (suitable when the transform function is fixed). If `false`, specifies that the transform should be rerun every time the value is requested - suitable when the transform function's behavior may change from frame to frame. When the value is cached, the transform can be updated by calling `updateTransform()` on the attribute. |
```javascript
let scaleMark = new Mark('scale', {
x: 50, y: 150, w: 40, h: 40,
rotation: 0,
alpha: 1.0,
color: 'salmon'
}).onEvent('click', async (mark, details) => {
// the details object is unused in this example, but could represent any information
await mark.animateTo('w', 60).animateTo('h', 60).wait(['w', 'h']);
mark.animateTo('w', 40).animateTo('h', 40);
});
let fadeMark = new Mark('fade', {
x: 150, y: 150, w: 40, h: 40,
rotation: 0,
alpha: 1.0,
color: 'salmon'
}).onEvent('click', async (mark, details) => {
await mark.animateTo('alpha', 0.3).wait('alpha');
mark.animateTo('alpha', 1.0);
});
// ... similar for the other marks
let rotateMark = ...
let colorMark = ...
let marks = new MarkRenderGroup([scaleMark, fadeMark, rotateMark, colorMark]);
```

### Mark
Then, in a click event handler, we can use the `PositionMap` class to identify
which mark was clicked (see [Interaction and Pan/Zoom]({{ site.baseurl }}/pages/05-interaction#retrieving-marks-by-position) for more details). We dispatch the
click event to the clicked mark:

### MarkRenderGroup
```javascript
clickedMark.dispatch('click');
```

To pass detail information to the mark in the event handler, we can simply add
an arbitrary details object as a second argument to the `dispatch` method.

> **TIP: Render Group-Level Events**
> You can define similar event handlers on an entire render group at once, using
> the `onEvent` method of `MarkRenderGroup`. The listener function takes the same
> arguments, a `Mark` object and an arbitrary details object.
>
{: .block-tip }
Loading

0 comments on commit cfd7f0b

Please sign in to comment.