Merge branch 'develop' into patch-1

This commit is contained in:
Nikolay Rozhkov 2023-10-06 12:42:56 +03:00 committed by GitHub
commit f346c3f511
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 7191 additions and 3633 deletions

View File

@ -18,6 +18,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
'er',
'pie',
'quadrantChart',
'xyChart',
'requirement',
'mindmap',
'timeline',

View File

@ -156,6 +156,7 @@
"vitepress",
"vueuse",
"xlink",
"xychart",
"yash",
"yokozuna",
"zenuml",

View File

@ -520,7 +520,15 @@ describe('Gantt diagram', () => {
);
});
it('should render a gantt diagram with very large intervals, skipping excludes if interval > 5 years', () => {
// TODO: fix it
//
// This test is skipped deliberately
// because it fails and blocks our development pipeline
// It was added as an attempt to fix gantt performance issues
//
// https://github.com/mermaid-js/mermaid/issues/3274
//
it.skip('should render a gantt diagram with very large intervals, skipping excludes if interval > 5 years', () => {
imgSnapshotTest(
`gantt
title A long Gantt Diagram
@ -528,7 +536,6 @@ describe('Gantt diagram', () => {
axisFormat %m-%d
tickInterval 1day
excludes weekends
section Section
A task : a1, 9999-10-01, 30d
Another task : after a1, 20d

View File

@ -0,0 +1,322 @@
import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
describe('XY Chart', () => {
it('should render the simplest possible chart', () => {
imgSnapshotTest(
`
xychart-beta
line [10, 30, 20]
`,
{}
);
cy.get('svg');
});
it('Should render a complete chart', () => {
imgSnapshotTest(
`
xychart-beta
title "Sales Revenue"
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
`,
{}
);
});
it('Should render a chart without title', () => {
imgSnapshotTest(
`
xychart-beta
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
`,
{}
);
cy.get('svg');
});
it('y-axis title not required', () => {
imgSnapshotTest(
`
xychart-beta
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis 4000 --> 11000
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]
`,
{}
);
cy.get('svg');
});
it('Should render a chart without y-axis with different range', () => {
imgSnapshotTest(
`
xychart-beta
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
bar [5000, 6000, 7500, 8200, 9500, 10500, 14000, 3200, 9200, 9900, 3400, 6000]
line [2000, 7000, 6500, 9200, 9500, 7500, 11000, 10200, 3200, 8500, 7000, 8800]
`,
{}
);
cy.get('svg');
});
it('x axis title not required', () => {
imgSnapshotTest(
`
xychart-beta
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
bar [5000, 6000, 7500, 8200, 9500, 10500, 14000, 3200, 9200, 9900, 3400, 6000]
line [2000, 7000, 6500, 9200, 9500, 7500, 11000, 10200, 3200, 8500, 7000, 8800]
`,
{}
);
cy.get('svg');
});
it('Multiple plots can be rendered', () => {
imgSnapshotTest(
`
xychart-beta
line [23, 46, 77, 34]
line [45, 32, 33, 12]
bar [87, 54, 99, 85]
line [78, 88, 22, 4]
line [22, 29, 75, 33]
bar [52, 96, 35, 10]
`,
{}
);
cy.get('svg');
});
it('Decimals and negative numbers are supported', () => {
imgSnapshotTest(
`
xychart-beta
y-axis -2.4 --> 3.5
line [+1.3, .6, 2.4, -.34]
`,
{}
);
cy.get('svg');
});
it('Render spark line with "plotReservedSpacePercent"', () => {
imgSnapshotTest(
`
---
config:
theme: dark
xyChart:
width: 200
height: 20
plotReservedSpacePercent: 100
---
xychart-beta
line [5000, 9000, 7500, 6200, 9500, 5500, 11000, 8200, 9200, 9500, 7000, 8800]
`,
{}
);
cy.get('svg');
});
it('Render spark bar without displaying other property', () => {
imgSnapshotTest(
`
---
config:
theme: dark
xyChart:
width: 200
height: 20
xAxis:
showLabel: false
showTitle: false
showTick: false
showAxisLine: false
yAxis:
showLabel: false
showTitle: false
showTick: false
showAxisLine: false
---
xychart-beta
bar [5000, 9000, 7500, 6200, 9500, 5500, 11000, 8200, 9200, 9500, 7000, 8800]
`,
{}
);
cy.get('svg');
});
it('Should use all the config from directive', () => {
imgSnapshotTest(
`
%%{init: {"xyChart": {"width": 1000, "height": 600, "titlePadding": 5, "titleFontSize": 10, "xAxis": {"labelFontSize": "20", "labelPadding": 10, "titleFontSize": 30, "titlePadding": 20, "tickLength": 10, "tickWidth": 5}, "yAxis": {"labelFontSize": "20", "labelPadding": 10, "titleFontSize": 30, "titlePadding": 20, "tickLength": 10, "tickWidth": 5}, "plotBorderWidth": 5, "chartOrientation": "horizontal", "plotReservedSpacePercent": 60 }}}%%
xychart-beta
title "Sales Revenue"
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
`,
{}
);
cy.get('svg');
});
it('Should use all the config from yaml', () => {
imgSnapshotTest(
`
---
config:
theme: forest
xyChart:
width: 1000
height: 600
titlePadding: 5
titleFontSize: 10
xAxis:
labelFontSize: 20
labelPadding: 10
titleFontSize: 30
titlePadding: 20
tickLength: 10
tickWidth: 5
axisLineWidth: 5
yAxis:
labelFontSize: 20
labelPadding: 10
titleFontSize: 30
titlePadding: 20
tickLength: 10
tickWidth: 5
axisLineWidth: 5
chartOrientation: horizontal
plotReservedSpacePercent: 60
---
xychart-beta
title "Sales Revenue"
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
`,
{}
);
cy.get('svg');
});
it('Render with show axis title false', () => {
imgSnapshotTest(
`
---
config:
xyChart:
xAxis:
showTitle: false
yAxis:
showTitle: false
---
xychart-beta
title "Sales Revenue"
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
`,
{}
);
cy.get('svg');
});
it('Render with show axis label false', () => {
imgSnapshotTest(
`
---
config:
xyChart:
xAxis:
showLabel: false
yAxis:
showLabel: false
---
xychart-beta
title "Sales Revenue"
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
`,
{}
);
cy.get('svg');
});
it('Render with show axis tick false', () => {
imgSnapshotTest(
`
---
config:
xyChart:
xAxis:
showTick: false
yAxis:
showTick: false
---
xychart-beta
title "Sales Revenue"
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
`,
{}
);
cy.get('svg');
});
it('Render with show axis line false', () => {
imgSnapshotTest(
`
---
config:
xyChart:
xAxis:
showAxisLine: false
yAxis:
showAxisLine: false
---
xychart-beta
title "Sales Revenue"
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
`,
{}
);
cy.get('svg');
});
it('Render all the theme color', () => {
imgSnapshotTest(
`
---
config:
themeVariables:
xyChart:
titleColor: "#ff0000"
backgroundColor: "#f0f8ff"
yAxisLabelColor: "#ee82ee"
yAxisTitleColor: "#7fffd4"
yAxisTickColor: "#87ceeb"
yAxisLineColor: "#ff6347"
xAxisLabelColor: "#7fffd4"
xAxisTitleColor: "#ee82ee"
xAxisTickColor: "#ff6347"
xAxisLineColor: "#87ceeb"
plotColorPalette: "#008000, #faba63"
---
xychart-beta
title "Sales Revenue"
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
`,
{}
);
cy.get('svg');
});
});

View File

@ -60,6 +60,9 @@
<li>
<h2><a href="./quadrantchart.html">Quadrant charts</a></h2>
</li>
<li>
<h2><a href="./xychart.html">XY charts</a></h2>
</li>
<li>
<h2><a href="./requirements.html">Requirements</a></h2>
</li>

184
demos/xychart.html Normal file
View File

@ -0,0 +1,184 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Mermaid Quick Test Page</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
<style>
div.mermaid {
/* font-family: 'trebuchet ms', verdana, arial; */
font-family: 'Courier New', Courier, monospace !important;
}
</style>
</head>
<body>
<h1>XY Charts demos</h1>
<pre class="mermaid">
xychart-beta
title "Sales Revenue (in $)"
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
</pre>
<hr />
<h1>XY Charts horizontal</h1>
<pre class="mermaid">
xychart-beta horizontal
title "Basic xychart"
x-axis "this is x axis" [category1, "category 2", category3, category4]
y-axis yaxisText 10 --> 150
bar "sample bat" [52, 96, 35, 10]
line [23, 46, 75, 43]
</pre>
<hr />
<h1>XY Charts only lines and bar</h1>
<pre class="mermaid">
xychart-beta
line [23, 46, 77, 34]
line [45, 32, 33, 12]
line [87, 54, 99, 85]
line [78, 88, 22, 4]
line [22, 29, 75, 33]
bar [52, 96, 35, 10]
</pre>
<hr />
<h1>XY Charts with +ve and -ve numbers</h1>
<pre class="mermaid">
xychart-beta
line [+1.3, .6, 2.4, -.34]
</pre>
<h1>XY Charts Bar with multiple category</h1>
<pre class="mermaid">
xychart-beta
title "Basic xychart with many categories"
x-axis "this is x axis" [category1, "category 2", category3, category4, category5, category6, category7]
y-axis yaxisText 10 --> 150
bar "sample bar" [52, 96, 35, 10, 87, 34, 67, 99]
</pre>
<h1>XY Charts line with multiple category</h1>
<pre class="mermaid">
xychart-beta
title "Line chart with many category"
x-axis "this is x axis" [category1, "category 2", category3, category4, category5, category6, category7]
y-axis yaxisText 10 --> 150
line "sample line" [52, 96, 35, 10, 87, 34, 67, 99]
</pre>
<h1>XY Charts category with large text</h1>
<pre class="mermaid">
xychart-beta
title "Basic xychart with many categories with category overlap"
x-axis "this is x axis" [category1, "Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.", category3, category4, category5, category6, category7]
y-axis yaxisText 10 --> 150
bar "sample bar" [52, 96, 35, 10, 87, 34, 67, 99]
</pre>
<h1>sparkline demo</h1>
<pre class="mermaid">
---
config:
theme: dark
xyChart:
width: 200
height: 20
plotReservedSpacePercent: 100
---
xychart-beta
line [5000, 9000, 7500, 6200, 9500, 5500, 11000, 8200, 9200, 9500, 7000, 8800]
</pre>
<h1>sparkBar demo</h1>
<pre class="mermaid">
---
config:
theme: dark
xyChart:
width: 200
height: 20
plotReservedSpacePercent: 100
---
xychart-beta
bar [5000, 9000, 7500, 6200, 9500, 5500, 11000, 8200, 9200, 9500, 7000, 8800]
</pre>
<h1>XY Charts demos with all configs</h1>
<pre class="mermaid">
---
config:
theme: forest
xyChart:
width: 1000
height: 600
titlePadding: 5
titleFontSize: 10
xAxis:
labelFontSize: 20
labelPadding: 10
titleFontSize: 30
titlePadding: 20
tickLength: 10
tickWidth: 5
axisLineWidth: 5
yAxis:
labelFontSize: 20
labelPadding: 10
titleFontSize: 30
titlePadding: 20
tickLength: 10
tickWidth: 5
axisLineWidth: 5
chartOrientation: horizontal
plotReservedSpacePercent: 60
---
xychart-beta
title "Sales Revene"
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
</pre>
<h1>XY Charts demos with all theme config</h1>
<pre class="mermaid">
---
config:
themeVariables:
xyChart:
titleColor: "#ff0000"
backgroundColor: "#f0f8ff"
yAxisLabelColor: "#ee82ee"
yAxisTitleColor: "#7fffd4"
yAxisTickColor: "#87ceeb"
yAxisLineColor: "#ff6347"
xAxisLabelColor: "#7fffd4"
xAxisTitleColor: "#ee82ee"
xAxisTickColor: "#ff6347"
xAxisLineColor: "#87ceeb"
plotColorPalette: "#008000, #faba63"
---
xychart-beta
title "Sales Revene"
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
</pre>
<hr />
<script type="module">
import mermaid from './mermaid.esm.mjs';
mermaid.initialize({
theme: 'default',
logLevel: 3,
securityLevel: 'loose',
});
</script>
</body>
</html>

View File

@ -14,7 +14,7 @@
#### Defined in
[defaultConfig.ts:268](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L268)
[defaultConfig.ts:272](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L272)
---

View File

@ -283,6 +283,26 @@ quadrantChart
Campaign F: [0.35, 0.78]
```
### [XY Chart](../syntax/xyChart.md)
```mermaid-example
xychart-beta
title "Sales Revenue"
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
```
```mermaid
xychart-beta
title "Sales Revenue"
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
```
## Installation
**In depth guides and examples can be found at [Getting Started](./getting-started.md) and [Usage](../config/usage.md).**

188
docs/syntax/xyChart.md Normal file
View File

@ -0,0 +1,188 @@
> **Warning**
>
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/syntax/xyChart.md](../../packages/mermaid/src/docs/syntax/xyChart.md).
# XY Chart
> In the context of mermaid-js, the XY chart is a comprehensive charting module that encompasses various types of charts that utilize both x-axis and y-axis for data representation. Presently, it includes two fundamental chart types: the bar chart and the line chart. These charts are designed to visually display and analyze data that involve two numerical variables.
> It's important to note that while the current implementation of mermaid-js includes these two chart types, the framework is designed to be dynamic and adaptable. Therefore, it has the capacity for expansion and the inclusion of additional chart types in the future. This means that users can expect an evolving suite of charting options within the XY chart module, catering to various data visualization needs as new chart types are introduced over time.
## Example
```mermaid-example
xychart-beta
title "Sales Revenue"
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
```
```mermaid
xychart-beta
title "Sales Revenue"
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
```
## Syntax
> **Note**
> All text values that contain only one word can be written without `"`. If a text value has many words in it, specifically if it contains spaces, enclose the value in `"`
### Orientations
The chart can be drawn horizontal or vertical, default value is vertical.
xychart-beta horizontal
...
### Title
The title is a short description of the chart and it will always render on top of the chart.
#### Example
xychart-beta
title "This is a simple example"
...
> **Note**
> If the title is a single word one no need to use `"`, but if it has space `"` is needed
### x-axis
The x-axis primarily serves as a categorical value, although it can also function as a numeric range value when needed.
#### Example
1. `x-axis title min --> max` x-axis will function as numeric with the given range
2. `x-axis "title with space" [cat1, "cat2 with space", cat3]` x-axis if categorical, categories are text type
### y-axis
The y-axis is employed to represent numerical range values, it cannot have categorical values.
#### Example
1. `y-axis title min --> max`
2. `y-axis title` it will only add the title, the range will be auto generated from data.
> **Note**
> Both x and y axis are optional if not provided we will try to create the range
### Line chart
A line chart offers the capability to graphically depict lines.
#### Example
1. `line [2.3, 45, .98, -3.4]` it can have all valid numeric values.
### Bar chart
A bar chart offers the capability to graphically depict bars.
#### Example
1. `bar [2.3, 45, .98, -3.4]` it can have all valid numeric values.
#### Simplest example
The only two things required are the chart name (`xychart-beta`) and one data set. So you will be able to draw a chart with a simple config like
xychart-beta
line [+1.3, .6, 2.4, -.34]
## Chart Configurations
| Parameter | Description | Default value |
| ------------------------ | ---------------------------------------------- | :-----------: |
| width | Width of the chart | 700 |
| height | Height of the chart | 500 |
| titlePadding | Top and Bottom padding of the title | 10 |
| titleFontSize | Title font size | 20 |
| showTitle | Title to be shown or not | true |
| xAxis | xAxis configuration | AxisConfig |
| yAxis | yAxis configuration | AxisConfig |
| chartOrientation | 'vertical' or 'horizontal' | 'vertical' |
| plotReservedSpacePercent | Minimum space plots will take inside the chart | 50 |
### AxisConfig
| Parameter | Description | Default value |
| ------------- | ------------------------------------ | :-----------: |
| showLabel | Show axis labels or tick values | true |
| labelFontSize | Font size of the label to be drawn | 14 |
| labelPadding | Top and Bottom padding of the label | 5 |
| showTitle | Axis title to be shown or not | true |
| titleFontSize | Axis title font size | 16 |
| titlePadding | Top and Bottom padding of Axis title | 5 |
| showTick | Tick to be shown or not | true |
| tickLength | How long the tick will be | 5 |
| tickWidth | How width the tick will be | 2 |
| showAxisLine | Axis line to be shown or not | true |
| axisLineWidth | Thickness of the axis line | 2 |
## Chart Theme Variables
> **Note**
> Themes for xychart resides inside xychart attribute so to set the variables use this syntax
> %%{init: { "themeVariables": {"xyChart": {"titleColor": "#ff0000"} } }}%%
| Parameter | Description |
| ---------------- | --------------------------------------------------------- |
| backgroundColor | Background color of the whole chart |
| titleColor | Color of the Title text |
| xAxisLableColor | Color of the x-axis labels |
| xAxisTitleColor | Color of the x-axis title |
| xAxisTickColor | Color of the x-axis tick |
| xAxisLineColor | Color of the x-axis line |
| yAxisLableColor | Color of the y-axis labels |
| yAxisTitleColor | Color of the y-axis title |
| yAxisTickColor | Color of the y-axis tick |
| yAxisLineColor | Color of the y-axis line |
| plotColorPalette | String of colors separated by comma e.g. "#f3456, #43445" |
## Example on config and theme
```mermaid-example
---
config:
xyChart:
width: 900
height: 600
themeVariables:
xyChart:
titleColor: "#ff0000"
---
xychart-beta
title "Sales Revenue"
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
```
```mermaid
---
config:
xyChart:
width: 900
height: 600
themeVariables:
xyChart:
titleColor: "#ff0000"
---
xychart-beta
title "Sales Revenue"
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
```

View File

@ -47,6 +47,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
'er',
'pie',
'quadrantChart',
'xyChart',
'requirement',
'mindmap',
'timeline',

View File

@ -150,6 +150,7 @@ export interface MermaidConfig {
er?: ErDiagramConfig;
pie?: PieDiagramConfig;
quadrantChart?: QuadrantChartConfig;
xyChart?: XYChartConfig;
requirement?: RequirementDiagramConfig;
mindmap?: MindmapDiagramConfig;
gitGraph?: GitGraphDiagramConfig;
@ -703,6 +704,194 @@ export interface QuadrantChartConfig extends BaseDiagramConfig {
*/
quadrantExternalBorderStrokeWidth?: number;
}
/**
* This object contains configuration for XYChart axis config
*
* This interface was referenced by `MermaidConfig`'s JSON-Schema
* via the `definition` "XYChartAxisConfig".
*/
export interface XYChartAxisConfig {
/**
* Should show the axis labels (tick text)
*/
showLabel?: boolean;
/**
* font size of the axis labels (tick text)
*/
labelFontSize?: number;
/**
* top and bottom space from axis label (tick text)
*/
labelPadding?: number;
/**
* Should show the axis title
*/
showTitle?: boolean;
/**
* font size of the axis title
*/
titleFontSize?: number;
/**
* top and bottom space from axis title
*/
titlePadding?: number;
/**
* Should show the axis tick lines
*/
showTick?: boolean;
/**
* length of the axis tick lines
*/
tickLength?: number;
/**
* width of the axis tick lines
*/
tickWidth?: number;
/**
* Show line across the axis
*/
showAxisLine?: boolean;
/**
* Width of the axis line
*/
axisLineWidth?: number;
}
/**
* This object contains configuration specific to XYCharts
*
* This interface was referenced by `MermaidConfig`'s JSON-Schema
* via the `definition` "XYChartConfig".
*/
export interface XYChartConfig extends BaseDiagramConfig {
/**
* width of the chart
*/
width?: number;
/**
* height of the chart
*/
height?: number;
/**
* Font size of the chart title
*/
titleFontSize?: number;
/**
* Top and bottom space from the chart title
*/
titlePadding?: number;
/**
* Should show the chart title
*/
showTitle?: boolean;
xAxis?: XYChartAxisConfig1;
yAxis?: XYChartAxisConfig2;
/**
* How to plot will be drawn horizontal or vertical
*/
chartOrientation?: 'vertical' | 'horizontal';
/**
* Minimum percent of space plots of the chart will take
*/
plotReservedSpacePercent?: number;
}
/**
* This object contains configuration for XYChart axis config
*/
export interface XYChartAxisConfig1 {
/**
* Should show the axis labels (tick text)
*/
showLabel?: boolean;
/**
* font size of the axis labels (tick text)
*/
labelFontSize?: number;
/**
* top and bottom space from axis label (tick text)
*/
labelPadding?: number;
/**
* Should show the axis title
*/
showTitle?: boolean;
/**
* font size of the axis title
*/
titleFontSize?: number;
/**
* top and bottom space from axis title
*/
titlePadding?: number;
/**
* Should show the axis tick lines
*/
showTick?: boolean;
/**
* length of the axis tick lines
*/
tickLength?: number;
/**
* width of the axis tick lines
*/
tickWidth?: number;
/**
* Show line across the axis
*/
showAxisLine?: boolean;
/**
* Width of the axis line
*/
axisLineWidth?: number;
}
/**
* This object contains configuration for XYChart axis config
*/
export interface XYChartAxisConfig2 {
/**
* Should show the axis labels (tick text)
*/
showLabel?: boolean;
/**
* font size of the axis labels (tick text)
*/
labelFontSize?: number;
/**
* top and bottom space from axis label (tick text)
*/
labelPadding?: number;
/**
* Should show the axis title
*/
showTitle?: boolean;
/**
* font size of the axis title
*/
titleFontSize?: number;
/**
* top and bottom space from axis title
*/
titlePadding?: number;
/**
* Should show the axis tick lines
*/
showTick?: boolean;
/**
* length of the axis tick lines
*/
tickLength?: number;
/**
* width of the axis tick lines
*/
tickWidth?: number;
/**
* Show line across the axis
*/
showAxisLine?: boolean;
/**
* Width of the axis line
*/
axisLineWidth?: number;
}
/**
* The object containing configurations specific for entity relationship diagrams
*

View File

@ -236,6 +236,10 @@ const config: RequiredDeep<MermaidConfig> = {
...defaultConfigJson.pie,
useWidth: 984,
},
xyChart: {
...defaultConfigJson.xyChart,
useWidth: undefined,
},
requirement: {
...defaultConfigJson.requirement,
useWidth: undefined,

View File

@ -7,6 +7,7 @@ import gantt from '../diagrams/gantt/ganttDetector.js';
import { info } from '../diagrams/info/infoDetector.js';
import { pie } from '../diagrams/pie/pieDetector.js';
import quadrantChart from '../diagrams/quadrant-chart/quadrantDetector.js';
import xychart from '../diagrams/xychart/xychartDetector.js';
import requirement from '../diagrams/requirement/requirementDetector.js';
import sequence from '../diagrams/sequence/sequenceDetector.js';
import classDiagram from '../diagrams/class/classDetector.js';
@ -85,6 +86,7 @@ export const addDiagrams = () => {
state,
journey,
quadrantChart,
sankey
sankey,
xychart
);
};

View File

@ -1,4 +1,3 @@
// @ts-ignore: TODO Fix ts errors
import { scaleLinear } from 'd3';
import { log } from '../../logger.js';
import type { BaseDiagramConfig, QuadrantChartConfig } from '../../config.type.js';

View File

@ -0,0 +1,45 @@
import type { ScaleBand } from 'd3';
import { scaleBand } from 'd3';
import { log } from '../../../../../logger.js';
import type { TextDimensionCalculator } from '../../textDimensionCalculator.js';
import { BaseAxis } from './baseAxis.js';
import type { XYChartAxisThemeConfig, XYChartAxisConfig } from '../../interfaces.js';
export class BandAxis extends BaseAxis {
private scale: ScaleBand<string>;
private categories: string[];
constructor(
axisConfig: XYChartAxisConfig,
axisThemeConfig: XYChartAxisThemeConfig,
categories: string[],
title: string,
textDimensionCalculator: TextDimensionCalculator
) {
super(axisConfig, title, textDimensionCalculator, axisThemeConfig);
this.categories = categories;
this.scale = scaleBand().domain(this.categories).range(this.getRange());
}
setRange(range: [number, number]): void {
super.setRange(range);
}
recalculateScale(): void {
this.scale = scaleBand()
.domain(this.categories)
.range(this.getRange())
.paddingInner(1)
.paddingOuter(0)
.align(0.5);
log.trace('BandAxis axis final categories, range: ', this.categories, this.getRange());
}
getTickValues(): (string | number)[] {
return this.categories;
}
getScaleValue(value: string): number {
return this.scale(value) || this.getRange()[0];
}
}

View File

@ -0,0 +1,422 @@
import type {
BoundingRect,
Dimension,
DrawableElem,
Point,
XYChartAxisConfig,
XYChartAxisThemeConfig,
} from '../../interfaces.js';
import type { TextDimensionCalculator } from '../../textDimensionCalculator.js';
import type { Axis, AxisPosition } from './index.js';
const BAR_WIDTH_TO_TICK_WIDTH_RATIO = 0.7;
const MAX_OUTER_PADDING_PERCENT_FOR_WRT_LABEL = 0.2;
export abstract class BaseAxis implements Axis {
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 showAxisLine = false;
protected outerPadding = 0;
protected titleTextHeight = 0;
protected labelTextHeight = 0;
constructor(
protected axisConfig: XYChartAxisConfig,
protected title: string,
protected textDimensionCalculator: TextDimensionCalculator,
protected axisThemeConfig: XYChartAxisThemeConfig
) {
this.range = [0, 10];
this.boundingRect = { x: 0, y: 0, width: 0, height: 0 };
this.axisPosition = 'left';
}
setRange(range: [number, number]): void {
this.range = range;
if (this.axisPosition === 'left' || this.axisPosition === 'right') {
this.boundingRect.height = range[1] - range[0];
} else {
this.boundingRect.width = range[1] - range[0];
}
this.recalculateScale();
}
getRange(): [number, number] {
return [this.range[0] + this.outerPadding, this.range[1] - this.outerPadding];
}
setAxisPosition(axisPosition: AxisPosition): void {
this.axisPosition = axisPosition;
this.setRange(this.range);
}
abstract getScaleValue(value: number | string): number;
abstract recalculateScale(): void;
abstract getTickValues(): Array<string | number>;
getTickDistance(): number {
const range = this.getRange();
return Math.abs(range[0] - range[1]) / this.getTickValues().length;
}
getAxisOuterPadding(): number {
return this.outerPadding;
}
private getLabelDimension(): Dimension {
return this.textDimensionCalculator.getMaxDimension(
this.getTickValues().map((tick) => tick.toString()),
this.axisConfig.labelFontSize
);
}
recalculateOuterPaddingToDrawBar(): void {
if (BAR_WIDTH_TO_TICK_WIDTH_RATIO * this.getTickDistance() > this.outerPadding * 2) {
this.outerPadding = Math.floor((BAR_WIDTH_TO_TICK_WIDTH_RATIO * this.getTickDistance()) / 2);
}
this.recalculateScale();
}
private calculateSpaceIfDrawnHorizontally(availableSpace: Dimension) {
let availableHeight = availableSpace.height;
if (this.axisConfig.showAxisLine && availableHeight > this.axisConfig.axisLineWidth) {
availableHeight -= this.axisConfig.axisLineWidth;
this.showAxisLine = true;
}
if (this.axisConfig.showLabel) {
const spaceRequired = this.getLabelDimension();
const maxPadding = MAX_OUTER_PADDING_PERCENT_FOR_WRT_LABEL * availableSpace.width;
this.outerPadding = Math.min(spaceRequired.width / 2, maxPadding);
const heightRequired = spaceRequired.height + this.axisConfig.labelPadding * 2;
this.labelTextHeight = spaceRequired.height;
if (heightRequired <= availableHeight) {
availableHeight -= heightRequired;
this.showLabel = true;
}
}
if (this.axisConfig.showTick && availableHeight >= this.axisConfig.tickLength) {
this.showTick = true;
availableHeight -= this.axisConfig.tickLength;
}
if (this.axisConfig.showTitle && this.title) {
const spaceRequired = this.textDimensionCalculator.getMaxDimension(
[this.title],
this.axisConfig.titleFontSize
);
const heightRequired = spaceRequired.height + this.axisConfig.titlePadding * 2;
this.titleTextHeight = spaceRequired.height;
if (heightRequired <= availableHeight) {
availableHeight -= heightRequired;
this.showTitle = true;
}
}
this.boundingRect.width = availableSpace.width;
this.boundingRect.height = availableSpace.height - availableHeight;
}
private calculateSpaceIfDrawnVertical(availableSpace: Dimension) {
let availableWidth = availableSpace.width;
if (this.axisConfig.showAxisLine && availableWidth > this.axisConfig.axisLineWidth) {
availableWidth -= this.axisConfig.axisLineWidth;
this.showAxisLine = true;
}
if (this.axisConfig.showLabel) {
const spaceRequired = this.getLabelDimension();
const maxPadding = MAX_OUTER_PADDING_PERCENT_FOR_WRT_LABEL * availableSpace.height;
this.outerPadding = Math.min(spaceRequired.height / 2, maxPadding);
const widthRequired = spaceRequired.width + this.axisConfig.labelPadding * 2;
if (widthRequired <= availableWidth) {
availableWidth -= widthRequired;
this.showLabel = true;
}
}
if (this.axisConfig.showTick && availableWidth >= this.axisConfig.tickLength) {
this.showTick = true;
availableWidth -= this.axisConfig.tickLength;
}
if (this.axisConfig.showTitle && this.title) {
const spaceRequired = this.textDimensionCalculator.getMaxDimension(
[this.title],
this.axisConfig.titleFontSize
);
const widthRequired = spaceRequired.height + this.axisConfig.titlePadding * 2;
this.titleTextHeight = spaceRequired.height;
if (widthRequired <= availableWidth) {
availableWidth -= widthRequired;
this.showTitle = true;
}
}
this.boundingRect.width = availableSpace.width - availableWidth;
this.boundingRect.height = availableSpace.height;
}
calculateSpace(availableSpace: Dimension): Dimension {
if (this.axisPosition === 'left' || this.axisPosition === 'right') {
this.calculateSpaceIfDrawnVertical(availableSpace);
} else {
this.calculateSpaceIfDrawnHorizontally(availableSpace);
}
this.recalculateScale();
return {
width: this.boundingRect.width,
height: this.boundingRect.height,
};
}
setBoundingBoxXY(point: Point): void {
this.boundingRect.x = point.x;
this.boundingRect.y = point.y;
}
private getDrawableElementsForLeftAxis(): DrawableElem[] {
const drawableElement: DrawableElem[] = [];
if (this.showAxisLine) {
const x = this.boundingRect.x + this.boundingRect.width - this.axisConfig.axisLineWidth / 2;
drawableElement.push({
type: 'path',
groupTexts: ['left-axis', 'axisl-line'],
data: [
{
path: `M ${x},${this.boundingRect.y} L ${x},${
this.boundingRect.y + this.boundingRect.height
} `,
strokeFill: this.axisThemeConfig.axisLineColor,
strokeWidth: this.axisConfig.axisLineWidth,
},
],
});
}
if (this.showLabel) {
drawableElement.push({
type: 'text',
groupTexts: ['left-axis', 'label'],
data: this.getTickValues().map((tick) => ({
text: tick.toString(),
x:
this.boundingRect.x +
this.boundingRect.width -
(this.showLabel ? this.axisConfig.labelPadding : 0) -
(this.showTick ? this.axisConfig.tickLength : 0) -
(this.showAxisLine ? this.axisConfig.axisLineWidth : 0),
y: this.getScaleValue(tick),
fill: this.axisThemeConfig.labelColor,
fontSize: this.axisConfig.labelFontSize,
rotation: 0,
verticalPos: 'middle',
horizontalPos: 'right',
})),
});
}
if (this.showTick) {
const x =
this.boundingRect.x +
this.boundingRect.width -
(this.showAxisLine ? this.axisConfig.axisLineWidth : 0);
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.axisThemeConfig.tickColor,
strokeWidth: this.axisConfig.tickWidth,
})),
});
}
if (this.showTitle) {
drawableElement.push({
type: 'text',
groupTexts: ['left-axis', 'title'],
data: [
{
text: this.title,
x: this.boundingRect.x + this.axisConfig.titlePadding,
y: this.boundingRect.y + this.boundingRect.height / 2,
fill: this.axisThemeConfig.titleColor,
fontSize: this.axisConfig.titleFontSize,
rotation: 270,
verticalPos: 'top',
horizontalPos: 'center',
},
],
});
}
return drawableElement;
}
private getDrawableElementsForBottomAxis(): DrawableElem[] {
const drawableElement: DrawableElem[] = [];
if (this.showAxisLine) {
const y = this.boundingRect.y + this.axisConfig.axisLineWidth / 2;
drawableElement.push({
type: 'path',
groupTexts: ['bottom-axis', 'axis-line'],
data: [
{
path: `M ${this.boundingRect.x},${y} L ${
this.boundingRect.x + this.boundingRect.width
},${y}`,
strokeFill: this.axisThemeConfig.axisLineColor,
strokeWidth: this.axisConfig.axisLineWidth,
},
],
});
}
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.axisConfig.labelPadding +
(this.showTick ? this.axisConfig.tickLength : 0) +
(this.showAxisLine ? this.axisConfig.axisLineWidth : 0),
fill: this.axisThemeConfig.labelColor,
fontSize: this.axisConfig.labelFontSize,
rotation: 0,
verticalPos: 'top',
horizontalPos: 'center',
})),
});
}
if (this.showTick) {
const y = this.boundingRect.y + (this.showAxisLine ? this.axisConfig.axisLineWidth : 0);
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.axisThemeConfig.tickColor,
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.boundingRect.height -
this.axisConfig.titlePadding -
this.titleTextHeight,
fill: this.axisThemeConfig.titleColor,
fontSize: this.axisConfig.titleFontSize,
rotation: 0,
verticalPos: 'top',
horizontalPos: 'center',
},
],
});
}
return drawableElement;
}
private getDrawableElementsForTopAxis(): DrawableElem[] {
const drawableElement: DrawableElem[] = [];
if (this.showAxisLine) {
const y = this.boundingRect.y + this.boundingRect.height - this.axisConfig.axisLineWidth / 2;
drawableElement.push({
type: 'path',
groupTexts: ['top-axis', 'axis-line'],
data: [
{
path: `M ${this.boundingRect.x},${y} L ${
this.boundingRect.x + this.boundingRect.width
},${y}`,
strokeFill: this.axisThemeConfig.axisLineColor,
strokeWidth: this.axisConfig.axisLineWidth,
},
],
});
}
if (this.showLabel) {
drawableElement.push({
type: 'text',
groupTexts: ['top-axis', 'label'],
data: this.getTickValues().map((tick) => ({
text: tick.toString(),
x: this.getScaleValue(tick),
y:
this.boundingRect.y +
(this.showTitle ? this.titleTextHeight + this.axisConfig.titlePadding * 2 : 0) +
this.axisConfig.labelPadding,
fill: this.axisThemeConfig.labelColor,
fontSize: this.axisConfig.labelFontSize,
rotation: 0,
verticalPos: 'top',
horizontalPos: 'center',
})),
});
}
if (this.showTick) {
const y = this.boundingRect.y;
drawableElement.push({
type: 'path',
groupTexts: ['top-axis', 'ticks'],
data: this.getTickValues().map((tick) => ({
path: `M ${this.getScaleValue(tick)},${
y + this.boundingRect.height - (this.showAxisLine ? this.axisConfig.axisLineWidth : 0)
} L ${this.getScaleValue(tick)},${
y +
this.boundingRect.height -
this.axisConfig.tickLength -
(this.showAxisLine ? this.axisConfig.axisLineWidth : 0)
}`,
strokeFill: this.axisThemeConfig.tickColor,
strokeWidth: this.axisConfig.tickWidth,
})),
});
}
if (this.showTitle) {
drawableElement.push({
type: 'text',
groupTexts: ['top-axis', 'title'],
data: [
{
text: this.title,
x: this.boundingRect.x + this.boundingRect.width / 2,
y: this.boundingRect.y + this.axisConfig.titlePadding,
fill: this.axisThemeConfig.titleColor,
fontSize: this.axisConfig.titleFontSize,
rotation: 0,
verticalPos: 'top',
horizontalPos: 'center',
},
],
});
}
return drawableElement;
}
getDrawableElements(): DrawableElem[] {
if (this.axisPosition === 'left') {
return this.getDrawableElementsForLeftAxis();
}
if (this.axisPosition === 'right') {
throw Error('Drawing of right axis is not implemented');
}
if (this.axisPosition === 'bottom') {
return this.getDrawableElementsForBottomAxis();
}
if (this.axisPosition === 'top') {
return this.getDrawableElementsForTopAxis();
}
return [];
}
}

View File

@ -0,0 +1,47 @@
import type { Group } from '../../../../../diagram-api/types.js';
import type {
AxisDataType,
ChartComponent,
XYChartAxisConfig,
XYChartAxisThemeConfig,
} from '../../interfaces.js';
import { isBandAxisData } from '../../interfaces.js';
import { TextDimensionCalculatorWithFont } from '../../textDimensionCalculator.js';
import { BandAxis } from './bandAxis.js';
import { LinearAxis } from './linearAxis.js';
export type AxisPosition = 'left' | 'right' | 'top' | 'bottom';
export interface Axis extends ChartComponent {
getScaleValue(value: string | number): number;
setAxisPosition(axisPosition: AxisPosition): void;
getAxisOuterPadding(): number;
getTickDistance(): number;
recalculateOuterPaddingToDrawBar(): void;
setRange(range: [number, number]): void;
}
export function getAxis(
data: AxisDataType,
axisConfig: XYChartAxisConfig,
axisThemeConfig: XYChartAxisThemeConfig,
tmpSVGGroup: Group
): Axis {
const textDimansionCalculator = new TextDimensionCalculatorWithFont(tmpSVGGroup);
if (isBandAxisData(data)) {
return new BandAxis(
axisConfig,
axisThemeConfig,
data.categories,
data.title,
textDimansionCalculator
);
}
return new LinearAxis(
axisConfig,
axisThemeConfig,
[data.min, data.max],
data.title,
textDimansionCalculator
);
}

View File

@ -0,0 +1,38 @@
import type { ScaleLinear } from 'd3';
import { scaleLinear } from 'd3';
import type { TextDimensionCalculator } from '../../textDimensionCalculator.js';
import { BaseAxis } from './baseAxis.js';
import type { XYChartAxisThemeConfig, XYChartAxisConfig } from '../../interfaces.js';
export class LinearAxis extends BaseAxis {
private scale: ScaleLinear<number, number>;
private domain: [number, number];
constructor(
axisConfig: XYChartAxisConfig,
axisThemeConfig: XYChartAxisThemeConfig,
domain: [number, number],
title: string,
textDimensionCalculator: TextDimensionCalculator
) {
super(axisConfig, title, textDimensionCalculator, axisThemeConfig);
this.domain = domain;
this.scale = scaleLinear().domain(this.domain).range(this.getRange());
}
getTickValues(): (string | number)[] {
return this.scale.ticks();
}
recalculateScale(): void {
const domain = [...this.domain]; // copy the array so if reverse is called two times it should not cancel the reverse effect
if (this.axisPosition === 'left') {
domain.reverse(); // since y-axis in svg start from top
}
this.scale = scaleLinear().domain(domain).range(this.getRange());
}
getScaleValue(value: number): number {
return this.scale(value);
}
}

View File

@ -0,0 +1,91 @@
import type { Group } from '../../../../diagram-api/types.js';
import type {
BoundingRect,
ChartComponent,
Dimension,
DrawableElem,
Point,
XYChartData,
XYChartThemeConfig,
XYChartConfig,
} from '../interfaces.js';
import type { TextDimensionCalculator } from '../textDimensionCalculator.js';
import { TextDimensionCalculatorWithFont } from '../textDimensionCalculator.js';
export class ChartTitle implements ChartComponent {
private boundingRect: BoundingRect;
private showChartTitle: boolean;
constructor(
private textDimensionCalculator: TextDimensionCalculator,
private chartConfig: XYChartConfig,
private chartData: XYChartData,
private chartThemeConfig: XYChartThemeConfig
) {
this.boundingRect = {
x: 0,
y: 0,
width: 0,
height: 0,
};
this.showChartTitle = false;
}
setBoundingBoxXY(point: Point): void {
this.boundingRect.x = point.x;
this.boundingRect.y = point.y;
}
calculateSpace(availableSpace: Dimension): Dimension {
const titleDimension = this.textDimensionCalculator.getMaxDimension(
[this.chartData.title],
this.chartConfig.titleFontSize
);
const widthRequired = Math.max(titleDimension.width, availableSpace.width);
const heightRequired = titleDimension.height + 2 * this.chartConfig.titlePadding;
if (
titleDimension.width <= widthRequired &&
titleDimension.height <= heightRequired &&
this.chartConfig.showTitle &&
this.chartData.title
) {
this.boundingRect.width = widthRequired;
this.boundingRect.height = heightRequired;
this.showChartTitle = true;
}
return {
width: this.boundingRect.width,
height: this.boundingRect.height,
};
}
getDrawableElements(): DrawableElem[] {
const drawableElem: DrawableElem[] = [];
if (this.showChartTitle) {
drawableElem.push({
groupTexts: ['chart-title'],
type: 'text',
data: [
{
fontSize: this.chartConfig.titleFontSize,
text: this.chartData.title,
verticalPos: 'middle',
horizontalPos: 'center',
x: this.boundingRect.x + this.boundingRect.width / 2,
y: this.boundingRect.y + this.boundingRect.height / 2,
fill: this.chartThemeConfig.titleColor,
rotation: 0,
},
],
});
}
return drawableElem;
}
}
export function getChartTitleComponent(
chartConfig: XYChartConfig,
chartData: XYChartData,
chartThemeConfig: XYChartThemeConfig,
tmpSVGGroup: Group
): ChartComponent {
const textDimensionCalculator = new TextDimensionCalculatorWithFont(tmpSVGGroup);
return new ChartTitle(textDimensionCalculator, chartConfig, chartData, chartThemeConfig);
}

View File

@ -0,0 +1,60 @@
import type { BarPlotData, BoundingRect, DrawableElem, XYChartConfig } from '../../interfaces.js';
import type { Axis } from '../axis/index.js';
export class BarPlot {
constructor(
private barData: BarPlotData,
private boundingRect: BoundingRect,
private xAxis: Axis,
private yAxis: Axis,
private orientation: XYChartConfig['chartOrientation'],
private plotIndex: number
) {}
getDrawableElement(): DrawableElem[] {
const finalData: [number, number][] = this.barData.data.map((d) => [
this.xAxis.getScaleValue(d[0]),
this.yAxis.getScaleValue(d[1]),
]);
const barPaddingPercent = 0.05;
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}`],
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,
})),
},
];
}
return [
{
groupTexts: ['plot', `bar-plot-${this.plotIndex}`],
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,
})),
},
];
}
}

View File

@ -0,0 +1,97 @@
import type {
XYChartData,
Dimension,
BoundingRect,
DrawableElem,
Point,
XYChartThemeConfig,
XYChartConfig,
} from '../../interfaces.js';
import type { Axis } from '../axis/index.js';
import type { ChartComponent } from '../../interfaces.js';
import { LinePlot } from './linePlot.js';
import { BarPlot } from './barPlot.js';
export interface Plot extends ChartComponent {
setAxes(xAxis: Axis, yAxis: Axis): void;
}
export class BasePlot implements Plot {
private boundingRect: BoundingRect;
private xAxis?: Axis;
private yAxis?: Axis;
constructor(
private chartConfig: XYChartConfig,
private chartData: XYChartData,
private chartThemeConfig: XYChartThemeConfig
) {
this.boundingRect = {
x: 0,
y: 0,
width: 0,
height: 0,
};
}
setAxes(xAxis: Axis, yAxis: Axis) {
this.xAxis = xAxis;
this.yAxis = yAxis;
}
setBoundingBoxXY(point: Point): void {
this.boundingRect.x = point.x;
this.boundingRect.y = point.y;
}
calculateSpace(availableSpace: Dimension): Dimension {
this.boundingRect.width = availableSpace.width;
this.boundingRect.height = availableSpace.height;
return {
width: this.boundingRect.width,
height: this.boundingRect.height,
};
}
getDrawableElements(): DrawableElem[] {
if (!(this.xAxis && this.yAxis)) {
throw Error('Axes must be passed to render Plots');
}
const drawableElem: DrawableElem[] = [];
for (const [i, plot] of this.chartData.plots.entries()) {
switch (plot.type) {
case 'line':
{
const linePlot = new LinePlot(
plot,
this.xAxis,
this.yAxis,
this.chartConfig.chartOrientation,
i
);
drawableElem.push(...linePlot.getDrawableElement());
}
break;
case 'bar':
{
const barPlot = new BarPlot(
plot,
this.boundingRect,
this.xAxis,
this.yAxis,
this.chartConfig.chartOrientation,
i
);
drawableElem.push(...barPlot.getDrawableElement());
}
break;
}
}
return drawableElem;
}
}
export function getPlotComponent(
chartConfig: XYChartConfig,
chartData: XYChartData,
chartThemeConfig: XYChartThemeConfig
): Plot {
return new BasePlot(chartConfig, chartData, chartThemeConfig);
}

View File

@ -0,0 +1,47 @@
import { line } from 'd3';
import type { DrawableElem, LinePlotData, XYChartConfig } from '../../interfaces.js';
import type { Axis } from '../axis/index.js';
export class LinePlot {
constructor(
private plotData: LinePlotData,
private xAxis: Axis,
private yAxis: Axis,
private orientation: XYChartConfig['chartOrientation'],
private plotIndex: number
) {}
getDrawableElement(): DrawableElem[] {
const finalData: [number, number][] = this.plotData.data.map((d) => [
this.xAxis.getScaleValue(d[0]),
this.yAxis.getScaleValue(d[1]),
]);
let path: string | null;
if (this.orientation === '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 [];
}
return [
{
groupTexts: ['plot', `line-plot-${this.plotIndex}`],
type: 'path',
data: [
{
path,
strokeFill: this.plotData.strokeFill,
strokeWidth: this.plotData.strokeWidth,
},
],
},
];
}
}

View File

@ -0,0 +1,15 @@
import type { Group } from '../../../diagram-api/types.js';
import type { DrawableElem, XYChartConfig, XYChartData, XYChartThemeConfig } from './interfaces.js';
import { Orchestrator } from './orchestrator.js';
export class XYChartBuilder {
static build(
config: XYChartConfig,
chartData: XYChartData,
chartThemeConfig: XYChartThemeConfig,
tmpSVGGroup: Group
): DrawableElem[] {
const orchestrator = new Orchestrator(config, chartData, chartThemeConfig, tmpSVGGroup);
return orchestrator.getDrawableElement();
}
}

View File

@ -0,0 +1,163 @@
export interface XYChartAxisThemeConfig {
titleColor: string;
labelColor: string;
tickColor: string;
axisLineColor: string;
}
export interface XYChartThemeConfig {
backgroundColor: string;
titleColor: string;
xAxisLabelColor: string;
xAxisTitleColor: string;
xAxisTickColor: string;
xAxisLineColor: string;
yAxisLabelColor: string;
yAxisTitleColor: string;
yAxisTickColor: string;
yAxisLineColor: string;
plotColorPalette: string;
}
export interface ChartComponent {
calculateSpace(availableSpace: Dimension): Dimension;
setBoundingBoxXY(point: Point): void;
getDrawableElements(): DrawableElem[];
}
export type SimplePlotDataType = [string, number][];
export interface LinePlotData {
type: 'line';
strokeFill: string;
strokeWidth: number;
data: SimplePlotDataType;
}
export interface BarPlotData {
type: 'bar';
fill: string;
data: SimplePlotDataType;
}
export type PlotData = LinePlotData | BarPlotData;
export function isBarPlot(data: PlotData): data is BarPlotData {
return data.type === 'bar';
}
export interface BandAxisDataType {
type: 'band';
title: string;
categories: string[];
}
export interface LinearAxisDataType {
type: 'linear';
title: string;
min: number;
max: number;
}
export type AxisDataType = LinearAxisDataType | BandAxisDataType;
export function isBandAxisData(data: AxisDataType): data is BandAxisDataType {
return data.type === 'band';
}
export function isLinearAxisData(data: AxisDataType): data is LinearAxisDataType {
return data.type === 'linear';
}
/**
* For now we are keeping this configs as we are removing the required fields while generating the config.type.ts file
* we should remove `XYChartAxisConfig` and `XYChartConfig` after we started using required fields
*/
export interface XYChartAxisConfig {
showLabel: boolean;
labelFontSize: number;
labelPadding: number;
showTitle: boolean;
titleFontSize: number;
titlePadding: number;
showTick: boolean;
tickLength: number;
tickWidth: number;
showAxisLine: boolean;
axisLineWidth: number;
}
export interface XYChartConfig {
width: number;
height: number;
titleFontSize: number;
titlePadding: number;
showTitle: boolean;
xAxis: XYChartAxisConfig;
yAxis: XYChartAxisConfig;
chartOrientation: 'vertical' | 'horizontal';
plotReservedSpacePercent: number;
}
export interface XYChartData {
xAxis: AxisDataType;
yAxis: AxisDataType;
title: string;
plots: PlotData[];
}
export interface Dimension {
width: number;
height: number;
}
export interface BoundingRect extends Point, Dimension {}
export interface Point {
x: number;
y: number;
}
export type TextHorizontalPos = 'left' | 'center' | 'right';
export type TextVerticalPos = 'top' | 'middle';
export interface RectElem extends Point {
width: number;
height: number;
fill: string;
strokeWidth: number;
strokeFill: string;
}
export interface TextElem extends Point {
text: string;
fill: string;
verticalPos: TextVerticalPos;
horizontalPos: TextHorizontalPos;
fontSize: number;
rotation: number;
}
export interface PathElem {
path: string;
fill?: string;
strokeWidth: number;
strokeFill: string;
}
export type DrawableElem =
| {
groupTexts: string[];
type: 'rect';
data: RectElem[];
}
| {
groupTexts: string[];
type: 'text';
data: TextElem[];
}
| {
groupTexts: string[];
type: 'path';
data: PathElem[];
};

View File

@ -0,0 +1,192 @@
import type {
ChartComponent,
DrawableElem,
XYChartConfig,
XYChartData,
XYChartThemeConfig,
} from './interfaces.js';
import { isBarPlot } from './interfaces.js';
import type { Axis } from './components/axis/index.js';
import { getAxis } from './components/axis/index.js';
import { getChartTitleComponent } from './components/chartTitle.js';
import type { Plot } from './components/plot/index.js';
import { getPlotComponent } from './components/plot/index.js';
import type { Group } from '../../../diagram-api/types.js';
export class Orchestrator {
private componentStore: {
title: ChartComponent;
plot: Plot;
xAxis: Axis;
yAxis: Axis;
};
constructor(
private chartConfig: XYChartConfig,
private chartData: XYChartData,
chartThemeConfig: XYChartThemeConfig,
tmpSVGGroup: Group
) {
this.componentStore = {
title: getChartTitleComponent(chartConfig, chartData, chartThemeConfig, tmpSVGGroup),
plot: getPlotComponent(chartConfig, chartData, chartThemeConfig),
xAxis: getAxis(
chartData.xAxis,
chartConfig.xAxis,
{
titleColor: chartThemeConfig.xAxisTitleColor,
labelColor: chartThemeConfig.xAxisLabelColor,
tickColor: chartThemeConfig.xAxisTickColor,
axisLineColor: chartThemeConfig.xAxisLineColor,
},
tmpSVGGroup
),
yAxis: getAxis(
chartData.yAxis,
chartConfig.yAxis,
{
titleColor: chartThemeConfig.yAxisTitleColor,
labelColor: chartThemeConfig.yAxisLabelColor,
tickColor: chartThemeConfig.yAxisTickColor,
axisLineColor: chartThemeConfig.yAxisLineColor,
},
tmpSVGGroup
),
};
}
private calculateVerticalSpace() {
let availableWidth = this.chartConfig.width;
let availableHeight = this.chartConfig.height;
let plotX = 0;
let plotY = 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,
});
plotY = spaceUsed.height;
availableHeight -= spaceUsed.height;
this.componentStore.xAxis.setAxisPosition('bottom');
spaceUsed = this.componentStore.xAxis.calculateSpace({
width: availableWidth,
height: availableHeight,
});
availableHeight -= spaceUsed.height;
this.componentStore.yAxis.setAxisPosition('left');
spaceUsed = this.componentStore.yAxis.calculateSpace({
width: availableWidth,
height: availableHeight,
});
plotX = spaceUsed.width;
availableWidth -= spaceUsed.width;
if (availableWidth > 0) {
chartWidth += availableWidth;
availableWidth = 0;
}
if (availableHeight > 0) {
chartHeight += availableHeight;
availableHeight = 0;
}
this.componentStore.plot.calculateSpace({
width: chartWidth,
height: chartHeight,
});
this.componentStore.plot.setBoundingBoxXY({ x: plotX, y: plotY });
this.componentStore.xAxis.setRange([plotX, plotX + chartWidth]);
this.componentStore.xAxis.setBoundingBoxXY({ x: plotX, y: plotY + chartHeight });
this.componentStore.yAxis.setRange([plotY, plotY + chartHeight]);
this.componentStore.yAxis.setBoundingBoxXY({ x: 0, y: plotY });
if (this.chartData.plots.some((p) => isBarPlot(p))) {
this.componentStore.xAxis.recalculateOuterPaddingToDrawBar();
}
}
private calculateHorizonatalSpace() {
let availableWidth = this.chartConfig.width;
let availableHeight = this.chartConfig.height;
let titleYEnd = 0;
let plotX = 0;
let plotY = 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,
});
titleYEnd = spaceUsed.height;
availableHeight -= spaceUsed.height;
this.componentStore.xAxis.setAxisPosition('left');
spaceUsed = this.componentStore.xAxis.calculateSpace({
width: availableWidth,
height: availableHeight,
});
availableWidth -= spaceUsed.width;
plotX = spaceUsed.width;
this.componentStore.yAxis.setAxisPosition('top');
spaceUsed = this.componentStore.yAxis.calculateSpace({
width: availableWidth,
height: availableHeight,
});
availableHeight -= spaceUsed.height;
plotY = titleYEnd + spaceUsed.height;
if (availableWidth > 0) {
chartWidth += availableWidth;
availableWidth = 0;
}
if (availableHeight > 0) {
chartHeight += availableHeight;
availableHeight = 0;
}
this.componentStore.plot.calculateSpace({
width: chartWidth,
height: chartHeight,
});
this.componentStore.plot.setBoundingBoxXY({ x: plotX, y: plotY });
this.componentStore.yAxis.setRange([plotX, plotX + chartWidth]);
this.componentStore.yAxis.setBoundingBoxXY({ x: plotX, y: titleYEnd });
this.componentStore.xAxis.setRange([plotY, plotY + chartHeight]);
this.componentStore.xAxis.setBoundingBoxXY({ x: 0, y: plotY });
if (this.chartData.plots.some((p) => isBarPlot(p))) {
this.componentStore.xAxis.recalculateOuterPaddingToDrawBar();
}
}
private calculateSpace() {
if (this.chartConfig.chartOrientation === 'horizontal') {
this.calculateHorizonatalSpace();
} else {
this.calculateVerticalSpace();
}
}
getDrawableElement() {
this.calculateSpace();
const drawableElem: DrawableElem[] = [];
this.componentStore.plot.setAxes(this.componentStore.xAxis, this.componentStore.yAxis);
for (const component of Object.values(this.componentStore)) {
drawableElem.push(...component.getDrawableElements());
}
return drawableElem;
}
}

View File

@ -0,0 +1,39 @@
import type { Dimension } from './interfaces.js';
import { computeDimensionOfText } from '../../../rendering-util/createText.js';
import type { Group } from '../../../diagram-api/types.js';
export interface TextDimensionCalculator {
getMaxDimension(texts: string[], fontSize: number): Dimension;
}
export class TextDimensionCalculatorWithFont implements TextDimensionCalculator {
constructor(private parentGroup: Group) {}
getMaxDimension(texts: string[], fontSize: number): Dimension {
if (!this.parentGroup) {
return {
width: texts.reduce((acc, cur) => Math.max(cur.length, acc), 0) * fontSize,
height: fontSize,
};
}
const dimension: Dimension = {
width: 0,
height: 0,
};
const elem = this.parentGroup
.append('g')
.attr('visibility', 'hidden')
.attr('font-size', fontSize);
for (const t of texts) {
const bbox = computeDimensionOfText(elem, 1, t);
const width = bbox ? bbox.width : t.length * fontSize;
const height = bbox ? bbox.height : fontSize;
dimension.width = Math.max(dimension.width, width);
dimension.height = Math.max(dimension.height, height);
}
elem.remove();
return dimension;
}
}

View File

@ -0,0 +1,171 @@
%lex
%options case-insensitive
%x string
%x md_string
%x title
%x acc_title
%x acc_descr
%x acc_descr_multiline
%s axis_data
%s axis_band_data
%s data
%s data_inner
%%
\%\%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */
<axis_data>(\r?\n) { this.popState(); return 'NEWLINE'; }
<data>(\r?\n) { this.popState(); return 'NEWLINE'; }
[\n\r]+ return 'NEWLINE';
\%\%[^\n]* /* do nothing */
"title" { return 'title'; }
"accTitle"\s*":"\s* { this.pushState("acc_title");return 'acc_title'; }
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
"accDescr"\s*":"\s* { this.pushState("acc_descr");return 'acc_descr'; }
<acc_descr>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; }
"accDescr"\s*"{"\s* { this.pushState("acc_descr_multiline");}
<acc_descr_multiline>"{" { this.popState(); }
<acc_descr_multiline>[^\}]* { return "acc_descr_multiline_value"; }
"xychart-beta" {return 'XYCHART';}
(?:"vertical"|"horizontal") {return 'CHART_ORIENTATION'}
"x-axis" { this.pushState("axis_data"); return "X_AXIS"; }
"y-axis" { this.pushState("axis_data"); return "Y_AXIS"; }
<axis_data>"[" { this.pushState("axis_band_data"); return 'SQUARE_BRACES_START'; }
<axis_data>"-->" { return 'ARROW_DELIMITER'; }
"line" { this.pushState("data"); return 'LINE'; }
"bar" { this.pushState("data"); return 'BAR'; }
<data>"[" { this.pushState("data_inner"); return 'SQUARE_BRACES_START'; }
<axis_data,data_inner>[+-]?(?:\d+(?:\.\d+)?|\.\d+) { return 'NUMBER_WITH_DECIMAL'; }
<data_inner,axis_band_data>"]" { this.popState(); return 'SQUARE_BRACES_END'; }
(?:"`) { this.pushState("md_string"); }
<md_string>(?:(?!`\").)+ { return "MD_STR"; }
<md_string>(?:`") { this.popState(); }
["] this.pushState("string");
<string>["] this.popState();
<string>[^"]* return "STR";
"[" return 'SQUARE_BRACES_START'
"]" return 'SQUARE_BRACES_END'
[A-Za-z]+ return 'ALPHA';
":" return 'COLON';
\+ return 'PLUS';
"," return 'COMMA';
\= return 'EQUALS';
"*" return 'MULT';
\# return 'BRKT';
[\_] return 'UNDERSCORE';
"." return 'DOT';
"&" return 'AMP';
\- return 'MINUS';
[0-9]+ return 'NUM';
\s+ /* skip */
";" return 'SEMI';
<<EOF>> return 'EOF';
/lex
%start start
%% /* language grammar */
start
: eol start
| XYCHART chartConfig start
| XYCHART start
| document
;
chartConfig
: CHART_ORIENTATION { yy.setOrientation($1); }
;
document
: /* empty */
| document statement
;
statement
: statement eol
| title text { yy.setDiagramTitle($text.text.trim()); }
| X_AXIS parseXAxis
| Y_AXIS parseYAxis
| LINE plotData { yy.setLineData({text: '', type: 'text'}, $plotData); }
| LINE text plotData { yy.setLineData($text, $plotData); }
| BAR plotData { yy.setBarData({text: '', type: 'text'}, $plotData); }
| BAR text plotData { yy.setBarData($text, $plotData); }
| acc_title acc_title_value { $$=$acc_title_value.trim();yy.setAccTitle($$); }
| acc_descr acc_descr_value { $$=$acc_descr_value.trim();yy.setAccDescription($$); }
| acc_descr_multiline_value { $$=$acc_descr_multiline_value.trim();yy.setAccDescription($$); }
;
plotData
: SQUARE_BRACES_START commaSeparatedNumbers SQUARE_BRACES_END { $$ = $commaSeparatedNumbers }
;
commaSeparatedNumbers
: NUMBER_WITH_DECIMAL COMMA commaSeparatedNumbers { $$ = [Number($NUMBER_WITH_DECIMAL), ...$commaSeparatedNumbers] }
| NUMBER_WITH_DECIMAL { $$ = [Number($NUMBER_WITH_DECIMAL)] }
;
parseXAxis
: text {yy.setXAxisTitle($text);}
| text xAxisData {yy.setXAxisTitle($text);}
| xAxisData {yy.setXAxisTitle({type: 'text', text: ''});}
;
xAxisData
: bandData {yy.setXAxisBand($bandData);}
| NUMBER_WITH_DECIMAL ARROW_DELIMITER NUMBER_WITH_DECIMAL {yy.setXAxisRangeData(Number($NUMBER_WITH_DECIMAL1), Number($NUMBER_WITH_DECIMAL2));}
;
bandData
: SQUARE_BRACES_START commaSeparatedTexts SQUARE_BRACES_END {$$ = $commaSeparatedTexts}
;
commaSeparatedTexts
: text COMMA commaSeparatedTexts { $$ = [$text, ...$commaSeparatedTexts] }
| text { $$ = [$text] }
;
parseYAxis
: text {yy.setYAxisTitle($text);}
| text yAxisData {yy.setYAxisTitle($text);}
| yAxisData {yy.setYAxisTitle({type: "text", text: ""});}
;
yAxisData
: NUMBER_WITH_DECIMAL ARROW_DELIMITER NUMBER_WITH_DECIMAL {yy.setYAxisRangeData(Number($NUMBER_WITH_DECIMAL1), Number($NUMBER_WITH_DECIMAL2));}
;
eol
: NEWLINE
| SEMI
| EOF
;
text: alphaNum { $$={text:$alphaNum, type: 'text'};}
| STR { $$={text: $STR, type: 'text'};}
| MD_STR { $$={text: $MD_STR, type: 'markdown'};}
;
alphaNum
: alphaNumToken {$$=$alphaNumToken;}
| alphaNum alphaNumToken {$$=$alphaNum+''+$alphaNumToken;}
;
alphaNumToken : AMP | NUM | ALPHA | PLUS | EQUALS | MULT | DOT | BRKT| MINUS | UNDERSCORE ;
%%

View File

@ -0,0 +1,448 @@
// @ts-ignore: Jison doesn't support type.
import { parser } from './xychart.jison';
import type { Mock } from 'vitest';
import { vi } from 'vitest';
const parserFnConstructor = (str: string) => {
return () => {
parser.parse(str);
};
};
const mockDB: Record<string, Mock<any, any>> = {
setOrientation: vi.fn(),
setDiagramTitle: vi.fn(),
setXAxisTitle: vi.fn(),
setXAxisRangeData: vi.fn(),
setXAxisBand: vi.fn(),
setYAxisTitle: vi.fn(),
setYAxisRangeData: vi.fn(),
setLineData: vi.fn(),
setBarData: vi.fn(),
};
function clearMocks() {
for (const key in mockDB) {
mockDB[key].mockRestore();
}
}
describe('Testing xychart jison file', () => {
beforeEach(() => {
parser.yy = mockDB;
clearMocks();
});
it('should throw error if xychart-beta text is not there', () => {
const str = 'xychart-beta-1';
expect(parserFnConstructor(str)).toThrow();
});
it('should not throw error if only xychart is there', () => {
const str = 'xychart-beta';
expect(parserFnConstructor(str)).not.toThrow();
});
it('parse title of the chart within "', () => {
const str = 'xychart-beta \n title "This is a title"';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setDiagramTitle).toHaveBeenCalledWith('This is a title');
});
it('parse title of the chart without "', () => {
const str = 'xychart-beta \n title oneLinertitle';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setDiagramTitle).toHaveBeenCalledWith('oneLinertitle');
});
it('parse chart orientation', () => {
const str = 'xychart-beta vertical';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setOrientation).toHaveBeenCalledWith('vertical');
});
it('parse chart orientation with spaces', () => {
let str = 'xychart-beta horizontal ';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setOrientation).toHaveBeenCalledWith('horizontal');
str = 'xychart-beta abc';
expect(parserFnConstructor(str)).toThrow();
});
it('parse x-axis', () => {
const str = 'xychart-beta \nx-axis xAxisName\n';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
text: 'xAxisName',
type: 'text',
});
});
it('parse x-axis with axis name without "', () => {
const str = 'xychart-beta \nx-axis xAxisName \n';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
text: 'xAxisName',
type: 'text',
});
});
it('parse x-axis with axis name with "', () => {
const str = 'xychart-beta \n x-axis "xAxisName has space"\n';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
text: 'xAxisName has space',
type: 'text',
});
});
it('parse x-axis with axis name with " with spaces', () => {
const str = 'xychart-beta \n x-axis " xAxisName has space " \n';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
text: ' xAxisName has space ',
type: 'text',
});
});
it('parse x-axis with axis name and range data', () => {
const str = 'xychart-beta \nx-axis xAxisName 45.5 --> 33 \n';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
text: 'xAxisName',
type: 'text',
});
expect(mockDB.setXAxisRangeData).toHaveBeenCalledWith(45.5, 33);
});
it('parse x-axis throw error for invalid range data', () => {
const str = 'xychart-beta \nx-axis xAxisName aaa --> 33 \n';
expect(parserFnConstructor(str)).toThrow();
});
it('parse x-axis with axis name and range data with only decimal part', () => {
const str = 'xychart-beta \nx-axis xAxisName 45.5 --> .34 \n';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
text: 'xAxisName',
type: 'text',
});
expect(mockDB.setXAxisRangeData).toHaveBeenCalledWith(45.5, 0.34);
});
it('parse x-axis without axisname and range data', () => {
const str = 'xychart-beta \nx-axis 45.5 --> 1.34 \n';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
text: '',
type: 'text',
});
expect(mockDB.setXAxisRangeData).toHaveBeenCalledWith(45.5, 1.34);
});
it('parse x-axis with axis name and category data', () => {
const str = 'xychart-beta \nx-axis xAxisName [ "cat1" , cat2a ] \n ';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
text: 'xAxisName',
type: 'text',
});
expect(mockDB.setXAxisBand).toHaveBeenCalledWith([
{
text: 'cat1',
type: 'text',
},
{ text: 'cat2a', type: 'text' },
]);
});
it('parse x-axis without axisname and category data', () => {
const str = 'xychart-beta \nx-axis [ "cat1" , cat2a ] \n ';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
text: '',
type: 'text',
});
expect(mockDB.setXAxisBand).toHaveBeenCalledWith([
{
text: 'cat1',
type: 'text',
},
{ text: 'cat2a', type: 'text' },
]);
});
it('parse x-axis throw error if unbalanced bracket', () => {
let str = 'xychart-beta \nx-axis xAxisName [ "cat1" [ cat2a ] \n ';
expect(parserFnConstructor(str)).toThrow();
str = 'xychart-beta \nx-axis xAxisName [ "cat1" , cat2a ] ] \n ';
expect(parserFnConstructor(str)).toThrow();
});
it('parse x-axis complete variant 1', () => {
const str = `xychart-beta \n x-axis "this is x axis" [category1, "category 2", category3]\n`;
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'this is x axis', type: 'text' });
expect(mockDB.setXAxisBand).toHaveBeenCalledWith([
{ text: 'category1', type: 'text' },
{ text: 'category 2', type: 'text' },
{ text: 'category3', type: 'text' },
]);
});
it('parse x-axis complete variant 2', () => {
const str =
'xychart-beta \nx-axis xAxisName [ "cat1 with space" , cat2 , cat3] \n ';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
expect(mockDB.setXAxisBand).toHaveBeenCalledWith([
{ text: 'cat1 with space', type: 'text' },
{ text: 'cat2', type: 'text' },
{ text: 'cat3', type: 'text' },
]);
});
it('parse x-axis complete variant 3', () => {
const str =
'xychart-beta \nx-axis xAxisName [ "cat1 with space" , cat2 asdf , cat3] \n ';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
expect(mockDB.setXAxisBand).toHaveBeenCalledWith([
{ text: 'cat1 with space', type: 'text' },
{ text: 'cat2asdf', type: 'text' },
{ text: 'cat3', type: 'text' },
]);
});
it('parse y-axis with axis name', () => {
const str = 'xychart-beta \ny-axis yAxisName\n';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
});
it('parse y-axis with axis name with spaces', () => {
const str = 'xychart-beta \ny-axis yAxisName \n';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
});
it('parse y-axis with axis name with "', () => {
const str = 'xychart-beta \n y-axis "yAxisName has space"\n';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({
text: 'yAxisName has space',
type: 'text',
});
});
it('parse y-axis with axis name with " and spaces', () => {
const str = 'xychart-beta \n y-axis " yAxisName has space " \n';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({
text: ' yAxisName has space ',
type: 'text',
});
});
it('parse y-axis with axis name with range data', () => {
const str = 'xychart-beta \ny-axis yAxisName 45.5 --> 33 \n';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
expect(mockDB.setYAxisRangeData).toHaveBeenCalledWith(45.5, 33);
});
it('parse y-axis without axisname with range data', () => {
const str = 'xychart-beta \ny-axis 45.5 --> 33 \n';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: '', type: 'text' });
expect(mockDB.setYAxisRangeData).toHaveBeenCalledWith(45.5, 33);
});
it('parse y-axis with axis name with range data with only decimal part', () => {
const str = 'xychart-beta \ny-axis yAxisName 45.5 --> .33 \n';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
expect(mockDB.setYAxisRangeData).toHaveBeenCalledWith(45.5, 0.33);
});
it('parse y-axis throw error for invalid number in range data', () => {
const str = 'xychart-beta \ny-axis yAxisName 45.5 --> abc \n';
expect(parserFnConstructor(str)).toThrow();
});
it('parse y-axis throws error if range data is passed', () => {
const str = 'xychart-beta \ny-axis yAxisName [ 45.3, 33 ] \n';
expect(parserFnConstructor(str)).toThrow();
});
it('parse both axis at once', () => {
const str = 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
});
it('parse line Data', () => {
const str = 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line lineTitle [23, 45, 56.6]';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setLineData).toHaveBeenCalledWith(
{ text: 'lineTitle', type: 'text' },
[23, 45, 56.6]
);
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
});
it('parse line Data with spaces and +,- symbols', () => {
const str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , -45 , 56.6 ] ';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
expect(mockDB.setLineData).toHaveBeenCalledWith(
{ text: 'lineTitle with space', type: 'text' },
[23, -45, 56.6]
);
});
it('parse line Data without title', () => {
const str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line [ +23 , -45 , 56.6 , .33] ';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
expect(mockDB.setLineData).toHaveBeenCalledWith(
{ text: '', type: 'text' },
[23, -45, 56.6, 0.33]
);
});
it('parse line Data throws error unbalanced brackets', () => {
let str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 [ -45 , 56.6 ] ';
expect(parserFnConstructor(str)).toThrow();
str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , -45 ] 56.6 ] ';
expect(parserFnConstructor(str)).toThrow();
});
it('parse line Data throws error if data is not provided', () => {
const str = 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" ';
expect(parserFnConstructor(str)).toThrow();
});
it('parse line Data throws error if data is empty', () => {
const str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ ] ';
expect(parserFnConstructor(str)).toThrow();
});
it('parse line Data throws error if , is not in proper', () => {
const str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , , -45 , 56.6 ] ';
expect(parserFnConstructor(str)).toThrow();
});
it('parse line Data throws error if not number', () => {
const str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , -4aa5 , 56.6 ] ';
expect(parserFnConstructor(str)).toThrow();
});
it('parse bar Data', () => {
const str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar barTitle [23, 45, 56.6, .22]';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
expect(mockDB.setBarData).toHaveBeenCalledWith(
{ text: 'barTitle', type: 'text' },
[23, 45, 56.6, 0.22]
);
});
it('parse bar Data spaces and +,- symbol', () => {
const str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , -45 , 56.6 ] ';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
expect(mockDB.setBarData).toHaveBeenCalledWith(
{ text: 'barTitle with space', type: 'text' },
[23, -45, 56.6]
);
});
it('parse bar Data without plot title', () => {
const str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar [ +23 , -45 , 56.6 ] ';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
expect(mockDB.setBarData).toHaveBeenCalledWith({ text: '', type: 'text' }, [23, -45, 56.6]);
});
it('parse bar should throw for unbalanced brackets', () => {
let str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 [ -45 , 56.6 ] ';
expect(parserFnConstructor(str)).toThrow();
str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , -45 ] 56.6 ] ';
expect(parserFnConstructor(str)).toThrow();
});
it('parse bar should throw error if data is not provided', () => {
const str = 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" ';
expect(parserFnConstructor(str)).toThrow();
});
it('parse bar should throw error if data is empty', () => {
const str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ ] ';
expect(parserFnConstructor(str)).toThrow();
});
it('parse bar should throw error if comma is not proper', () => {
const str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , , -45 , 56.6 ] ';
expect(parserFnConstructor(str)).toThrow();
});
it('parse bar should throw error if number is not passed', () => {
const str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , -4aa5 , 56.6 ] ';
expect(parserFnConstructor(str)).toThrow();
});
it('parse multiple bar and line varient 1', () => {
const str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar barTitle1 [23, 45, 56.6] \n line lineTitle1 [11, 45.5, 67, 23] \n bar barTitle2 [13, 42, 56.89] \n line lineTitle2 [45, 99, 012]';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
expect(mockDB.setBarData).toHaveBeenCalledWith(
{ text: 'barTitle1', type: 'text' },
[23, 45, 56.6]
);
expect(mockDB.setBarData).toHaveBeenCalledWith(
{ text: 'barTitle2', type: 'text' },
[13, 42, 56.89]
);
expect(mockDB.setLineData).toHaveBeenCalledWith(
{ text: 'lineTitle1', type: 'text' },
[11, 45.5, 67, 23]
);
expect(mockDB.setLineData).toHaveBeenCalledWith(
{ text: 'lineTitle2', type: 'text' },
[45, 99, 12]
);
});
it('parse multiple bar and line varient 2', () => {
const str = `
xychart-beta horizontal
title Basic xychart
x-axis "this is x axis" [category1, "category 2", category3]
y-axis yaxisText 10 --> 150
bar barTitle1 [23, 45, 56.6]
line lineTitle1 [11, 45.5, 67, 23]
bar barTitle2 [13, 42, 56.89]
line lineTitle2 [45, 99, 012]`;
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yaxisText', type: 'text' });
expect(mockDB.setYAxisRangeData).toHaveBeenCalledWith(10, 150);
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'this is x axis', type: 'text' });
expect(mockDB.setXAxisBand).toHaveBeenCalledWith([
{ text: 'category1', type: 'text' },
{ text: 'category 2', type: 'text' },
{ text: 'category3', type: 'text' },
]);
expect(mockDB.setBarData).toHaveBeenCalledWith(
{ text: 'barTitle1', type: 'text' },
[23, 45, 56.6]
);
expect(mockDB.setBarData).toHaveBeenCalledWith(
{ text: 'barTitle2', type: 'text' },
[13, 42, 56.89]
);
expect(mockDB.setLineData).toHaveBeenCalledWith(
{ text: 'lineTitle1', type: 'text' },
[11, 45.5, 67, 23]
);
expect(mockDB.setLineData).toHaveBeenCalledWith(
{ text: 'lineTitle2', type: 'text' },
[45, 99, 12]
);
});
});

View File

@ -0,0 +1,229 @@
import {
clear as commonClear,
getAccDescription,
getAccTitle,
getDiagramTitle,
setAccDescription,
setAccTitle,
setDiagramTitle,
} from '../common/commonDb.js';
import * as configApi from '../../config.js';
import defaultConfig from '../../defaultConfig.js';
import { getThemeVariables } from '../../themes/theme-default.js';
import { cleanAndMerge } from '../../utils.js';
import { sanitizeText } from '../common/common.js';
import { XYChartBuilder } from './chartBuilder/index.js';
import type {
DrawableElem,
SimplePlotDataType,
XYChartConfig,
XYChartData,
XYChartThemeConfig,
} from './chartBuilder/interfaces.js';
import { isBandAxisData, isLinearAxisData } from './chartBuilder/interfaces.js';
import type { Group } from '../../diagram-api/types.js';
let plotIndex = 0;
let tmpSVGGroup: Group;
let xyChartConfig: XYChartConfig = getChartDefaultConfig();
let xyChartThemeConfig: XYChartThemeConfig = getChartDefaultThemeConfig();
let xyChartData: XYChartData = getChartDefaultData();
let plotColorPalette = xyChartThemeConfig.plotColorPalette.split(',').map((color) => color.trim());
let hasSetXAxis = false;
let hasSetYAxis = false;
interface NormalTextType {
type: 'text';
text: string;
}
function getChartDefaultThemeConfig(): XYChartThemeConfig {
const defaultThemeVariables = getThemeVariables();
const config = configApi.getConfig();
return cleanAndMerge(defaultThemeVariables.xyChart, config.themeVariables.xyChart);
}
function getChartDefaultConfig(): XYChartConfig {
const config = configApi.getConfig();
return cleanAndMerge<XYChartConfig>(
defaultConfig.xyChart as XYChartConfig,
config.xyChart as XYChartConfig
);
}
function getChartDefaultData(): XYChartData {
return {
yAxis: {
type: 'linear',
title: '',
min: Infinity,
max: -Infinity,
},
xAxis: {
type: 'band',
title: '',
categories: [],
},
title: '',
plots: [],
};
}
function textSanitizer(text: string) {
const config = configApi.getConfig();
return sanitizeText(text.trim(), config);
}
function setTmpSVGG(SVGG: Group) {
tmpSVGGroup = SVGG;
}
function setOrientation(oriantation: string) {
if (oriantation === 'horizontal') {
xyChartConfig.chartOrientation = 'horizontal';
} else {
xyChartConfig.chartOrientation = 'vertical';
}
}
function setXAxisTitle(title: NormalTextType) {
xyChartData.xAxis.title = textSanitizer(title.text);
}
function setXAxisRangeData(min: number, max: number) {
xyChartData.xAxis = { type: 'linear', title: xyChartData.xAxis.title, min, max };
hasSetXAxis = true;
}
function setXAxisBand(categories: NormalTextType[]) {
xyChartData.xAxis = {
type: 'band',
title: xyChartData.xAxis.title,
categories: categories.map((c) => textSanitizer(c.text)),
};
hasSetXAxis = true;
}
function setYAxisTitle(title: NormalTextType) {
xyChartData.yAxis.title = textSanitizer(title.text);
}
function setYAxisRangeData(min: number, max: number) {
xyChartData.yAxis = { type: 'linear', title: xyChartData.yAxis.title, min, max };
hasSetYAxis = true;
}
// this function does not set `hasSetYAxis` as there can be multiple data so we should calculate the range accordingly
function setYAxisRangeFromPlotData(data: number[]) {
const minValue = Math.min(...data);
const maxValue = Math.max(...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: Math.min(prevMinValue, minValue),
max: Math.max(prevMaxValue, maxValue),
};
}
function transformDataWithoutCategory(data: number[]): SimplePlotDataType {
let retData: SimplePlotDataType = [];
if (data.length === 0) {
return retData;
}
if (!hasSetXAxis) {
const prevMinValue = isLinearAxisData(xyChartData.xAxis) ? xyChartData.xAxis.min : Infinity;
const prevMaxValue = isLinearAxisData(xyChartData.xAxis) ? xyChartData.xAxis.max : -Infinity;
setXAxisRangeData(Math.min(prevMinValue, 1), Math.max(prevMaxValue, data.length));
}
if (!hasSetYAxis) {
setYAxisRangeFromPlotData(data);
}
if (isBandAxisData(xyChartData.xAxis)) {
retData = xyChartData.xAxis.categories.map((c, i) => [c, data[i]]);
}
if (isLinearAxisData(xyChartData.xAxis)) {
const min = xyChartData.xAxis.min;
const max = xyChartData.xAxis.max;
const step = (max - min + 1) / data.length;
const categories: string[] = [];
for (let i = min; i <= max; i += step) {
categories.push(`${i}`);
}
retData = categories.map((c, i) => [c, data[i]]);
}
return retData;
}
function getPlotColorFromPalette(plotIndex: number): string {
return plotColorPalette[plotIndex === 0 ? 0 : plotIndex % plotColorPalette.length];
}
function setLineData(title: NormalTextType, data: number[]) {
const plotData = transformDataWithoutCategory(data);
xyChartData.plots.push({
type: 'line',
strokeFill: getPlotColorFromPalette(plotIndex),
strokeWidth: 2,
data: plotData,
});
plotIndex++;
}
function setBarData(title: NormalTextType, data: number[]) {
const plotData = transformDataWithoutCategory(data);
xyChartData.plots.push({
type: 'bar',
fill: getPlotColorFromPalette(plotIndex),
data: plotData,
});
plotIndex++;
}
function getDrawableElem(): DrawableElem[] {
if (xyChartData.plots.length === 0) {
throw Error('No Plot to render, please provide a plot with some data');
}
xyChartData.title = getDiagramTitle();
return XYChartBuilder.build(xyChartConfig, xyChartData, xyChartThemeConfig, tmpSVGGroup);
}
function getChartThemeConfig() {
return xyChartThemeConfig;
}
function getChartConfig() {
return xyChartConfig;
}
const clear = function () {
commonClear();
plotIndex = 0;
xyChartConfig = getChartDefaultConfig();
xyChartData = getChartDefaultData();
xyChartThemeConfig = getChartDefaultThemeConfig();
plotColorPalette = xyChartThemeConfig.plotColorPalette.split(',').map((color) => color.trim());
hasSetXAxis = false;
hasSetYAxis = false;
};
export default {
getDrawableElem,
clear,
setAccTitle,
getAccTitle,
setDiagramTitle,
getDiagramTitle,
getAccDescription,
setAccDescription,
setOrientation,
setXAxisTitle,
setXAxisRangeData,
setXAxisBand,
setYAxisTitle,
setYAxisRangeData,
setLineData,
setBarData,
setTmpSVGG,
getChartThemeConfig,
getChartConfig,
};

View File

@ -0,0 +1,24 @@
import type {
DiagramDetector,
DiagramLoader,
ExternalDiagramDefinition,
} from '../../diagram-api/types.js';
const id = 'xychart';
const detector: DiagramDetector = (txt) => {
return /^\s*xychart-beta/.test(txt);
};
const loader: DiagramLoader = async () => {
const { diagram } = await import('./xychartDiagram.js');
return { id, diagram };
};
const plugin: ExternalDiagramDefinition = {
id,
detector,
loader,
};
export default plugin;

View File

@ -0,0 +1,11 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: Jison doesn't support types.
import parser from './parser/xychart.jison';
import db from './xychartDb.js';
import renderer from './xychartRenderer.js';
export const diagram: DiagramDefinition = {
parser,
db,
renderer,
};

View File

@ -0,0 +1,123 @@
import type { Diagram } from '../../Diagram.js';
import { log } from '../../logger.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import type {
DrawableElem,
TextElem,
TextHorizontalPos,
TextVerticalPos,
} from './chartBuilder/interfaces.js';
import type XYChartDB from './xychartDb.js';
export const draw = (txt: string, id: string, _version: string, diagObj: Diagram) => {
const db = diagObj.db as typeof XYChartDB;
const themeConfig = db.getChartThemeConfig();
const chartConfig = db.getChartConfig();
function getDominantBaseLine(horizontalPos: TextVerticalPos) {
return horizontalPos === 'top' ? 'text-before-edge' : 'middle';
}
function getTextAnchor(verticalPos: TextHorizontalPos) {
return verticalPos === 'left' ? 'start' : verticalPos === 'right' ? 'end' : 'middle';
}
function getTextTransformation(data: TextElem) {
return `translate(${data.x}, ${data.y}) rotate(${data.rotation || 0})`;
}
log.debug('Rendering xychart chart\n' + txt);
const svg = selectSvgElement(id);
const group = svg.append('g').attr('class', 'main');
const background = group
.append('rect')
.attr('width', chartConfig.width)
.attr('height', chartConfig.height)
.attr('class', 'background');
// @ts-ignore: TODO Fix ts errors
configureSvgSize(svg, chartConfig.height, chartConfig.width, true);
svg.attr('viewBox', `0 0 ${chartConfig.width} ${chartConfig.height}`);
background.attr('fill', themeConfig.backgroundColor);
db.setTmpSVGG(svg.append('g').attr('class', 'mermaid-tmp-group'));
const shapes: DrawableElem[] = db.getDrawableElem();
const groups: Record<string, any> = {};
function getGroup(gList: string[]) {
let elem = group;
let prefix = '';
for (const [i] of gList.entries()) {
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) {
continue;
}
const shapeGroup = getGroup(shape.groupTexts);
switch (shape.type) {
case 'rect':
shapeGroup
.selectAll('rect')
.data(shape.data)
.enter()
.append('rect')
.attr('x', (data) => data.x)
.attr('y', (data) => data.y)
.attr('width', (data) => data.width)
.attr('height', (data) => data.height)
.attr('fill', (data) => data.fill)
.attr('stroke', (data) => data.strokeFill)
.attr('stroke-width', (data) => data.strokeWidth);
break;
case 'text':
shapeGroup
.selectAll('text')
.data(shape.data)
.enter()
.append('text')
.attr('x', 0)
.attr('y', 0)
.attr('fill', (data) => data.fill)
.attr('font-size', (data) => data.fontSize)
.attr('dominant-baseline', (data) => getDominantBaseLine(data.verticalPos))
.attr('text-anchor', (data) => getTextAnchor(data.horizontalPos))
.attr('transform', (data) => getTextTransformation(data))
.text((data) => data.text);
break;
case 'path':
shapeGroup
.selectAll('path')
.data(shape.data)
.enter()
.append('path')
.attr('d', (data) => data.path)
.attr('fill', (data) => (data.fill ? data.fill : 'none'))
.attr('stroke', (data) => data.strokeFill)
.attr('stroke-width', (data) => data.strokeWidth);
break;
}
}
};
export default {
draw,
};

View File

@ -150,6 +150,7 @@ function sidebarSyntax() {
{ text: 'Timeline 🔥', link: '/syntax/timeline' },
{ text: 'Zenuml 🔥', link: '/syntax/zenuml' },
{ text: 'Sankey 🔥', link: '/syntax/sankey' },
{ text: 'XYChart 🔥', link: '/syntax/xychart' },
{ text: 'Other Examples', link: '/syntax/examples' },
],
},

View File

@ -117,3 +117,14 @@ quadrantChart
Campaign E: [0.40, 0.34]
Campaign F: [0.35, 0.78]
```
### [XY Chart](../syntax/xyChart.md)
```mermaid-example
xychart-beta
title "Sales Revenue"
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
```

View File

@ -0,0 +1,165 @@
# XY Chart
> In the context of mermaid-js, the XY chart is a comprehensive charting module that encompasses various types of charts that utilize both x-axis and y-axis for data representation. Presently, it includes two fundamental chart types: the bar chart and the line chart. These charts are designed to visually display and analyze data that involve two numerical variables.
> It's important to note that while the current implementation of mermaid-js includes these two chart types, the framework is designed to be dynamic and adaptable. Therefore, it has the capacity for expansion and the inclusion of additional chart types in the future. This means that users can expect an evolving suite of charting options within the XY chart module, catering to various data visualization needs as new chart types are introduced over time.
## Example
```mermaid-example
xychart-beta
title "Sales Revenue"
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
```
## Syntax
```note
All text values that contain only one word can be written without `"`. If a text value has many words in it, specifically if it contains spaces, enclose the value in `"`
```
### Orientations
The chart can be drawn horizontal or vertical, default value is vertical.
```
xychart-beta horizontal
...
```
### Title
The title is a short description of the chart and it will always render on top of the chart.
#### Example
```
xychart-beta
title "This is a simple example"
...
```
```note
If the title is a single word one no need to use `"`, but if it has space `"` is needed
```
### x-axis
The x-axis primarily serves as a categorical value, although it can also function as a numeric range value when needed.
#### Example
1. `x-axis title min --> max` x-axis will function as numeric with the given range
2. `x-axis "title with space" [cat1, "cat2 with space", cat3]` x-axis if categorical, categories are text type
### y-axis
The y-axis is employed to represent numerical range values, it cannot have categorical values.
#### Example
1. `y-axis title min --> max`
2. `y-axis title` it will only add the title, the range will be auto generated from data.
```note
Both x and y axis are optional if not provided we will try to create the range
```
### Line chart
A line chart offers the capability to graphically depict lines.
#### Example
1. `line [2.3, 45, .98, -3.4]` it can have all valid numeric values.
### Bar chart
A bar chart offers the capability to graphically depict bars.
#### Example
1. `bar [2.3, 45, .98, -3.4]` it can have all valid numeric values.
#### Simplest example
The only two things required are the chart name (`xychart-beta`) and one data set. So you will be able to draw a chart with a simple config like
```
xychart-beta
line [+1.3, .6, 2.4, -.34]
```
## Chart Configurations
| Parameter | Description | Default value |
| ------------------------ | ---------------------------------------------- | :-----------: |
| width | Width of the chart | 700 |
| height | Height of the chart | 500 |
| titlePadding | Top and Bottom padding of the title | 10 |
| titleFontSize | Title font size | 20 |
| showTitle | Title to be shown or not | true |
| xAxis | xAxis configuration | AxisConfig |
| yAxis | yAxis configuration | AxisConfig |
| chartOrientation | 'vertical' or 'horizontal' | 'vertical' |
| plotReservedSpacePercent | Minimum space plots will take inside the chart | 50 |
### AxisConfig
| Parameter | Description | Default value |
| ------------- | ------------------------------------ | :-----------: |
| showLabel | Show axis labels or tick values | true |
| labelFontSize | Font size of the label to be drawn | 14 |
| labelPadding | Top and Bottom padding of the label | 5 |
| showTitle | Axis title to be shown or not | true |
| titleFontSize | Axis title font size | 16 |
| titlePadding | Top and Bottom padding of Axis title | 5 |
| showTick | Tick to be shown or not | true |
| tickLength | How long the tick will be | 5 |
| tickWidth | How width the tick will be | 2 |
| showAxisLine | Axis line to be shown or not | true |
| axisLineWidth | Thickness of the axis line | 2 |
## Chart Theme Variables
```note
Themes for xychart resides inside xychart attribute so to set the variables use this syntax
%%{init: { "themeVariables": {"xyChart": {"titleColor": "#ff0000"} } }}%%
```
| Parameter | Description |
| ---------------- | --------------------------------------------------------- |
| backgroundColor | Background color of the whole chart |
| titleColor | Color of the Title text |
| xAxisLableColor | Color of the x-axis labels |
| xAxisTitleColor | Color of the x-axis title |
| xAxisTickColor | Color of the x-axis tick |
| xAxisLineColor | Color of the x-axis line |
| yAxisLableColor | Color of the y-axis labels |
| yAxisTitleColor | Color of the y-axis title |
| yAxisTickColor | Color of the y-axis tick |
| yAxisLineColor | Color of the y-axis line |
| plotColorPalette | String of colors separated by comma e.g. "#f3456, #43445" |
## Example on config and theme
```mermaid-example
---
config:
xyChart:
width: 900
height: 600
themeVariables:
xyChart:
titleColor: "#ff0000"
---
xychart-beta
title "Sales Revenue"
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
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]
```

View File

@ -1,5 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// @ts-nocheck TODO: Fix types
import type { Group } from '../diagram-api/types.js';
import type { D3TSpanElement, D3TextElement } from '../diagrams/common/commonTypes.js';
import { log } from '../logger.js';
import { decodeEntities } from '../mermaidAPI.js';
import { markdownToHTML, markdownToLines } from '../rendering-util/handle-markdown-text.js';
@ -76,6 +78,21 @@ function computeWidthOfText(parentNode: any, lineHeight: number, line: MarkdownL
return textLength;
}
export function computeDimensionOfText(
parentNode: Group,
lineHeight: number,
text: string
): DOMRect | undefined {
const testElement: D3TextElement = parentNode.append('text');
const testSpan: D3TSpanElement = createTspan(testElement, 1, lineHeight);
updateTextContentAndStyles(testSpan, [{ content: text, type: 'normal' }]);
const textDimension: DOMRect | undefined = testSpan.node()?.getBoundingClientRect();
if (textDimension) {
testElement.remove();
}
return textDimension;
}
/**
* Creates a formatted text element by breaking lines and applying styles based on
* the given structuredText.

View File

@ -43,6 +43,7 @@ required:
- er
- pie
- quadrantChart
- xyChart
- requirement
- mindmap
- gitGraph
@ -197,6 +198,8 @@ properties:
$ref: '#/$defs/PieDiagramConfig'
quadrantChart:
$ref: '#/$defs/QuadrantChartConfig'
xyChart:
$ref: '#/$defs/XYChartConfig'
requirement:
$ref: '#/$defs/RequirementDiagramConfig'
mindmap:
@ -982,6 +985,132 @@ $defs: # JSON Schema definition (maybe we should move these to a seperate file)
type: number
minimum: 0
default: 2
XYChartAxisConfig:
title: XYChart axis config
description: This object contains configuration for XYChart axis config
type: object
unevaluatedProperties: true
required:
- showLabel
- labelFontSize
- labelPadding
- showTitle
- titleFontSize
- titlePadding
- showTick
- tickLength
- tickWidth
- showAxisLine
- axisLineWidth
properties:
showLabel:
description: Should show the axis labels (tick text)
type: boolean
default: true
labelFontSize:
description: font size of the axis labels (tick text)
type: number
default: 14
minimum: 1
labelPadding:
description: top and bottom space from axis label (tick text)
type: number
default: 5
minimum: 0
showTitle:
description: Should show the axis title
type: boolean
default: true
titleFontSize:
description: font size of the axis title
type: number
default: 16
minimum: 1
titlePadding:
description: top and bottom space from axis title
type: number
default: 5
minimum: 0
showTick:
description: Should show the axis tick lines
type: boolean
default: true
tickLength:
description: length of the axis tick lines
type: number
default: 5
minimum: 1
tickWidth:
description: width of the axis tick lines
type: number
default: 2
minimum: 1
showAxisLine:
description: Show line across the axis
type: boolean
default: true
axisLineWidth:
description: Width of the axis line
type: number
default: 2
minimum: 1
XYChartConfig:
title: XYChart Config
allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }]
description: This object contains configuration specific to XYCharts
type: object
unevaluatedProperties: false
required:
- width
- height
- titleFontSize
- titlePadding
- xAxis
- yAxis
- showTitle
- chartOrientation
- plotReservedSpacePercent
properties:
width:
description: width of the chart
type: number
default: 700
minimum: 1
height:
description: height of the chart
type: number
default: 500
minimum: 1
titleFontSize:
description: Font size of the chart title
type: number
default: 20
minimum: 1
titlePadding:
description: Top and bottom space from the chart title
type: number
default: 10
minimum: 0
showTitle:
description: Should show the chart title
type: boolean
default: true
xAxis:
$ref: '#/$defs/XYChartAxisConfig'
default: { '$ref': '#/$defs/XYChartAxisConfig' }
yAxis:
$ref: '#/$defs/XYChartAxisConfig'
default: { '$ref': '#/$defs/XYChartAxisConfig' }
chartOrientation:
description: How to plot will be drawn horizontal or vertical
tsType: '"vertical" | "horizontal"'
default: 'vertical'
plotReservedSpacePercent:
description: Minimum percent of space plots of the chart will take
type: number
default: 50
minimum: 30
ErDiagramConfig:
title: Er Diagram Config

View File

@ -245,6 +245,23 @@ class Theme {
this.quadrantExternalBorderStrokeFill || this.primaryBorderColor;
this.quadrantTitleFill = this.quadrantTitleFill || this.primaryTextColor;
/* xychart */
this.xyChart = {
backgroundColor: this.xyChart?.backgroundColor || this.background,
titleColor: this.xyChart?.titleColor || this.primaryTextColor,
xAxisTitleColor: this.xyChart?.xAxisTitleColor || this.primaryTextColor,
xAxisLabelColor: this.xyChart?.xAxisLabelColor || this.primaryTextColor,
xAxisTickColor: this.xyChart?.xAxisTickColor || this.primaryTextColor,
xAxisLineColor: this.xyChart?.xAxisLineColor || this.primaryTextColor,
yAxisTitleColor: this.xyChart?.yAxisTitleColor || this.primaryTextColor,
yAxisLabelColor: this.xyChart?.yAxisLabelColor || this.primaryTextColor,
yAxisTickColor: this.xyChart?.yAxisTickColor || this.primaryTextColor,
yAxisLineColor: this.xyChart?.yAxisLineColor || this.primaryTextColor,
plotColorPalette:
this.xyChart?.plotColorPalette ||
'#FFF4DD,#FFD8B1,#FFA07A,#ECEFF1,#D6DBDF,#C3E0A8,#FFB6A4,#FFD74D,#738FA7,#FFFFF0',
};
/* requirement-diagram */
this.requirementBackground = this.requirementBackground || this.primaryColor;
this.requirementBorderColor = this.requirementBorderColor || this.primaryBorderColor;

View File

@ -251,6 +251,23 @@ class Theme {
this.quadrantExternalBorderStrokeFill || this.primaryBorderColor;
this.quadrantTitleFill = this.quadrantTitleFill || this.primaryTextColor;
/* xychart */
this.xyChart = {
backgroundColor: this.xyChart?.backgroundColor || this.background,
titleColor: this.xyChart?.titleColor || this.primaryTextColor,
xAxisTitleColor: this.xyChart?.xAxisTitleColor || this.primaryTextColor,
xAxisLabelColor: this.xyChart?.xAxisLabelColor || this.primaryTextColor,
xAxisTickColor: this.xyChart?.xAxisTickColor || this.primaryTextColor,
xAxisLineColor: this.xyChart?.xAxisLineColor || this.primaryTextColor,
yAxisTitleColor: this.xyChart?.yAxisTitleColor || this.primaryTextColor,
yAxisLabelColor: this.xyChart?.yAxisLabelColor || this.primaryTextColor,
yAxisTickColor: this.xyChart?.yAxisTickColor || this.primaryTextColor,
yAxisLineColor: this.xyChart?.yAxisLineColor || this.primaryTextColor,
plotColorPalette:
this.xyChart?.plotColorPalette ||
'#3498db,#2ecc71,#e74c3c,#f1c40f,#bdc3c7,#ffffff,#34495e,#9b59b6,#1abc9c,#e67e22',
};
/* class */
this.classText = this.primaryTextColor;

View File

@ -272,6 +272,23 @@ class Theme {
this.quadrantExternalBorderStrokeFill || this.primaryBorderColor;
this.quadrantTitleFill = this.quadrantTitleFill || this.primaryTextColor;
/* xychart */
this.xyChart = {
backgroundColor: this.xyChart?.backgroundColor || this.background,
titleColor: this.xyChart?.titleColor || this.primaryTextColor,
xAxisTitleColor: this.xyChart?.xAxisTitleColor || this.primaryTextColor,
xAxisLabelColor: this.xyChart?.xAxisLabelColor || this.primaryTextColor,
xAxisTickColor: this.xyChart?.xAxisTickColor || this.primaryTextColor,
xAxisLineColor: this.xyChart?.xAxisLineColor || this.primaryTextColor,
yAxisTitleColor: this.xyChart?.yAxisTitleColor || this.primaryTextColor,
yAxisLabelColor: this.xyChart?.yAxisLabelColor || this.primaryTextColor,
yAxisTickColor: this.xyChart?.yAxisTickColor || this.primaryTextColor,
yAxisLineColor: this.xyChart?.yAxisLineColor || this.primaryTextColor,
plotColorPalette:
this.xyChart?.plotColorPalette ||
'#ECECFF,#8493A6,#FFC3A0,#DCDDE1,#B8E994,#D1A36F,#C3CDE6,#FFB6C1,#496078,#F8F3E3',
};
/* requirement-diagram */
this.requirementBackground = this.requirementBackground || this.primaryColor;
this.requirementBorderColor = this.requirementBorderColor || this.primaryBorderColor;

View File

@ -240,6 +240,23 @@ class Theme {
this.quadrantExternalBorderStrokeFill || this.primaryBorderColor;
this.quadrantTitleFill = this.quadrantTitleFill || this.primaryTextColor;
/* xychart */
this.xyChart = {
backgroundColor: this.xyChart?.backgroundColor || this.background,
titleColor: this.xyChart?.titleColor || this.primaryTextColor,
xAxisTitleColor: this.xyChart?.xAxisTitleColor || this.primaryTextColor,
xAxisLabelColor: this.xyChart?.xAxisLabelColor || this.primaryTextColor,
xAxisTickColor: this.xyChart?.xAxisTickColor || this.primaryTextColor,
xAxisLineColor: this.xyChart?.xAxisLineColor || this.primaryTextColor,
yAxisTitleColor: this.xyChart?.yAxisTitleColor || this.primaryTextColor,
yAxisLabelColor: this.xyChart?.yAxisLabelColor || this.primaryTextColor,
yAxisTickColor: this.xyChart?.yAxisTickColor || this.primaryTextColor,
yAxisLineColor: this.xyChart?.yAxisLineColor || this.primaryTextColor,
plotColorPalette:
this.xyChart?.plotColorPalette ||
'#CDE498,#FF6B6B,#A0D2DB,#D7BDE2,#F0F0F0,#FFC3A0,#7FD8BE,#FF9A8B,#FAF3E0,#FFF176',
};
/* requirement-diagram */
this.requirementBackground = this.requirementBackground || this.primaryColor;
this.requirementBorderColor = this.requirementBorderColor || this.primaryBorderColor;

View File

@ -271,6 +271,23 @@ class Theme {
this.quadrantExternalBorderStrokeFill || this.primaryBorderColor;
this.quadrantTitleFill = this.quadrantTitleFill || this.primaryTextColor;
/* xychart */
this.xyChart = {
backgroundColor: this.xyChart?.backgroundColor || this.background,
titleColor: this.xyChart?.titleColor || this.primaryTextColor,
xAxisTitleColor: this.xyChart?.xAxisTitleColor || this.primaryTextColor,
xAxisLabelColor: this.xyChart?.xAxisLabelColor || this.primaryTextColor,
xAxisTickColor: this.xyChart?.xAxisTickColor || this.primaryTextColor,
xAxisLineColor: this.xyChart?.xAxisLineColor || this.primaryTextColor,
yAxisTitleColor: this.xyChart?.yAxisTitleColor || this.primaryTextColor,
yAxisLabelColor: this.xyChart?.yAxisLabelColor || this.primaryTextColor,
yAxisTickColor: this.xyChart?.yAxisTickColor || this.primaryTextColor,
yAxisLineColor: this.xyChart?.yAxisLineColor || this.primaryTextColor,
plotColorPalette:
this.xyChart?.plotColorPalette ||
'#EEE,#6BB8E4,#8ACB88,#C7ACD6,#E8DCC2,#FFB2A8,#FFF380,#7E8D91,#FFD8B1,#FAF3E0',
};
/* requirement-diagram */
this.requirementBackground = this.requirementBackground || this.primaryColor;
this.requirementBorderColor = this.requirementBorderColor || this.primaryBorderColor;

File diff suppressed because it is too large Load Diff