Published
Edited
Mar 15, 2022
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
render = (dataOrShape, shapeFn = undefined) => myRender(bfjs.render(dataOrShape, shapeFn))
Insert cell
listData = mkList("foo", [1, 2, 3, 4, 5]);
Insert cell
Insert cell
render(listData, randomSetShapeFn);
Insert cell
Insert cell
Insert cell
render(M.text({contents: "Hello World!", fontSize: "18pt", }))
Insert cell
Insert cell
viewof ellipseCX = Inputs.range([10, 200], {value: 80, step: 5, label: "ellipse cx"})
Insert cell
render(M.ellipse({cx: ellipseCX, cy: 50, "fill": "firebrick"}));
Insert cell
render(M.rect({width: 100, height: 200, "fill": "steelblue"}));
Insert cell
Insert cell
Insert cell
viewof rect = view(M.rect({width: 100, height: 200, "fill": "steelblue"}));
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
render(createShape({
shapes: {
"hello": M.text({x: 0, contents: "Hello", fontSize: "18pt", }),
"world": M.text({x: 70, contents: "World!", fontSize: "18pt", }),
},
}));
Insert cell
render(createShape({
shapes: {
"s1": M.rect({x: 0, y: 30, width: 50, height: 80, fill: "steelblue", }),
"s2": M.rect({x: 70, width: 100, height: 100, fill: "firebrick", }),
},
}));
Insert cell
Primitive objects and groups are both shapes, which means you can put groups inside other groups! Like this:
Insert cell
viewof twoRects = view(createShape({
shapes: {
"s1": M.rect({x: 0, y: 30, width: 50, height: 80, fill: "steelblue", }),
"s2": M.rect({x: 70, width: 100, height: 100, fill: "firebrick", }),
},
}));
Insert cell
render(createShape({
shapes: {
"group": twoRects,
"world": M.text({x: 70, contents: "World!", fontSize: "18pt", }),
}
}))
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
render(createShape({
shapes: {
"s1": M.rect({width: 50, height: 80, fill: "steelblue", }),
"s2": M.rect({width: 100, height: 100, fill: "firebrick", }),
},
rels: {
"s1->s2": [C.hSpace(spacing), C.alignBottom],
},
}))
Insert cell
Insert cell
Insert cell
Insert cell
render(createShape({
shapes: {
"s1": M.rect({width: 50, height: 80, fill: "steelblue", }),
"s2": M.rect({width: 100, height: 100, fill: "firebrick", }),
},
rels: {
"s1->s2": [C.hSpace(20), C.alignBottom],
},
}))
Insert cell
Insert cell
render("coral", (color) => createShape({
shapes: {
"s1": M.rect({width: 50, height: 80, fill: "steelblue", }),
"s2": M.rect({width: 100, height: 100, fill: color, }),
},
rels: {
"s1->s2": [C.hSpace(20), C.alignBottom],
},
}))
Insert cell
Insert cell
render("coral", createShape({
shapes: {
"s1": M.rect({width: 50, height: 80, fill: "steelblue", }),
$$s2: (color) => M.rect({width: 100, height: 100, fill: color, }),
},
rels: {
"s1->s2": [C.hSpace(20), C.alignBottom],
},
}))
Insert cell
Insert cell
render({s1: "steelblue", s2: "coral"}, ({s1, s2}) => createShape({
shapes: {
"s1": M.rect({width: 50, height: 80, fill: s1, }),
"s2": M.rect({width: 100, height: 100, fill: s2, }),
},
rels: {
"s1->s2": [C.hSpace(20), C.alignBottom],
},
}))
Insert cell
Again we have the same readability problem as before. It's difficult to tell how the data gets mapped into shapes. So instead, we can be more precise using the `fields` field of the Bluefish record!
Insert cell
render({s1: "steelblue", s2: "coral"}, createShape({
shapes: {
"$s1$": (s1) => M.rect({width: 50, height: 80, fill: s1, }),
"$s2$": (s2) => M.rect({width: 100, height: 100, fill: s2, }),
},
rels: {
"s1->s2": [C.hSpace(20), C.alignBottom],
},
}))
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
render(mkList("marbles", _.range(1, numMarbles + 1)), marblesListShape);
Insert cell
mkList("marbles", _.range(1, numMarbles + 1))
Insert cell
Insert cell
marbleShape = createShape({
shapes: {
"circle": M.ellipse({ rx: 300 / 6, ry: 200 / 6, fill: "coral" }),
"$$label": (n) => M.text({ contents: n.toString(), fontSize: "24px" }),
},
rels: { "label->circle": [C.alignCenter, C.alignMiddle] },
});
Insert cell
marblesShape2 = createShape({
shapes: {
$0$: marbleShape,
$1$: marbleShape,
},
rels: {"0->1": [C.hSpace(5.), C.alignMiddle]},
})
Insert cell
render([1, 2], marblesShape2);
Insert cell
Insert cell
render([1, 2, 3], marblesShape2);
Insert cell
Insert cell
render([1], marblesShape2);
Insert cell
Insert cell
marblesShape3 = createShape({
shapes: {
$0$: marbleShape,
$1$: marbleShape,
$2$: marbleShape,
},
rels: {
"0->1": [C.hSpace(5.), C.alignMiddle],
"1->2": [C.hSpace(5.), C.alignMiddle],
},
})
Insert cell
Insert cell
Insert cell
marblesShapeSetAlmost = createShape({
shapes: {
$$marbles: marbleShape,
},
rels: {
"0->1": [C.hSpace(5.), C.alignMiddle],
"1->2": [C.hSpace(5.), C.alignMiddle],
},
})
Insert cell
But this doesn't quite work, because our relations don't refer to the right things anymore! So where do these mapped shapes go?

They are constructed as a *shape* inside the `$object`.
Insert cell
render([1, 2, 3], marblesShapeSetAlmost);
Insert cell
To specify constraints between shapes *inside* of other shapes, we can use paths like so.
Insert cell
marblesShapeSet = createShape({
shapes: {
$$marbles: marbleShape,
},
rels: {
"marbles/0->marbles/1": [C.hSpace(5.), C.alignMiddle],
"marbles/1->marbles/2": [C.hSpace(5.), C.alignMiddle],
},
})
Insert cell
render([1, 2, 3], marblesShapeSet);
Insert cell
Insert cell
render([1, 2, 3, 4], marblesShapeSet);
Insert cell
Insert cell
## Relations As Data

**TODO: probably should introduce references earlier, so I don't have to introduce them and also the idea of a relation.**
Insert cell
Insert cell
Insert cell
marblesList = ({
marbles: [1, 2, 3],
neighbors: [
{
curr: ref("../../marbles/0"),
next: ref("../../marbles/1"),
},
{
curr: ref("../../marbles/1"),
next: ref("../../marbles/2"),
}
]
})
Insert cell
marblesShapeList = createShape({
shapes: {
$marbles$: marbleShape, // renders _every_ marble in the set using marbleGlyph
$neighbors$: createShape({
shapes: {
$curr$: 'ref',
$next$: 'ref',
},
rels: { "curr->next": [C.hSpace(5.), C.alignMiddle] }
})
},
})
Insert cell
render(marblesList, marblesShapeList)
Insert cell
Insert cell
Insert cell
marblesShapeListTransposed = createShape({
shapes: {
$marbles$: marbleShape, // renders _every_ marble in the set using marbleGlyph
$neighbors$: createShape({
shapes: {
$curr$: 'ref',
$next$: 'ref',
},
rels: { "curr->next": [C.vSpace(5.), C.alignCenter] }
})
},
})
Insert cell
render(marblesList, marblesShapeListTransposed)
Insert cell
Insert cell
render(marblesList, marblesShapeListTransposedLink)
Insert cell
Insert cell
**TODO: put motivating example here of adding a bunch of circles and wanting them to be spaced evenly or something.**
Insert cell
ref = bfjs.ref;
Insert cell
Suppose we want to make a list of marbles.
We can start by writing something like this.
Insert cell
marblesSet = [1, 2, 3];
Insert cell
Insert cell
Insert cell
## Making a List Function
Lists are a pretty common data structure, so we can write some code to automatically produce this neighbor relation for us:
Insert cell
_ = require("lodash");
Insert cell
mkList = (name, xs) => ({
[name]: xs,
neighbors: xs.length < 2 ? [] :
_.zipWith(_.range(xs.length - 1), _.range(1, xs.length), (curr, next) => (
{
curr: ref(`../../${name}/${curr}`),
next: ref(`../../${name}/${next}`),
}
))
})
Insert cell
ptr = (obj) => ({ $ptr: true, value: obj })
Insert cell
mkListExperimental = (name, xs) => ({
[name]: xs,
neighbors: xs.length < 2 ? [] :
_.zipWith(xs.slice(1), xs.slice(-1), (curr, next) => ({
curr: ptr(curr),
next: ptr(next),
}))
})
Insert cell
{
const one = Object(1);
const two = Object(2);
const three = Object(3);
({
marbles: [one, two, three],
neighbors: [
{
curr: ptr(one),
next: ptr(two),
},
{
curr: ptr(two),
next: ptr(three),
},
]
})
}
Insert cell
{
const one = Object(1);
const two = Object(2);
const three = Object(3);
({
marbles: [one, two, three],
neighbors: [
{
curr: one,
next: two,
},
{
curr: two,
next: three,
},
]
})
}
Insert cell
/* play around with some input arrays here! */
mkList("marbles", [1, 2, 3]);
Insert cell
render(1, marbleShape);
Insert cell
render([1, 2], marblesShape);
Insert cell
// uh oh!
render([1, 2, 3], marblesShape);
Insert cell
marblesListShape = createShape({
shapes: {
$marbles$: marbleShape, // renders _every_ marble in the set using marbleGlyph
$neighbors$: createShape({
shapes: {
$curr$: 'ref',
$next$: 'ref',
},
rels: { "curr->next": [C.hSpace(5.), C.alignCenterY] }
})
},
})
Insert cell
render(mkList("marbles", [1, 2]), marblesListShape);
Insert cell
render(mkList("marbles", [1, 2, 3]), marblesListShape);
Insert cell
## Composition in Bluefish
Insert cell
someMarbles = render(mkList("marbles", [1, 2, 3]), marblesListShape);
Insert cell
render(createShape({
shapes: {
"someMarbles": M.nil()/* marblesListShape(mkList("marbles", [1, 2, 3])) */, /* uh oh! cannot currently put someMarbles here (even if rendering step is taken away!! too many hacks) */
}
}));
Insert cell
## Let's Make a Bar Chart!(?!)
Insert cell
/* https://vega.github.io/vega-lite/examples/bar.html */
data = [
{ "category": "A", "value": 28 }, { "category": "B", "value": 55 }, { "category": "C", "value": 43 },
{ "category": "D", "value": 91 }, { "category": "E", "value": 81 }, { "category": "F", "value": 53 },
{ "category": "G", "value": 19 }, { "category": "H", "value": 87 }, { "category": "I", "value": 52 }
];
Insert cell
bar = createShapeFn({
shapes: {
// this tick mark might be a relation glyph in the future
"tick": M.rect({ width: 1., height: 8., fill: "gray" })
},
fields: {
"category": (contents) => M.text({ contents, fontSize: "12px" }),
"value": (height) => M.rect({ width: 20, height, fill: "steelblue" }),
},
rels: {
"value->tick": [C.vSpace(5), C.vAlignCenter],
"tick->category": [C.vSpace(1), C.vAlignCenter],
},
})
Insert cell
render(data[0], bar)
Insert cell
Insert cell
Insert cell
bars = createShapeFn({
fields: {
bars: bar,
neighbors: createShapeFn({
// TODO: need to add the ability to access the children of a ref back for this!!
rels: { "curr->next": [C[alignment], C.hSpace(barSpacing)] }
})
}
});
Insert cell
render(mkList("bars", data), bars);
Insert cell
stackedData = [
{ "category": "A", "value": 28, "anotherValue": 42, },
{ "category": "B", "value": 55, "anotherValue": 42, },
{ "category": "C", "value": 43, "anotherValue": 42, },
{ "category": "D", "value": 91, "anotherValue": 42, },
{ "category": "E", "value": 81, "anotherValue": 42, },
{ "category": "F", "value": 53, "anotherValue": 42, },
{ "category": "G", "value": 19, "anotherValue": 42, },
{ "category": "H", "value": 87, "anotherValue": 42, },
{ "category": "I", "value": 52, "anotherValue": 42, },
];
Insert cell
stackedBar = createShapeFn({
shapes: {
// this tick mark might be a relation glyph in the future
"tick": M.rect({ width: 1., height: 8., fill: "gray" })
},
fields: {
"category": (contents) => M.text({ contents, fontSize: "12px" }),
"value": (height) => M.rect({ width: 20, height, fill: "steelblue" }),
"anotherValue": (height) => M.rect({ width: 20, height, fill: "coral" }),
},
rels: {
"anotherValue->value": [C.vSpace(0), C.vAlignCenter],
"value->tick": [C.vSpace(5), C.vAlignCenter],
"tick->category": [C.vSpace(1), C.vAlignCenter],
},
})
Insert cell
stackedBars = createShapeFn({
shapes: {
},
fields: {
"elements": stackedBar,
neighbors: createShapeFn({
// TODO: need to add the ability to access the children of a ref back for this!!
rels: { "curr->next": [C[alignment], C.hSpace(barSpacing)] }
})
}
});
Insert cell
render(mkList("elements", stackedData), stackedBars)
Insert cell
yAxis = createShapeFn({
shapes: {
"axisLine": M.rect({width: 2, height: 100, fill: "gray"}),
},
fields: {
"yTicks": (n) => createShape({
inheritFrame: true,
bbox: {
centerY: n,
},
shapes: {
"label": M.text({contents: n.toString()}),
"tick": M.rect({height: 2, width: 10, fill: "gray"}),
},
rels: {
"label->tick": [C.hSpace(2), C.alignMiddle],
}
})
},
rels: {
"yTicks->axisLine": [C.hSpace(0)],
}
})
Insert cell
axisTicks = ({ "yTicks": [20, 30, 50, 70] })
Insert cell
render(axisTicks, yAxis)
Insert cell
mkList("bars", data)
Insert cell
splatTestData = ({
node: 20,
children: mkList('elements', [1, 2, 3, 4, 5]),
})
Insert cell
treeSplatTestShape = createShape({
shapes: {
$node$: (n) => M.text({ contents: n.toString(), fontSize: "18px" }),
$children$: createShape({
shapes: {
$elements$: (n) => M.text({ contents: n.toString(), fontSize: "18px" }),
$neighbors$: createShape({
shapes: {
$curr$: 'ref',
$next$: 'ref',
},
rels: { "curr->next": [C.hSpace(20), C.alignMiddle] },
})
},
}),
},
rels: {
/* TODO: this constraint doesn't seem to be enforced properly... */
// "node->children/elements": [C.vSpace(5)],
"node->children/elements": [C.vSpace(5)],
"node->children": [C.vSpace(5)],
// "node->children/neighbors": [C.vSpace(5)],
}
})
Insert cell
render(splatTestData, treeSplatTestShape)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
dev = true
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more