Added highcharts visualizer and data transformations
This commit is contained in:
Родитель
a62da9ff53
Коммит
aea895b427
23
README.md
23
README.md
|
@ -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/).
|
||||
|
|
3
index.ts
3
index.ts
|
@ -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';
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
});
|
Загрузка…
Ссылка в новой задаче