Interactivity is a very important feature of Diagramatics. It allows you to add interactivity to your diagrams, and make them more engaging.
The interactive objects in Diagramatics are slider, locator, and label.
Setting up Interactivity
In the html file, what you need to have is a div to hold the interactive controls, and also to link the css to style the control elements.
*In the example below, we import Diagramatics as a package. But you can also import it from the
CDN
index.html
<!DOCTYPE html>
<html>
<head>
<!-- optional css for interactive controls -->
<link href="diagramatics/css/diagramatics.css" rel="stylesheet">
</head>
<body>
<!-- svg component to draw the diagram -->
<svg id="mysvg"></svg>
<!-- optional div to hold interactive controls -->
<div id="controldiv"></div>
</body>
<script src="index.js" type="module"></script>
</html>
In the javascript, you need to create the interactive object, let's call it int. You need to pass in the control div and the svg element.
index.js
// import the necessary functions from the library
import {square, draw_to_svg, diagram_combine, V2, Interactive} from 'diagramatics'
// get the svg and control element
let mysvg = document.getElementById('mysvg');
let controldiv = document.getElementById('controldiv');
// define the `draw` helper function
let draw = (...diagrams) => {
draw_to_svg(mysvg, diagram_combine(...diagrams));
};
// create the interactive object
let int = new Interactive(controldiv, mysvg);
// ================= build the diagram objects ============================
int.draw_function = (inp) => {
let x = inp['x'];
let big_sq = square(10).fill();
let small_sq = square(2).fill('red').translate(V2(x,0));
draw(big_sq, small_sq);
}
int.slider('x', -10, 10, 0);
int.draw();
Slider
Slider allows you to select a value from a range.
// create the draw_function that will be run each time the interactive control is changed
int.draw_function = (inp) => {
// read the value of the variable `x`
let x = inp['x'];
let big_sq = square(40);
let small_sq = square(10).fill('blue').position(V2(x, 0));
draw(big_sq, small_sq);
}
// create the slider
int.slider('x', -20, 20, 0);
// do the initial draw
// * without the initial draw, the svg object will be blank
// and will only be drawn after the slider has been moved
int.draw();
Interactive.slider (
variable_name
:
string , min
:
number , max
:
number , value
:
number , step?
:
number , time?
:
number , display_format_func?
:
formatFunction )
// create the draw_function that will be run each time the interactive control is changed
int.draw_function = (inp) => {
// read the value of the variable `x`
let x = inp['x'];
let big_sq = square(40);
let small_sq = square(10).fill('blue').position(V2(x, 0));
draw(big_sq, small_sq);
}
// create the slider
int.slider('x', -20, 20, 0);
// do the initial draw
// * without the initial draw, the svg object will be blank
// and will only be drawn after the slider has been moved
int.draw();
Locator
Locator allows you to select a position in a 2D space.
You can have a free locator, or a locator that is constrained to a track.
If you don't define the track_diagram parameter, then the locator will be free.
// create the draw_function that will be run each time the interactive control is changed
int.draw_function = (inp) => {
// read the value of the variable `p`
let p = inp['p'];
let big_sq = square(40);
let small_sq = square(10).fill('lightgray').position(p);
draw(big_sq, small_sq);
}
// create the locator
int.locator('p', V2(0, 0), 2, 'blue');
// do the initial draw
int.draw();
int.locator_initial_draw();
// create the draw_function that will be run each time the interactive control is changed
int.draw_function = (inp) => {
// read the value of the variable `p`
let p = inp['p'];
let big_sq = square(40);
let small_sq = square(10).fill('lightgray').position(p);
draw(big_sq, small_sq);
}
// create the locator
int.locator('p', V2(0, 0), 2, 'blue');
// do the initial draw
int.draw();
int.locator_initial_draw();
The locator will snap to the nearest point on the track.
// create the track for the locator
// you have to define it outside the `draw_function`
let pent = regular_polygon(5, 18);
// create the draw_function that will be run each time the interactive control is changed
int.draw_function = (inp) => {
// read the value of the variable `p`
let p = inp['p'];
let big_sq = square(40).strokedasharray([5]);
let small_sq = square(5).fill('lightgray').position(p);
draw(big_sq, pent, small_sq);
}
// create the locator
int.locator('p', V2(0, 0), 2, 'blue', pent);
// do the initial draw
int.draw();
int.locator_initial_draw();
// create the track for the locator
// you have to define it outside the `draw_function`
let pent = regular_polygon(5, 18);
// create the draw_function that will be run each time the interactive control is changed
int.draw_function = (inp) => {
// read the value of the variable `p`
let p = inp['p'];
let big_sq = square(40).strokedasharray([5]);
let small_sq = square(5).fill('lightgray').position(p);
draw(big_sq, pent, small_sq);
}
// create the locator
int.locator('p', V2(0, 0), 2, 'blue', pent);
// do the initial draw
int.draw();
int.locator_initial_draw();
If you want to have more point on the track, you can use the subdivide modifier (mod.subdivide).
// create the track for the locator
// you have to define it outside the `draw_function`
// resample so you have more point to snap to
let pent = regular_polygon(5, 18).apply(mod.subdivide(20));
// create the draw_function that will be run each time the interactive control is changed
int.draw_function = (inp) => {
// read the value of the variable `p`
let p = inp['p'];
let big_sq = square(40).strokedasharray([5]);
let small_sq = square(5).fill('lightgray').position(p);
draw(big_sq, pent, small_sq);
}
// create the locator
int.locator('p', V2(0, 0), 2, 'blue', pent);
// do the initial draw
int.draw();
int.locator_initial_draw();
// create the track for the locator
// you have to define it outside the `draw_function`
// resample so you have more point to snap to
let pent = regular_polygon(5, 18).apply(mod.subdivide(20));
// create the draw_function that will be run each time the interactive control is changed
int.draw_function = (inp) => {
// read the value of the variable `p`
let p = inp['p'];
let big_sq = square(40).strokedasharray([5]);
let small_sq = square(5).fill('lightgray').position(p);
draw(big_sq, pent, small_sq);
}
// create the locator
int.locator('p', V2(0, 0), 2, 'blue', pent);
// do the initial draw
int.draw();
int.locator_initial_draw();
Label
Label allows you to display a value. You can set the value of the label using int.set(varname : string, value : any).
int.draw_function = (inp) => {
// read the value of the variable `p`
let p = inp['p'];
let r = p.length();
let sq = square(2);
let circ = circle(r).fill('lightgray');
// calculate the area of the circle
let area = Math.PI * r * r;
int.set('A', area);
draw(sq, circ);
}
int.label('A',0);
int.locator('p', Vdir(to_radian(30)).scale(0.3), 0.1, 'blue');
int.draw();
int.locator_draw();
Interactive.label(
variable_name
:
string , value
:
any , display_format_func?
:
formatFunction )
int.draw_function = (inp) => {
// read the value of the variable `p`
let p = inp['p'];
let r = p.length();
let sq = square(2);
let circ = circle(r).fill('lightgray');
// calculate the area of the circle
let area = Math.PI * r * r;
int.set('A', area);
draw(sq, circ);
}
int.label('A',0);
int.locator('p', Vdir(to_radian(30)).scale(0.3), 0.1, 'blue');
int.draw();
int.locator_draw();
Custom external interaction
You can also create your own interactive object and interact with Diagramatics using int.set() and int.get().
For example, you can create a custom button.
let button_l = document.getElementById('custom-button-l');
let button_r = document.getElementById('custom-button-r');
int.draw_function = (inp) => {
// read the value of the variable `x`
let x = inp['x'];
let big_sq = square(40);
let small_sq = square(10).fill('blue').position(V2(x, 0));
draw(big_sq, small_sq);
}
int.label('x', 0);
int.draw();
// setup custom behaviour
button_l.onclick = () => {
int.set('x', int.get('x') - 1);
int.draw();
}
button_r.onclick = () => {
int.set('x', int.get('x') + 1);
int.draw();
}
let button_l = document.getElementById('custom-button-l');
let button_r = document.getElementById('custom-button-r');
int.draw_function = (inp) => {
// read the value of the variable `x`
let x = inp['x'];
let big_sq = square(40);
let small_sq = square(10).fill('blue').position(V2(x, 0));
draw(big_sq, small_sq);
}
int.label('x', 0);
int.draw();
// setup custom behaviour
button_l.onclick = () => {
int.set('x', int.get('x') - 1);
int.draw();
}
button_r.onclick = () => {
int.set('x', int.get('x') + 1);
int.draw();
}
Toggle Button
*introduced in v1.2.0
let sq = square(40);
let button_off = square(10).fill('lightgrey');
let button_on = square(10).fill('blue');
// example, binary (boolean) data to decimal string
function bin_to_decstr(x2,x1,x0){
let val = 0;
if (x2) val += 4;
if (x1) val += 2;
if (x0) val += 1;
return String(val);
}
int.draw_function = (inp) => {
let b0 = inp['b0'];
let b1 = inp['b1'];
let b2 = inp['b2'];
let decstr = bin_to_decstr(b2,b1,b0);
let text = textvar(decstr).fontsize(100).translate(V2(0,5));
draw(sq, text);
}
let p0 = V2( 12, -12);
let p1 = V2( 0, -12);
let p2 = V2(-12, -12);
int.button_toggle('b0', button_on.position(p0), button_off.position(p0), false);
int.button_toggle('b1', button_on.position(p1), button_off.position(p1), false);
int.button_toggle('b2', button_on.position(p2), button_off.position(p2), false);
int.draw();
Interactive.button_toggle (
name
:
string , diagram_on
:
Diagram , diagram_off
:
Diagram , state
:
boolean = false )
let sq = square(40);
let button_off = square(10).fill('lightgrey');
let button_on = square(10).fill('blue');
// example, binary (boolean) data to decimal string
function bin_to_decstr(x2,x1,x0){
let val = 0;
if (x2) val += 4;
if (x1) val += 2;
if (x0) val += 1;
return String(val);
}
int.draw_function = (inp) => {
let b0 = inp['b0'];
let b1 = inp['b1'];
let b2 = inp['b2'];
let decstr = bin_to_decstr(b2,b1,b0);
let text = textvar(decstr).fontsize(100).translate(V2(0,5));
draw(sq, text);
}
let p0 = V2( 12, -12);
let p1 = V2( 0, -12);
let p2 = V2(-12, -12);
int.button_toggle('b0', button_on.position(p0), button_off.position(p0), false);
int.button_toggle('b1', button_on.position(p1), button_off.position(p1), false);
int.button_toggle('b2', button_on.position(p2), button_off.position(p2), false);
int.draw();
Click Button
*introduced in v1.2.0
// click button doesn't have a state, so it can't be accessed
// using `int.get()` or `inp[name]` inside `int.draw_function`
let sq = square(40);
let button = square(10).fill('lightgrey');
let button_pressed = square(10).fill('blue');
draw(sq);
const callback = () => {
alert('button clicked');
}
int.button_click('b', button, button_pressed, callback);
int.draw();
Interactive.button_click (
name
:
string , diagram
:
Diagram , diagram_pressed
:
Diagram , callback
:
() => any )
// click button doesn't have a state, so it can't be accessed
// using `int.get()` or `inp[name]` inside `int.draw_function`
let sq = square(40);
let button = square(10).fill('lightgrey');
let button_pressed = square(10).fill('blue');
draw(sq);
const callback = () => {
alert('button clicked');
}
int.button_click('b', button, button_pressed, callback);
int.draw();
Custom SVG Object
*introduced in v1.2.0
// prepare the diagram
let sq = square(20);
let circ = circle(5).fill('blue').stroke('white');
let obj = circ.combine(circ.translate(V2(3,0)))
.move_origin('center-center').position(V2(0,0));
draw(sq);
// `int.custom_object` returns the html SVGSVGElement
let elem = int.custom_object('cust_obj_id', ['random_class'], obj);
// the id and classlist will be added to the element
// you can then modify the element using the usual javascript DOM manipulation
elem.style.cursor = 'pointer';
elem.onclick = () => {
alert('hi');
}
elem.onmouseenter = () => {
elem.style.fillOpacity = 0.5;
}
elem.onmouseleave = () => {
elem.style.fillOpacity = 1;
}
// don't forget to draw
int.draw()
// prepare the diagram
let sq = square(20);
let circ = circle(5).fill('blue').stroke('white');
let obj = circ.combine(circ.translate(V2(3,0)))
.move_origin('center-center').position(V2(0,0));
draw(sq);
// `int.custom_object` returns the html SVGSVGElement
let elem = int.custom_object('cust_obj_id', ['random_class'], obj);
// the id and classlist will be added to the element
// you can then modify the element using the usual javascript DOM manipulation
elem.style.cursor = 'pointer';
elem.onclick = () => {
alert('hi');
}
elem.onmouseenter = () => {
elem.style.fillOpacity = 0.5;
}
elem.onmouseleave = () => {
elem.style.fillOpacity = 1;
}
// don't forget to draw
int.draw()
Drag and Drop
*introduced in v1.2.0
// create a Drag and Drop container object
// need to be used in conjunction with `int.dnd_draggable`
Interactive.dnd_container(
name
:
string , diagram
:
Diagram )
// create a Drag and Drop container object
// need to be used in conjunction with `int.dnd_draggable`
// create a Drag and Drop draggable object
// prepare the diagrams
let target_box = square(5).fill('lightgrey');
let target0 = target_box.position(V2(-6,0));
let target1 = target_box.position(V2(0,0));
let target2 = target_box.position(V2(6,0));
let texta = text('a')
let bga = circle(2).fill('blue');
let sourcea = diagram_combine(texta, bga).position(V2(-3,-6));
let textb = text('b')
let bgb = circle(2).fill('blue');
let sourceb = diagram_combine(textb, bgb).position(V2(3,-6));
// create a bounding box rectangle object so that we know the size of the diagram
// if the DnD objects is contained within another diagram, you can simply draw that diagram instead
// without the need to create a bounding box
let dnd_objects = diagram_combine(sourcea, sourceb, target0, target1, target2);
let dnd_rect = rectangle_corner(...dnd_objects.bounding_box()).stroke('none').fill('none');
draw(dnd_rect);
int.dnd_container('t0', target0)
int.dnd_container('t1', target1)
int.dnd_container('t2', target2)
int.dnd_draggable('a', sourcea, sourcea.fill('lightgrey'))
int.dnd_draggable('b', sourceb, sourceb.fill('lightgrey'))
int.drag_and_drop_initial_draw();
// create a Drag and Drop draggable object
// prepare the diagrams
let target_box = square(5).fill('lightgrey');
let target0 = target_box.position(V2(-6,0));
let target1 = target_box.position(V2(0,0));
let target2 = target_box.position(V2(6,0));
let texta = text('a')
let bga = circle(2).fill('blue');
let sourcea = diagram_combine(texta, bga).position(V2(-3,-6));
let textb = text('b')
let bgb = circle(2).fill('blue');
let sourceb = diagram_combine(textb, bgb).position(V2(3,-6));
// create a bounding box rectangle object so that we know the size of the diagram
// if the DnD objects is contained within another diagram, you can simply draw that diagram instead
// without the need to create a bounding box
let dnd_objects = diagram_combine(sourcea, sourceb, target0, target1, target2);
let dnd_rect = rectangle_corner(...dnd_objects.bounding_box()).stroke('none').fill('none');
draw(dnd_rect);
int.dnd_container('t0', target0)
int.dnd_container('t1', target1)
int.dnd_container('t2', target2)
int.dnd_draggable('a', sourcea, sourcea.fill('lightgrey'))
int.dnd_draggable('b', sourceb, sourceb.fill('lightgrey'))
int.drag_and_drop_initial_draw();
You can access the draggable position by using it's name inside int.draw_function.
NOTE: it's not possible to int.set() the draggable position.
let sq = square(40);
let containerbox = square(10).fill('lightgrey');
let cont0 = containerbox.position(V2(-10,-10));
let cont1 = containerbox.position(V2(10,10));
let cont2 = containerbox.position(V2(10,-10));
let cont3 = containerbox.position(V2(-10,10));
let obj = circle(4).fill('blue').position(cont3.origin);
int.draw_function = (inp) => {
let a = inp['a'];
let l = line(V2(0,0), a);
draw(sq, l)
}
int.dnd_container('c0', cont0);
int.dnd_container('c1', cont1);
int.dnd_container('c2', cont2);
int.dnd_draggable('a', obj, cont3);
int.draw();
int.drag_and_drop_initial_draw();
let sq = square(40);
let containerbox = square(10).fill('lightgrey');
let cont0 = containerbox.position(V2(-10,-10));
let cont1 = containerbox.position(V2(10,10));
let cont2 = containerbox.position(V2(10,-10));
let cont3 = containerbox.position(V2(-10,10));
let obj = circle(4).fill('blue').position(cont3.origin);
int.draw_function = (inp) => {
let a = inp['a'];
let l = line(V2(0,0), a);
draw(sq, l)
}
int.dnd_container('c0', cont0);
int.dnd_container('c1', cont1);
int.dnd_container('c2', cont2);
int.dnd_draggable('a', obj, cont3);
int.draw();
int.drag_and_drop_initial_draw();
// retrieve the state data from the DnD objects
// TODO: write example for this
// retrieve the state data from the DnD objects
// TODO: write example for this
Display Format
By default, the label and slider will display the value as `italic_name` = `value`. You can change the display format by passing in a function to the display_format_func parameter.
The formatFunction have the following signature:
(name : string, value : any, prec? : number) => string