Skip to content

Commit

Permalink
[Canvas] Type demodata and pointseries; improve types (#36055)
Browse files Browse the repository at this point in the history
* [Canvas] Type `demodata` and `pointseries`; improve types

* Fix Pointseries defect

* Remove unnecessary changes
  • Loading branch information
clintandrewhall authored May 4, 2019
1 parent 02dbf2d commit fe2cd66
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { get, map } from 'lodash';
// @ts-ignore untyped Elastic library
import { getType } from '@kbn/interpreter/common';
import { Style, Ticks, DatatableColumn, AxisConfig, isAxisConfig } from '../../types';
import { Style, Ticks, AxisConfig, isAxisConfig, PointSeriesColumns } from '../../types';

type Position = 'bottom' | 'top' | 'left' | 'right';
interface Config {
Expand All @@ -22,7 +22,7 @@ interface Config {
}

interface Options {
columns?: DatatableColumn[];
columns?: PointSeriesColumns;
ticks?: Ticks;
font?: Style | {};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
*/

import { get, sortBy } from 'lodash';
import { DatatableColumn, DatatableRow, DatatableColumnType, Ticks } from '../../types';
import { PointSeriesColumns, DatatableRow, Ticks } from '../../types';

export const getTickHash = (columns: DatatableColumn[], rows: DatatableRow[]) => {
export const getTickHash = (columns: PointSeriesColumns, rows: DatatableRow[]) => {
const ticks: Ticks = {
x: {
hash: {},
Expand All @@ -19,7 +19,7 @@ export const getTickHash = (columns: DatatableColumn[], rows: DatatableRow[]) =>
},
};

if (get<DatatableColumn[], DatatableColumnType>(columns, 'x.type') === 'string') {
if (get(columns, 'x.type') === 'string') {
sortBy(rows, ['x']).forEach(row => {
if (!ticks.x.hash[row.x]) {
ticks.x.hash[row.x] = ticks.x.counter++;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import {
Style,
Palette,
Legend,
DatatableColumn,
} from '../../types';

interface Arguments {
Expand Down Expand Up @@ -110,16 +109,8 @@ export function plot(): ContextFunction<'plot', PointSeries, Arguments, Render<a
text?: string;
} = {};

const x =
get<DatatableColumn[], DatatableColumn['type']>(context.columns, 'x.type') ===
'string'
? ticks.x.hash[point.x]
: point.x;
const y =
get<DatatableColumn[], DatatableColumn['type']>(context.columns, 'y.type') ===
'string'
? ticks.y.hash[point.y]
: point.y;
const x = get(context.columns, 'x.type') === 'string' ? ticks.x.hash[point.x] : point.x;
const y = get(context.columns, 'y.type') === 'string' ? ticks.y.hash[point.y] : point.y;

if (point.size != null) {
attrs.size = point.size;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@
*/

import { sortBy } from 'lodash';
// @ts-ignore
import { queryDatatable } from '../../../../common/lib/datatable/query';
// @ts-ignore
import { getDemoRows } from './get_demo_rows';
import { ContextFunction, Filter, Datatable, DatatableColumn, DatatableRow } from '../../types';

export function demodata() {
interface Arguments {
type: string | null;
}

export function demodata(): ContextFunction<'demodata', Filter, Arguments, Datatable> {
return {
name: 'demodata',
aliases: [],
Expand All @@ -29,7 +36,8 @@ export function demodata() {
fn: (context, args) => {
const demoRows = getDemoRows(args.type);

let set = {};
let set = {} as { columns: DatatableColumn[]; rows: DatatableRow[] };

if (args.type === 'ci') {
set = {
columns: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/

// @ts-ignore untyped local
import { demodata } from './demodata';
import { escount } from './escount';
import { esdocs } from './esdocs';
// @ts-ignore untyped local
import { pointseries } from './pointseries';
import { server } from './server';
import { essql } from './essql';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,41 @@
* you may not use this file except in compliance with the Elastic License.
*/

// @ts-ignore Untyped library
import uniqBy from 'lodash.uniqby';
// @ts-ignore Untyped Elastic library
import { evaluate } from 'tinymath';
import { groupBy, zipObject, omit, values } from 'lodash';
import moment from 'moment';
// @ts-ignore Untyped local
import { pivotObjectArray } from '../../../../common/lib/pivot_object_array';
// @ts-ignore Untyped local
import { unquoteString } from '../../../../common/lib/unquote_string';
// @ts-ignore Untyped local
import { isColumnReference } from './lib/is_column_reference';
// @ts-ignore Untyped local
import { getExpressionType } from './lib/get_expression_type';
import {
ContextFunction,
Datatable,
DatatableRow,
PointSeries,
PointSeriesColumnName,
PointSeriesColumns,
} from '../../types';

// TODO: pointseries performs poorly, that's why we run it on the server.

const columnExists = (cols, colName) => cols.includes(unquoteString(colName));
const columnExists = (cols: string[], colName: string): boolean =>
cols.includes(unquoteString(colName));

export function pointseries() {
function keysOf<T, K extends keyof T>(obj: T): K[] {
return Object.keys(obj) as K[];
}

type Arguments = { [key in PointSeriesColumnName]: string | null };

export function pointseries(): ContextFunction<'pointseries', Datatable, Arguments, PointSeries> {
return {
name: 'pointseries',
type: 'pointseries',
Expand Down Expand Up @@ -56,21 +77,22 @@ export function pointseries() {
// Note: can't replace pivotObjectArray with datatableToMathContext, lose name of non-numeric columns
const columnNames = context.columns.map(col => col.name);
const mathScope = pivotObjectArray(context.rows, columnNames);
const autoQuoteColumn = col => {
if (!columnNames.includes(col)) {
const autoQuoteColumn = (col: string | null) => {
if (!col || !columnNames.includes(col)) {
return col;
}

return col.match(/\s/) ? `'${col}'` : col;
};

const measureNames = [];
const dimensions = [];
const columns = {};
const measureNames: PointSeriesColumnName[] = [];
const dimensions: Array<{ name: keyof Arguments; value: string }> = [];
const columns = {} as PointSeriesColumns;

// Separates args into dimensions and measures arrays
// by checking if arg is a column reference (dimension)
Object.keys(args).forEach(arg => {
const mathExp = autoQuoteColumn(args[arg]);
keysOf(args).forEach(argName => {
const mathExp = autoQuoteColumn(args[argName]);

if (mathExp != null && mathExp.trim() !== '') {
const col = {
Expand All @@ -86,25 +108,29 @@ export function pointseries() {
}

dimensions.push({
name: arg,
name: argName,
value: mathExp,
});
col.type = getExpressionType(context.columns, mathExp);
col.role = 'dimension';
} else {
measureNames.push(arg);
measureNames.push(argName);
col.type = 'number';
col.role = 'measure';
}

columns[arg] = col;
// @ts-ignore untyped local: get_expression_type
columns[argName] = col;
}
});

const PRIMARY_KEY = '%%CANVAS_POINTSERIES_PRIMARY_KEY%%';
const rows = context.rows.map((row, i) => ({ ...row, [PRIMARY_KEY]: i }));
const rows: DatatableRow[] = context.rows.map((row, i) => ({
...row,
[PRIMARY_KEY]: i,
}));

function normalizeValue(expression, value) {
function normalizeValue(expression: string, value: string) {
switch (getExpressionType(context.columns, expression)) {
case 'string':
return String(value);
Expand All @@ -120,9 +146,9 @@ export function pointseries() {
// Dimensions
// Group rows by their dimension values, using the argument values and preserving the PRIMARY_KEY
// There's probably a better way to do this
const results = rows.reduce((acc, row, i) => {
const results: DatatableRow = rows.reduce((rowAcc: DatatableRow, row, i) => {
const newRow = dimensions.reduce(
(acc, { name, value }) => {
(acc: Record<string, string | number>, { name, value }) => {
try {
acc[name] = args[name]
? normalizeValue(value, evaluate(value, mathScope)[i])
Expand All @@ -137,19 +163,24 @@ export function pointseries() {
{ [PRIMARY_KEY]: row[PRIMARY_KEY] }
);

return Object.assign(acc, { [row[PRIMARY_KEY]]: newRow });
return Object.assign(rowAcc, { [row[PRIMARY_KEY]]: newRow });
}, {});

// Measures
// First group up all of the distinct dimensioned bits. Each of these will be reduced to just 1 value
// for each measure
const measureKeys = groupBy(rows, row =>
dimensions.map(({ name }) => (args[name] ? row[args[name]] : '_all')).join('::%BURLAP%::')
const measureKeys = groupBy<DatatableRow>(rows, row =>
dimensions
.map(({ name }) => {
const value = args[name];
return value ? row[value] : '_all';
})
.join('::%BURLAP%::')
);

// Then compute that 1 value for each measure
values(measureKeys).forEach(rows => {
const subtable = { type: 'datatable', columns: context.columns, rows: rows };
values<DatatableRow[]>(measureKeys).forEach(valueRows => {
const subtable = { type: 'datatable', columns: context.columns, rows: valueRows };
const subScope = pivotObjectArray(subtable.rows, subtable.columns.map(col => col.name));
const measureValues = measureNames.map(measure => {
try {
Expand All @@ -165,7 +196,7 @@ export function pointseries() {
}
});

rows.forEach(row => {
valueRows.forEach(row => {
Object.assign(results[row[PRIMARY_KEY]], zipObject(measureNames, measureValues));
});
});
Expand All @@ -178,7 +209,7 @@ export function pointseries() {

return {
type: 'pointseries',
columns: columns,
columns,
rows: resultingRows,
};
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,30 @@ export interface Datatable {

export type Legend = 'nw' | 'sw' | 'ne' | 'se';

/**
* Allowed column names in a PointSeries
*/
export type PointSeriesColumnName = 'x' | 'y' | 'color' | 'size' | 'text';

/**
* Column in a PointSeries
*/
export interface PointSeriesColumn {
type: 'number' | 'string';
role: 'measure' | 'dimension';
expression: string;
}

/**
* Represents a collection of valid Columns in a PointSeries
*/
export type PointSeriesColumns = { [key in PointSeriesColumnName]: PointSeriesColumn };

/**
* A `PointSeries` in Canvas is a unique structure that represents dots on a chart.
*/
export interface PointSeries {
columns: DatatableColumn[];
columns: PointSeriesColumns;
rows: Array<Record<string, any>>;
type: 'pointseries';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { functions as serverFunctions } from '../../functions/server';
* A Function type which represents a Function in Canvas. This type assumes
* any Context can be provided and used by the Function implementation.
*/
export interface Function<Name, Arguments, Return> {
export interface Function<Name extends string, Arguments, Return> {
/** Arguments for the Function */
args: { [key in keyof Arguments]: ArgumentType<Arguments[key]> };
aliases?: string[];
Expand All @@ -30,7 +30,7 @@ export interface Function<Name, Arguments, Return> {
/**
* A Function type which restricts the incoming Context to a specific type.
*/
export interface ContextFunction<Name, Context, Arguments, Return>
export interface ContextFunction<Name extends string, Context, Arguments, Return>
extends Function<Name, Arguments, Return> {
/** The incoming Context provided to the Function; the information piped in. */
context?: {
Expand All @@ -43,7 +43,7 @@ export interface ContextFunction<Name, Context, Arguments, Return>
/**
* A Function type which restricts the incoming Context specifically to `null`.
*/
export interface NullContextFunction<Name, Arguments, Return>
export interface NullContextFunction<Name extends string, Arguments, Return>
extends ContextFunction<Name, null, Arguments, Return> {
/** The incoming Context provided to the Function; the information piped in. */
context?: {
Expand All @@ -53,27 +53,36 @@ export interface NullContextFunction<Name, Arguments, Return>
fn(context: null, args: Arguments, handlers: FunctionHandlers): Return;
}

/**
* A type which infers all of the Function names.
*/
// A reducing type for Function Factories to a base `Function`.
// This is useful for collecting all of the Functions and the concepts they share as
// one useable type.
// prettier-ignore
export type AvailableFunctions<FnFactory> =
type FunctionFactories<FnFactory> =
FnFactory extends ContextFunctionFactory<infer Name, infer Context, infer Arguments, infer Return> ?
{ name: Name, context: Context, arguments: Arguments, return: Return } :
Function<Name, Arguments, Return> :
FnFactory extends NullContextFunctionFactory<infer Name, infer Arguments, infer Return> ?
{ name: Name, arguments: Arguments, return: Return } :
Function<Name, Arguments, Return> :
FnFactory extends FunctionFactory<infer Name, infer Arguments, infer Return> ?
{ name: Name, arguments: Arguments, return: Return } :
never;
Function<Name, Arguments, Return> :
never;

/**
* A type containing all available Functions.
*/
export type AvailableFunctions = FunctionFactories<Functions>;

/**
* A type containing all of the Function names available to Canvas, formally exported.
*/
// prettier-ignore
export type AvailableFunctionNames =
AvailableFunctions<typeof commonFunctions[number]>['name'] &
AvailableFunctions<typeof browserFunctions[number]>['name'] &
AvailableFunctions<typeof serverFunctions[number]>['name'];
export type AvailableFunctionNames = AvailableFunctions['name'];

// A type containing all of the raw Function definitions in Canvas.
// prettier-ignore
type Functions =
typeof commonFunctions[number] &
typeof browserFunctions[number] &
typeof serverFunctions[number];

// A union of strings representing Canvas Function "types". This is used in the `type` field
// of the Function specification. We may refactor this to be a known type, rather than a
Expand Down

0 comments on commit fe2cd66

Please sign in to comment.