Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions src/format.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import {format as isoFormat} from "isoformat";
import {memoize1} from "./memoize.js";

const numberFormat = memoize1(locale => new Intl.NumberFormat(locale));
const monthFormat = memoize1((locale, month) => new Intl.DateTimeFormat(locale, {timeZone: "UTC", month}));
const weekdayFormat = memoize1((locale, weekday) => new Intl.DateTimeFormat(locale, {timeZone: "UTC", weekday}));

export function formatNumber(locale = "en-US") {
const format = numberFormat(locale);
return i => i != null && !isNaN(i) ? format.format(i) : undefined;
}

export function formatMonth(locale = "en-US", month = "short") {
const format = new Intl.DateTimeFormat(locale, {timeZone: "UTC", month});
return i => {
if (i != null && !isNaN(i = new Date(Date.UTC(2000, +i)))) {
return format.format(i);
}
};
const format = monthFormat(locale, month);
return i => i != null && !isNaN(i = new Date(Date.UTC(2000, +i))) ? format.format(i) : undefined;
}

export function formatWeekday(locale = "en-US", weekday = "short") {
const format = new Intl.DateTimeFormat(locale, {timeZone: "UTC", weekday});
return i => {
if (i != null && !isNaN(i = new Date(Date.UTC(2001, 0, +i)))) {
return format.format(i);
}
};
const format = weekdayFormat(locale, weekday);
return i => i != null && !isNaN(i = new Date(Date.UTC(2001, 0, +i))) ? format.format(i) : undefined;
}

export function formatIsoDate(date) {
Expand Down
27 changes: 8 additions & 19 deletions src/mark.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import {ascending, descending, rollup, sort} from "d3";
import {color} from "d3";
import {nonempty} from "./defined.js";
import {ascending, color, descending, rollup, sort} from "d3";
import {plot} from "./plot.js";
import {registry} from "./scales/index.js";
import {styles} from "./style.js";
Expand Down Expand Up @@ -209,22 +207,6 @@ export function maybeZ({z, fill, stroke} = {}) {
return z;
}

// Applies the specified titles via selection.call.
export function title(L) {
return L ? selection => selection
.filter(i => nonempty(L[i]))
.append("title")
.text(i => L[i]) : () => {};
}

// title for groups (lines, areas).
export function titleGroup(L) {
return L ? selection => selection
.filter(([i]) => nonempty(L[i]))
.append("title")
.text(([i]) => L[i]) : () => {};
}

// Returns a Uint32Array with elements [0, 1, 2, … data.length - 1].
export function range(data) {
return Uint32Array.from(data, indexOf);
Expand Down Expand Up @@ -318,6 +300,13 @@ export function isTemporal(values) {
}
}

export function isNumeric(values) {
for (const value of values) {
if (value == null) continue;
return typeof value === "number";
}
}

export function markify(mark) {
return mark instanceof Mark ? mark : new Render(mark);
}
Expand Down
12 changes: 6 additions & 6 deletions src/marks/text.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {create} from "d3";
import {filter, nonempty} from "../defined.js";
import {Mark, indexOf, identity, string, maybeNumber, maybeTuple, numberChannel} from "../mark.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyAttr, applyTransform, offset} from "../style.js";
import {Mark, indexOf, identity, string, maybeNumber, maybeTuple, numberChannel, isNumeric, isTemporal} from "../mark.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyAttr, applyText, applyTransform, offset} from "../style.js";

const defaults = {
strokeLinejoin: "round"
Expand Down Expand Up @@ -55,7 +55,7 @@ export class Text extends Mark {
const cx = (marginLeft + width - marginRight) / 2;
const cy = (marginTop + height - marginBottom) / 2;
return create("svg:g")
.call(applyIndirectTextStyles, this)
.call(applyIndirectTextStyles, this, T)
.call(applyTransform, x, y, offset, offset)
.call(g => g.selectAll()
.data(index)
Expand All @@ -71,7 +71,7 @@ export class Text extends Mark {
: `translate(${cx},${cy}) rotate(${rotate})`)
: text => text.attr("x", X ? i => X[i] : cx).attr("y", Y ? i => Y[i] : cy))
.call(applyAttr, "font-size", FS && (i => FS[i]))
.text(i => T[i])
.call(applyText, T)
.call(applyChannelStyles, this, channels))
.node();
}
Expand All @@ -90,13 +90,13 @@ export function textY(data, {y = identity, ...options} = {}) {
return new Text(data, {...options, y});
}

function applyIndirectTextStyles(selection, mark) {
function applyIndirectTextStyles(selection, mark, T) {
applyIndirectStyles(selection, mark);
applyAttr(selection, "text-anchor", mark.textAnchor);
applyAttr(selection, "font-family", mark.fontFamily);
applyAttr(selection, "font-size", mark.fontSize);
applyAttr(selection, "font-style", mark.fontStyle);
applyAttr(selection, "font-variant", mark.fontVariant);
applyAttr(selection, "font-variant", mark.fontVariant === undefined && (isNumeric(T) || isTemporal(T)) ? "tabular-nums" : mark.fontVariant);
applyAttr(selection, "font-weight", mark.fontWeight);
}

Expand Down
10 changes: 10 additions & 0 deletions src/memoize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function memoize1(compute) {
let cacheValue, cacheKeys = {};
return (...keys) => {
if (cacheKeys.length !== keys.length || cacheKeys.some((k, i) => k !== keys[i])) {
cacheKeys = keys;
cacheValue = compute(...keys);
}
return cacheValue;
};
}
29 changes: 24 additions & 5 deletions src/style.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {namespaces} from "d3";
import {string, number, maybeColor, maybeNumber, title, titleGroup} from "./mark.js";
import {filter} from "./defined.js";
import {isoFormat, namespaces} from "d3";
import {string, number, maybeColor, maybeNumber, isTemporal, isNumeric} from "./mark.js";
import {filter, nonempty} from "./defined.js";
import {formatNumber} from "./format.js";

export const offset = typeof window !== "undefined" && window.devicePixelRatio > 1 ? 0 : 0.5;

Expand Down Expand Up @@ -108,6 +109,24 @@ export function styles(
];
}

// Applies the specified titles via selection.call.
export function applyTitle(selection, L) {
if (L) selection.filter(i => nonempty(L[i])).append("title").call(applyText, L);
}

// Like applyTitle, but for grouped data (lines, areas).
export function applyTitleGroup(selection, L) {
if (L) selection.filter(([i]) => nonempty(L[i])).append("title").call(applyTextGroup, L);
}

export function applyText(selection, T) {
if (T) selection.text(isTemporal(T) ? i => isoFormat(T[i]) : isNumeric(T) ? (f => i => f(T[i]))(formatNumber()) : i => T[i]);
}

export function applyTextGroup(selection, T) {
if (T) selection.text(isTemporal(T) ? ([i]) => isoFormat(T[i]) : isNumeric(T) ? (f => ([i]) => f(T[i]))(formatNumber()) : ([i]) => T[i]);
}

export function applyChannelStyles(selection, {target}, {title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO, strokeWidth: SW, opacity: O, href: H}) {
if (F) applyAttr(selection, "fill", i => F[i]);
if (FO) applyAttr(selection, "fill-opacity", i => FO[i]);
Expand All @@ -116,7 +135,7 @@ export function applyChannelStyles(selection, {target}, {title: L, fill: F, fill
if (SW) applyAttr(selection, "stroke-width", i => SW[i]);
if (O) applyAttr(selection, "opacity", i => O[i]);
if (H) applyHref(selection, i => H[i], target);
title(L)(selection);
applyTitle(selection, L);
}

export function applyGroupedChannelStyles(selection, {target}, {title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO, strokeWidth: SW, opacity: O, href: H}) {
Expand All @@ -127,7 +146,7 @@ export function applyGroupedChannelStyles(selection, {target}, {title: L, fill:
if (SW) applyAttr(selection, "stroke-width", ([i]) => SW[i]);
if (O) applyAttr(selection, "opacity", ([i]) => O[i]);
if (H) applyHref(selection, ([i]) => H[i], target);
titleGroup(L)(selection);
applyTitleGroup(selection, L);
}

export function applyIndirectStyles(selection, mark) {
Expand Down
71 changes: 71 additions & 0 deletions test/marks/memoize-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {memoize1} from "../../src/memoize.js";
import assert from "assert";

it("memoize1(compute) returns the cached value with repeated calls", () => {
let index = 0;
const m = memoize1(() => ++index);
assert.strictEqual(m(), 1);
assert.strictEqual(m(), 1);
assert.strictEqual(m(), 1);
assert.strictEqual(m(0), 2);
assert.strictEqual(m(0), 2);
assert.strictEqual(m(0), 2);
});

it("memoize1(compute) computes a new value with a different argument value", () => {
let index = 0;
const m = memoize1(() => ++index);
assert.strictEqual(m(), 1);
assert.strictEqual(m(0), 2);
assert.strictEqual(m(undefined), 3);
assert.strictEqual(m(null), 4);
assert.strictEqual(m(0), 5);
assert.strictEqual(m(0, 1), 6);
assert.strictEqual(m(0, 2), 7);
});

it("memoize1(compute) computes a new value with different number of arguments", () => {
let index = 0;
const m = memoize1(() => ++index);
assert.strictEqual(m(0), 1);
assert.strictEqual(m(0, 0), 2);
assert.strictEqual(m(0, 0, 0), 3);
assert.strictEqual(m(0, 0), 4);
assert.strictEqual(m(0), 5);
});

it("memoize1(compute) only caches a single value", () => {
let index = 0;
const m = memoize1(() => ++index);
assert.strictEqual(m(0), 1);
assert.strictEqual(m(1), 2);
assert.strictEqual(m(1), 2);
assert.strictEqual(m(0), 3);
assert.strictEqual(m(0), 3);
assert.strictEqual(m(1), 4);
assert.strictEqual(m(1), 4);
});

it("memoize1(compute) determines equality with strict equals", () => {
let index = 0;
const m = memoize1(() => ++index);
assert.strictEqual(m([0]), 1);
assert.strictEqual(m([1]), 2);
assert.strictEqual(m([1]), 3);
assert.strictEqual(m([0]), 4);
assert.strictEqual(m([0]), 5);
assert.strictEqual(m([1]), 6);
assert.strictEqual(m([1]), 7);
});

it("memoize1(compute) passes the specified arguments to compute", () => {
let index = 0;
const m = memoize1((...args) => [...args, ++index]);
assert.deepStrictEqual(m(0), [0, 1]);
assert.deepStrictEqual(m(1), [1, 2]);
assert.deepStrictEqual(m(1), [1, 2]);
assert.deepStrictEqual(m(0, 1), [0, 1, 3]);
assert.deepStrictEqual(m(0, 1), [0, 1, 3]);
assert.deepStrictEqual(m(1, 0), [1, 0, 4]);
assert.deepStrictEqual(m(1, 0), [1, 0, 4]);
});
Loading