diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/Interfaces.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/Interfaces.ts index 569a5d11c..51350ca25 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/Interfaces.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/Interfaces.ts @@ -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[]; }; diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/Orchestrator.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/Orchestrator.ts index fdd60d634..dce2cefe9 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/Orchestrator.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/Orchestrator.ts @@ -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}` diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/ChartTitle.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/ChartTitle.ts index 51e27b0cd..f02d7b05f 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/ChartTitle.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/ChartTitle.ts @@ -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: [ { diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/BaseAxis.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/BaseAxis.ts index 14107da52..f6ed16caf 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/BaseAxis.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/BaseAxis.ts @@ -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; } diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/LinearAxis.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/LinearAxis.ts index 9bf7c33b0..7d46a46b0 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/LinearAxis.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/LinearAxis.ts @@ -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; @@ -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()); } diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/plot/LinePlot.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/plot/LinePlot.ts index b60fea905..c6bb8e408 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/plot/LinePlot.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/plot/LinePlot.ts @@ -19,7 +19,7 @@ export class LinePlot { } return [ { - groupText: 'line-plot', + groupTexts: ['plot', 'line-plot'], type: 'path', data: [ { diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/plot/PlotBorder.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/plot/PlotBorder.ts new file mode 100644 index 000000000..d3eaf118d --- /dev/null +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/plot/PlotBorder.ts @@ -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, + }, + ], + }, + ]; + } +} diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/plot/index.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/plot/index.ts index 5af28127d..fb91a053a 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/plot/index.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/plot/index.ts @@ -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: { diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/index.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/index.ts index a47074e73..628965d44 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/index.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/index.ts @@ -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, diff --git a/packages/mermaid/src/diagrams/xychart/xychartRenderer.ts b/packages/mermaid/src/diagrams/xychart/xychartRenderer.ts index a871177ce..dd9b27583 100644 --- a/packages/mermaid/src/diagrams/xychart/xychartRenderer.ts +++ b/packages/mermaid/src/diagrams/xychart/xychartRenderer.ts @@ -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 = {}; + + 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':