Skip to content

Commit

Permalink
docs(sources): Add JSDoc to widget sources
Browse files Browse the repository at this point in the history
  • Loading branch information
donmccurdy committed Jun 26, 2024
1 parent 3221da8 commit b7dd7be
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 82 deletions.
47 changes: 31 additions & 16 deletions src/sources/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {GroupDateType} from '..';
import {GroupDateType} from '../constants';
import {
AggregationType,
SortColumnType,
Expand All @@ -10,28 +10,48 @@ import {
* WIDGET API REQUESTS
*/

/** Common options for {@link WidgetBaseSource} requests. */
interface BaseRequestOptions {
spatialFilter?: SpatialFilter;
abortController?: AbortController;
filterOwner?: string;
}

/** Options for {@link WidgetBaseSource#getCategories}. */
export interface CategoryRequestOptions extends BaseRequestOptions {
column: string;
operation?: AggregationType;
operationColumn?: string;
}

/** Options for {@link WidgetBaseSource#getFormula}. */
export interface FormulaRequestOptions extends BaseRequestOptions {
column: string;
operation?: AggregationType;
operationExp?: string;
}

export interface CategoryRequestOptions extends BaseRequestOptions {
/** Options for {@link WidgetBaseSource#getHistogram}. */
export interface HistogramRequestOptions extends BaseRequestOptions {
column: string;
ticks: number[];
operation?: AggregationType;
operationColumn?: string;
}

/** Options for {@link WidgetBaseSource#getRange}. */
export interface RangeRequestOptions extends BaseRequestOptions {
column: string;
}

/** Options for {@link WidgetBaseSource#getScatter}. */
export interface ScatterRequestOptions extends BaseRequestOptions {
xAxisColumn: string;
xAxisJoinOperation?: AggregationType;
yAxisColumn: string;
yAxisJoinOperation?: AggregationType;
}

/** Options for {@link WidgetBaseSource#getTable}. */
export interface TableRequestOptions extends BaseRequestOptions {
columns: string[];
sortBy?: string;
Expand All @@ -41,13 +61,7 @@ export interface TableRequestOptions extends BaseRequestOptions {
rowsPerPage?: number;
}

export interface ScatterRequestOptions extends BaseRequestOptions {
xAxisColumn: string;
xAxisJoinOperation?: AggregationType;
yAxisColumn: string;
yAxisJoinOperation?: AggregationType;
}

/** Options for {@link WidgetBaseSource#getTimeSeries}. */
export interface TimeSeriesRequestOptions extends BaseRequestOptions {
column: string;
stepSize?: GroupDateType;
Expand All @@ -60,32 +74,33 @@ export interface TimeSeriesRequestOptions extends BaseRequestOptions {
splitByCategoryValues?: string[];
}

export interface HistogramRequestOptions extends BaseRequestOptions {
column: string;
ticks: number[];
operation?: AggregationType;
}

/******************************************************************************
* WIDGET API RESPONSES
*/

/** Response from {@link WidgetBaseSource#getFormula}. */
export type FormulaResponse = {value: number};

/** Response from {@link WidgetBaseSource#getCategories}. */
export type CategoryResponse = {name: string; value: number}[];

/** Response from {@link WidgetBaseSource#getRange}. */
export type RangeResponse = {min: number; max: number};

/** Response from {@link WidgetBaseSource#getTable}. */
export type TableResponse = {
totalCount: number;
rows: Record<string, number | string>[];
};

/** Response from {@link WidgetBaseSource#getScatter}. */
export type ScatterResponse = [number, number][];

/** Response from {@link WidgetBaseSource#getTimeSeries}. */
export type TimeSeriesResponse = {
rows: {name: string; value: number}[];
categories: string[];
};

/** Response from {@link WidgetBaseSource#getHistogram}. */
export type HistogramResponse = number[];
200 changes: 134 additions & 66 deletions src/sources/widget-base-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export interface WidgetBaseSourceProps extends Omit<SourceOptions, 'filters'> {

export type WidgetSource = WidgetBaseSource<WidgetBaseSourceProps>;

/**
* Source for Widget API requests on a data source defined by a SQL query.
*
* Abstract class. Use {@link WidgetQuerySource} or {@link WidgetTableSource}.
*/
export abstract class WidgetBaseSource<Props extends WidgetBaseSourceProps> {
readonly props: Props;

Expand All @@ -51,6 +56,12 @@ export abstract class WidgetBaseSource<Props extends WidgetBaseSourceProps> {
this.props = {...WidgetBaseSource.defaultProps, ...props};
}

/**
* Subclasses of {@link WidgetBaseSource} must implement this method, calling
* {@link WidgetBaseSource.prototype._getModelSource} for common source
* properties, and adding additional required properties including 'type' and
* 'data'.
*/
protected abstract getModelSource(owner: string | undefined): ModelSource;

protected _getModelSource(
Expand All @@ -69,6 +80,42 @@ export abstract class WidgetBaseSource<Props extends WidgetBaseSourceProps> {
};
}

/****************************************************************************
* CATEGORIES
*/

/**
* Returns a list of labeled datapoints for categorical data. Suitable for
* charts including grouped bar charts, pie charts, and tree charts.
*/
async getCategories(
props: CategoryRequestOptions
): Promise<CategoryResponse> {
const {filterOwner, spatialFilter, abortController, ...params} = props;
const {column, operation, operationColumn} = params;

type CategoriesModelResponse = {rows: {name: string; value: number}[]};

return executeModel({
model: 'category',
source: {...this.getModelSource(filterOwner), spatialFilter},
params: {
column,
operation,
operationColumn: operationColumn || column,
},
opts: {abortController},
}).then((res: CategoriesModelResponse) => normalizeObjectKeys(res.rows));
}

/****************************************************************************
* FORMULA
*/

/**
* Returns a scalar numerical statistic over all matching data. Suitable
* for 'headline' or 'scorecard' figures such as counts and sums.
*/
async getFormula(props: FormulaRequestOptions): Promise<FormulaResponse> {
const {
filterOwner,
Expand All @@ -89,26 +136,51 @@ export abstract class WidgetBaseSource<Props extends WidgetBaseSourceProps> {
}).then((res: FormulaModelResponse) => normalizeObjectKeys(res.rows[0]));
}

async getCategories(
props: CategoryRequestOptions
): Promise<CategoryResponse> {
/****************************************************************************
* HISTOGRAM
*/

/**
* Returns a list of labeled datapoints for 'bins' of data defined as ticks
* over a numerical range. Suitable for histogram charts.
*/
async getHistogram(
props: HistogramRequestOptions
): Promise<HistogramResponse> {
const {filterOwner, spatialFilter, abortController, ...params} = props;
const {column, operation, operationColumn} = params;
const {column, operation, ticks} = params;

type CategoriesModelResponse = {rows: {name: string; value: number}[]};
type HistogramModelResponse = {rows: {tick: number; value: number}[]};

return executeModel({
model: 'category',
const data = await executeModel({
model: 'histogram',
source: {...this.getModelSource(filterOwner), spatialFilter},
params: {
column,
operation,
operationColumn: operationColumn || column,
},
params: {column, operation, ticks},
opts: {abortController},
}).then((res: CategoriesModelResponse) => normalizeObjectKeys(res.rows));
}).then((res: HistogramModelResponse) => normalizeObjectKeys(res.rows));

if (data.length) {
// Given N ticks the API returns up to N+1 bins, omitting any empty bins. Bins
// include 1 bin below the lowest tick, N-1 between ticks, and 1 bin above the highest tick.
const result = Array(ticks.length + 1).fill(0);
data.forEach(
({tick, value}: {tick: number; value: number}) => (result[tick] = value)
);
return result;
}

return [];
}

/****************************************************************************
* RANGE
*/

/**
* Returns a range (min and max) for a numerical column of matching rows.
* Suitable for displaying certain 'headline' or 'scorecard' statistics,
* or rendering a range slider UI for filtering.
*/
async getRange(props: RangeRequestOptions): Promise<RangeResponse> {
const {filterOwner, spatialFilter, abortController, ...params} = props;
const {column} = params;
Expand All @@ -123,32 +195,14 @@ export abstract class WidgetBaseSource<Props extends WidgetBaseSourceProps> {
}).then((res: RangeModelResponse) => normalizeObjectKeys(res.rows[0]));
}

async getTable(props: TableRequestOptions): Promise<TableResponse> {
const {filterOwner, spatialFilter, abortController, ...params} = props;
const {columns, sortBy, sortDirection, page = 0, rowsPerPage = 10} = params;

type TableModelResponse = {
rows: Record<string, number | string>[];
metadata: {total: number};
};

return executeModel({
model: 'table',
source: {...this.getModelSource(filterOwner), spatialFilter},
params: {
column: columns,
sortBy,
sortDirection,
limit: rowsPerPage,
offset: page * rowsPerPage,
},
opts: {abortController},
}).then((res: TableModelResponse) => ({
rows: normalizeObjectKeys(res.rows),
totalCount: res.metadata.total,
}));
}
/****************************************************************************
* SCATTER
*/

/**
* Returns a list of bivariate datapoints defined as numerical 'x' and 'y'
* values. Suitable for rendering scatter plots.
*/
async getScatter(props: ScatterRequestOptions): Promise<ScatterResponse> {
const {filterOwner, spatialFilter, abortController, ...params} = props;
const {xAxisColumn, xAxisJoinOperation, yAxisColumn, yAxisJoinOperation} =
Expand All @@ -175,6 +229,48 @@ export abstract class WidgetBaseSource<Props extends WidgetBaseSourceProps> {
.then((res) => res.map(({x, y}: {x: number; y: number}) => [x, y]));
}

/****************************************************************************
* TABLE
*/

/**
* Returns a list of arbitrary data rows, with support for pagination and
* sorting. Suitable for displaying tables and lists.
*/
async getTable(props: TableRequestOptions): Promise<TableResponse> {
const {filterOwner, spatialFilter, abortController, ...params} = props;
const {columns, sortBy, sortDirection, page = 0, rowsPerPage = 10} = params;

type TableModelResponse = {
rows: Record<string, number | string>[];
metadata: {total: number};
};

return executeModel({
model: 'table',
source: {...this.getModelSource(filterOwner), spatialFilter},
params: {
column: columns,
sortBy,
sortDirection,
limit: rowsPerPage,
offset: page * rowsPerPage,
},
opts: {abortController},
}).then((res: TableModelResponse) => ({
rows: normalizeObjectKeys(res.rows),
totalCount: res.metadata.total,
}));
}

/****************************************************************************
* TIME SERIES
*/

/**
* Returns a series of labeled numerical values, grouped into equally-sized
* time intervals. Suitable for rendering time series charts.
*/
async getTimeSeries(
props: TimeSeriesRequestOptions
): Promise<TimeSeriesResponse> {
Expand Down Expand Up @@ -216,32 +312,4 @@ export abstract class WidgetBaseSource<Props extends WidgetBaseSourceProps> {
categories: res.metadata?.categories,
}));
}

async getHistogram(
props: HistogramRequestOptions
): Promise<HistogramResponse> {
const {filterOwner, spatialFilter, abortController, ...params} = props;
const {column, operation, ticks} = params;

type HistogramModelResponse = {rows: {tick: number; value: number}[]};

const data = await executeModel({
model: 'histogram',
source: {...this.getModelSource(filterOwner), spatialFilter},
params: {column, operation, ticks},
opts: {abortController},
}).then((res: HistogramModelResponse) => normalizeObjectKeys(res.rows));

if (data.length) {
// Given N ticks the API returns up to N+1 bins, omitting any empty bins. Bins
// include 1 bin below the lowest tick, N-1 between ticks, and 1 bin above the highest tick.
const result = Array(ticks.length + 1).fill(0);
data.forEach(
({tick, value}: {tick: number; value: number}) => (result[tick] = value)
);
return result;
}

return [];
}
}
22 changes: 22 additions & 0 deletions src/sources/widget-query-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,28 @@ type LayerQuerySourceOptions =
| Omit<H3QuerySourceOptions, 'filters'>
| Omit<QuadbinQuerySourceOptions, 'filters'>;

/**
* Source for Widget API requests on a data source defined by a SQL query.
*
* Generally not intended to be constructed directly. Instead, call
* {@link vectorQuerySource}, {@link h3QuerySource}, or {@link quadbinQuerySource},
* which can be shared with map layers. Sources contain a `widgetSource` property,
* for use by widget implementations.
*
* Example:
*
* ```javascript
* import { vectorQuerySource } from '@carto/api-client';
*
* const data = vectorQuerySource({
* accessToken: '••••',
* connectionName: 'carto_dw',
* sqlQuery: 'SELECT * FROM carto-demo-data.demo_tables.retail_stores'
* });
*
* const { widgetSource } = await data;
* ```
*/
export class WidgetQuerySource extends WidgetBaseSource<
LayerQuerySourceOptions & WidgetBaseSourceProps
> {
Expand Down
Loading

0 comments on commit b7dd7be

Please sign in to comment.