diff --git a/tsp-typescript-client/fixtures/tsp-client/fetch-generic-xy-0.json b/tsp-typescript-client/fixtures/tsp-client/fetch-generic-xy-0.json new file mode 100644 index 0000000..1345492 --- /dev/null +++ b/tsp-typescript-client/fixtures/tsp-client/fetch-generic-xy-0.json @@ -0,0 +1,229 @@ +{ + "model": { + "title": "Function Density View", + "series": [ + { + "seriesId": 3, + "seriesName": "15652", + "xValues": [ + [ + 0, + 604229397 + ], + [ + 604229398, + 1208458795 + ], + [ + 1208458796, + 1812688193 + ], + [ + 1812688194, + 2416917590 + ], + [ + 2416917591, + 3021146988 + ] + ], + "yValues": [ + 3.0, + 0.0, + 0.0, + 0.0, + 1.0 + ], + "style": { + "parentKey": null, + "values": { + "series-type": "bar" + } + }, + "xValuesDescription": { + "dataType": "DURATION", + "unit": "ns", + "axisDomain": { + "type": "range", + "start": 0, + "end": 3021146988 + }, + "label": "Execution Time" + }, + "yValuesDescription": { + "dataType": "NUMBER", + "unit": "", + "axisDomain": null, + "label": "Number of Executions" + } + }, + { + "seriesId": 4, + "seriesName": "15653", + "xValues": [ + [ + 0, + 604229397 + ], + [ + 604229398, + 1208458795 + ], + [ + 1208458796, + 1812688193 + ], + [ + 1812688194, + 2416917590 + ], + [ + 2416917591, + 3021146988 + ] + ], + "yValues": [ + 3.0, + 0.0, + 0.0, + 0.0, + 1.0 + ], + "style": { + "parentKey": null, + "values": { + "series-type": "bar" + } + }, + "xValuesDescription": { + "dataType": "DURATION", + "unit": "ns", + "axisDomain": { + "type": "range", + "start": 0, + "end": 3021146988 + }, + "label": "Execution Time" + }, + "yValuesDescription": { + "dataType": "NUMBER", + "unit": "", + "axisDomain": null, + "label": "Number of Executions" + } + }, + { + "seriesId": 2, + "seriesName": "15646", + "xValues": [ + [ + 0, + 604229397 + ], + [ + 604229398, + 1208458795 + ], + [ + 1208458796, + 1812688193 + ], + [ + 1812688194, + 2416917590 + ], + [ + 2416917591, + 3021146988 + ] + ], + "yValues": [ + 3.0, + 0.0, + 0.0, + 0.0, + 1.0 + ], + "style": { + "parentKey": null, + "values": { + "series-type": "bar" + } + }, + "xValuesDescription": { + "dataType": "DURATION", + "unit": "ns", + "axisDomain": { + "type": "range", + "start": 0, + "end": 3021146988 + }, + "label": "Execution Time" + }, + "yValuesDescription": { + "dataType": "NUMBER", + "unit": "", + "axisDomain": null, + "label": "Number of Executions" + } + }, + { + "seriesId": 1, + "seriesName": "15647", + "xValues": [ + [ + 0, + 604229397 + ], + [ + 604229398, + 1208458795 + ], + [ + 1208458796, + 1812688193 + ], + [ + 1812688194, + 2416917590 + ], + [ + 2416917591, + 3021146988 + ] + ], + "yValues": [ + 3.0, + 0.0, + 0.0, + 0.0, + 1.0 + ], + "style": { + "parentKey": null, + "values": { + "series-type": "bar" + } + }, + "xValuesDescription": { + "dataType": "DURATION", + "unit": "ns", + "axisDomain": { + "type": "range", + "start": 0, + "end": 3021146988 + }, + "label": "Execution Time" + }, + "yValuesDescription": { + "dataType": "NUMBER", + "unit": "", + "axisDomain": null, + "label": "Number of Executions" + } + } + ] + }, + "statusMessage": "Completed", + "status": "COMPLETED" +} \ No newline at end of file diff --git a/tsp-typescript-client/fixtures/tsp-client/fetch-generic-xy-1.json b/tsp-typescript-client/fixtures/tsp-client/fetch-generic-xy-1.json new file mode 100644 index 0000000..1ecf815 --- /dev/null +++ b/tsp-typescript-client/fixtures/tsp-client/fetch-generic-xy-1.json @@ -0,0 +1,54 @@ +{ + "model": { + "title": "Function Density View", + "series": [ + { + "seriesId": 3, + "seriesName": "15652", + "xValues": [ + "red", + "blue", + "green", + "yellow", + "black" + ], + "yValues": [ + 3.0, + 0.0, + 0.0, + 0.0, + 1.0 + ], + "style": { + "parentKey": null, + "values": { + "series-type": "bar" + } + }, + "xValuesDescription": { + "dataType": "STRING", + "unit": "", + "axisDomain": { + "type": "categorical", + "categories": [ + "red", + "blue", + "green", + "yellow", + "black" + ] + }, + "label": "Execution Time" + }, + "yValuesDescription": { + "dataType": "NUMBER", + "unit": "", + "axisDomain": null, + "label": "Number of Executions" + } + } + ] + }, + "statusMessage": "Completed", + "status": "COMPLETED" +} \ No newline at end of file diff --git a/tsp-typescript-client/fixtures/tsp-client/fetch-generic-xy-tree-0.json b/tsp-typescript-client/fixtures/tsp-client/fetch-generic-xy-tree-0.json new file mode 100644 index 0000000..7ef295c --- /dev/null +++ b/tsp-typescript-client/fixtures/tsp-client/fetch-generic-xy-tree-0.json @@ -0,0 +1,55 @@ +{ + "model": { + "autoExpandLevel": -1, + "entries": [ + { + "id": 0, + "parentId": -1, + "style": null, + "labels": [ + "ls_ust" + ], + "hasData": true + }, + { + "id": 1, + "parentId": 0, + "style": null, + "labels": [ + "15647" + ], + "hasData": true + }, + { + "id": 2, + "parentId": 0, + "style": null, + "labels": [ + "15646" + ], + "hasData": true + }, + { + "id": 3, + "parentId": 0, + "style": null, + "labels": [ + "15652" + ], + "hasData": true + }, + { + "id": 4, + "parentId": 0, + "style": null, + "labels": [ + "15653" + ], + "hasData": true + } + ], + "headers": [] + }, + "statusMessage": "Completed", + "status": "COMPLETED" +} \ No newline at end of file diff --git a/tsp-typescript-client/src/models/axis-domain.ts b/tsp-typescript-client/src/models/axis-domain.ts new file mode 100644 index 0000000..6461473 --- /dev/null +++ b/tsp-typescript-client/src/models/axis-domain.ts @@ -0,0 +1,50 @@ +import { createNormalizer, toBigInt } from '../protocol/serialization'; + +/** + * Represent a categorical axis domain. + * + * Example categories: ["blue", "green", "yellow"] + */ +export interface AxisDomainCategorical { + type: 'categorical'; categories: string[]; +} + +/** + * Represent a ranged axis domain. + */ +export interface AxisDomainRange { + type: 'range'; start: bigint; end: bigint; +} + +/** + * Represent an axis domain, which can be either ranged or categorical. + */ +export type AxisDomain = AxisDomainCategorical | AxisDomainRange; + +export function isAxisDomainRange(ad: AxisDomain | undefined | null): ad is AxisDomainRange { + return !!ad && ad.type === 'range'; +} + +export function isAxisDomainCategorical(ad: AxisDomain | undefined | null): ad is AxisDomainCategorical { + return !!ad && ad.type === 'categorical'; +} + +export const AxisDomainRange = createNormalizer({ + start: toBigInt, + end: toBigInt, +}); + +export const AxisDomainCategorical = (v: unknown): AxisDomainCategorical => { + const x = v as any; + return { type: 'categorical', categories: Array.isArray(x?.categories) ? x.categories as string[] : [] }; +}; + +export const AxisDomain = (v: unknown): AxisDomain => { + const x = v as any; + if (!x || typeof x !== 'object') throw new Error('AxisDomain: invalid value'); + return x.type === 'range' + ? AxisDomainRange(x) + : x.type === 'categorical' + ? AxisDomainCategorical(x) + : (x as AxisDomain); +}; diff --git a/tsp-typescript-client/src/models/output-descriptor.ts b/tsp-typescript-client/src/models/output-descriptor.ts index 18dabcf..b07f02a 100644 --- a/tsp-typescript-client/src/models/output-descriptor.ts +++ b/tsp-typescript-client/src/models/output-descriptor.ts @@ -21,6 +21,10 @@ export enum ProviderType { * A provider for a tree, whose entries have XY series. The x-series is time. */ TREE_TIME_XY = "TREE_TIME_XY", + /** + * A provider for a tree, whose entries have XY series. The x-series can be categorical. + */ + TREE_GENERIC_XY = "TREE_GENERIC_XY", /** * A provider for a Time Graph model, which has entries with a start and end * time, each entry has a series of states, arrows link from one series to diff --git a/tsp-typescript-client/src/models/query/query-helper.ts b/tsp-typescript-client/src/models/query/query-helper.ts index 78113bb..6754de1 100644 --- a/tsp-typescript-client/src/models/query/query-helper.ts +++ b/tsp-typescript-client/src/models/query/query-helper.ts @@ -126,13 +126,31 @@ export class QueryHelper { */ public static selectionTimeRangeQuery(start: bigint, end: bigint, nbTimes: number, items: number[], additionalProperties?: { [key: string]: any }): Query - public static selectionTimeRangeQuery(start: bigint, end: bigint, third: number | number[], fourth: number[] | { [key: string]: any }, fifth?: { [key: string]: any }): Query { + /** + * Build a sampled time range query with selected items + * @param start Start time + * @param end End time + * @param nbTimes Number of time samples + * @param items Array of item IDs + * @param additionalProperties Use this optional parameter to add custom properties to your query + * @param nbSamples For sampling points that are not time-based, nbSamples is used as key instead + * of nbTimes. This flag configures whether to use nbTimes as key. The key default + * to be nbTimes. + */ + public static selectionTimeRangeQuery(start: bigint, end: bigint, nbTimes: number, items: number[], additionalProperties: { [key: string]: any } | undefined, nbSamples: boolean): Query; + + public static selectionTimeRangeQuery(start: bigint, end: bigint, third: number | number[], fourth: number[] | { [key: string]: any }, fifth?: { [key: string]: any }, nbSamples: boolean = false): Query { if (typeof third === 'number') { - const nbTimes = third; + const count = third; const items = fourth; const additionalProperties = fifth; + const range = + nbSamples + ? { start, end, nbSamples: count } + : { start, end, nbTimes: count }; + const selectionTimeObj = { - [this.REQUESTED_TIMERANGE_KEY]: { start, end, nbTimes }, + [this.REQUESTED_TIMERANGE_KEY]: range, [this.REQUESTED_ITEMS_KEY]: items }; return new Query({ ...selectionTimeObj, ...additionalProperties }); diff --git a/tsp-typescript-client/src/models/sampling.ts b/tsp-typescript-client/src/models/sampling.ts new file mode 100644 index 0000000..8946ca2 --- /dev/null +++ b/tsp-typescript-client/src/models/sampling.ts @@ -0,0 +1,67 @@ +import { toBigInt } from '../protocol/serialization'; + +export type StartEndRange = [bigint, bigint]; + +export const StartEndRange = (v: unknown): StartEndRange => { + if (!Array.isArray(v) || v.length !== 2) { + throw new Error('StartEndRange: expected [start,end]'); + } + const [s, e] = v; + return [toBigInt(s as any), toBigInt(e as any)]; +}; + +/** + * Represent sampling on a list of timestamps. + */ +export type TimestampSampling = bigint[]; + +/** + * Represent sampling on a list of categories(strings). + */ +export type CategorySampling = string[]; + +/** + * Represent sampling on a list of ranges.. + */ +export type RangeSampling = StartEndRange[]; + +/** + * Represent sampling on either timestamps, ranges or categories. + */ +export type Sampling = TimestampSampling | CategorySampling | RangeSampling; + +export const isRangeSampling = (s: Sampling): s is [bigint, bigint][] => + Array.isArray(s) && Array.isArray(s[0]); + +export const isTimestampSampling = (s: Sampling): s is bigint[] => + Array.isArray(s) && typeof s[0] === 'bigint'; + +export const isCategorySampling = (s: Sampling): s is string[] => + Array.isArray(s) && typeof s[0] === 'string'; + +export const Sampling = (v: unknown): Sampling => { + if (!Array.isArray(v)) { + throw new Error('Sampling: expected bare array'); + } + + const arr = v as unknown[]; + if (arr.length === 0) { + // empty is fine; ambiguous but OK to return empty array + return []; + } + + const first = arr[0]; + + // Timestamp sampling → coerce each value to bigint + if (typeof first === 'number' || typeof first === 'bigint') { + return arr.map(toBigInt) as bigint[]; + } + + // ranges → array of [bigint, bigint] + if (Array.isArray(first) && first.length === 2) { + return (v as unknown[]).map(StartEndRange); + } + + // Category sampling → strings; leave as-is + return arr as string[]; +}; diff --git a/tsp-typescript-client/src/models/xy.ts b/tsp-typescript-client/src/models/xy.ts index c12cc0f..caf1b1d 100644 --- a/tsp-typescript-client/src/models/xy.ts +++ b/tsp-typescript-client/src/models/xy.ts @@ -1,13 +1,22 @@ -import { array, assertNumber, createNormalizer, toBigInt } from '../protocol/serialization'; +import { array, assertNumber, createNormalizer } from '../protocol/serialization'; +import { AxisDomain } from './axis-domain'; +import { DataType } from './data-type'; import { Entry } from './entry'; +import { Sampling } from './sampling'; import { OutputElementStyle } from "./styles"; +export const XYAxisDescription = createNormalizer({ + axisDomain: AxisDomain, +}); + export const XYSeries = createNormalizer({ seriesId: assertNumber, - xValues: array(toBigInt), + xValues: Sampling, yValues: array(assertNumber), tags: array(assertNumber), style: OutputElementStyle, + xValuesDescription: XYAxisDescription, + yValuesDescription: XYAxisDescription, }); export interface XyEntry extends Entry { @@ -34,17 +43,17 @@ export interface XYSeries { /** * Description of the X axis */ - xAxis: XYAxis; + xValuesDescription: XYAxisDescription; /** * Description of the Y axis */ - yAxis: XYAxis; + yValuesDescription: XYAxisDescription; /** * Series' X values */ - xValues: bigint[]; + xValues: Sampling; /** * Series' Y values @@ -84,7 +93,7 @@ export interface XYModel { /** * Description of an axis for XY chart */ -export interface XYAxis { +export interface XYAxisDescription { /** * Label of the axis */ @@ -98,5 +107,10 @@ export interface XYAxis { /** * Type of data for this axis, to give hint on number formatting */ - dataType: string; + dataType: DataType; + + /** + * Selection range for this axis + */ + axisDomain?: AxisDomain; } diff --git a/tsp-typescript-client/src/protocol/http-tsp-client.ts b/tsp-typescript-client/src/protocol/http-tsp-client.ts index 9e39d4b..e934f20 100644 --- a/tsp-typescript-client/src/protocol/http-tsp-client.ts +++ b/tsp-typescript-client/src/protocol/http-tsp-client.ts @@ -241,6 +241,54 @@ export class HttpTspClient implements ITspClient { return RestClient.post(url, parameters, GenericResponse(XYModel)); } + /** + * Fetch generic XY tree with non-time x-axis + * @param expUUID Experiment UUID + * @param outputID Output ID + * @param parameters Query object + * @returns Generic entry response with entries + */ + public async fetchGenericXYTree( + expUUID: string, + outputID: string, + parameters: Query + ): Promise>>> { + const url = + this.baseUrl + + "/experiments/" + + expUUID + + "/outputs/genericXY/" + + outputID + + "/tree"; + return RestClient.post( + url, + parameters, + GenericResponse(EntryModel(Entry)) + ); + } + + /** + * Fetch generic XY with non-time x-axis. model extends XYModel + * @param expUUID Experiment UUID + * @param outputID Output ID + * @param parameters Query object + * @returns XY model response with the model + */ + public async fetchGenericXY( + expUUID: string, + outputID: string, + parameters: Query + ): Promise>> { + const url = + this.baseUrl + + "/experiments/" + + expUUID + + "/outputs/genericXY/" + + outputID + + "/xy"; + return RestClient.post(url, parameters, GenericResponse(XYModel)); + } + /** * Fetch XY tooltip * @param expUUID Experiment UUID diff --git a/tsp-typescript-client/src/protocol/tsp-client.test.ts b/tsp-typescript-client/src/protocol/tsp-client.test.ts index 5239674..098cc67 100644 --- a/tsp-typescript-client/src/protocol/tsp-client.test.ts +++ b/tsp-typescript-client/src/protocol/tsp-client.test.ts @@ -6,6 +6,7 @@ import { HttpTspClient } from './http-tsp-client'; import { DataType } from '../models/data-type'; import { ConfigurationParameterDescriptor } from '../models/configuration-source'; import { QueryHelper } from '../models/query/query-helper'; +import { isAxisDomainCategorical, isAxisDomainRange } from '../models/axis-domain'; describe('HttpTspClient Deserialization', () => { @@ -358,6 +359,93 @@ describe('HttpTspClient Deserialization', () => { } }); + it('fetchGenericXY', async () => { + // Test response with ranged sampling + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-generic-xy-0.json')); + const response = await client.fetchGenericXY('not-relevant', 'not-relevant', new Query({})); + const genericResponse = response.getModel()!; + const xy = genericResponse.model; + + expect(xy.series).toHaveLength(4); + for (const serie of xy.series) { + expect(typeof serie.seriesId).toEqual('number'); + expect(serie.xValues).toHaveLength(5); + expect(serie.yValues).toHaveLength(5); + for (const xValue of serie.xValues) { + if (Array.isArray(xValue) && xValue.length === 2) { + const [start, end] = xValue; + expect(typeof start).toBe('bigint'); + expect(typeof end).toBe('bigint'); + } else { + fail('xValues is not RangeSampling ([start,end] tuple)'); + } + } + + for (const yValue of serie.yValues) { + expect(typeof yValue).toEqual('number'); + } + + const xValuesDescription = serie.xValuesDescription; + expect(typeof xValuesDescription.dataType).toBe('string'); + expect(typeof xValuesDescription.unit).toBe('string'); + expect(typeof xValuesDescription.label).toBe('string'); + const xAxisDomain = serie.xValuesDescription.axisDomain; + if (isAxisDomainRange(xAxisDomain)) { + expect(typeof xAxisDomain.start).toBe('bigint'); + expect(typeof xAxisDomain.end).toBe('bigint'); + } else { + fail('xAxisDomain is not AxisDomainTimeRange'); + } + + const yValuesDescription = serie.yValuesDescription; + expect(typeof yValuesDescription.dataType).toBe('string'); + expect(typeof yValuesDescription.unit).toBe('string'); + expect(typeof yValuesDescription.label).toBe('string'); + const yAxisDomain = yValuesDescription.axisDomain; + expect(yAxisDomain).toBe(null); + } + + // Test response with categorized sampling + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-generic-xy-1.json')); + const response_categorized = await client.fetchGenericXY('not-relevant', 'not-relevant', new Query({})); + const genericResponse_categorized = response_categorized.getModel()!; + const xy_categorized = genericResponse_categorized.model; + + expect(xy_categorized.series).toHaveLength(1); + for (const serie of xy_categorized.series) { + expect(typeof serie.seriesId).toEqual('number'); + expect(serie.xValues).toHaveLength(5); + expect(serie.yValues).toHaveLength(5); + for (const xValue of serie.xValues) { + expect(typeof xValue).toEqual('string'); + } + + for (const yValue of serie.yValues) { + expect(typeof yValue).toEqual('number'); + } + + const xValuesDescription = serie.xValuesDescription; + expect(typeof xValuesDescription.dataType).toBe('string'); + expect(typeof xValuesDescription.unit).toBe('string'); + expect(typeof xValuesDescription.label).toBe('string'); + const xAxisDomain = serie.xValuesDescription.axisDomain; + if (isAxisDomainCategorical(xAxisDomain)) { + for (const category of xAxisDomain.categories) { + expect(typeof category).toBe('string'); + } + } else { + fail('xAxisDomain is not AxisDomainTimeRange'); + } + + const yValuesDescription = serie.yValuesDescription; + expect(typeof yValuesDescription.dataType).toBe('string'); + expect(typeof yValuesDescription.unit).toBe('string'); + expect(typeof yValuesDescription.label).toBe('string'); + const yAxisDomain = yValuesDescription.axisDomain; + expect(yAxisDomain).toBe(null); + } + }); + it('fetchDataTree', async () => { httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-data-tree-0.json')); const response = await client.fetchDataTree('not-relevant', 'not-relevant', new Query({})); @@ -409,6 +497,20 @@ describe('HttpTspClient Deserialization', () => { } }); + it('fetchGenericXYTree', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-generic-xy-tree-0.json')); + const response = await client.fetchGenericXYTree('not-relevant', 'not-relevant', new Query({})); + const genericResponse = response.getModel()!; + const model = genericResponse.model; + + expect(model.autoExpandLevel).toEqual(-1); + expect(model.entries).toHaveLength(5); + expect(model.headers).toHaveLength(0); + for (const entry of model.entries) { + expect(typeof entry.id).toEqual('number'); + } + }); + it('openTrace', async () => { httpRequestMock.mockReturnValueOnce(fixtures.asResponse('open-trace-0.json')); const response = await client.openTrace(new Query({})); diff --git a/tsp-typescript-client/src/protocol/tsp-client.ts b/tsp-typescript-client/src/protocol/tsp-client.ts index dfc6bdc..564f669 100644 --- a/tsp-typescript-client/src/protocol/tsp-client.ts +++ b/tsp-typescript-client/src/protocol/tsp-client.ts @@ -167,6 +167,32 @@ export interface ITspClient { seriesID?: string ): Promise>>; + /** + * Fetch generic XY tree with non-time x-axis. + * @param expUUID Experiment UUID + * @param outputID Output ID + * @param parameters Query object + * @returns Generic entry response with entries + */ + fetchGenericXYTree( + expUUID: string, + outputID: string, + parameters: Query + ): Promise>>>; + + /** + * Fetch generic XY with non-time x-axis. model extends XYModel + * @param expUUID Experiment UUID + * @param outputID Output ID + * @param parameters Query object + * @returns XY model response with the model + */ + fetchGenericXY( + expUUID: string, + outputID: string, + parameters: Query + ): Promise>>; + /** * Fetch Time Graph tree, Model extends TimeGraphEntry * @param expUUID Experiment UUID