@coroboros/sparkline 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,65 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+ - Initial release under the `@coroboros` scope.
12
+ - TypeScript source (migrated from the legacy JavaScript implementation).
13
+ - Dual ESM + CJS build via [tsdown](https://tsdown.dev).
14
+ - Vitest test suite, Biome linting and formatting.
15
+ - `title` option — injects a `<title>` element, `role="img"` and `aria-label`.
16
+ - `ariaLabel` option — explicit `aria-label` override for the accessible name.
17
+ - `description` option — injects a `<desc>` element for screen-reader long-form context.
18
+ - `precision` option (default `2`) — integer in `[0, 6]` controlling decimal
19
+ places emitted on every coordinate.
20
+ - `EMPTY_VALUES` error code for `values: []`.
21
+ - Broader color support: CSS named colors, hex (3/4/6/8 digits), functional
22
+ notations (`rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()`, `lab()`, `lch()`,
23
+ `oklab()`, `oklch()`, `color()`), plus `currentColor` and `transparent`.
24
+ - `SparklineError` now accepts an `Error.cause` for wrapping underlying errors.
25
+ - `mitata`-powered benchmark suite (`pnpm bench`) and baseline file at
26
+ `bench/baseline.md`.
27
+ - Property-based tests with `fast-check` (SVG shape, coordinate bounds,
28
+ decimal budget, stroke-color fallback).
29
+ - `"sideEffects": false` for tree-shaking.
30
+
31
+ ### Changed
32
+ - **BREAKING: positional API** — `sparkline(values, options?)` replaces
33
+ `sparkline({ values, ...options })`. Mirrors the `fetch(url, init)` idiom.
34
+ - **BREAKING: default stroke color** — now `#C9A96E` (Coroboros sand) instead
35
+ of `#57bd0f`.
36
+ - **Polyline inset** — points are inset by `strokeWidth / 2` so stroke caps no
37
+ longer clip the viewBox edges.
38
+ - **Decorative-by-default accessibility** — when none of `title`, `ariaLabel`,
39
+ or `description` is provided, the SVG renders `aria-hidden="true"`. Pass any
40
+ of them to opt into an accessible image with `role="img"`.
41
+ - **SVG output format** — polyline points are now standard `x,y x,y` pairs that
42
+ span the full `0 → width` range. The previous quirky `"0, y x, y x, …"`
43
+ format (with leading `'0'` and orphan trailing number) is replaced.
44
+ - **Error class** — `LibError` → `SparklineError`, with a simpler constructor
45
+ `new SparklineError(code, message)` and a `name` fixed to `'SparklineError'`.
46
+ - **Runtime validation** — strict and non-polymorphic. String-to-number
47
+ coercion for numeric options is removed. Invalid style options fall back to
48
+ defaults; invalid `values` throw.
49
+ - **Width / height minimum** — reduced from `>= 10` to `> 0`.
50
+ - **Debugging** — replaces the in-tree debugger with Node's built-in
51
+ [`util.debuglog`](https://nodejs.org/api/util.html#utildebuglogsection-callback).
52
+ Enable with `NODE_DEBUG=sparkline` (previously `DEBUG=sparkline:*`).
53
+ - `strokeWidth: 0` and `strokeOpacity: 0` are now honored instead of being
54
+ replaced by the defaults.
55
+
56
+ ### Removed
57
+ - In-tree zero-dep re-implementation of the `debug` package (`src/debug/`).
58
+ - `src/helpers/` polymorphic cast / object / math utilities.
59
+ - `src/LibError/` class and `src/Sparkline/` internal class.
60
+ - `src/settings/` module — defaults are inlined in `src/index.ts`.
61
+ - Default export — only named exports are shipped.
62
+
63
+ ### Security
64
+ - XML-escape user-supplied `title`, `ariaLabel`, `description`, and `stroke`
65
+ values before injecting them into the SVG string.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026–End of Time Coroboros
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ <div align="center">
2
+
3
+ <img src="assets/logo.png" width="288" height="288" alt="@coroboros/sparkline"/>
4
+
5
+ <!-- omit in toc -->
6
+ # @coroboros/sparkline
7
+
8
+ **Lightweight, zero-dependency SVG sparkline generator for Node.js.**
9
+
10
+ Calculates polyline points from a numeric series and returns a pure SVG string. Tune width, height, stroke, coordinate precision, and accessibility metadata.
11
+
12
+ [![npm](https://img.shields.io/npm/v/@coroboros/sparkline?style=flat-square&color=000000)](https://www.npmjs.com/package/@coroboros/sparkline)
13
+ [![ci](https://img.shields.io/github/actions/workflow/status/coroboros/sparkline/ci.yml?branch=develop&style=flat-square&label=ci&color=000000)](https://github.com/coroboros/sparkline/actions/workflows/ci.yml)
14
+ [![license](https://img.shields.io/badge/license-MIT-000000?style=flat-square)](https://opensource.org/licenses/MIT)
15
+ [![stars](https://img.shields.io/github/stars/coroboros/sparkline?style=flat-square&label=stars&color=000000)](https://github.com/coroboros/sparkline)
16
+ [![coroboros.com](https://img.shields.io/badge/coroboros.com-000000?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTAiLz48cGF0aCBkPSJNMiAxMmgyME0xMiAyYTE1LjMgMTUuMyAwIDAgMSA0IDEwIDE1LjMgMTUuMyAwIDAgMS00IDEwIDE1LjMgMTUuMyAwIDAgMS00LTEwIDE1LjMgMTUuMyAwIDAgMSA0LTEweiIvPjwvc3ZnPg==)](https://coroboros.com)
17
+
18
+ </div>
19
+
20
+ ## Requirements
21
+
22
+ - Node.js `>=22` LTS. We recommend [fnm](https://github.com/Schniz/fnm) for version management (Rust-based, faster than nvm).
23
+ - Any of the following package managers: `pnpm`, `npm`, `yarn`, `bun`.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pnpm add @coroboros/sparkline
29
+ ```
30
+
31
+ ```bash
32
+ npm install @coroboros/sparkline
33
+ ```
34
+
35
+ ```bash
36
+ yarn add @coroboros/sparkline
37
+ ```
38
+
39
+ ```bash
40
+ bun add @coroboros/sparkline
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ```ts
46
+ // ESM (recommended)
47
+ import { sparkline } from '@coroboros/sparkline';
48
+ ```
49
+
50
+ ```js
51
+ // CommonJS
52
+ const { sparkline } = require('@coroboros/sparkline');
53
+ ```
54
+
55
+ ```ts
56
+ import { sparkline, SparklineError } from '@coroboros/sparkline';
57
+
58
+ const values = [10, 50, 50, 200, 0];
59
+
60
+ try {
61
+ const svg = sparkline(values, {
62
+ width: 135,
63
+ height: 50,
64
+ stroke: '#C9A96E',
65
+ strokeWidth: 1.25,
66
+ strokeOpacity: 1,
67
+ precision: 2,
68
+ title: 'Price (24h)',
69
+ ariaLabel: 'Price trend over the last 24 hours',
70
+ description: 'Hourly close for the last 24 hours.',
71
+ });
72
+ // svg is a string containing <svg>…<polyline/>…</svg>
73
+ } catch (err) {
74
+ if (err instanceof SparklineError) {
75
+ console.error(err.code, err.message);
76
+ }
77
+ }
78
+ ```
79
+
80
+ ## API
81
+
82
+ ### `sparkline(values, options?)`
83
+
84
+ Generates an SVG sparkline from an array of numbers.
85
+
86
+ **Parameters**
87
+
88
+ | Option | Type | Default | Description |
89
+ | --------------- | ---------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
90
+ | `values` | `ReadonlyArray<number>`| *(required)* | Finite numbers used to draw the sparkline. Must contain at least one element. Passed positionally. |
91
+ | `width` | `number` | `135` | SVG width, in pixels. Must be `> 0`. Invalid values fall back to the default. |
92
+ | `height` | `number` | `50` | SVG height, in pixels. Must be `> 0`. |
93
+ | `stroke` | `string` | `#C9A96E` | Stroke color — CSS named color, hex (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`), or functional notation (`rgb()`, `rgba()`, `hsl()`, `oklch()`, …). |
94
+ | `strokeWidth` | `number` | `1.25` | Stroke width, in pixels. Must be `>= 0`. `0` is honored and produces an invisible polyline. |
95
+ | `strokeOpacity` | `number` | `1` | Stroke opacity, in `[0, 1]`. |
96
+ | `precision` | `integer` | `2` | Decimal places kept on every coordinate. Integer in `[0, 6]`. Lower values produce smaller SVGs. |
97
+ | `title` | `string` | *(optional)* | Adds a `<title>` element and sets `role="img"` plus `aria-label` on the root `<svg>` (when `ariaLabel` is omitted). |
98
+ | `ariaLabel` | `string` | *(optional)* | Explicit `aria-label` on the root `<svg>`. Takes precedence over `title` for the accessible name. |
99
+ | `description` | `string` | *(optional)* | Adds a `<desc>` element for screen-reader long-form context. |
100
+
101
+ **Returns** — `string`. The SVG markup (including `<polyline>`).
102
+
103
+ **Throws** — `SparklineError` (see [Errors](#errors)).
104
+
105
+ Invalid style options (`width`, `height`, `stroke`, `strokeWidth`, `strokeOpacity`, `precision`) fall back silently to their defaults. Invalid `values` throw.
106
+
107
+ ### Accessibility
108
+
109
+ The polyline is inset by `strokeWidth / 2` so stroke caps never clip the viewBox edges.
110
+
111
+ When none of `title`, `ariaLabel`, or `description` are provided, the SVG is marked `aria-hidden="true"` — treat it as decorative. Pass any of them to opt into an accessible image: the root gets `role="img"` plus `aria-label` (from `ariaLabel` or `title`), and `<title>` / `<desc>` nest inside the SVG.
112
+
113
+ ### Examples
114
+
115
+ Rendered examples live in [`assets/examples`](assets/examples).
116
+
117
+ ![Bitcoin](assets/examples/bitcoin/sparkline.svg "Bitcoin")
118
+ ![Ethereum](assets/examples/ethereum/sparkline.svg "Ethereum")
119
+ ![Chainlink](assets/examples/chainlink/sparkline.svg "Chainlink")
120
+ ![Kusama](assets/examples/kusama/sparkline.svg "Kusama")
121
+ ![Tether](assets/examples/tether/sparkline.svg "Tether")
122
+
123
+ ### Errors
124
+
125
+ Errors thrown by `sparkline` are instances of `SparklineError`, which extends the native `Error`.
126
+
127
+ ```ts
128
+ class SparklineError extends Error {
129
+ readonly name: 'SparklineError';
130
+ readonly code: 'MISSING_VALUES' | 'INVALID_VALUES' | 'EMPTY_VALUES';
131
+ readonly message: string;
132
+ }
133
+ ```
134
+
135
+ | Code | Description |
136
+ | ----------------- | ------------------------------------------------------------- |
137
+ | `MISSING_VALUES` | The `values` option is missing. |
138
+ | `INVALID_VALUES` | `values` is not an array, or contains non-finite numbers. |
139
+ | `EMPTY_VALUES` | `values` is an empty array. |
140
+
141
+ ### Debugging
142
+
143
+ The library uses Node's built-in [`util.debuglog`](https://nodejs.org/api/util.html#utildebuglogsection-callback). Enable it with:
144
+
145
+ ```bash
146
+ NODE_DEBUG=sparkline node your-script.js
147
+ ```
148
+
149
+ ## Contributing
150
+
151
+ Bug reports and PRs welcome.
152
+
153
+ - Open an issue before submitting non-trivial PRs.
154
+ - Commits follow [Conventional Commits](https://www.conventionalcommits.org/).
155
+ - Run `pnpm lint && pnpm typecheck && pnpm test` before pushing.
156
+ - Target the `develop` branch.
157
+
158
+ ## License
159
+
160
+ [MIT](LICENSE.md)
package/dist/index.cjs ADDED
@@ -0,0 +1,275 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let node_util = require("node:util");
3
+ //#region src/render.ts
4
+ const escapeXml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5
+ const render = (options) => {
6
+ const { values, width, height, stroke, strokeWidth, strokeOpacity, title, ariaLabel, description, precision } = options;
7
+ const n = values.length;
8
+ let min = values[0];
9
+ let max = min;
10
+ for (let i = 1; i < n; i += 1) {
11
+ const v = values[i];
12
+ if (v < min) min = v;
13
+ if (v > max) max = v;
14
+ }
15
+ const diff = max - min;
16
+ const inset = strokeWidth / 2;
17
+ const innerWidth = Math.max(0, width - strokeWidth);
18
+ const innerHeight = Math.max(0, height - strokeWidth);
19
+ const stepX = n === 1 ? 0 : innerWidth / (n - 1);
20
+ const midY = height / 2;
21
+ const factor = 10 ** precision;
22
+ const points = new Array(n);
23
+ for (let i = 0; i < n; i += 1) {
24
+ const rawX = n === 1 ? width / 2 : inset + i * stepX;
25
+ const rawY = diff === 0 ? midY : inset + (max - values[i]) / diff * innerHeight;
26
+ points[i] = `${Math.round(rawX * factor) / factor},${Math.round(rawY * factor) / factor}`;
27
+ }
28
+ const svgAttrs = [
29
+ "xmlns=\"http://www.w3.org/2000/svg\"",
30
+ `width="${width}"`,
31
+ `height="${height}"`,
32
+ `viewBox="0 0 ${width} ${height}"`
33
+ ];
34
+ let body = "";
35
+ if (title !== void 0 || ariaLabel !== void 0 || description !== void 0) {
36
+ svgAttrs.push("role=\"img\"");
37
+ const label = ariaLabel ?? title;
38
+ if (label !== void 0) svgAttrs.push(`aria-label="${escapeXml(label)}"`);
39
+ if (title !== void 0) body += `<title>${escapeXml(title)}</title>`;
40
+ if (description !== void 0) body += `<desc>${escapeXml(description)}</desc>`;
41
+ } else svgAttrs.push("aria-hidden=\"true\"");
42
+ body += `<polyline points="${points.join(" ")}" fill="none" stroke="${escapeXml(stroke)}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}"/>`;
43
+ return `<svg ${svgAttrs.join(" ")}>${body}</svg>`;
44
+ };
45
+ //#endregion
46
+ //#region src/colors.ts
47
+ const CSS_NAMED_COLORS = new Set([
48
+ "aliceblue",
49
+ "antiquewhite",
50
+ "aqua",
51
+ "aquamarine",
52
+ "azure",
53
+ "beige",
54
+ "bisque",
55
+ "black",
56
+ "blanchedalmond",
57
+ "blue",
58
+ "blueviolet",
59
+ "brown",
60
+ "burlywood",
61
+ "cadetblue",
62
+ "chartreuse",
63
+ "chocolate",
64
+ "coral",
65
+ "cornflowerblue",
66
+ "cornsilk",
67
+ "crimson",
68
+ "currentcolor",
69
+ "cyan",
70
+ "darkblue",
71
+ "darkcyan",
72
+ "darkgoldenrod",
73
+ "darkgray",
74
+ "darkgreen",
75
+ "darkgrey",
76
+ "darkkhaki",
77
+ "darkmagenta",
78
+ "darkolivegreen",
79
+ "darkorange",
80
+ "darkorchid",
81
+ "darkred",
82
+ "darksalmon",
83
+ "darkseagreen",
84
+ "darkslateblue",
85
+ "darkslategray",
86
+ "darkslategrey",
87
+ "darkturquoise",
88
+ "darkviolet",
89
+ "deeppink",
90
+ "deepskyblue",
91
+ "dimgray",
92
+ "dimgrey",
93
+ "dodgerblue",
94
+ "firebrick",
95
+ "floralwhite",
96
+ "forestgreen",
97
+ "fuchsia",
98
+ "gainsboro",
99
+ "ghostwhite",
100
+ "gold",
101
+ "goldenrod",
102
+ "gray",
103
+ "green",
104
+ "greenyellow",
105
+ "grey",
106
+ "honeydew",
107
+ "hotpink",
108
+ "indianred",
109
+ "indigo",
110
+ "ivory",
111
+ "khaki",
112
+ "lavender",
113
+ "lavenderblush",
114
+ "lawngreen",
115
+ "lemonchiffon",
116
+ "lightblue",
117
+ "lightcoral",
118
+ "lightcyan",
119
+ "lightgoldenrodyellow",
120
+ "lightgray",
121
+ "lightgreen",
122
+ "lightgrey",
123
+ "lightpink",
124
+ "lightsalmon",
125
+ "lightseagreen",
126
+ "lightskyblue",
127
+ "lightslategray",
128
+ "lightslategrey",
129
+ "lightsteelblue",
130
+ "lightyellow",
131
+ "lime",
132
+ "limegreen",
133
+ "linen",
134
+ "magenta",
135
+ "maroon",
136
+ "mediumaquamarine",
137
+ "mediumblue",
138
+ "mediumorchid",
139
+ "mediumpurple",
140
+ "mediumseagreen",
141
+ "mediumslateblue",
142
+ "mediumspringgreen",
143
+ "mediumturquoise",
144
+ "mediumvioletred",
145
+ "midnightblue",
146
+ "mintcream",
147
+ "mistyrose",
148
+ "moccasin",
149
+ "navajowhite",
150
+ "navy",
151
+ "none",
152
+ "oldlace",
153
+ "olive",
154
+ "olivedrab",
155
+ "orange",
156
+ "orangered",
157
+ "orchid",
158
+ "palegoldenrod",
159
+ "palegreen",
160
+ "paleturquoise",
161
+ "palevioletred",
162
+ "papayawhip",
163
+ "peachpuff",
164
+ "peru",
165
+ "pink",
166
+ "plum",
167
+ "powderblue",
168
+ "purple",
169
+ "rebeccapurple",
170
+ "red",
171
+ "rosybrown",
172
+ "royalblue",
173
+ "saddlebrown",
174
+ "salmon",
175
+ "sandybrown",
176
+ "seagreen",
177
+ "seashell",
178
+ "sienna",
179
+ "silver",
180
+ "skyblue",
181
+ "slateblue",
182
+ "slategray",
183
+ "slategrey",
184
+ "snow",
185
+ "springgreen",
186
+ "steelblue",
187
+ "tan",
188
+ "teal",
189
+ "thistle",
190
+ "tomato",
191
+ "transparent",
192
+ "turquoise",
193
+ "violet",
194
+ "wheat",
195
+ "white",
196
+ "whitesmoke",
197
+ "yellow",
198
+ "yellowgreen"
199
+ ]);
200
+ const HEX = /^#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i;
201
+ const FUNCTIONAL = /^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\s*\([^()]*\)$/i;
202
+ const isColor = (value) => {
203
+ if (typeof value !== "string") return false;
204
+ const v = value.trim();
205
+ if (v === "") return false;
206
+ if (HEX.test(v) || FUNCTIONAL.test(v)) return true;
207
+ return CSS_NAMED_COLORS.has(v.toLowerCase());
208
+ };
209
+ //#endregion
210
+ //#region src/error.ts
211
+ var SparklineError = class extends Error {
212
+ code;
213
+ constructor(code, message, options) {
214
+ super(message, options);
215
+ this.name = "SparklineError";
216
+ this.code = code;
217
+ }
218
+ };
219
+ //#endregion
220
+ //#region src/validate.ts
221
+ const isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
222
+ const nonEmptyString = (v) => typeof v === "string" && v.trim() !== "";
223
+ const isInteger = (v) => isFiniteNumber(v) && Number.isInteger(v);
224
+ const validate = (values, options, defaults) => {
225
+ if (values === void 0 || values === null) throw new SparklineError("MISSING_VALUES", "values argument is required");
226
+ if (!Array.isArray(values)) throw new SparklineError("INVALID_VALUES", "values must be an array of finite numbers");
227
+ if (values.length === 0) throw new SparklineError("EMPTY_VALUES", "values must contain at least one number");
228
+ for (let i = 0; i < values.length; i += 1) if (!isFiniteNumber(values[i])) throw new SparklineError("INVALID_VALUES", `values[${i}] is not a finite number`);
229
+ const { width, height, stroke, strokeWidth, strokeOpacity, title, ariaLabel, description, precision } = options ?? {};
230
+ return {
231
+ values,
232
+ width: isFiniteNumber(width) && width > 0 ? width : defaults.width,
233
+ height: isFiniteNumber(height) && height > 0 ? height : defaults.height,
234
+ stroke: isColor(stroke) ? stroke : defaults.stroke,
235
+ strokeWidth: isFiniteNumber(strokeWidth) && strokeWidth >= 0 ? strokeWidth : defaults.strokeWidth,
236
+ strokeOpacity: isFiniteNumber(strokeOpacity) && strokeOpacity >= 0 && strokeOpacity <= 1 ? strokeOpacity : defaults.strokeOpacity,
237
+ title: nonEmptyString(title) ? title : void 0,
238
+ ariaLabel: nonEmptyString(ariaLabel) ? ariaLabel : void 0,
239
+ description: nonEmptyString(description) ? description : void 0,
240
+ precision: isInteger(precision) && precision >= 0 && precision <= 6 ? precision : defaults.precision
241
+ };
242
+ };
243
+ //#endregion
244
+ //#region src/index.ts
245
+ /**
246
+ * @coroboros/sparkline
247
+ */
248
+ const debug = (0, node_util.debuglog)("sparkline");
249
+ const DEFAULTS = {
250
+ width: 135,
251
+ height: 50,
252
+ stroke: "#C9A96E",
253
+ strokeWidth: 1.25,
254
+ strokeOpacity: 1,
255
+ precision: 2
256
+ };
257
+ /**
258
+ * Generate an SVG sparkline from an array of numeric values.
259
+ *
260
+ * @param values - Finite numbers used to draw the sparkline.
261
+ * @param options - Optional style and accessibility options.
262
+ * @returns The SVG markup as a string.
263
+ */
264
+ const sparkline = (values, options) => {
265
+ const validated = validate(values, options, DEFAULTS);
266
+ debug("options: %o", validated);
267
+ const svg = render(validated);
268
+ debug("svg: %d bytes", svg.length);
269
+ return svg;
270
+ };
271
+ //#endregion
272
+ exports.SparklineError = SparklineError;
273
+ exports.sparkline = sparkline;
274
+
275
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/render.ts","../src/colors.ts","../src/error.ts","../src/validate.ts","../src/index.ts"],"sourcesContent":["import type { ValidatedOptions } from './validate.js';\n\nconst escapeXml = (s: string): string =>\n s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n\nexport const render = (options: ValidatedOptions): string => {\n const {\n values,\n width,\n height,\n stroke,\n strokeWidth,\n strokeOpacity,\n title,\n ariaLabel,\n description,\n precision,\n } = options;\n const n = values.length;\n\n let min = values[0] as number;\n let max = min;\n for (let i = 1; i < n; i += 1) {\n const v = values[i] as number;\n if (v < min) min = v;\n if (v > max) max = v;\n }\n const diff = max - min;\n\n const inset = strokeWidth / 2;\n const innerWidth = Math.max(0, width - strokeWidth);\n const innerHeight = Math.max(0, height - strokeWidth);\n const stepX = n === 1 ? 0 : innerWidth / (n - 1);\n const midY = height / 2;\n const factor = 10 ** precision;\n\n const points: string[] = new Array(n);\n for (let i = 0; i < n; i += 1) {\n const rawX = n === 1 ? width / 2 : inset + i * stepX;\n const rawY = diff === 0 ? midY : inset + ((max - (values[i] as number)) / diff) * innerHeight;\n const x = Math.round(rawX * factor) / factor;\n const y = Math.round(rawY * factor) / factor;\n points[i] = `${x},${y}`;\n }\n\n const svgAttrs: string[] = [\n 'xmlns=\"http://www.w3.org/2000/svg\"',\n `width=\"${width}\"`,\n `height=\"${height}\"`,\n `viewBox=\"0 0 ${width} ${height}\"`,\n ];\n\n let body = '';\n const hasAccessibleContent =\n title !== undefined || ariaLabel !== undefined || description !== undefined;\n\n if (hasAccessibleContent) {\n svgAttrs.push('role=\"img\"');\n const label = ariaLabel ?? title;\n if (label !== undefined) {\n svgAttrs.push(`aria-label=\"${escapeXml(label)}\"`);\n }\n if (title !== undefined) {\n body += `<title>${escapeXml(title)}</title>`;\n }\n if (description !== undefined) {\n body += `<desc>${escapeXml(description)}</desc>`;\n }\n } else {\n svgAttrs.push('aria-hidden=\"true\"');\n }\n\n body +=\n `<polyline points=\"${points.join(' ')}\"` +\n ` fill=\"none\"` +\n ` stroke=\"${escapeXml(stroke)}\"` +\n ` stroke-width=\"${strokeWidth}\"` +\n ` stroke-opacity=\"${strokeOpacity}\"/>`;\n\n return `<svg ${svgAttrs.join(' ')}>${body}</svg>`;\n};\n","const CSS_NAMED_COLORS: ReadonlySet<string> = new Set([\n 'aliceblue',\n 'antiquewhite',\n 'aqua',\n 'aquamarine',\n 'azure',\n 'beige',\n 'bisque',\n 'black',\n 'blanchedalmond',\n 'blue',\n 'blueviolet',\n 'brown',\n 'burlywood',\n 'cadetblue',\n 'chartreuse',\n 'chocolate',\n 'coral',\n 'cornflowerblue',\n 'cornsilk',\n 'crimson',\n 'currentcolor',\n 'cyan',\n 'darkblue',\n 'darkcyan',\n 'darkgoldenrod',\n 'darkgray',\n 'darkgreen',\n 'darkgrey',\n 'darkkhaki',\n 'darkmagenta',\n 'darkolivegreen',\n 'darkorange',\n 'darkorchid',\n 'darkred',\n 'darksalmon',\n 'darkseagreen',\n 'darkslateblue',\n 'darkslategray',\n 'darkslategrey',\n 'darkturquoise',\n 'darkviolet',\n 'deeppink',\n 'deepskyblue',\n 'dimgray',\n 'dimgrey',\n 'dodgerblue',\n 'firebrick',\n 'floralwhite',\n 'forestgreen',\n 'fuchsia',\n 'gainsboro',\n 'ghostwhite',\n 'gold',\n 'goldenrod',\n 'gray',\n 'green',\n 'greenyellow',\n 'grey',\n 'honeydew',\n 'hotpink',\n 'indianred',\n 'indigo',\n 'ivory',\n 'khaki',\n 'lavender',\n 'lavenderblush',\n 'lawngreen',\n 'lemonchiffon',\n 'lightblue',\n 'lightcoral',\n 'lightcyan',\n 'lightgoldenrodyellow',\n 'lightgray',\n 'lightgreen',\n 'lightgrey',\n 'lightpink',\n 'lightsalmon',\n 'lightseagreen',\n 'lightskyblue',\n 'lightslategray',\n 'lightslategrey',\n 'lightsteelblue',\n 'lightyellow',\n 'lime',\n 'limegreen',\n 'linen',\n 'magenta',\n 'maroon',\n 'mediumaquamarine',\n 'mediumblue',\n 'mediumorchid',\n 'mediumpurple',\n 'mediumseagreen',\n 'mediumslateblue',\n 'mediumspringgreen',\n 'mediumturquoise',\n 'mediumvioletred',\n 'midnightblue',\n 'mintcream',\n 'mistyrose',\n 'moccasin',\n 'navajowhite',\n 'navy',\n 'none',\n 'oldlace',\n 'olive',\n 'olivedrab',\n 'orange',\n 'orangered',\n 'orchid',\n 'palegoldenrod',\n 'palegreen',\n 'paleturquoise',\n 'palevioletred',\n 'papayawhip',\n 'peachpuff',\n 'peru',\n 'pink',\n 'plum',\n 'powderblue',\n 'purple',\n 'rebeccapurple',\n 'red',\n 'rosybrown',\n 'royalblue',\n 'saddlebrown',\n 'salmon',\n 'sandybrown',\n 'seagreen',\n 'seashell',\n 'sienna',\n 'silver',\n 'skyblue',\n 'slateblue',\n 'slategray',\n 'slategrey',\n 'snow',\n 'springgreen',\n 'steelblue',\n 'tan',\n 'teal',\n 'thistle',\n 'tomato',\n 'transparent',\n 'turquoise',\n 'violet',\n 'wheat',\n 'white',\n 'whitesmoke',\n 'yellow',\n 'yellowgreen',\n]);\n\nconst HEX = /^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$/i;\nconst FUNCTIONAL = /^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\s*\\([^()]*\\)$/i;\n\nexport const isColor = (value: unknown): value is string => {\n if (typeof value !== 'string') {\n return false;\n }\n const v = value.trim();\n if (v === '') {\n return false;\n }\n if (HEX.test(v) || FUNCTIONAL.test(v)) {\n return true;\n }\n return CSS_NAMED_COLORS.has(v.toLowerCase());\n};\n","export type SparklineErrorCode = 'MISSING_VALUES' | 'INVALID_VALUES' | 'EMPTY_VALUES';\n\nexport class SparklineError extends Error {\n readonly code: SparklineErrorCode;\n\n constructor(code: SparklineErrorCode, message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = 'SparklineError';\n this.code = code;\n }\n}\n","import { isColor } from './colors.js';\nimport { SparklineError } from './error.js';\n\nexport type SparklineOptions = {\n width?: number;\n height?: number;\n stroke?: string;\n strokeWidth?: number;\n strokeOpacity?: number;\n title?: string;\n ariaLabel?: string;\n description?: string;\n precision?: number;\n};\n\nexport type SparklineDefaults = {\n width: number;\n height: number;\n stroke: string;\n strokeWidth: number;\n strokeOpacity: number;\n precision: number;\n};\n\nexport type ValidatedOptions = {\n values: ReadonlyArray<number>;\n width: number;\n height: number;\n stroke: string;\n strokeWidth: number;\n strokeOpacity: number;\n title: string | undefined;\n ariaLabel: string | undefined;\n description: string | undefined;\n precision: number;\n};\n\nconst isFiniteNumber = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);\n\nconst nonEmptyString = (v: unknown): v is string => typeof v === 'string' && v.trim() !== '';\n\nconst isInteger = (v: unknown): v is number => isFiniteNumber(v) && Number.isInteger(v);\n\nexport const validate = (\n values: ReadonlyArray<number> | undefined,\n options: SparklineOptions | undefined,\n defaults: SparklineDefaults,\n): ValidatedOptions => {\n if (values === undefined || values === null) {\n throw new SparklineError('MISSING_VALUES', 'values argument is required');\n }\n if (!Array.isArray(values)) {\n throw new SparklineError('INVALID_VALUES', 'values must be an array of finite numbers');\n }\n if (values.length === 0) {\n throw new SparklineError('EMPTY_VALUES', 'values must contain at least one number');\n }\n for (let i = 0; i < values.length; i += 1) {\n if (!isFiniteNumber(values[i])) {\n throw new SparklineError('INVALID_VALUES', `values[${i}] is not a finite number`);\n }\n }\n\n const opts = options ?? {};\n const {\n width,\n height,\n stroke,\n strokeWidth,\n strokeOpacity,\n title,\n ariaLabel,\n description,\n precision,\n } = opts;\n\n return {\n values,\n width: isFiniteNumber(width) && width > 0 ? width : defaults.width,\n height: isFiniteNumber(height) && height > 0 ? height : defaults.height,\n stroke: isColor(stroke) ? stroke : defaults.stroke,\n strokeWidth:\n isFiniteNumber(strokeWidth) && strokeWidth >= 0 ? strokeWidth : defaults.strokeWidth,\n strokeOpacity:\n isFiniteNumber(strokeOpacity) && strokeOpacity >= 0 && strokeOpacity <= 1\n ? strokeOpacity\n : defaults.strokeOpacity,\n title: nonEmptyString(title) ? title : undefined,\n ariaLabel: nonEmptyString(ariaLabel) ? ariaLabel : undefined,\n description: nonEmptyString(description) ? description : undefined,\n precision:\n isInteger(precision) && precision >= 0 && precision <= 6 ? precision : defaults.precision,\n };\n};\n","/**\n * @coroboros/sparkline\n */\nimport { debuglog } from 'node:util';\nimport { render } from './render.js';\nimport { type SparklineOptions, validate } from './validate.js';\n\nconst debug = debuglog('sparkline');\n\nconst DEFAULTS = {\n width: 135,\n height: 50,\n stroke: '#C9A96E',\n strokeWidth: 1.25,\n strokeOpacity: 1,\n precision: 2,\n} as const;\n\n/**\n * Generate an SVG sparkline from an array of numeric values.\n *\n * @param values - Finite numbers used to draw the sparkline.\n * @param options - Optional style and accessibility options.\n * @returns The SVG markup as a string.\n */\nexport const sparkline = (values: ReadonlyArray<number>, options?: SparklineOptions): string => {\n const validated = validate(values, options, DEFAULTS);\n debug('options: %o', validated);\n const svg = render(validated);\n debug('svg: %d bytes', svg.length);\n return svg;\n};\n\nexport type { SparklineErrorCode } from './error.js';\nexport { SparklineError } from './error.js';\nexport type { SparklineOptions } from './validate.js';\n"],"mappings":";;;AAEA,MAAM,aAAa,MACjB,EAAE,QAAQ,MAAM,QAAQ,CAAC,QAAQ,MAAM,OAAO,CAAC,QAAQ,MAAM,OAAO,CAAC,QAAQ,MAAM,SAAS;AAE9F,MAAa,UAAU,YAAsC;CAC3D,MAAM,EACJ,QACA,OACA,QACA,QACA,aACA,eACA,OACA,WACA,aACA,cACE;CACJ,MAAM,IAAI,OAAO;CAEjB,IAAI,MAAM,OAAO;CACjB,IAAI,MAAM;AACV,MAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;EAC7B,MAAM,IAAI,OAAO;AACjB,MAAI,IAAI,IAAK,OAAM;AACnB,MAAI,IAAI,IAAK,OAAM;;CAErB,MAAM,OAAO,MAAM;CAEnB,MAAM,QAAQ,cAAc;CAC5B,MAAM,aAAa,KAAK,IAAI,GAAG,QAAQ,YAAY;CACnD,MAAM,cAAc,KAAK,IAAI,GAAG,SAAS,YAAY;CACrD,MAAM,QAAQ,MAAM,IAAI,IAAI,cAAc,IAAI;CAC9C,MAAM,OAAO,SAAS;CACtB,MAAM,SAAS,MAAM;CAErB,MAAM,SAAmB,IAAI,MAAM,EAAE;AACrC,MAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;EAC7B,MAAM,OAAO,MAAM,IAAI,QAAQ,IAAI,QAAQ,IAAI;EAC/C,MAAM,OAAO,SAAS,IAAI,OAAO,SAAU,MAAO,OAAO,MAAiB,OAAQ;AAGlF,SAAO,KAAK,GAFF,KAAK,MAAM,OAAO,OAAO,GAAG,OAErB,GADP,KAAK,MAAM,OAAO,OAAO,GAAG;;CAIxC,MAAM,WAAqB;EACzB;EACA,UAAU,MAAM;EAChB,WAAW,OAAO;EAClB,gBAAgB,MAAM,GAAG,OAAO;EACjC;CAED,IAAI,OAAO;AAIX,KAFE,UAAU,KAAA,KAAa,cAAc,KAAA,KAAa,gBAAgB,KAAA,GAE1C;AACxB,WAAS,KAAK,eAAa;EAC3B,MAAM,QAAQ,aAAa;AAC3B,MAAI,UAAU,KAAA,EACZ,UAAS,KAAK,eAAe,UAAU,MAAM,CAAC,GAAG;AAEnD,MAAI,UAAU,KAAA,EACZ,SAAQ,UAAU,UAAU,MAAM,CAAC;AAErC,MAAI,gBAAgB,KAAA,EAClB,SAAQ,SAAS,UAAU,YAAY,CAAC;OAG1C,UAAS,KAAK,uBAAqB;AAGrC,SACE,qBAAqB,OAAO,KAAK,IAAI,CAAC,wBAE1B,UAAU,OAAO,CAAC,kBACZ,YAAY,oBACV,cAAc;AAEpC,QAAO,QAAQ,SAAS,KAAK,IAAI,CAAC,GAAG,KAAK;;;;AC/E5C,MAAM,mBAAwC,IAAI,IAAI;CACpD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,MAAM,MAAM;AACZ,MAAM,aAAa;AAEnB,MAAa,WAAW,UAAoC;AAC1D,KAAI,OAAO,UAAU,SACnB,QAAO;CAET,MAAM,IAAI,MAAM,MAAM;AACtB,KAAI,MAAM,GACR,QAAO;AAET,KAAI,IAAI,KAAK,EAAE,IAAI,WAAW,KAAK,EAAE,CACnC,QAAO;AAET,QAAO,iBAAiB,IAAI,EAAE,aAAa,CAAC;;;;ACtK9C,IAAa,iBAAb,cAAoC,MAAM;CACxC;CAEA,YAAY,MAA0B,SAAiB,SAA+B;AACpF,QAAM,SAAS,QAAQ;AACvB,OAAK,OAAO;AACZ,OAAK,OAAO;;;;;AC6BhB,MAAM,kBAAkB,MAA4B,OAAO,MAAM,YAAY,OAAO,SAAS,EAAE;AAE/F,MAAM,kBAAkB,MAA4B,OAAO,MAAM,YAAY,EAAE,MAAM,KAAK;AAE1F,MAAM,aAAa,MAA4B,eAAe,EAAE,IAAI,OAAO,UAAU,EAAE;AAEvF,MAAa,YACX,QACA,SACA,aACqB;AACrB,KAAI,WAAW,KAAA,KAAa,WAAW,KACrC,OAAM,IAAI,eAAe,kBAAkB,8BAA8B;AAE3E,KAAI,CAAC,MAAM,QAAQ,OAAO,CACxB,OAAM,IAAI,eAAe,kBAAkB,4CAA4C;AAEzF,KAAI,OAAO,WAAW,EACpB,OAAM,IAAI,eAAe,gBAAgB,0CAA0C;AAErF,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,EACtC,KAAI,CAAC,eAAe,OAAO,GAAG,CAC5B,OAAM,IAAI,eAAe,kBAAkB,UAAU,EAAE,0BAA0B;CAKrF,MAAM,EACJ,OACA,QACA,QACA,aACA,eACA,OACA,WACA,aACA,cAVW,WAAW,EAAE;AAa1B,QAAO;EACL;EACA,OAAO,eAAe,MAAM,IAAI,QAAQ,IAAI,QAAQ,SAAS;EAC7D,QAAQ,eAAe,OAAO,IAAI,SAAS,IAAI,SAAS,SAAS;EACjE,QAAQ,QAAQ,OAAO,GAAG,SAAS,SAAS;EAC5C,aACE,eAAe,YAAY,IAAI,eAAe,IAAI,cAAc,SAAS;EAC3E,eACE,eAAe,cAAc,IAAI,iBAAiB,KAAK,iBAAiB,IACpE,gBACA,SAAS;EACf,OAAO,eAAe,MAAM,GAAG,QAAQ,KAAA;EACvC,WAAW,eAAe,UAAU,GAAG,YAAY,KAAA;EACnD,aAAa,eAAe,YAAY,GAAG,cAAc,KAAA;EACzD,WACE,UAAU,UAAU,IAAI,aAAa,KAAK,aAAa,IAAI,YAAY,SAAS;EACnF;;;;;;;ACrFH,MAAM,SAAA,GAAA,UAAA,UAAiB,YAAY;AAEnC,MAAM,WAAW;CACf,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,aAAa;CACb,eAAe;CACf,WAAW;CACZ;;;;;;;;AASD,MAAa,aAAa,QAA+B,YAAuC;CAC9F,MAAM,YAAY,SAAS,QAAQ,SAAS,SAAS;AACrD,OAAM,eAAe,UAAU;CAC/B,MAAM,MAAM,OAAO,UAAU;AAC7B,OAAM,iBAAiB,IAAI,OAAO;AAClC,QAAO"}
@@ -0,0 +1,34 @@
1
+ //#region src/validate.d.ts
2
+ type SparklineOptions = {
3
+ width?: number;
4
+ height?: number;
5
+ stroke?: string;
6
+ strokeWidth?: number;
7
+ strokeOpacity?: number;
8
+ title?: string;
9
+ ariaLabel?: string;
10
+ description?: string;
11
+ precision?: number;
12
+ };
13
+ //#endregion
14
+ //#region src/error.d.ts
15
+ type SparklineErrorCode = 'MISSING_VALUES' | 'INVALID_VALUES' | 'EMPTY_VALUES';
16
+ declare class SparklineError extends Error {
17
+ readonly code: SparklineErrorCode;
18
+ constructor(code: SparklineErrorCode, message: string, options?: {
19
+ cause?: unknown;
20
+ });
21
+ }
22
+ //#endregion
23
+ //#region src/index.d.ts
24
+ /**
25
+ * Generate an SVG sparkline from an array of numeric values.
26
+ *
27
+ * @param values - Finite numbers used to draw the sparkline.
28
+ * @param options - Optional style and accessibility options.
29
+ * @returns The SVG markup as a string.
30
+ */
31
+ declare const sparkline: (values: ReadonlyArray<number>, options?: SparklineOptions) => string;
32
+ //#endregion
33
+ export { SparklineError, type SparklineErrorCode, type SparklineOptions, sparkline };
34
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/validate.ts","../src/error.ts","../src/index.ts"],"mappings":";KAGY,gBAAA;EACV,KAAA;EACA,MAAA;EACA,MAAA;EACA,WAAA;EACA,aAAA;EACA,KAAA;EACA,SAAA;EACA,WAAA;EACA,SAAA;AAAA;;;KCZU,kBAAA;AAAA,cAEC,cAAA,SAAuB,KAAA;EAAA,SACzB,IAAA,EAAM,kBAAA;cAEH,IAAA,EAAM,kBAAA,EAAoB,OAAA,UAAiB,OAAA;IAAY,KAAA;EAAA;AAAA;;;ADFrE;;;;;;;AAAA,cEsBa,SAAA,GAAa,MAAA,EAAQ,aAAA,UAAuB,OAAA,GAAU,gBAAA"}
@@ -0,0 +1,34 @@
1
+ //#region src/validate.d.ts
2
+ type SparklineOptions = {
3
+ width?: number;
4
+ height?: number;
5
+ stroke?: string;
6
+ strokeWidth?: number;
7
+ strokeOpacity?: number;
8
+ title?: string;
9
+ ariaLabel?: string;
10
+ description?: string;
11
+ precision?: number;
12
+ };
13
+ //#endregion
14
+ //#region src/error.d.ts
15
+ type SparklineErrorCode = 'MISSING_VALUES' | 'INVALID_VALUES' | 'EMPTY_VALUES';
16
+ declare class SparklineError extends Error {
17
+ readonly code: SparklineErrorCode;
18
+ constructor(code: SparklineErrorCode, message: string, options?: {
19
+ cause?: unknown;
20
+ });
21
+ }
22
+ //#endregion
23
+ //#region src/index.d.ts
24
+ /**
25
+ * Generate an SVG sparkline from an array of numeric values.
26
+ *
27
+ * @param values - Finite numbers used to draw the sparkline.
28
+ * @param options - Optional style and accessibility options.
29
+ * @returns The SVG markup as a string.
30
+ */
31
+ declare const sparkline: (values: ReadonlyArray<number>, options?: SparklineOptions) => string;
32
+ //#endregion
33
+ export { SparklineError, type SparklineErrorCode, type SparklineOptions, sparkline };
34
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/validate.ts","../src/error.ts","../src/index.ts"],"mappings":";KAGY,gBAAA;EACV,KAAA;EACA,MAAA;EACA,MAAA;EACA,WAAA;EACA,aAAA;EACA,KAAA;EACA,SAAA;EACA,WAAA;EACA,SAAA;AAAA;;;KCZU,kBAAA;AAAA,cAEC,cAAA,SAAuB,KAAA;EAAA,SACzB,IAAA,EAAM,kBAAA;cAEH,IAAA,EAAM,kBAAA,EAAoB,OAAA,UAAiB,OAAA;IAAY,KAAA;EAAA;AAAA;;;ADFrE;;;;;;;AAAA,cEsBa,SAAA,GAAa,MAAA,EAAQ,aAAA,UAAuB,OAAA,GAAU,gBAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,273 @@
1
+ import { debuglog } from "node:util";
2
+ //#region src/render.ts
3
+ const escapeXml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4
+ const render = (options) => {
5
+ const { values, width, height, stroke, strokeWidth, strokeOpacity, title, ariaLabel, description, precision } = options;
6
+ const n = values.length;
7
+ let min = values[0];
8
+ let max = min;
9
+ for (let i = 1; i < n; i += 1) {
10
+ const v = values[i];
11
+ if (v < min) min = v;
12
+ if (v > max) max = v;
13
+ }
14
+ const diff = max - min;
15
+ const inset = strokeWidth / 2;
16
+ const innerWidth = Math.max(0, width - strokeWidth);
17
+ const innerHeight = Math.max(0, height - strokeWidth);
18
+ const stepX = n === 1 ? 0 : innerWidth / (n - 1);
19
+ const midY = height / 2;
20
+ const factor = 10 ** precision;
21
+ const points = new Array(n);
22
+ for (let i = 0; i < n; i += 1) {
23
+ const rawX = n === 1 ? width / 2 : inset + i * stepX;
24
+ const rawY = diff === 0 ? midY : inset + (max - values[i]) / diff * innerHeight;
25
+ points[i] = `${Math.round(rawX * factor) / factor},${Math.round(rawY * factor) / factor}`;
26
+ }
27
+ const svgAttrs = [
28
+ "xmlns=\"http://www.w3.org/2000/svg\"",
29
+ `width="${width}"`,
30
+ `height="${height}"`,
31
+ `viewBox="0 0 ${width} ${height}"`
32
+ ];
33
+ let body = "";
34
+ if (title !== void 0 || ariaLabel !== void 0 || description !== void 0) {
35
+ svgAttrs.push("role=\"img\"");
36
+ const label = ariaLabel ?? title;
37
+ if (label !== void 0) svgAttrs.push(`aria-label="${escapeXml(label)}"`);
38
+ if (title !== void 0) body += `<title>${escapeXml(title)}</title>`;
39
+ if (description !== void 0) body += `<desc>${escapeXml(description)}</desc>`;
40
+ } else svgAttrs.push("aria-hidden=\"true\"");
41
+ body += `<polyline points="${points.join(" ")}" fill="none" stroke="${escapeXml(stroke)}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}"/>`;
42
+ return `<svg ${svgAttrs.join(" ")}>${body}</svg>`;
43
+ };
44
+ //#endregion
45
+ //#region src/colors.ts
46
+ const CSS_NAMED_COLORS = new Set([
47
+ "aliceblue",
48
+ "antiquewhite",
49
+ "aqua",
50
+ "aquamarine",
51
+ "azure",
52
+ "beige",
53
+ "bisque",
54
+ "black",
55
+ "blanchedalmond",
56
+ "blue",
57
+ "blueviolet",
58
+ "brown",
59
+ "burlywood",
60
+ "cadetblue",
61
+ "chartreuse",
62
+ "chocolate",
63
+ "coral",
64
+ "cornflowerblue",
65
+ "cornsilk",
66
+ "crimson",
67
+ "currentcolor",
68
+ "cyan",
69
+ "darkblue",
70
+ "darkcyan",
71
+ "darkgoldenrod",
72
+ "darkgray",
73
+ "darkgreen",
74
+ "darkgrey",
75
+ "darkkhaki",
76
+ "darkmagenta",
77
+ "darkolivegreen",
78
+ "darkorange",
79
+ "darkorchid",
80
+ "darkred",
81
+ "darksalmon",
82
+ "darkseagreen",
83
+ "darkslateblue",
84
+ "darkslategray",
85
+ "darkslategrey",
86
+ "darkturquoise",
87
+ "darkviolet",
88
+ "deeppink",
89
+ "deepskyblue",
90
+ "dimgray",
91
+ "dimgrey",
92
+ "dodgerblue",
93
+ "firebrick",
94
+ "floralwhite",
95
+ "forestgreen",
96
+ "fuchsia",
97
+ "gainsboro",
98
+ "ghostwhite",
99
+ "gold",
100
+ "goldenrod",
101
+ "gray",
102
+ "green",
103
+ "greenyellow",
104
+ "grey",
105
+ "honeydew",
106
+ "hotpink",
107
+ "indianred",
108
+ "indigo",
109
+ "ivory",
110
+ "khaki",
111
+ "lavender",
112
+ "lavenderblush",
113
+ "lawngreen",
114
+ "lemonchiffon",
115
+ "lightblue",
116
+ "lightcoral",
117
+ "lightcyan",
118
+ "lightgoldenrodyellow",
119
+ "lightgray",
120
+ "lightgreen",
121
+ "lightgrey",
122
+ "lightpink",
123
+ "lightsalmon",
124
+ "lightseagreen",
125
+ "lightskyblue",
126
+ "lightslategray",
127
+ "lightslategrey",
128
+ "lightsteelblue",
129
+ "lightyellow",
130
+ "lime",
131
+ "limegreen",
132
+ "linen",
133
+ "magenta",
134
+ "maroon",
135
+ "mediumaquamarine",
136
+ "mediumblue",
137
+ "mediumorchid",
138
+ "mediumpurple",
139
+ "mediumseagreen",
140
+ "mediumslateblue",
141
+ "mediumspringgreen",
142
+ "mediumturquoise",
143
+ "mediumvioletred",
144
+ "midnightblue",
145
+ "mintcream",
146
+ "mistyrose",
147
+ "moccasin",
148
+ "navajowhite",
149
+ "navy",
150
+ "none",
151
+ "oldlace",
152
+ "olive",
153
+ "olivedrab",
154
+ "orange",
155
+ "orangered",
156
+ "orchid",
157
+ "palegoldenrod",
158
+ "palegreen",
159
+ "paleturquoise",
160
+ "palevioletred",
161
+ "papayawhip",
162
+ "peachpuff",
163
+ "peru",
164
+ "pink",
165
+ "plum",
166
+ "powderblue",
167
+ "purple",
168
+ "rebeccapurple",
169
+ "red",
170
+ "rosybrown",
171
+ "royalblue",
172
+ "saddlebrown",
173
+ "salmon",
174
+ "sandybrown",
175
+ "seagreen",
176
+ "seashell",
177
+ "sienna",
178
+ "silver",
179
+ "skyblue",
180
+ "slateblue",
181
+ "slategray",
182
+ "slategrey",
183
+ "snow",
184
+ "springgreen",
185
+ "steelblue",
186
+ "tan",
187
+ "teal",
188
+ "thistle",
189
+ "tomato",
190
+ "transparent",
191
+ "turquoise",
192
+ "violet",
193
+ "wheat",
194
+ "white",
195
+ "whitesmoke",
196
+ "yellow",
197
+ "yellowgreen"
198
+ ]);
199
+ const HEX = /^#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i;
200
+ const FUNCTIONAL = /^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\s*\([^()]*\)$/i;
201
+ const isColor = (value) => {
202
+ if (typeof value !== "string") return false;
203
+ const v = value.trim();
204
+ if (v === "") return false;
205
+ if (HEX.test(v) || FUNCTIONAL.test(v)) return true;
206
+ return CSS_NAMED_COLORS.has(v.toLowerCase());
207
+ };
208
+ //#endregion
209
+ //#region src/error.ts
210
+ var SparklineError = class extends Error {
211
+ code;
212
+ constructor(code, message, options) {
213
+ super(message, options);
214
+ this.name = "SparklineError";
215
+ this.code = code;
216
+ }
217
+ };
218
+ //#endregion
219
+ //#region src/validate.ts
220
+ const isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
221
+ const nonEmptyString = (v) => typeof v === "string" && v.trim() !== "";
222
+ const isInteger = (v) => isFiniteNumber(v) && Number.isInteger(v);
223
+ const validate = (values, options, defaults) => {
224
+ if (values === void 0 || values === null) throw new SparklineError("MISSING_VALUES", "values argument is required");
225
+ if (!Array.isArray(values)) throw new SparklineError("INVALID_VALUES", "values must be an array of finite numbers");
226
+ if (values.length === 0) throw new SparklineError("EMPTY_VALUES", "values must contain at least one number");
227
+ for (let i = 0; i < values.length; i += 1) if (!isFiniteNumber(values[i])) throw new SparklineError("INVALID_VALUES", `values[${i}] is not a finite number`);
228
+ const { width, height, stroke, strokeWidth, strokeOpacity, title, ariaLabel, description, precision } = options ?? {};
229
+ return {
230
+ values,
231
+ width: isFiniteNumber(width) && width > 0 ? width : defaults.width,
232
+ height: isFiniteNumber(height) && height > 0 ? height : defaults.height,
233
+ stroke: isColor(stroke) ? stroke : defaults.stroke,
234
+ strokeWidth: isFiniteNumber(strokeWidth) && strokeWidth >= 0 ? strokeWidth : defaults.strokeWidth,
235
+ strokeOpacity: isFiniteNumber(strokeOpacity) && strokeOpacity >= 0 && strokeOpacity <= 1 ? strokeOpacity : defaults.strokeOpacity,
236
+ title: nonEmptyString(title) ? title : void 0,
237
+ ariaLabel: nonEmptyString(ariaLabel) ? ariaLabel : void 0,
238
+ description: nonEmptyString(description) ? description : void 0,
239
+ precision: isInteger(precision) && precision >= 0 && precision <= 6 ? precision : defaults.precision
240
+ };
241
+ };
242
+ //#endregion
243
+ //#region src/index.ts
244
+ /**
245
+ * @coroboros/sparkline
246
+ */
247
+ const debug = debuglog("sparkline");
248
+ const DEFAULTS = {
249
+ width: 135,
250
+ height: 50,
251
+ stroke: "#C9A96E",
252
+ strokeWidth: 1.25,
253
+ strokeOpacity: 1,
254
+ precision: 2
255
+ };
256
+ /**
257
+ * Generate an SVG sparkline from an array of numeric values.
258
+ *
259
+ * @param values - Finite numbers used to draw the sparkline.
260
+ * @param options - Optional style and accessibility options.
261
+ * @returns The SVG markup as a string.
262
+ */
263
+ const sparkline = (values, options) => {
264
+ const validated = validate(values, options, DEFAULTS);
265
+ debug("options: %o", validated);
266
+ const svg = render(validated);
267
+ debug("svg: %d bytes", svg.length);
268
+ return svg;
269
+ };
270
+ //#endregion
271
+ export { SparklineError, sparkline };
272
+
273
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/render.ts","../src/colors.ts","../src/error.ts","../src/validate.ts","../src/index.ts"],"sourcesContent":["import type { ValidatedOptions } from './validate.js';\n\nconst escapeXml = (s: string): string =>\n s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n\nexport const render = (options: ValidatedOptions): string => {\n const {\n values,\n width,\n height,\n stroke,\n strokeWidth,\n strokeOpacity,\n title,\n ariaLabel,\n description,\n precision,\n } = options;\n const n = values.length;\n\n let min = values[0] as number;\n let max = min;\n for (let i = 1; i < n; i += 1) {\n const v = values[i] as number;\n if (v < min) min = v;\n if (v > max) max = v;\n }\n const diff = max - min;\n\n const inset = strokeWidth / 2;\n const innerWidth = Math.max(0, width - strokeWidth);\n const innerHeight = Math.max(0, height - strokeWidth);\n const stepX = n === 1 ? 0 : innerWidth / (n - 1);\n const midY = height / 2;\n const factor = 10 ** precision;\n\n const points: string[] = new Array(n);\n for (let i = 0; i < n; i += 1) {\n const rawX = n === 1 ? width / 2 : inset + i * stepX;\n const rawY = diff === 0 ? midY : inset + ((max - (values[i] as number)) / diff) * innerHeight;\n const x = Math.round(rawX * factor) / factor;\n const y = Math.round(rawY * factor) / factor;\n points[i] = `${x},${y}`;\n }\n\n const svgAttrs: string[] = [\n 'xmlns=\"http://www.w3.org/2000/svg\"',\n `width=\"${width}\"`,\n `height=\"${height}\"`,\n `viewBox=\"0 0 ${width} ${height}\"`,\n ];\n\n let body = '';\n const hasAccessibleContent =\n title !== undefined || ariaLabel !== undefined || description !== undefined;\n\n if (hasAccessibleContent) {\n svgAttrs.push('role=\"img\"');\n const label = ariaLabel ?? title;\n if (label !== undefined) {\n svgAttrs.push(`aria-label=\"${escapeXml(label)}\"`);\n }\n if (title !== undefined) {\n body += `<title>${escapeXml(title)}</title>`;\n }\n if (description !== undefined) {\n body += `<desc>${escapeXml(description)}</desc>`;\n }\n } else {\n svgAttrs.push('aria-hidden=\"true\"');\n }\n\n body +=\n `<polyline points=\"${points.join(' ')}\"` +\n ` fill=\"none\"` +\n ` stroke=\"${escapeXml(stroke)}\"` +\n ` stroke-width=\"${strokeWidth}\"` +\n ` stroke-opacity=\"${strokeOpacity}\"/>`;\n\n return `<svg ${svgAttrs.join(' ')}>${body}</svg>`;\n};\n","const CSS_NAMED_COLORS: ReadonlySet<string> = new Set([\n 'aliceblue',\n 'antiquewhite',\n 'aqua',\n 'aquamarine',\n 'azure',\n 'beige',\n 'bisque',\n 'black',\n 'blanchedalmond',\n 'blue',\n 'blueviolet',\n 'brown',\n 'burlywood',\n 'cadetblue',\n 'chartreuse',\n 'chocolate',\n 'coral',\n 'cornflowerblue',\n 'cornsilk',\n 'crimson',\n 'currentcolor',\n 'cyan',\n 'darkblue',\n 'darkcyan',\n 'darkgoldenrod',\n 'darkgray',\n 'darkgreen',\n 'darkgrey',\n 'darkkhaki',\n 'darkmagenta',\n 'darkolivegreen',\n 'darkorange',\n 'darkorchid',\n 'darkred',\n 'darksalmon',\n 'darkseagreen',\n 'darkslateblue',\n 'darkslategray',\n 'darkslategrey',\n 'darkturquoise',\n 'darkviolet',\n 'deeppink',\n 'deepskyblue',\n 'dimgray',\n 'dimgrey',\n 'dodgerblue',\n 'firebrick',\n 'floralwhite',\n 'forestgreen',\n 'fuchsia',\n 'gainsboro',\n 'ghostwhite',\n 'gold',\n 'goldenrod',\n 'gray',\n 'green',\n 'greenyellow',\n 'grey',\n 'honeydew',\n 'hotpink',\n 'indianred',\n 'indigo',\n 'ivory',\n 'khaki',\n 'lavender',\n 'lavenderblush',\n 'lawngreen',\n 'lemonchiffon',\n 'lightblue',\n 'lightcoral',\n 'lightcyan',\n 'lightgoldenrodyellow',\n 'lightgray',\n 'lightgreen',\n 'lightgrey',\n 'lightpink',\n 'lightsalmon',\n 'lightseagreen',\n 'lightskyblue',\n 'lightslategray',\n 'lightslategrey',\n 'lightsteelblue',\n 'lightyellow',\n 'lime',\n 'limegreen',\n 'linen',\n 'magenta',\n 'maroon',\n 'mediumaquamarine',\n 'mediumblue',\n 'mediumorchid',\n 'mediumpurple',\n 'mediumseagreen',\n 'mediumslateblue',\n 'mediumspringgreen',\n 'mediumturquoise',\n 'mediumvioletred',\n 'midnightblue',\n 'mintcream',\n 'mistyrose',\n 'moccasin',\n 'navajowhite',\n 'navy',\n 'none',\n 'oldlace',\n 'olive',\n 'olivedrab',\n 'orange',\n 'orangered',\n 'orchid',\n 'palegoldenrod',\n 'palegreen',\n 'paleturquoise',\n 'palevioletred',\n 'papayawhip',\n 'peachpuff',\n 'peru',\n 'pink',\n 'plum',\n 'powderblue',\n 'purple',\n 'rebeccapurple',\n 'red',\n 'rosybrown',\n 'royalblue',\n 'saddlebrown',\n 'salmon',\n 'sandybrown',\n 'seagreen',\n 'seashell',\n 'sienna',\n 'silver',\n 'skyblue',\n 'slateblue',\n 'slategray',\n 'slategrey',\n 'snow',\n 'springgreen',\n 'steelblue',\n 'tan',\n 'teal',\n 'thistle',\n 'tomato',\n 'transparent',\n 'turquoise',\n 'violet',\n 'wheat',\n 'white',\n 'whitesmoke',\n 'yellow',\n 'yellowgreen',\n]);\n\nconst HEX = /^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$/i;\nconst FUNCTIONAL = /^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\s*\\([^()]*\\)$/i;\n\nexport const isColor = (value: unknown): value is string => {\n if (typeof value !== 'string') {\n return false;\n }\n const v = value.trim();\n if (v === '') {\n return false;\n }\n if (HEX.test(v) || FUNCTIONAL.test(v)) {\n return true;\n }\n return CSS_NAMED_COLORS.has(v.toLowerCase());\n};\n","export type SparklineErrorCode = 'MISSING_VALUES' | 'INVALID_VALUES' | 'EMPTY_VALUES';\n\nexport class SparklineError extends Error {\n readonly code: SparklineErrorCode;\n\n constructor(code: SparklineErrorCode, message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = 'SparklineError';\n this.code = code;\n }\n}\n","import { isColor } from './colors.js';\nimport { SparklineError } from './error.js';\n\nexport type SparklineOptions = {\n width?: number;\n height?: number;\n stroke?: string;\n strokeWidth?: number;\n strokeOpacity?: number;\n title?: string;\n ariaLabel?: string;\n description?: string;\n precision?: number;\n};\n\nexport type SparklineDefaults = {\n width: number;\n height: number;\n stroke: string;\n strokeWidth: number;\n strokeOpacity: number;\n precision: number;\n};\n\nexport type ValidatedOptions = {\n values: ReadonlyArray<number>;\n width: number;\n height: number;\n stroke: string;\n strokeWidth: number;\n strokeOpacity: number;\n title: string | undefined;\n ariaLabel: string | undefined;\n description: string | undefined;\n precision: number;\n};\n\nconst isFiniteNumber = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);\n\nconst nonEmptyString = (v: unknown): v is string => typeof v === 'string' && v.trim() !== '';\n\nconst isInteger = (v: unknown): v is number => isFiniteNumber(v) && Number.isInteger(v);\n\nexport const validate = (\n values: ReadonlyArray<number> | undefined,\n options: SparklineOptions | undefined,\n defaults: SparklineDefaults,\n): ValidatedOptions => {\n if (values === undefined || values === null) {\n throw new SparklineError('MISSING_VALUES', 'values argument is required');\n }\n if (!Array.isArray(values)) {\n throw new SparklineError('INVALID_VALUES', 'values must be an array of finite numbers');\n }\n if (values.length === 0) {\n throw new SparklineError('EMPTY_VALUES', 'values must contain at least one number');\n }\n for (let i = 0; i < values.length; i += 1) {\n if (!isFiniteNumber(values[i])) {\n throw new SparklineError('INVALID_VALUES', `values[${i}] is not a finite number`);\n }\n }\n\n const opts = options ?? {};\n const {\n width,\n height,\n stroke,\n strokeWidth,\n strokeOpacity,\n title,\n ariaLabel,\n description,\n precision,\n } = opts;\n\n return {\n values,\n width: isFiniteNumber(width) && width > 0 ? width : defaults.width,\n height: isFiniteNumber(height) && height > 0 ? height : defaults.height,\n stroke: isColor(stroke) ? stroke : defaults.stroke,\n strokeWidth:\n isFiniteNumber(strokeWidth) && strokeWidth >= 0 ? strokeWidth : defaults.strokeWidth,\n strokeOpacity:\n isFiniteNumber(strokeOpacity) && strokeOpacity >= 0 && strokeOpacity <= 1\n ? strokeOpacity\n : defaults.strokeOpacity,\n title: nonEmptyString(title) ? title : undefined,\n ariaLabel: nonEmptyString(ariaLabel) ? ariaLabel : undefined,\n description: nonEmptyString(description) ? description : undefined,\n precision:\n isInteger(precision) && precision >= 0 && precision <= 6 ? precision : defaults.precision,\n };\n};\n","/**\n * @coroboros/sparkline\n */\nimport { debuglog } from 'node:util';\nimport { render } from './render.js';\nimport { type SparklineOptions, validate } from './validate.js';\n\nconst debug = debuglog('sparkline');\n\nconst DEFAULTS = {\n width: 135,\n height: 50,\n stroke: '#C9A96E',\n strokeWidth: 1.25,\n strokeOpacity: 1,\n precision: 2,\n} as const;\n\n/**\n * Generate an SVG sparkline from an array of numeric values.\n *\n * @param values - Finite numbers used to draw the sparkline.\n * @param options - Optional style and accessibility options.\n * @returns The SVG markup as a string.\n */\nexport const sparkline = (values: ReadonlyArray<number>, options?: SparklineOptions): string => {\n const validated = validate(values, options, DEFAULTS);\n debug('options: %o', validated);\n const svg = render(validated);\n debug('svg: %d bytes', svg.length);\n return svg;\n};\n\nexport type { SparklineErrorCode } from './error.js';\nexport { SparklineError } from './error.js';\nexport type { SparklineOptions } from './validate.js';\n"],"mappings":";;AAEA,MAAM,aAAa,MACjB,EAAE,QAAQ,MAAM,QAAQ,CAAC,QAAQ,MAAM,OAAO,CAAC,QAAQ,MAAM,OAAO,CAAC,QAAQ,MAAM,SAAS;AAE9F,MAAa,UAAU,YAAsC;CAC3D,MAAM,EACJ,QACA,OACA,QACA,QACA,aACA,eACA,OACA,WACA,aACA,cACE;CACJ,MAAM,IAAI,OAAO;CAEjB,IAAI,MAAM,OAAO;CACjB,IAAI,MAAM;AACV,MAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;EAC7B,MAAM,IAAI,OAAO;AACjB,MAAI,IAAI,IAAK,OAAM;AACnB,MAAI,IAAI,IAAK,OAAM;;CAErB,MAAM,OAAO,MAAM;CAEnB,MAAM,QAAQ,cAAc;CAC5B,MAAM,aAAa,KAAK,IAAI,GAAG,QAAQ,YAAY;CACnD,MAAM,cAAc,KAAK,IAAI,GAAG,SAAS,YAAY;CACrD,MAAM,QAAQ,MAAM,IAAI,IAAI,cAAc,IAAI;CAC9C,MAAM,OAAO,SAAS;CACtB,MAAM,SAAS,MAAM;CAErB,MAAM,SAAmB,IAAI,MAAM,EAAE;AACrC,MAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;EAC7B,MAAM,OAAO,MAAM,IAAI,QAAQ,IAAI,QAAQ,IAAI;EAC/C,MAAM,OAAO,SAAS,IAAI,OAAO,SAAU,MAAO,OAAO,MAAiB,OAAQ;AAGlF,SAAO,KAAK,GAFF,KAAK,MAAM,OAAO,OAAO,GAAG,OAErB,GADP,KAAK,MAAM,OAAO,OAAO,GAAG;;CAIxC,MAAM,WAAqB;EACzB;EACA,UAAU,MAAM;EAChB,WAAW,OAAO;EAClB,gBAAgB,MAAM,GAAG,OAAO;EACjC;CAED,IAAI,OAAO;AAIX,KAFE,UAAU,KAAA,KAAa,cAAc,KAAA,KAAa,gBAAgB,KAAA,GAE1C;AACxB,WAAS,KAAK,eAAa;EAC3B,MAAM,QAAQ,aAAa;AAC3B,MAAI,UAAU,KAAA,EACZ,UAAS,KAAK,eAAe,UAAU,MAAM,CAAC,GAAG;AAEnD,MAAI,UAAU,KAAA,EACZ,SAAQ,UAAU,UAAU,MAAM,CAAC;AAErC,MAAI,gBAAgB,KAAA,EAClB,SAAQ,SAAS,UAAU,YAAY,CAAC;OAG1C,UAAS,KAAK,uBAAqB;AAGrC,SACE,qBAAqB,OAAO,KAAK,IAAI,CAAC,wBAE1B,UAAU,OAAO,CAAC,kBACZ,YAAY,oBACV,cAAc;AAEpC,QAAO,QAAQ,SAAS,KAAK,IAAI,CAAC,GAAG,KAAK;;;;AC/E5C,MAAM,mBAAwC,IAAI,IAAI;CACpD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,MAAM,MAAM;AACZ,MAAM,aAAa;AAEnB,MAAa,WAAW,UAAoC;AAC1D,KAAI,OAAO,UAAU,SACnB,QAAO;CAET,MAAM,IAAI,MAAM,MAAM;AACtB,KAAI,MAAM,GACR,QAAO;AAET,KAAI,IAAI,KAAK,EAAE,IAAI,WAAW,KAAK,EAAE,CACnC,QAAO;AAET,QAAO,iBAAiB,IAAI,EAAE,aAAa,CAAC;;;;ACtK9C,IAAa,iBAAb,cAAoC,MAAM;CACxC;CAEA,YAAY,MAA0B,SAAiB,SAA+B;AACpF,QAAM,SAAS,QAAQ;AACvB,OAAK,OAAO;AACZ,OAAK,OAAO;;;;;AC6BhB,MAAM,kBAAkB,MAA4B,OAAO,MAAM,YAAY,OAAO,SAAS,EAAE;AAE/F,MAAM,kBAAkB,MAA4B,OAAO,MAAM,YAAY,EAAE,MAAM,KAAK;AAE1F,MAAM,aAAa,MAA4B,eAAe,EAAE,IAAI,OAAO,UAAU,EAAE;AAEvF,MAAa,YACX,QACA,SACA,aACqB;AACrB,KAAI,WAAW,KAAA,KAAa,WAAW,KACrC,OAAM,IAAI,eAAe,kBAAkB,8BAA8B;AAE3E,KAAI,CAAC,MAAM,QAAQ,OAAO,CACxB,OAAM,IAAI,eAAe,kBAAkB,4CAA4C;AAEzF,KAAI,OAAO,WAAW,EACpB,OAAM,IAAI,eAAe,gBAAgB,0CAA0C;AAErF,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,EACtC,KAAI,CAAC,eAAe,OAAO,GAAG,CAC5B,OAAM,IAAI,eAAe,kBAAkB,UAAU,EAAE,0BAA0B;CAKrF,MAAM,EACJ,OACA,QACA,QACA,aACA,eACA,OACA,WACA,aACA,cAVW,WAAW,EAAE;AAa1B,QAAO;EACL;EACA,OAAO,eAAe,MAAM,IAAI,QAAQ,IAAI,QAAQ,SAAS;EAC7D,QAAQ,eAAe,OAAO,IAAI,SAAS,IAAI,SAAS,SAAS;EACjE,QAAQ,QAAQ,OAAO,GAAG,SAAS,SAAS;EAC5C,aACE,eAAe,YAAY,IAAI,eAAe,IAAI,cAAc,SAAS;EAC3E,eACE,eAAe,cAAc,IAAI,iBAAiB,KAAK,iBAAiB,IACpE,gBACA,SAAS;EACf,OAAO,eAAe,MAAM,GAAG,QAAQ,KAAA;EACvC,WAAW,eAAe,UAAU,GAAG,YAAY,KAAA;EACnD,aAAa,eAAe,YAAY,GAAG,cAAc,KAAA;EACzD,WACE,UAAU,UAAU,IAAI,aAAa,KAAK,aAAa,IAAI,YAAY,SAAS;EACnF;;;;;;;ACrFH,MAAM,QAAQ,SAAS,YAAY;AAEnC,MAAM,WAAW;CACf,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,aAAa;CACb,eAAe;CACf,WAAW;CACZ;;;;;;;;AASD,MAAa,aAAa,QAA+B,YAAuC;CAC9F,MAAM,YAAY,SAAS,QAAQ,SAAS,SAAS;AACrD,OAAM,eAAe,UAAU;CAC/B,MAAM,MAAM,OAAO,UAAU;AAC7B,OAAM,iBAAiB,IAAI,OAAO;AAClC,QAAO"}
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@coroboros/sparkline",
3
+ "version": "1.0.0",
4
+ "description": "Lightweight, zero-dependency SVG sparkline generator for Node.js.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.mjs",
9
+ "types": "./dist/index.d.cts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./dist/index.d.mts",
14
+ "default": "./dist/index.mjs"
15
+ },
16
+ "require": {
17
+ "types": "./dist/index.d.cts",
18
+ "default": "./dist/index.cjs"
19
+ }
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md",
26
+ "LICENSE.md",
27
+ "CHANGELOG.md"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsdown",
31
+ "dev": "tsdown --watch",
32
+ "lint": "biome check .",
33
+ "lint:fix": "biome check --write .",
34
+ "typecheck": "tsc --noEmit",
35
+ "test": "vitest run",
36
+ "test:watch": "vitest",
37
+ "test:coverage": "vitest run --coverage",
38
+ "bench": "pnpm build && node bench/sparkline.bench.mjs",
39
+ "prepublishOnly": "pnpm lint && pnpm typecheck && pnpm test && pnpm build"
40
+ },
41
+ "keywords": [
42
+ "coroboros",
43
+ "sparkline",
44
+ "svg",
45
+ "chart",
46
+ "trading",
47
+ "crypto",
48
+ "typescript"
49
+ ],
50
+ "author": "Coroboros <ob@coroboros.com> (https://github.com/coroboros)",
51
+ "license": "MIT",
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/coroboros/sparkline.git"
55
+ },
56
+ "bugs": {
57
+ "url": "https://github.com/coroboros/sparkline/issues"
58
+ },
59
+ "homepage": "https://github.com/coroboros/sparkline#readme",
60
+ "engines": {
61
+ "node": ">=22"
62
+ },
63
+ "packageManager": "pnpm@10.33.0",
64
+ "publishConfig": {
65
+ "access": "public"
66
+ },
67
+ "private": false,
68
+ "preferGlobal": false,
69
+ "analyze": false,
70
+ "devDependencies": {
71
+ "@biomejs/biome": "^2.4.12",
72
+ "@types/node": "^22.0.0",
73
+ "@vitest/coverage-v8": "^4.1.4",
74
+ "fast-check": "^4.7.0",
75
+ "mitata": "^1.0.34",
76
+ "svg-parser": "^2.0.4",
77
+ "tsdown": "^0.21.9",
78
+ "typescript": "^6.0.3",
79
+ "vitest": "^4.1.4"
80
+ }
81
+ }