@uwdata/vgplot 0.4.0 → 0.6.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.
Files changed (65) hide show
  1. package/README.md +4 -2
  2. package/dist/vgplot.js +6677 -6503
  3. package/dist/vgplot.min.js +12 -33
  4. package/package.json +8 -10
  5. package/src/api.js +347 -0
  6. package/src/connect.js +14 -0
  7. package/src/context.js +20 -0
  8. package/src/index.js +14 -303
  9. package/src/inputs.js +24 -0
  10. package/src/{directives → plot}/attributes.js +32 -5
  11. package/src/{directives → plot}/interactors.js +8 -6
  12. package/src/{directives → plot}/legends.js +14 -6
  13. package/src/{directives → plot}/marks.js +18 -13
  14. package/src/plot/named-plots.js +49 -0
  15. package/src/plot/plot.js +9 -0
  16. package/src/directives/plot.js +0 -39
  17. package/src/interactors/Highlight.js +0 -101
  18. package/src/interactors/Interval1D.js +0 -90
  19. package/src/interactors/Interval2D.js +0 -102
  20. package/src/interactors/Nearest.js +0 -66
  21. package/src/interactors/PanZoom.js +0 -121
  22. package/src/interactors/Toggle.js +0 -111
  23. package/src/interactors/util/brush.js +0 -45
  24. package/src/interactors/util/close-to.js +0 -9
  25. package/src/interactors/util/get-field.js +0 -4
  26. package/src/interactors/util/invert.js +0 -3
  27. package/src/interactors/util/patchScreenCTM.js +0 -13
  28. package/src/interactors/util/sanitize-styles.js +0 -9
  29. package/src/interactors/util/to-kebab-case.js +0 -9
  30. package/src/layout/index.js +0 -2
  31. package/src/legend.js +0 -64
  32. package/src/marks/ConnectedMark.js +0 -63
  33. package/src/marks/ContourMark.js +0 -89
  34. package/src/marks/DenseLineMark.js +0 -146
  35. package/src/marks/Density1DMark.js +0 -104
  36. package/src/marks/Density2DMark.js +0 -69
  37. package/src/marks/Grid2DMark.js +0 -191
  38. package/src/marks/HexbinMark.js +0 -88
  39. package/src/marks/Mark.js +0 -195
  40. package/src/marks/RasterMark.js +0 -122
  41. package/src/marks/RasterTileMark.js +0 -332
  42. package/src/marks/RegressionMark.js +0 -117
  43. package/src/marks/util/bin-field.js +0 -17
  44. package/src/marks/util/density.js +0 -226
  45. package/src/marks/util/extent.js +0 -56
  46. package/src/marks/util/grid.js +0 -57
  47. package/src/marks/util/handle-param.js +0 -14
  48. package/src/marks/util/is-arrow-table.js +0 -3
  49. package/src/marks/util/is-color.js +0 -18
  50. package/src/marks/util/is-constant-option.js +0 -40
  51. package/src/marks/util/is-symbol.js +0 -20
  52. package/src/marks/util/raster.js +0 -44
  53. package/src/marks/util/stats.js +0 -133
  54. package/src/marks/util/to-data-array.js +0 -58
  55. package/src/plot-attributes.js +0 -211
  56. package/src/plot-renderer.js +0 -161
  57. package/src/plot.js +0 -136
  58. package/src/spec/parse-data.js +0 -69
  59. package/src/spec/parse-spec.js +0 -422
  60. package/src/spec/to-module.js +0 -465
  61. package/src/spec/util.js +0 -43
  62. package/src/symbols.js +0 -3
  63. package/src/transforms/bin.js +0 -81
  64. package/src/transforms/index.js +0 -3
  65. /package/src/{directives → plot}/data.js +0 -0
@@ -1,465 +0,0 @@
1
- import { create } from '@uwdata/mosaic-sql';
2
- import { parseData } from './parse-data.js';
3
- import { ParseContext } from './parse-spec.js';
4
- import {
5
- error, paramRef, toArray,
6
- isArray, isNumberOrString, isObject, isString, isFunction
7
- } from './util.js';
8
-
9
- const TOPOJSON = 'https://cdn.jsdelivr.net/npm/topojson@3.0.2/+esm';
10
-
11
- const SpecParsers = new Map([
12
- ['plot', { type: isArray, parse: parsePlot }],
13
- ['mark', { type: isString, parse: parseNakedMark }],
14
- ['legend', { type: isString, parse: parseLegend }],
15
- ['hconcat', { type: isArray, parse: parseHConcat }],
16
- ['vconcat', { type: isArray, parse: parseVConcat }],
17
- ['hspace', { type: isNumberOrString, parse: parseHSpace }],
18
- ['vspace', { type: isNumberOrString, parse: parseVSpace }],
19
- ['input', { type: isString, parse: parseInput }]
20
- ]);
21
-
22
- const DataFormats = new Map([
23
- ['csv', parseCSVData],
24
- ['json', parseJSONData],
25
- ['geojson', parseGeoJSONData],
26
- ['topojson', parseTopoJSONData],
27
- ['parquet', parseParquetData],
28
- ['table', parseTableData]
29
- ]);
30
-
31
- export function specToModule(spec, options) {
32
- spec = isString(spec) ? JSON.parse(spec) : spec;
33
- return new CodegenContext(options).generate(spec);
34
- }
35
-
36
- function maybeNewline(entry) {
37
- return entry?.length ? [''] : [];
38
- }
39
-
40
- class CodegenContext extends ParseContext {
41
- constructor(options) {
42
- super({
43
- specParsers: SpecParsers,
44
- dataFormats: DataFormats,
45
- ...options
46
- });
47
- this.imports = options?.imports || new Map([
48
- ['@uwdata/vgplot', '* as vg']
49
- ]);
50
- this.depth = 0;
51
- }
52
-
53
- async generate(input) {
54
- // eslint-disable-next-line no-unused-vars
55
- const { meta, data = {}, plotDefaults = {}, params, ...spec } = input;
56
-
57
- // parse data definitions
58
- const dataCode = await Promise.all(
59
- Object.keys(data).flatMap(name => {
60
- const q = parseData(name, data[name], this);
61
- return !q ? []
62
- : q.data ? `const ${name} = ${q.data};`
63
- : `await vg.coordinator().exec(\n ${q}\n);`;
64
- })
65
- );
66
-
67
- // parse default attributes
68
- const defaultList = Object.keys(plotDefaults)
69
- .map(key => parseAttribute(plotDefaults, key, this));
70
- let defaultCode = [];
71
- if (defaultList.length) {
72
- this.plotDefaults = 'defaultAttributes';
73
- defaultCode = [
74
- 'const defaultAttributes = [',
75
- defaultList.map(d => ' ' + d).join(',\n'),
76
- '];'
77
- ];
78
- }
79
-
80
- // parse param/selection definitions
81
- for (const name in params) {
82
- this.params.set(`$${name}`, parseParam(params[name], this));
83
- }
84
-
85
- const specCode = [
86
- `export default ${parseSpec(spec, this)};`
87
- ];
88
-
89
- const paramCode = [];
90
- for (const [key, value] of this.params) {
91
- paramCode.push(`const ${key} = ${value};`);
92
- }
93
-
94
- const importsCode = [];
95
- for (const [pkg, methods] of this.imports) {
96
- importsCode.push(
97
- isString(methods)
98
- ? `import ${methods} from "${pkg}";`
99
- : `import { ${methods.join(', ')} } from "${pkg}";`
100
- );
101
- }
102
-
103
- return [
104
- ...importsCode,
105
- ...maybeNewline(importsCode),
106
- ...dataCode,
107
- ...maybeNewline(dataCode),
108
- ...paramCode,
109
- ...maybeNewline(paramCode),
110
- ...defaultCode,
111
- ...maybeNewline(defaultCode),
112
- ...specCode
113
- ].join('\n');
114
- }
115
-
116
- addImport(pkg, method) {
117
- if (!this.imports.has(pkg)) {
118
- this.imports.set(pkg, []);
119
- }
120
- this.imports.get(pkg).push(method);
121
- }
122
-
123
- setImports(pkg, all) {
124
- this.imports.set(pkg, all);
125
- }
126
-
127
- maybeParam(value, ctr = 'vg.Param.value()') {
128
- const { params } = this;
129
- const name = paramRef(value);
130
- if (name) {
131
- const $name = `$${name}`;
132
- if (!params.has($name)) {
133
- params.set($name, ctr);
134
- }
135
- return $name;
136
- }
137
- return JSON.stringify(value);
138
- }
139
-
140
- maybeSelection(value) {
141
- return this.maybeParam(value, 'vg.Selection.intersect()');
142
- }
143
-
144
- maybeTransform(value) {
145
- if (isObject(value)) {
146
- return value.expr ? parseExpression(value, this)
147
- : value.agg ? parseExpression(value, this, 'agg', 'agg')
148
- : parseTransform(value, this);
149
- }
150
- }
151
-
152
- indent() {
153
- this.depth += 1;
154
- }
155
-
156
- undent() {
157
- this.depth -= 1;
158
- }
159
-
160
- tab() {
161
- return Array.from({ length: this.depth }, () => ' ').join('');
162
- }
163
- }
164
-
165
- function parseExpression(spec, ctx, key = 'expr', method = 'sql') {
166
- const { label } = spec;
167
- const expr = spec[key]
168
- const tokens = expr.split(/(\\'|\\"|"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\$\w+)/g);
169
- let str = '';
170
-
171
- for (let i = 0; i < tokens.length; ++i) {
172
- const tok = tokens[i];
173
- if (tok.startsWith('$')) {
174
- str += `\${${ctx.maybeParam(tok)}}`;
175
- } else {
176
- str += tok;
177
- }
178
- }
179
-
180
- return `vg.${method}\`${str}\``
181
- + (label ? `.annotate({ label: ${JSON.stringify(label)} })` : '');
182
- }
183
-
184
- function parseTransform(spec, ctx) {
185
- const { transforms } = ctx;
186
- let name;
187
- for (const key in spec) {
188
- if (transforms.has(key)) {
189
- name = key;
190
- }
191
- }
192
- if (!name) {
193
- return; // return undefined to signal no transform
194
- }
195
-
196
- const args = name === 'count' || name == null ? [] : toArray(spec[name]);
197
- let str = `vg.${name}(`
198
- + args.map(v => ctx.maybeParam(v)).join(', ')
199
- + ')';
200
-
201
- if (spec.distinct) {
202
- str += '.distinct()'
203
- }
204
- if (spec.orderby) {
205
- const p = toArray(spec.orderby).map(v => ctx.maybeParam(v));
206
- str += `.orderby(${p.join(', ')})`;
207
- }
208
- if (spec.partitionby) {
209
- const p = toArray(spec.partitionby).map(v => ctx.maybeParam(v));
210
- str += `.partitionby(${p.join(', ')})`;
211
- }
212
- if (spec.rows) {
213
- str += `.rows(${ctx.maybeParam(spec.rows)})`;
214
- } else if (spec.range) {
215
- str += `.range(${ctx.maybeParam(spec.rows)})`;
216
- }
217
- return str;
218
- }
219
-
220
- function parseParam(param, ctx) {
221
- param = isObject(param) ? param : { value: param };
222
- const { select = 'value' } = param;
223
- const parser = ctx.paramParsers.get(select);
224
- if (!parser) {
225
- error(`Unrecognized param type: ${select}`, param);
226
- }
227
- if (select === 'value') {
228
- const { value, date } = param;
229
- return Array.isArray(value)
230
- ? `vg.Param.array([${value.map(v => ctx.maybeParam(v)).join(', ')}])`
231
- : date ? `vg.Param.value(new Date(${JSON.stringify(date)}))`
232
- : `vg.Param.value(${JSON.stringify(value)})`;
233
- } else {
234
- return `vg.Selection.${select}()`;
235
- }
236
- }
237
-
238
- function dataOptions(options) {
239
- const opt = [];
240
- for (const key in options) {
241
- opt.push(`${key}: ${JSON.stringify(options[key])}`);
242
- }
243
- return opt.length ? `, { ${opt.join(', ')} }` : '';
244
- }
245
-
246
- function parseTableData(name, spec) {
247
- // eslint-disable-next-line no-unused-vars
248
- const { query, type, ...options } = spec;
249
- if (query) {
250
- return `\`${create(name, query, options)}\``;
251
- }
252
- }
253
-
254
- function parseParquetData(name, spec) {
255
- // eslint-disable-next-line no-unused-vars
256
- const { file, type, ...options } = spec;
257
- return `vg.loadParquet("${name}", "${file}"${dataOptions(options)})`;
258
- }
259
-
260
- function parseCSVData(name, spec) {
261
- // eslint-disable-next-line no-unused-vars
262
- const { file, type, ...options } = spec;
263
- return `vg.loadCSV("${name}", "${file}"${dataOptions(options)})`;
264
- }
265
-
266
- function parseJSONData(name, spec) {
267
- // eslint-disable-next-line no-unused-vars
268
- const { data, file, type, ...options } = spec;
269
- const opt = dataOptions(options);
270
- if (data) {
271
- const d = '[\n '
272
- + data.map(d => JSON.stringify(d)).join(',\n ')
273
- + '\n ]';
274
- return `vg.loadObjects("${name}", ${d}${opt})`;
275
- } else {
276
- return `vg.loadCSV("${name}", "${file}"${opt})`;
277
- }
278
- }
279
-
280
- function fetchJSON(spec) {
281
- const { data, file } = spec;
282
- return data
283
- ? JSON.stringify(data)
284
- : `await fetch("${file}")\n .then(r => r.json())`;
285
- }
286
-
287
- function parseGeoJSONData(name, spec, ctx) {
288
- ctx.datasets.set(name, name);
289
- return { data: fetchJSON(spec) };
290
- }
291
-
292
- function parseTopoJSONData(name, spec, ctx) {
293
- ctx.datasets.set(name, name);
294
- const json = fetchJSON(spec);
295
- let data;
296
- if (spec.feature) {
297
- ctx.addImport(TOPOJSON, 'feature');
298
- const object = `json.objects['${spec.feature}']`;
299
- data = json + `\n .then(json => feature(json, ${object}).features)`;
300
- } else {
301
- ctx.addImport(TOPOJSON, 'mesh');
302
- const object = spec.mesh ? `json.objects['${spec.mesh}']` : 'undefined';
303
- const filter = spec.filter === 'interior' ? ', (a, b) => a !== b'
304
- : spec.filter === 'exterior' ? ', (a, b) => a === b'
305
- : '';
306
- data = json + `\n .then(json => [mesh(json, ${object}${filter})])`;
307
- }
308
- return { data };
309
- }
310
-
311
- function parseSpec(spec, ctx) {
312
- for (const [key, { type, parse }] of ctx.specParsers) {
313
- const value = spec[key];
314
- if (value != null) {
315
- if (type(value)) {
316
- return parse(spec, ctx);
317
- } else {
318
- error(`Invalid property type: ${key}`, spec);
319
- }
320
- }
321
- }
322
- error(`Invalid specification.`, spec);
323
- }
324
-
325
- function parseHSpace(spec, ctx) {
326
- return `${ctx.tab()}vg.hspace(${spec.hspace})`;
327
- }
328
-
329
- function parseVSpace(spec, ctx) {
330
- return `${ctx.tab()}vg.vspace(${spec.vspace})`;
331
- }
332
-
333
- function parseInput(spec, ctx) {
334
- const { input, ...options } = spec;
335
- const fn = ctx.inputs.get(input);
336
- if (!isFunction(fn)) {
337
- error(`Unrecognized input: ${input}`, spec);
338
- }
339
- const opt = [];
340
- for (const key in options) {
341
- opt.push(`${key}: ${ctx.maybeSelection(options[key])}`);
342
- }
343
- return `${ctx.tab()}vg.${input}({ ${opt.join(', ')} })`;
344
- }
345
-
346
- function parseVConcat(spec, ctx) {
347
- ctx.indent();
348
- const items = spec.vconcat.map(s => parseSpec(s, ctx));
349
- ctx.undent();
350
- return `${ctx.tab()}vg.vconcat(\n${items.join(',\n')}\n${ctx.tab()})`;
351
- }
352
-
353
- function parseHConcat(spec, ctx) {
354
- ctx.indent();
355
- const items = spec.hconcat.map(s => parseSpec(s, ctx));
356
- ctx.undent();
357
- return `${ctx.tab()}vg.hconcat(\n${items.join(',\n')}\n${ctx.tab()})`;
358
- }
359
-
360
- function parsePlot(spec, ctx) {
361
- const { plot, ...attributes } = spec;
362
-
363
- ctx.indent();
364
- const attrs = [
365
- ...(ctx.plotDefaults ? [`${ctx.tab()}...defaultAttributes`] : []),
366
- ...Object.keys(attributes).map(key => parseAttribute(spec, key, ctx))
367
- ];
368
- const entries = plot.map(e => parseEntry(e, ctx));
369
- const items = entries.concat(attrs);
370
- ctx.undent();
371
-
372
- return `${ctx.tab()}vg.plot(\n${items.join(',\n')}\n${ctx.tab()})`;
373
- }
374
-
375
- function parseNakedMark(spec, ctx) {
376
- return parsePlot({ plot: [spec] }, ctx);
377
- }
378
-
379
- function parseLegend(spec, ctx) {
380
- const { legend, ...options } = spec;
381
- const type = `${legend}Legend`;
382
- if (!isFunction(ctx.legends.get(type))) {
383
- error(`Unrecognized legend type: ${legend}`, spec);
384
- }
385
- const opt = [];
386
- for (const key in options) {
387
- opt.push(`${key}: ${ctx.maybeSelection(options[key])}`);
388
- }
389
- return `${ctx.tab()}vg.${type}({ ${opt.join(', ')} })`;
390
- }
391
-
392
- function parseAttribute(spec, name, ctx) {
393
- const fn = ctx.attributes.get(name);
394
- if (!isFunction(fn)) {
395
- error(`Unrecognized attribute: ${name}`, spec);
396
- }
397
- const value = spec[name];
398
- const arg = value === 'Fixed' ? 'vg.Fixed' : ctx.maybeParam(value);
399
- return `${ctx.tab()}vg.${name}(${arg})`;
400
- }
401
-
402
- function parseEntry(spec, ctx) {
403
- return isString(spec.mark) ? parseMark(spec, ctx)
404
- : isString(spec.legend) ? parseLegend(spec, ctx)
405
- : isString(spec.select) ? parseInteractor(spec, ctx)
406
- : error(`Invalid plot entry.`, spec);
407
- }
408
-
409
- function parseMark(spec, ctx) {
410
- const { mark, data, ...options } = spec;
411
-
412
- if (!isFunction(ctx.marks.get(mark))) {
413
- error(`Unrecognized mark type: ${mark}`, spec);
414
- }
415
-
416
- const input = parseMarkData(data, ctx);
417
- const opt = [];
418
- for (const key in options) {
419
- opt.push(`${key}: ${parseMarkOption(options[key], ctx)}`);
420
- }
421
- const d = input || '';
422
- const o = opt.length ? `{ ${opt.join(', ')} }` : '';
423
- let arg = `${d}${o}`;
424
- if (d && o) {
425
- ctx.indent();
426
- arg = `\n${ctx.tab()}${d},\n${ctx.tab()}${o}\n`;
427
- ctx.undent();
428
- arg += ctx.tab();
429
- }
430
- return `${ctx.tab()}vg.${mark}(${arg})`;
431
- }
432
-
433
- function parseMarkData(spec, ctx) {
434
- if (!spec) return null; // no data, likely a decoration mark
435
- if (isArray(spec)) return JSON.stringify(spec); // data provided directly
436
- const { from: table, ...options } = spec;
437
- if (ctx.datasets.has(table)) {
438
- // client-managed data, simply pass through
439
- return ctx.datasets.get(table);
440
- } else {
441
- // source-managed data, create from descriptor
442
- const opt = [];
443
- for (const key in options) {
444
- opt.push(`${key}: ${ctx.maybeSelection(options[key])}`);
445
- }
446
- const arg = opt.length ? `, { ${opt.join(', ')} }` : '';
447
- return `vg.from("${table}"${arg})`;
448
- }
449
- }
450
-
451
- function parseMarkOption(spec, ctx) {
452
- return ctx.maybeTransform(spec) || ctx.maybeParam(spec);
453
- }
454
-
455
- function parseInteractor(spec, ctx) {
456
- const { select, ...options } = spec;
457
- if (!isFunction(ctx.interactors.get(select))) {
458
- error(`Unrecognized interactor type: ${select}`, spec);
459
- }
460
- const opt = [];
461
- for (const key in options) {
462
- opt.push(`${key}: ${ctx.maybeSelection(options[key])}`);
463
- }
464
- return `${ctx.tab()}vg.${select}({ ${opt.join(', ')} })`;
465
- }
package/src/spec/util.js DELETED
@@ -1,43 +0,0 @@
1
- export function paramRef(value) {
2
- const type = typeof value;
3
- return type === 'object' ? value?.param
4
- : type === 'string' ? paramStr(value)
5
- : null;
6
- }
7
-
8
- export function paramStr(value) {
9
- return value?.[0] === '$' ? value.slice(1) : null;
10
- }
11
-
12
- export function toArray(value) {
13
- return [value].flat();
14
- }
15
-
16
- export function isArray(value) {
17
- return Array.isArray(value);
18
- }
19
-
20
- export function isObject(value) {
21
- return value !== null && typeof value === 'object' && !isArray(value);
22
- }
23
-
24
- export function isNumber(value) {
25
- return typeof value === 'number';
26
- }
27
-
28
- export function isNumberOrString(value) {
29
- const t = typeof value;
30
- return t === 'number' || t === 'string';
31
- }
32
-
33
- export function isString(value) {
34
- return typeof value === 'string';
35
- }
36
-
37
- export function isFunction(value) {
38
- return typeof value === 'function';
39
- }
40
-
41
- export function error(message, data) {
42
- throw Object.assign(Error(message), { data });
43
- }
package/src/symbols.js DELETED
@@ -1,3 +0,0 @@
1
- export const Fixed = Symbol('Fixed');
2
- export const Transient = Symbol('Transient');
3
- export const Transform = Symbol('Transform');
@@ -1,81 +0,0 @@
1
- import { asColumn } from '@uwdata/mosaic-sql';
2
- import { Transform } from '../symbols.js';
3
-
4
- const EXTENT = [
5
- 'rectY-x', 'rectX-y', 'rect-x', 'rect-y'
6
- ];
7
-
8
- function hasExtent(channel, type) {
9
- return EXTENT.includes(`${type}-${channel}`);
10
- }
11
-
12
- export function bin(field, options = { steps: 25 }) {
13
- const fn = (mark, channel) => {
14
- return hasExtent(channel, mark.type)
15
- ? {
16
- [`${channel}1`]: binField(mark, field, options),
17
- [`${channel}2`]: binField(mark, field, { ...options, offset: 1 })
18
- }
19
- : {
20
- [channel]: binField(mark, field, options)
21
- };
22
- };
23
- fn[Transform] = true;
24
- return fn;
25
- }
26
-
27
- function binField(mark, column, options) {
28
- return {
29
- column,
30
- label: column,
31
- get stats() { return ['min', 'max']; },
32
- get columns() { return [column]; },
33
- get basis() { return column; },
34
- toString() {
35
- const { min, max } = mark.stats[column];
36
- const b = bins(min, max, options);
37
- const col = asColumn(column);
38
- const base = b.min === 0 ? col : `(${col} - ${b.min})`;
39
- const alpha = `${(b.max - b.min) / b.steps}::DOUBLE`;
40
- const off = options.offset ? `${options.offset} + ` : '';
41
- return `${b.min} + ${alpha} * (${off}FLOOR(${base} / ${alpha})::INTEGER)`;
42
- }
43
- };
44
- }
45
-
46
- export function bins(min, max, options) {
47
- let { steps = 25, minstep = 0, nice = true } = options;
48
-
49
- if (nice !== false) {
50
- // use span to determine step size
51
- const span = max - min;
52
- const maxb = steps;
53
- const logb = Math.LN10;
54
- const level = Math.ceil(Math.log(maxb) / logb);
55
- let step = Math.max(
56
- minstep,
57
- Math.pow(10, Math.round(Math.log(span) / logb) - level)
58
- );
59
-
60
- // increase step size if too many bins
61
- while (Math.ceil(span / step) > maxb) { step *= 10; }
62
-
63
- // decrease step size if allowed
64
- const div = [5, 2];
65
- let v;
66
- for (let i = 0, n = div.length; i < n; ++i) {
67
- v = step / div[i];
68
- if (v >= minstep && span / v <= maxb) step = v;
69
- }
70
-
71
- v = Math.log(step);
72
- const precision = v >= 0 ? 0 : ~~(-v / logb) + 1;
73
- const eps = Math.pow(10, -precision - 1);
74
- v = Math.floor(min / step + eps) * step;
75
- min = min < v ? v - step : v;
76
- max = Math.ceil(max / step) * step;
77
- steps = Math.round((max - min) / step);
78
- }
79
-
80
- return { min, max, steps };
81
- }
@@ -1,3 +0,0 @@
1
- export {
2
- bin
3
- } from './bin.js';
File without changes