Added highcharts visualizer and data transformations

This commit is contained in:
Violet Voronetzky 2019-12-01 10:19:14 +02:00
Родитель a62da9ff53
Коммит aea895b427
12 изменённых файлов: 5631 добавлений и 44 удалений

Просмотреть файл

@ -8,11 +8,18 @@ Draw charts from Azure Data Explorer queries
## Installation
npm install adx-query-charts
## Dependencies
Make sure to install the following packages before using the adx-query-charts library:
1. [moment](https://www.npmjs.com/package/moment): `npm i moment`
2. [lodash](https://www.npmjs.com/package/lodash): `npm i lodash`
3. [highcharts](https://www.npmjs.com/package/highcharts): `npm i highcharts`
## Usage
```typescript
import * as Charts from 'adx-query-charts';
const chartHelper = new Charts.KustoChartHelper();
const highchartsVisualizer = new Charts.HighchartsVisualizer();
const chartHelper = chartHelper = new Charts.KustoChartHelper('chart-elem-id', highchartsVisualizer);
const chartOptions: Charts.IChartOptions = {
chartType: Charts.ChartType.Column,
columnsSelection: {
@ -20,8 +27,20 @@ const chartOptions: Charts.IChartOptions = {
yAxes: [{ name: 'requestCount', type: Charts.DraftColumnType.Int }]
}
};
const transformed: Charts.ITransformedQueryResultData = chartHelper.transformQueryResultData(queryResult.data, chartOptions);
// Draw the chart - the chart will be drawn inside an element with 'chart-elem-id' id
chartHelper.draw(queryResultData, chartOptions);
```
## API
### IChartOptions
| Option name: | Type: | Details: | Default value: |
| ------------------- |--------------------| --------------------------------------------- | ----------------|
| chartType | ChartType | Mandatory. <br>The type of the chart to draw | |
| columnsSelection | IAxesInfo<IColumn> | The columns selection for the Axes and the split-by of the chart | If not provided, default columns will be selected. <br>See: getDefaultSelection method|
| maxUniqueXValues | number | The maximum number of the unique X-axis values.<br>The chart will show the biggest values, and the rest will be aggregated to a separate data point.| 100 |
| exceedMaxDataPointLabel| string | The label of the data point that contains the aggregated value of all the X-axis values that exceed the 'maxUniqueXValues'.| 'OTHER' |
| aggregationType| AggregationType | Multiple rows with the same values for the X-axis and the split-by will be aggregated using a function of this type.<br>For example, assume we get the following query result data:<br>['2016-08-02T10:00:00Z', 'Chrome 51.0', 15], ['2016-08-02T10:00:00Z', 'Internet Explorer 9.0', 4]<br>When drawing a chart with columnsSelection = { xAxis: timestamp, yAxes: count_ }, and aggregationType = AggregationType.Sum we need to aggregate the values of the same timestamp value and return one row with ["2016-08-02T10:00:00Z", 19] |AggregationType.Sum|
| utcOffset | number | The desired offset from UTC for date values. Used to handle timezone.<br>The offset will be added to the original date from the query results data.|0 |
## Test
Unit tests are written using [Jest](https://jestjs.io/).

Просмотреть файл

@ -1,4 +1,5 @@
'use strict';
export * from './src/common/chartModels';
export { KustoChartHelper, ITransformedQueryResultData } from './src/common/kustoChartHelper';
export { KustoChartHelper, ITransformedQueryResultData } from './src/common/kustoChartHelper';
export { HighchartsVisualizer } from './src/visualizers/highcharts/highchartsVisualizer';

4861
package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -34,7 +34,8 @@
"jest": "^24.9.0",
"@types/lodash": "^4.14.135",
"lodash": "^4.17.15",
"moment": "^2.24.0"
"moment": "^2.24.0",
"@types/highcharts": "7.0.0"
},
"jest": {
"testMatch": [

Просмотреть файл

@ -113,6 +113,13 @@ export interface IChartOptions {
* [Default value: AggregationType.Sum]
*/
aggregationType?: AggregationType;
/**
* The desired offset from UTC for date values. Used to handle timezone.
* The offset will be added to the original date from the query results data.
* [Default value: 0]
*/
utcOffset?: number;
}
export interface IChartHelper {
@ -121,7 +128,7 @@ export interface IChartHelper {
* @param queryResultData - The original query result data
* @param options - The information required to draw the chart
*/
draw(queryResultData: IQueryResultData, options: IChartOptions): void;
draw(queryResultData: IQueryResultData, chartOptions: IChartOptions): void;
/**
* Return the supported column types for the axes and the split-by for a specific chart type

Просмотреть файл

@ -5,6 +5,8 @@
import { IChartHelper, IQueryResultData, ChartType, DraftColumnType, ISupportedColumnTypes, IColumn, ISupportedColumns, IAxesInfo, IChartOptions, AggregationType } from './chartModels';
import { SeriesVisualize } from '../transformers/seriesVisualize';
import { LimitVisResultsSingleton, LimitedResults, ILimitAndAggregateParams } from '../transformers/limitVisResults';
import { IVisualizer } from '../visualizers/IVisualizer';
import { Utilities } from './utilities';
//#endregion Imports
@ -31,17 +33,45 @@ export class KustoChartHelper implements IChartHelper {
columnsSelection: undefined,
maxUniqueXValues: 100,
exceedMaxDataPointLabel: 'OTHER',
aggregationType: AggregationType.Sum
aggregationType: AggregationType.Sum,
utcOffset: 0
}
private queryResultData: IQueryResultData;
private readonly seriesVisualize: SeriesVisualize;
private readonly elementId: string;
private readonly visualizer: IVisualizer;
//#endregion Private members
//#region Constructor
public constructor(elementId: string, visualizer: IVisualizer) {
this.elementId = elementId;
this.visualizer = visualizer;
this.seriesVisualize = SeriesVisualize.getInstance();
}
//#endregion Constructor
//#region Public methods
public draw(queryResultData: IQueryResultData, options: IChartOptions): void {
// TODO: Not implemented yet
public draw(queryResultData: IQueryResultData, chartOptions: IChartOptions): void {
// Update the chart options with defaults for optional values that weren't provided
chartOptions = this.updateDefaultChartOptions(queryResultData, chartOptions);
// Apply query data transformation
const resolvedAsSeriesData: IQueryResultData = this.tryResolveResultsAsSeries(queryResultData);
const transformed = this.transformQueryResultData(resolvedAsSeriesData, chartOptions);
this.transformedQueryResultData = transformed.data;
const visualizerOptions = {
elementId: this.elementId,
queryResultData: this.transformedQueryResultData,
chartOptions: chartOptions
};
this.visualizer.drawNewChart(visualizerOptions);
}
public getSupportedColumnTypes(chartType: ChartType): ISupportedColumnTypes {
@ -153,22 +183,9 @@ export class KustoChartHelper implements IChartHelper {
}
private tryResolveResultsAsSeries(queryResultData: IQueryResultData): IQueryResultData {
// Transform the query results only once
if (this.queryResultData !== queryResultData) {
this.queryResultData = queryResultData;
this.transformedQueryResultData = queryResultData;
const resolvedAsSeriesData: IQueryResultData = this.seriesVisualize.tryResolveResultsAsSeries(queryResultData);
// Tries to resolve the results as series
const seriesVisualize = SeriesVisualize.getInstance();
const updatedQueryResultData: IQueryResultData = seriesVisualize.tryResolveResultsAsSeries(queryResultData);
if (updatedQueryResultData) {
this.isResolveAsSeries = true;
this.transformedQueryResultData = updatedQueryResultData;
}
}
return this.transformedQueryResultData;
return resolvedAsSeriesData || queryResultData;
}
private getSupportedColumns(queryResultData: IQueryResultData, supportedTypes: DraftColumnType[]): IColumn[] {
@ -244,25 +261,6 @@ export class KustoChartHelper implements IChartHelper {
return null;
}
// Returns the index of the column with the same name and type in the columns array
private getColumnIndex(queryResultData: IQueryResultData, columnToFind: IColumn): number {
const columns: IColumn[] = queryResultData && queryResultData.columns;
if (!columns) {
return -1;
}
for (let i = 0; i < columns.length; i++) {
const currentColumn: IColumn = columns[i];
if (currentColumn.name == columnToFind.name && currentColumn.type == columnToFind.type) {
return i;
}
}
return -1;
}
/**
* Search for certain columns in the 'queryResultData'. If the column exist:
* 1. Add the column name and type to the 'chartColumns' array
@ -277,7 +275,7 @@ export class KustoChartHelper implements IChartHelper {
private addColumnsIfExistInResult(columnsToAdd: IColumn[], queryResultData: IQueryResultData, indexes: number[], chartColumns: IColumn[]): boolean {
for (let i = 0; i < columnsToAdd.length; ++i) {
const column = columnsToAdd[i];
const indexOfColumn = this.getColumnIndex(queryResultData, column);
const indexOfColumn = Utilities.getColumnIndex(queryResultData, column);
if (indexOfColumn < 0) {
return false;

72
src/common/utilities.ts Normal file
Просмотреть файл

@ -0,0 +1,72 @@
'use strict';
import * as moment from 'moment';
import { IQueryResultData, IColumn, DraftColumnType } from './chartModels';
export class Utilities {
// Returns the index of the column with the same name and type in the columns array
public static getColumnIndex(queryResultData: IQueryResultData, columnToFind: IColumn): number {
const columns: IColumn[] = queryResultData && queryResultData.columns;
if (!columns) {
return -1;
}
for (let i = 0; i < columns.length; i++) {
const currentColumn: IColumn = columns[i];
if (Utilities.areColumnsEqual(currentColumn, columnToFind)) {
return i;
}
}
return -1;
}
/**
* Adds the desired offset (from UTC) to the date, and return a valid Date object
* @param dateVal - The value that represents the date to transform.
* @param utcOffset - The offset from UTC.
* @returns A valid Date object.
*/
public static getValideDate(dateVal: any, utcOffset: number): Date {
const date = new Date(dateVal);
if (date.toDateString() === 'Invalid Date') {
return null;
}
const utcVal = date.toUTCString();
const utcMoment = moment.utc(utcVal, 'ddd, DD MMM YYYY HH:mm:ss Z');
// Since moment.utc doesn't update milliseconds -> fall-back to Date.getMilliseconds
utcMoment.milliseconds = () => {
return date.getMilliseconds() || (dateVal.getMilliseconds && dateVal.getMilliseconds()) || 0;
};
if (!utcMoment.isValid()) {
return null;
}
const dateWithOffset = utcMoment.utcOffset(utcOffset);
const isoDateStr = dateWithOffset.format('YYYY-MM-DDTHH:mm:ss.sss');
return new Date(isoDateStr);
}
public static isNumeric(columnType: DraftColumnType): boolean {
return columnType === DraftColumnType.Int ||
columnType === DraftColumnType.Long ||
columnType === DraftColumnType.Real ||
columnType === DraftColumnType.Decimal;
}
public static isDate(columnType: DraftColumnType): boolean {
return columnType === DraftColumnType.DateTime ||
columnType === DraftColumnType.TimeSpan;
}
public static areColumnsEqual(first: IColumn, second: IColumn): boolean {
return first.name == second.name && first.type == second.type;
}
}

Просмотреть файл

@ -0,0 +1,15 @@
'use strict';
//#region Imports
import { IVisualizerOptions } from './IVisualizerOptions';
//#endregion Imports
export interface IVisualizer {
/**
* Draw the chart on an existing DOM element
* @param options - The information required to the visualizer to draw the chart
*/
drawNewChart(options: IVisualizerOptions): void;
}

Просмотреть файл

@ -0,0 +1,9 @@
'use strict';
import { IQueryResultData, IChartOptions } from "../common/chartModels";
export interface IVisualizerOptions {
elementId: string;
queryResultData: IQueryResultData;
chartOptions: IChartOptions
}

Просмотреть файл

@ -0,0 +1,167 @@
'use strict';
//#region Imports
import * as _ from 'lodash';
import { Utilities } from '../../common/utilities';
import { IVisualizerOptions } from '../IVisualizerOptions';
//#endregion Imports
export interface ICategoriesAndSeries {
categories?: string[];
series: any[];
}
export class DataTransformer {
//#region Public static methods
public static getCategoriesAndSeries(options: IVisualizerOptions, isDatetimeAxis: boolean): ICategoriesAndSeries {
const columnsSelection = options.chartOptions.columnsSelection;
const xAxisColumn = columnsSelection.xAxis;
const xAxisColumnIndex = Utilities.getColumnIndex(options.queryResultData, xAxisColumn);
let categoriesAndSeries = {
series: [],
categories: isDatetimeAxis ? undefined : []
};
if(columnsSelection.splitBy && columnsSelection.splitBy.length > 0) {
DataTransformer.getSplitByCategoriesAndSeries(options, xAxisColumnIndex, isDatetimeAxis, categoriesAndSeries);
} else {
DataTransformer.getStandardCategoriesAndSeries(options, xAxisColumnIndex, isDatetimeAxis, categoriesAndSeries);
}
return categoriesAndSeries;
}
//#endregion Public static methods
private static getStandardCategoriesAndSeries(options: IVisualizerOptions, xAxisColumnIndex: number, isDatetimeAxis: boolean, categoriesAndSeries: ICategoriesAndSeries): void {
const chartOptions = options.chartOptions;
const yAxesIndexes = _.map(chartOptions.columnsSelection.yAxes, (yAxisColumn) => {
return Utilities.getColumnIndex(options.queryResultData, yAxisColumn);
});
const seriesMap = {};
options.queryResultData.rows.forEach((row) => {
let xAxisValue: any = row[xAxisColumnIndex];
// If the a-axis is a date, convert it's value to MS as this is what expected by 'Highcharts'
if(isDatetimeAxis) {
const dateValue = Utilities.getValideDate(xAxisValue, chartOptions.utcOffset);
xAxisValue = dateValue.valueOf();
} else {
categoriesAndSeries.categories.push(xAxisValue);
}
_.forEach(yAxesIndexes, (yAxisIndex, i) => {
const yAxisColumnName = chartOptions.columnsSelection.yAxes[i].name;
const yAxisValue = row[yAxisIndex];
if(!seriesMap[yAxisColumnName]) {
seriesMap[yAxisColumnName] = [];
}
const data = isDatetimeAxis? [xAxisValue, yAxisValue] : yAxisValue;
seriesMap[yAxisColumnName].push(data);
});
});
for (let yAxisColumnName in seriesMap) {
categoriesAndSeries.series.push({
name: yAxisColumnName,
data: seriesMap[yAxisColumnName]
});
}
}
public static getSplitByCategoriesAndSeries(options: IVisualizerOptions, xAxisColumnIndex: number, isDatetimeAxis: boolean, categoriesAndSeries: ICategoriesAndSeries): void {
if(isDatetimeAxis) {
DataTransformer.getSplitByCategoriesAndSeriesForDateXAxis(options, xAxisColumnIndex, categoriesAndSeries);
return;
}
const columnsSelection = options.chartOptions.columnsSelection;
const yAxisColumn = columnsSelection.yAxes[0];
const splitByColumn = columnsSelection.splitBy[0];
const yAxisColumnIndex = Utilities.getColumnIndex(options.queryResultData, yAxisColumn);
const splitByColumnIndex = Utilities.getColumnIndex(options.queryResultData, splitByColumn);
const uniqueXValues = {};
const uniqueSplitByValues = {};
options.queryResultData.rows.forEach((row) => {
const xValue = row[xAxisColumnIndex];
const yValue = row[yAxisColumnIndex];
const splitByValue = row[splitByColumnIndex];
if(!uniqueXValues[xValue]) {
uniqueXValues[xValue] = true;
}
if(!uniqueSplitByValues[splitByValue]) {
uniqueSplitByValues[splitByValue] = {};
}
uniqueSplitByValues[splitByValue][xValue] = yValue;
});
// Populate X-Axis
categoriesAndSeries.categories = _.keys(uniqueXValues);
// Populate Split by
for (let splitByValue in uniqueSplitByValues) {
const currentSeries = {
name: splitByValue,
data: []
};
const xValueToYValueMap = uniqueSplitByValues[splitByValue];
// Set a split-by value for each unique x value
categoriesAndSeries.categories.forEach((xValue) => {
const yValue = xValueToYValueMap[xValue] || null;
currentSeries.data.push(yValue);
});
categoriesAndSeries.series.push(currentSeries);
}
}
private static getSplitByCategoriesAndSeriesForDateXAxis(options: IVisualizerOptions, xAxisColumnIndex: number, categoriesAndSeries: ICategoriesAndSeries): void {
const columnsSelection = options.chartOptions.columnsSelection;
const yAxisColumn = columnsSelection.yAxes[0];
const splitByColumn = columnsSelection.splitBy[0];
const yAxisColumnIndex = Utilities.getColumnIndex(options.queryResultData, yAxisColumn);
const splitByColumnIndex = Utilities.getColumnIndex(options.queryResultData, splitByColumn);
const splitByMap = {};
options.queryResultData.rows.forEach((row) => {
const splitByValue: string = <string>row[splitByColumnIndex];
const yValue = row[yAxisColumnIndex];
let xValue = row[xAxisColumnIndex];
// For date the a-axis, convert it's value to ms as this is what expected by Highcharts
const dateValue = Utilities.getValideDate(xValue, options.chartOptions.utcOffset);
xValue = dateValue.valueOf();
if(!splitByMap[splitByValue]) {
splitByMap[splitByValue] = [];
}
splitByMap[splitByValue].push([xValue, yValue]);
});
for (let splitByValue in splitByMap) {
categoriesAndSeries.series.push({
name: splitByValue,
data: splitByMap[splitByValue]
});
}
}
}

Просмотреть файл

@ -0,0 +1,61 @@
'use strict';
//#region Imports
import * as _ from 'lodash';
import * as Highcharts from 'highcharts';
import { IChartOptions } from '../../common/chartModels';
import { Utilities } from '../../common/utilities';
import { IVisualizer } from '../IVisualizer';
import { IVisualizerOptions } from '../IVisualizerOptions';
import { DataTransformer } from './dataTransformer';
//#endregion Imports
export class HighchartsVisualizer implements IVisualizer {
public drawNewChart(options: IVisualizerOptions): void {
const chartOptions = options.chartOptions;
const isDatetimeAxis = Utilities.isDate(chartOptions.columnsSelection.xAxis.type);
const categoriesAndSeries = DataTransformer.getCategoriesAndSeries(options, isDatetimeAxis);
const highchartsOptions: Highcharts.Options = {
chart: {
type: 'column'
},
xAxis: {
type: isDatetimeAxis ? 'datetime' : undefined,
categories: categoriesAndSeries.categories,
title: {
text: this.getXAxisTitle(options.chartOptions),
align: 'middle'
}
},
yAxis: this.getYAxis(chartOptions),
series: categoriesAndSeries.series
};
// Draw the chart
Highcharts.chart(options.elementId, highchartsOptions);
}
//#region Private methods
private getYAxis(chartOptions: IChartOptions): Highcharts.YAxisOptions {
const yAxis = chartOptions.columnsSelection.yAxes[0];
const yAxisOptions = {
title: {
text: yAxis.name
}
}
return yAxisOptions;
}
private getXAxisTitle(chartOptions: IChartOptions): string {
const xAxisColumn = chartOptions.columnsSelection.xAxis;
return xAxisColumn.name;
}
//#endregion Private methods
}

Просмотреть файл

@ -0,0 +1,376 @@
'use strict';
import * as _ from 'lodash';
import { DraftColumnType, IColumn } from '../../src/common/chartModels';
import { DataTransformer, ICategoriesAndSeries } from '../../src/visualizers/highcharts/dataTransformer';
describe('Unit tests for Highcharts CategoriesAndSeries', () => {
//#region beforeEach
beforeEach(() => {
// Add mock to date.valueOf -> return the full year
jest
.spyOn(Date.prototype, 'valueOf')
.mockImplementation(function() {
const date = this;
return date.getFullYear();
});
})
//#endregion beforeEach
//#region Tests
describe('Validate getCategoriesAndSeries method', () => {
//#region getStandardCategoriesAndSeries
it("Validate getStandardCategoriesAndSeries: non-date x-axis and 1 y-axis", () => {
const rows = [
['Israel', 'Herzliya', 30],
['United States', 'New York', 100],
['Japan', 'Tokyo', 20],
];
const columns: IColumn[] = [
{ name: 'country', type: DraftColumnType.String },
{ name: 'city', type: DraftColumnType.String },
{ name: 'request_count', type: DraftColumnType.Int },
];
// Input
const options: any = {
chartOptions: {
columnsSelection: {
xAxis: columns[0], // country
yAxes: [columns[2]] // request_count
},
utcOffset: 0
},
queryResultData: {
rows: rows,
columns: columns
}
}
// Act
const result = DataTransformer.getCategoriesAndSeries(options, /*isDatetimeAxis*/ false);
const expectedCategoriesAndSeries: ICategoriesAndSeries = {
series: [{
name: 'request_count',
data: [30, 100, 20]
}],
categories: ['Israel', 'United States', 'Japan']
};
// Assert
expect(result).toEqual(expectedCategoriesAndSeries);
});
it("Validate getStandardCategoriesAndSeries: date x-axis and 1 y-axis", () => {
const rows = [
['Israel', '2019-05-25T00:00:00Z', 'Herzliya', 30],
['Japan', '2019-05-25T00:00:00Z', 'Tokyo', 20],
['United States', '2000-06-26T00:00:00Z', 'New York', 100],
];
const columns: IColumn[] = [
{ name: 'country', type: DraftColumnType.String },
{ name: 'timestamp', type: DraftColumnType.DateTime },
{ name: 'city', type: DraftColumnType.String },
{ name: 'request_count', type: DraftColumnType.Int },
];
// Input
const options: any = {
chartOptions: {
columnsSelection: {
xAxis: columns[1], // timestamp
yAxes: [columns[3]] // request_count
},
utcOffset: 0
},
queryResultData: {
rows: rows,
columns: columns
}
}
// Act
const result = DataTransformer.getCategoriesAndSeries(options, /*isDatetimeAxis*/ true);
const expectedCategoriesAndSeries: ICategoriesAndSeries = {
series: [{
name: 'request_count',
data: [[2019, 30], [2019, 20], [2000, 100]]
}],
categories: undefined
};
// Assert
expect(result).toEqual(expectedCategoriesAndSeries);
});
it("Validate getStandardCategoriesAndSeries: non-date x-axis and multiple y-axis", () => {
const rows = [
['Israel', 'Herzliya', 30, 300],
['United States', 'New York', 100, 150],
['Japan', 'Tokyo', 20, 200],
];
const columns: IColumn[] = [
{ name: 'country', type: DraftColumnType.String },
{ name: 'city', type: DraftColumnType.String },
{ name: 'request_count', type: DraftColumnType.Int },
{ name: 'second_count', type: DraftColumnType.Int },
];
// Input
const options: any = {
chartOptions: {
columnsSelection: {
xAxis: columns[1], // city
yAxes: [columns[2], columns[3]] // request_count and second_count
},
utcOffset: 0
},
queryResultData: {
rows: rows,
columns: columns
}
}
// Act
const result = DataTransformer.getCategoriesAndSeries(options, /*isDatetimeAxis*/ false);
const expectedCategoriesAndSeries: ICategoriesAndSeries = {
series: [{
name: 'request_count',
data: [30, 100, 20]
},
{
name: 'second_count',
data: [300, 150, 200]
}],
categories: ['Herzliya', 'New York', 'Tokyo']
};
// Assert
expect(result).toEqual(expectedCategoriesAndSeries);
});
it("Validate getStandardCategoriesAndSeries: date x-axis and multiple y-axis", () => {
const rows = [
['2019-05-25T00:00:00Z', 'Israel', 'Herzliya', 30, 300],
['2019-05-25T00:00:00Z', 'Japan', 'Tokyo', 20, 150],
['2000-06-26T00:00:00Z', 'United States', 'New York', 100, 200],
];
const columns: IColumn[] = [
{ name: 'timestamp', type: DraftColumnType.DateTime },
{ name: 'country', type: DraftColumnType.String },
{ name: 'city', type: DraftColumnType.String },
{ name: 'request_count', type: DraftColumnType.Int },
{ name: 'second_count', type: DraftColumnType.Long },
];
// Input
const options: any = {
chartOptions: {
columnsSelection: {
xAxis: columns[0], // timestamp
yAxes: [columns[3], columns[4]] // request_count and second_count
},
utcOffset: 0
},
queryResultData: {
rows: rows,
columns: columns
}
}
// Act
const result = DataTransformer.getCategoriesAndSeries(options, /*isDatetimeAxis*/ true);
const expectedCategoriesAndSeries: ICategoriesAndSeries = {
series: [{
name: 'request_count',
data: [[2019, 30], [2019, 20], [2000, 100]]
},
{
name: 'second_count',
data: [[2019, 300], [2019, 150], [2000, 200]]
}],
categories: undefined
};
// Assert
expect(result).toEqual(expectedCategoriesAndSeries);
});
//#endregion getStandardCategoriesAndSeries
//#region getSplitByCategoriesAndSeries
it("Validate getCategoriesAndSeries: non-date x-axis with splitBy", () => {
const rows = [
['United States', 'Atlanta', 300],
['United States', 'Redmond', 20],
['Israel', 'Herzliya', 1000],
['Israel', 'Tel Aviv', 10],
['United States', 'New York', 100],
['Japan', 'Tokyo', 20],
['Israel', 'Jerusalem', 5],
['United States', 'Boston', 200],
];
const columns: IColumn[] = [
{ name: 'country', type: DraftColumnType.String },
{ name: 'city', type: DraftColumnType.String },
{ name: 'request_count', type: DraftColumnType.Int },
];
// Input
const options: any = {
chartOptions: {
columnsSelection: {
xAxis: columns[0], // country
yAxes: [columns[2]], // request_count
splitBy: [columns[1]] // city
},
utcOffset: 0
},
queryResultData: {
rows: rows,
columns: columns
}
}
// Act
const result = DataTransformer.getCategoriesAndSeries(options, /*isDatetimeAxis*/ false);
const expectedCategoriesAndSeries: ICategoriesAndSeries = {
series: [{
name: 'Atlanta',
data: [300, null, null]
},
{
name: 'Redmond',
data: [20, null, null]
},
{
name: 'Herzliya',
data: [null, 1000, null]
},
{
name: 'Tel Aviv',
data: [null, 10, null]
},
{
name: 'New York',
data: [100, null, null]
},
{
name: 'Tokyo',
data: [null, null, 20]
},
{
name: 'Jerusalem',
data: [null, 5, null]
},
{
name: 'Boston',
data: [200, null, null]
}],
categories: ['United States', 'Israel', 'Japan']
};
// Assert
expect(result).toEqual(expectedCategoriesAndSeries);
});
it("Validate getCategoriesAndSeries: date x-axis with splitBy", () => {
const rows = [
['Israel', '1988-06-26T00:00:00Z', 'Jerusalem', 500],
['Israel', '2000-06-26T00:00:00Z', 'Herzliya', 1000],
['United States', '2000-06-26T00:00:00Z', 'Boston', 200],
['Israel', '2000-06-26T00:00:00Z', 'Tel Aviv', 10],
['United States', '2000-06-26T00:00:00Z', 'New York', 100],
['Japan', '2019-05-25T00:00:00Z', 'Tokyo', 20],
['United States', '2019-05-25T00:00:00Z', 'Atlanta', 300],
['United States', '2019-05-25T00:00:00Z', 'Redmond', 20]
];
const columns: IColumn[] = [
{ name: 'country', type: DraftColumnType.String },
{ name: 'timestamp', type: DraftColumnType.DateTime },
{ name: 'city', type: DraftColumnType.String },
{ name: 'request_count', type: DraftColumnType.Int },
];
// Input
const options: any = {
chartOptions: {
columnsSelection: {
xAxis: columns[1], // timestamp
yAxes: [columns[3]], // request_count
splitBy: [columns[2]], // city
},
utcOffset: 0
},
queryResultData: {
rows: rows,
columns: columns
}
}
// Act
const result = DataTransformer.getCategoriesAndSeries(options, /*isDatetimeAxis*/ true);
const expectedCategoriesAndSeries: ICategoriesAndSeries = {
series: [{
name: 'Jerusalem',
data: [[1988, 500]]
},
{
name: 'Herzliya',
data: [[2000, 1000]]
},
{
name: 'Boston',
data: [[2000, 200]]
},
{
name: 'Tel Aviv',
data: [[2000, 10]]
},
{
name: 'New York',
data: [[2000, 100]]
},
{
name: 'Tokyo',
data: [[2019, 20]]
},
{
name: 'Atlanta',
data: [[2019, 300]]
},
{
name: 'Redmond',
data: [[2019, 20]]
}],
categories: undefined
};
// Assert
expect(result).toEqual(expectedCategoriesAndSeries);
});
//#endregion getSplitByCategoriesAndSeries
});
//#endregion Tests
});