New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@evidence-dev/component-utilities

Package Overview
Dependencies
Maintainers
5
Versions
387
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@evidence-dev/component-utilities - npm Package Compare versions

Comparing version 0.0.0-e1b03a15 to 0.0.0-e22a6592

src/tests/getCompletedData.fixture.manual.js

7

CHANGELOG.md
# @evidence-dev/component-utilities
## 1.1.2
### Patch Changes
- 4944f21c: getCompletedData() fills all x values for categorical series
- 287126fe: Ensure that numeric and date x-axis series are sorted
## 1.1.1

@@ -4,0 +11,0 @@

2

package.json
{
"name": "@evidence-dev/component-utilities",
"version": "0.0.0-e1b03a15",
"version": "0.0.0-e22a6592",
"description": "",

@@ -5,0 +5,0 @@ "main": "index.js",

@@ -5,2 +5,11 @@ import { registerTheme, init } from 'echarts';

/**
* @typedef {import("echarts").EChartsOption & {
* dispatch?: ReturnType<typeof import("svelte").createEventDispatcher>;
* showAllXAxisLabels?: boolean;
* }
* } ActionParams
*/
/** @type {import("svelte/action").Action<HTMLElement, ActionParams>} */
export default (node, option) => {

@@ -428,13 +437,2 @@ registerTheme('evidence-light', {

// If the x-axis of a series is numeric, or a date; ensure that it is in order
option.series = option.series.map((s) => {
if (typeof s.data?.[0][0] === 'number') {
s.data = s.data.sort((a, b) => a[0] - b[0]);
} else if (s.data?.[0][0] instanceof Date) {
s.data = s.data.sort((a, b) => a[0].getTime() - b[0]?.getTime() ?? 0);
}
return s;
});
chart.setOption(option);

@@ -449,3 +447,3 @@

const containerElement = document.querySelector('div.content > article');
const resizeChart = debounce(() => {
const onWindowResize = debounce(() => {
chart.resize({

@@ -456,16 +454,46 @@ animation: {

});
updateLabelWidths();
}, 100);
const updateLabelWidths = () => {
// Make sure we operate on an up-to-date options object
/** @type {import("echarts").EChartsOption} */
const prevOption = chart.getOption();
if (!prevOption) return;
// If the options object includes showing all x axis labels
// Note: this isn't a standard option, but right now this is the easiest way to pass something to the action.
// We don't want to have multiple resize observers if we can avoid it, and this is all due for a cleanup anyways
if (prevOption.showAllXAxisLabels) {
// Get all the possible x values
const distinctXValues = new Set(prevOption.series.flatMap((s) => s.data?.map((d) => d[0])));
const modConst = 4 / 5;
const clientWidth = node?.clientWidth ?? 0;
/** @type {import("echarts").EChartsOption} */
const newOption = {
xAxis: {
axisLabel: {
interval: 0,
overflow: 'truncate',
width: (clientWidth * modConst) / distinctXValues.size
}
}
};
chart.setOption(newOption);
}
};
if (window.ResizeObserver && containerElement) {
// TODO: This was originally added to combat a bug here: https://github.com/evidence-dev/evidence/pull/450
// Another solution is required. Something like lodash debounce might be an easy win to solve this.
resizeObserver = new ResizeObserver(resizeChart);
resizeObserver = new ResizeObserver(onWindowResize);
resizeObserver.observe(containerElement);
} else {
window.addEventListener('resize', resizeChart);
window.addEventListener('resize', onWindowResize);
}
onWindowResize();
return {
update(option) {
chart.setOption(option, true, true);
updateLabelWidths();
},

@@ -476,3 +504,3 @@ destroy() {

} else {
window.removeEventListener('resize', resizeChart);
window.removeEventListener('resize', onWindowResize);
}

@@ -479,0 +507,0 @@ chart.dispose();

@@ -13,3 +13,3 @@ import ssf from 'ssf';

export const getCustomFormats = () => {
return getContext(CUSTOM_FORMATTING_SETTINGS_CONTEXT_KEY).getCustomFormats() || [];
return getContext(CUSTOM_FORMATTING_SETTINGS_CONTEXT_KEY)?.getCustomFormats() || [];
};

@@ -16,0 +16,0 @@

@@ -28,7 +28,44 @@ import { tidy, complete, mutate } from '@tidyjs/tidy';

// Ensures that all permutations of this map exist in the output
// e.g. can include series and x values to ensure that all series have all x values
const expandKeys = {};
const xIsDate = data[0]?.[x] instanceof Date;
/** @type {Array<number | string>} */
let xDistinct;
const exampleX = data[0]?.[x];
switch (typeof exampleX) {
case 'object':
// If x is not a date; this shouldn't be hit, abort!
if (!(exampleX instanceof Date)) {
throw new Error('Unexpected object property, expected string, date, or number');
}
// Map dates to numeric values
xDistinct = getDistinctValues(
data.map((d) => ({ [x]: d[x].getTime() })),
x
);
// We don't fillX here because date numbers are very large, so a small interval would create a _massive_ array
break;
case 'number':
// Numbers are the most straightforward
xDistinct = getDistinctValues(data, x);
if (fillX) {
// Attempt to derive the interval between X values and interpolate missing values in that set (within the bounds of min/max)
const interval = findInterval(xDistinct);
expandKeys[x] = vectorSeq(xDistinct, interval);
}
break;
case 'string':
xDistinct = getDistinctValues(data, x);
expandKeys[x] = xDistinct;
break;
}
const output = [];
for (const value of Object.values(groups)) {
let xIsDate = value[0]?.[x] instanceof Date;
const nullySpec = { series: null };
const nullySpec = series ? { [series]: null } : {};
if (nullsZero) {

@@ -41,20 +78,2 @@ nullySpec[y] = 0;

const expandKeys = {};
if (fillX) {
/** @type {Array<number>} */
let xDistinct;
if (xIsDate)
xDistinct = getDistinctValues(
value.map((d) => ({ [x]: d[x].getTime() })),
x
);
else xDistinct = getDistinctValues(value, x);
/** @type {number} */
let interval = findInterval(xDistinct);
// Array of all possible x values
expandKeys[x] = vectorSeq(xDistinct, interval);
}
if (series) {

@@ -65,6 +84,5 @@ expandKeys[series] = series;

const tidyFuncs = [];
if (Object.keys(expandKeys).length === 0) {
// empty object, no special configuration
tidyFuncs.push(complete([x], nullySpec));
// empty object, no special configuration
} else {

@@ -74,2 +92,3 @@ tidyFuncs.push(complete(expandKeys, nullySpec));

if (xIsDate) {
// Ensure that x is actually a date
tidyFuncs.push(

@@ -81,5 +100,6 @@ mutate({

}
output.push(...tidy(value, ...tidyFuncs));
output.push(tidy(value, ...tidyFuncs));
}
return output;
return output.flat();
}

@@ -9,2 +9,16 @@ import { faker } from '@faker-js/faker';

/**
* @typedef {Object} GenSeriesKeyOpts
* @property {string} [x]
* @property {string} [y]
* @property {MockSeries} [series]
*/
/**
* @typedef {Object} GenSeriesResult
* @property { { series: MockSeries, value: number, time: number }[] } data
* @property { Record<MockSeries, { interval: number }> } series
* @property { GenSeriesKeyOpts } keys
*/
/**
* @typedef {Object} GenSeriesOpts

@@ -14,23 +28,34 @@ * @property {boolean} xHasGaps Determines if the x axis will have all expected values

* @property {boolean} seriesAlwaysExists
* @property {number} [seriesLen] Max length of each series
* @property {number} [minSeriesLen] Min length of each series
* @property {number} [maxSeriesLen] Max length of each series
* @property {number} [minSeriesCount] Min number of series
* @property {number} [maxSeriesCount] Max number of series
* @property {number} [minInterval] Min interval between x values (e.g. interval of 2 is 0,2,4)
* @property {number} [maxInterval] Max interval between x values (e.g. interval of 2 is 0,2,4)
* @property {number} [maxOffset] Max offset for initial x value (e.g. interval of 2, offset of 1 is 1,3,5)
* @property {'number' | 'date'} [xType] determines the type of the x axis
* @property {'number' | 'date' | 'categories'} [xType] determines the type of the x axis
* @property { GenSeriesKeyOpts} [keys] Allows changing the structure of the output
*/
/**
* @param {}
* @returns { { data: { series: MockSeries, value: number, time: number }[], series: Record<MockSeries, { interval: number }> } }
* @param {GenSeriesOpts}
* @returns {GenSeriesResult}
*/
export const genSeries = ({
const genNumericSeries = ({
xHasGaps = false,
yHasNulls = false,
seriesAlwaysExists = true,
maxSeriesLen = 20,
minSeriesLen = 10,
minSeriesLen = 2,
maxSeriesLen = 10,
minInterval = 1,
maxInterval = 5,
minSeriesCount = 2,
maxSeriesCount = 5,
minInterval = 1,
maxInterval = 20,
maxOffset = 100,
xType = 'number'
maxOffset = 10,
xType = 'number',
keys = {
x: 'time',
y: 'value',
series: 'series'
}
} = {}) => {

@@ -43,4 +68,4 @@ const data = [];

faker.number.int({
min: 2,
max: maxSeriesCount
min: minSeriesCount,
max: maxSeriesCount < minSeriesCount ? minSeriesCount : maxSeriesCount
})

@@ -58,2 +83,3 @@ )

);
for (const [seriesName, d] of Object.entries(series)) {

@@ -68,5 +94,5 @@ const initialValue = xType === 'number' ? d.offset : new Date();

data.push({
series: seriesName,
value: faker.number.float({ min: -1000, max: 1000 }),
time: genTime(i)
[keys.series]: seriesName,
[keys.y]: faker.number.float({ min: -1000, max: 1000 }),
[keys.x]: genTime(i)
});

@@ -101,5 +127,5 @@ }

data.push({
series: null,
value: faker.number.float({ min: -1000, max: 1000 }),
time: genTime(i)
[keys.series]: null,
[keys.y]: faker.number.float({ min: -1000, max: 1000 }),
[keys.x]: genTime(i)
});

@@ -121,4 +147,66 @@ }

series,
data
data,
keys
};
};
/**
* @param {GenSeriesOpts}
* @returns {GenSeriesResult}
*/
const getCatagoricalSeries = ({
minSeriesLen = 5,
maxSeriesLen = 50,
minSeriesCount = 2,
maxSeriesCount = 10,
keys = {
x: 'category',
y: 'value',
series: 'series'
}
} = {}) => {
const seriesLength = faker.number.int({ min: minSeriesLen, max: maxSeriesLen });
const seriesCount = faker.number.int({ min: minSeriesCount, max: maxSeriesCount });
const categories = new Array(seriesLength).fill(null).map(() => faker.location.streetAddress());
const series = new Array(seriesCount).fill(null).map(() => faker.company.name());
const data = series.reduce((a, v) => {
for (const category of categories) {
a.push({
[keys.series]: v,
[keys.x]: category,
[keys.y]: faker.number.int({ min: 0, max: 10000 })
});
}
return a;
}, []);
return {
series: Object.fromEntries(
series.map((seriesName) => [seriesName, { len: seriesLength, offset: 0, interval: 1 }])
),
data,
keys
};
};
/**
* @param {GenSeriesOpts} cfg
* @returns {GenSeriesResult}
*/
export const genSeries = (cfg = {}) => {
let v;
switch (cfg.xType) {
case 'date':
case 'number':
default:
v = genNumericSeries(cfg);
break;
case 'category':
v = getCatagoricalSeries(cfg);
break;
}
return v;
};
import { describe, it, expect } from 'vitest';
import getCompletedData from '../getCompletedData';
import { genSeries } from './getCompletedData.fixture.js';
import { genSeries } from './getCompletedData.fixture';
import { MissingYCase } from './getCompletedData.fixture.manual';
const sortFunc = (a, b) => {
const deltaSeries = a.series?.charCodeAt(0) ?? -1 - b.series?.charCodeAt(0) ?? -1;
if (deltaSeries !== 0) return deltaSeries;
const deltaTime = a.time - b.time;
if (deltaTime !== 0) return deltaTime;
const deltaValue = a.value - b.value;
if (deltaValue !== 0) return deltaValue;
return 0;
/**
* @param {string} a
* @param {string} b
*/
const stringSortFunc = (a, b) => {
// Iterate through the strings
for (let i = 0; i < a.length && i < b.length; i++) {
const diff = a.charCodeAt(i) - b.charCodeAt(i);
if (diff !== 0) return diff;
}
return a.length - b.length; // the longer string wins
};
const series = [];
let series = [];
const simple = false;
const fixturePermutations = {
xHasGaps: simple ? [true, false] : [true, false],
yHasNulls: simple ? [true, false] : [true, false],
seriesAlwaysExists: simple ? [true, false] : [true, false],
xType: simple ? ['category'] : ['date', 'number', 'category'],
keys: simple
? [undefined, { x: 'someX', y: 'someY', series: 'someSeries' }]
: [undefined, { x: 'someX', y: 'someY', series: 'someSeries' }]
};
/*

@@ -23,12 +39,11 @@ This is responsible for generating a variety of scenarios that the function may encounter

*/
for (const xHasGaps of [true, false]) {
for (const yHasNulls of [true, false]) {
for (const seriesAlwaysExists of [true, false]) {
for (const xType of ['date', 'number'])
series.push({
xHasGaps,
yHasNulls,
seriesAlwaysExists,
xType,
data: genSeries({
for (const xHasGaps of fixturePermutations.xHasGaps) {
for (const yHasNulls of fixturePermutations.yHasNulls)
for (const seriesAlwaysExists of fixturePermutations.seriesAlwaysExists)
for (const xType of fixturePermutations.xType)
for (const keys of fixturePermutations.keys)
series.push({
description: `(automatic) xType = "${xType}", xHasGaps = ${xHasGaps}, yHasNulls = ${yHasNulls}, seriesAlwaysExists = ${seriesAlwaysExists}, keys = "${JSON.stringify(
keys
)}"`,
xHasGaps,

@@ -38,22 +53,44 @@ yHasNulls,

xType,
seriesLen: 3,
seriesCount: 2,
maxInterval: 1,
maxOffset: 0
})
});
}
}
keys,
manual: false,
data: genSeries({
xHasGaps,
yHasNulls,
seriesAlwaysExists,
xType,
keys,
minSeriesLen: 2,
maxSeriesLen: 2,
maxSeriesCount: 2,
maxInterval: 1,
maxOffset: 0
})
});
}
series.push({
description: '(manual) Manual gap values injected',
data: MissingYCase
});
// Garlic Naan | Goa Chicken | Chicken Curry
/**
* @typedef {Object} SeriesFixture
* @property {import("./getCompletedData.fixture.js").GenSeriesResult} data
* @property { 'date' | 'number' } xType
* @property {boolean} seriesAlwaysExists
* @property {boolean} yHasNulls
* @property {boolean} xHasgaps
* @property {boolean} manual
*/
describe('getCompletedData', () => {
describe.each(series)(
'xHasGaps = $xHasGaps, yHasNulls = $yHasNulls, seriesAlwaysExists = $seriesAlwaysExists, xType = "$xType"',
'$description',
/**
* @param {ReturnType<typeof genSeries>} opts
* @param {SeriesFixture} opts
*/
(opts) => {
const { data, series } = opts.data;
const { data, keys } = opts.data;
it('returns no duplicate rows', () => {
const result = getCompletedData(data, 'time', 'value', 'series', false, false);
const result = getCompletedData(data, keys.x, keys.y, keys.series, false, false);
for (const row of result) {

@@ -63,3 +100,5 @@ expect(

(row2) =>
row.time === row2.time && row.value === row2.value && row.series === row2.series
row[keys.x] === row2[keys.x] &&
row[keys.y] === row2[keys.y] &&
row[keys.series] === row2[keys.series]
).length

@@ -71,8 +110,11 @@ ).toBe(1);

it('replaces nulls with zero if nullsZero is set', () => {
const result = getCompletedData(data, 'time', 'value', 'series', true, true);
const result = getCompletedData(data, keys.x, keys.y, keys.series, true, true);
for (const row of data.filter((r) => r.value === null)) {
for (const row of data.filter((r) => r[keys.y] === null)) {
expect(
result.find((r) => r.series === row.series && r.time.toString() === row.time.toString())
?.value
result.find(
(r) =>
r[keys.series] === row[keys.series] &&
r[keys.x].toString() === row[keys.x].toString()
)?.[keys.y]
).toBeCloseTo(0);

@@ -83,28 +125,60 @@ }

it('does not fill x-axis values if fillX is not set', () => {
const result = getCompletedData(data, 'time', 'value', 'series', false, false);
const result = getCompletedData(data, keys.x, keys.y, keys.series, false, false);
// Expect specific behavior here based on your function's logic
expect(result.every((r) => data.some((d) => r.time === d.time && r.series === d.series)));
expect(
result.every((r) =>
data.some((d) => r[keys.x] === d[keys.x] && r[keys.series] === d[keys.series])
)
);
});
it('returns the original data if series is not defined', () => {
const result = getCompletedData(data, 'time', 'value', undefined, false, false);
it('returns identical columns to the original data', () => {
const result = getCompletedData(data, keys.x, keys.y, keys.series, false, false);
const r = result.sort(sortFunc);
const d = data.sort(sortFunc);
const r = Object.keys(result[0]).sort(stringSortFunc);
const d = Object.keys(data[0]).sort(stringSortFunc);
expect(r).toEqual(d);
});
// This condition is only applicable to non-date series
if (opts.xType !== 'date')
it('contains series each with identical lengths', () => {
const result = getCompletedData(data, keys.x, keys.y, keys.series, false, true);
/** @type {any[][]} */
let groupedSeries = [];
for (const seriesName of Object.keys(opts.data.series)) {
const seriesItems = result.filter((d) => d[keys.series] === seriesName);
groupedSeries.push(seriesItems);
}
expect(groupedSeries[0].length, 'Series must have more than one row').toBeGreaterThan(0);
for (const s of groupedSeries) {
expect(s.length, 'Series lengths must all be equal').toEqual(groupedSeries[0].length);
}
});
it('returns the original data if series is not defined', () => {
const { x, y } = keys;
const result = getCompletedData(data, x, y, undefined, false, false);
expect(data).toEqual(expect.arrayContaining(result));
});
it('fills missing x-axis values with null if fillX is set and not nullsZero', () => {
const result = getCompletedData(data, 'time', 'value', 'series', false, true);
const { x, y, series } = keys;
const result = getCompletedData(data, x, y, series, false, true);
// Expect specific behavior here based on your function's logic
for (const seriesName in series) {
for (const row of result.filter((r) => r.series === seriesName)) {
const inputRow = data.find((d) => d.series === row.series && d.time === row.time);
for (const row of result.filter((r) => r[series] === seriesName)) {
const inputRow = data.find((d) => d[series] === row[series] && d[x] === row[x]);
if (inputRow) {
// This row already existed
expect(row.value).toEqual(inputRow.value);
expect(row[y]).toEqual(inputRow[y]);
} else {
// This row was inserted
expect(row.value).toBe(null);
expect(row[y]).toBe(null);
}

@@ -116,13 +190,15 @@ }

it('fills missing x-axis values with zero if fillX and nullsZero are set', () => {
const result = getCompletedData(data, 'time', 'value', 'series', true, true);
const { x, y, series } = keys;
const result = getCompletedData(data, x, y, series, true, true);
// Expect specific behavior here based on your function's logic
for (const seriesName in series) {
for (const row of result.filter((r) => r.series === seriesName)) {
const inputRow = data.find((d) => d.series === row.series && d.time === row.time);
if (inputRow && inputRow.value !== null) {
for (const row of result.filter((r) => r[series] === seriesName)) {
const inputRow = data.find((d) => d[series] === row[series] && d[x] === row[x]);
if (inputRow && inputRow[y] !== null) {
// This row already existed
expect(row.value).toEqual(inputRow.value);
expect(row[y]).toEqual(inputRow[y]);
} else {
// This row was inserted
expect(row.value).toBe(0);
expect(row[y]).toBe(0);
}

@@ -129,0 +205,0 @@ }

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc