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

View File

@@ -67,6 +67,15 @@ export class Orchestrator {
chartHeight += availableHeight; chartHeight += availableHeight;
availableHeight = 0; 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( log.trace(
`Final chart dimansion: x = ${chartX}, y = ${chartY}, width = ${chartWidth}, height = ${chartHeight}` `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[] = []; const drawableElem: DrawableElem[] = [];
if (this.boundingRect.height > 0 && this.boundingRect.width > 0) { if (this.boundingRect.height > 0 && this.boundingRect.width > 0) {
drawableElem.push({ drawableElem.push({
groupText: 'chart-title', groupTexts: ['chart-title'],
type: 'text', type: 'text',
data: [ data: [
{ {

View File

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

View File

@@ -1,8 +1,8 @@
import { ScaleLinear, scaleLinear } from 'd3'; 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 { ITextDimensionCalculator } from '../../TextDimensionCalculator.js';
import { BaseAxis } from './BaseAxis.js'; import { BaseAxis } from './BaseAxis.js';
import { log } from '../../../../../logger.js';
export class LinearAxis extends BaseAxis { export class LinearAxis extends BaseAxis {
private scale: ScaleLinear<number, number>; private scale: ScaleLinear<number, number>;
@@ -24,10 +24,11 @@ export class LinearAxis extends BaseAxis {
} }
recalculateScale(): void { 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') { 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()); log.trace('Linear axis final domain, range: ', this.domain, this.getRange());
} }

View File

@@ -19,7 +19,7 @@ export class LinePlot {
} }
return [ return [
{ {
groupText: 'line-plot', groupTexts: ['plot', 'line-plot'],
type: 'path', type: 'path',
data: [ 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 { IAxis } from '../axis/index.js';
import { ChartComponent } from './../Interfaces.js'; import { ChartComponent } from './../Interfaces.js';
import { LinePlot } from './LinePlot.js'; import { LinePlot } from './LinePlot.js';
import { PlotBorder } from './PlotBorder.js';
export interface IPlot extends ChartComponent { export interface IPlot extends ChartComponent {
@@ -59,7 +60,9 @@ export class Plot implements IPlot {
if(!(this.xAxis && this.yAxis)) { if(!(this.xAxis && this.yAxis)) {
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()
];
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: {

View File

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

View File

@@ -1,13 +1,13 @@
import { select } from 'd3'; import { select, Selection } from 'd3';
import { Diagram } from '../../Diagram.js'; import { Diagram } from '../../Diagram.js';
import * as configApi from '../../config.js'; import * as configApi from '../../config.js';
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import { configureSvgSize } from '../../setupGraphViewbox.js'; import { configureSvgSize } from '../../setupGraphViewbox.js';
import { import {
DrawableElem, DrawableElem,
TextElem, TextElem,
TextHorizontalPos, TextHorizontalPos,
TextVerticalPos, TextVerticalPos,
} from './chartBuilder/Interfaces.js'; } from './chartBuilder/Interfaces.js';
export const draw = (txt: string, id: string, _version: string, diagObj: Diagram) => { 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 // @ts-ignore: TODO Fix ts errors
const shapes: DrawableElem[] = diagObj.db.getDrawableElem(); 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) { for (const shape of shapes) {
if (shape.data.length === 0) { if (shape.data.length === 0) {
log.trace( 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)}` `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) { switch (shape.type) {
case 'rect': case 'rect':