This commit is contained in:
Violet Voronetzky 2019-12-03 14:19:09 +02:00
Родитель f65edfe839
Коммит ee54ae541e
19 изменённых файлов: 695 добавлений и 483 удалений

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

@ -0,0 +1,10 @@
'use strict';
export interface ChartTypeOptions {
chartType: string;
plotOptions?: Highcharts.PlotOptions; // See: https://api.highcharts.com/highcharts/plotOptions
}
export const PERCENTAGE = 'percent';
export const STACKED = 'normal';
export const UNSTACKED = undefined;

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

@ -0,0 +1,23 @@
'use strict';
import { HighchartsChart } from './highchartsChart';
import { ChartTypeOptions } from '../chartTypeOptions';
export abstract class Area extends HighchartsChart {
//#region Methods override
protected getChartTypeOptions(): ChartTypeOptions {
return {
chartType: 'area',
plotOptions: {
area: {
stacking: this.getStackingOption()
}
}
}
}
//#endregion Methods override
protected abstract getStackingOption(): any;
}

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

@ -0,0 +1,23 @@
'use strict';
import { HighchartsChart } from './highchartsChart';
import { ChartTypeOptions } from '../chartTypeOptions';
export abstract class Column extends HighchartsChart {
//#region Methods override
protected getChartTypeOptions(): ChartTypeOptions {
return {
chartType: 'column',
plotOptions: {
column: {
stacking: this.getStackingOption()
}
}
}
}
//#endregion Methods override
protected abstract getStackingOption(): any;
}

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

@ -0,0 +1,13 @@
'use strict';
import { Pie } from './pie';
export class Donut extends Pie {
//#region Methods override
protected getInnerSize(): any {
return '40%';
}
//#endregion Methods override
}

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

@ -0,0 +1,220 @@
'use strict';
import * as _ from 'lodash';
import * as Highcharts from 'highcharts';
import { IVisualizerOptions } from '../../IVisualizerOptions';
import { ChartTypeOptions } from '../chartTypeOptions';
import { Utilities } from '../../../common/utilities';
export interface ICategoriesAndSeries {
categories?: string[];
series: any[];
}
export abstract class HighchartsChart {
protected options: IVisualizerOptions;
public constructor(options: IVisualizerOptions) {
this.options = options;
}
public getHighchartsOptions(): Highcharts.Options {
const chartOptions = this.options.chartOptions;
const chartTypeOptions = this.getChartTypeOptions();
const isDatetimeAxis = Utilities.isDate(chartOptions.columnsSelection.xAxis.type);
const categoriesAndSeries = this.getCategoriesAndSeries(isDatetimeAxis);
const highchartsOptions: Highcharts.Options = {
chart: {
type: chartTypeOptions.chartType
},
plotOptions: chartTypeOptions.plotOptions,
xAxis: {
type: isDatetimeAxis ? 'datetime' : undefined,
categories: categoriesAndSeries.categories,
title: {
text: this.getXAxisTitle(),
align: 'middle'
}
},
yAxis: this.getYAxis(),
series: categoriesAndSeries.series
};
return highchartsOptions;
}
protected getCategoriesAndSeries(isDatetimeAxis: boolean): ICategoriesAndSeries {
const columnsSelection = this.options.chartOptions.columnsSelection;
const xAxisColumn = columnsSelection.xAxis;
const xAxisColumnIndex = Utilities.getColumnIndex(this.options.queryResultData, xAxisColumn);
let categoriesAndSeries = {
series: [],
categories: isDatetimeAxis ? undefined : []
};
if(columnsSelection.splitBy && columnsSelection.splitBy.length > 0) {
this.getSplitByCategoriesAndSeries(xAxisColumnIndex, isDatetimeAxis, categoriesAndSeries);
} else {
this.getStandardCategoriesAndSeries(xAxisColumnIndex, isDatetimeAxis, categoriesAndSeries);
}
return categoriesAndSeries;
}
protected getStandardCategoriesAndSeries(xAxisColumnIndex: number, isDatetimeAxis: boolean, categoriesAndSeries: ICategoriesAndSeries): void {
const chartOptions = this.options.chartOptions;
const yAxesIndexes = _.map(chartOptions.columnsSelection.yAxes, (yAxisColumn) => {
return Utilities.getColumnIndex(this.options.queryResultData, yAxisColumn);
});
const seriesMap = {};
this.options.queryResultData.rows.forEach((row) => {
let xAxisValue: any = row[xAxisColumnIndex];
// If the x-axis is a date, convert it's value to milliseconds as this is what expected by 'Highcharts'
if(isDatetimeAxis) {
const dateValue = Utilities.getValidDate(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]
});
}
}
protected getSplitByCategoriesAndSeries( xAxisColumnIndex: number, isDatetimeAxis: boolean, categoriesAndSeries: ICategoriesAndSeries): void {
if(isDatetimeAxis) {
this.getSplitByCategoriesAndSeriesForDateXAxis(this.options, xAxisColumnIndex, categoriesAndSeries);
return;
}
const columnsSelection = this.options.chartOptions.columnsSelection;
const yAxisColumn = columnsSelection.yAxes[0];
const splitByColumn = columnsSelection.splitBy[0];
const yAxisColumnIndex = Utilities.getColumnIndex(this.options.queryResultData, yAxisColumn);
const splitByColumnIndex = Utilities.getColumnIndex(this.options.queryResultData, splitByColumn);
const uniqueXValues = {};
const uniqueSplitByValues = {};
this.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);
}
}
protected 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.getValidDate(<string>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]
});
}
}
//#region Abstract methods
protected abstract getChartTypeOptions(): ChartTypeOptions;
//#endregion Abstract methods
//#region Private methods
private getYAxis(): Highcharts.YAxisOptions {
const yAxis = this.options.chartOptions.columnsSelection.yAxes[0];
const yAxisOptions = {
title: {
text: yAxis.name
}
}
return yAxisOptions;
}
private getXAxisTitle(): string {
const xAxisColumn = this.options.chartOptions.columnsSelection.xAxis;
return xAxisColumn.name;
}
//#endregion Private methods
}

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

@ -0,0 +1,59 @@
'use strict';
//#region Imports
import { ChartType } from '../../../common/chartModels';
import { IVisualizerOptions } from '../../IVisualizerOptions';
import { HighchartsChart } from './highchartsChart';
import { Line } from './line';
import { Scatter } from './scatter';
import { UnstackedArea } from './unstackedArea';
import { StackedArea } from './stackedArea';
import { PercentageArea } from './percentageArea';
import { UnstackedColumn } from './unstackedColumn';
import { StackedColumn } from './stackedColumn';
import { PercentageColumn } from './percentageColumn';
import { Pie } from './pie';
import { Donut } from './donut';
//#endregion Imports
export class HighchartsChartFactory {
public static create(options: IVisualizerOptions): HighchartsChart {
switch (options.chartOptions.chartType) {
case ChartType.Line: {
return new Line(options);
}
case ChartType.Scatter: {
return new Scatter(options);
}
case ChartType.UnstackedArea: {
return new UnstackedArea(options);
}
case ChartType.StackedArea: {
return new StackedArea(options);
}
case ChartType.PercentageArea: {
return new PercentageArea(options);
}
case ChartType.UnstackedColumn: {
return new UnstackedColumn(options);
}
case ChartType.StackedColumn: {
return new StackedColumn(options);
}
case ChartType.PercentageColumn: {
return new PercentageColumn(options);
}
case ChartType.Pie: {
return new Pie(options);
}
case ChartType.Donut: {
return new Donut(options);
}
default: {
return new UnstackedColumn(options);
}
}
}
}

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

@ -0,0 +1,21 @@
'use strict';
import { HighchartsChart } from './highchartsChart';
import { ChartTypeOptions, UNSTACKED } from '../chartTypeOptions';
export class Line extends HighchartsChart {
//#region Methods override
protected getChartTypeOptions(): ChartTypeOptions {
return {
chartType: 'line',
plotOptions: {
line: {
stacking: UNSTACKED
}
}
}
}
//#endregion Methods override
}

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

@ -0,0 +1,14 @@
'use strict';
import { Area } from './area';
import { PERCENTAGE } from '../chartTypeOptions';
export class PercentageArea extends Area {
//#region Methods override
protected getStackingOption(): any {
return PERCENTAGE;
}
//#endregion Methods override
}

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

@ -0,0 +1,14 @@
'use strict';
import { Column } from './column';
import { PERCENTAGE } from '../chartTypeOptions';
export class PercentageColumn extends Column {
//#region Methods override
protected getStackingOption(): any {
return PERCENTAGE;
}
//#endregion Methods override
}

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

@ -0,0 +1,144 @@
'use strict';
import * as _ from 'lodash';
import { HighchartsChart, ICategoriesAndSeries } from './highchartsChart';
import { ChartTypeOptions } from '../chartTypeOptions';
import { Utilities } from '../../../common/utilities';
export class Pie extends HighchartsChart {
//#region Methods override
protected getChartTypeOptions(): ChartTypeOptions {
return {
chartType: 'pie',
plotOptions: {
pie: {
innerSize: this.getInnerSize(),
showInLegend: true
}
}
}
}
protected getStandardCategoriesAndSeries(xAxisColumnIndex: number, isDatetimeAxis: boolean, categoriesAndSeries: ICategoriesAndSeries): void {
const yAxisColumn = this.options.chartOptions.columnsSelection.yAxes[0]; // We allow only 1 yAxis in pie charts
const yAxisColumnIndex = Utilities.getColumnIndex(this.options.queryResultData, yAxisColumn);
// Build the data for the pie
const pieSeries = {
name: yAxisColumn.name,
data: []
}
this.options.queryResultData.rows.forEach((row) => {
const xAxisValue = row[xAxisColumnIndex];
const yAxisValue = row[yAxisColumnIndex];
pieSeries.data.push({
name: xAxisValue,
y: yAxisValue
})
});
categoriesAndSeries.series.push(pieSeries);
categoriesAndSeries.categories = undefined;
}
protected getSplitByCategoriesAndSeries(xAxisColumnIndex: number, isDatetimeAxis: boolean, categoriesAndSeries: ICategoriesAndSeries): void {
const yAxisColumn = this.options.chartOptions.columnsSelection.yAxes[0]; // We allow only 1 yAxis in pie charts
const yAxisColumnIndex = Utilities.getColumnIndex(this.options.queryResultData, yAxisColumn);
const splitByIndexes = [xAxisColumnIndex];
this.options.chartOptions.columnsSelection.splitBy.forEach((splitByColumn) => {
splitByIndexes.push(Utilities.getColumnIndex(this.options.queryResultData, splitByColumn));
});
// Build the data for the multi-level pie
let pieData = {};
let pieLevelData = pieData;
this.options.queryResultData.rows.forEach((row) => {
const yAxisValue = row[yAxisColumnIndex];
splitByIndexes.forEach((splitByIndex) => {
const splitByValue: string = <string>row[splitByIndex];
let splitByMap = pieLevelData[splitByValue];
if(!splitByMap) {
pieLevelData[splitByValue] = {
drillDown: {},
y: 0
};
}
pieLevelData[splitByValue].y += yAxisValue;
pieLevelData = pieLevelData[splitByValue].drillDown;
});
pieLevelData = pieData;
});
categoriesAndSeries.series = this.spreadMultiLevelSeries(pieData);
categoriesAndSeries.categories = undefined;
}
//#endregion Methods override
protected getInnerSize(): string {
return '0';
}
//#region Private methods
private spreadMultiLevelSeries(pieData: any, level: number = 0, series: any[] = []): any[] {
const chartOptions = this.options.chartOptions;
const levelsCount = chartOptions.columnsSelection.splitBy.length + 1;
const firstLevelSize = Math.round(100 / levelsCount);
for (let key in pieData) {
let currentSeries = series[level];
let pieLevelValue = pieData[key];
if(!currentSeries) {
let column = (level === 0) ? chartOptions.columnsSelection.xAxis : chartOptions.columnsSelection.splitBy[level - 1];
currentSeries = {
name: column.name,
data: []
};
if(level === 0) {
currentSeries.size = `${firstLevelSize}%`;
} else {
const prevLevelSizeStr = series[level - 1].size;
const prevLevelSize = Number(prevLevelSizeStr.substring(0, 2));
currentSeries.size = `${prevLevelSize + 10}%`;
currentSeries.innerSize = `${prevLevelSize}%`;
}
// We do not show labels for multi-level pie
currentSeries.dataLabels = {
enabled: false
}
series.push(currentSeries);
}
currentSeries.data.push({
name: key,
y: pieLevelValue.y
});
let drillDown = pieLevelValue.drillDown;
if(!_.isEmpty(drillDown)) {
this.spreadMultiLevelSeries(drillDown, level + 1, series);
}
}
return series;
}
//#endregion Private methods
}

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

@ -0,0 +1,21 @@
'use strict';
import { HighchartsChart } from './highchartsChart';
import { ChartTypeOptions, UNSTACKED } from '../chartTypeOptions';
export class Scatter extends HighchartsChart {
//#region Methods override
protected getChartTypeOptions(): ChartTypeOptions {
return {
chartType: 'scatter',
plotOptions: {
scatter: {
stacking: UNSTACKED
}
}
}
}
//#endregion Methods override
}

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

@ -0,0 +1,14 @@
'use strict';
import { Area } from './area';
import { STACKED } from '../chartTypeOptions';
export class StackedArea extends Area {
//#region Methods override
protected getStackingOption(): any {
return STACKED;
}
//#endregion Methods override
}

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

@ -0,0 +1,14 @@
'use strict';
import { Column } from './column';
import { STACKED } from '../chartTypeOptions';
export class StackedColumn extends Column {
//#region Methods override
protected getStackingOption(): any {
return STACKED;
}
//#endregion Methods override
}

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

@ -0,0 +1,14 @@
'use strict';
import { Area } from './area';
import { UNSTACKED } from '../chartTypeOptions';
export class UnstackedArea extends Area {
//#region Methods override
protected getStackingOption(): any {
return UNSTACKED;
}
//#endregion Methods override
}

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

@ -0,0 +1,14 @@
'use strict';
import { Column } from './column';
import { UNSTACKED } from '../chartTypeOptions';
export class UnstackedColumn extends Column {
//#region Methods override
protected getStackingOption(): any {
return UNSTACKED;
}
//#endregion Methods override
}

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

@ -1,98 +0,0 @@
'use strict';
import { ChartType } from '../../common/chartModels';
export interface ChartTypeOptions {
chartType: string;
plotOptions?: Highcharts.PlotOptions;
}
const PERCENTAGE = 'percent';
const STACKED = 'normal';
const UNSTACKED = undefined;
// See: https://api.highcharts.com/highcharts/plotOptions
export const CommonChartTypeToHighcharts: { [key in ChartType]: ChartTypeOptions; } = {
[ChartType.Line]: {
chartType: 'line',
plotOptions: {
line: {
stacking: UNSTACKED
}
}
},
[ChartType.Scatter]: {
chartType: 'scatter',
plotOptions: {
scatter: {
stacking: UNSTACKED
}
}
},
[ChartType.UnstackedArea]: {
chartType: 'area',
plotOptions: {
area: {
stacking: UNSTACKED
}
}
},
[ChartType.StackedArea]: {
chartType: 'area',
plotOptions: {
area: {
stacking: STACKED
}
}
},
[ChartType.PercentageArea]: {
chartType: 'area',
plotOptions: {
area: {
stacking: PERCENTAGE
}
}
},
[ChartType.UnstackedColumn]: {
chartType: 'column',
plotOptions: {
column: {
stacking: UNSTACKED
}
}
},
[ChartType.StackedColumn]: {
chartType: 'column',
plotOptions: {
column: {
stacking: STACKED
}
}
},
[ChartType.PercentageColumn]: {
chartType: 'column',
plotOptions: {
column: {
stacking: PERCENTAGE
}
}
},
[ChartType.Pie]: {
chartType: 'pie',
plotOptions: {
pie: {
innerSize: '0',
showInLegend: true
}
}
},
[ChartType.Donut]: {
chartType: 'pie',
plotOptions: {
pie: {
innerSize: '40%',
showInLegend: true
}
}
},
}

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

@ -1,286 +0,0 @@
'use strict';
//#region Imports
import * as _ from 'lodash';
import { Utilities } from '../../common/utilities';
import { IVisualizerOptions } from '../IVisualizerOptions';
import { ChartType, IChartOptions } from '../../common/chartModels';
//#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);
const isSplitByChart = columnsSelection.splitBy && columnsSelection.splitBy.length > 0;
let categoriesAndSeries = {
series: [],
categories: isDatetimeAxis ? undefined : []
};
if(Utilities.isPieOrDonut(options.chartOptions.chartType)) {
if(isSplitByChart) {
DataTransformer.getPieSplitByCategoriesAndSeries(options, xAxisColumnIndex, categoriesAndSeries);
} else {
DataTransformer.getPieStandardCategoriesAndSeries(options, xAxisColumnIndex, categoriesAndSeries);
}
} else {
if(isSplitByChart) {
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 x-axis is a date, convert it's value to milliseconds as this is what expected by 'Highcharts'
if(isDatetimeAxis) {
const dateValue = Utilities.getValidDate(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.getValidDate(<string>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]
});
}
}
private static getPieStandardCategoriesAndSeries(options: IVisualizerOptions, xAxisColumnIndex: number, categoriesAndSeries: ICategoriesAndSeries): void {
const yAxisColumn = options.chartOptions.columnsSelection.yAxes[0]; // We allow only 1 yAxis in pie charts
const yAxisColumnIndex = Utilities.getColumnIndex(options.queryResultData, yAxisColumn);
// Build the data for the pie
const pieSeries = {
name: yAxisColumn.name,
data: []
}
options.queryResultData.rows.forEach((row) => {
const xAxisValue = row[xAxisColumnIndex];
const yAxisValue = row[yAxisColumnIndex];
pieSeries.data.push({
name: xAxisValue,
y: yAxisValue
})
});
categoriesAndSeries.series.push(pieSeries);
}
private static getPieSplitByCategoriesAndSeries(options: IVisualizerOptions, xAxisColumnIndex: number, categoriesAndSeries: ICategoriesAndSeries): void {
const yAxisColumn = options.chartOptions.columnsSelection.yAxes[0]; // We allow only 1 yAxis in pie charts
const yAxisColumnIndex = Utilities.getColumnIndex(options.queryResultData, yAxisColumn);
const splitByIndexes = [xAxisColumnIndex];
options.chartOptions.columnsSelection.splitBy.forEach((splitByColumn) => {
splitByIndexes.push(Utilities.getColumnIndex(options.queryResultData, splitByColumn));
});
// Build the data for the multi-level pie
let pieData = {};
let pieLevelData = pieData;
options.queryResultData.rows.forEach((row) => {
const yAxisValue = row[yAxisColumnIndex];
splitByIndexes.forEach((splitByIndex) => {
const splitByValue: string = <string>row[splitByIndex];
let splitByMap = pieLevelData[splitByValue];
if(!splitByMap) {
pieLevelData[splitByValue] = {
drillDown: {},
y: 0
};
}
pieLevelData[splitByValue].y += yAxisValue;
pieLevelData = pieLevelData[splitByValue].drillDown;
});
pieLevelData = pieData;
});
categoriesAndSeries.series = DataTransformer.spreadMultiLevelSeries(options.chartOptions, pieData);
}
private static spreadMultiLevelSeries(chartOptions: IChartOptions, pieData: any, level: number = 0, series: any[] = []): any[] {
const levelsCount = chartOptions.columnsSelection.splitBy.length + 1;
const firstLevelSize = Math.round(100 / levelsCount);
for (let key in pieData) {
let currentSeries = series[level];
let pieLevelValue = pieData[key];
if(!currentSeries) {
let column = (level === 0) ? chartOptions.columnsSelection.xAxis : chartOptions.columnsSelection.splitBy[level - 1];
currentSeries = {
name: column.name,
data: []
};
if(level === 0) {
currentSeries.size = `${firstLevelSize}%`;
} else {
const prevLevelSizeStr = series[level - 1].size;
const prevLevelSize = Number(prevLevelSizeStr.substring(0, 2));
currentSeries.size = `${prevLevelSize + 10}%`;
currentSeries.innerSize = `${prevLevelSize}%`;
}
// We do not show labels for multi-level pie
currentSeries.dataLabels = {
enabled: false
}
series.push(currentSeries);
}
currentSeries.data.push({
name: key,
y: pieLevelValue.y
});
let drillDown = pieLevelValue.drillDown;
if(!_.isEmpty(drillDown)) {
DataTransformer.spreadMultiLevelSeries(chartOptions, drillDown, level + 1, series);
}
}
return series;
}
}

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

@ -4,61 +4,18 @@
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';
import { CommonChartTypeToHighcharts } from './commonChartTypeToHighcharts';
import { HighchartsChartFactory } from './charts/highchartsChartFactory';
//#endregion Imports
export class HighchartsVisualizer implements IVisualizer {
public drawNewChart(options: IVisualizerOptions): void {
const chartOptions = options.chartOptions;
const chartTypeOptions = CommonChartTypeToHighcharts[chartOptions.chartType];
const isDatetimeAxis = Utilities.isDate(chartOptions.columnsSelection.xAxis.type);
const categoriesAndSeries = DataTransformer.getCategoriesAndSeries(options, isDatetimeAxis);
const highchartsOptions: Highcharts.Options = {
chart: {
type: chartTypeOptions.chartType
},
plotOptions: chartTypeOptions.plotOptions,
xAxis: {
type: isDatetimeAxis ? 'datetime' : undefined,
categories: categoriesAndSeries.categories,
title: {
text: this.getXAxisTitle(options.chartOptions),
align: 'middle'
}
},
yAxis: this.getYAxis(chartOptions),
series: categoriesAndSeries.series
};
const chart = HighchartsChartFactory.create(options);
const highchartsOptions = chart.getHighchartsOptions();
// 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
}

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

@ -2,9 +2,10 @@
import * as _ from 'lodash';
import { DraftColumnType, IColumn, ChartType } from '../../src/common/chartModels';
import { DataTransformer, ICategoriesAndSeries } from '../../src/visualizers/highcharts/dataTransformer';
import { HighchartsChartFactory } from '../../src/visualizers/highcharts/charts/highchartsChartFactory';
import { ICategoriesAndSeries } from '../../src/visualizers/highcharts/charts/highchartsChart';
describe('Unit tests for Highcharts CategoriesAndSeries', () => {
describe('Unit tests for HighchartsChart.getCategoriesAndSeries method', () => {
//#region beforeEach
beforeEach(() => {
@ -24,9 +25,9 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
//#region Tests
describe('Validate getCategoriesAndSeries method', () => {
//#region getStandardCategoriesAndSeries
//#region Line chart getStandardCategoriesAndSeries
it('Validate getStandardCategoriesAndSeries: non-date x-axis and 1 y-axis', () => {
it('Validate getStandardCategoriesAndSeries for Line chart: non-date x-axis and 1 y-axis', () => {
const rows = [
['Israel', 'Herzliya', 30],
['United States', 'New York', 100],
@ -41,6 +42,7 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
// Input
const options: any = {
chartType: ChartType.Line,
chartOptions: {
columnsSelection: {
xAxis: columns[0], // country
@ -55,9 +57,10 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
}
// Act
const result = DataTransformer.getCategoriesAndSeries(options, /*isDatetimeAxis*/ false);
const chart = HighchartsChartFactory.create(options);
const result: any = chart.getHighchartsOptions();
const expectedCategoriesAndSeries: ICategoriesAndSeries = {
const expected: ICategoriesAndSeries = {
series: [{
name: 'request_count',
data: [30, 100, 20]
@ -66,10 +69,11 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
};
// Assert
expect(result).toEqual(expectedCategoriesAndSeries);
expect(result.series).toEqual(expected.series);
expect(result.xAxis.categories).toEqual(expected.categories);
});
it('Validate getStandardCategoriesAndSeries: date x-axis and 1 y-axis', () => {
it('Validate getStandardCategoriesAndSeries for Line chart: 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],
@ -85,6 +89,7 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
// Input
const options: any = {
chartType: ChartType.Line,
chartOptions: {
columnsSelection: {
xAxis: columns[1], // timestamp
@ -99,9 +104,10 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
}
// Act
const result = DataTransformer.getCategoriesAndSeries(options, /*isDatetimeAxis*/ true);
const chart = HighchartsChartFactory.create(options);
const result: any = chart.getHighchartsOptions();
const expectedCategoriesAndSeries: ICategoriesAndSeries = {
const expected: ICategoriesAndSeries = {
series: [{
name: 'request_count',
data: [[2019, 30], [2019, 20], [2000, 100]]
@ -110,10 +116,11 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
};
// Assert
expect(result).toEqual(expectedCategoriesAndSeries);
expect(result.series).toEqual(expected.series);
expect(result.xAxis.categories).toEqual(expected.categories);
});
it('Validate getStandardCategoriesAndSeries: non-date x-axis and multiple y-axis', () => {
it('Validate getStandardCategoriesAndSeries for Line chart: non-date x-axis and multiple y-axis', () => {
const rows = [
['Israel', 'Herzliya', 30, 300],
['United States', 'New York', 100, 150],
@ -129,6 +136,7 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
// Input
const options: any = {
chartType: ChartType.Line,
chartOptions: {
columnsSelection: {
xAxis: columns[1], // city
@ -143,9 +151,10 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
}
// Act
const result = DataTransformer.getCategoriesAndSeries(options, /*isDatetimeAxis*/ false);
const chart = HighchartsChartFactory.create(options);
const result: any = chart.getHighchartsOptions();
const expectedCategoriesAndSeries: ICategoriesAndSeries = {
const expected: ICategoriesAndSeries = {
series: [{
name: 'request_count',
data: [30, 100, 20]
@ -158,10 +167,11 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
};
// Assert
expect(result).toEqual(expectedCategoriesAndSeries);
expect(result.series).toEqual(expected.series);
expect(result.xAxis.categories).toEqual(expected.categories);
});
it('Validate getStandardCategoriesAndSeries: date x-axis and multiple y-axis', () => {
it('Validate getStandardCategoriesAndSeries for Line chart: 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],
@ -192,9 +202,10 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
}
// Act
const result = DataTransformer.getCategoriesAndSeries(options, /*isDatetimeAxis*/ true);
const chart = HighchartsChartFactory.create(options);
const result: any = chart.getHighchartsOptions();
const expectedCategoriesAndSeries: ICategoriesAndSeries = {
const expected: ICategoriesAndSeries = {
series: [{
name: 'request_count',
data: [[2019, 30], [2019, 20], [2000, 100]]
@ -207,14 +218,15 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
};
// Assert
expect(result).toEqual(expectedCategoriesAndSeries);
expect(result.series).toEqual(expected.series);
expect(result.xAxis.categories).toEqual(expected.categories);
});
//#endregion getStandardCategoriesAndSeries
//#endregion Line chart getStandardCategoriesAndSeries
//#region getSplitByCategoriesAndSeries
//#region Line chart getSplitByCategoriesAndSeries
it('Validate getCategoriesAndSeries: non-date x-axis with splitBy', () => {
it('Validate getCategoriesAndSeries for Line chart: non-date x-axis with splitBy', () => {
const rows = [
['United States', 'Atlanta', 300],
['United States', 'Redmond', 20],
@ -234,6 +246,7 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
// Input
const options: any = {
chartType: ChartType.Line,
chartOptions: {
columnsSelection: {
xAxis: columns[0], // country
@ -249,9 +262,10 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
}
// Act
const result = DataTransformer.getCategoriesAndSeries(options, /*isDatetimeAxis*/ false);
const chart = HighchartsChartFactory.create(options);
const result: any = chart.getHighchartsOptions();
const expectedCategoriesAndSeries: ICategoriesAndSeries = {
const expected: ICategoriesAndSeries = {
series: [{
name: 'Atlanta',
data: [300, null, null]
@ -288,10 +302,11 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
};
// Assert
expect(result).toEqual(expectedCategoriesAndSeries);
expect(result.series).toEqual(expected.series);
expect(result.xAxis.categories).toEqual(expected.categories);
});
it('Validate getCategoriesAndSeries: date x-axis with splitBy', () => {
it('Validate getCategoriesAndSeries for Line chart: date x-axis with splitBy', () => {
const rows = [
['Israel', '1988-06-26T00:00:00Z', 'Jerusalem', 500],
['Israel', '2000-06-26T00:00:00Z', 'Herzliya', 1000],
@ -313,6 +328,7 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
// Input
const options: any = {
chartOptions: {
chartType: ChartType.Line,
columnsSelection: {
xAxis: columns[1], // timestamp
yAxes: [columns[3]], // request_count
@ -327,9 +343,10 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
}
// Act
const result = DataTransformer.getCategoriesAndSeries(options, /*isDatetimeAxis*/ true);
const chart = HighchartsChartFactory.create(options);
const result: any = chart.getHighchartsOptions();
const expectedCategoriesAndSeries: ICategoriesAndSeries = {
const expected: ICategoriesAndSeries = {
series: [{
name: 'Jerusalem',
data: [[1988, 500]]
@ -366,14 +383,15 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
};
// Assert
expect(result).toEqual(expectedCategoriesAndSeries);
expect(result.series).toEqual(expected.series);
expect(result.xAxis.categories).toEqual(expected.categories);
});
//#endregion getSplitByCategoriesAndSeries
//#endregion Line chart getSplitByCategoriesAndSeries
//#region getPieStandardCategoriesAndSeries
it('Validate getPieStandardCategoriesAndSeries', () => {
//#region Pie chart getStandardCategoriesAndSeries
it('Validate getCategoriesAndSeries for Pie chart', () => {
const rows = [
['Israel', 'Tel Aviv', 10],
['United States', 'Redmond', 5],
@ -407,9 +425,10 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
}
// Act
const result = DataTransformer.getCategoriesAndSeries(options, /*isDatetimeAxis*/ false);
const expectedCategoriesAndSeries: ICategoriesAndSeries = {
const chart = HighchartsChartFactory.create(options);
const result: any = chart.getHighchartsOptions();
const expected: ICategoriesAndSeries = {
series: [{
name: 'request_count',
data: [
@ -422,17 +441,17 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
{ name: 'Boston', y: 1 }
]
}],
categories: []
categories: undefined
};
// Assert
expect(result.series).toEqual(expectedCategoriesAndSeries.series);
expect(result.categories).toEqual(expectedCategoriesAndSeries.categories);
expect(result.series).toEqual(expected.series);
expect(result.categories).toEqual(expected.categories);
});
//#endregion getPieStandardCategoriesAndSeries
//#endregion Pie chart getStandardCategoriesAndSeries
//#region getPieSplitByCategoriesAndSeries
//#region Pie chart getSplitByCategoriesAndSeries
function validateResults(result, expected) {
const seriesToValidate = _.map(result.series, (currentSeries) => {
@ -449,7 +468,7 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
expect(result.categories).toEqual(expected.categories);
}
it('Validate getPieSplitByCategoriesAndSeries: pie chart with 2 levels', () => {
it('Validate getSplitByCategoriesAndSeries for Line chart: pie chart with 2 levels', () => {
const rows = [
['Israel', 'Tel Aviv', 10],
['United States', 'Redmond', 5],
@ -484,9 +503,10 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
}
// Act
const result = DataTransformer.getCategoriesAndSeries(options, /*isDatetimeAxis*/ false);
const chart = HighchartsChartFactory.create(options);
const result: any = chart.getHighchartsOptions();
const expectedCategoriesAndSeries: ICategoriesAndSeries = {
const expected: ICategoriesAndSeries = {
series: [{
name: 'country',
size: '50%',
@ -510,14 +530,14 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
]
}],
categories: []
categories: undefined
};
// Assert
validateResults(result, expectedCategoriesAndSeries);
validateResults(result, expected);
});
it('Validate getPieSplitByCategoriesAndSeries: pie chart with 3 levels', () => {
it('Validate getSplitByCategoriesAndSeries for Line chart: pie chart with 3 levels', () => {
const rows = [
['Internet Explorer', 'v8', '0', 10],
['Chrome', 'v65', '0', 5],
@ -560,9 +580,10 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
}
// Act
const result = DataTransformer.getCategoriesAndSeries(options, /*isDatetimeAxis*/ false);
const chart = HighchartsChartFactory.create(options);
const result: any = chart.getHighchartsOptions();
const expectedCategoriesAndSeries: ICategoriesAndSeries = {
const expected: ICategoriesAndSeries = {
series: [{
name: 'browser',
size: '33%',
@ -608,14 +629,14 @@ describe('Unit tests for Highcharts CategoriesAndSeries', () => {
{ name: '0', y: 20 }
]
}],
categories: []
categories: undefined
};
// Assert
validateResults(result, expectedCategoriesAndSeries);
validateResults(result, expected);
});
//#endregion getPieSplitByCategoriesAndSeries
//#endregion Pie chart getSplitByCategoriesAndSeries
});
//#endregion Tests