diff --git a/packages/viz/LICENSE b/packages/viz/LICENSE
new file mode 100644
index 0000000000..8dada3edaf
--- /dev/null
+++ b/packages/viz/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/packages/viz/README.md b/packages/viz/README.md
new file mode 100644
index 0000000000..187037b470
--- /dev/null
+++ b/packages/viz/README.md
@@ -0,0 +1,65 @@
+
+
+# ![viz](https://media.thi.ng/umbrella/banners/thing-viz.svg?e4890169)
+
+[![npm version](https://img.shields.io/npm/v/@thi.ng/viz.svg)](https://www.npmjs.com/package/@thi.ng/viz)
+![npm downloads](https://img.shields.io/npm/dm/@thi.ng/viz.svg)
+[![Twitter Follow](https://img.shields.io/twitter/follow/thing_umbrella.svg?style=flat-square&label=twitter)](https://twitter.com/thing_umbrella)
+
+This project is part of the
+[@thi.ng/umbrella](https://github.com/thi-ng/umbrella/) monorepo.
+
+- [About](#about)
+ - [Status](#status)
+- [Installation](#installation)
+- [Dependencies](#dependencies)
+- [API](#api)
+- [Authors](#authors)
+- [License](#license)
+
+## About
+
+Declarative, functional & multi-format data visualization toolkit based around [@thi.ng/hiccup](https://github.com/thi-ng/umbrella/tree/develop/packages/hiccup).
+
+### Status
+
+**ALPHA** - bleeding edge / work-in-progress
+
+## Installation
+
+```bash
+yarn add @thi.ng/viz
+```
+
+```html
+// ES module
+
+
+// UMD
+
+```
+
+Package sizes (gzipped, pre-treeshake): CJS: 797 bytes
+
+## Dependencies
+
+- [@thi.ng/api](https://github.com/thi-ng/umbrella/tree/develop/packages/api)
+- [@thi.ng/arrays](https://github.com/thi-ng/umbrella/tree/develop/packages/arrays)
+- [@thi.ng/associative](https://github.com/thi-ng/umbrella/tree/develop/packages/associative)
+- [@thi.ng/math](https://github.com/thi-ng/umbrella/tree/develop/packages/math)
+- [@thi.ng/strings](https://github.com/thi-ng/umbrella/tree/develop/packages/strings)
+- [@thi.ng/transducers](https://github.com/thi-ng/umbrella/tree/develop/packages/transducers)
+
+## API
+
+[Generated API docs](https://docs.thi.ng/umbrella/viz/)
+
+TODO
+
+## Authors
+
+Karsten Schmidt
+
+## License
+
+© 2014 - 2020 Karsten Schmidt // Apache Software License 2.0
diff --git a/packages/viz/api-extractor.json b/packages/viz/api-extractor.json
new file mode 100644
index 0000000000..94972e6bed
--- /dev/null
+++ b/packages/viz/api-extractor.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../../api-extractor.json"
+}
diff --git a/packages/viz/package.json b/packages/viz/package.json
new file mode 100644
index 0000000000..95276ed723
--- /dev/null
+++ b/packages/viz/package.json
@@ -0,0 +1,76 @@
+{
+ "name": "@thi.ng/viz",
+ "version": "0.0.1",
+ "description": "Declarative, functional & multi-format data visualization toolkit based around @thi.ng/hiccup",
+ "module": "./index.js",
+ "main": "./lib/index.js",
+ "umd:main": "./lib/index.umd.js",
+ "typings": "./index.d.ts",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/thi-ng/umbrella.git"
+ },
+ "homepage": "https://github.com/thi-ng/umbrella/tree/master/packages/viz#readme",
+ "funding": {
+ "type": "patreon",
+ "url": "https://patreon.com/thing_umbrella"
+ },
+ "author": "Karsten Schmidt ",
+ "license": "Apache-2.0",
+ "scripts": {
+ "build": "yarn clean && yarn build:es6 && node ../../scripts/bundle-module",
+ "build:release": "yarn clean && yarn build:es6 && node ../../scripts/bundle-module all",
+ "build:es6": "tsc --declaration",
+ "build:test": "rimraf build && tsc -p test/tsconfig.json",
+ "test": "mocha test",
+ "cover": "nyc mocha test && nyc report --reporter=lcov",
+ "clean": "rimraf *.js *.d.ts *.map .nyc_output build coverage doc lib",
+ "doc:readme": "ts-node -P ../../tools/tsconfig.json ../../tools/src/readme.ts",
+ "doc:ae": "mkdir -p .ae/doc .ae/temp && node_modules/.bin/api-extractor run --local --verbose",
+ "doc": "node_modules/.bin/typedoc --mode modules --out doc src",
+ "pub": "yarn build:release && yarn publish --access public",
+ "tool:intervals": "ts-node -P tools/tsconfig.json tools/intervals.ts"
+ },
+ "devDependencies": {
+ "@istanbuljs/nyc-config-typescript": "^1.0.1",
+ "@microsoft/api-extractor": "^7.9.11",
+ "@types/mocha": "^8.0.3",
+ "@types/node": "^14.6.1",
+ "mocha": "^8.1.2",
+ "nyc": "^15.1.0",
+ "ts-node": "^9.0.0",
+ "typedoc": "^0.18.0",
+ "typescript": "^4.0.2"
+ },
+ "dependencies": {
+ "@thi.ng/api": "^6.12.3",
+ "@thi.ng/arrays": "^0.7.0",
+ "@thi.ng/associative": "^5.0.5",
+ "@thi.ng/math": "^2.0.4",
+ "@thi.ng/strings": "^1.9.5",
+ "@thi.ng/transducers": "^7.3.0"
+ },
+ "files": [
+ "plot",
+ "*.js",
+ "*.d.ts",
+ "lib"
+ ],
+ "keywords": [
+ "2d",
+ "dataviz",
+ "es6",
+ "hiccup",
+ "svg",
+ "typescript",
+ "visualization"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "sideEffects": false,
+ "thi.ng": {
+ "status": "alpha",
+ "year": 2014
+ }
+}
diff --git a/packages/viz/src/api.ts b/packages/viz/src/api.ts
new file mode 100644
index 0000000000..d1944556dc
--- /dev/null
+++ b/packages/viz/src/api.ts
@@ -0,0 +1,36 @@
+import type { Fn, Fn2 } from "@thi.ng/api";
+
+export type Domain = number[];
+export type Range = number[];
+
+export type ScaleFn = Fn;
+
+export type PlotFn = Fn;
+
+export interface AxisSpec {
+ scale: ScaleFn;
+ domain: Domain;
+ range: Range;
+ pos: number;
+ visible: boolean;
+ attribs: any;
+ labelAttribs: any;
+ label: Fn2;
+ labelOffset: number[];
+ format: Fn;
+ major: Partial;
+ minor: Partial;
+}
+
+export interface TickSpec {
+ ticks: Fn>;
+ size: number;
+}
+
+export interface VizSpec {
+ attribs?: any;
+ xaxis: AxisSpec;
+ yaxis: AxisSpec;
+ project?: Fn;
+ plots: PlotFn[];
+}
diff --git a/packages/viz/src/axis.ts b/packages/viz/src/axis.ts
new file mode 100644
index 0000000000..ccf1ce2db5
--- /dev/null
+++ b/packages/viz/src/axis.ts
@@ -0,0 +1,37 @@
+import { mergeDeepObj } from "@thi.ng/associative";
+import { inRange, roundTo } from "@thi.ng/math";
+import { float } from "@thi.ng/strings";
+import { filter, range } from "@thi.ng/transducers";
+import { AxisSpec, Domain } from "./api";
+import { linearScale } from "./scale";
+
+const NO_TICKS = () => [];
+
+const axisDefaults = (): Partial => ({
+ attribs: { stroke: "#000" },
+ label: (pos, body) => ["text", {}, pos, body],
+ labelAttribs: {
+ fill: "#000",
+ stroke: "none",
+ },
+ labelOffset: [0, 0],
+ format: float(2),
+ visible: true,
+ major: { ticks: NO_TICKS, size: 10 },
+ minor: { ticks: NO_TICKS, size: 5 },
+});
+
+export const linTickMarks = (step = 1) => ([d1, d2]: Domain) => [
+ ...filter(
+ (x) => inRange(x, d1, d2),
+ range(roundTo(d1, step), d2 + step, step)
+ ),
+];
+
+export const linearAxis = (
+ src: Partial & Pick
+) => {
+ const spec = mergeDeepObj(axisDefaults(), src);
+ !spec.scale && (spec.scale = linearScale(spec.domain, spec.range));
+ return spec;
+};
diff --git a/packages/viz/src/date.ts b/packages/viz/src/date.ts
new file mode 100644
index 0000000000..1124eec41c
--- /dev/null
+++ b/packages/viz/src/date.ts
@@ -0,0 +1,94 @@
+import type { Domain } from "./api";
+
+export const SECOND = 1000;
+export const MINUTE = 60 * SECOND;
+export const HOUR = 60 * MINUTE;
+export const DAY = 24 * HOUR;
+export const WEEK = 7 * DAY;
+export const MONTH = 30.4375 * DAY;
+export const YEAR = 365.25 * DAY;
+
+export const floorDay = (epoch: number) => {
+ const d = new Date(epoch);
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
+};
+
+export const floorMonth = (epoch: number) => {
+ const d = new Date(epoch);
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth());
+};
+
+export const floorYear = (epoch: number) => {
+ const d = new Date(epoch);
+ return Date.UTC(d.getUTCFullYear(), 0);
+};
+
+export const ceilDay = (epoch: number) => {
+ const d = new Date(epoch + DAY);
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
+};
+
+export const ceilMonth = (epoch: number) => {
+ let d = new Date(epoch);
+ d = new Date(epoch + DAYS_IN_MONTH[d.getUTCMonth()] * DAY);
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth());
+};
+
+export const ceilYear = (epoch: number) => {
+ const d = new Date(epoch);
+ return Date.UTC(d.getUTCFullYear() + 1, 0);
+};
+
+const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
+
+export function* years([from, to]: Domain) {
+ const d1 = new Date(from);
+ const d2 = new Date(to);
+ for (
+ let y1 = d1.getUTCFullYear(), y2 = d2.getUTCFullYear();
+ y1 <= y2;
+ y1++
+ ) {
+ const epoch = Date.UTC(y1, 0);
+ if (epoch >= from) yield epoch;
+ }
+}
+
+export function* months([from, to]: Domain) {
+ const d1 = new Date(from);
+ const d2 = new Date(to);
+ let m1 = d1.getUTCMonth();
+ let y1 = d1.getUTCFullYear();
+ const m2 = d2.getUTCMonth();
+ const y2 = d2.getUTCFullYear();
+ while (y1 <= y2 || m1 <= m2) {
+ const epoch = Date.UTC(y1, m1);
+ if (epoch >= from && epoch < to) yield epoch;
+ if (++m1 === 12) {
+ m1 = 0;
+ ++y1;
+ }
+ }
+}
+
+export function* days([from, to]: Domain) {
+ const t1 = new Date(from);
+ const t2 = new Date(to);
+ let d1 = t1.getUTCDate();
+ let m1 = t1.getUTCMonth();
+ let y1 = t1.getUTCFullYear();
+ const d2 = t2.getUTCDate();
+ const m2 = t2.getUTCMonth();
+ const y2 = t2.getUTCFullYear();
+ while (y1 <= y2 || m1 <= m2 || d1 <= d2) {
+ const epoch = Date.UTC(y1, m1, d1);
+ if (epoch >= from && epoch < to) yield epoch;
+ if (++d1 > DAYS_IN_MONTH[m1]) {
+ d1 = 1;
+ if (++m1 === 12) {
+ m1 = 0;
+ ++y1;
+ }
+ }
+ }
+}
diff --git a/packages/viz/src/domain.ts b/packages/viz/src/domain.ts
new file mode 100644
index 0000000000..df11d60931
--- /dev/null
+++ b/packages/viz/src/domain.ts
@@ -0,0 +1,32 @@
+import type { Fn } from "@thi.ng/api";
+import { ensureArray } from "@thi.ng/arrays";
+import { mix } from "@thi.ng/math";
+import { juxtR, map, max, min, transduce } from "@thi.ng/transducers";
+import type { Domain } from "./api";
+
+export const uniformDomain = ([d1, d2]: Domain, src: Iterable) => {
+ const vals = ensureArray(src);
+ const norm = vals.length > 1 ? 1 / (vals.length - 1) : 0;
+ return vals.map((x, i) => [mix(d1, d2, i * norm), x]);
+};
+
+export const dataBounds = (fn: Fn, src: T[], pad = 0) => {
+ const b = transduce(map(fn), juxtR(min(), max()), src);
+ b[0] -= pad;
+ b[1] += pad;
+ return b;
+};
+
+export const dataBounds2 = (
+ min: Fn,
+ max: Fn,
+ src: T[],
+ padMin = 0,
+ padMax = padMin
+) => [dataMin(min, src, padMin), dataMax(max, src, padMax)];
+
+export const dataMin = (fn: Fn, src: T[], pad = 0) =>
+ transduce(map(fn), min(), src) - pad;
+
+export const dataMax = (fn: Fn, src: T[], pad = 0) =>
+ transduce(map(fn), max(), src) + pad;
diff --git a/packages/viz/src/index.ts b/packages/viz/src/index.ts
new file mode 100644
index 0000000000..d9c18e49df
--- /dev/null
+++ b/packages/viz/src/index.ts
@@ -0,0 +1,13 @@
+export * from "./api";
+export * from "./axis";
+export * from "./date";
+export * from "./domain";
+export * from "./plot";
+export * from "./scale";
+
+export * from "./plot/area";
+export * from "./plot/candle";
+export * from "./plot/line";
+export * from "./plot/scatter";
+export * from "./plot/stacked-intervals";
+export * from "./plot/utils";
diff --git a/packages/viz/src/plot.ts b/packages/viz/src/plot.ts
new file mode 100644
index 0000000000..60280fd70b
--- /dev/null
+++ b/packages/viz/src/plot.ts
@@ -0,0 +1,112 @@
+import type { Fn } from "@thi.ng/api";
+import { eqDelta } from "@thi.ng/math";
+import { comp, filter, iterator, mapcat } from "@thi.ng/transducers";
+import type { AxisSpec, VizSpec } from "./api";
+
+const axisCommon = (
+ { domain, major, minor, attribs, labelAttribs }: AxisSpec,
+ axis: any,
+ majorTickFn: Fn,
+ minorTickFn: Fn,
+ labelFn: Fn
+) => {
+ const majorTicks = [...major.ticks!(domain)];
+ return [
+ "g",
+ attribs,
+ axis,
+ ["path", {}, [...mapcat(majorTickFn, majorTicks)]],
+ [
+ "path",
+ {},
+ [
+ ...iterator(
+ comp(
+ filter(
+ (x) =>
+ majorTicks.find((y) => eqDelta(x, y)) ===
+ undefined
+ ),
+ mapcat(minorTickFn)
+ ),
+ minor.ticks!(domain)
+ ),
+ ],
+ ],
+ ["g", { stroke: "none", ...labelAttribs }, ...majorTicks.map(labelFn)],
+ ];
+};
+
+export const cartesianAxisX = (spec: AxisSpec) => {
+ const {
+ pos,
+ scale,
+ format,
+ label,
+ labelOffset: [lx, ly],
+ range: [r1, r2],
+ } = spec;
+ const tick = (dy: number) => (x: number) => [
+ ["M", [scale(x), pos]],
+ ["v", dy],
+ ];
+ return axisCommon(
+ spec,
+ [
+ "path",
+ {},
+ [
+ ["M", [r1, pos]],
+ ["L", [r2, pos]],
+ ],
+ ],
+ tick(spec.major.size!),
+ tick(spec.minor.size!),
+ (x) => label([scale(x) + lx, pos + ly], format(x))
+ );
+};
+
+export const cartesianAxisY = (spec: AxisSpec) => {
+ const {
+ pos,
+ scale,
+ format,
+ label,
+ labelOffset: [lx, ly],
+ range: [r1, r2],
+ } = spec;
+ const tick = (dx: number) => (y: number) => [
+ ["M", [pos, scale(y)]],
+ ["h", dx],
+ ];
+ return axisCommon(
+ spec,
+ [
+ "path",
+ {},
+ [
+ ["M", [pos, r1]],
+ ["L", [pos, r2]],
+ ],
+ ],
+ tick(spec.major.size!),
+ tick(spec.minor.size!),
+ (y) => label([pos + lx, scale(y) + ly], format(y))
+ );
+};
+
+const DEFAULT_ATTRIBS: any = {
+ "font-family": "Arial, Helvetica, sans-serif",
+ "font-size": "10px",
+};
+
+export const plotCartesian = (spec: VizSpec) => {
+ const { xaxis, yaxis, plots } = spec;
+ return [
+ "g",
+ { ...DEFAULT_ATTRIBS, ...spec.attribs },
+ ...plots.map((fn) => fn(spec)),
+ xaxis.visible ? cartesianAxisX(xaxis) : null,
+ yaxis.visible ? cartesianAxisY(yaxis) : null,
+ ];
+};
diff --git a/packages/viz/src/plot/area.ts b/packages/viz/src/plot/area.ts
new file mode 100644
index 0000000000..d5644d9436
--- /dev/null
+++ b/packages/viz/src/plot/area.ts
@@ -0,0 +1,25 @@
+import { ensureArray } from "@thi.ng/arrays";
+import type { PlotFn } from "../api";
+import { processedPoints, valueMapper } from "./utils";
+
+export interface AreaPlotOpts {
+ attribs: any;
+}
+
+export const areaPlot = (
+ data: Iterable,
+ opts: Partial = {}
+): PlotFn => (spec) => {
+ const $data = ensureArray(data);
+ const map = valueMapper(spec.xaxis, spec.yaxis, spec.project);
+ const y0 = spec.yaxis.domain[0];
+ return [
+ "polygon",
+ opts.attribs || {},
+ [
+ map([$data[0][0], y0]),
+ ...processedPoints(spec, data),
+ map([$data[$data.length - 1][0], y0]),
+ ],
+ ];
+};
diff --git a/packages/viz/src/plot/candle.ts b/packages/viz/src/plot/candle.ts
new file mode 100644
index 0000000000..06e19c1080
--- /dev/null
+++ b/packages/viz/src/plot/candle.ts
@@ -0,0 +1,46 @@
+import type { Fn2, NumOrString } from "@thi.ng/api";
+import { map } from "@thi.ng/transducers";
+import type { PlotFn } from "../api";
+import { valueMapper } from "./utils";
+
+export type Candle = { o: number; h: number; l: number; c: number };
+
+export interface CandlePlotOpts {
+ up: Fn2;
+ down: Fn2;
+ title: Fn2;
+ width: number;
+}
+
+export const candlePlot = (
+ data: Iterable<[number, { o: number; h: number; l: number; c: number }]>,
+ opts: Partial = {}
+): PlotFn => (spec) => {
+ const mapper = valueMapper(spec.xaxis, spec.yaxis, spec.project);
+ const w = (opts.width || 6) / 2;
+ return [
+ "g",
+ {},
+ ...map(([x, candle]) => {
+ const { o, h, l, c } = candle;
+ const $o = mapper([x, o]);
+ const $c = mapper([x, c]);
+ return [
+ "g",
+ c >= o ? opts.up!(x, candle) : opts.down!(x, candle),
+ opts.title ? ["title", {}, opts.title(x, candle)] : null,
+ ["line", {}, mapper([x, l]), mapper([x, h])],
+ [
+ "polygon",
+ {},
+ [
+ [$o[0] - w, $o[1]],
+ [$c[0] - w, $c[1]],
+ [$c[0] + w, $c[1]],
+ [$o[0] + w, $o[1]],
+ ],
+ ],
+ ];
+ }, data),
+ ];
+};
diff --git a/packages/viz/src/plot/line.ts b/packages/viz/src/plot/line.ts
new file mode 100644
index 0000000000..607297f2e4
--- /dev/null
+++ b/packages/viz/src/plot/line.ts
@@ -0,0 +1,15 @@
+import type { PlotFn } from "../api";
+import { processedPoints } from "./utils";
+
+export interface LinePlotOpts {
+ attribs: any;
+}
+
+export const linePlot = (
+ data: Iterable,
+ opts: Partial = {}
+): PlotFn => (spec) => [
+ "polyline",
+ opts.attribs || {},
+ [...processedPoints(spec, data)],
+];
diff --git a/packages/viz/src/plot/scatter.ts b/packages/viz/src/plot/scatter.ts
new file mode 100644
index 0000000000..744e3dd6e7
--- /dev/null
+++ b/packages/viz/src/plot/scatter.ts
@@ -0,0 +1,15 @@
+import type { PlotFn } from "../api";
+import { processedPoints } from "./utils";
+
+export interface ScatterPlotOpts {
+ attribs: any;
+}
+
+export const scatterPlot = (
+ data: Iterable,
+ opts: Partial = {}
+): PlotFn => (spec) => [
+ "points",
+ opts.attribs || {},
+ [...processedPoints(spec, data)],
+];
diff --git a/packages/viz/src/plot/stacked-intervals.ts b/packages/viz/src/plot/stacked-intervals.ts
new file mode 100644
index 0000000000..6ae546a70e
--- /dev/null
+++ b/packages/viz/src/plot/stacked-intervals.ts
@@ -0,0 +1,83 @@
+import type { Fn, Fn2 } from "@thi.ng/api";
+import {
+ comp,
+ filter,
+ indexed,
+ iterator,
+ map,
+ mapcat,
+ push,
+ some,
+ transduce,
+} from "@thi.ng/transducers";
+import type { Domain, PlotFn } from "../api";
+import { valueMapper } from "./utils";
+
+export interface StackedIntervalOpts {
+ attribs?: any;
+ interval: Fn;
+ overlap: number;
+ sort?: Fn2<[number[], T], [number[], T], number>;
+ shape: Fn2<[number[], number[], T, number], Fn, any>;
+}
+
+type Row = [number[], T][];
+
+const overlap = ([a, b]: number[], [c, d]: number[], pad = 0) =>
+ a <= d + pad && b + pad >= c;
+
+const rowStacking = (data: [number[], T][], pad = 0) =>
+ data.reduce((acc, item) => {
+ const rx = item[0];
+ for (let i = 0; true; i++) {
+ const row = acc[i];
+ if (!row || !some((y) => overlap(rx, y[0], pad), row)) {
+ row ? row.push(item) : (acc[i] = [item]);
+ return acc;
+ }
+ }
+ }, []>[]);
+
+const processRow = (mapper: Fn, [d1, d2]: Domain) => ([
+ i,
+ row,
+]: [number, Row]) =>
+ row.map(
+ ([[a, b], item]) =>
+ <[number[], number[], T, number]>[
+ mapper([Math.max(d1, a), i]),
+ mapper([Math.min(d2, b), i]),
+ item,
+ i,
+ ]
+ );
+
+export const stackedIntervals = (
+ data: T[],
+ opts: StackedIntervalOpts
+): PlotFn => (spec) => {
+ const mapper = valueMapper(spec.xaxis, spec.yaxis, spec.project);
+ const domain = spec.xaxis.domain;
+ return [
+ "g",
+ opts.attribs,
+ ...iterator(
+ comp(
+ indexed(),
+ mapcat(processRow(mapper, domain)),
+ map((x) => opts.shape(x, mapper))
+ ),
+ rowStacking(
+ transduce(
+ comp(
+ map((x) => <[number[], T]>[opts.interval(x), x]),
+ filter(([x]) => overlap(domain, x, opts.overlap))
+ ),
+ push<[number[], T]>(),
+ data
+ ).sort(opts.sort || ((a, b) => a[0][0] - b[0][0])),
+ opts.overlap
+ )
+ ),
+ ];
+};
diff --git a/packages/viz/src/plot/utils.ts b/packages/viz/src/plot/utils.ts
new file mode 100644
index 0000000000..b81fa7103b
--- /dev/null
+++ b/packages/viz/src/plot/utils.ts
@@ -0,0 +1,23 @@
+import type { Fn } from "@thi.ng/api";
+import { clamp, inRange } from "@thi.ng/math";
+import type { AxisSpec, VizSpec } from "../api";
+
+export const valueMapper = (
+ { scale: scaleX }: AxisSpec,
+ { scale: scaleY, domain: [dmin, dmax] }: AxisSpec,
+ project: Fn = identity
+) => ([x, y]: number[]) => project([scaleX(x), scaleY(clamp(y, dmin, dmax))]);
+
+const identity = (x: any) => x;
+
+export function* processedPoints(
+ { xaxis, yaxis, project }: VizSpec,
+ data: Iterable
+) {
+ const mapper = valueMapper(xaxis, yaxis, project);
+ const [dmin, dmax] = xaxis.domain;
+ for (let p of data) {
+ if (!inRange(p[0], dmin, dmax)) continue;
+ yield <[number[], number[]]>[mapper(p), p];
+ }
+}
diff --git a/packages/viz/src/scale.ts b/packages/viz/src/scale.ts
new file mode 100644
index 0000000000..436766cbbe
--- /dev/null
+++ b/packages/viz/src/scale.ts
@@ -0,0 +1,53 @@
+import { fit, mix } from "@thi.ng/math";
+import type { Domain, Range, ScaleFn } from "./api";
+
+export const linearScale = ([d1, d2]: Domain, [r1, r2]: Range): ScaleFn => (
+ x
+) => fit(x, d1, d2, r1, r2);
+
+export const logScale = (
+ domain: Domain,
+ [r1, r2]: Range,
+ base = 10
+): ScaleFn => {
+ const lb = Math.log(base);
+ const log = (x: number) =>
+ x > 0 ? Math.log(x) / lb : x < 0 ? -Math.log(-x) / lb : 0;
+ const d1l = log(domain[0]);
+ const drange = log(domain[1]) - d1l;
+ return (x) => mix(r1, r2, (log(x) - d1l) / drange);
+};
+
+// export const lensScale = (
+// [d1, d2]: Domain,
+// [r1, r2]: Range,
+// focus = (d1 + d2) / 2,
+// strength: number
+// ): ScaleFn => {
+// const dr = d2 - d1;
+// const f = (focus - d1) / dr;
+// return (x) => mixLens(r1, r2, (x - d1) / dr, f, strength);
+// };
+
+/*
+(defn mix-circular-flipped
+ [a b t]
+ (mm/submadd
+ b a
+ (clojure.core/-
+ (mm/sub
+ (Math/sqrt (mm/sub 1.0 (mm/mul t t))) 1.0))
+ a))
+
+(defn mix-lens
+ [a b t pos strength]
+ (let [v (mm/submadd b a t a)]
+ (mm/add (if (< t pos)
+ (mm/subm
+ ((if (pos? strength) mix-circular-flipped mix-circular)
+ a (mm/submadd b a pos a) (/ t pos)) v strength)
+ (mm/subm
+ ((if (neg? strength) mix-circular-flipped mix-circular)
+ (mm/submadd b a pos a) b (mm/subdiv t pos 1.0 pos)) v (abs* strength)))
+ v)))
+*/
diff --git a/packages/viz/test/index.ts b/packages/viz/test/index.ts
new file mode 100644
index 0000000000..6746f523a0
--- /dev/null
+++ b/packages/viz/test/index.ts
@@ -0,0 +1,140 @@
+import { range } from "@thi.ng/transducers";
+import * as assert from "assert";
+import {
+ cartesianAxisX,
+ cartesianAxisY,
+ linearAxis,
+ linTickMarks,
+ uniformDomain,
+} from "../src";
+
+describe("viz", () => {
+ it("uniformDomain", () => {
+ assert.deepEqual(uniformDomain([100, 200], range(5)), [
+ [100, 0],
+ [125, 1],
+ [150, 2],
+ [175, 3],
+ [200, 4],
+ ]);
+ });
+
+ it("svgCartesianAxisX", () => {
+ const axis = cartesianAxisX(
+ linearAxis({
+ domain: [0, 4],
+ range: [50, 250],
+ pos: 100,
+ major: { ticks: linTickMarks() },
+ minor: { ticks: linTickMarks() },
+ })
+ );
+ assert.deepEqual(axis, [
+ "g",
+ { stroke: "#000" },
+ [
+ "path",
+ {},
+ [
+ ["M", [50, 100]],
+ ["L", [250, 100]],
+ ],
+ ],
+ [
+ "path",
+ {},
+ [
+ ["M", [50, 100]],
+ ["v", 10],
+ ["M", [100, 100]],
+ ["v", 10],
+ ["M", [150, 100]],
+ ["v", 10],
+ ["M", [200, 100]],
+ ["v", 10],
+ ["M", [250, 100]],
+ ["v", 10],
+ ],
+ ],
+ ["path", {}, []],
+ [
+ "g",
+ {
+ stroke: "none",
+ fill: "#000",
+ },
+ ["text", {}, [50, 100], "0.00"],
+ ["text", {}, [100, 100], "1.00"],
+ ["text", {}, [150, 100], "2.00"],
+ ["text", {}, [200, 100], "3.00"],
+ ["text", {}, [250, 100], "4.00"],
+ ],
+ ]);
+ });
+
+ it("svgCartesianAxisY", () => {
+ const axis = cartesianAxisY(
+ linearAxis({
+ domain: [0, 4],
+ range: [100, 0],
+ pos: 100,
+ labelAttribs: { "text-anchor": "end" },
+ labelOffset: [-15, 5],
+ major: { ticks: linTickMarks(), size: -10 },
+ minor: { ticks: linTickMarks() },
+ })
+ );
+ assert.deepEqual(axis, [
+ "g",
+ { stroke: "#000" },
+ [
+ "path",
+ {},
+ [
+ ["M", [100, 100]],
+ ["L", [100, 0]],
+ ],
+ ],
+ [
+ "path",
+ {},
+ [
+ ["M", [100, 100]],
+ ["h", -10],
+ ["M", [100, 75]],
+ ["h", -10],
+ ["M", [100, 50]],
+ ["h", -10],
+ ["M", [100, 25]],
+ ["h", -10],
+ ["M", [100, 0]],
+ ["h", -10],
+ ],
+ ],
+ ["path", {}, []],
+ [
+ "g",
+ {
+ fill: "#000",
+ stroke: "none",
+ "text-anchor": "end",
+ },
+ ["text", {}, [85, 105], "0.00"],
+ ["text", {}, [85, 80], "1.00"],
+ ["text", {}, [85, 55], "2.00"],
+ ["text", {}, [85, 30], "3.00"],
+ ["text", {}, [85, 5], "4.00"],
+ ],
+ ]);
+ });
+
+ it("linechart", () => {
+ // const vals = [
+ // [0, 2],
+ // [1, 0.5],
+ // [2, 1],
+ // [3, 0.75],
+ // [4, 0.25],
+ // ];
+ });
+});
diff --git a/packages/viz/test/tsconfig.json b/packages/viz/test/tsconfig.json
new file mode 100644
index 0000000000..72b29d55ac
--- /dev/null
+++ b/packages/viz/test/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../build",
+ "module": "commonjs"
+ },
+ "include": ["./**/*.ts", "../src/**/*.ts"]
+}
diff --git a/packages/viz/tools/candles.ts b/packages/viz/tools/candles.ts
new file mode 100644
index 0000000000..ffa074ef26
--- /dev/null
+++ b/packages/viz/tools/candles.ts
@@ -0,0 +1,81 @@
+import { serialize } from "@thi.ng/hiccup";
+import { convertTree, svg } from "@thi.ng/hiccup-svg";
+import { float, Z2 } from "@thi.ng/strings";
+import { readFileSync, writeFileSync } from "fs";
+import {
+ candlePlot,
+ dataBounds,
+ dataMax,
+ dataMin,
+ days,
+ HOUR,
+ linearAxis,
+ linTickMarks,
+ plotCartesian,
+} from "../src";
+
+const prices: any[] = JSON.parse(
+ readFileSync("dev/ohlc.json").toString()
+).Data.map((x: any) => ({ ...x, time: x.time * 1000 }));
+const res = plotCartesian({
+ xaxis: linearAxis({
+ domain: dataBounds((x) => x.time, prices, 1 * HOUR),
+ range: [100, 1250],
+ pos: 500,
+ labelOffset: [0, 20],
+ labelAttribs: { "text-anchor": "middle" },
+ format: (x) => {
+ const d = new Date(x);
+ return `${d.getFullYear()}-${Z2(d.getMonth() + 1)}-${Z2(
+ d.getDate()
+ )}`;
+ },
+ major: { ticks: days },
+ minor: { ticks: () => [] },
+ }),
+ yaxis: linearAxis({
+ domain: [
+ dataMin((x) => x.low, prices, 50),
+ dataMax((x) => x.high, prices, 50),
+ ],
+ range: [500, 20],
+ pos: 100,
+ labelOffset: [-15, 3],
+ labelAttribs: { "text-anchor": "end" },
+ format: (x) => `$${float(2)(x)}`,
+ major: { ticks: linTickMarks(100) },
+ minor: { ticks: linTickMarks(50) },
+ }),
+ plots: [
+ // areaPlot(vals, {
+ // attribs: { fill: "red" },
+ // }),
+ // scatterPlot(vals, {
+ // attribs: { size: 5, fill: "none", stroke: "red" },
+ // }),
+ candlePlot(
+ prices.map((p: any) => [
+ p.time,
+ { o: p.open, h: p.high, l: p.low, c: p.close },
+ ]),
+ {
+ down: () => ({
+ stroke: "#c00",
+ fill: "#fff",
+ }),
+ up: () => ({
+ stroke: "#0c0",
+ fill: "#0c0",
+ }),
+ width: 4,
+ }
+ ),
+ ],
+});
+
+writeFileSync(
+ "export/linechart.svg",
+ serialize(
+ convertTree(svg({ width: 1280, height: 560, "font-size": "10px" }, res))
+ )
+);
diff --git a/packages/viz/tools/tsconfig.json b/packages/viz/tools/tsconfig.json
new file mode 100644
index 0000000000..9655cbea10
--- /dev/null
+++ b/packages/viz/tools/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../build",
+ "module": "commonjs",
+ "noUnusedLocals": false,
+ "noUnusedParameters": false
+ },
+ "include": ["./**/*.ts", "../src/**/*.ts"]
+}
diff --git a/packages/viz/tpl.readme.md b/packages/viz/tpl.readme.md
new file mode 100644
index 0000000000..82b7b45100
--- /dev/null
+++ b/packages/viz/tpl.readme.md
@@ -0,0 +1,48 @@
+# ${pkg.banner}
+
+[![npm version](https://img.shields.io/npm/v/${pkg.name}.svg)](https://www.npmjs.com/package/${pkg.name})
+![npm downloads](https://img.shields.io/npm/dm/${pkg.name}.svg)
+[![Twitter Follow](https://img.shields.io/twitter/follow/thing_umbrella.svg?style=flat-square&label=twitter)](https://twitter.com/thing_umbrella)
+
+This project is part of the
+[@thi.ng/umbrella](https://github.com/thi-ng/umbrella/) monorepo.
+
+
+
+## About
+
+${pkg.description}
+
+${status}
+
+${supportPackages}
+
+${relatedPackages}
+
+${blogPosts}
+
+## Installation
+
+${pkg.install}
+
+${pkg.size}
+
+## Dependencies
+
+${pkg.deps}
+
+${examples}
+
+## API
+
+${docLink}
+
+TODO
+
+## Authors
+
+${authors}
+
+## License
+
+© ${copyright} // ${license}
diff --git a/packages/viz/tsconfig.json b/packages/viz/tsconfig.json
new file mode 100644
index 0000000000..893b9979c5
--- /dev/null
+++ b/packages/viz/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": ".",
+ "module": "es6",
+ "target": "es6"
+ },
+ "include": [
+ "./src/**/*.ts"
+ ]
+}