Skip to content

Commit 311acaf

Browse files
committed
Added lambda animations, edge.traverse and examples
1 parent b821133 commit 311acaf

File tree

16 files changed

+11747
-81
lines changed

16 files changed

+11747
-81
lines changed

README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,28 @@
1010

1111
## Installation
1212

13-
To install the library using pip:
13+
Python 3.6 or higher is required.
14+
15+
AlgorithmX can be installed using pip:
1416

1517
```bash
1618
pip install algorithmx
1719
```
1820

19-
To enable the Jupyter widget in classic notebooks:
21+
or using conda:
2022

2123
```bash
22-
jupyter nbextension enable --sys-prefix --py algorithmx
24+
conda install algorithmx
2325
```
2426

25-
To enable in JupyterLab:
27+
The Jupyter widget will typically be enabled by default. However, if you installed using pip with notebook version <5.3, you will have to manually enable it using:
2628

2729
```bash
28-
# if you haven't used widgets before
29-
jupyter labextension install @jupyter-widgets/jupyterlab-manager
30-
31-
jupyter labextension install algorithmx-jupyter
30+
jupyter nbextension enable --sys-prefix --py algorithmx
3231
```
3332

33+
with the <a href="https://jupyter-notebook.readthedocs.io/en/stable/extending/frontend_extensions.html#installing-and-enabling-extensions">appropriate flag</a>.
34+
3435
## Example Usage
3536

3637
If you wish to use the library through a HTTP/WebSocket server, follow the template below:

algorithmx/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
version_info = (1, 0, 0, 'beta', 3)
1+
version_info = (1, 0, 0, 'beta', 4)
22
__version__ = '.'.join(map(str, version_info))

algorithmx/graphics/CanvasSelection.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ def nodes(self, ids: Iterable[Union[str, int]]) -> NodeSelection:
5858
5959
:return: A new selection corresponding to the given nodes.
6060
"""
61-
node_context = create_child_context(parent=self._context, name='nodes', ids=[str(k) for k in ids], data=ids)
61+
node_context = create_child_context(parent=self._context, name='nodes',
62+
ids=[str(k) for k in ids], data=list(ids))
6263
return NodeSelection(node_context)
6364

6465
def edge(self, edge: EdgeSelector) -> None:
@@ -94,7 +95,7 @@ def edges(self, edges: Iterable[EdgeSelector]) -> None:
9495
initattr.append({'source': str(edge[0]), 'target': str(edge[1])})
9596

9697
edge_context = create_child_context(parent=self._context, name='edges',
97-
ids=edge_ids, data=edges, initattr=initattr)
98+
ids=edge_ids, data=list(edges), initattr=initattr)
9899
return EdgeSelection(edge_context)
99100

100101
def label(self, id: Union[str, int] = 'title') -> LabelSelection:
@@ -124,7 +125,8 @@ def size(self: S, size: ElementArg[Tuple[NumExpr, NumExpr]]) -> S:
124125
"""
125126
/**
126127
Sets the width and height of the canvas. This will determine the coordinate system, and will update the ``width`` and
127-
``height`` attributes of the main SVG element, unless otherwise specified with :meth:`~svgattr`.
128+
``height`` attributes of the main SVG element, unless otherwise specified with :meth:`~svgattr`. Note that
129+
size is not animated by default.
128130
129131
:param size: A (width, height) tuple describing the size of the canvas.
130132
:type size: :data:`~graphics.types.ElementArg`\\[Tuple[:data:`~graphics.types.NumExpr`, :data:`~graphics.types.NumExpr`]]

algorithmx/graphics/EdgeSelection.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
1-
from typing import Union, Tuple, Iterable, TypeVar
1+
from typing import Union, Tuple, Iterable, Optional, TypeVar
22

33
from .Selection import Selection
44
from .LabelSelection import LabelSelection
55
from .context import create_child_context
66
from .types import ElementArg, NumExpr
7-
from .utils import attr_event
7+
from .utils import attr_event, update_animation
88

99
S = TypeVar('S', bound='EdgeSelection')
1010

1111
class EdgeSelection(Selection):
12+
def traverse(self: S, source: Optional[ElementArg[Union[str, int]]] = None) -> S:
13+
"""
14+
Sets the animation type to "traverse" (see :meth:`~graphics.Selection.animate`), and configures the node at which the traversal
15+
should begin. This will typically be followed by :meth:`~color`.
16+
17+
If no source is given, the first node in each edge tuple used to construct the selection will be used.
18+
If the source is not connected, the edge's actual source will be used.
19+
20+
:param source: The ID of the node at which the traversal animation should begin.
21+
:type source: Optional[:data:`~graphics.types.ElementArg`\\[Union[str, int]]]
22+
"""
23+
context = self._context.copy()
24+
new_source = lambda e, i: self._context.initattr[i]['source'] if source is None else source
25+
context.animation = update_animation(context, new_source,
26+
lambda d: {'type': 'traverse', 'data': {'source': str(d)}})
27+
return self.__class__(context)
28+
29+
1230
def label(self, id: Union[str, int] = 'weight') -> LabelSelection:
1331
"""
1432
Selects a single label, attached to the edge, by its ID.
@@ -64,8 +82,8 @@ def thickness(self: S, thickness: ElementArg[NumExpr]) -> S:
6482

6583
def color(self: S, color: ElementArg[str]) -> S:
6684
"""
67-
Sets color of the edge. Note that this can be animated with a traversal animation ("traverse" or
68-
"traverse-reverse", see :meth:`~graphics.Selection.animate`).
85+
Sets color of the edge. Note that this can be animated with a traversal animation (see :meth:`~traverse`).
86+
The default duration of color animations is ``0.5``, to make traversals clearer.
6987
7088
:param color: A CSS color string.
7189
:type color: :data:`~graphics.types.ElementArg`\\[str]

algorithmx/graphics/Selection.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from .context import SelectionContext, create_child_context
55
from .EventHandler import DispatchEventType
6-
from .utils import attr_event, queue_event, call_element_fn, is_iterable
6+
from .utils import attr_event, queue_event, update_animation, call_element_fn, is_iterable
77
from .types import ElementArg, ElementFn
88

99
S = TypeVar('S', bound='Selection')
@@ -93,42 +93,41 @@ def eventQ(self: S, queue: Union[str, int, None] = 'default') -> S:
9393
context.queue = str(queue)
9494
return self.__class__(context)
9595

96-
def animate(self: S, animation_type: str) -> S:
96+
def animate(self: S, animation_type: ElementArg[str]) -> S:
9797
"""
9898
Configures the type of animation which should be used for all attribute changes triggered by the selection.
9999
100-
:param type: One of the following strings:
100+
:param animation_type: One of the following strings:
101101
102102
* "normal": The standard animation, applicable in most cases.
103103
* "scale": Animates the size of elements being added/removed.
104104
* "fade": Animates the opacity of elements being added/removed.
105105
* "scale-face": Animates both the size and opacity of elements being added/removed.
106-
* "traverse": Changes the color of edges using a traversal animation (from source to target).
107-
* "traverse-reverse": Changes the color of edges using a reversed traversal animation (from target to source).
106+
* "traverse": Changes the color of edges using a traversal animation.
108107
109-
:type animation_type: str
108+
:type animation_type: :data:`~graphics.types.ElementArg`\\[str]
110109
111110
:return: A new instance of the current selection using the specified animation type.
112111
"""
113112
context = self._context.copy()
114-
context.animation['type'] = animation_type
113+
context.animation = update_animation(context, animation_type, lambda d: {'type': d, 'data': {}})
115114
return self.__class__(context)
116115

117-
def duration(self: S, seconds: Union[int, float]) -> S:
116+
def duration(self: S, seconds: ElementArg[Union[int, float]]) -> S:
118117
"""
119-
Configures the duration of all animations triggered by the selection. A duration of 0 will ensure that changes
120-
occur immediately.
118+
Configures the duration of all animations triggered by the selection. A duration of ``0`` will ensure that changes
119+
occur immediately. The default duration is ``0.35``.
121120
122121
:param seconds: The animation duration, in seconds.
123-
:type seconds: Union[int, float]
122+
:type seconds: :data:`~graphics.types.ElementArg`\\[Union[int, float]]
124123
125124
:return: A new instance of the current selection using the specified animation duration.
126125
"""
127126
context = self._context.copy()
128-
context.animation['duration'] = seconds
127+
context.animation = update_animation(context, seconds, lambda d: {'duration': d})
129128
return self.__class__(context)
130129

131-
def ease(self: S, ease: str) -> S:
130+
def ease(self: S, ease: ElementArg[str]) -> S:
132131
"""
133132
Configures the ease function used in all animations triggered by the selection. This will affect the way attributes
134133
transition from one value to another. More information is available here: `<https://github.com/d3/d3-ease>`_.
@@ -146,38 +145,39 @@ def ease(self: S, ease: str) -> S:
146145
"back", "back-in", "back-out", "back-in-out",
147146
"bounce", "bounce-in", "bounce-out", "bounce-in-out".
148147
149-
:type ease: str
148+
:type ease: :data:`~graphics.types.ElementArg`\\[str]
150149
151150
:return: A new instance of the current selection using the specified animation ease.
152151
"""
153152
context = self._context.copy()
154-
context.animation['ease'] = ease
153+
context.animation = update_animation(context, ease, lambda d: {'ease': d})
155154
return self.__class__(context)
156155

157-
def highlight(self: S, seconds: Optional[Union[int, float]] = None) -> S:
156+
def highlight(self: S, seconds: Optional[ElementArg[Union[int, float]]] = None) -> S:
158157
"""
159158
Returns a new selection through which all attribute changes are temporary. This is typically used to draw attention
160159
to a certain element without permanently changing its attributes.
161160
162161
:param seconds: The amount of time attributes should remain 'highlighted', in seconds, before
163-
changing back to their original values. If not provided, an appropriate default will be used.
164-
:type seconds: Optional[Union[int, float]]
162+
changing back to their original values. Defaults to ``0.5``.
163+
:type seconds: Optional[:data:`~graphics.types.ElementArg`\\[Union[int, float]]]
165164
166165
:return: A new instance of the current selection, where all attribute changes are temporary.
167166
"""
168167
context = self._context.copy()
169168
context.highlight = True
170169
if seconds is not None:
171-
context.animation['linger'] = seconds
170+
context.animation = update_animation(context, seconds, lambda d: {'linger': d})
172171
return self.__class__(context)
173172

174-
def data(self: S, data: Union[Iterable[Any], ElementFn[Any]]) -> S:
173+
def data(self: S, data: Union[Iterable[Any], ElementFn[Any], None]) -> S:
175174
"""
176175
Binds the selection to a list of data values. This will decide the arguments provided whenever an attribute is
177176
configured using a function (see :data:`~graphics.types.ElementArg`).
178177
179178
:param data: An iterable container of values to use as the data of this selection, which should have the same length as the number
180179
of elements in the selection. Alternatively, a function (:data:`~graphics.types.ElementFn`) transforming the selection's previous data.
180+
Use ``null`` to unbind the selection from its data, in which case the selection will fall back on its parent's data.
181181
:type: data: Union[Iterable[Any], ElementFn[Any]]
182182
183183
:raise Exception: If the length of the data does not equal the number of elements in the selection.

algorithmx/graphics/utils.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@ def create_parent_attr(sel: SelectionContext, arg, attr):
2828
else:
2929
return {sel.name: {k: get_attr_entry(sel, arg, attr, i) for i, k in enumerate(sel.ids)}}
3030

31-
def get_full_attributes(sel: SelectionContext, arg, attr):
31+
def create_full_attr(sel: SelectionContext, arg, attr):
3232
if sel.parent is None:
3333
return get_attr_entry(sel, arg, attr, 0)
3434
else:
35-
return get_full_attributes(sel.parent, arg, create_parent_attr(sel, arg, attr))
35+
return create_full_attr(sel.parent, arg, create_parent_attr(sel, arg, attr))
3636

3737

3838
def attr_event(sel: SelectionContext, arg, attr) -> Dict:
39-
full_attr = get_full_attributes(sel, arg, attr)
39+
full_attr = create_full_attr(sel, arg, attr)
4040

4141
return {
4242
'type': DispatchEventType.Highlight if sel.highlight else DispatchEventType.Update,
@@ -52,3 +52,22 @@ def queue_event(sel: SelectionContext, event_type: str,
5252
'queue': sel.queue,
5353
'data': {'queues': queue_list}
5454
}
55+
56+
def merge_dict_rec(a: Dict, b: Dict) -> Dict:
57+
new_dict = {}
58+
for k in a:
59+
if not k in b:
60+
new_dict[k] = a[k]
61+
elif type(a[k]) is dict and type(b[k]) is dict:
62+
new_dict[k] = merge_dict_rec(a[k], b[k])
63+
else:
64+
new_dict[k] = b[k]
65+
return {**b, **new_dict}
66+
67+
def update_animation(sel: SelectionContext, arg, attr) -> Dict:
68+
if (len(sel.animation) == 0 or (len(sel.animation) == 1 and '**' in sel.animation)) and not callable(arg):
69+
# optimization to minimize the amount of transmitted data
70+
return merge_dict_rec(sel.animation, {'**': attr(arg)})
71+
else:
72+
anim_attr = create_full_attr(sel, arg, lambda a: {'**': attr(a)})
73+
return merge_dict_rec(sel.animation, anim_attr)

algorithmx/jupyter/JupyterWidget.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ def subscribe(self, listener: Callable[[ReceiveEvent], Any]):
4646
def canvas(self) -> CanvasSelection:
4747
"""
4848
Creates a new :class:`~graphics.CanvasSelection` which will dispatch and receive events through the Jupyter
49-
widget.
49+
widget. The default canvas size is (400, 250).
5050
51-
Note that by default, you need to hold down the ``ctrl``/``cmd`` key to zoom in on the canvas
51+
Note that by default, you need to hold down the ``ctrl``/``cmd`` key to zoom in
5252
(see :meth:`~graphics.CanvasSelection.zoomkey`).
5353
"""
5454
return canvas_selection('_jupyter', self)

algorithmx/nbextension/static/extension.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ define(function() {
2424
window['requirejs'].config({
2525
paths: Object.assign({
2626
webcola: 'https://unpkg.com/webcola@^3.0.0/WebCola/cola.min',
27-
algorithmx: 'https://unpkg.com/algorithmx@latest/dist/algorithmx.min'
27+
algorithmx: 'https://unpkg.com/algorithmx@^1.0.0-beta.1/dist/algorithmx.min'
2828
}, d3Paths),
2929
shim: {
3030
algorithmx: {

algorithmx/server/algorithmx.html

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,9 @@
77
<title>AlgorithmX</title>
88
<script src="https://d3js.org/d3.v5.min.js"></script>
99
<script src="https://unpkg.com/webcola@^3.0.0/WebCola/cola.min.js"></script>
10-
<script src="https://unpkg.com/algorithmx@latest"></script>
11-
12-
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400" rel="stylesheet">
13-
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600" rel="stylesheet">
14-
<link href="https://fonts.googleapis.com/css?family=Quicksand" rel="stylesheet">
10+
<script src="https://unpkg.com/algorithmx@^1.0.0-beta.1"></script>
1511

12+
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300" rel="stylesheet">
1613
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css">
1714

1815
<style>

docs/source/_static/helper.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ window.addEventListener('load', function() {
2626
cache_require.config({
2727
paths: Object.assign({
2828
webcola: 'https://unpkg.com/webcola@^3.0.0/WebCola/cola.min',
29-
algorithmx: 'https://unpkg.com/algorithmx@latest/dist/algorithmx.min'
29+
algorithmx: 'https://unpkg.com/algorithmx@^1.0.0-beta.1/dist/algorithmx.min'
3030
}, d3Paths),
3131
shim: {
3232
algorithmx: {

0 commit comments

Comments
 (0)