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 interface LinePlotData {
type: ChartPlotEnum.LINE | ChartPlotEnum.BAR;
type: ChartPlotEnum.LINE;
strokeFill: string,
strokeWidth: number,
data: SimplePlotDataType;
}
export type PlotData = LinePlotData;
export interface BarPlotData {
type: ChartPlotEnum.BAR;
fill: string,
data: SimplePlotDataType;
}
export type PlotData = LinePlotData | BarPlotData;
export interface BandAxisDataType {
title: string;

View File

@ -1,5 +1,5 @@
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 { ChartComponent } from './Interfaces.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 availableHeight = this.chartConfig.height;
let chartX = 0;
@ -67,7 +67,7 @@ export class Orchestrator {
chartHeight += availableHeight;
availableHeight = 0;
}
const plotBorderWidthHalf = this.chartConfig.plotBorderWidth/2;
const plotBorderWidthHalf = this.chartConfig.plotBorderWidth / 2;
chartX += plotBorderWidthHalf;
chartY += plotBorderWidthHalf;
chartWidth -= this.chartConfig.plotBorderWidth;
@ -88,6 +88,83 @@ export class Orchestrator {
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() {
this.calculateSpace();
const drawableElem: DrawableElem[] = [];

View File

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

View File

@ -41,6 +41,10 @@ export abstract class BaseAxis implements IAxis {
abstract getTickValues(): Array<string | number>;
getTickDistance(): number {
return Math.abs(this.range[0] - this.range[1]) / this.getTickValues().length;
}
getTickInnerPadding(): number {
return this.innerPadding * 2;
// 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;
if (this.axisConfig.showLabel) {
const spaceRequired = this.getLabelDimension();
this.innerPadding = spaceRequired.width / 2;
this.innerPadding = spaceRequired.height / 2;
const widthRequired = spaceRequired.width + this.axisConfig.lablePadding * 2;
log.trace('width required for axis label: ', widthRequired);
if (widthRequired <= availableWidth) {
@ -122,7 +126,7 @@ export abstract class BaseAxis implements IAxis {
this.recalculateScale();
return { width: 0, height: 0 };
}
if (this.axisPosition === 'left') {
if (this.axisPosition === 'left' || this.axisPosition === 'right') {
this.calculateSpaceIfDrawnVertical(availableSpace);
} else {
this.calculateSpaceIfDrawnHorizontally(availableSpace);
@ -247,14 +251,72 @@ export abstract class BaseAxis implements IAxis {
}
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[] {
if (this.axisPosition === 'left') {
return this.getDrawaableElementsForLeftAxis();
}
if (this.axisPosition === 'right') {
throw Error("Drawing of right axis is not implemented");
}
if (this.axisPosition === 'bottom') {
return this.getDrawaableElementsForBottomAxis();
}
if (this.axisPosition === 'top') {
return this.getDrawaableElementsForTopAxis();
}
return [];
}
}

View File

@ -9,12 +9,13 @@ import { ChartComponent } from '../../Interfaces.js';
import { BandAxis } from './BandAxis.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 {
getScaleValue(value: string | number): number;
setAxisPosition(axisPosition: AxisPosition): void;
getTickInnerPadding(): number;
getTickDistance(): number;
setRange(range: [number, number]): void;
}

View File

@ -1,40 +1,66 @@
import { line } from 'd3';
import { BoundingRect, DrawableElem, SimplePlotDataType } from '../../Interfaces.js';
import {
BarPlotData,
BoundingRect,
DrawableElem,
OrientationEnum,
SimplePlotDataType,
} from '../../Interfaces.js';
import { IAxis } from '../axis/index.js';
export class BarPlot {
constructor(
private data: SimplePlotDataType,
private barData: BarPlotData,
private boundingRect: BoundingRect,
private xAxis: IAxis,
private yAxis: IAxis
private yAxis: IAxis,
private orientation: OrientationEnum
) {}
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.yAxis.getScaleValue(d[1]),
]);
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;
return [
{
groupTexts: ['plot', 'bar-plot'],
type: 'rect',
data: finalData.map((data) => ({
x: data[0] - barWidthHalf,
y: data[1],
width: barWidth,
height: this.boundingRect.y + this.boundingRect.height - data[1],
fill: '#ff0000',
strokeWidth: 0,
strokeFill: '#0000ff',
})),
},
];
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 [
{
groupTexts: ['plot', 'bar-plot'],
type: 'rect',
data: finalData.map((data) => ({
x: data[0] - barWidthHalf,
y: data[1],
width: barWidth,
height: this.boundingRect.y + this.boundingRect.height - data[1],
fill: this.barData.fill,
strokeWidth: 0,
strokeFill: this.barData.fill,
})),
},
];
}
}
}

View File

@ -1,19 +1,31 @@
import { line } from 'd3';
import { DrawableElem, SimplePlotDataType } from '../../Interfaces.js';
import { DrawableElem, LinePlotData, OrientationEnum } from '../../Interfaces.js';
import { IAxis } from '../axis/index.js';
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[] {
const finalData: [number, number][] = this.data.map((d) => [
const finalData: [number, number][] = this.plotData.data.map((d) => [
this.xAxis.getScaleValue(d[0]),
this.yAxis.getScaleValue(d[1]),
]);
const path = line()
.x((d) => d[0])
.y((d) => d[1])(finalData);
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])
.y((d) => d[1])(finalData);
}
if (!path) {
return [];
}
@ -24,8 +36,8 @@ export class LinePlot {
data: [
{
path,
strokeFill: '#0000ff',
strokeWidth: 2,
strokeFill: this.plotData.strokeFill,
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 {
constructor(private boundingRect: BoundingRect) {}
constructor(private boundingRect: BoundingRect, private orientation: OrientationEnum) {}
getDrawableElement(): DrawableElem[] {
const {x, y, width, height} = this.boundingRect;
if(this.orientation === OrientationEnum.HORIZONTAL) {
return [
{
groupTexts: ['plot', 'chart-border'],
type: 'path',
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',
strokeWidth: 1,
},

View File

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

View File

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