Added axis tick and plot border

This commit is contained in:
Subhash Halder 2023-06-10 22:37:23 +05:30
parent 183bc0a978
commit cc1d6af232
10 changed files with 166 additions and 65 deletions

View File

@ -35,11 +35,15 @@ export interface AxisConfig {
showLabel: boolean;
labelFontSize: number;
lablePadding: number;
labelColor: string;
labelFill: string;
showTitle: boolean;
titleFontSize: number;
titlePadding: number;
titleColor: string;
titleFill: string;
showTick: boolean;
tickLength: number;
tickWidth: number;
tickFill: string;
}
export interface XYChartConfig {
@ -52,6 +56,7 @@ export interface XYChartConfig {
showtitle: boolean;
xAxis: AxisConfig;
yAxis: AxisConfig;
plotBorderWidth: number;
chartOrientation: OrientationEnum;
plotReservedSpacePercent: number;
}
@ -139,17 +144,17 @@ export interface PathElem {
export type DrawableElem =
| {
groupText: string;
groupTexts: string[];
type: 'rect';
data: RectElem[];
}
| {
groupText: string;
groupTexts: string[];
type: 'text';
data: TextElem[];
}
| {
groupText: string;
groupTexts: string[];
type: 'path';
data: PathElem[];
};

View File

@ -67,6 +67,15 @@ export class Orchestrator {
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}`

View File

@ -57,7 +57,7 @@ export class ChartTitle implements ChartComponent {
const drawableElem: DrawableElem[] = [];
if (this.boundingRect.height > 0 && this.boundingRect.width > 0) {
drawableElem.push({
groupText: 'chart-title',
groupTexts: ['chart-title'],
type: 'text',
data: [
{

View File

@ -1,20 +1,15 @@
import {
Dimension,
Point,
DrawableElem,
BoundingRect,
AxisConfig,
} from '../../Interfaces.js';
import { Dimension, Point, DrawableElem, BoundingRect, AxisConfig } from '../../Interfaces.js';
import { AxisPosition, IAxis } from './index.js';
import { ITextDimensionCalculator } from '../../TextDimensionCalculator.js';
import { log } from '../../../../../logger.js';
export abstract class BaseAxis implements IAxis {
protected boundingRect: BoundingRect = {x: 0, y: 0, width: 0, height: 0};
protected boundingRect: BoundingRect = { x: 0, y: 0, width: 0, height: 0 };
protected axisPosition: AxisPosition = 'left';
private range: [number, number];
protected showTitle = false;
protected showLabel = false;
protected showTick = false;
protected innerPadding = 0;
constructor(
@ -25,7 +20,6 @@ export abstract class BaseAxis implements IAxis {
this.range = [0, 10];
this.boundingRect = { x: 0, y: 0, width: 0, height: 0 };
this.axisPosition = 'left';
}
setRange(range: [number, number]): void {
@ -54,7 +48,7 @@ export abstract class BaseAxis implements IAxis {
);
}
private calculateSpaceIfDrawnVertical(availableSpace: Dimension) {
private calculateSpaceIfDrawnHorizontally(availableSpace: Dimension) {
let availableHeight = availableSpace.height;
if (this.axisConfig.showLabel) {
const spaceRequired = this.getLabelDimension();
@ -66,6 +60,10 @@ export abstract class BaseAxis implements IAxis {
this.showLabel = true;
}
}
if (this.axisConfig.showTick && availableHeight >= this.axisConfig.tickLength) {
this.showTick = true;
availableHeight -= this.axisConfig.tickLength;
}
if (this.axisConfig.showTitle) {
const spaceRequired = this.textDimensionCalculator.getDimension(
[this.title],
@ -82,7 +80,7 @@ export abstract class BaseAxis implements IAxis {
this.boundingRect.height = availableSpace.height - availableHeight;
}
private calculateSpaceIfDrawnHorizontally(availableSpace: Dimension) {
private calculateSpaceIfDrawnVertical(availableSpace: Dimension) {
let availableWidth = availableSpace.width;
if (this.axisConfig.showLabel) {
const spaceRequired = this.getLabelDimension();
@ -94,6 +92,10 @@ export abstract class BaseAxis implements IAxis {
this.showLabel = true;
}
}
if (this.axisConfig.showTick && availableWidth >= this.axisConfig.tickLength) {
this.showTick = true;
availableWidth -= this.axisConfig.tickLength;
}
if (this.axisConfig.showTitle) {
const spaceRequired = this.textDimensionCalculator.getDimension(
[this.title],
@ -116,9 +118,9 @@ export abstract class BaseAxis implements IAxis {
return { width: 0, height: 0 };
}
if (this.axisPosition === 'left') {
this.calculateSpaceIfDrawnHorizontally(availableSpace);
} else {
this.calculateSpaceIfDrawnVertical(availableSpace);
} else {
this.calculateSpaceIfDrawnHorizontally(availableSpace);
}
this.recalculateScale();
return {
@ -137,12 +139,16 @@ export abstract class BaseAxis implements IAxis {
if (this.showLabel) {
drawableElement.push({
type: 'text',
groupText: 'left-axis-label',
groupTexts: ['left-axis', 'label'],
data: this.getTickValues().map((tick) => ({
text: tick.toString(),
x: this.boundingRect.x + this.boundingRect.width - this.axisConfig.lablePadding,
x:
this.boundingRect.x +
this.boundingRect.width -
this.axisConfig.lablePadding -
this.axisConfig.tickLength,
y: this.getScaleValue(tick),
fill: this.axisConfig.labelColor,
fill: this.axisConfig.labelFill,
fontSize: this.axisConfig.labelFontSize,
rotation: 0,
verticalPos: 'right',
@ -150,21 +156,35 @@ export abstract class BaseAxis implements IAxis {
})),
});
}
if (this.showTick) {
const x = this.boundingRect.x + this.boundingRect.width;
drawableElement.push({
type: 'path',
groupTexts: ['left-axis', 'ticks'],
data: this.getTickValues().map((tick) => ({
path: `M ${x},${this.getScaleValue(tick)} L ${x - this.axisConfig.tickLength},${this.getScaleValue(tick)}`,
strokeFill: this.axisConfig.tickFill,
strokeWidth: this.axisConfig.tickWidth,
})),
});
}
if (this.showTitle) {
drawableElement.push({
type: 'text',
groupText: 'right-axis-label',
data: [{
text: this.title,
x: this.boundingRect.x + this.axisConfig.titlePadding,
y: this.range[0] + (this.range[1] - this.range[0])/2,
fill: this.axisConfig.titleColor,
fontSize: this.axisConfig.titleFontSize,
rotation: 270,
verticalPos: 'center',
horizontalPos: 'top',
}]
})
groupTexts: ['left-axis', 'title'],
data: [
{
text: this.title,
x: this.boundingRect.x + this.axisConfig.titlePadding,
y: this.range[0] + (this.range[1] - this.range[0]) / 2,
fill: this.axisConfig.titleFill,
fontSize: this.axisConfig.titleFontSize,
rotation: 270,
verticalPos: 'center',
horizontalPos: 'top',
},
],
});
}
return drawableElement;
}
@ -173,12 +193,12 @@ export abstract class BaseAxis implements IAxis {
if (this.showLabel) {
drawableElement.push({
type: 'text',
groupText: 'right-axis-lable',
groupTexts: ['bottom-axis', 'label'],
data: this.getTickValues().map((tick) => ({
text: tick.toString(),
x: this.getScaleValue(tick),
y: this.boundingRect.y + this.axisConfig.lablePadding,
fill: this.axisConfig.labelColor,
y: this.boundingRect.y + this.axisConfig.lablePadding + this.axisConfig.tickLength,
fill: this.axisConfig.labelFill,
fontSize: this.axisConfig.labelFontSize,
rotation: 0,
verticalPos: 'center',
@ -186,21 +206,35 @@ export abstract class BaseAxis implements IAxis {
})),
});
}
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} L ${this.getScaleValue(tick)},${y + this.axisConfig.tickLength}`,
strokeFill: this.axisConfig.tickFill,
strokeWidth: this.axisConfig.tickWidth,
})),
});
}
if (this.showTitle) {
drawableElement.push({
type: 'text',
groupText: 'right-axis-label',
data: [{
text: this.title,
x: this.range[0] + (this.range[1] - this.range[0])/2,
y: this.boundingRect.y + this.boundingRect.height - this.axisConfig.titlePadding,
fill: this.axisConfig.titleColor,
fontSize: this.axisConfig.titleFontSize,
rotation: 0,
verticalPos: 'center',
horizontalPos: 'bottom',
}]
})
groupTexts: ['bottom-axis', 'title'],
data: [
{
text: this.title,
x: this.range[0] + (this.range[1] - this.range[0]) / 2,
y: this.boundingRect.y + this.boundingRect.height - this.axisConfig.titlePadding,
fill: this.axisConfig.titleFill,
fontSize: this.axisConfig.titleFontSize,
rotation: 0,
verticalPos: 'center',
horizontalPos: 'bottom',
},
],
});
}
return drawableElement;
}

View File

@ -1,8 +1,8 @@
import { ScaleLinear, scaleLinear } from 'd3';
import { AxisConfig, Dimension } from '../../Interfaces.js';
import { log } from '../../../../../logger.js';
import { AxisConfig } from '../../Interfaces.js';
import { ITextDimensionCalculator } from '../../TextDimensionCalculator.js';
import { BaseAxis } from './BaseAxis.js';
import { log } from '../../../../../logger.js';
export class LinearAxis extends BaseAxis {
private scale: ScaleLinear<number, number>;
@ -24,10 +24,11 @@ export class LinearAxis extends BaseAxis {
}
recalculateScale(): void {
const domain = [...this.domain]; // copy the array so if reverse is called twise it shouldnot cancel the reverse effect
if (this.axisPosition === 'left') {
this.domain.reverse(); // since yaxis in svg start from top
domain.reverse(); // since yaxis in svg start from top
}
this.scale = scaleLinear().domain(this.domain).range(this.getRange());
this.scale = scaleLinear().domain(domain).range(this.getRange());
log.trace('Linear axis final domain, range: ', this.domain, this.getRange());
}

View File

@ -19,7 +19,7 @@ export class LinePlot {
}
return [
{
groupText: 'line-plot',
groupTexts: ['plot', 'line-plot'],
type: 'path',
data: [
{

View File

@ -0,0 +1,21 @@
import { BoundingRect, DrawableElem } from '../../Interfaces.js';
export class PlotBorder {
constructor(private boundingRect: BoundingRect) {}
getDrawableElement(): DrawableElem[] {
const {x, y, width, height} = this.boundingRect;
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}`,
strokeFill: '#000000',
strokeWidth: 1,
},
],
},
];
}
}

View File

@ -11,6 +11,7 @@ import {
import { IAxis } from '../axis/index.js';
import { ChartComponent } from './../Interfaces.js';
import { LinePlot } from './LinePlot.js';
import { PlotBorder } from './PlotBorder.js';
export interface IPlot extends ChartComponent {
@ -59,7 +60,9 @@ export class Plot implements IPlot {
if(!(this.xAxis && this.yAxis)) {
throw Error("Axes must be passed to render Plots");
}
const drawableElem: DrawableElem[] = [];
const drawableElem: DrawableElem[] = [
...new PlotBorder(this.boundingRect).getDrawableElement()
];
for(const plot of this.chartData.plots) {
switch(plot.type) {
case ChartPlotEnum.LINE: {

View File

@ -23,25 +23,34 @@ export class XYChartBuilder {
titleFill: '#000000',
titlePadding: 5,
showtitle: true,
plotBorderWidth: 2,
yAxis: {
showLabel: true,
labelFontSize: 14,
lablePadding: 5,
labelColor: '#000000',
labelFill: '#000000',
showTitle: true,
titleFontSize: 16,
titlePadding: 5,
titleColor: '#000000',
titleFill: '#000000',
showTick: true,
tickLength: 5,
tickWidth: 2,
tickFill: '#000000',
},
xAxis: {
showLabel: true,
labelFontSize: 14,
lablePadding: 5,
labelColor: '#000000',
labelFill: '#000000',
showTitle: true,
titleFontSize: 16,
titlePadding: 5,
titleColor: '#000000',
titleFill: '#000000',
showTick: true,
tickLength: 5,
tickWidth: 2,
tickFill: '#000000',
},
chartOrientation: OrientationEnum.HORIZONTAL,
plotReservedSpacePercent: 50,

View File

@ -1,13 +1,13 @@
import { select } from 'd3';
import { select, Selection } from 'd3';
import { Diagram } from '../../Diagram.js';
import * as configApi from '../../config.js';
import { log } from '../../logger.js';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import {
DrawableElem,
TextElem,
TextHorizontalPos,
TextVerticalPos,
DrawableElem,
TextElem,
TextHorizontalPos,
TextVerticalPos,
} from './chartBuilder/Interfaces.js';
export const draw = (txt: string, id: string, _version: string, diagObj: Diagram) => {
@ -54,6 +54,25 @@ export const draw = (txt: string, id: string, _version: string, diagObj: Diagram
// @ts-ignore: TODO Fix ts errors
const shapes: DrawableElem[] = diagObj.db.getDrawableElem();
const groups: Record<string, any> = {};
function getGroup(gList: string[]) {
let elem = group;
let prefix = '';
for (let i = 0; i < gList.length; i++) {
let parent = group;
if (i > 0 && groups[prefix]) {
parent = groups[prefix];
}
prefix += gList[i];
elem = groups[prefix];
if (!elem) {
elem = groups[prefix] = parent.append('g').attr('class', gList[i]);
}
}
return elem;
}
for (const shape of shapes) {
if (shape.data.length === 0) {
log.trace(
@ -69,7 +88,7 @@ export const draw = (txt: string, id: string, _version: string, diagObj: Diagram
`Drawing shape of type: ${shape.type} with data: ${JSON.stringify(shape.data, null, 2)}`
);
const shapeGroup = group.append('g').attr('class', shape.groupText);
const shapeGroup = getGroup(shape.groupTexts);
switch (shape.type) {
case 'rect':