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 @@

XY Charts demos

bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000] line [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000] +

XY Charts demos with negative and stacked

+
+    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]
+    
+

XY Charts demos with negative and stacked horizontal

+
+    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]
+    

XY Charts horizontal

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();