diff --git a/examples/blocks/gists.json b/examples/blocks/gists.json
index a1e528c23c..9d7617b6e6 100644
--- a/examples/blocks/gists.json
+++ b/examples/blocks/gists.json
@@ -7,5 +7,6 @@
"csv": "02d8fd10aef21b19d6165cf92e43e668",
"iex": "eb151fdd9f98bde987538cbc20e003f6",
"citibike": "bc8d7e6f72e09c9dbd7424b4332cacad",
- "movies": "6b4dcebf65db4ebe4fe53a6de5ea0b48"
+ "movies": "6b4dcebf65db4ebe4fe53a6de5ea0b48",
+ "fractal": "5485f6b630b08d38218822e507f09f21"
}
\ No newline at end of file
diff --git a/examples/blocks/src/fractal/.block b/examples/blocks/src/fractal/.block
new file mode 100644
index 0000000000..37ce96a73a
--- /dev/null
+++ b/examples/blocks/src/fractal/.block
@@ -0,0 +1,2 @@
+license: apache-2.0
+height: 800
\ No newline at end of file
diff --git a/examples/blocks/src/fractal/README.md b/examples/blocks/src/fractal/README.md
new file mode 100644
index 0000000000..7ca1777a9c
--- /dev/null
+++ b/examples/blocks/src/fractal/README.md
@@ -0,0 +1,3 @@
+Demo of [Perspective](https://github.com/finos/perspective).
+
+A classic [fractal](https://en.wikipedia.org/wiki/Mandelbrot_set) implemented entirely in ExprTK/Perspective.
diff --git a/examples/blocks/src/fractal/index.css b/examples/blocks/src/fractal/index.css
new file mode 100644
index 0000000000..97085f7154
--- /dev/null
+++ b/examples/blocks/src/fractal/index.css
@@ -0,0 +1,107 @@
+perspective-viewer {
+ flex: 1;
+ margin: 24px;
+ overflow: visible;
+ --d3fc-positive--gradient: linear-gradient(
+ #94d0ff,
+ #8795e8,
+ #966bff,
+ #ad8cff,
+ #c774e8,
+ #c774a9,
+ #ff6ad5,
+ #ff6a8b,
+ #ff8b8b,
+ #ffa58b,
+ #ffde8b,
+ #cdde8b,
+ #8bde8b,
+ #20de8b
+ );
+}
+
+#app {
+ display: flex;
+ flex-direction: column;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #f2f4f6;
+}
+
+#controls {
+ display: flex;
+ margin: 24px 24px 0px 40px;
+}
+
+.range {
+ position: relative;
+ display: inline-flex;
+ flex-direction: column;
+ margin-right: 24px;
+}
+
+span,
+input,
+button {
+ font-family: "Open Sans";
+ font-size: 12px;
+ background: none;
+ margin: 0px;
+ border-color: #ccc;
+ color: #666;
+ padding: 6px 12px 6px 0px;
+}
+
+input {
+ height: 14px;
+ border-width: 0px;
+ border-style: solid;
+ border-bottom-width: 1px;
+ color: inherit;
+ outline: none;
+}
+
+input[type="range"] {
+ margin-top: 2px;
+}
+
+input[type="number"] {
+ font-family: "Roboto Mono";
+}
+
+input:focus {
+ border-color: #1a7da1;
+}
+
+input::placeholder {
+ color: #ccc;
+}
+
+button {
+ border: 1px solid #ccc;
+ text-transform: uppercase;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
+ padding-left: 12px;
+ height: 28px;
+ outline: none;
+}
+
+button:hover {
+ cursor: pointer;
+}
+
+#run {
+ justify-self: center;
+ margin-right: 24px;
+ height: 83px;
+ width: 80px;
+}
+
+#run:disabled {
+ opacity: 0.2;
+}
diff --git a/examples/blocks/src/fractal/index.html b/examples/blocks/src/fractal/index.html
new file mode 100644
index 0000000000..19f907d21b
--- /dev/null
+++ b/examples/blocks/src/fractal/index.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/blocks/src/fractal/index.js b/examples/blocks/src/fractal/index.js
new file mode 100644
index 0000000000..93b158d8d3
--- /dev/null
+++ b/examples/blocks/src/fractal/index.js
@@ -0,0 +1,158 @@
+function generate_mandelbrot(params) {
+ return `
+// color
+var height := ${params.height};
+var width := ${params.width};
+var xmin := ${params.xmin};
+var xmax := ${params.xmax};
+var ymin := ${params.ymin};
+var ymax := ${params.ymax};
+var iterations := ${params.iterations};
+
+var x := floor("index" / height);
+var y := "index" % height;
+var c := iterations;
+
+var cx := xmin + ((xmax - xmin) * x) / (width - 1);
+var cy := ymin + ((ymax - ymin) * y) / (height - 1);
+
+var vx := 0;
+var vy := 0;
+var vxx := 0;
+var vyy := 0;
+var vxy := 0;
+
+for (var ii := 0; ii < iterations; ii += 1) {
+ if (vxx + vyy <= float(4)) {
+ vxy := vx * vy;
+ vxx := vx * vx;
+ vyy := vy * vy;
+ vx := vxx - vyy + cx;
+ vy := vxy + vxy + cy;
+ c -= 1;
+ }
+};
+
+c`;
+}
+
+function generate_layout(params) {
+ return {
+ plugin: "Heatmap",
+ settings: true,
+ row_pivots: [`floor("index" / ${params.height})`],
+ column_pivots: [`"index" % ${params.height}`],
+ columns: ["color"],
+ expressions: [
+ generate_mandelbrot(params).trim(),
+ `floor("index" / ${params.height})`,
+ `"index" % ${params.height}`,
+ ],
+ };
+}
+
+async function generate_data(table) {
+ const run = document.getElementById("run");
+ let json = new Array(width * height);
+ for (let x = 0; x < width; ++x) {
+ for (let y = 0; y < height; ++y) {
+ const index = x * height + y;
+ json[index] = {
+ index,
+ };
+ }
+ }
+
+ await table.replace(json);
+ run.innerHTML = `Run`;
+}
+
+// GUI
+
+function get_gui_params() {
+ return [
+ "xmin",
+ "xmax",
+ "ymin",
+ "ymax",
+ "width",
+ "height",
+ "iterations",
+ ].reduce((acc, x) => {
+ acc[x] = window[x].valueAsNumber;
+ return acc;
+ }, {});
+}
+
+function make_range(x, y, range, name) {
+ const title = () =>
+ name +
+ " [" +
+ x.valueAsNumber.toFixed(1) +
+ ", " +
+ y.valueAsNumber.toFixed(1) +
+ "]";
+
+ x.addEventListener("input", () => {
+ window.run.disabled = false;
+ x.value = Math.min(x.valueAsNumber, y.valueAsNumber - 0.1);
+ range.innerHTML = title();
+ });
+
+ y.addEventListener("input", () => {
+ window.run.disabled = false;
+ y.value = Math.max(x.valueAsNumber + 0.1, y.valueAsNumber);
+ range.innerHTML = title();
+ });
+}
+
+const make_run_click_callback = (worker, state) => async () => {
+ if (window.run.innerHTML.trim() !== "Run") {
+ window.run.innerHTML = "Run";
+ return;
+ }
+
+ window.run.disabled = true;
+ if (!state.table) {
+ state.table = await worker.table({
+ index: "integer",
+ });
+ window.viewer.load(Promise.resolve(state.table));
+ }
+
+ const run = document.getElementById("run");
+ const params = get_gui_params();
+ const new_size = params.width * params.height;
+ if (!state.size || state.size !== new_size) {
+ let json = {index: new Array(new_size)};
+ for (let x = 0; x < new_size; ++x) {
+ json.index[x] = x;
+ }
+
+ state.table.replace(json);
+ }
+
+ state.size = new_size;
+ run.innerHTML = `Run`;
+ window.viewer.restore(generate_layout(params));
+};
+
+function set_runnable() {
+ window.run.disabled = false;
+}
+
+window.addEventListener("DOMContentLoaded", async function () {
+ const heatmap_plugin = await window.viewer.getPlugin("Heatmap");
+ heatmap_plugin.max_cells = 100000;
+ make_range(xmin, xmax, xrange, "X");
+ make_range(ymin, ymax, yrange, "Y");
+ window.width.addEventListener("input", set_runnable);
+ window.height.addEventListener("input", set_runnable);
+ window.iterations.addEventListener("input", set_runnable);
+
+ run.addEventListener(
+ "click",
+ make_run_click_callback(window.perspective.worker(), {})
+ );
+ run.dispatchEvent(new Event("click"));
+});
diff --git a/examples/blocks/src/mandelbrot/index.html b/examples/blocks/src/mandelbrot/index.html
deleted file mode 100644
index 26187d43b4..0000000000
--- a/examples/blocks/src/mandelbrot/index.html
+++ /dev/null
@@ -1,341 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file