Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RowArrayAdapter #416

Draft
wants to merge 39 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d79b2af
Define RowArrayAdapter interface
ayjayt Mar 10, 2024
212f648
Add test class to adapter and test it
ayjayt Mar 10, 2024
0c9c722
Add findIndex() to rowArrayAdapter and test
ayjayt Mar 10, 2024
e4909ac
Add RowArrayAdapter type to data input[] types
ayjayt Mar 10, 2024
75648d1
Fix adapter .length to be prop not fn
ayjayt Mar 11, 2024
5058956
Fix ArrayAsAdapter to return a dataPoint, not array:
ayjayt Mar 11, 2024
166662e
Add generic to test class for adapter:
ayjayt Mar 11, 2024
19e1b3d
Modify validate and validate.test to allow adapter:
ayjayt Mar 11, 2024
015c193
Change test adapter to support number[]
ayjayt Mar 11, 2024
a02a641
Fix test adapter to obey another corner case
ayjayt Mar 11, 2024
44486e8
Rework type system a bit for array-adapter:
ayjayt Mar 17, 2024
adcc017
Write min/max() for ArrayAsAdapter
ayjayt Mar 18, 2024
9483409
Improve type validations in adapter
ayjayt Mar 18, 2024
eaa1c46
Shim Array.forEach function in ArrayAsAdapter
ayjayt Mar 18, 2024
f0e0733
Allow AdapterRandomizer tester to handle any type
ayjayt Mar 18, 2024
43b95ae
Update tests to use new AdapterRandomizer API
ayjayt Mar 18, 2024
fd0334d
Test new min/max and forEach functions, reorganize
ayjayt Mar 18, 2024
1134d0b
Fix arrayAdapter tests to test more data
ayjayt Mar 18, 2024
10be8ee
Make utils.ts min/max use adapter min/max and test
ayjayt Mar 18, 2024
c574b2d
Resolve some linter errors
ayjayt Mar 18, 2024
1f43a33
Resolve another linter error
ayjayt Mar 18, 2024
05e1825
Format and spell-check
ayjayt Mar 18, 2024
ffb8cd5
Merge branch 'main' into pikul-feature-array-adapter
ayjayt Mar 27, 2024
4675c4e
Merge branch 'main' into pikul-feature-array-adapter
ayjayt Apr 3, 2024
74925a9
Merge branch 'main' into pikul-feature-array-adapter
ayjayt Apr 14, 2024
e850567
Add min/maxWithIndex functions to rowArrayAdapter
ayjayt Apr 15, 2024
100c1fa
Return [index, value] from min/max in util.ts
ayjayt Apr 15, 2024
95a0cbf
Fix calculateMeta... to use ArrayAdapter
ayjayt Apr 15, 2024
7e9666d
Fix mabeMaybeAdapter to accept 0 proability
ayjayt Apr 15, 2024
85e4e4b
Make min/max in test adapter skip nan/null vals
ayjayt Apr 15, 2024
e8dd1c1
Test calculateMeta... w/ stochastic adapter wrapping
ayjayt Apr 15, 2024
4ae773b
Adapt usesAxis to RowArrayAdapter w/ tests
ayjayt Apr 16, 2024
b1d2871
Add forEach to RowArrayAdapter interface type decl
ayjayt Apr 16, 2024
5d6a470
Allow adpaters in last utils methods, write tests
ayjayt Apr 16, 2024
ceb36c7
Merge branch 'main' into pikul-feature-array-...:
ayjayt Apr 16, 2024
6b4d65e
Revert "Merge branch 'main' into pikul-feature-array-...:"
ayjayt Apr 16, 2024
ce64aad
Reapply "Merge branch 'main' into pikul-feature-array-...:"
ayjayt Apr 29, 2024
bf66d3d
Merge branch 'main' into pikul-feature-array-adapter
ayjayt Apr 29, 2024
88c0675
Adapt checkForNumberInput for ArrayAdapter
ayjayt Apr 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/c2mChart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ import { launchOptionDialog } from "./optionDialog";
import { launchInfoDialog } from "./infoDialog";
import { AudioNotificationType } from "./audio/AudioEngine";
import { DEFAULT_LANGUAGE, translate, AVAILABLE_LANGUAGES } from "./translator";
import type { RowArrayAdapter } from "./rowArrayAdapter";

/**
* ValidUserInputRows are the types the user can submit as data rows.
* Note that number will be converted to SimpleDataPoint[],
* so the effective types we accept is more restrictive.
*/
type ValidUserInputRows =
| RowArrayAdapter<SupportedDataPointType>
| (number | SupportedDataPointType)[];

/**
* Metadata about previous levels. Used to quickly return to parents.
Expand Down Expand Up @@ -1385,14 +1395,14 @@ export class c2m {
(value, index) => index
);
this._data = Object.values(userData).map((row) =>
convertDataRow(row)
convertDataRow(row as ValidUserInputRows)
);
return;
}

this._groups = [""];
this._visible_group_indices = [0];
this._data = [convertDataRow(userData)];
this._data = [convertDataRow(userData as ValidUserInputRows)];
}

/**
Expand Down
230 changes: 230 additions & 0 deletions src/rowArrayAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
// The point of rowArrayAdapter is that we don't have to copy data into c2m if a library
// we're integrating with already stores it differently.
// TODO: supporting sort/continuous might be a little verbose
// author: Andrew Pikul ([email protected])

import type { SupportedDataPointType } from "./dataPoint";
import { isHighLowDataPoint, isOHLCDataPoint } from "./dataPoint";

/**
* An interface that imitates an array to give c2m read-access to chart data stored elsewhere.
*/
export interface RowArrayAdapter<T extends SupportedDataPointType> {
length: number;
min: (prop: string) => number;
minWithIndex: (prop: string) => [number, number];
max: (prop: string) => number;
maxWithIndex: (prop: string) => [number, number];
at: (index: number) => T;
findIndex(test: (T) => boolean): number;
forEach(
callbackFn: (value: T, index: number, array: ArrayAsAdapter<T>) => void,
thisArg?: unknown
): void;
}

/**
* Check if an object implements the RowArrayAdapter interface. Used where we use Array.isArray().
* @param obj - the object to check
* @returns true if the object implements the interface
*/
export function isRowArrayAdapter(
obj: unknown
): obj is RowArrayAdapter<SupportedDataPointType> {
return (
obj &&
typeof obj === "object" &&
"length" in obj && // TODO: Could, if they give us "length()" instead of "length", we fix it for them?
"min" in obj &&
"max" in obj &&
"at" in obj
);
}

/**
* Create a RowArrayAdapter from an actual array.
*/
export class ArrayAsAdapter<T extends number | SupportedDataPointType> {
_array: T[];

/**
* Construct adapter from supplied array
* @param array - the underlying array from the adapter
*/
constructor(array: (T | SupportedDataPointType)[]) {
// NOTE: If you give us a SupportedDataPointType, we will attempt to cast it for you to type T
if (!array) {
this._array = [] as T[];
return; // (Should throw error? don't think c2m allows empty data)
}
this._array = array as T[]; // Don't inherit array, we want to fail Array.isArray()
}

/**
* Shims the Array.length property
* @returns the length of the array
*/
get length(): number {
return this._array.length;
}

/**
* Implements a min() function, in this case a shim over Math.min()
* @param prop - a string indicating the property that is assessed for min
* @returns the minimum value of the array
*/
min(prop: string): number {
return this.minWithIndex(prop)[1];
}

/**
* Implements a function like min() but returns an array of [index, value]
* @param prop - a string indicating the property that is assessed for min
* @returns [index, value] corresponding to the minimum of the row
*/
minWithIndex(prop: string): [number, number] {
if (!this._array) return [-1, NaN];
return this._array.reduce(
(
localMinimum: [number, number],
point: T,
currentIndex: number
): [number, number] => {
let val: number = NaN;
if (typeof point === "number") {
if (prop) return [-1, NaN];
val = point;
// eslint and tsc disagree about whether or not the above condition
// is sufficient to guarantee type exclusion, tsc says no. the argument
// gets rather abstract wrt `extends`, but this is just a test implementation
// and real implementations should not support numbers anyway
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
} else if (prop in (point as SupportedDataPointType)) {
// online linter wants me to specify [index: string]:number to use `in`
val = point[prop] as number;
} else if (isOHLCDataPoint(point) && prop === "y") {
val = Math.min(
point.high,
point.low,
point.open,
point.close
);
} else if (isHighLowDataPoint(point) && prop === "y") {
val = Math.min(point.high, point.low);
} else return localMinimum;
if (isNaN(val) || val === null) {
return localMinimum;
}
if (isNaN(localMinimum[1])) {
return [currentIndex, val];
}
return val < localMinimum[1]
? [currentIndex, val]
: localMinimum;
},
[-1, NaN] // Initial value of reduce()
);
}

/**
* Implements a max() function, in this case a shim over Math.max()
* @param prop - a string indicating the property that is assessed for min
* @returns the maximum value of the array
*/
max(prop: string): number {
return this.maxWithIndex(prop)[1];
}

/**
* Implements a function like max(), but returns an array of [index, value]
* @param prop - a string indicating the property that is assessed for min
* @returns [index, value] coresponding to the maximum of the row
*/
maxWithIndex(prop: string): [number, number] {
if (!this._array) return [-1, NaN];
return this._array.reduce(
(
localMaximum: [number, number],
point: T,
currentIndex: number
): [number, number] => {
let val: number = NaN;
if (typeof point === "number") {
if (prop) return [-1, NaN];
val = point;
// eslint and tsc disagree about whether or not the above condition
// is sufficient to guarantee type exclusion, tsc says no. the argument
// gets rather abstract wrt `extends`, but this is just a test implementation
// and real implementations should not support numbers anyway
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
} else if (prop in (point as SupportedDataPointType)) {
// online linter wants me to specify [index: string]:number to use `in`
val = point[prop] as number;
} else if (isOHLCDataPoint(point) && prop === "y") {
val = Math.max(
point.high,
point.low,
point.open,
point.close
);
} else if (isHighLowDataPoint(point) && prop === "y") {
val = Math.max(point.high, point.low);
} else return localMaximum;
if (isNaN(val) || val === null) {
return localMaximum;
}
if (isNaN(localMaximum[1])) {
return [currentIndex, val];
}
return val > localMaximum[1]
? [currentIndex, val]
: localMaximum;
},
[-1, NaN] // Initial value of reduce()
);
}

/**
* Shims the Array.at() function
* @param index - the index of the value you'd like to access
* @returns the value at the supplied index
*/
at(index: number): T {
return this._array.at(index);
}

/**
* Shims the Array.findIndex() function, finds index of first element which satisfies the test function
* @param test - then function by which we test
* @returns index of first element
*/
findIndex(test: (T) => boolean): number {
return this._array.findIndex(test);
}

/**
* forEach imitates an arrays forEach():
* @param callbackFn - a function to execute for each element in the "array".
* It accepts three arguments:
* value: T
* index: number
* array: the current ArrayAsAdapter<T>
*
* and returns nothing.
* @param thisArg - an optional argument to set as "this" in your callbackFn.
* As in Array.prototype.forEach(), if the callbackFn is defined by arrow syntax,
* the arrow syntax will lexically bind its own this and ignore thisArg.
*/
forEach(
callbackFn: (value: T, index: number, array: ArrayAsAdapter<T>) => void,
thisArg?: unknown
): void {
this._array.forEach((innerValue: T, innerIndex: number) => {
// typescript doesn't like us binding thisArg which is `unknown` type
// but we're shimming a javascript function and the user has the right
// to assign legitimately any value they'd like as `this`
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
callbackFn.bind(thisArg)(innerValue, innerIndex, this);
}); // purposely use => because we need our lexically-scoped this
}
}
11 changes: 9 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type { AudioEngine } from "./audio/";
import type { c2m } from "./c2mChart";
import type { SupportedDataPointType } from "./dataPoint";
import type { RowArrayAdapter } from "./rowArrayAdapter";

/**
* Details for a given hotkey
Expand Down Expand Up @@ -71,7 +72,10 @@ export type SonifyTypes = {
* The data that should be presented in this chart.
* This key is required for all charts.
*/
data: dataSet | SupportedInputType[];
data:
| dataSet
| SupportedInputType[]
| RowArrayAdapter<SupportedDataPointType>;
/**
* The HTML element in the DOM that represents this chart.
* This will be used to handle keyboard events to enable the user to interact with the chart.
Expand Down Expand Up @@ -111,7 +115,10 @@ export type SonifyTypes = {
* A dictionary of data, where the key is the group name, and the value is the array of data points
*/
export type dataSet = {
[groupName: string]: SupportedInputType[] | null;
[groupName: string]:
| SupportedInputType[]
| RowArrayAdapter<SupportedDataPointType>
| null;
};

/**
Expand Down
Loading
Loading