Added support for horizontal drawing

This commit is contained in:
Subhash Halder
2023-06-22 23:08:08 +05:30
parent 547a22edef
commit 0a731e1ee1
10 changed files with 247 additions and 53 deletions

View File

@@ -47,11 +47,19 @@ export interface XYChartConfig {
export type SimplePlotDataType = [string | number, number][]; export type SimplePlotDataType = [string | number, number][];
export interface LinePlotData { export interface LinePlotData {
type: ChartPlotEnum.LINE | ChartPlotEnum.BAR; type: ChartPlotEnum.LINE;
strokeFill: string,
strokeWidth: number,
data: SimplePlotDataType; data: SimplePlotDataType;
} }
export type PlotData = LinePlotData; export interface BarPlotData {
type: ChartPlotEnum.BAR;
fill: string,
data: SimplePlotDataType;
}
export type PlotData = LinePlotData | BarPlotData;
export interface BandAxisDataType { export interface BandAxisDataType {
title: string; title: string;

View File

@@ -1,5 +1,5 @@
import { log } from '../../../logger.js'; import { log } from '../../../logger.js';
import { DrawableElem, XYChartConfig, XYChartData } from './Interfaces.js'; import { DrawableElem, OrientationEnum, XYChartConfig, XYChartData } from './Interfaces.js';
import { getChartTitleComponent } from './components/ChartTitle.js'; import { getChartTitleComponent } from './components/ChartTitle.js';
import { ChartComponent } from './Interfaces.js'; import { ChartComponent } from './Interfaces.js';
import { IAxis, getAxis } from './components/axis/index.js'; import { IAxis, getAxis } from './components/axis/index.js';
@@ -21,7 +21,7 @@ export class Orchestrator {
}; };
} }
private calculateSpace() { private calculateVerticalSpace() {
let availableWidth = this.chartConfig.width; let availableWidth = this.chartConfig.width;
let availableHeight = this.chartConfig.height; let availableHeight = this.chartConfig.height;
let chartX = 0; let chartX = 0;
@@ -67,7 +67,7 @@ export class Orchestrator {
chartHeight += availableHeight; chartHeight += availableHeight;
availableHeight = 0; availableHeight = 0;
} }
const plotBorderWidthHalf = this.chartConfig.plotBorderWidth/2; const plotBorderWidthHalf = this.chartConfig.plotBorderWidth / 2;
chartX += plotBorderWidthHalf; chartX += plotBorderWidthHalf;
chartY += plotBorderWidthHalf; chartY += plotBorderWidthHalf;
chartWidth -= this.chartConfig.plotBorderWidth; chartWidth -= this.chartConfig.plotBorderWidth;
@@ -88,6 +88,83 @@ export class Orchestrator {
this.componentStore.yAxis.setBoundingBoxXY({ x: 0, y: chartY }); this.componentStore.yAxis.setBoundingBoxXY({ x: 0, y: chartY });
} }
private calculateHorizonatalSpace() {
let availableWidth = this.chartConfig.width;
let availableHeight = this.chartConfig.height;
let titleYEnd = 0;
let chartX = 0;
let chartY = 0;
let chartWidth = Math.floor((availableWidth * this.chartConfig.plotReservedSpacePercent) / 100);
let chartHeight = Math.floor(
(availableHeight * this.chartConfig.plotReservedSpacePercent) / 100
);
let spaceUsed = this.componentStore.plot.calculateSpace({
width: chartWidth,
height: chartHeight,
});
availableWidth -= spaceUsed.width;
availableHeight -= spaceUsed.height;
spaceUsed = this.componentStore.title.calculateSpace({
width: this.chartConfig.width,
height: availableHeight,
});
log.trace('space used by title: ', spaceUsed);
titleYEnd = spaceUsed.height;
availableHeight -= spaceUsed.height;
this.componentStore.xAxis.setAxisPosition('left');
spaceUsed = this.componentStore.xAxis.calculateSpace({
width: availableWidth,
height: availableHeight,
});
availableWidth -= spaceUsed.width;
chartX = spaceUsed.width;
log.trace('space used by xaxis: ', spaceUsed);
this.componentStore.yAxis.setAxisPosition('top');
spaceUsed = this.componentStore.yAxis.calculateSpace({
width: availableWidth,
height: availableHeight,
});
log.trace('space used by yaxis: ', spaceUsed);
availableHeight -= spaceUsed.height;
chartY = titleYEnd + spaceUsed.height;
if (availableWidth > 0) {
chartWidth += availableWidth;
availableWidth = 0;
}
if (availableHeight > 0) {
chartHeight += availableHeight;
availableHeight = 0;
}
const plotBorderWidthHalf = this.chartConfig.plotBorderWidth / 2;
chartX += plotBorderWidthHalf;
chartY += plotBorderWidthHalf;
chartWidth -= this.chartConfig.plotBorderWidth;
chartHeight -= this.chartConfig.plotBorderWidth;
this.componentStore.plot.calculateSpace({
width: chartWidth,
height: chartHeight,
});
log.trace(
`Final chart dimansion: x = ${chartX}, y = ${chartY}, width = ${chartWidth}, height = ${chartHeight}`
);
this.componentStore.plot.setBoundingBoxXY({ x: chartX, y: chartY });
this.componentStore.yAxis.setRange([chartX, chartX + chartWidth]);
this.componentStore.yAxis.setBoundingBoxXY({ x: chartX, y: titleYEnd });
this.componentStore.xAxis.setRange([chartY, chartY + chartHeight]);
this.componentStore.xAxis.setBoundingBoxXY({ x: 0, y: chartY });
}
private calculateSpace() {
if (this.chartConfig.chartOrientation === OrientationEnum.HORIZONTAL) {
this.calculateHorizonatalSpace();
} else {
this.calculateVerticalSpace();
}
}
getDrawableElement() { getDrawableElement() {
this.calculateSpace(); this.calculateSpace();
const drawableElem: DrawableElem[] = []; const drawableElem: DrawableElem[] = [];

View File

@@ -13,7 +13,6 @@ import { ChartComponent } from '../Interfaces.js';
export class ChartTitle implements ChartComponent { export class ChartTitle implements ChartComponent {
private boundingRect: BoundingRect; private boundingRect: BoundingRect;
private showChartTitle: boolean; private showChartTitle: boolean;
private orientation: OrientationEnum;
constructor( constructor(
private textDimensionCalculator: ITextDimensionCalculator, private textDimensionCalculator: ITextDimensionCalculator,
private chartConfig: XYChartConfig, private chartConfig: XYChartConfig,
@@ -26,10 +25,6 @@ export class ChartTitle implements ChartComponent {
height: 0, height: 0,
}; };
this.showChartTitle = !!(this.chartData.title && this.chartConfig.showtitle); this.showChartTitle = !!(this.chartData.title && this.chartConfig.showtitle);
this.orientation = OrientationEnum.VERTICAL;
}
setOrientation(orientation: OrientationEnum): void {
this.orientation = orientation;
} }
setBoundingBoxXY(point: Point): void { setBoundingBoxXY(point: Point): void {
this.boundingRect.x = point.x; this.boundingRect.x = point.x;

View File

@@ -41,6 +41,10 @@ export abstract class BaseAxis implements IAxis {
abstract getTickValues(): Array<string | number>; abstract getTickValues(): Array<string | number>;
getTickDistance(): number {
return Math.abs(this.range[0] - this.range[1]) / this.getTickValues().length;
}
getTickInnerPadding(): number { getTickInnerPadding(): number {
return this.innerPadding * 2; return this.innerPadding * 2;
// return Math.abs(this.range[0] - this.range[1]) / this.getTickValues().length; // return Math.abs(this.range[0] - this.range[1]) / this.getTickValues().length;
@@ -89,7 +93,7 @@ export abstract class BaseAxis implements IAxis {
let availableWidth = availableSpace.width; let availableWidth = availableSpace.width;
if (this.axisConfig.showLabel) { if (this.axisConfig.showLabel) {
const spaceRequired = this.getLabelDimension(); const spaceRequired = this.getLabelDimension();
this.innerPadding = spaceRequired.width / 2; this.innerPadding = spaceRequired.height / 2;
const widthRequired = spaceRequired.width + this.axisConfig.lablePadding * 2; const widthRequired = spaceRequired.width + this.axisConfig.lablePadding * 2;
log.trace('width required for axis label: ', widthRequired); log.trace('width required for axis label: ', widthRequired);
if (widthRequired <= availableWidth) { if (widthRequired <= availableWidth) {
@@ -122,7 +126,7 @@ export abstract class BaseAxis implements IAxis {
this.recalculateScale(); this.recalculateScale();
return { width: 0, height: 0 }; return { width: 0, height: 0 };
} }
if (this.axisPosition === 'left') { if (this.axisPosition === 'left' || this.axisPosition === 'right') {
this.calculateSpaceIfDrawnVertical(availableSpace); this.calculateSpaceIfDrawnVertical(availableSpace);
} else { } else {
this.calculateSpaceIfDrawnHorizontally(availableSpace); this.calculateSpaceIfDrawnHorizontally(availableSpace);
@@ -247,14 +251,72 @@ export abstract class BaseAxis implements IAxis {
} }
return drawableElement; return drawableElement;
} }
private getDrawaableElementsForTopAxis(): DrawableElem[] {
const drawableElement: DrawableElem[] = [];
if (this.showLabel) {
drawableElement.push({
type: 'text',
groupTexts: ['bottom-axis', 'label'],
data: this.getTickValues().map((tick) => ({
text: tick.toString(),
x: this.getScaleValue(tick),
y: this.boundingRect.y + this.boundingRect.height - this.axisConfig.lablePadding - this.axisConfig.tickLength,
fill: this.axisConfig.labelFill,
fontSize: this.axisConfig.labelFontSize,
rotation: 0,
verticalPos: 'center',
horizontalPos: 'bottom',
})),
});
}
if (this.showTick) {
const y = this.boundingRect.y;
drawableElement.push({
type: 'path',
groupTexts: ['bottom-axis', 'ticks'],
data: this.getTickValues().map((tick) => ({
path: `M ${this.getScaleValue(tick)},${y + this.boundingRect.height} L ${this.getScaleValue(tick)},${
y + this.boundingRect.height - this.axisConfig.tickLength
}`,
strokeFill: this.axisConfig.tickFill,
strokeWidth: this.axisConfig.tickWidth,
})),
});
}
if (this.showTitle) {
drawableElement.push({
type: 'text',
groupTexts: ['bottom-axis', 'title'],
data: [
{
text: this.title,
x: this.range[0] + (this.range[1] - this.range[0]) / 2,
y: this.boundingRect.y + this.axisConfig.titlePadding,
fill: this.axisConfig.titleFill,
fontSize: this.axisConfig.titleFontSize,
rotation: 0,
verticalPos: 'center',
horizontalPos: 'top',
},
],
});
}
return drawableElement;
}
getDrawableElements(): DrawableElem[] { getDrawableElements(): DrawableElem[] {
if (this.axisPosition === 'left') { if (this.axisPosition === 'left') {
return this.getDrawaableElementsForLeftAxis(); return this.getDrawaableElementsForLeftAxis();
} }
if (this.axisPosition === 'right') {
throw Error("Drawing of right axis is not implemented");
}
if (this.axisPosition === 'bottom') { if (this.axisPosition === 'bottom') {
return this.getDrawaableElementsForBottomAxis(); return this.getDrawaableElementsForBottomAxis();
} }
if (this.axisPosition === 'top') {
return this.getDrawaableElementsForTopAxis();
}
return []; return [];
} }
} }

View File

@@ -9,12 +9,13 @@ import { ChartComponent } from '../../Interfaces.js';
import { BandAxis } from './BandAxis.js'; import { BandAxis } from './BandAxis.js';
import { LinearAxis } from './LinearAxis.js'; import { LinearAxis } from './LinearAxis.js';
export type AxisPosition = 'left' | 'bottom' | 'top' | 'bottom'; export type AxisPosition = 'left' | 'right' | 'top' | 'bottom';
export interface IAxis extends ChartComponent { export interface IAxis extends ChartComponent {
getScaleValue(value: string | number): number; getScaleValue(value: string | number): number;
setAxisPosition(axisPosition: AxisPosition): void; setAxisPosition(axisPosition: AxisPosition): void;
getTickInnerPadding(): number; getTickInnerPadding(): number;
getTickDistance(): number;
setRange(range: [number, number]): void; setRange(range: [number, number]): void;
} }

View File

@@ -1,26 +1,51 @@
import { line } from 'd3'; import {
import { BoundingRect, DrawableElem, SimplePlotDataType } from '../../Interfaces.js'; BarPlotData,
BoundingRect,
DrawableElem,
OrientationEnum,
SimplePlotDataType,
} from '../../Interfaces.js';
import { IAxis } from '../axis/index.js'; import { IAxis } from '../axis/index.js';
export class BarPlot { export class BarPlot {
constructor( constructor(
private data: SimplePlotDataType, private barData: BarPlotData,
private boundingRect: BoundingRect, private boundingRect: BoundingRect,
private xAxis: IAxis, private xAxis: IAxis,
private yAxis: IAxis private yAxis: IAxis,
private orientation: OrientationEnum
) {} ) {}
getDrawableElement(): DrawableElem[] { getDrawableElement(): DrawableElem[] {
const finalData: [number, number][] = this.data.map((d) => [ const finalData: [number, number][] = this.barData.data.map((d) => [
this.xAxis.getScaleValue(d[0]), this.xAxis.getScaleValue(d[0]),
this.yAxis.getScaleValue(d[1]), this.yAxis.getScaleValue(d[1]),
]); ]);
const barPaddingPercent = 5; const barPaddingPercent = 5;
const barWidth = this.xAxis.getTickInnerPadding() * (1 - barPaddingPercent / 100); const barWidth =
Math.min(this.xAxis.getTickInnerPadding(), this.xAxis.getTickDistance()) *
(1 - barPaddingPercent / 100);
const barWidthHalf = barWidth / 2; const barWidthHalf = barWidth / 2;
if (this.orientation === OrientationEnum.HORIZONTAL) {
return [
{
groupTexts: ['plot', 'bar-plot'],
type: 'rect',
data: finalData.map((data) => ({
x: this.boundingRect.x,
y: data[0] - barWidthHalf,
height: barWidth,
width: data[1] - this.boundingRect.x,
fill: this.barData.fill,
strokeWidth: 0,
strokeFill: this.barData.fill,
})),
},
];
} else {
return [ return [
{ {
groupTexts: ['plot', 'bar-plot'], groupTexts: ['plot', 'bar-plot'],
@@ -30,11 +55,12 @@ export class BarPlot {
y: data[1], y: data[1],
width: barWidth, width: barWidth,
height: this.boundingRect.y + this.boundingRect.height - data[1], height: this.boundingRect.y + this.boundingRect.height - data[1],
fill: '#ff0000', fill: this.barData.fill,
strokeWidth: 0, strokeWidth: 0,
strokeFill: '#0000ff', strokeFill: this.barData.fill,
})), })),
}, },
]; ];
} }
}
} }

View File

@@ -1,19 +1,31 @@
import { line } from 'd3'; import { line } from 'd3';
import { DrawableElem, SimplePlotDataType } from '../../Interfaces.js'; import { DrawableElem, LinePlotData, OrientationEnum } from '../../Interfaces.js';
import { IAxis } from '../axis/index.js'; import { IAxis } from '../axis/index.js';
export class LinePlot { export class LinePlot {
constructor(private data: SimplePlotDataType, private xAxis: IAxis, private yAxis: IAxis) {} constructor(
private plotData: LinePlotData,
private xAxis: IAxis,
private yAxis: IAxis,
private orientation: OrientationEnum
) {}
getDrawableElement(): DrawableElem[] { getDrawableElement(): DrawableElem[] {
const finalData: [number, number][] = this.data.map((d) => [ const finalData: [number, number][] = this.plotData.data.map((d) => [
this.xAxis.getScaleValue(d[0]), this.xAxis.getScaleValue(d[0]),
this.yAxis.getScaleValue(d[1]), this.yAxis.getScaleValue(d[1]),
]); ]);
const path = line() let path: string | null;
if (this.orientation === OrientationEnum.HORIZONTAL) {
path = line()
.y((d) => d[0])
.x((d) => d[1])(finalData);
} else {
path = line()
.x((d) => d[0]) .x((d) => d[0])
.y((d) => d[1])(finalData); .y((d) => d[1])(finalData);
}
if (!path) { if (!path) {
return []; return [];
} }
@@ -24,8 +36,8 @@ export class LinePlot {
data: [ data: [
{ {
path, path,
strokeFill: '#0000ff', strokeFill: this.plotData.strokeFill,
strokeWidth: 2, strokeWidth: this.plotData.strokeWidth,
}, },
], ],
}, },

View File

@@ -1,16 +1,31 @@
import { BoundingRect, DrawableElem } from '../../Interfaces.js'; import { BoundingRect, DrawableElem, OrientationEnum } from '../../Interfaces.js';
export class PlotBorder { export class PlotBorder {
constructor(private boundingRect: BoundingRect) {} constructor(private boundingRect: BoundingRect, private orientation: OrientationEnum) {}
getDrawableElement(): DrawableElem[] { getDrawableElement(): DrawableElem[] {
const {x, y, width, height} = this.boundingRect; const {x, y, width, height} = this.boundingRect;
if(this.orientation === OrientationEnum.HORIZONTAL) {
return [ return [
{ {
groupTexts: ['plot', 'chart-border'], groupTexts: ['plot', 'chart-border'],
type: 'path', type: 'path',
data: [ data: [
{ {
path: `M ${x},${y} L ${x + width},${y} L ${x + width},${y + height} L ${x},${y + height} L ${x},${y}`, path: `M ${x},${y} L ${x + width},${y} M ${x + width},${y + height} M ${x},${y + height} L ${x},${y}`,
strokeFill: '#000000',
strokeWidth: 1,
},
],
},
];
}
return [
{
groupTexts: ['plot', 'chart-border'],
type: 'path',
data: [
{
path: `M ${x},${y} M ${x + width},${y} M ${x + width},${y + height} L ${x},${y + height} L ${x},${y}`,
strokeFill: '#000000', strokeFill: '#000000',
strokeWidth: 1, strokeWidth: 1,
}, },

View File

@@ -21,7 +21,6 @@ export interface IPlot extends ChartComponent {
export class Plot implements IPlot { export class Plot implements IPlot {
private boundingRect: BoundingRect; private boundingRect: BoundingRect;
private orientation: OrientationEnum;
private xAxis?: IAxis; private xAxis?: IAxis;
private yAxis?: IAxis; private yAxis?: IAxis;
@@ -35,15 +34,11 @@ export class Plot implements IPlot {
width: 0, width: 0,
height: 0, height: 0,
}; };
this.orientation = OrientationEnum.VERTICAL;
} }
setAxes(xAxis: IAxis, yAxis: IAxis) { setAxes(xAxis: IAxis, yAxis: IAxis) {
this.xAxis = xAxis; this.xAxis = xAxis;
this.yAxis = yAxis; this.yAxis = yAxis;
} }
setOrientation(orientation: OrientationEnum): void {
this.orientation = orientation;
}
setBoundingBoxXY(point: Point): void { setBoundingBoxXY(point: Point): void {
this.boundingRect.x = point.x; this.boundingRect.x = point.x;
this.boundingRect.y = point.y; this.boundingRect.y = point.y;
@@ -62,17 +57,17 @@ export class Plot implements IPlot {
throw Error("Axes must be passed to render Plots"); throw Error("Axes must be passed to render Plots");
} }
const drawableElem: DrawableElem[] = [ const drawableElem: DrawableElem[] = [
...new PlotBorder(this.boundingRect).getDrawableElement() ...new PlotBorder(this.boundingRect, this.chartConfig.chartOrientation).getDrawableElement()
]; ];
for(const plot of this.chartData.plots) { for(const plot of this.chartData.plots) {
switch(plot.type) { switch(plot.type) {
case ChartPlotEnum.LINE: { case ChartPlotEnum.LINE: {
const linePlot = new LinePlot(plot.data, this.xAxis, this.yAxis); const linePlot = new LinePlot(plot, this.xAxis, this.yAxis, this.chartConfig.chartOrientation);
drawableElem.push(...linePlot.getDrawableElement()) drawableElem.push(...linePlot.getDrawableElement())
} }
break; break;
case ChartPlotEnum.BAR: { case ChartPlotEnum.BAR: {
const barPlot = new BarPlot(plot.data, this.boundingRect, this.xAxis, this.yAxis) const barPlot = new BarPlot(plot, this.boundingRect, this.xAxis, this.yAxis, this.chartConfig.chartOrientation)
drawableElem.push(...barPlot.getDrawableElement()); drawableElem.push(...barPlot.getDrawableElement());
} }
break; break;

View File

@@ -69,6 +69,7 @@ export class XYChartBuilder {
plots: [ plots: [
{ {
type: ChartPlotEnum.BAR, type: ChartPlotEnum.BAR,
fill: '#0000bb',
data: [ data: [
['category1', 23], ['category1', 23],
['category2', 56], ['category2', 56],
@@ -77,6 +78,8 @@ export class XYChartBuilder {
}, },
{ {
type: ChartPlotEnum.LINE, type: ChartPlotEnum.LINE,
strokeFill: '#bb0000',
strokeWidth: 2,
data: [ data: [
['category1', 33], ['category1', 33],
['category2', 45], ['category2', 45],