mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-01-14 06:43:25 +08:00
Added support for horizontal drawing
This commit is contained in:
parent
547a22edef
commit
0a731e1ee1
@ -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;
|
||||
|
@ -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[] = [];
|
||||
|
@ -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;
|
||||
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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;
|
||||
|
@ -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],
|
||||
|
Loading…
x
Reference in New Issue
Block a user