Merge branch 'develop' into patch-1
This commit is contained in:
commit
f346c3f511
|
@ -18,6 +18,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
|
|||
'er',
|
||||
'pie',
|
||||
'quadrantChart',
|
||||
'xyChart',
|
||||
'requirement',
|
||||
'mindmap',
|
||||
'timeline',
|
||||
|
|
|
@ -156,6 +156,7 @@
|
|||
"vitepress",
|
||||
"vueuse",
|
||||
"xlink",
|
||||
"xychart",
|
||||
"yash",
|
||||
"yokozuna",
|
||||
"zenuml",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -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).**
|
||||
|
|
|
@ -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]
|
||||
```
|
|
@ -47,6 +47,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
|
|||
'er',
|
||||
'pie',
|
||||
'quadrantChart',
|
||||
'xyChart',
|
||||
'requirement',
|
||||
'mindmap',
|
||||
'timeline',
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -236,6 +236,10 @@ const config: RequiredDeep<MermaidConfig> = {
|
|||
...defaultConfigJson.pie,
|
||||
useWidth: 984,
|
||||
},
|
||||
xyChart: {
|
||||
...defaultConfigJson.xyChart,
|
||||
useWidth: undefined,
|
||||
},
|
||||
requirement: {
|
||||
...defaultConfigJson.requirement,
|
||||
useWidth: undefined,
|
||||
|
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -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 [];
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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[];
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 ;
|
||||
|
||||
%%
|
|
@ -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]
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
|
@ -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;
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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' },
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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]
|
||||
```
|
||||
|
|
|
@ -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]
|
||||
```
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
7223
pnpm-lock.yaml
7223
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue