diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000000..2f7c9f526a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,5 @@ +--- +name: Bug +about: Something isn’t working +labels: "bug" +--- diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md new file mode 100644 index 0000000000..02b88e82ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -0,0 +1,5 @@ +--- +name: Enhancement +about: New feature or request +labels: "enhancement" +--- diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 317ed67f9a..608cfe8622 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -16,17 +16,17 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 20 cache: yarn - run: yarn --frozen-lockfile - run: yarn prepublishOnly - run: yarn docs:build - - uses: actions/configure-pages@v3 - - uses: actions/upload-pages-artifact@v1 + - uses: actions/configure-pages@v4 + - uses: actions/upload-pages-artifact@v3 with: path: docs/.vitepress/dist - name: Deploy id: deployment - uses: actions/deploy-pages@v1 + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000000..a8ca29c08b --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,25 @@ +name: Publish + +on: + workflow_dispatch: {} + release: + types: [published] + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: 'https://registry.npmjs.org' + cache: 'yarn' + - run: yarn --frozen-lockfile + - run: yarn test + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2423cfc5cd..4eaaa5b855 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 20 cache: yarn @@ -24,7 +24,7 @@ jobs: - run: yarn test:prettier - run: yarn prepublishOnly - run: yarn docs:build - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: name: test-output-changes diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cd26467c2..616499268b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Year: **Current (2024)** · [2023](./CHANGELOG-2023.md) · [2022](./CHANGELOG-2022.md) · [2021](./CHANGELOG-2021.md) +## 0.6.15 + +[Released June 11, 2024.](https://github.com/observablehq/plot/releases/tag/v0.6.15) + ## 0.6.14 [Released March 12, 2024.](https://github.com/observablehq/plot/releases/tag/v0.6.14) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 5aaf69e194..351b8e372d 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -68,6 +68,7 @@ export default defineConfig({ {text: "Legends", link: "/features/legends"}, {text: "Curves", link: "/features/curves"}, {text: "Formats", link: "/features/formats"}, + {text: "Intervals", link: "/features/intervals"}, {text: "Markers", link: "/features/markers"}, {text: "Shorthand", link: "/features/shorthand"}, {text: "Accessibility", link: "/features/accessibility"} diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index 8387c9018d..fd9606bd99 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -17,11 +17,11 @@ export default { } }; -async function enableAnalytics(router) { +function enableAnalytics(router) { if (typeof location === "undefined" || location.origin !== "https://observablehq.com") return; - const {pageLoad, routeChanged} = await import("https://events.observablehq.com/client.js"); - let pageLoaded; - watch(router.route, () => { + let pageLoaded = false; + watch(router.route, async () => { + const {pageLoad, routeChanged} = await import("https://events.observablehq.com/client.js"); if (pageLoaded) { routeChanged(); } else { diff --git a/docs/data/api.data.ts b/docs/data/api.data.ts index 3efbb7c156..bbabdd8cce 100644 --- a/docs/data/api.data.ts +++ b/docs/data/api.data.ts @@ -43,6 +43,7 @@ function getHref(name: string, path: string): string { switch (path) { case "features/curve": case "features/format": + case "features/interval": case "features/mark": case "features/marker": case "features/plot": diff --git a/docs/features/formats.md b/docs/features/formats.md index 8455dd5902..ee8673279b 100644 --- a/docs/features/formats.md +++ b/docs/features/formats.md @@ -9,6 +9,14 @@ import * as d3 from "d3"; These helper functions are provided for convenience as a **tickFormat** option for the [axis mark](../marks/axis.md), as the **text** option for a [text mark](../marks/text.md), or other use. See also [d3-format](https://d3js.org/d3-format), [d3-time-format](https://d3js.org/d3-time-format), and JavaScript’s built-in [date formatting](https://observablehq.com/@mbostock/date-formatting) and [number formatting](https://observablehq.com/@mbostock/number-formatting). +## formatNumber(*locale*) {#formatNumber} + +```js +Plot.formatNumber("en-US")(Math.PI) // "3.142" +``` + +Returns a function that formats a given number according to the specified *locale*. The *locale* is a [BCP 47 language tag](https://tools.ietf.org/html/bcp47) and defaults to U.S. English. + ## formatIsoDate(*date*) {#formatIsoDate} ```js diff --git a/docs/features/intervals.md b/docs/features/intervals.md new file mode 100644 index 0000000000..f631ecdea0 --- /dev/null +++ b/docs/features/intervals.md @@ -0,0 +1,61 @@ + + +# Intervals + +Plot provides several built-in interval implementations for use with the **tick** option for [scales](./scales.md), as the **thresholds** option for a [bin transform](../transforms/bin.md), or other use. See also [d3-time](https://d3js.org/d3-time). You can also implement custom intervals. + +At a minimum, intervals implement *interval*.**floor** and *interval*.**offset**. Range intervals additionally implement *interval*.**range**, and nice intervals additionally implement *interval*.**ceil**. These latter implementations are required in some contexts; see Plot’s TypeScript definitions for details. + +The *interval*.**floor** method takes a *value* and returns the corresponding value representing the greatest interval boundary less than or equal to the specified *value*. For example, for the “day” time interval, it returns the preceding midnight: + +```js +Plot.utcInterval("day").floor(new Date("2013-04-12T12:34:56Z")) // 2013-04-12 +``` + +The *interval*.**offset** method takes a *value* and returns the corresponding value equal to *value* plus *step* intervals. If *step* is not specified it defaults to 1. If *step* is negative, then the returned value will be less than the specified *value*. For example: + +```js +Plot.utcInterval("day").offset(new Date("2013-04-12T12:34:56Z"), 1) // 2013-04-13T12:34:56Z +Plot.utcInterval("day").offset(new Date("2013-04-12T12:34:56Z"), -2) // 2013-03-22T12:34:56Z +``` + +The *interval*.**range** method returns an array of values representing every interval boundary greater than or equal to *start* (inclusive) and less than *stop* (exclusive). The first value in the returned array is the least boundary greater than or equal to *start*; subsequent values are offset by intervals and floored. + +```js +Plot.utcInterval("week").range(new Date("2013-04-12T12:34:56Z"), new Date("2013-05-12T12:34:56Z")) // [2013-04-14, 2013-04-21, 2013-04-28, 2013-05-05, 2013-05-12] +``` + +The *interval*.**ceil** method returns the value representing the least interval boundary value greater than or equal to the specified *value*. For example, for the “day” time interval, it returns the preceding midnight: + +```js +Plot.utcInterval("day").ceil(new Date("2013-04-12T12:34:56Z")) // 2013-04-13 +``` + +## numberInterval(*period*) {#numberInterval} + +```js +Plot.numberInterval(2) +``` + +Given a number *period*, returns a corresponding range interval implementation. If *period* is a negative number, the resulting interval uses 1 / -*period*; this allows more precise results when *period* is a negative integer. The returned interval implements the *interval*.range, *interval*.floor, and *interval*.offset methods. + +## timeInterval(*period*) {#timeInterval} + +```js +Plot.timeInterval("2 days") +``` + +Given a string *period* describing a local time interval, returns a corresponding nice interval implementation. The period can be *second*, *minute*, *hour*, *day*, *week*, *month*, *quarter*, *half*, *year*, *monday*, *tuesday*, *wednesday*, *thursday*, *friday*, *saturday*, or *sunday*, or a skip interval consisting of a number followed by the interval name (possibly pluralized), such as *3 months* or *10 years*. The returned interval implements the *interval*.range, *interval*.floor, *interval*.ceil, and *interval*.offset methods. + +## utcInterval(*period*) {#utcInterval} + +```js +Plot.utcInterval("2 days") +``` + +Given a string *period* describing a UTC time interval, returns a corresponding nice interval implementation. The period can be *second*, *minute*, *hour*, *day*, *week*, *month*, *quarter*, *half*, *year*, *monday*, *tuesday*, *wednesday*, *thursday*, *friday*, *saturday*, or *sunday*, or a skip interval consisting of a number followed by the interval name (possibly pluralized), such as *3 months* or *10 years*. The returned interval implements the *interval*.range, *interval*.floor, *interval*.ceil, and *interval*.offset methods. diff --git a/docs/features/transforms.md b/docs/features/transforms.md index dac990022e..4c309c985c 100644 --- a/docs/features/transforms.md +++ b/docs/features/transforms.md @@ -181,7 +181,7 @@ The **transform** function is passed three arguments, *data*, *facets*, and *opt If the **transform** option is specified, it supersedes any basic transforms (*i.e.*, the **filter**, **sort** and **reverse** options are ignored). However, the **transform** option is rarely used directly; instead one of Plot’s built-in transforms are used, and these transforms automatically compose with the basic **filter**, **sort** and **reverse** transforms. -While transform functions often produce new *data* or *facets*, they may return the passed-in *data* and *facets* as-is, and often have a side-effect of constructing derived channels. For example, the count of elements in a [groupX transform](../transforms/group.md) might be returned as a new *y* channel. In this case, the transform is typically expressed as an options transform: a function that takes a mark *options* object and returns a new, transformed options object, where the returned options object implements the **transform** option. Transform functions should not mutate the input *data* or *facets*. Likewise options transforms should not mutate the input *options* object. +While transform functions often produce new *data* or *facets*, they may return the passed-in *data* and *facets* as-is, and often have a side effect of constructing derived channels. For example, the count of elements in a [groupX transform](../transforms/group.md) might be returned as a new *y* channel. In this case, the transform is typically expressed as an options transform: a function that takes a mark *options* object and returns a new, transformed options object, where the returned options object implements the **transform** option. Transform functions should not mutate the input *data* or *facets*. Likewise options transforms should not mutate the input *options* object. When implementing a custom transform for generic usage, keep in mind that it needs to be compatible with Plot’s [faceting system](./facets.md), which partitions the original dataset into discrete subsets. diff --git a/docs/marks/tip.md b/docs/marks/tip.md index 388d10c7de..f4ccdaed37 100644 --- a/docs/marks/tip.md +++ b/docs/marks/tip.md @@ -131,7 +131,7 @@ Plot.rectY(olympians, Plot.binX({y: "sum"}, {x: "weight", y: (d) => d.sex === "m ``` ::: -The order and formatting of channels in the tip can be customized with the **format** option , which accepts a key-value object mapping channel names to formats. Each [format](../features/formats.md) can be a string (for number or time formats), a function that receives the value as input and returns a string, true to use the default format, and null or false to suppress. The order of channels in the tip follows their order in the format object followed by any additional channels. +The order and formatting of channels in the tip can be customized with the **format** option , which accepts a key-value object mapping channel names to formats. Each [format](../features/formats.md) can be a string (for number or time formats), a function that receives the value as input and returns a string, true to use the default format, and null or false to suppress. The order of channels in the tip follows their order in the format object followed by any additional channels. When using the **title** channel, the **format** option may be specified as a string or a function; the given format will then apply to the **title** channel. A channel’s label can be specified alongside its value as a {value, label} object; if a channel label is not specified, the associated scale’s label is used, if any; if there is no associated scale, or if the scale has no label, the channel name is used instead. diff --git a/docs/transforms/bin.md b/docs/transforms/bin.md index c70855e370..99cbb83aa2 100644 --- a/docs/transforms/bin.md +++ b/docs/transforms/bin.md @@ -274,7 +274,7 @@ The following named reducers are supported: * *y* - the middle of the bin’s *y* extent (when binning on *y*) * *y1* - the lower bound of the bin’s *y* extent (when binning on *y*) * *y2* - the upper bound of the bin’s *y* extent (when binning on *y*) -* *z* - the bin’s *z* value (*z*, *fill*, or *stroke*) +* *z* - the bin’s *z* value (*z*, *fill*, or *stroke*) In addition, a reducer may be specified as: diff --git a/docs/transforms/group.md b/docs/transforms/group.md index 9c368a73a3..8a52cf4973 100644 --- a/docs/transforms/group.md +++ b/docs/transforms/group.md @@ -370,7 +370,7 @@ The following named reducers are supported: * *identity* - the array of values * *x* - the group’s *x* value (when grouping on *x*) * *y* - the group’s *y* value (when grouping on *y*) -* *z* - the group’s *z* value (*z*, *fill*, or *stroke*) +* *z* - the group’s *z* value (*z*, *fill*, or *stroke*) In addition, a reducer may be specified as: @@ -441,7 +441,7 @@ Plot.groupZ({x: "proportion"}, {fill: "species"}) Groups on the first channel of **z**, **fill**, or **stroke**, if any. If none of **z**, **fill**, or **stroke** are channels, then all data (within each facet) is placed into a single group. -## find(*test*) {#find} +## find(*test*) {#find} ```js Plot.groupX( diff --git a/package.json b/package.json index 59da041c0d..879dfdd35a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@observablehq/plot", "description": "A JavaScript library for exploratory data visualization.", - "version": "0.6.14", + "version": "0.6.15", "author": { "name": "Observable, Inc.", "url": "https://observablehq.com" @@ -44,7 +44,7 @@ "@observablehq/plot": "./src/index.js" }, "sideEffects": [ - "./src/plot.js" + "./src/index.js" ], "devDependencies": { "@observablehq/runtime": "^5.7.3", diff --git a/src/format.d.ts b/src/format.d.ts index 160041cd36..c073bfa07b 100644 --- a/src/format.d.ts +++ b/src/format.d.ts @@ -1,3 +1,13 @@ +/** + * Returns a function that formats a given number according to the specified + * *locale*. + * + * [1]: https://tools.ietf.org/html/bcp47 + * + * @param locale - a [BCP 47 language tag][1]; defaults to U.S. English. + */ +export function formatNumber(locale?: string): (i: number) => string; + /** * Returns a function that formats a given month number (from 0 = January to 11 * = December) according to the specified *locale* and *format*. diff --git a/src/index.js b/src/index.js index 9fde7ce2d5..df33d998d4 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,11 @@ +import {Mark} from "./mark.js"; +import {plot} from "./plot.js"; + +// Note: this side effect avoids a circular dependency. +Mark.prototype.plot = function ({marks = [], ...options} = {}) { + return plot({...options, marks: [...marks, this]}); +}; + export {plot} from "./plot.js"; export {Mark, marks} from "./mark.js"; export {Area, area, areaX, areaY} from "./marks/area.js"; @@ -45,6 +53,8 @@ export {select, selectFirst, selectLast, selectMaxX, selectMaxY, selectMinX, sel export {stackX, stackX1, stackX2, stackY, stackY1, stackY2} from "./transforms/stack.js"; export {treeNode, treeLink} from "./transforms/tree.js"; export {pointer, pointerX, pointerY} from "./interactions/pointer.js"; -export {formatIsoDate, formatWeekday, formatMonth} from "./format.js"; +export {formatIsoDate, formatNumber, formatWeekday, formatMonth} from "./format.js"; export {scale} from "./scales.js"; export {legend} from "./legends.js"; +export {numberInterval} from "./options.js"; +export {timeInterval, utcInterval} from "./time.js"; diff --git a/src/interval.d.ts b/src/interval.d.ts index 29882a04ff..2c76a015a4 100644 --- a/src/interval.d.ts +++ b/src/interval.d.ts @@ -1,4 +1,4 @@ -// For internal use. +/** A named interval. */ export type LiteralTimeInterval = | "3 months" | "10 years" @@ -124,3 +124,16 @@ export type RangeInterval = LiteralInterval | RangeIntervalImplement * - a number (for number intervals), defining intervals at integer multiples of *n* */ export type NiceInterval = LiteralInterval | NiceIntervalImplementation; + +/** + * Given a number *period*, returns a corresponding numeric range interval. If + * *period* is a negative number, the returned interval uses 1 / -*period*, + * allowing greater precision when *period* is a negative integer. + */ +export function numberInterval(period: number): RangeIntervalImplementation; + +/** Given a string *period*, returns a corresponding local time nice interval. */ +export function timeInterval(period: LiteralTimeInterval): NiceIntervalImplementation; + +/** Given a string *period*, returns a corresponding UTC nice interval. */ +export function utcInterval(period: LiteralTimeInterval): NiceIntervalImplementation; diff --git a/src/mark.js b/src/mark.js index 0e867689b5..768105025e 100644 --- a/src/mark.js +++ b/src/mark.js @@ -131,7 +131,7 @@ export class Mark { } export function marks(...marks) { - marks.plot = Mark.prototype.plot; // Note: depends on side-effect in plot! + marks.plot = Mark.prototype.plot; return marks; } diff --git a/src/marks/axis.js b/src/marks/axis.js index a36f1f6aa3..406ff70dd5 100644 --- a/src/marks/axis.js +++ b/src/marks/axis.js @@ -652,7 +652,7 @@ function inferTextChannel(scale, data, ticks, tickFormat, anchor) { // possible, or the default ISO format (2014-01-26). TODO We need a better way // to infer whether the ordinal scale is UTC or local time. export function inferTickFormat(scale, data, ticks, tickFormat, anchor) { - return typeof tickFormat === "function" + return typeof tickFormat === "function" && !(scale.type === "log" && scale.tickFormat) ? tickFormat : tickFormat === undefined && data && isTemporal(data) ? inferTimeFormat(scale.type, data, anchor) ?? formatDefault diff --git a/src/marks/raster.js b/src/marks/raster.js index 61abbc2eea..2d5662ede7 100644 --- a/src/marks/raster.js +++ b/src/marks/raster.js @@ -316,14 +316,15 @@ export function interpolatorBarycentric({random = randomLcg(42)} = {}) { if (x < 0 || x >= width || y < 0 || y >= height) continue; const xp = x + 0.5; // sample pixel centroids const yp = y + 0.5; - const ga = ((By - Cy) * (xp - Cx) + (yp - Cy) * (Cx - Bx)) / z; - if (ga < 0) continue; - const gb = ((Cy - Ay) * (xp - Cx) + (yp - Cy) * (Ax - Cx)) / z; - if (gb < 0) continue; - const gc = 1 - ga - gb; - if (gc < 0) continue; + const s = Math.sign(z); + const ga = (By - Cy) * (xp - Cx) + (yp - Cy) * (Cx - Bx); + if (ga * s < 0) continue; + const gb = (Cy - Ay) * (xp - Cx) + (yp - Cy) * (Ax - Cx); + if (gb * s < 0) continue; + const gc = z - (ga + gb); + if (gc * s < 0) continue; const i = x + width * y; - W[i] = mix(va, ga, vb, gb, vc, gc, x, y); + W[i] = mix(va, ga / z, vb, gb / z, vc, gc / z, x, y); S[i] = 1; } } diff --git a/src/marks/tip.d.ts b/src/marks/tip.d.ts index e115e9db32..906051820c 100644 --- a/src/marks/tip.d.ts +++ b/src/marks/tip.d.ts @@ -74,7 +74,7 @@ export interface TipOptions extends MarkOptions, TextStyles { * is interpreted as a (UTC) time format for temporal channels, and otherwise * a number format. */ - format?: {[name in ChannelName]?: boolean | string | ((d: any, i: number) => string)}; + format?: {[name in ChannelName]?: null | boolean | TipFormat} | TipFormat; /** The image filter for the tip’s box; defaults to a drop shadow. */ pathFilter?: string; @@ -86,6 +86,18 @@ export interface TipOptions extends MarkOptions, TextStyles { textPadding?: number; } +/** + * How to format channel values; one of: + * + * - a [d3-format][1] string for numeric scales + * - a [d3-time-format][2] string for temporal scales + * - a function passed a channel *value* and *index*, returning a string + * + * [1]: https://d3js.org/d3-time + * [2]: https://d3js.org/d3-time-format + */ +export type TipFormat = string | ((d: any, i: number) => string); + /** * Returns a new tip mark for the given *data* and *options*. * diff --git a/src/marks/tip.js b/src/marks/tip.js index 5516e3859a..d741b8e103 100644 --- a/src/marks/tip.js +++ b/src/marks/tip.js @@ -84,7 +84,7 @@ export class Tip extends Mark { for (const key in defaults) if (key in this.channels) this[key] = defaults[key]; // apply default even if channel this.splitLines = splitter(this); this.clipLine = clipper(this); - this.format = {...format}; // defensive copy before mutate; also promote nullish to empty + this.format = typeof format === "string" || typeof format === "function" ? {title: format} : {...format}; // defensive copy before mutate; also promote nullish to empty } render(index, scales, values, dimensions, context) { const mark = this; @@ -120,10 +120,10 @@ export class Tip extends Mark { // channels as name-value pairs. let sources, format; if ("title" in values) { - sources = values.channels; + sources = getSourceChannels.call(this, {title: values.channels.title}, scales); format = formatTitle; } else { - sources = getSourceChannels.call(this, values, scales); + sources = getSourceChannels.call(this, values.channels, scales); format = formatChannels; } @@ -319,7 +319,7 @@ function getPath(anchor, m, r, width, height) { } // Note: mutates this.format! -function getSourceChannels({channels}, scales) { +function getSourceChannels(channels, scales) { const sources = {}; // Promote x and y shorthand for paired channels (in order). @@ -384,7 +384,7 @@ function maybeExpandPairedFormat(format, channels, key) { } function formatTitle(i, index, {title}) { - return formatDefault(title.value[i], i); + return this.format.title(title.value[i], i); } function* formatChannels(i, index, channels, scales, values) { diff --git a/src/options.js b/src/options.js index 8ffa10afcb..a170e4c003 100644 --- a/src/options.js +++ b/src/options.js @@ -1,7 +1,7 @@ import {quantile, range as rangei} from "d3"; import {parse as isoParse} from "isoformat"; import {defined} from "./defined.js"; -import {maybeTimeInterval, maybeUtcInterval} from "./time.js"; +import {timeInterval, utcInterval} from "./time.js"; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray export const TypedArray = Object.getPrototypeOf(Uint8Array); @@ -25,7 +25,7 @@ export function valueof(data, value, type) { } function maybeTake(values, index) { - return index ? take(values, index) : values; + return values != null && index ? take(values, index) : values; } function maybeTypedMap(data, f, type) { @@ -322,27 +322,30 @@ export function maybeIntervalTransform(interval, type) { // range} object similar to a D3 time interval. export function maybeInterval(interval, type) { if (interval == null) return; - if (typeof interval === "number") { - if (0 < interval && interval < 1 && Number.isInteger(1 / interval)) interval = -1 / interval; - const n = Math.abs(interval); - return interval < 0 - ? { - floor: (d) => Math.floor(d * n) / n, - offset: (d) => (d * n + 1) / n, // note: no optional step for simplicity - range: (lo, hi) => rangei(Math.ceil(lo * n), hi * n).map((x) => x / n) - } - : { - floor: (d) => Math.floor(d / n) * n, - offset: (d) => d + n, // note: no optional step for simplicity - range: (lo, hi) => rangei(Math.ceil(lo / n), hi / n).map((x) => x * n) - }; - } - if (typeof interval === "string") return (type === "time" ? maybeTimeInterval : maybeUtcInterval)(interval); + if (typeof interval === "number") return numberInterval(interval); + if (typeof interval === "string") return (type === "time" ? timeInterval : utcInterval)(interval); if (typeof interval.floor !== "function") throw new Error("invalid interval; missing floor method"); if (typeof interval.offset !== "function") throw new Error("invalid interval; missing offset method"); return interval; } +export function numberInterval(interval) { + interval = +interval; + if (0 < interval && interval < 1 && Number.isInteger(1 / interval)) interval = -1 / interval; + const n = Math.abs(interval); + return interval < 0 + ? { + floor: (d) => Math.floor(d * n) / n, + offset: (d, s = 1) => (d * n + Math.floor(s)) / n, + range: (lo, hi) => rangei(Math.ceil(lo * n), hi * n).map((x) => x / n) + } + : { + floor: (d) => Math.floor(d / n) * n, + offset: (d, s = 1) => d + n * Math.floor(s), + range: (lo, hi) => rangei(Math.ceil(lo / n), hi / n).map((x) => x * n) + }; +} + // Like maybeInterval, but requires a range method too. export function maybeRangeInterval(interval, type) { interval = maybeInterval(interval, type); diff --git a/src/plot.js b/src/plot.js index 541218f306..a091d8d8a8 100644 --- a/src/plot.js +++ b/src/plot.js @@ -332,6 +332,7 @@ export function plot(options = {}) { if (subtitle != null) figure.append(createTitleElement(document, subtitle, "h3")); figure.append(...legends, svg); if (caption != null) figure.append(createFigcaption(document, caption)); + if ("value" in svg) (figure.value = svg.value), delete svg.value; } figure.scale = exposeScales(scales.scales); @@ -367,13 +368,6 @@ function createFigcaption(document, caption) { return e; } -function plotThis({marks = [], ...options} = {}) { - return plot({...options, marks: [...marks, this]}); -} - -// Note: This side-effect avoids a circular dependency. -Mark.prototype.plot = plotThis; - function flatMarks(marks) { return marks .flat(Infinity) @@ -409,7 +403,7 @@ function applyScaleTransform(channel, options) { type, percent, interval, - transform = percent ? (x) => x * 100 : maybeIntervalTransform(interval, type) + transform = percent ? (x) => (x == null ? NaN : x * 100) : maybeIntervalTransform(interval, type) } = options[scale] ?? {}; if (transform == null) return; channel.value = map(channel.value, transform); diff --git a/src/scales.d.ts b/src/scales.d.ts index 80ba1d3ed2..94112d6180 100644 --- a/src/scales.d.ts +++ b/src/scales.d.ts @@ -588,7 +588,7 @@ export interface ScaleOptions extends ScaleDefaults { * [1]: https://d3js.org/d3-time * [2]: https://d3js.org/d3-time-format */ - tickFormat?: string | ((t: any, i: number) => any) | null; + tickFormat?: string | ((d: any, i: number) => any) | null; /** * The rotation angle of axis tick labels in degrees clocksize; defaults to 0. diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index 6a4195b9df..88feec4d07 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -123,8 +123,9 @@ export function createScaleQ( const [min, max] = extent(domain); if (min > 0 || max < 0) { domain = slice(domain); - if (orderof(domain) !== Math.sign(min)) domain[domain.length - 1] = 0; // [2, 1] or [-2, -1] - else domain[0] = 0; // [1, 2] or [-1, -2] + const o = orderof(domain) || 1; // treat degenerate as ascending + if (o === Math.sign(min)) domain[0] = 0; // [1, 2] or [-1, -2] + else domain[domain.length - 1] = 0; // [2, 1] or [-2, -1] } } diff --git a/src/time.js b/src/time.js index 63722f587c..118c99daad 100644 --- a/src/time.js +++ b/src/time.js @@ -183,11 +183,11 @@ export function parseTimeInterval(input) { return [name, period]; } -export function maybeTimeInterval(input) { +export function timeInterval(input) { return asInterval(parseTimeInterval(input), "time"); } -export function maybeUtcInterval(input) { +export function utcInterval(input) { return asInterval(parseTimeInterval(input), "utc"); } @@ -209,7 +209,7 @@ export function generalizeTimeInterval(interval, n) { if (!tickIntervals.some(([, d]) => d === duration)) return; // nonstandard or unknown interval if (duration % durationDay === 0 && durationDay < duration && duration < durationMonth) return; // not generalizable const [i] = tickIntervals[bisector(([, step]) => Math.log(step)).center(tickIntervals, Math.log(duration * n))]; - return (interval[intervalType] === "time" ? maybeTimeInterval : maybeUtcInterval)(i); + return (interval[intervalType] === "time" ? timeInterval : utcInterval)(i); } function formatTimeInterval(name, type, anchor) { diff --git a/src/transforms/basic.d.ts b/src/transforms/basic.d.ts index fddc507801..d120ecf917 100644 --- a/src/transforms/basic.d.ts +++ b/src/transforms/basic.d.ts @@ -10,7 +10,7 @@ import type {ScaleFunctions} from "../scales.js"; * the data, *facets*, and the plot’s *options*. The transform function returns * new mark data and facets; the returned **data** defaults to the passed * *data*, and the returned **facets** defaults to the passed *facets*. The mark - * is the *this* context. Transform functions can also trigger side-effects, say + * is the *this* context. Transform functions can also trigger side effects, say * to populate lazily-derived columns; see also Plot.column. */ export type TransformFunction = ( diff --git a/src/transforms/bin.js b/src/transforms/bin.js index 16abb9d883..02321a3345 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -28,7 +28,7 @@ import { mid, valueof } from "../options.js"; -import {maybeUtcInterval} from "../time.js"; +import {utcInterval} from "../time.js"; import {basic} from "./basic.js"; import { hasOutput, @@ -322,7 +322,7 @@ export function maybeThresholds(thresholds, interval, defaultThresholds = thresh case "auto": return thresholdAuto; } - return maybeUtcInterval(thresholds); + return utcInterval(thresholds); } return thresholds; // pass array, count, or function to bin.thresholds } diff --git a/test/data/4kpoints.json b/test/data/4kpoints.json new file mode 100644 index 0000000000..e6087c966d --- /dev/null +++ b/test/data/4kpoints.json @@ -0,0 +1,504 @@ +{ + "x": [ + 22,22,23,23,24,24,26,24,24,24,23,23,23,23,23,23,24,24,24,24,24,25,25,25,25,25,25,25,25, + 25,25,26,25,25,26,26,26,26,27,27,27,27,27,27,27,26,26,26,26,26,26,26,26,26,26,25,25,26, + 26,26,26,26,27,27,27,27,27,27,27,27,66,66,66,66,66,67,67,67,68,68,68,67,67,67,67,67,67, + 66,66,66,66,66,66,67,66,67,67,68,68,68,68,68,69,69,68,68,68,68,68,68,68,69,70,70,69,69, + 69,69,69,69,69,69,69,69,69,69,70,70,70,70,69,69,69,69,69,70,71,106,106,106,107,107,107, + 107,107,107,107,106,107,106,106,107,107,107,107,108,108,108,108,108,108,107,108,108,109,108, + 109,109,109,109,109,109,109,109,109,109,109,110,110,111,111,111,111,111,111,111,111,111,111, + 111,111,111,111,111,111,111,111,111,112,112,112,112,112,112,112,112,112,113,112,112,111,147, + 148,148,148,148,148,149,149,149,149,150,150,149,149,148,148,148,149,149,150,150,150,150,150, + 150,150,150,150,150,150,150,150,150,151,151,151,151,152,152,152,152,152,152,152,152,151,151, + 151,151,151,151,152,153,153,154,154,155,155,155,154,154,189,189,189,190,190,190,190,190,190, + 190,190,190,190,190,191,190,191,191,191,191,191,191,191,191,191,191,191,191,190,190,190,190, + 189,189,189,190,191,191,191,191,191,191,192,192,192,192,193,193,193,194,194,194,194,194,194, + 194,194,195,195,195,195,196,196,197,197,197,197,197,197,197,197,197,197,197,232,233,232,232, + 232,232,232,232,232,233,232,232,232,232,232,232,232,232,232,233,234,234,234,234,234,234,234, + 233,234,234,234,234,234,233,233,233,233,234,234,235,235,235,235,234,234,235,235,235,235,235, + 235,235,235,234,234,235,235,236,237,237,237,237,237,237,237,237,237,236,237,237,237,237,237, + 238,238,238,237,238,238,237,237,273,274,273,274,274,274,274,274,274,274,274,274,274,274,274, + 274,274,274,274,274,274,274,273,273,272,271,271,272,273,273,275,276,276,276,277,276,276,277, + 277,278,278,278,279,279,279,279,279,279,279,279,279,279,279,279,279,279,279,279,279,280,280, + 280,280,282,280,280,280,280,281,280,315,315,315,315,315,315,315,315,315,315,315,316,316,316, + 316,316,316,316,317,317,317,317,317,317,317,317,317,317,317,317,317,318,319,318,318,318,318, + 319,319,319,319,319,319,319,319,320,320,320,320,320,320,320,320,320,320,320,321,320,321,321, + 321,322,322,322,322,323,323,323,323,323,354,354,355,355,355,356,356,356,357,357,357,357,357, + 357,357,358,357,358,358,358,358,359,359,358,358,358,357,358,358,359,359,359,360,360,360,360, + 360,360,360,360,361,361,361,362,362,362,362,363,362,363,363,363,363,363,363,363,363,363,363, + 363,11,17,39,52,61,78,86,95,101,105,122,165,181,183,197,200,213,225,237,251,261,264,276,280, + 285,293,308,318,323,328,342,347,353,360,364,367,377,383,387,391,396,406,414,425,440,448,450, + 454,465,470,477,481,489,492,509,512,532,535,550,552,555,565,572,577,579,0,11,15,29,34,46,54, + 61,82,85,88,103,114,133,141,154,163,179,201,210,228,233,238,244,258,263,273,280,286,302,305, + 310,314,323,326,330,342,347,349,351,364,372,390,397,405,414,415,437,449,452,456,463,472,475, + 481,490,499,517,522,533,558,567,574,578,2,21,27,33,41,52,63,75,83,87,105,115,118,122,132, + 136,150,159,163,165,176,179,191,200,210,215,216,219,221,228,232,256,281,284,288,294,308,317, + 322,323,327,339,342,348,359,364,373,378,381,392,403,406,411,415,421,430,433,440,448,453,457, + 462,480,483,490,500,519,521,532,544,563,573,577,2,7,38,41,52,62,66,69,75,83,91,93,95,101, + 104,110,115,130,132,135,137,141,155,159,162,164,166,169,175,186,193,199,206,219,226,231,235, + 242,246,254,260,283,287,290,301,308,314,320,330,337,343,351,364,374,384,387,399,404,410,421, + 433,444,447,459,466,480,489,490,498,500,502,513,517,521,524,528,532,539,557,560,567,573,577, + 0,4,21,30,35,54,63,73,80,81,88,99,107,112,116,119,126,127,135,138,143,145,155,160,163,165, + 170,172,177,184,207,220,224,233,243,246,264,279,281,287,300,313,320,334,345,350,357,361,364, + 370,380,397,402,406,411,427,444,447,450,458,478,489,491,499,504,522,532,541,560,572,0,27,31, + 50,59,64,78,86,88,93,95,98,107,108,111,112,124,126,143,145,149,150,152,154,156,161,164,168, + 170,171,184,190,194,206,211,232,243,248,263,267,274,282,297,303,309,321,327,353,356,364,379, + 404,414,429,432,447,449,454,466,477,489,494,505,517,532,535,546,564,572,4,6,24,31,33,36,52, + 56,72,86,98,100,111,112,118,122,124,127,136,141,148,150,152,154,160,166,173,178,197,208,215, + 231,237,249,251,269,272,280,299,308,310,316,321,331,341,346,352,363,369,378,384,386,405,407, + 413,423,426,430,440,447,448,461,464,473,481,489,492,502,524,532,545,559,572,0,13,21,27,31, + 42,50,71,80,92,101,121,124,134,139,148,157,167,172,174,177,196,206,210,216,222,233,236,251, + 266,287,293,297,300,307,313,321,324,328,339,346,348,351,358,365,367,373,381,384,387,395,402, + 405,420,429,446,452,461,471,473,478,481,489,494,508,522,531,544,571,577,1,12,19,26,33,42,52, + 58,79,85,97,102,110,112,117,123,129,137,142,149,153,159,162,172,176,179,182,187,194,208,224, + 235,253,260,281,293,298,306,312,328,340,349,360,363,367,371,378,393,401,405,417,424,429,436, + 446,448,466,468,478,484,489,497,503,506,515,521,531,535,553,556,562,570,571,3,13,20,25,32, + 35,38,42,46,52,80,99,102,111,115,121,128,132,141,147,154,160,172,177,179,200,204,216,221, + 229,248,252,271,276,279,283,289,294,298,302,305,326,343,347,358,367,370,372,382,390,395,400, + 405,412,426,431,433,445,448,458,469,473,483,489,490,495,504,509,521,526,531,535,547,570,10, + 18,25,29,53,59,73,93,96,106,114,127,130,133,147,164,169,182,188,201,208,217,227,233,245,246, + 248,255,267,273,276,277,278,286,289,293,299,304,314,325,333,341,345,363,365,373,386,405,407, + 414,425,426,435,445,447,460,467,473,478,482,486,489,495,506,513,523,531,547,568,570,0,11,25, + 30,51,53,69,70,83,90,103,105,115,121,135,143,161,163,172,183,185,187,190,194,201,203,216, + 223,228,233,243,263,265,281,286,297,302,314,320,339,342,351,374,380,382,387,391,399,403,405, + 419,426,438,446,452,458,465,473,476,488,497,503,511,520,531,532,534,549,570,572,4,5,22,24, + 42,51,55,56,59,72,74,80,84,105,115,117,129,134,135,137,145,151,159,171,178,182,195,203,217, + 221,227,234,240,255,258,277,278,290,298,309,316,319,332,336,340,343,348,350,358,363,369,372, + 379,389,395,397,402,405,413,417,423,429,430,438,445,447,466,472,480,485,488,496,514,531,534, + 554,567,570,4,13,25,34,49,54,68,73,75,100,101,114,120,141,146,157,161,174,183,184,194,201, + 217,226,236,250,273,278,292,294,297,301,315,319,321,325,345,369,384,392,399,405,415,427,438, + 441,445,463,470,486,488,509,515,530,532,547,555,570,8,32,51,72,73,92,99,113,133,144,151,152, + 159,173,176,181,185,196,206,219,240,243,260,278,280,295,301,316,319,327,332,336,341,353,362, + 373,387,394,405,410,427,445,450,461,469,482,488,493,514,529,539,545,570,571,8,11,30,49,56, + 68,74,86,92,105,111,117,131,132,140,149,162,168,182,194,201,204,217,251,272,294,301,316,319, + 337,365,387,404,409,416,422,427,432,434,439,445,446,450,460,469,486,491,513,518,529,534,537, + 570,10,20,26,42,44,48,53,64,69,87,92,109,131,152,154,163,177,194,199,213,218,256,277,281, + 299,303,317,319,341,362,364,372,386,404,406,440,445,452,464,487,508,516,521,525,529,544,570, + 573,14,34,49,56,76,92,97,120,139,146,174,195,206,216,234,238,259,272,279,280,301,307,319, + 329,336,348,352,373,379,394,404,415,423,437,442,445,457,465,478,486,487,487,494,507,521,524, + 529,539,566,570,18,30,43,50,61,69,88,99,128,149,171,193,207,213,220,243,249,265,283,319,326, + 340,350,377,383,391,405,426,432,436,440,443,447,450,458,463,484,487,506,526,528,555,570,576, + 6,17,25,46,72,74,93,114,136,152,158,165,175,179,193,200,222,245,250,260,267,272,291,306,319, + 328,343,348,354,381,398,405,411,419,428,431,435,441,445,450,470,484,498,520,528,539,563,2,16, + 19,24,36,41,69,74,80,85,90,102,127,133,163,176,200,223,247,263,278,287,295,312,319,323,342, + 360,374,378,396,405,438,445,461,487,500,528,529,537,557,571,9,16,23,42,46,77,83,89,98,111, + 128,157,166,175,210,228,235,247,264,278,294,319,342,374,409,428,444,459,487,495,506,528,549, + 568,571,7,13,19,27,40,42,77,93,95,121,132,154,161,180,204,228,235,253,258,291,303,328,353, + 360,379,385,390,394,396,402,404,436,444,451,454,460,463,465,468,477,484,486,509,548,574,4,19, + 21,42,45,67,89,93,132,135,156,163,179,204,226,252,260,276,286,300,325,341,366,369,374,378, + 389,404,415,435,444,457,467,478,486,501,520,528,539,553,570,4,11,16,34,39,56,68,78,92,100, + 110,123,135,146,151,162,169,191,215,237,259,282,292,305,328,332,343,351,367,375,377,389,403, + 407,412,434,444,454,461,463,476,486,488,504,528,541,565,569,5,9,15,39,59,68,78,88,97,117, + 137,157,164,177,191,217,231,251,272,276,301,312,332,338,352,365,373,387,402,407,426,444,446, + 467,485,486,505,509,525,528,531,538,565,568,7,12,16,18,42,65,68,87,109,134,153,163,175,209, + 231,255,274,279,297,301,323,334,345,360,368,374,390,411,424,432,444,455,461,478,483,486,499, + 510,522,524,528,544,558,568,8,11,12,18,34,38,52,55,68,71,91,110,111,124,145,166,197,217, + 235,239,259,277,286,296,312,316,318,322,334,352,360,373,392,402,411,424,433,441,443,460,465, + 478,486,497,515,527,533,545,551,567,569,17,18,40,43,51,63,85,90,123,133,150,159,163,182,206, + 229,235,253,269,276,285,293,341,349,359,366,375,390,402,414,425,435,443,449,461,469,480,485, + 486,493,504,508,519,523,530,534,554,567,573,5,19,47,68,91,111,121,132,151,165,171,189,208, + 220,240,260,280,300,313,321,341,376,381,400,402,420,424,433,439,442,445,453,472,473,485,489, + 494,498,508,515,525,527,533,535,549,560,567,569,9,29,38,62,86,92,110,120,134,157,164,181, + 205,220,244,250,268,275,292,302,316,339,364,372,379,388,401,410,435,442,458,462,466,472,479, + 483,484,492,503,507,509,516,527,532,540,547,556,568,5,18,25,29,38,59,68,79,96,119,137,156, + 169,176,189,196,199,208,237,249,269,288,298,307,326,361,377,401,402,442,447,454,463,477,485, + 487,494,502,511,516,522,523,527,535,542,562,568,571,17,29,31,41,54,76,94,97,108,120,143,150, + 166,189,190,216,237,244,260,273,276,293,300,314,317,338,359,381,401,403,425,437,442,448,472, + 478,484,494,498,504,516,518,525,530,533,539,542,554,564,568,574,10,16,22,29,37,46,60,81,103, + 108,119,125,146,165,187,206,227,242,248,268,272,280,290,291,310,331,349,353,366,375,380,395, + 406,416,417,427,436,442,448,456,465,475,484,493,496,508,513,518,526,540,554,567,10,15,17,24, + 29,39,62,80,85,98,108,110,119,126,157,169,182,189,198,221,233,244,246,265,268,271,290,314, + 330,353,368,375,383,399,411,423,442,447,470,493,505,511,515,525,531,542,554,567,14,20,24,35, + 56,57,67,79,100,120,123,142,163,172,184,190,206,219,233,241,254,261,274,295,315,336,357,371, + 379,393,400,410,415,420,427,437,442,457,461,478,484,500,513,521,525,535,557,567,6,8,10,22, + 24,36,44,58,66,67,74,89,108,111,125,134,149,155,179,202,225,233,241,253,254,271,284,292,300, + 315,322,338,344,350,352,369,376,392,401,405,417,422,428,439,442,444,459,464,471,473,483,485, + 488,494,501,512,518,525,534,545,554,558,396,396,396,396,397,397,397,397,398,398,398,398,398, + 398,398,398,398,398,398,399,399,399,399,399,400,400,400,400,401,401,401,400,400,401,401,402, + 402,403,404,404,406,403,404,405,405,404,404,405,405,405,406,406,406,406,408,409,410,440,439, + 439,438,437,437,437,437,438,438,439,439,439,439,439,439,440,439,440,440,440,440,441,441,442, + 442,442,444,444,445,445,445,445,445,446,446,446,445,445,446,447,447,447,447,449,449,448,448, + 447,447,446,478,478,478,478,478,478,478,479,479,479,480,480,481,481,481,482,482,482,482,482, + 482,482,481,481,482,481,482,483,483,483,483,483,483,484,485,486,485,485,485,485,486,486,486, + 487,487,487,487,487,488,489,489,489,489,489,489,489,489,489,489,490,491,519,519,519,520,520, + 521,521,521,521,522,522,522,522,522,522,522,523,522,523,524,524,525,526,525,525,524,524,526, + 527,527,527,527,528,528,528,529,529,531,531,531,531,532,532,532,532,534,535,562,562,562,563, + 563,563,562,563,563,563,563,563,563,563,563,563,564,564,564,564,564,564,564,565,565,566,566, + 566,566,566,566,566,566,566,567,567,567,567,567,566,566,567,567,569,570,570,571,570,569,569, + 569,569,569,570,570,571,571,572,572,573,573,573,572,567,13,24,35,58,79,101,125,129,144,172, + 186,206,221,227,231,233,241,248,272,286,303,308,316,319,329,344,350,359,363,371,382,391,405, + 411,418,425,432,435,442,446,452,461,468,477,483,492,505,511,514,516,525,531,552,572,3,19,25, + 29,36,42,74,89,100,104,108,112,137,160,168,183,191,207,229,241,254,279,303,318,328,352,366, + 372,378,388,403,408,414,416,420,425,429,450,454,468,471,478,483,486,493,502,525,534,547,549, + 572,5,18,20,40,42,59,65,79,86,128,150,162,172,216,221,234,238,241,259,273,293,319,338,378, + 399,417,420,426,436,440,442,443,454,461,462,464,476,481,489,492,503,531,537,546,6,16,19,26, + 37,60,67,83,97,108,121,145,168,189,204,216,225,227,236,250,266,272,274,294,316,318,321,336, + 344,358,365,369,375,389,400,404,414,418,425,432,437,450,458,464,468,473,480,485,493,503,524, + 529,532,545,566,568,22,45,67,76,87,110,139,161,172,183,191,205,227,246,262,270,275,282,296, + 299,303,305,318,323,327,336,347,358,366,369,376,390,394,400,401,413,436,441,458,464,469,480, + 503,525,548,565,571,5,19,25,36,61,84,109,133,157,174,180,191,204,227,233,239,250,256,273, + 296,318,334,341,344,352,358,375,386,397,416,439,442,447,452,462,476,486,490,496,506,521,543, + 566,2,12,15,22,36,44,52,62,81,88,117,138,160,171,183,191,205,216,227,247,255,268,291,300, + 312,326,335,355,363,370,375,380,387,394,409,426,432,442,454,460,475,484,490,497,518,525,540, + 550,560,565,9,22,24,36,45,68,83,106,131,151,162,173,196,200,221,246,257,261,273,277,292,300, + 304,317,324,328,339,346,353,361,365,374,393,397,407,413,420,429,435,439,453,458,479,481,504, + 515,526,549,565,571,12,22,35,59,82,103,107,111,125,147,161,170,190,199,200,212,220,242,262, + 273,285,291,301,307,325,330,347,352,363,368,378,382,388,403,414,418,421,429,445,462,492,496, + 514,523,538,556,560,12,23,30,47,53,70,77,106,109,139,157,173,199,222,247,258,273,292,305, + 315,330,338,362,364,381,388,392,394,398,419,424,430,444,467,482,490,495,539,571,3,12,19,34, + 42,64,68,82,86,106,108,131,154,160,177,191,199,222,245,263,267,273,283,305,315,319,328,349, + 371,393,422,431,443,449,472,494,519,524,537,560,564,3,11,15,37,39,54,60,63,70,72,86,94,107, + 118,141,153,165,191,197,226,248,263,270,273,285,293,314,315,321,328,351,364,372,378,381,400, + 414,438,442,463,479,482,490,506,527,548,564,569,3,11,15,38,53,57,60,81,91,104,106,118,141, + 150,157,166,189,214,238,261,274,277,301,328,353,367,377,402,404,425,438,457,461,479,481,503, + 524,525,528,542,548,552,560,564,571,12,39,46,69,74,91,106,114,136,147,150,160,184,190,215, + 238,262,274,285,309,315,325,334,349,379,403,408,424,448,468,474,475,483,489,494,511,519,526, + 533,539,545,556,564,12,20,25,38,44,47,74,92,98,106,115,117,123,132,146,150,162,172,198,221, + 246,272,297,322,346,359,372,396,408,420,435,444,453,468,476,482,486,492,507,516,522,528,537, + 541,542,551,564,17,40,64,71,74,83,88,93,110,132,155,161,176,200,221,244,267,290,305,316,328, + 350,372,395,408,418,425,439,440,448,464,480,485,509,521,524,532,533,540,548,564,570,1,7,13, + 23,42,46,67,69,86,92,115,123,139,165,199,257,283,307,312,320,324,348,372,408,416,421,428, + 438,453,462,486,510,534,560,1,2,18,49,57,72,80,103,126,149,170,186,210,234,256,276,298,321, + 343,356,367,389,406,408,421,430,437,448,452,475,479,496,498,519,522,529,544,567,23,50,75,102, + 105,109,113,119,122,127,137,143,169,179,203,230,261,273,278,304,314,329,344,354,356,378,403, + 412,428,448,452,470,477,501,510,527,531,552,569,12,20,28,42,47,64,66,74,88,98,102,132,135, + 148,161,168,180,184,198,204,228,231,243,266,273,289,312,315,328,344,349,374,397,403,411,418, + 428,437,441,466,468,479,489,513,525,536,550,564,11,38,53,58,80,106,110,131,158,182,184,196, + 210,234,262,279,289,306,332,350,356,380,383,409,425,434,438,458,471,479,483,508,525,533,557, + 16,31,56,61,70,93,107,131,159,178,184,201,211,257,273,279,282,302,315,334,344,358,376,390, + 405,418,428,440,443,453,466,475,479,489,506,520,525,537,543,563,568,14,17,39,47,61,74,109, + 118,137,148,156,177,181,189,199,216,232,243,270,286,305,341,354,367,376,396,401,410,422,431, + 461,475,492,518,545,570 + ], + "y": [ + 7,10,13,18,24,29,33,39,44,50,56,57,61,66,73,76,78,83,89,93,99,105,115,120,122,127,131, + 136,138,144,149,162,164,172,181,182,184,187,193,200,204,209,214,216,221,227,233,238,248,251, + 254,255,259,266,272,277,278,281,288,293,298,304,307,309,316,320,326,332,338,342,4,9,12,17, + 20,23,26,33,38,45,49,56,62,66,73,78,83,89,93,99,106,109,113,115,121,124,127,132,138,143, + 150,155,161,165,170,176,182,188,196,206,210,215,221,223,234,238,242,245,249,255,260,265,275, + 280,282,288,292,298,301,304,309,319,322,327,332,343,348,6,11,15,18,23,29,33,35,39,44,52,56, + 62,66,71,72,78,81,88,90,94,99,105,112,116,121,127,132,139,144,147,150,155,161,165,166,171, + 176,179,183,189,193,199,201,204,205,211,218,220,227,232,237,244,249,253,256,260,265,271,275, + 283,286,289,293,300,303,306,311,317,319,324,325,332,337,5,11,17,23,27,33,39,43,50,55,62,66, + 73,77,82,91,93,99,104,111,116,119,121,127,132,138,144,150,156,161,165,171,174,182,188,193, + 199,204,210,215,219,225,231,237,243,249,257,259,265,270,276,281,288,293,295,304,314,320,324, + 331,336,6,11,16,22,27,31,33,38,43,44,47,49,55,61,66,71,77,79,83,87,90,99,104,107,115,117, + 120,126,132,135,138,143,150,153,160,165,172,176,183,186,187,190,192,199,203,208,210,214,219, + 225,227,231,237,244,247,254,263,269,277,281,287,292,298,301,303,305,308,313,316,319,325,333, + 340,344,6,10,10,15,19,22,27,34,37,38,43,48,54,56,59,64,71,75,81,87,94,97,99,104,109,112, + 114,120,127,133,138,142,147,149,153,158,159,165,175,177,181,186,192,196,197,203,208,213,219, + 221,225,231,234,240,242,244,248,253,259,262,268,269,275,279,280,286,292,296,298,303,306,308, + 310,313,318,319,324,326,331,336,341,6,10,15,21,26,32,43,46,58,64,70,75,80,85,89,92,98,103, + 106,108,111,116,120,126,130,135,137,142,148,151,162,164,170,171,175,181,189,192,196,200,202, + 206,208,214,218,224,230,234,242,246,252,254,258,263,267,270,272,275,277,286,291,302,305,307, + 312,318,321,326,331,340,5,11,16,21,26,31,37,43,48,51,58,64,70,74,78,81,86,92,96,99,103, + 107,110,114,119,125,126,129,130,136,145,149,152,158,163,169,174,180,183,185,191,196,202,204, + 207,213,219,222,223,230,236,240,246,251,258,262,268,274,282,286,294,301,307,308,312,317,319, + 322,329,336,5,9,14,17,26,35,42,48,53,58,63,69,75,80,85,91,96,102,106,113,118,130,135,141, + 146,152,153,157,162,169,171,179,186,188,196,202,205,207,212,223,228,235,240,245,257,258,262, + 268,274,283,286,290,299,306,312,317,328,334,338,340,331,331,332,332,332,332,332,332,332,332, + 332,331,331,330,330,330,330,329,330,331,330,330,330,330,330,329,329,328,328,329,329,329,329, + 328,328,328,328,328,328,328,328,327,327,327,327,327,327,327,327,327,326,326,326,326,325,325, + 324,324,324,324,324,324,323,323,323,326,326,326,326,326,326,327,327,326,326,326,324,325,324, + 324,325,325,325,325,325,325,325,324,324,324,324,324,324,324,324,324,324,324,323,323,323,323, + 323,323,323,323,323,322,322,322,322,322,321,321,320,320,320,320,320,320,319,319,319,319,319, + 318,318,318,318,320,320,321,321,321,321,319,319,319,319,319,319,319,320,320,320,320,320,320, + 320,320,320,320,319,319,319,319,319,319,319,319,319,318,318,318,318,317,317,317,318,319,319, + 319,318,317,317,317,316,316,316,316,316,316,316,316,316,316,316,315,315,315,315,314,314,314, + 314,314,314,314,314,313,313,312,316,316,316,315,315,314,314,314,313,313,313,313,314,314,314, + 314,314,315,315,315,315,315,315,315,315,314,314,314,314,313,313,313,313,313,313,313,313,313, + 313,313,313,312,312,312,312,312,312,312,312,312,312,312,312,311,311,311,311,311,310,311,310, + 310,310,309,309,309,309,309,309,309,309,309,309,309,309,308,308,308,307,307,306,306,306,310, + 310,309,309,309,308,309,309,309,309,310,309,309,309,309,309,309,309,309,309,309,309,309,309, + 309,309,309,309,309,309,308,308,308,308,308,308,307,307,307,307,307,307,307,307,306,307,306, + 306,306,305,305,305,305,305,304,304,304,304,304,304,303,303,303,303,303,303,303,303,302,302, + 304,305,304,304,304,303,304,304,304,304,304,304,304,304,304,304,303,303,303,303,302,302,302, + 302,302,302,302,302,303,303,303,303,303,303,303,303,303,303,302,302,302,302,301,301,301,301, + 301,300,300,300,300,300,300,300,300,299,299,298,298,298,298,299,297,298,297,297,296,296,296, + 298,298,298,298,299,298,298,298,298,298,298,298,298,298,298,297,297,297,297,298,298,299,299, + 299,299,299,299,299,297,297,296,296,296,296,296,296,295,295,295,295,296,296,296,296,295,295, + 295,294,294,294,294,294,294,294,294,292,292,292,293,293,293,293,293,293,292,293,293,293,292, + 291,291,291,291,293,293,293,294,294,293,293,292,292,293,293,293,293,293,293,293,293,293,293, + 293,293,293,292,292,292,292,292,292,292,291,291,291,291,291,290,290,290,290,290,290,290,290, + 290,290,290,290,289,289,289,289,289,289,289,289,289,288,288,288,287,287,287,287,287,287,286, + 286,286,285,285,285,289,289,289,289,288,288,288,287,289,288,288,287,288,288,288,289,288,288, + 288,288,287,287,287,287,287,287,287,287,287,287,287,286,285,285,286,287,287,288,287,285,285, + 284,284,284,284,283,284,284,283,283,283,282,282,282,282,283,283,283,282,282,282,282,282,282, + 281,281,281,281,282,282,281,281,280,282,281,281,281,281,281,281,281,281,281,282,283,283,282, + 282,281,281,281,281,281,281,281,281,281,281,281,281,280,280,280,280,280,278,278,279,279,279, + 279,279,279,279,279,278,278,278,278,278,278,278,278,278,278,278,277,277,276,276,276,276,276, + 276,276,275,275,275,275,275,275,275,275,275,275,274,274,277,276,276,276,276,276,275,275,275, + 275,275,275,275,276,276,277,277,277,277,276,276,276,275,275,275,275,275,275,275,275,275,275, + 275,275,275,275,275,275,274,274,274,274,274,274,274,273,273,273,273,273,273,273,272,272,272, + 272,271,271,271,271,271,271,271,271,271,271,271,270,269,269,271,271,272,272,271,271,271,271, + 271,271,271,271,271,271,271,271,270,270,270,269,269,269,269,270,270,270,269,269,269,269,269, + 269,269,270,269,269,268,268,268,268,268,268,268,268,268,268,268,268,268,268,267,267,266,266, + 266,266,266,265,265,265,264,265,265,264,265,265,265,264,264,264,265,265,266,266,265,265,265, + 265,265,265,265,265,265,265,265,265,264,264,264,264,264,264,265,264,264,264,264,264,263,263, + 263,263,263,264,264,263,263,262,262,262,262,262,263,262,262,262,262,262,262,261,261,261,261, + 262,262,262,262,262,261,261,261,261,261,260,260,260,260,260,260,260,260,260,259,258,258,258, + 257,257,260,259,259,260,260,260,260,260,260,259,260,260,260,259,259,259,259,259,259,259,260, + 260,259,259,259,259,258,258,259,259,259,259,258,258,258,258,258,257,256,256,256,256,256,256, + 256,256,256,255,255,254,254,252,253,253,253,253,253,253,254,255,255,255,255,255,254,253,253, + 253,253,253,254,254,254,254,254,254,253,253,253,253,252,252,252,252,252,251,251,251,251,251, + 251,251,251,251,250,250,250,250,250,250,249,249,249,248,248,248,249,248,247,247,246,246,249, + 249,248,249,249,249,249,250,250,249,250,250,250,250,249,249,248,248,247,248,247,248,248,247, + 246,246,246,246,246,246,245,246,245,245,245,244,244,244,244,244,244,244,244,243,243,243,243, + 243,242,242,242,242,241,243,243,243,245,245,244,244,244,244,244,244,244,243,242,242,242,242, + 243,243,242,242,241,241,241,241,241,241,240,239,240,240,240,239,238,239,239,239,239,238,237, + 237,236,236,236,236,236,236,235,237,238,239,239,238,238,238,237,238,238,237,237,237,236,236, + 236,236,236,236,236,237,236,236,235,235,235,235,234,234,234,234,234,234,233,233,233,233,233, + 233,232,233,232,232,231,231,231,231,231,229,229,232,232,232,233,233,233,233,233,231,231,230, + 231,232,232,231,231,231,230,230,230,230,229,229,229,229,228,228,227,227,227,227,227,227,227, + 227,227,227,227,225,225,225,225,225,225,227,227,227,227,226,226,228,227,227,226,225,225,224, + 224,225,225,225,225,224,224,224,224,224,224,224,223,223,223,223,223,222,222,222,222,222,222, + 222,222,222,222,222,222,221,220,220,220,219,221,221,221,221,221,221,221,221,221,221,220,221, + 219,219,219,219,219,220,219,218,218,218,218,219,219,219,219,218,217,217,217,217,217,216,214, + 215,216,214,215,214,214,214,216,216,216,216,216,215,215,215,215,215,215,215,215,214,214,214, + 214,213,214,214,214,213,212,212,212,212,211,211,210,210,210,210,210,208,208,210,210,210,210, + 210,210,210,211,211,211,210,210,210,210,209,208,209,209,209,208,207,207,206,206,206,206,206, + 206,206,206,206,205,205,205,205,205,205,205,205,205,205,205,204,202,201,204,204,204,204,204, + 205,204,204,204,204,204,204,203,203,204,202,202,202,202,201,202,202,202,201,201,201,200,201, + 202,200,200,200,200,200,200,199,199,198,198,197,196,201,200,200,199,199,199,199,200,199,199, + 198,198,198,198,198,199,199,199,197,197,196,196,196,196,196,196,196,196,196,196,195,195,195, + 195,195,194,194,193,193,193,193,193,193,194,193,192,192,192,193,193,193,194,194,194,194,194, + 193,193,193,193,193,191,192,192,192,192,192,192,192,191,190,190,190,190,190,190,189,189,190, + 189,189,188,188,188,188,188,187,187,187,187,186,186,188,187,187,187,187,187,187,188,189,188, + 188,188,188,186,186,186,185,185,185,185,185,185,185,185,186,185,184,184,183,182,182,182,182, + 182,182,182,181,182,181,181,181,181,181,181,183,183,183,183,183,183,182,182,183,183,181,181, + 181,182,181,182,183,181,182,182,182,181,181,181,180,180,180,180,180,179,179,179,179,180,179, + 178,178,178,178,177,177,177,177,177,176,176,176,176,175,175,175,176,176,177,176,176,176,176, + 176,176,176,176,176,176,176,176,175,175,175,175,175,175,175,173,173,173,173,173,173,173,172, + 172,172,172,172,172,172,171,171,171,171,171,170,170,170,170,170,169,169,169,172,172,171,172, + 172,171,171,171,171,171,171,171,172,172,170,170,170,169,169,169,169,169,169,168,168,168,168, + 168,168,168,168,167,166,166,166,166,166,166,165,165,165,165,164,164,164,164,164,164,166,167, + 166,165,165,165,165,165,164,165,165,165,165,165,165,165,165,164,163,163,163,162,162,162,162, + 162,161,161,161,161,161,161,160,160,160,159,159,159,159,159,159,158,158,158,158,158,159,159, + 160,161,161,161,161,161,161,161,161,161,161,161,161,161,161,160,160,160,159,159,159,159,159, + 158,157,157,157,157,157,156,156,156,156,156,155,155,155,156,155,155,155,155,155,155,154,153, + 153,153,155,155,155,155,156,155,154,154,153,153,154,154,154,154,154,155,153,152,152,153,153, + 153,153,151,151,152,152,152,151,151,151,150,150,150,149,149,149,149,149,149,149,149,149,149, + 149,148,148,147,147,147,147,150,150,150,150,150,150,151,149,150,150,150,150,150,150,150,150, + 149,149,149,148,148,148,147,147,146,147,147,146,146,147,147,146,146,146,146,146,146,146,145, + 145,144,144,144,145,144,144,144,143,143,143,142,141,143,144,144,144,144,144,143,143,143,143, + 144,144,144,144,143,143,143,143,144,143,143,143,143,142,142,142,142,141,141,141,141,140,141, + 141,140,140,140,139,138,138,138,138,137,137,137,137,136,136,138,138,137,137,137,137,138,138, + 139,138,138,138,138,138,137,137,138,138,138,138,138,138,136,136,136,136,135,135,135,134,134, + 134,134,134,133,133,133,133,133,133,132,132,133,133,133,133,132,131,132,132,132,131,131,132, + 132,132,132,132,132,132,132,132,132,132,132,132,132,132,131,131,131,131,131,130,131,131,131, + 130,130,130,130,130,130,129,129,129,128,128,128,128,127,127,127,127,128,128,128,127,127,127, + 127,127,127,126,126,126,126,126,126,126,4,8,14,20,23,25,27,30,36,42,46,51,58,62,68,71,74, + 80,84,90,96,97,101,105,108,110,113,118,124,127,129,139,143,150,159,161,177,201,205,210,225, + 234,244,249,266,272,274,282,288,299,303,308,312,328,332,338,349,3,7,14,19,24,25,30,37,40, + 46,55,57,61,67,74,75,79,82,85,87,90,94,100,102,110,117,138,185,203,209,211,221,238,248,250, + 254,259,263,266,281,290,294,297,307,320,323,329,332,337,341,348,2,4,8,12,19,24,30,34,36,40, + 46,50,55,56,62,67,70,72,78,84,86,89,100,101,105,112,116,120,123,131,138,145,148,163,177, + 179,193,195,199,200,210,212,215,240,242,248,249,254,271,281,292,297,306,312,314,319,325,328, + 331,335,341,1,2,9,12,17,23,25,29,34,35,39,42,47,50,56,60,66,72,77,82,88,94,98,104,110, + 111,115,149,160,170,177,181,192,211,215,225,228,256,262,270,279,301,317,318,323,330,335,0,4, + 11,12,17,21,27,33,38,42,44,49,54,60,64,65,70,76,80,81,88,93,94,98,103,108,109,114,120, + 123,128,131,138,141,147,148,153,154,159,168,169,181,183,196,199,208,212,218,229,230,235,241, + 245,260,263,290,291,301,306,312,317,323,335,338,126,127,127,127,127,127,128,128,128,126,126, + 126,126,126,126,126,126,126,126,126,125,125,125,125,125,125,124,124,124,124,124,124,123,123, + 123,123,124,124,124,124,124,123,123,122,122,122,122,122,122,122,122,122,120,120,121,121,121, + 121,122,122,121,121,122,122,122,122,120,121,121,121,121,120,121,121,120,120,119,119,119,118, + 118,118,118,118,118,118,118,118,118,117,117,117,116,116,116,116,115,116,116,115,115,115,115, + 115,114,116,116,116,116,116,116,115,115,115,115,117,116,116,115,116,115,115,115,116,116,115, + 114,112,112,113,112,112,112,112,112,112,112,112,112,112,112,111,111,111,111,111,111,111,111, + 110,110,110,110,110,109,109,109,110,111,111,111,111,107,108,109,110,110,110,109,109,109,109, + 108,107,108,107,107,107,107,107,107,107,107,107,107,107,107,107,107,107,106,106,105,105,105, + 105,105,105,104,104,104,104,104,103,103,105,106,105,105,106,104,104,104,104,103,103,104,104, + 104,103,102,103,103,103,104,104,102,102,102,102,102,101,101,102,102,101,101,101,101,101,101, + 101,101,100,100,100,100,99,98,98,99,99,98,99,99,99,99,100,98,99,99,98,98,98,98,99,99,98, + 98,98,98,97,96,96,96,96,96,96,96,96,96,96,94,94,95,94,94,94,94,94,94,94,94,93,93,94,94, + 94,95,94,94,94,94,94,95,94,93,93,93,93,93,93,93,93,91,92,92,92,92,92,92,91,91,91,91,90, + 90,90,90,90,90,90,90,90,89,89,89,89,89,88,88,88,88,88,88,89,88,88,88,88,89,89,88,89,90, + 89,88,87,87,88,87,86,86,85,84,85,86,86,86,86,86,86,86,85,85,85,84,84,84,84,84,84,84,84, + 84,84,84,84,84,83,82,82,81,81,81,83,83,83,83,83,84,84,84,85,82,82,82,82,82,82,81,81,81, + 80,80,80,80,81,81,81,81,80,80,80,80,80,80,80,80,80,80,80,79,79,78,78,78,77,76,77,76,76, + 77,78,78,78,78,78,78,78,78,77,77,77,77,76,75,75,75,75,74,74,74,74,75,74,74,73,73,73,73, + 74,74,74,74,73,73,72,72,71,69,73,73,73,73,73,73,73,73,73,72,72,73,72,72,71,71,72,71,70, + 70,70,70,70,71,70,70,69,69,69,69,68,68,67,67,67,67,66,66,66,65,66,67,66,66,67,67,66,66, + 66,66,66,66,65,66,67,66,66,66,65,65,65,63,64,64,64,65,65,64,64,64,63,63,63,63,63,63,62, + 63,62,62,62,62,62,61,60,60,61,60,60,61,61,61,62,62,63,63,60,60,60,61,61,60,61,61,60,60, + 60,59,59,59,59,58,58,58,58,58,58,58,57,56,56,56,57,57,56,55,55,55,55,55,55,55,54,54,55, + 55,55,56,56,55,55,55,55,55,55,55,55,54,54,54,53,53,53,53,53,53,53,53,52,52,52,52,52,51, + 51,51,50,50,50,50,50,49,49,49,49,49,49,50,51,50,49,49,49,49,49,49,49,49,49,49,49,49,49, + 50,49,49,48,48,48,48,48,47,47,47,46,46,46,46,45,46,46,45,45,45,45,45,44,44,44,43,43,43, + 43,43,44,44,45,45,44,44,44,44,44,44,44,44,44,43,43,44,43,43,43,43,43,42,42,42,42,41,41, + 40,41,40,40,40,40,39,39,39,39,39,39,39,38,38,39,40,40,40,39,39,38,38,38,38,39,39,39,39, + 38,38,37,37,37,37,37,37,36,36,35,35,36,36,36,36,36,35,34,33,34,34,33,33,33,33,33,33,33, + 33,33,33,33,34,33,32,31,31,31,31,31,30,30,29,29,29,29,30,30,30,30,29,29,29,29,28,28,27, + 29,27,27,29,29,29,29,29,29,28,27,27,27,27,27,27,27,26,26,26,26,26,26,26,26,25,25,25,25, + 25,24,24,24,23,23,22,22,22,21,24,24,24,23,23,23,23,23,23,23,23,22,22,22,22,22,22,22,22, + 22,22,22,21,22,21,21,20,20,20,20,20,19,20,19,19,19,19,19,19,18,18,18,18,17,17,17,16,17, + 18,18,17,17,16,18,18,17,16,16,16,16,17,15,16,15,15,16,15,15,14,14,14,14,14,14,14,13,13, + 13,12,12,11,11,11,13,13,12,12,12,11,11,11,11,12,11,11,10,10,10,10,10,10,10,10,9,9,9,9, + 8,7,7,7,7,7,8,8,8,7,7,6,6,6,5,4,4,7,7,7,6,6,6,6,6,5,5,5,5,5,5,6,6,5,5,5,5,5,4, + 4,3,3,3,4,4,3,3,2,2,2,1,1,0 + ], + "v": [ + 7,6,9,12,18,24,31,43,49,61,70,72,80,88,100,103,107,102,89,63,36,-3,-26,-21,-21,23,60,90, + 81,7,18,52,45,25,-29,-37,-48,-73,-99,-108,-114,-114,-112,-111,-105,-103,-109,-102,-49,-41,-29, + -28,-44,-31,-31,-45,-53,-70,-69,-62,-45,-39,-37,-60,-60,-56,-62,-82,-107,-116,69,66,62,60,55, + 50,45,39,33,27,21,16,8,4,-2,-7,-8,-16,-20,-26,-29,-31,-36,-35,-36,-37,-39,-39,-41,-42,-44, + -48,-52,-56,-59,-71,-78,-85,-87,-99,-101,-97,-89,-85,-56,-65,-46,-37,-16,-4,-6,-22,-41,-50, + -51,-59,-60,-65,-63,-65,-72,-86,-91,-97,-102,-97,-91,13,9,7,4,2,-4,-5,-4,-2,2,4,2,-6,-22, + -28,-28,-30,-41,-46,-46,-49,-50,-51,-53,-55,-58,-54,-56,-53,-56,-54,-54,-54,-56,-57,-59,-61, + -63,-66,-68,-71,-73,-75,-74,-72,-73,-68,-62,-61,-57,-51,-46,-38,-31,-26,-19,-8,8,27,38,51, + 53,57,77,115,141,154,190,167,136,54,44,45,47,16,6,1,-3,-6,-12,-15,-16,-22,-37,-66,-72,-83, + -87,-89,-96,-96,-95,-95,-95,-94,-93,-91,-91,-90,-92,-88,-86,-84,-79,-78,-75,-71,-66,-57,-55, + -52,-51,-48,-44,-44,-33,-22,-4,13,35,23,23,26,39,65,69,54,73,85,111,115,141,162,182,204,57, + 52,51,48,40,33,31,23,16,15,12,7,-1,-9,-18,-27,-34,-38,-42,-49,-66,-78,-83,-83,-74,-74,-77, + -78,-82,-82,-84,-83,-79,-73,-67,-60,-48,-41,-25,-19,-17,-11,-7,7,17,30,35,44,55,68,70,88, + 130,157,159,131,77,61,47,39,18,10,-3,-1,-19,-26,-34,-31,-15,0,25,33,-21,69,106,104,104,99, + 101,98,100,100,101,103,101,101,105,104,102,101,101,99,91,85,116,130,137,165,113,97,87,72,91, + 107,115,119,112,107,97,89,91,107,133,139,173,197,218,230,233,231,221,201,171,162,141,128,129, + 134,138,138,156,169,138,105,86,79,45,22,16,-3,4,7,17,26,28,28,23,33,48,59,116,135,139,145, + 141,161,159,156,154,152,152,155,156,152,149,142,136,129,119,108,97,93,90,87,76,71,90,121, + 186,194,152,139,142,64,62,124,136,141,143,160,179,180,175,180,186,186,179,172,143,119,108, + 118,133,117,113,112,111,111,109,107,96,87,70,74,72,83,39,-1,-44,-47,-81,-100,-141,-159,-146, + 141,136,131,128,126,125,133,121,118,116,102,87,71,56,44,34,24,12,-12,-16,-18,-19,-24,-2,69, + 125,134,153,160,202,204,145,164,181,135,70,74,102,106,104,99,90,75,64,49,27,29,32,37,37,34, + 15,12,11,-15,-23,-17,-16,-23,-30,-47,-87,-101,-104,-103,-107,-106,-105,-99,-96,107,94,79,76, + 55,51,67,85,106,129,132,96,58,33,28,31,-2,-9,-11,-35,-29,-5,5,2,-13,-10,-8,17,41,106,96, + 94,62,51,-1,-41,-65,-67,-89,-69,-63,-65,-65,-62,-53,-54,-52,-54,-63,-73,-67,-72,-91,-78,-51, + -25,-40,-52,-61,-66,-112,-109,-56,-89,-100,-89,-71,-14,65,26,109,57,105,101,28,33,-6,95,139, + 65,5,14,-150,-157,-161,-149,-111,-101,-99,-94,-78,-66,1,-41,-34,-28,46,29,26,16,6,30,47,68, + 62,48,47,50,30,30,24,29,6,-1,-38,-41,-6,-2,51,53,63,116,169,261,258,-96,-106,-94,-57,-48, + -67,-90,-92,-83,-76,-61,85,50,288,414,162,75,152,12,-30,68,93,120,157,75,58,-58,-132,-146, + -120,-113,-109,-106,-106,-101,-97,-86,-49,-53,-62,-14,90,23,5,26,42,44,67,59,50,42,37,27,24, + 19,3,-20,-48,-42,-29,48,108,167,195,-97,-76,-56,-56,-63,-96,-84,-87,-71,-48,150,126,126,151, + 315,387,206,106,100,112,239,200,29,-14,-45,18,7,18,11,22,40,158,-95,-100,-111,-123,-111,-102, + -107,-106,-103,-96,-96,-86,-53,-25,31,30,27,-4,15,18,26,37,45,63,64,59,55,50,44,41,22,19, + 2,-23,-60,-64,-48,-13,85,114,118,-87,-92,-71,-76,-97,-76,-71,-69,-79,-70,-22,-3,39,188,241, + 186,223,319,322,309,305,316,121,103,113,137,135,169,212,49,-10,-31,-44,8,9,24,28,42,50,116, + 153,-61,-64,-71,-117,-112,-108,-98,-108,-104,-101,-91,-51,-22,-17,-13,-7,11,17,25,59,47,34, + 24,21,15,-1,-3,-22,-27,-30,-56,-75,-93,-63,-73,-69,-57,89,66,49,56,68,-86,-89,-65,-55,-68, + -91,-68,-72,-66,-62,-34,81,174,185,285,226,258,262,230,232,223,218,101,132,132,160,196,234, + 137,35,-47,-6,6,39,8,0,133,-18,-44,-59,-104,-103,-99,-101,-95,-94,-81,-81,-78,-67,-52,-26, + -7,4,7,19,10,9,6,7,0,-9,-9,-16,-27,-67,-82,-69,-13,-6,-88,-36,-20,-84,-76,-71,-61,-27,-18, + 45,35,36,137,141,148,146,157,158,91,92,90,97,104,102,112,174,147,139,138,137,54,11,-1,-46, + -43,43,-7,-20,111,124,88,3,-90,-81,-66,-87,-96,-94,-95,-90,-71,-21,-19,-6,-6,-12,-11,-11, + -10,-23,-20,-18,-39,-72,-90,-89,-78,-49,-41,-94,-92,-42,-59,-69,-68,-94,-92,-60,-31,30,38,97, + 97,95,102,104,99,61,45,69,83,89,97,148,142,111,101,-3,-48,-41,20,7,-39,-37,117,116,63,-61, + -41,-43,-44,-56,-79,-83,-86,-91,-84,-85,-65,-77,-80,-48,-44,-41,-25,-29,-25,-25,-19,-17,-32, + -34,-32,-34,-39,-42,-54,-93,-96,-89,-76,-66,-112,-62,-52,-66,-74,-82,-100,-56,-37,11,35,66, + 62,38,33,62,82,126,135,120,128,10,-35,-30,-34,-31,7,4,-47,55,13,-29,-27,-28,-16,-28,-38, + -45,-49,-78,-73,-73,-76,-84,-69,-68,-63,-59,-58,-52,-73,-65,-56,-14,-33,-37,-40,-36,-25,-28, + -29,-32,-45,-56,-73,-83,-88,-91,-84,-86,-108,-58,-61,-69,-87,-72,-102,-97,-28,-17,35,47,53, + 57,68,62,39,32,34,63,54,36,42,89,94,103,85,67,21,-25,-26,-3,-32,-8,72,-5,-11,13,-18,-49, + -75,-75,-81,-72,-68,-69,-62,-48,-37,-34,-25,-27,-32,-35,-54,-55,-27,-31,-39,-43,-53,-63,-65, + -67,-64,-67,-72,-73,-89,-89,-86,-96,-95,-89,-56,-62,-70,-78,-86,-88,-98,-98,-97,-10,74,80,51, + 44,60,47,56,57,67,36,18,44,64,65,20,8,26,20,25,-11,-6,38,71,61,45,22,10,24,27,37,-33, + -68,-68,-66,-63,-59,-59,-39,-42,-38,-40,-22,-27,-40,-30,-33,-44,-47,-40,-54,-56,-56,-62,-64, + -66,-55,-63,-52,-56,-56,-59,-83,-103,-50,-27,-45,-52,-93,-86,-16,72,71,44,31,73,117,134,74, + 9,21,57,53,34,50,82,61,50,28,26,26,8,30,52,67,74,70,43,35,31,20,17,-8,-26,-37,-47,-67, + -64,-61,-52,-27,-39,-36,-27,-41,-43,-38,-53,-54,-58,-66,-61,-57,-58,-56,-58,-69,-55,-56,-51, + -55,-77,-100,-102,-80,-60,-31,-53,-97,-98,-32,-20,51,57,36,29,18,39,189,109,4,3,65,66,63, + 62,60,61,65,73,125,109,96,88,31,56,50,108,96,40,29,-7,-17,-41,-45,-63,-38,-34,-32,-19,-14, + -24,-21,-22,-41,-32,-19,-33,-48,-44,-43,-45,-41,-58,-56,-65,-69,-68,-65,-65,-66,-83,-97,-97, + -66,-65,-29,-31,-80,-96,-83,-80,-83,-1,19,42,40,8,8,6,100,170,150,126,51,26,5,73,64,61,78, + 84,130,125,115,113,107,72,71,115,109,93,57,6,-18,-23,-24,-31,-33,-33,-41,-43,-55,-52,-42, + -40,-25,-4,1,13,28,12,-21,-27,-26,-19,-18,-23,-16,-14,-40,-49,-54,-57,-61,-67,-67,-74,-71, + -78,-84,-84,-68,-44,-19,-43,-87,-85,-6,31,32,-18,-18,-2,25,8,35,10,20,84,73,74,108,111,156, + 151,138,127,107,111,93,92,76,69,5,-15,-21,-26,-35,-39,-19,-11,-4,-2,-24,-12,-7,-11,-2,-24, + -33,-52,-55,-71,-67,-65,-63,-60,-61,-71,-74,-41,-88,14,21,-32,-36,-21,56,45,36,33,27,75,88, + 83,89,137,146,177,170,167,143,112,112,111,99,16,11,-18,-29,-41,-46,-51,-55,-40,-30,-19,-5,1, + -11,-17,-19,-28,-32,-29,-30,-43,-60,-51,-47,-47,-67,-67,-75,-74,-55,-85,-85,-22,-11,-43,4, + -40,-31,-13,42,43,40,41,19,33,83,156,159,156,175,177,117,113,110,29,12,53,-60,-36,-15,-8, + -8,-12,-13,-22,-25,-27,-36,-35,-38,-33,-29,-1,-7,-41,-51,-51,-50,-47,-67,-87,-80,-76,-88,-89, + -86,-89,-62,-37,-61,-63,-41,16,13,6,-4,39,152,174,164,160,165,118,113,112,109,37,24,-51,-65, + -67,-67,-58,-41,-41,-32,-39,-43,-34,-21,-49,-59,-52,-55,-61,-62,-69,-69,-103,-100,-101,-98, + -63,-71,-67,-27,3,7,-7,130,153,144,130,126,144,130,122,119,113,106,34,-19,-36,-45,-52,-73, + -72,-69,-61,-52,-51,-53,-52,-52,-54,-53,-39,-38,-39,-41,-52,-71,-73,-75,-68,-85,-79,-82,-107, + -108,-106,-107,-93,-76,-79,-73,-14,-16,-34,88,142,153,150,122,121,118,119,37,-3,-51,-56,-84, + -81,-75,-80,-71,-71,-73,-70,-71,-72,-73,-71,-69,-66,-67,-85,-89,-89,-88,-88,-86,-95,-108,-99, + -113,-76,-73,-84,-48,-16,-33,-45,-51,-35,-19,68,105,163,121,119,104,109,106,116,91,36,-25, + -62,-69,-68,-89,-87,-91,-89,-90,-90,-90,-91,-94,-96,-98,-92,-93,-98,-106,-108,-108,-100,-94, + -106,-104,-98,-122,-125,-91,-84,-81,-86,-89,-78,-25,-23,-53,-34,102,188,138,114,119,128,122, + 64,29,4,-62,-88,-85,-86,-96,-95,-104,-110,-121,-122,-114,-123,-120,-117,-107,-103,-106,-107, + -99,-125,-129,-93,-93,-96,-90,-66,-25,-51,-55,-42,161,208,201,175,148,146,144,27,-49,-92,-102, + -109,-116,-128,-131,-137,-137,-128,-121,-110,-110,-103,-99,-97,-114,-132,-133,-95,-94,-93,-47, + -27,-51,-57,-31,112,221,221,200,196,169,116,22,-54,-67,-76,-79,-83,-87,-90,-95,-101,-117,-122, + -121,-118,-116,-118,-119,-120,-131,-137,-139,-143,-126,-113,-97,-98,-99,-130,-128,-104,-99,-101, + -28,-24,-55,-61,-34,92,219,222,215,189,176,141,56,7,-53,-59,-61,-67,-79,-100,-114,-129,-125, + -135,-135,-133,-136,-140,-136,-134,-131,-121,-114,-90,-89,-79,-124,-118,-110,-95,-97,-95,-90, + -75,-47,-33,-44,-52,-61,-60,2,134,236,221,167,156,133,65,53,35,17,-12,-20,-26,-63,-97,-102, + -105,-116,-118,-111,-120,-122,-101,-122,-131,-134,-123,-124,-113,-110,-59,-54,-52,-117,-100,-85, + -97,-95,-89,-58,-40,-65,-68,-53,-8,139,203,234,189,175,138,115,62,54,41,49,28,-17,-89,-97, + -108,-106,-106,-103,-64,-71,-116,-118,-106,-105,-112,-118,-104,-102,-7,-7,9,-3,-96,-82,-84,-93, + -74,-44,-61,-72,-58,56,179,229,185,171,140,132,93,70,59,63,75,81,1,-81,-111,-92,-89,-72,-51, + -59,-47,-47,-83,-100,-90,-90,-95,-100,-97,-95,17,21,26,48,-77,-79,-71,-70,-78,-81,-87,-66, + -64,-53,-58,-73,-5,82,173,193,215,176,143,135,105,105,102,99,90,95,94,121,19,-46,-67,-93, + -84,-78,-76,-44,-15,-53,-48,-61,-77,-78,-88,-87,-89,-86,-85,74,75,-61,-64,-63,-68,-79,-82, + -53,-58,-71,-80,-81,-62,5,109,130,169,173,160,138,121,113,119,106,120,132,46,-40,-71,-83,-66, + -67,-57,-41,-2,-50,-50,-52,-49,-62,-54,-65,-69,-77,-79,-78,-74,-69,-35,70,-53,-63,-74,-59, + -52,-55,-74,-80,-80,-52,5,51,134,147,135,74,59,81,102,126,117,-22,-30,-74,-75,-73,-69,-70, + -69,-54,-37,-38,-47,-45,-46,-49,-32,-42,-59,-59,-70,-70,-67,-58,-62,-61,-27,48,-12,-51,-66, + -70,-58,-51,-59,-84,-85,-70,-11,51,148,157,145,135,98,126,126,98,43,123,92,83,-10,-47,-71, + -67,-48,-44,-43,-38,-39,-36,-34,-30,-17,-17,-18,4,-6,-29,-41,-46,-41,-45,-43,-1,42,153,-7, + -47,-52,-58,-66,-52,-61,-86,-91,-86,-67,-45,-42,-9,105,144,102,89,72,107,190,18,47,-10,-15, + -69,-64,-61,-45,-45,-34,-17,-20,-16,-12,5,-8,-19,-23,-30,-41,-33,-33,-32,-24,152,107,-18,-42, + -52,-61,-60,-55,-52,-75,-82,-95,-71,-70,13,116,132,83,62,63,54,73,151,163,90,-11,29,-22,-31, + -67,-70,-66,-59,-57,0,-13,25,17,19,0,1,13,-7,-11,-10,-19,-27,-28,-22,-27,-45,-34,-2,67,-1, + -31,-44,-50,-55,-54,-50,-51,-83,-95,-81,-37,75,125,116,68,64,58,54,53,125,93,0,-6,-21,-9, + -2,-22,-22,-37,-38,-61,-60,-57,-52,-54,-56,3,1,104,120,19,26,12,6,-5,-10,-12,-10,-13,-13,7, + 22,-5,-40,-47,-46,-54,-56,-56,-55,-60,-95,-95,-92,-84,-69,29,120,116,115,110,106,102,91,266, + 118,19,-20,-14,-6,-31,-18,-34,-38,-49,-22,44,26,56,41,9,8,14,0,-6,24,61,81,28,-43,-42,-41, + -42,-52,-57,-63,-84,-93,-77,-89,-84,-60,15,114,106,124,126,150,141,213,60,5,-3,-8,-34,-31, + -22,-26,-31,-22,-39,-42,-10,27,32,45,74,35,18,16,11,0,-4,-13,-1,3,42,60,8,-22,-43,-41,-39, + -38,-44,-56,-56,-64,-76,-90,-92,-89,-60,70,103,95,119,120,194,233,193,143,166,139,46,24,23, + 14,-19,-5,-25,-26,-26,-14,-18,-20,-38,-45,-48,36,24,43,51,111,106,98,81,128,31,22,15,14,9, + 9,2,171,166,158,155,147,143,138,128,105,81,65,54,61,67,82,86,84,72,110,46,52,55,43,34,33, + 25,10,-20,-26,-27,-26,-42,-26,-22,-16,-10,-49,-100,-102,-103,-88,-61,-16,-5,-27,-39,-36,-34, + -57,-21,-14,2,12,30,41,35,42,140,146,154,158,161,162,162,145,121,69,-6,-9,-12,13,9,10,23, + 32,35,40,48,43,15,16,-6,-9,-36,-99,-124,-111,-114,-98,-40,-29,-17,-8,-16,-28,-33,-53,-35, + -19,-12,24,59,51,47,48,51,55,70,202,199,192,182,166,151,125,108,103,87,71,65,58,63,55,53, + 54,55,59,69,73,74,71,72,67,56,57,51,61,45,-3,-9,-12,-47,-48,-50,-126,-134,-136,-134,-134, + -133,-122,-10,0,-31,-44,-55,-58,-54,-39,-24,-8,3,2,3,6,6,6,2,-6,68,69,69,69,66,63,53,62, + 58,58,58,63,58,53,47,50,53,51,51,50,49,41,38,38,37,36,32,8,7,-79,-78,-95,-123,-128,-123, + -89,-81,-75,-70,-55,-66,-84,-29,-29,-9,19,44,65,67,69,69,69,71,74,72,70,63,62,51,41,32,25, + 24,22,25,28,29,13,7,8,6,5,3,5,3,3,1,-1,-4,-6,-12,-22,-28,-33,-34,-45,-72,-74,-95,-99, + -114,-115,-110,-103,-96,-82,-80,-69,-67,-68,-92,-97,-69,-66,-11,55,114,158,169,100,120,-19,23, + -6,-42,-41,-49,-70,-72,-89,-90,-81,-39,75,86,101,87,67,88,184,135,106,117,125,134,107,22,15, + -10,-7,-27,-9,-24,-29,-23,-1,-24,-30,-27,-42,-35,-22,54,43,39,61,65,82,58,47,39,22,16,7,2, + -45,-10,-22,-22,-18,-24,-33,-37,-51,-51,-58,-61,-83,-93,-93,-86,-77,-24,83,54,91,123,56,99, + 56,-18,-11,-32,-19,-1,-23,-28,-11,-13,-9,-28,-23,-15,-10,28,33,45,57,64,61,72,32,18,13,12, + 4,-35,32,20,-9,-18,-36,-34,-30,-33,-80,-94,-94,-92,18,24,102,81,47,79,88,53,-2,-8,0,10, + -30,-22,3,-23,-13,-8,-7,22,26,25,36,50,56,65,76,74,26,22,14,-20,5,10,-7,16,-31,-31,-29, + -41,-53,-69,-92,-95,-87,-39,-92,27,37,112,63,81,77,76,22,-13,-19,-6,-23,-39,-11,-2,-14,1, + 39,33,39,11,8,34,20,15,15,38,50,52,63,65,69,83,80,39,36,40,17,5,4,-12,22,-29,-24,-30, + -56,-89,-97,-84,-92,-87,-18,120,80,107,96,90,69,38,33,39,47,-18,-3,-26,-21,-12,1,19,13,-8, + 47,67,43,41,34,17,16,43,65,63,72,60,38,15,6,4,21,17,36,105,-25,-23,-50,-86,-96,-97,-93, + -78,29,160,137,123,129,125,93,45,-16,-4,-9,-6,13,-2,-3,16,54,29,41,46,50,65,74,82,82,74, + 86,63,41,23,7,25,32,32,52,135,71,7,-18,-21,-25,-65,-91,-97,-102,-88,-67,45,78,100,133,138, + 116,55,47,18,-1,-6,30,24,13,16,18,29,43,50,22,24,49,82,77,80,73,70,72,45,49,31,21,16,14, + 24,74,89,108,80,-18,-23,-44,-80,-97,-97,-95,-13,7,50,126,143,142,119,111,69,55,52,19,6,1, + -4,0,9,39,46,47,93,109,41,42,26,15,24,35,70,64,70,68,63,56,49,26,29,19,27,88,181,0,-15, + -36,-41,-38,-71,-90,-93,-88,-42,-3,2,27,50,121,148,129,101,87,76,63,20,18,16,25,56,61,80, + 89,115,71,37,33,30,8,39,47,65,68,60,52,35,24,25,62,105,158,91,39,-6,-1,-28,-30,-83,-89, + -79,-5,64,136,148,136,101,80,56,52,46,76,85,131,100,108,102,85,31,21,3,19,43,55,61,65,38, + 22,78,41,67,158,139,0,-2,11,7,-27,-30,-71,-85,-85,-69,-27,-2,69,130,150,150,142,128,88,71, + 67,73,88,129,96,20,-10,15,9,43,62,56,48,40,25,25,71,54,57,146,145,44,20,15,2,-4,31,16, + -22,-44,-72,-71,-64,-18,-2,86,135,150,150,147,131,127,90,87,85,90,123,142,136,103,94,68,40, + -14,-12,37,52,56,64,60,46,35,32,35,53,51,55,136,105,36,24,43,49,6,-6,-36,-59,-61,-58,-58, + -12,52,113,152,152,148,119,102,119,137,79,57,56,15,-10,42,30,60,63,64,46,54,62,40,42,40, + 38,41,49,51,117,96,16,9,45,2,-19,-43,-36,-37,-40,-12,-1,61,118,148,155,148,118,111,108,104, + 102,60,55,53,19,31,49,59,61,62,65,68,54,51,67,49,44,51,49,51,44,51,62,104,99,87,12,31, + 23,3,-14,-17,-29,-31,-22,-22,-27,-21,24,78,126,153,139,110,84,86,65,64,77,73,70,76,78,73, + 70,71,74,73,58,67,63,65,51,51,52,53,62,35,91,32,22,18,11,14,19,2,-19,-18,-18,-6,36,83, + 123,150,153,140,121,105,70,60,77,105,112,118,118,118,127,119,87,77,59,58,58,63,62,56,58,71, + 73,35,32,32,41,86,79,33,23,14,12,-12,-18,-17,-10,44,139,157,137,137,114,107,63,62,133,136, + 143,148,144,153,144,85,55,59,71,41,38,25,48,58,32,23,0,-17,-12,5,25,66,102,130,153,147, + 114,62,46,64,112,143,145,151,159,162,168,169,135,125,74,78,62,62,63,65,76,24,66,34,-2,-4, + -8,-10,-14,-20,-19,-12,-11,11,27,58,97,139,152,149,141,125,92,58,53,55,116,151,155,156,166, + 169,169,157,83,69,62,60,67,75,19,16,21,51,63,49,50,41,21,2,4,-15,-7,-3,9,18,44,52,54,60, + 95,98,114,145,154,159,133,128,98,77,74,145,153,153,154,155,150,158,161,179,182,169,138,72, + 64,64,66,69,14,28,58,64,39,6,5,-5,8,47,49,54,70,106,145,162,162,147,91,75,87,193,190,145, + 140,148,153,172,187,182,171,84,64,63,73,9,18,60,64,57,24,7,2,14,43,48,58,68,139,159,165, + 165,156,136,86,72,104,224,194,143,131,136,147,153,168,186,188,193,167,102,70,67,63,64,68,65, + 7,5,33,49,69,64,10,8,12,16,26,49,55,57,63,83,106,126,163,171,156,86,112,223,271,171,149, + 130,130,130,212,206,161,70,65,64 + ] +} diff --git a/test/interval-test.js b/test/interval-test.js new file mode 100644 index 0000000000..4f7fcefbf9 --- /dev/null +++ b/test/interval-test.js @@ -0,0 +1,64 @@ +import assert from "assert"; +import {numberInterval} from "../src/options.js"; + +describe("numberInterval(interval)", () => { + it("coerces the given interval to a number", () => { + assert.deepStrictEqual(numberInterval("1").range(0, 10), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + }); + it("implements range", () => { + assert.deepStrictEqual(numberInterval(1).range(0, 10), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + assert.deepStrictEqual(numberInterval(1).range(1, 9), [1, 2, 3, 4, 5, 6, 7, 8]); + assert.deepStrictEqual(numberInterval(2).range(1, 9), [2, 4, 6, 8]); + assert.deepStrictEqual(numberInterval(-1).range(2, 5), [2, 3, 4]); + assert.deepStrictEqual(numberInterval(-2).range(2, 5), [2, 2.5, 3, 3.5, 4, 4.5]); + assert.deepStrictEqual(numberInterval(2).range(0, 10), [0, 2, 4, 6, 8]); + assert.deepStrictEqual(numberInterval(-2).range(0, 5), [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5]); + }); + it("considers descending ranges to be empty", () => { + assert.deepStrictEqual(numberInterval(1).range(10, 0), []); + assert.deepStrictEqual(numberInterval(1).range(-1, -9), []); + }); + it("considers invalid ranges to be empty", () => { + assert.deepStrictEqual(numberInterval(1).range(0, Infinity), []); + assert.deepStrictEqual(numberInterval(1).range(NaN, 0), []); + }); + it("considers invalid intervals to be empty", () => { + assert.deepStrictEqual(numberInterval(NaN).range(0, 10), []); + assert.deepStrictEqual(numberInterval(-Infinity).range(0, 10), []); + assert.deepStrictEqual(numberInterval(0).range(0, 10), []); + }); + it("implements floor", () => { + assert.strictEqual(numberInterval(1).floor(9.9), 9); + assert.strictEqual(numberInterval(2).floor(9), 8); + assert.strictEqual(numberInterval(-2).floor(8.6), 8.5); + }); + it("implements offset", () => { + assert.strictEqual(numberInterval(1).offset(8), 9); + assert.strictEqual(numberInterval(2).offset(8), 10); + assert.strictEqual(numberInterval(-2).offset(8), 8.5); + }); + it("implements offset with step", () => { + assert.strictEqual(numberInterval(1).offset(8, 2), 10); + assert.strictEqual(numberInterval(2).offset(8, 2), 12); + assert.strictEqual(numberInterval(-2).offset(8, 2), 9); + }); + it("does not require an aligned offset", () => { + assert.strictEqual(numberInterval(2).offset(7), 9); + assert.strictEqual(numberInterval(-2).offset(7.1), 7.6); + }); + it("floors the offset step", () => { + assert.strictEqual(numberInterval(1).offset(8, 2.5), 10); + assert.strictEqual(numberInterval(2).offset(8, 2.5), 12); + assert.strictEqual(numberInterval(-2).offset(8, 2.5), 9); + }); + it("coerces the offset step", () => { + assert.strictEqual(numberInterval(1).offset(8, "2.5"), 10); + assert.strictEqual(numberInterval(2).offset(8, "2.5"), 12); + assert.strictEqual(numberInterval(-2).offset(8, "2.5"), 9); + }); + it("allows a negative offset step", () => { + assert.strictEqual(numberInterval(1).offset(8, -2), 6); + assert.strictEqual(numberInterval(2).offset(8, -2), 4); + assert.strictEqual(numberInterval(-2).offset(8, -2), 7); + }); +}); diff --git a/test/jsdom.js b/test/jsdom.js index 94e9193c25..28781b2f70 100644 --- a/test/jsdom.js +++ b/test/jsdom.js @@ -19,7 +19,6 @@ function withJsdom(run) { const jsdom = new JSDOM(""); global.window = jsdom.window; global.document = jsdom.window.document; - global.navigator = jsdom.window.navigator; global.Event = jsdom.window.Event; global.Node = jsdom.window.Node; global.NodeList = jsdom.window.NodeList; @@ -30,7 +29,6 @@ function withJsdom(run) { } finally { delete global.window; delete global.document; - delete global.navigator; delete global.Event; delete global.Node; delete global.NodeList; diff --git a/test/marks/format-test.js b/test/marks/format-test.js index 3dad221547..a46754d1a4 100644 --- a/test/marks/format-test.js +++ b/test/marks/format-test.js @@ -1,14 +1,22 @@ import * as Plot from "@observablehq/plot"; import assert from "assert"; +it("formatNumber(locale) does the right thing", () => { + assert.strictEqual(Plot.formatNumber()(Math.PI), "3.142"); + assert.strictEqual(Plot.formatNumber()(12345), "12,345"); + assert.strictEqual(Plot.formatNumber("en")(Math.PI), "3.142"); + assert.strictEqual(Plot.formatNumber("en")(12345), "12,345"); + assert.strictEqual(Plot.formatNumber("fr")(Math.PI), "3,142"); + assert.strictEqual(Plot.formatNumber("fr")(12345), "12\u202f345"); +}); + it("formatMonth(locale, format) does the right thing", () => { assert.strictEqual(Plot.formatMonth("en", "long")(0), "January"); assert.strictEqual(Plot.formatMonth("en", "short")(0), "Jan"); assert.strictEqual(Plot.formatMonth("en", "narrow")(0), "J"); }); -// GitHub Actions does not support locales. -it.skip("formatMonth('fr', format) does the right thing", () => { +it("formatMonth('fr', format) does the right thing", () => { assert.strictEqual(Plot.formatMonth("fr", "long")(11), "décembre"); assert.strictEqual(Plot.formatMonth("fr", "short")(11), "déc."); assert.strictEqual(Plot.formatMonth("fr", "narrow")(11), "D"); @@ -27,7 +35,7 @@ it("formatMonth(locale) has the expected default", () => { assert.strictEqual(Plot.formatMonth("en", undefined)(0), "Jan"); }); -it.skip("formatMonth('fr') has the expected default", () => { +it("formatMonth('fr') has the expected default", () => { assert.strictEqual(Plot.formatMonth("fr")(11), "déc."); assert.strictEqual(Plot.formatMonth("fr", undefined)(11), "déc."); }); diff --git a/test/marks/time-test.js b/test/marks/time-test.js index 8445bcd820..250cb150df 100644 --- a/test/marks/time-test.js +++ b/test/marks/time-test.js @@ -1,155 +1,155 @@ import assert from "assert"; import * as d3 from "d3"; -import {maybeTimeInterval, maybeUtcInterval} from "../../src/time.js"; +import {timeInterval, utcInterval} from "../../src/time.js"; -it("maybeTimeInterval('period') returns the expected time interval", () => { - assert.strictEqual(maybeTimeInterval("second"), d3.timeSecond); - assert.strictEqual(maybeTimeInterval("minute"), d3.timeMinute); - assert.strictEqual(maybeTimeInterval("hour"), d3.timeHour); - assert.strictEqual(maybeTimeInterval("day"), d3.timeDay); - assert.strictEqual(maybeTimeInterval("week"), d3.timeWeek); - assert.strictEqual(maybeTimeInterval("month"), d3.timeMonth); - assert.strictEqual(maybeTimeInterval("year"), d3.timeYear); - assert.strictEqual(maybeTimeInterval("monday"), d3.timeMonday); - assert.strictEqual(maybeTimeInterval("tuesday"), d3.timeTuesday); - assert.strictEqual(maybeTimeInterval("wednesday"), d3.timeWednesday); - assert.strictEqual(maybeTimeInterval("thursday"), d3.timeThursday); - assert.strictEqual(maybeTimeInterval("friday"), d3.timeFriday); - assert.strictEqual(maybeTimeInterval("saturday"), d3.timeSaturday); - assert.strictEqual(maybeTimeInterval("sunday"), d3.timeSunday); +it("timeInterval('period') returns the expected time interval", () => { + assert.strictEqual(timeInterval("second"), d3.timeSecond); + assert.strictEqual(timeInterval("minute"), d3.timeMinute); + assert.strictEqual(timeInterval("hour"), d3.timeHour); + assert.strictEqual(timeInterval("day"), d3.timeDay); + assert.strictEqual(timeInterval("week"), d3.timeWeek); + assert.strictEqual(timeInterval("month"), d3.timeMonth); + assert.strictEqual(timeInterval("year"), d3.timeYear); + assert.strictEqual(timeInterval("monday"), d3.timeMonday); + assert.strictEqual(timeInterval("tuesday"), d3.timeTuesday); + assert.strictEqual(timeInterval("wednesday"), d3.timeWednesday); + assert.strictEqual(timeInterval("thursday"), d3.timeThursday); + assert.strictEqual(timeInterval("friday"), d3.timeFriday); + assert.strictEqual(timeInterval("saturday"), d3.timeSaturday); + assert.strictEqual(timeInterval("sunday"), d3.timeSunday); }); -it("maybeTimeInterval('periods') returns the expected time interval", () => { - assert.strictEqual(maybeTimeInterval("seconds"), d3.timeSecond); - assert.strictEqual(maybeTimeInterval("minutes"), d3.timeMinute); - assert.strictEqual(maybeTimeInterval("hours"), d3.timeHour); - assert.strictEqual(maybeTimeInterval("days"), d3.timeDay); - assert.strictEqual(maybeTimeInterval("weeks"), d3.timeWeek); - assert.strictEqual(maybeTimeInterval("months"), d3.timeMonth); - assert.strictEqual(maybeTimeInterval("years"), d3.timeYear); - assert.strictEqual(maybeTimeInterval("mondays"), d3.timeMonday); - assert.strictEqual(maybeTimeInterval("tuesdays"), d3.timeTuesday); - assert.strictEqual(maybeTimeInterval("wednesdays"), d3.timeWednesday); - assert.strictEqual(maybeTimeInterval("thursdays"), d3.timeThursday); - assert.strictEqual(maybeTimeInterval("fridays"), d3.timeFriday); - assert.strictEqual(maybeTimeInterval("saturdays"), d3.timeSaturday); - assert.strictEqual(maybeTimeInterval("sundays"), d3.timeSunday); +it("timeInterval('periods') returns the expected time interval", () => { + assert.strictEqual(timeInterval("seconds"), d3.timeSecond); + assert.strictEqual(timeInterval("minutes"), d3.timeMinute); + assert.strictEqual(timeInterval("hours"), d3.timeHour); + assert.strictEqual(timeInterval("days"), d3.timeDay); + assert.strictEqual(timeInterval("weeks"), d3.timeWeek); + assert.strictEqual(timeInterval("months"), d3.timeMonth); + assert.strictEqual(timeInterval("years"), d3.timeYear); + assert.strictEqual(timeInterval("mondays"), d3.timeMonday); + assert.strictEqual(timeInterval("tuesdays"), d3.timeTuesday); + assert.strictEqual(timeInterval("wednesdays"), d3.timeWednesday); + assert.strictEqual(timeInterval("thursdays"), d3.timeThursday); + assert.strictEqual(timeInterval("fridays"), d3.timeFriday); + assert.strictEqual(timeInterval("saturdays"), d3.timeSaturday); + assert.strictEqual(timeInterval("sundays"), d3.timeSunday); }); -it("maybeTimeInterval('1 periods) returns the expected time interval", () => { - assert.strictEqual(maybeTimeInterval("1 second"), d3.timeSecond); - assert.strictEqual(maybeTimeInterval("1 minute"), d3.timeMinute); - assert.strictEqual(maybeTimeInterval("1 hour"), d3.timeHour); - assert.strictEqual(maybeTimeInterval("1 day"), d3.timeDay); - assert.strictEqual(maybeTimeInterval("1 week"), d3.timeWeek); - assert.strictEqual(maybeTimeInterval("1 month"), d3.timeMonth); - assert.strictEqual(maybeTimeInterval("1 year"), d3.timeYear); - assert.strictEqual(maybeTimeInterval("1 monday"), d3.timeMonday); - assert.strictEqual(maybeTimeInterval("1 tuesday"), d3.timeTuesday); - assert.strictEqual(maybeTimeInterval("1 wednesday"), d3.timeWednesday); - assert.strictEqual(maybeTimeInterval("1 thursday"), d3.timeThursday); - assert.strictEqual(maybeTimeInterval("1 friday"), d3.timeFriday); - assert.strictEqual(maybeTimeInterval("1 saturday"), d3.timeSaturday); - assert.strictEqual(maybeTimeInterval("1 sunday"), d3.timeSunday); +it("timeInterval('1 periods) returns the expected time interval", () => { + assert.strictEqual(timeInterval("1 second"), d3.timeSecond); + assert.strictEqual(timeInterval("1 minute"), d3.timeMinute); + assert.strictEqual(timeInterval("1 hour"), d3.timeHour); + assert.strictEqual(timeInterval("1 day"), d3.timeDay); + assert.strictEqual(timeInterval("1 week"), d3.timeWeek); + assert.strictEqual(timeInterval("1 month"), d3.timeMonth); + assert.strictEqual(timeInterval("1 year"), d3.timeYear); + assert.strictEqual(timeInterval("1 monday"), d3.timeMonday); + assert.strictEqual(timeInterval("1 tuesday"), d3.timeTuesday); + assert.strictEqual(timeInterval("1 wednesday"), d3.timeWednesday); + assert.strictEqual(timeInterval("1 thursday"), d3.timeThursday); + assert.strictEqual(timeInterval("1 friday"), d3.timeFriday); + assert.strictEqual(timeInterval("1 saturday"), d3.timeSaturday); + assert.strictEqual(timeInterval("1 sunday"), d3.timeSunday); }); -it("maybeTimeInterval('1 periods') returns the expected time interval", () => { - assert.strictEqual(maybeTimeInterval("1 seconds"), d3.timeSecond); - assert.strictEqual(maybeTimeInterval("1 minutes"), d3.timeMinute); - assert.strictEqual(maybeTimeInterval("1 hours"), d3.timeHour); - assert.strictEqual(maybeTimeInterval("1 days"), d3.timeDay); - assert.strictEqual(maybeTimeInterval("1 weeks"), d3.timeWeek); - assert.strictEqual(maybeTimeInterval("1 months"), d3.timeMonth); - assert.strictEqual(maybeTimeInterval("1 years"), d3.timeYear); - assert.strictEqual(maybeTimeInterval("1 mondays"), d3.timeMonday); - assert.strictEqual(maybeTimeInterval("1 tuesdays"), d3.timeTuesday); - assert.strictEqual(maybeTimeInterval("1 wednesdays"), d3.timeWednesday); - assert.strictEqual(maybeTimeInterval("1 thursdays"), d3.timeThursday); - assert.strictEqual(maybeTimeInterval("1 fridays"), d3.timeFriday); - assert.strictEqual(maybeTimeInterval("1 saturdays"), d3.timeSaturday); - assert.strictEqual(maybeTimeInterval("1 sundays"), d3.timeSunday); +it("timeInterval('1 periods') returns the expected time interval", () => { + assert.strictEqual(timeInterval("1 seconds"), d3.timeSecond); + assert.strictEqual(timeInterval("1 minutes"), d3.timeMinute); + assert.strictEqual(timeInterval("1 hours"), d3.timeHour); + assert.strictEqual(timeInterval("1 days"), d3.timeDay); + assert.strictEqual(timeInterval("1 weeks"), d3.timeWeek); + assert.strictEqual(timeInterval("1 months"), d3.timeMonth); + assert.strictEqual(timeInterval("1 years"), d3.timeYear); + assert.strictEqual(timeInterval("1 mondays"), d3.timeMonday); + assert.strictEqual(timeInterval("1 tuesdays"), d3.timeTuesday); + assert.strictEqual(timeInterval("1 wednesdays"), d3.timeWednesday); + assert.strictEqual(timeInterval("1 thursdays"), d3.timeThursday); + assert.strictEqual(timeInterval("1 fridays"), d3.timeFriday); + assert.strictEqual(timeInterval("1 saturdays"), d3.timeSaturday); + assert.strictEqual(timeInterval("1 sundays"), d3.timeSunday); }); -it("maybeTimeInterval('n seconds') returns the expected time interval", () => { +it("timeInterval('n seconds') returns the expected time interval", () => { const start = new Date("2012-01-01T12:01:02"); const end = new Date("2012-01-01T12:14:08"); - assert.deepStrictEqual(maybeTimeInterval("5 seconds").range(start, end), d3.timeSecond.every(5).range(start, end)); - assert.deepStrictEqual(maybeTimeInterval("15 seconds").range(start, end), d3.timeSecond.every(15).range(start, end)); - assert.deepStrictEqual(maybeTimeInterval("45 seconds").range(start, end), d3.timeSecond.every(45).range(start, end)); + assert.deepStrictEqual(timeInterval("5 seconds").range(start, end), d3.timeSecond.every(5).range(start, end)); + assert.deepStrictEqual(timeInterval("15 seconds").range(start, end), d3.timeSecond.every(15).range(start, end)); + assert.deepStrictEqual(timeInterval("45 seconds").range(start, end), d3.timeSecond.every(45).range(start, end)); }); -it("maybeUtcInterval('period') returns the expected UTC interval", () => { - assert.strictEqual(maybeUtcInterval("second"), d3.utcSecond); - assert.strictEqual(maybeUtcInterval("minute"), d3.utcMinute); - assert.strictEqual(maybeUtcInterval("hour"), d3.utcHour); - assert.strictEqual(maybeUtcInterval("day"), d3.unixDay); - assert.strictEqual(maybeUtcInterval("week"), d3.utcWeek); - assert.strictEqual(maybeUtcInterval("month"), d3.utcMonth); - assert.strictEqual(maybeUtcInterval("year"), d3.utcYear); - assert.strictEqual(maybeUtcInterval("monday"), d3.utcMonday); - assert.strictEqual(maybeUtcInterval("tuesday"), d3.utcTuesday); - assert.strictEqual(maybeUtcInterval("wednesday"), d3.utcWednesday); - assert.strictEqual(maybeUtcInterval("thursday"), d3.utcThursday); - assert.strictEqual(maybeUtcInterval("friday"), d3.utcFriday); - assert.strictEqual(maybeUtcInterval("saturday"), d3.utcSaturday); - assert.strictEqual(maybeUtcInterval("sunday"), d3.utcSunday); +it("utcInterval('period') returns the expected UTC interval", () => { + assert.strictEqual(utcInterval("second"), d3.utcSecond); + assert.strictEqual(utcInterval("minute"), d3.utcMinute); + assert.strictEqual(utcInterval("hour"), d3.utcHour); + assert.strictEqual(utcInterval("day"), d3.unixDay); + assert.strictEqual(utcInterval("week"), d3.utcWeek); + assert.strictEqual(utcInterval("month"), d3.utcMonth); + assert.strictEqual(utcInterval("year"), d3.utcYear); + assert.strictEqual(utcInterval("monday"), d3.utcMonday); + assert.strictEqual(utcInterval("tuesday"), d3.utcTuesday); + assert.strictEqual(utcInterval("wednesday"), d3.utcWednesday); + assert.strictEqual(utcInterval("thursday"), d3.utcThursday); + assert.strictEqual(utcInterval("friday"), d3.utcFriday); + assert.strictEqual(utcInterval("saturday"), d3.utcSaturday); + assert.strictEqual(utcInterval("sunday"), d3.utcSunday); }); -it("maybeUtcInterval('periods') returns the expected UTC interval", () => { - assert.strictEqual(maybeUtcInterval("seconds"), d3.utcSecond); - assert.strictEqual(maybeUtcInterval("minutes"), d3.utcMinute); - assert.strictEqual(maybeUtcInterval("hours"), d3.utcHour); - assert.strictEqual(maybeUtcInterval("days"), d3.unixDay); - assert.strictEqual(maybeUtcInterval("weeks"), d3.utcWeek); - assert.strictEqual(maybeUtcInterval("months"), d3.utcMonth); - assert.strictEqual(maybeUtcInterval("years"), d3.utcYear); - assert.strictEqual(maybeUtcInterval("mondays"), d3.utcMonday); - assert.strictEqual(maybeUtcInterval("tuesdays"), d3.utcTuesday); - assert.strictEqual(maybeUtcInterval("wednesdays"), d3.utcWednesday); - assert.strictEqual(maybeUtcInterval("thursdays"), d3.utcThursday); - assert.strictEqual(maybeUtcInterval("fridays"), d3.utcFriday); - assert.strictEqual(maybeUtcInterval("saturdays"), d3.utcSaturday); - assert.strictEqual(maybeUtcInterval("sundays"), d3.utcSunday); +it("utcInterval('periods') returns the expected UTC interval", () => { + assert.strictEqual(utcInterval("seconds"), d3.utcSecond); + assert.strictEqual(utcInterval("minutes"), d3.utcMinute); + assert.strictEqual(utcInterval("hours"), d3.utcHour); + assert.strictEqual(utcInterval("days"), d3.unixDay); + assert.strictEqual(utcInterval("weeks"), d3.utcWeek); + assert.strictEqual(utcInterval("months"), d3.utcMonth); + assert.strictEqual(utcInterval("years"), d3.utcYear); + assert.strictEqual(utcInterval("mondays"), d3.utcMonday); + assert.strictEqual(utcInterval("tuesdays"), d3.utcTuesday); + assert.strictEqual(utcInterval("wednesdays"), d3.utcWednesday); + assert.strictEqual(utcInterval("thursdays"), d3.utcThursday); + assert.strictEqual(utcInterval("fridays"), d3.utcFriday); + assert.strictEqual(utcInterval("saturdays"), d3.utcSaturday); + assert.strictEqual(utcInterval("sundays"), d3.utcSunday); }); -it("maybeUtcInterval('1 periods) returns the expected UTC interval", () => { - assert.strictEqual(maybeUtcInterval("1 second"), d3.utcSecond); - assert.strictEqual(maybeUtcInterval("1 minute"), d3.utcMinute); - assert.strictEqual(maybeUtcInterval("1 hour"), d3.utcHour); - assert.strictEqual(maybeUtcInterval("1 day"), d3.unixDay); - assert.strictEqual(maybeUtcInterval("1 week"), d3.utcWeek); - assert.strictEqual(maybeUtcInterval("1 month"), d3.utcMonth); - assert.strictEqual(maybeUtcInterval("1 year"), d3.utcYear); - assert.strictEqual(maybeUtcInterval("1 monday"), d3.utcMonday); - assert.strictEqual(maybeUtcInterval("1 tuesday"), d3.utcTuesday); - assert.strictEqual(maybeUtcInterval("1 wednesday"), d3.utcWednesday); - assert.strictEqual(maybeUtcInterval("1 thursday"), d3.utcThursday); - assert.strictEqual(maybeUtcInterval("1 friday"), d3.utcFriday); - assert.strictEqual(maybeUtcInterval("1 saturday"), d3.utcSaturday); - assert.strictEqual(maybeUtcInterval("1 sunday"), d3.utcSunday); +it("utcInterval('1 periods) returns the expected UTC interval", () => { + assert.strictEqual(utcInterval("1 second"), d3.utcSecond); + assert.strictEqual(utcInterval("1 minute"), d3.utcMinute); + assert.strictEqual(utcInterval("1 hour"), d3.utcHour); + assert.strictEqual(utcInterval("1 day"), d3.unixDay); + assert.strictEqual(utcInterval("1 week"), d3.utcWeek); + assert.strictEqual(utcInterval("1 month"), d3.utcMonth); + assert.strictEqual(utcInterval("1 year"), d3.utcYear); + assert.strictEqual(utcInterval("1 monday"), d3.utcMonday); + assert.strictEqual(utcInterval("1 tuesday"), d3.utcTuesday); + assert.strictEqual(utcInterval("1 wednesday"), d3.utcWednesday); + assert.strictEqual(utcInterval("1 thursday"), d3.utcThursday); + assert.strictEqual(utcInterval("1 friday"), d3.utcFriday); + assert.strictEqual(utcInterval("1 saturday"), d3.utcSaturday); + assert.strictEqual(utcInterval("1 sunday"), d3.utcSunday); }); -it("maybeUtcInterval('1 periods') returns the expected UTC interval", () => { - assert.strictEqual(maybeUtcInterval("1 seconds"), d3.utcSecond); - assert.strictEqual(maybeUtcInterval("1 minutes"), d3.utcMinute); - assert.strictEqual(maybeUtcInterval("1 hours"), d3.utcHour); - assert.strictEqual(maybeUtcInterval("1 days"), d3.unixDay); - assert.strictEqual(maybeUtcInterval("1 weeks"), d3.utcWeek); - assert.strictEqual(maybeUtcInterval("1 months"), d3.utcMonth); - assert.strictEqual(maybeUtcInterval("1 years"), d3.utcYear); - assert.strictEqual(maybeUtcInterval("1 mondays"), d3.utcMonday); - assert.strictEqual(maybeUtcInterval("1 tuesdays"), d3.utcTuesday); - assert.strictEqual(maybeUtcInterval("1 wednesdays"), d3.utcWednesday); - assert.strictEqual(maybeUtcInterval("1 thursdays"), d3.utcThursday); - assert.strictEqual(maybeUtcInterval("1 fridays"), d3.utcFriday); - assert.strictEqual(maybeUtcInterval("1 saturdays"), d3.utcSaturday); - assert.strictEqual(maybeUtcInterval("1 sundays"), d3.utcSunday); +it("utcInterval('1 periods') returns the expected UTC interval", () => { + assert.strictEqual(utcInterval("1 seconds"), d3.utcSecond); + assert.strictEqual(utcInterval("1 minutes"), d3.utcMinute); + assert.strictEqual(utcInterval("1 hours"), d3.utcHour); + assert.strictEqual(utcInterval("1 days"), d3.unixDay); + assert.strictEqual(utcInterval("1 weeks"), d3.utcWeek); + assert.strictEqual(utcInterval("1 months"), d3.utcMonth); + assert.strictEqual(utcInterval("1 years"), d3.utcYear); + assert.strictEqual(utcInterval("1 mondays"), d3.utcMonday); + assert.strictEqual(utcInterval("1 tuesdays"), d3.utcTuesday); + assert.strictEqual(utcInterval("1 wednesdays"), d3.utcWednesday); + assert.strictEqual(utcInterval("1 thursdays"), d3.utcThursday); + assert.strictEqual(utcInterval("1 fridays"), d3.utcFriday); + assert.strictEqual(utcInterval("1 saturdays"), d3.utcSaturday); + assert.strictEqual(utcInterval("1 sundays"), d3.utcSunday); }); -it("maybeUtcInterval('n seconds') returns the expected UTC interval", () => { +it("utcInterval('n seconds') returns the expected UTC interval", () => { const start = new Date("2012-01-01T12:01:02"); const end = new Date("2012-01-01T12:14:08"); - assert.deepStrictEqual(maybeUtcInterval("5 seconds").range(start, end), d3.utcSecond.every(5).range(start, end)); - assert.deepStrictEqual(maybeUtcInterval("15 seconds").range(start, end), d3.utcSecond.every(15).range(start, end)); - assert.deepStrictEqual(maybeUtcInterval("45 seconds").range(start, end), d3.utcSecond.every(45).range(start, end)); + assert.deepStrictEqual(utcInterval("5 seconds").range(start, end), d3.utcSecond.every(5).range(start, end)); + assert.deepStrictEqual(utcInterval("15 seconds").range(start, end), d3.utcSecond.every(15).range(start, end)); + assert.deepStrictEqual(utcInterval("45 seconds").range(start, end), d3.utcSecond.every(45).range(start, end)); }); diff --git a/test/output/interpolateBarycentric4.svg b/test/output/interpolateBarycentric4.svg new file mode 100644 index 0000000000..26f848e98b --- /dev/null +++ b/test/output/interpolateBarycentric4.svg @@ -0,0 +1,71 @@ + + + + + 265 + 266 + 267 + 268 + 269 + 270 + 271 + 272 + 273 + 274 + 275 + 276 + + + + 100 + 150 + 200 + 250 + 300 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/interpolateBarycentric4k.svg b/test/output/interpolateBarycentric4k.svg new file mode 100644 index 0000000000..d005e0f9a1 --- /dev/null +++ b/test/output/interpolateBarycentric4k.svg @@ -0,0 +1,54 @@ + + + + + 0 + 50 + 100 + 150 + 200 + 250 + 300 + 350 + + + + 100 + 200 + 300 + 400 + 500 + + + + + + \ No newline at end of file diff --git a/test/output/logTickFormatFunction.svg b/test/output/logTickFormatFunction.svg new file mode 100644 index 0000000000..f8e3c26905 --- /dev/null +++ b/test/output/logTickFormatFunction.svg @@ -0,0 +1,82 @@ + + + + + 1 + 2 + + + + + + + + 10 + 20 + + + + + + + + 100 + 200 + + + + + + + + 1,000 + 2,000 + + + + \ No newline at end of file diff --git a/test/output/logTickFormatFunctionSv.svg b/test/output/logTickFormatFunctionSv.svg new file mode 100644 index 0000000000..628d7f37d2 --- /dev/null +++ b/test/output/logTickFormatFunctionSv.svg @@ -0,0 +1,82 @@ + + + + + 1 + 2 + + + + + + + + 10 + 20 + + + + + + + + 100 + 200 + + + + + + + + 1 000 + 2 000 + + + + \ No newline at end of file diff --git a/test/output/mixedFacets.svg b/test/output/mixedFacets.svg new file mode 100644 index 0000000000..78d9af4406 --- /dev/null +++ b/test/output/mixedFacets.svg @@ -0,0 +1,148 @@ + + + + + a + + + b + + + + name + + + + 2024Jan + + + Feb + + + + + + 0 + 2 + 4 + 6 + 8 + + + 0 + 2 + 4 + 6 + 8 + + + + ↑ value + + + + + a + b + + + a + b + + + + name + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/percentNull.svg b/test/output/percentNull.svg new file mode 100644 index 0000000000..3cd911e600 --- /dev/null +++ b/test/output/percentNull.svg @@ -0,0 +1,47 @@ + + + + + 100 + + + + 1.0 + 1.5 + 2.0 + 2.5 + 3.0 + 3.5 + 4.0 + 4.5 + 5.0 + + + + + \ No newline at end of file diff --git a/test/output/pointerViewofTitle.html b/test/output/pointerViewofTitle.html new file mode 100644 index 0000000000..066b5f8008 --- /dev/null +++ b/test/output/pointerViewofTitle.html @@ -0,0 +1,406 @@ +
+
+

Penguins

+ + + + 14 + 15 + 16 + 17 + 18 + 19 + 20 + 21 + + + ↑ culmen_depth_mm + + + + 35 + 40 + 45 + 50 + 55 + + + culmen_length_mm → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
\ No newline at end of file diff --git a/test/output/rasterPenguinsBarycentric.svg b/test/output/rasterPenguinsBarycentric.svg index 1a3097079d..ce7d198caf 100644 --- a/test/output/rasterPenguinsBarycentric.svg +++ b/test/output/rasterPenguinsBarycentric.svg @@ -66,7 +66,7 @@ body_mass_g → - + diff --git a/test/output/rasterPenguinsBarycentricBlur.svg b/test/output/rasterPenguinsBarycentricBlur.svg new file mode 100644 index 0000000000..f420a79fac --- /dev/null +++ b/test/output/rasterPenguinsBarycentricBlur.svg @@ -0,0 +1,415 @@ + + + + + 175 + 180 + 185 + 190 + 195 + 200 + 205 + 210 + 215 + 220 + 225 + 230 + + + ↑ flipper_length_mm + + + + 3,000 + 3,500 + 4,000 + 4,500 + 5,000 + 5,500 + 6,000 + + + body_mass_g → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/tipFormatTitleIgnoreFormat.svg b/test/output/tipFormatTitleFormat.svg similarity index 96% rename from test/output/tipFormatTitleIgnoreFormat.svg rename to test/output/tipFormatTitleFormat.svg index f83f3730b5..bf3a364540 100644 --- a/test/output/tipFormatTitleIgnoreFormat.svg +++ b/test/output/tipFormatTitleFormat.svg @@ -22,7 +22,7 @@ - ​0 + ​0.01 \ No newline at end of file diff --git a/test/output/tipFormatTitleFormatFunction.svg b/test/output/tipFormatTitleFormatFunction.svg new file mode 100644 index 0000000000..e766740988 --- /dev/null +++ b/test/output/tipFormatTitleFormatFunction.svg @@ -0,0 +1,28 @@ + + + + + 0 + + + + + ​0.02 + + + \ No newline at end of file diff --git a/test/output/tipFormatTitleFormatShorthand.svg b/test/output/tipFormatTitleFormatShorthand.svg new file mode 100644 index 0000000000..24916ca8cf --- /dev/null +++ b/test/output/tipFormatTitleFormatShorthand.svg @@ -0,0 +1,28 @@ + + + + + 0 + + + + + ​0.03 + + + \ No newline at end of file diff --git a/test/output/zeroNegativeDegenerateY.svg b/test/output/zeroNegativeDegenerateY.svg new file mode 100644 index 0000000000..9ebb283ba2 --- /dev/null +++ b/test/output/zeroNegativeDegenerateY.svg @@ -0,0 +1,75 @@ + + + + + −0.24 + −0.22 + −0.20 + −0.18 + −0.16 + −0.14 + −0.12 + −0.10 + −0.08 + −0.06 + −0.04 + −0.02 + 0.00 + + + + 0.0 + 0.2 + 0.4 + 0.6 + 0.8 + 1.0 + 1.2 + 1.4 + 1.6 + 1.8 + 2.0 + + + + + \ No newline at end of file diff --git a/test/output/zeroNegativeY.svg b/test/output/zeroNegativeY.svg new file mode 100644 index 0000000000..b3f5493ae1 --- /dev/null +++ b/test/output/zeroNegativeY.svg @@ -0,0 +1,75 @@ + + + + + −0.24 + −0.22 + −0.20 + −0.18 + −0.16 + −0.14 + −0.12 + −0.10 + −0.08 + −0.06 + −0.04 + −0.02 + 0.00 + + + + 0.0 + 0.2 + 0.4 + 0.6 + 0.8 + 1.0 + 1.2 + 1.4 + 1.6 + 1.8 + 2.0 + + + + + \ No newline at end of file diff --git a/test/output/zeroPositiveDegenerateY.svg b/test/output/zeroPositiveDegenerateY.svg new file mode 100644 index 0000000000..533b2ad023 --- /dev/null +++ b/test/output/zeroPositiveDegenerateY.svg @@ -0,0 +1,75 @@ + + + + + 0.00 + 0.02 + 0.04 + 0.06 + 0.08 + 0.10 + 0.12 + 0.14 + 0.16 + 0.18 + 0.20 + 0.22 + 0.24 + + + + 0.0 + 0.2 + 0.4 + 0.6 + 0.8 + 1.0 + 1.2 + 1.4 + 1.6 + 1.8 + 2.0 + + + + + \ No newline at end of file diff --git a/test/output/zeroPositiveY.svg b/test/output/zeroPositiveY.svg new file mode 100644 index 0000000000..0f3ef1ed84 --- /dev/null +++ b/test/output/zeroPositiveY.svg @@ -0,0 +1,75 @@ + + + + + 0.00 + 0.02 + 0.04 + 0.06 + 0.08 + 0.10 + 0.12 + 0.14 + 0.16 + 0.18 + 0.20 + 0.22 + 0.24 + + + + 0.0 + 0.2 + 0.4 + 0.6 + 0.8 + 1.0 + 1.2 + 1.4 + 1.6 + 1.8 + 2.0 + + + + + \ No newline at end of file diff --git a/test/plot.js b/test/plot.js index 57247af187..83fd9301ae 100644 --- a/test/plot.js +++ b/test/plot.js @@ -19,7 +19,7 @@ for (const [name, plot] of Object.entries(plots)) { reindexMarker(root); reindexClip(root); let expected; - let actual = beautify.html(root.outerHTML, { + let actual = beautify.html(root.outerHTML.replaceAll(" ", "\xa0"), { indent_size: 2, inline: ["title", "tspan", "span", "svg", "a", "i"], indent_inner_html: false diff --git a/test/plots/index.ts b/test/plots/index.ts index 0a3ee3eb0d..907109e590 100644 --- a/test/plots/index.ts +++ b/test/plots/index.ts @@ -132,6 +132,7 @@ export * from "./industry-unemployment.js"; export * from "./infinity-log.js"; export * from "./integer-interval.js"; export * from "./intern-facet.js"; +export * from "./interpolate-barycentric.js"; export * from "./interval-aware.js"; export * from "./intraday-histogram.js"; export * from "./kitten.js"; @@ -151,6 +152,7 @@ export * from "./linear-regression-cars.js"; export * from "./linear-regression-mtcars.js"; export * from "./linear-regression-penguins.js"; export * from "./log-degenerate.js"; +export * from "./log-tick-format.js"; export * from "./long-labels.js"; export * from "./markers.js"; export * from "./markov-chain.js"; @@ -164,6 +166,7 @@ export * from "./metro-unemployment-ridgeline.js"; export * from "./metro-unemployment-slope.js"; export * from "./metro-unemployment-stroke.js"; export * from "./metro-unemployment.js"; +export * from "./mixed-facets.js"; export * from "./moby-dick-faceted.js"; export * from "./moby-dick-letter-frequency.js"; export * from "./moby-dick-letter-pairs.js"; @@ -215,6 +218,7 @@ export * from "./penguin-species-island-sex.js"; export * from "./penguin-species-island.js"; export * from "./penguin-species.js"; export * from "./penguin-voronoi-1d.js"; +export * from "./percent-null.js"; export * from "./pointer-linked.js"; export * from "./pointer.js"; export * from "./polylinear.js"; @@ -340,3 +344,4 @@ export * from "./yearly-requests-dot.js"; export * from "./yearly-requests-line.js"; export * from "./yearly-requests.js"; export * from "./young-adults.js"; +export * from "./zero.js"; diff --git a/test/plots/interpolate-barycentric.ts b/test/plots/interpolate-barycentric.ts new file mode 100644 index 0000000000..ef55f1c48d --- /dev/null +++ b/test/plots/interpolate-barycentric.ts @@ -0,0 +1,31 @@ +import * as Plot from "@observablehq/plot"; + +export async function interpolateBarycentric4() { + const I = [0, 1, 2, 3]; + const X = [297, 295, 80, 59]; + const Y = [269, 266, 275, 265]; + return Plot.plot({ + color: {scheme: "greys", domain: [-0.1, 8]}, + x: {domain: [53, 314]}, + y: {domain: [265, 276]}, + marks: [ + Plot.frame({fill: "red"}), + Plot.raster(I, {pixelSize: 1, x: X, y: Y, fill: I, interpolate: "barycentric", imageRendering: "pixelated"}), + Plot.delaunayMesh(I, {x: X, y: Y, stroke: "black"}), + Plot.dot(I, {x: X, y: Y, r: 2, fill: "black"}) + ] + }); +} + +export async function interpolateBarycentric4k() { + const {x, y, v} = await fetch("data/4kpoints.json").then((d) => d.json()); + return Plot.plot({ + color: {scheme: "rdbu", range: [0.2, 0.8]}, + x: {domain: [0.5, 580.5]}, + y: {domain: [0, 350]}, + marks: [ + Plot.frame({fill: "black"}), + Plot.raster({length: x.length}, {x, y, fill: v, interpolate: "barycentric", imageRendering: "pixelated"}) + ] + }); +} diff --git a/test/plots/log-tick-format.ts b/test/plots/log-tick-format.ts new file mode 100644 index 0000000000..1bc5e27503 --- /dev/null +++ b/test/plots/log-tick-format.ts @@ -0,0 +1,9 @@ +import * as Plot from "@observablehq/plot"; + +export async function logTickFormatFunction() { + return Plot.plot({x: {type: "log", domain: [1, 4200], tickFormat: Plot.formatNumber()}}); +} + +export async function logTickFormatFunctionSv() { + return Plot.plot({x: {type: "log", domain: [1, 4200], tickFormat: Plot.formatNumber("sv-SE")}}); +} diff --git a/test/plots/mixed-facets.ts b/test/plots/mixed-facets.ts new file mode 100644 index 0000000000..6f8243c995 --- /dev/null +++ b/test/plots/mixed-facets.ts @@ -0,0 +1,20 @@ +import * as Plot from "@observablehq/plot"; + +export async function mixedFacets() { + const data = [ + {date: new Date("2024-01-01"), name: "a", value: 1}, + {date: new Date("2024-01-01"), name: "b", value: 2}, + {date: new Date("2024-01-01"), name: "a", value: 2}, + {date: new Date("2024-02-01"), name: "b", value: 3}, + {date: new Date("2024-02-01"), name: "a", value: 5}, + {date: new Date("2024-02-01"), name: "b", value: 2}, + {date: new Date("2024-02-01"), name: "a", value: 3} + ]; + return Plot.plot({ + marks: [ + Plot.barY(data, {x: "name", y: "value", fill: "name", fx: "date", fy: "name"}), + Plot.barY(data, {x: "name", y: "value", fx: "date", stroke: "currentColor"}), + Plot.ruleY([0]) + ] + }); +} diff --git a/test/plots/percent-null.ts b/test/plots/percent-null.ts new file mode 100644 index 0000000000..93c762ef9b --- /dev/null +++ b/test/plots/percent-null.ts @@ -0,0 +1,7 @@ +import * as Plot from "@observablehq/plot"; + +export async function percentNull() { + const time = [1, 2, 3, 4, 5]; + const value = [null, null, 1, null, null]; + return Plot.dot(time, {x: time, y: value}).plot({y: {percent: true}}); +} diff --git a/test/plots/pointer.ts b/test/plots/pointer.ts index 1e9df45067..e291a4c8f4 100644 --- a/test/plots/pointer.ts +++ b/test/plots/pointer.ts @@ -35,6 +35,16 @@ export async function pointerViewof() { return html`
${plot}${textarea}
`; } +export async function pointerViewofTitle() { + const penguins = await d3.csv("data/penguins.csv", d3.autoType); + const plot = Plot.dot(penguins, {x: "culmen_length_mm", y: "culmen_depth_mm", tip: true}).plot({title: "Penguins"}); + const textarea = html`