diff --git a/.cspell/mermaid-terms.txt b/.cspell/mermaid-terms.txt index 3fa5eff269..864988f42c 100644 --- a/.cspell/mermaid-terms.txt +++ b/.cspell/mermaid-terms.txt @@ -5,6 +5,7 @@ braintree catmull compositTitleSize doublecircle +drawables elems gantt gitgraph diff --git a/demos/xychart.html b/demos/xychart.html index 70d3d845ae..56d0b4ba52 100644 --- a/demos/xychart.html +++ b/demos/xychart.html @@ -23,6 +23,24 @@
+ xychart-beta + title "Sales Revenue (in $)" + x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec] + bar [5000, 6000, 7500, 8200, -500, 10500, 11000, 10200, 9200, 8500, 7000, 6000] + bar [3000, 2000, 500, 1200, -3400, 3500, 1000, 8200, 2300, 9900, 9000, 3000] + line [5000, 6000, 7500, 8200, -200, 10500, 11000, 10200, 9200, 8500, 7000, 6000] ++
+ xychart-beta horizontal + title "Sales Revenue (in $)" + x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec] + bar [5000, 6000, 7500, 8200, -500, 10500, 11000, 10200, 9200, 8500, 7000, 6000] + bar [3000, 2000, 500, 1200, -3400, 3500, 1000, 8200, 2300, 9900, 9000, 3000] + line [5000, 6000, 7500, 8200, -200, 10500, 11000, 10200, 9200, 8500, 7000, 6000] +
diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/bandAxis.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/bandAxis.ts index 864ef1316e..63fe86e203 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/bandAxis.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/bandAxis.ts @@ -21,6 +21,10 @@ export class BandAxis extends BaseAxis { this.scale = scaleBand().domain(this.categories).range(this.getRange()); } + isZeroBasedDomain(): boolean { + return true; + } + setRange(range: [number, number]): void { super.setRange(range); } 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 c3240a4a7b..f299bb77c1 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/baseAxis.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/baseAxis.ts @@ -54,6 +54,8 @@ export abstract class BaseAxis implements Axis { this.setRange(this.range); } + abstract isZeroBasedDomain(): boolean; + abstract getScaleValue(value: number | string): number; abstract recalculateScale(): void; diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/index.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/index.ts index cde0d6a93c..086c193087 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/index.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/index.ts @@ -19,6 +19,7 @@ export interface Axis extends ChartComponent { getTickDistance(): number; recalculateOuterPaddingToDrawBar(): void; setRange(range: [number, number]): void; + isZeroBasedDomain(): boolean; } export function getAxis( 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 8107732d93..f0543c6930 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/linearAxis.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/linearAxis.ts @@ -20,6 +20,9 @@ export class LinearAxis extends BaseAxis { this.scale = scaleLinear().domain(this.domain).range(this.getRange()); } + isZeroBasedDomain(): boolean { + return this.domain[0] < 0; + } getTickValues(): (string | number)[] { return this.scale.ticks(); } diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/plot/barPlot.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/plot/barPlot.ts index 95ffcf1958..c11ca7710b 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/plot/barPlot.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/plot/barPlot.ts @@ -3,7 +3,7 @@ import type { Axis } from '../axis/index.js'; export class BarPlot { constructor( - private barData: BarPlotData[], + private barDataArr: BarPlotData[], private boundingRect: BoundingRect, private xAxis: Axis, private yAxis: Axis, @@ -12,85 +12,83 @@ export class BarPlot { ) {} getDrawableElement(): DrawableElem[] { - const offset = new Array(this.barData[0].data.length).fill(0); - const enlarge = new Array(this.barData[0].data.length).fill(0); - return this.barData.map((barData, dataIndex) => { - const finalData: [number, number][] = barData.data.map((d) => [ - this.xAxis.getScaleValue(d[0]), - this.yAxis.getScaleValue(d[1]), - ]); + const result: DrawableElem[] = []; + this.barDataArr.reduce<{ positiveBase: number[]; negativeBase: number[] }>( + (acc, barData, dataIndex) => { + const barPaddingPercent = 0.05; - const barPaddingPercent = 0.05; + const barWidth = + Math.min(this.xAxis.getAxisOuterPadding() * 2, this.xAxis.getTickDistance()) * + (1 - barPaddingPercent); + const barWidthHalf = barWidth / 2; - const barWidth = - Math.min(this.xAxis.getAxisOuterPadding() * 2, this.xAxis.getTickDistance()) * - (1 - barPaddingPercent); - const barWidthHalf = barWidth / 2; - - if (this.orientation === 'horizontal') { - return { - groupTexts: ['plot', `bar-plot-${this.plotIndex}-${dataIndex}`], - type: 'rect', - data: finalData.map((data, index) => { - const adjustForAxisOuterPadding = dataIndex > 0 ? this.yAxis.getAxisOuterPadding() : 0; - let x = offset[index] + this.boundingRect.x; - let width = data[1] - this.boundingRect.x - adjustForAxisOuterPadding; - if (enlarge[index] > 0) { - x -= enlarge[index]; - width += enlarge[index]; - enlarge[index] = 0; - offset[index] -= adjustForAxisOuterPadding; - } - offset[index] += width; - if (barData.data[index][1] === 0 && enlarge[index] === 0) { - enlarge[index] = width; - } - if (barData.data[index][1] === 0) { - width = 0; - } - return { - x, - y: data[0] - barWidthHalf, - height: barWidth, - width, - fill: barData.fill, - strokeWidth: 0, - strokeFill: barData.fill, - }; - }), - }; - } - return { - groupTexts: ['plot', `bar-plot-${this.plotIndex}-${dataIndex}`], - type: 'rect', - data: finalData.map((data, index) => { - const adjustForAxisOuterPadding = dataIndex > 0 ? this.yAxis.getAxisOuterPadding() : 0; - const y = data[1] - offset[index] + adjustForAxisOuterPadding; - let height = - this.boundingRect.y + this.boundingRect.height - data[1] - adjustForAxisOuterPadding; - if (enlarge[index] > 0) { - height += enlarge[index]; - enlarge[index] = 0; - offset[index] -= adjustForAxisOuterPadding; - } - offset[index] += height; - if (barData.data[index][1] === 0 && enlarge[index] === 0) { - enlarge[index] = height; - } - if (barData.data[index][1] === 0) { - height = 0; - } - return { - x: data[0] - barWidthHalf, - y, - width: barWidth, - height, - fill: barData.fill, - strokeWidth: 0, - strokeFill: barData.fill, - }; - }), - }; - }); + if (this.orientation === 'horizontal') { + result.push({ + groupTexts: ['plot', `bar-plot-${this.plotIndex}-${dataIndex}`], + type: 'rect', + data: barData.data.map((data, i) => { + const scaledX = this.xAxis.getScaleValue(data[0]); + const scaledY = this.yAxis.getScaleValue(data[1]); + const basePoint = this.yAxis.isZeroBasedDomain() + ? this.yAxis.getScaleValue(0) + : this.boundingRect.x; + const width = Math.abs(basePoint - scaledY); + let widthAdjusted = 0; + const isPositive = data[1] >= 0; + if (isPositive) { + widthAdjusted = acc.positiveBase[i] || 0; + acc.positiveBase[i] = widthAdjusted + width; + } else { + widthAdjusted = acc.negativeBase[i] || 0; + acc.negativeBase[i] = widthAdjusted + width; + } + return { + x: isPositive ? basePoint + widthAdjusted : basePoint - widthAdjusted - width, + y: scaledX - barWidthHalf, + height: barWidth, + width, + fill: barData.fill, + strokeWidth: 0, + strokeFill: barData.fill, + }; + }), + }); + } else { + result.push({ + groupTexts: ['plot', `bar-plot-${this.plotIndex}-${dataIndex}`], + type: 'rect', + data: barData.data.map((data, i) => { + const scaledX = this.xAxis.getScaleValue(data[0]); + const scaledY = this.yAxis.getScaleValue(data[1]); + const basePoint = this.yAxis.isZeroBasedDomain() + ? this.yAxis.getScaleValue(0) + : this.boundingRect.y + this.boundingRect.height; + const height = Math.abs(basePoint - scaledY); + let heightAdjusted = 0; + const isPositive = data[1] >= 0; + if (isPositive) { + heightAdjusted = acc.positiveBase[i] || 0; + acc.positiveBase[i] = heightAdjusted + height; + } else { + heightAdjusted = acc.negativeBase[i] || 0; + acc.negativeBase[i] = heightAdjusted + height; + } + return { + x: scaledX - barWidthHalf, + y: isPositive ? scaledY - heightAdjusted : basePoint + heightAdjusted, + width: barWidth, + height, + fill: barData.fill, + strokeWidth: 0, + strokeFill: barData.fill, + }; + }), + }); + } + return acc; + }, + { positiveBase: [], negativeBase: [] } + ); + return result; } } 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 4f0e3d0023..645bd8ee74 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/plot/index.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/plot/index.ts @@ -66,16 +66,6 @@ export class BasePlot implements Plot { ) as BarPlotData[]; let plotIndex = 0; - if (linePlots.length) { - const linePlot = new LinePlot( - linePlots, - this.xAxis, - this.yAxis, - this.chartConfig.chartOrientation, - plotIndex - ); - drawableElem.push(...linePlot.getDrawableElement()); - } if (barPlots.length) { const barPlot = new BarPlot( barPlots, @@ -88,6 +78,16 @@ export class BasePlot implements Plot { drawableElem.push(...barPlot.getDrawableElement()); plotIndex++; } + if (linePlots.length) { + const linePlot = new LinePlot( + linePlots, + this.xAxis, + this.yAxis, + this.chartConfig.chartOrientation, + plotIndex + ); + drawableElem.push(...linePlot.getDrawableElement()); + } return drawableElem; } } diff --git a/packages/mermaid/src/diagrams/xychart/xychartDb.ts b/packages/mermaid/src/diagrams/xychart/xychartDb.ts index c73cb6c8e2..4d2c1d19f5 100644 --- a/packages/mermaid/src/diagrams/xychart/xychartDb.ts +++ b/packages/mermaid/src/diagrams/xychart/xychartDb.ts @@ -28,6 +28,8 @@ let plotIndex = 0; let tmpSVGGroup: Group; +let barPlotMaxData: number[] = []; + let xyChartConfig: XYChartConfig = getChartDefaultConfig(); let xyChartThemeConfig: XYChartThemeConfig = getChartDefaultThemeConfig(); let xyChartData: XYChartData = getChartDefaultData(); @@ -60,7 +62,7 @@ function getChartDefaultData(): XYChartData { yAxis: { type: 'linear', title: '', - min: 0, + min: Infinity, max: -Infinity, }, xAxis: { @@ -113,21 +115,27 @@ function setYAxisRangeData(min: number, max: number) { // this function does not set `hasSetYAxis` as there can be multiple data so we should calculate the range accordingly function setYAxisRangeFromPlotData(data: number[], plotType: PlotType) { - const sum = new Array(data.length).fill(0); + let minValue = 0; + let maxValue = -Infinity; if (plotType === PlotType.BAR) { - dataSets.push(data); - for (let i = 0; i < data.length; i++) { - for (const entry of dataSets) { - sum[i] += entry[i]; - } + let i = 0; + for (const d of data) { + barPlotMaxData[i] = (barPlotMaxData[i] || 0) + d; + maxValue = Math.max(...barPlotMaxData); + minValue = Math.min(...barPlotMaxData); + i++; } + } else { + maxValue = Math.max(...data); + minValue = Math.min(...data); } - + const prevMinValue = isLinearAxisData(xyChartData.yAxis) ? xyChartData.yAxis.min : Infinity; + const prevMaxValue = isLinearAxisData(xyChartData.yAxis) ? xyChartData.yAxis.max : -Infinity; xyChartData.yAxis = { type: 'linear', title: xyChartData.yAxis.title, - min: isLinearAxisData(xyChartData.yAxis) ? xyChartData.yAxis.min : Math.min(...sum), - max: Math.max(...sum), + min: Math.min(prevMinValue, minValue), + max: Math.max(prevMaxValue, maxValue), }; } @@ -207,6 +215,7 @@ function getChartConfig() { const clear = function () { commonClear(); plotIndex = 0; + barPlotMaxData = []; xyChartConfig = getChartDefaultConfig(); xyChartData = getChartDefaultData(); xyChartThemeConfig = getChartDefaultThemeConfig();