Addressed all requested changes

This commit is contained in:
Subhash Halder
2023-08-20 17:51:53 +05:30
parent 526de36c86
commit 6c2bd33f36
19 changed files with 378 additions and 285 deletions

View File

@@ -17,7 +17,7 @@
<h1>XY Charts demos</h1> <h1>XY Charts demos</h1>
<pre class="mermaid"> <pre class="mermaid">
xychart-beta horizontal xychart-beta horizontal
title Basic xychart title "Basic xychart"
x-axis "this is x axis" [category1, "category 2", category3, category4] x-axis "this is x axis" [category1, "category 2", category3, category4]
y-axis yaxisText 10 --> 150 y-axis yaxisText 10 --> 150
bar "sample bat" [52, 96, 35, 10] bar "sample bat" [52, 96, 35, 10]
@@ -29,7 +29,7 @@
<h1>XY Charts demos</h1> <h1>XY Charts demos</h1>
<pre class="mermaid"> <pre class="mermaid">
xychart-beta xychart-beta
title Basic xychart title "Basic xychart"
x-axis "this is x axis" [category1, "category 2", category3, category4] x-axis "this is x axis" [category1, "category 2", category3, category4]
y-axis yaxisText 10 --> 150 y-axis yaxisText 10 --> 150
bar "sample bat" [52, 96, 35, 10] bar "sample bat" [52, 96, 35, 10]
@@ -48,6 +48,24 @@
bar "sample bat" [52, 96, 35, 10] bar "sample bat" [52, 96, 35, 10]
</pre> </pre>
<h1>XY Charts demos</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 bat" [52, 96, 35, 10, 87, 34, 67, 99]
</pre>
<h1>XY Charts demos</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 bat" [52, 96, 35, 10, 87, 34, 67, 99]
</pre>
<hr /> <hr />
<script type="module"> <script type="module">

View File

@@ -40,7 +40,7 @@ export interface BarPlotData {
export type PlotData = LinePlotData | BarPlotData; export type PlotData = LinePlotData | BarPlotData;
export function isBarPlot(data: PlotData): data is BarPlotData { export function isBarPlot(data: PlotData): data is BarPlotData {
return data.type === 'line'; return data.type === 'bar';
} }
export interface BandAxisDataType { export interface BandAxisDataType {

View File

@@ -8,23 +8,25 @@ import {
} from './Interfaces.js'; } from './Interfaces.js';
import { getChartTitleComponent } from './components/ChartTitle.js'; import { getChartTitleComponent } from './components/ChartTitle.js';
import { ChartComponent } from './Interfaces.js'; import { ChartComponent } from './Interfaces.js';
import { IAxis, getAxis } from './components/axis/index.js'; import { Axis, getAxis } from './components/axis/index.js';
import { IPlot, getPlotComponent } from './components/plot/index.js'; import { Plot, getPlotComponent } from './components/plot/index.js';
import { SVGGType } from '../xychartDb.js';
export class Orchestrator { export class Orchestrator {
private componentStore: { private componentStore: {
title: ChartComponent; title: ChartComponent;
plot: IPlot; plot: Plot;
xAxis: IAxis; xAxis: Axis;
yAxis: IAxis; yAxis: Axis;
}; };
constructor( constructor(
private chartConfig: XYChartConfig, private chartConfig: XYChartConfig,
private chartData: XYChartData, private chartData: XYChartData,
private chartThemeConfig: XYChartThemeConfig private chartThemeConfig: XYChartThemeConfig,
private tmpSVGGElem: SVGGType
) { ) {
this.componentStore = { this.componentStore = {
title: getChartTitleComponent(chartConfig, chartData, chartThemeConfig), title: getChartTitleComponent(chartConfig, chartData, chartThemeConfig, tmpSVGGElem),
plot: getPlotComponent(chartConfig, chartData, chartThemeConfig), plot: getPlotComponent(chartConfig, chartData, chartThemeConfig),
xAxis: getAxis( xAxis: getAxis(
chartData.xAxis, chartData.xAxis,
@@ -34,7 +36,7 @@ export class Orchestrator {
labelColor: chartThemeConfig.xAxisLableColor, labelColor: chartThemeConfig.xAxisLableColor,
tickColor: chartThemeConfig.xAxisTickColor, tickColor: chartThemeConfig.xAxisTickColor,
}, },
chartConfig.fontFamily tmpSVGGElem
), ),
yAxis: getAxis( yAxis: getAxis(
chartData.yAxis, chartData.yAxis,
@@ -44,7 +46,7 @@ export class Orchestrator {
labelColor: chartThemeConfig.yAxisLableColor, labelColor: chartThemeConfig.yAxisLableColor,
tickColor: chartThemeConfig.yAxisTickColor, tickColor: chartThemeConfig.yAxisTickColor,
}, },
chartConfig.fontFamily tmpSVGGElem
), ),
}; };
} }

View File

@@ -1,49 +1,15 @@
import { Dimension } from './Interfaces.js'; import { Dimension } from './Interfaces.js';
import { computeDimensionOfText } from '../../../rendering-util/createText.js';
import { SVGGType } from '../xychartDb.js';
export interface ITextDimensionCalculator { export interface TextDimensionCalculator {
getDimension(texts: string[], fontSize: number, fontFamily?: string): Dimension; getMaxDimension(texts: string[], fontSize: number): Dimension;
} }
export class TextDimensionCalculator implements ITextDimensionCalculator { export class TextDimensionCalculatorWithFont implements TextDimensionCalculator {
getDimension(texts: string[], fontSize: number): Dimension { constructor(private parentGroup: SVGGType) {}
return { getMaxDimension(texts: string[], fontSize: number): Dimension {
width: texts.reduce((acc, cur) => Math.max(cur.length, acc), 0) * fontSize, if (!this.parentGroup) {
height: fontSize,
};
}
}
export class TextDimensionCalculatorWithFont implements ITextDimensionCalculator {
private container: HTMLSpanElement | null = null;
private hiddenElementId = 'mermaid-text-dimension-calculator';
constructor(fontFamily?: string) {
if (document) {
let parentContainer = document.getElementById(this.hiddenElementId);
if (!parentContainer) {
parentContainer = document.createElement('div');
parentContainer.id = this.hiddenElementId;
parentContainer.style.position = 'absolute';
parentContainer.style.top = '-100px';
parentContainer.style.left = '0px';
parentContainer.style.visibility = 'hidden';
document.body.append(parentContainer);
}
const fontClassName = `font-${fontFamily}`;
const prevContainerAvailable = parentContainer.getElementsByClassName(fontClassName);
if (prevContainerAvailable.length > 0) {
this.container = prevContainerAvailable.item(0) as HTMLSpanElement;
} else {
this.container = document.createElement('div');
this.container.className = fontClassName;
if (fontFamily) {
this.container.style.fontFamily = fontFamily;
}
parentContainer.append(this.container);
}
}
}
getDimension(texts: string[], fontSize: number): Dimension {
if (!this.container) {
return { return {
width: texts.reduce((acc, cur) => Math.max(cur.length, acc), 0) * fontSize, width: texts.reduce((acc, cur) => Math.max(cur.length, acc), 0) * fontSize,
height: fontSize, height: fontSize,
@@ -55,14 +21,17 @@ export class TextDimensionCalculatorWithFont implements ITextDimensionCalculator
height: 0, height: 0,
}; };
this.container.style.fontSize = `${fontSize}px`; const elem = this.parentGroup
.append('g')
.attr('visibility', 'hidden')
.attr('font-size', fontSize);
for (const t of texts) { for (const t of texts) {
this.container.innerHTML = t; const bbox = computeDimensionOfText(elem, 1, t);
const bbox = this.container.getBoundingClientRect();
dimension.width = Math.max(dimension.width, bbox.width); dimension.width = Math.max(dimension.width, bbox.width);
dimension.height = Math.max(dimension.height, bbox.height); dimension.height = Math.max(dimension.height, bbox.height);
} }
elem.remove();
return dimension; return dimension;
} }
} }

View File

@@ -1,3 +1,4 @@
import { SVGGType } from '../../xychartDb.js';
import { import {
BoundingRect, BoundingRect,
ChartComponent, ChartComponent,
@@ -9,7 +10,7 @@ import {
XYChartConfig, XYChartConfig,
} from '../Interfaces.js'; } from '../Interfaces.js';
import { import {
ITextDimensionCalculator, TextDimensionCalculator,
TextDimensionCalculatorWithFont, TextDimensionCalculatorWithFont,
} from '../TextDimensionCalculator.js'; } from '../TextDimensionCalculator.js';
@@ -17,7 +18,7 @@ export class ChartTitle implements ChartComponent {
private boundingRect: BoundingRect; private boundingRect: BoundingRect;
private showChartTitle: boolean; private showChartTitle: boolean;
constructor( constructor(
private textDimensionCalculator: ITextDimensionCalculator, private textDimensionCalculator: TextDimensionCalculator,
private chartConfig: XYChartConfig, private chartConfig: XYChartConfig,
private chartData: XYChartData, private chartData: XYChartData,
private chartThemeConfig: XYChartThemeConfig private chartThemeConfig: XYChartThemeConfig
@@ -35,7 +36,7 @@ export class ChartTitle implements ChartComponent {
this.boundingRect.y = point.y; this.boundingRect.y = point.y;
} }
calculateSpace(availableSpace: Dimension): Dimension { calculateSpace(availableSpace: Dimension): Dimension {
const titleDimension = this.textDimensionCalculator.getDimension( const titleDimension = this.textDimensionCalculator.getMaxDimension(
[this.chartData.title], [this.chartData.title],
this.chartConfig.titleFontSize this.chartConfig.titleFontSize
); );
@@ -82,8 +83,9 @@ export class ChartTitle implements ChartComponent {
export function getChartTitleComponent( export function getChartTitleComponent(
chartConfig: XYChartConfig, chartConfig: XYChartConfig,
chartData: XYChartData, chartData: XYChartData,
chartThemeConfig: XYChartThemeConfig chartThemeConfig: XYChartThemeConfig,
tmpSVGGElem: SVGGType
): ChartComponent { ): ChartComponent {
const textDimensionCalculator = new TextDimensionCalculatorWithFont(chartConfig.fontFamily); const textDimensionCalculator = new TextDimensionCalculatorWithFont(tmpSVGGElem);
return new ChartTitle(textDimensionCalculator, chartConfig, chartData, chartThemeConfig); return new ChartTitle(textDimensionCalculator, chartConfig, chartData, chartThemeConfig);
} }

View File

@@ -1,6 +1,6 @@
import { ScaleBand, scaleBand } from 'd3'; import { ScaleBand, scaleBand } from 'd3';
import { log } from '../../../../../logger.js'; import { log } from '../../../../../logger.js';
import { ITextDimensionCalculator } from '../../TextDimensionCalculator.js'; import { TextDimensionCalculator } from '../../TextDimensionCalculator.js';
import { BaseAxis } from './BaseAxis.js'; import { BaseAxis } from './BaseAxis.js';
import { XYChartAxisThemeConfig, XYChartAxisConfig } from '../../Interfaces.js'; import { XYChartAxisThemeConfig, XYChartAxisConfig } from '../../Interfaces.js';
@@ -13,7 +13,7 @@ export class BandAxis extends BaseAxis {
axisThemeConfig: XYChartAxisThemeConfig, axisThemeConfig: XYChartAxisThemeConfig,
categories: string[], categories: string[],
title: string, title: string,
textDimensionCalculator: ITextDimensionCalculator textDimensionCalculator: TextDimensionCalculator
) { ) {
super(axisConfig, title, textDimensionCalculator, axisThemeConfig); super(axisConfig, title, textDimensionCalculator, axisThemeConfig);
this.categories = categories; this.categories = categories;

View File

@@ -7,10 +7,12 @@ import {
XYChartAxisThemeConfig, XYChartAxisThemeConfig,
XYChartAxisConfig, XYChartAxisConfig,
} from '../../Interfaces.js'; } from '../../Interfaces.js';
import { ITextDimensionCalculator } from '../../TextDimensionCalculator.js'; import { TextDimensionCalculator } from '../../TextDimensionCalculator.js';
import { AxisPosition, IAxis } from './index.js'; import { AxisPosition, Axis } from './index.js';
export abstract class BaseAxis implements IAxis { const BAR_WIDTH_TO_TICK_WIDTH_RATIO = 0.7;
export abstract class BaseAxis implements Axis {
protected boundingRect: BoundingRect = { x: 0, y: 0, width: 0, height: 0 }; protected boundingRect: BoundingRect = { x: 0, y: 0, width: 0, height: 0 };
protected axisPosition: AxisPosition = 'left'; protected axisPosition: AxisPosition = 'left';
private range: [number, number]; private range: [number, number];
@@ -22,7 +24,7 @@ export abstract class BaseAxis implements IAxis {
constructor( constructor(
protected axisConfig: XYChartAxisConfig, protected axisConfig: XYChartAxisConfig,
protected title: string, protected title: string,
protected textDimensionCalculator: ITextDimensionCalculator, protected textDimensionCalculator: TextDimensionCalculator,
protected axisThemeConfig: XYChartAxisThemeConfig protected axisThemeConfig: XYChartAxisThemeConfig
) { ) {
this.range = [0, 10]; this.range = [0, 10];
@@ -58,15 +60,15 @@ export abstract class BaseAxis implements IAxis {
} }
private getLabelDimension(): Dimension { private getLabelDimension(): Dimension {
return this.textDimensionCalculator.getDimension( return this.textDimensionCalculator.getMaxDimension(
this.getTickValues().map((tick) => tick.toString()), this.getTickValues().map((tick) => tick.toString()),
this.axisConfig.labelFontSize this.axisConfig.labelFontSize
); );
} }
recalculateOuterPaddingToDrawBar(): void { recalculateOuterPaddingToDrawBar(): void {
if (0.7 * this.getTickDistance() > this.outerPadding * 2) { if (BAR_WIDTH_TO_TICK_WIDTH_RATIO * this.getTickDistance() > this.outerPadding * 2) {
this.outerPadding = Math.floor((0.7 * this.getTickDistance()) / 2); this.outerPadding = Math.floor((BAR_WIDTH_TO_TICK_WIDTH_RATIO * this.getTickDistance()) / 2);
} }
this.recalculateScale(); this.recalculateScale();
} }
@@ -88,7 +90,7 @@ export abstract class BaseAxis implements IAxis {
availableHeight -= this.axisConfig.tickLength; availableHeight -= this.axisConfig.tickLength;
} }
if (this.axisConfig.showTitle) { if (this.axisConfig.showTitle) {
const spaceRequired = this.textDimensionCalculator.getDimension( const spaceRequired = this.textDimensionCalculator.getMaxDimension(
[this.title], [this.title],
this.axisConfig.labelFontSize this.axisConfig.labelFontSize
); );
@@ -120,7 +122,7 @@ export abstract class BaseAxis implements IAxis {
availableWidth -= this.axisConfig.tickLength; availableWidth -= this.axisConfig.tickLength;
} }
if (this.axisConfig.showTitle) { if (this.axisConfig.showTitle) {
const spaceRequired = this.textDimensionCalculator.getDimension( const spaceRequired = this.textDimensionCalculator.getMaxDimension(
[this.title], [this.title],
this.axisConfig.labelFontSize this.axisConfig.labelFontSize
); );
@@ -270,7 +272,7 @@ export abstract class BaseAxis implements IAxis {
if (this.showLabel) { if (this.showLabel) {
drawableElement.push({ drawableElement.push({
type: 'text', type: 'text',
groupTexts: ['bottom-axis', 'label'], groupTexts: ['top-axis', 'label'],
data: this.getTickValues().map((tick) => ({ data: this.getTickValues().map((tick) => ({
text: tick.toString(), text: tick.toString(),
x: this.getScaleValue(tick), x: this.getScaleValue(tick),

View File

@@ -1,6 +1,6 @@
import { ScaleLinear, scaleLinear } from 'd3'; import { ScaleLinear, scaleLinear } from 'd3';
import { log } from '../../../../../logger.js'; import { log } from '../../../../../logger.js';
import { ITextDimensionCalculator } from '../../TextDimensionCalculator.js'; import { TextDimensionCalculator } from '../../TextDimensionCalculator.js';
import { BaseAxis } from './BaseAxis.js'; import { BaseAxis } from './BaseAxis.js';
import { XYChartAxisThemeConfig, XYChartAxisConfig } from '../../Interfaces.js'; import { XYChartAxisThemeConfig, XYChartAxisConfig } from '../../Interfaces.js';
@@ -13,7 +13,7 @@ export class LinearAxis extends BaseAxis {
axisThemeConfig: XYChartAxisThemeConfig, axisThemeConfig: XYChartAxisThemeConfig,
domain: [number, number], domain: [number, number],
title: string, title: string,
textDimensionCalculator: ITextDimensionCalculator textDimensionCalculator: TextDimensionCalculator
) { ) {
super(axisConfig, title, textDimensionCalculator, axisThemeConfig); super(axisConfig, title, textDimensionCalculator, axisThemeConfig);
this.domain = domain; this.domain = domain;

View File

@@ -1,3 +1,4 @@
import { SVGGType } from '../../../xychartDb.js';
import { import {
AxisDataType, AxisDataType,
ChartComponent, ChartComponent,
@@ -11,7 +12,7 @@ import { LinearAxis } from './LinearAxis.js';
export type AxisPosition = 'left' | 'right' | 'top' | 'bottom'; export type AxisPosition = 'left' | 'right' | 'top' | 'bottom';
export interface IAxis extends ChartComponent { export interface Axis extends ChartComponent {
getScaleValue(value: string | number): number; getScaleValue(value: string | number): number;
setAxisPosition(axisPosition: AxisPosition): void; setAxisPosition(axisPosition: AxisPosition): void;
getAxisOuterPadding(): number; getAxisOuterPadding(): number;
@@ -24,9 +25,9 @@ export function getAxis(
data: AxisDataType, data: AxisDataType,
axisConfig: XYChartAxisConfig, axisConfig: XYChartAxisConfig,
axisThemeConfig: XYChartAxisThemeConfig, axisThemeConfig: XYChartAxisThemeConfig,
fontFamily?: string tmpSVGGElem: SVGGType
): IAxis { ): Axis {
const textDimansionCalculator = new TextDimensionCalculatorWithFont(fontFamily); const textDimansionCalculator = new TextDimensionCalculatorWithFont(tmpSVGGElem);
if (isBandAxisData(data)) { if (isBandAxisData(data)) {
return new BandAxis( return new BandAxis(
axisConfig, axisConfig,

View File

@@ -1,12 +1,12 @@
import { BarPlotData, BoundingRect, DrawableElem, XYChartConfig } from '../../Interfaces.js'; import { BarPlotData, BoundingRect, DrawableElem, XYChartConfig } from '../../Interfaces.js';
import { IAxis } from '../axis/index.js'; import { Axis } from '../axis/index.js';
export class BarPlot { export class BarPlot {
constructor( constructor(
private barData: BarPlotData, private barData: BarPlotData,
private boundingRect: BoundingRect, private boundingRect: BoundingRect,
private xAxis: IAxis, private xAxis: Axis,
private yAxis: IAxis, private yAxis: Axis,
private orientation: XYChartConfig['chartOrientation'], private orientation: XYChartConfig['chartOrientation'],
private plotIndex: number private plotIndex: number
) {} ) {}

View File

@@ -1,12 +1,12 @@
import { line } from 'd3'; import { line } from 'd3';
import { DrawableElem, LinePlotData, XYChartConfig } from '../../Interfaces.js'; import { DrawableElem, LinePlotData, XYChartConfig } from '../../Interfaces.js';
import { IAxis } from '../axis/index.js'; import { Axis } from '../axis/index.js';
export class LinePlot { export class LinePlot {
constructor( constructor(
private plotData: LinePlotData, private plotData: LinePlotData,
private xAxis: IAxis, private xAxis: Axis,
private yAxis: IAxis, private yAxis: Axis,
private orientation: XYChartConfig['chartOrientation'], private orientation: XYChartConfig['chartOrientation'],
private plotIndex: number private plotIndex: number
) {} ) {}

View File

@@ -7,20 +7,20 @@ import {
XYChartThemeConfig, XYChartThemeConfig,
XYChartConfig, XYChartConfig,
} from '../../Interfaces.js'; } from '../../Interfaces.js';
import { IAxis } from '../axis/index.js'; import { Axis } from '../axis/index.js';
import { ChartComponent } from '../../Interfaces.js'; import { ChartComponent } from '../../Interfaces.js';
import { LinePlot } from './LinePlot.js'; import { LinePlot } from './LinePlot.js';
import { PlotBorder } from './PlotBorder.js'; import { PlotBorder } from './PlotBorder.js';
import { BarPlot } from './BarPlot.js'; import { BarPlot } from './BarPlot.js';
export interface IPlot extends ChartComponent { export interface Plot extends ChartComponent {
setAxes(xAxis: IAxis, yAxis: IAxis): void; setAxes(xAxis: Axis, yAxis: Axis): void;
} }
export class Plot implements IPlot { export class Plot implements Plot {
private boundingRect: BoundingRect; private boundingRect: BoundingRect;
private xAxis?: IAxis; private xAxis?: Axis;
private yAxis?: IAxis; private yAxis?: Axis;
constructor( constructor(
private chartConfig: XYChartConfig, private chartConfig: XYChartConfig,
@@ -34,7 +34,7 @@ export class Plot implements IPlot {
height: 0, height: 0,
}; };
} }
setAxes(xAxis: IAxis, yAxis: IAxis) { setAxes(xAxis: Axis, yAxis: Axis) {
this.xAxis = xAxis; this.xAxis = xAxis;
this.yAxis = yAxis; this.yAxis = yAxis;
} }
@@ -99,6 +99,6 @@ export function getPlotComponent(
chartConfig: XYChartConfig, chartConfig: XYChartConfig,
chartData: XYChartData, chartData: XYChartData,
chartThemeConfig: XYChartThemeConfig chartThemeConfig: XYChartThemeConfig
): IPlot { ): Plot {
return new Plot(chartConfig, chartData, chartThemeConfig); return new Plot(chartConfig, chartData, chartThemeConfig);
} }

View File

@@ -1,4 +1,5 @@
import { log } from '../../../logger.js'; import { log } from '../../../logger.js';
import { SVGGType } from '../xychartDb.js';
import { DrawableElem, XYChartData, XYChartConfig, XYChartThemeConfig } from './Interfaces.js'; import { DrawableElem, XYChartData, XYChartConfig, XYChartThemeConfig } from './Interfaces.js';
import { Orchestrator } from './Orchestrator.js'; import { Orchestrator } from './Orchestrator.js';
@@ -6,12 +7,13 @@ export class XYChartBuilder {
static build( static build(
config: XYChartConfig, config: XYChartConfig,
chartData: XYChartData, chartData: XYChartData,
chartThemeConfig: XYChartThemeConfig chartThemeConfig: XYChartThemeConfig,
tmpSVGGElem: SVGGType
): DrawableElem[] { ): DrawableElem[] {
log.trace(`Build start with Config: ${JSON.stringify(config, null, 2)}`); log.trace(`Build start with Config: ${JSON.stringify(config, null, 2)}`);
log.trace(`Build start with ChartData: ${JSON.stringify(chartData, null, 2)}`); log.trace(`Build start with ChartData: ${JSON.stringify(chartData, null, 2)}`);
log.trace(`Build start with ChartThemeConfig: ${JSON.stringify(chartThemeConfig, null, 2)}`); log.trace(`Build start with ChartThemeConfig: ${JSON.stringify(chartThemeConfig, null, 2)}`);
const orchestrator = new Orchestrator(config, chartData, chartThemeConfig); const orchestrator = new Orchestrator(config, chartData, chartThemeConfig, tmpSVGGElem);
return orchestrator.getDrawableElement(); return orchestrator.getDrawableElement();
} }
} }

View File

@@ -15,9 +15,9 @@
%s data %s data
%s data_inner %s data_inner
%% %%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; } \%\%\{ { this.pushState('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; } <open_directive>((?:(?!\}\%\%)[^:.])*) { this.pushState('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; } <type_directive>":" { this.popState(); this.pushState('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; } <type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive'; <arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
\%\%(?!\{)[^\n]* /* skip comments */ \%\%(?!\{)[^\n]* /* skip comments */
@@ -27,41 +27,39 @@
[\n\r]+ return 'NEWLINE'; [\n\r]+ return 'NEWLINE';
\%\%[^\n]* /* do nothing */ \%\%[^\n]* /* do nothing */
title { this.begin("title"); return 'title'; } "title" { return 'title'; }
<title>(?!\n|;|#)*[^\n]* { this.popState(); return "title_value"; }
"accTitle"\s*":"\s* { this.pushState("acc_title");return 'acc_title'; }
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; } <acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; } "accDescr"\s*":"\s* { this.pushState("acc_descr");return 'acc_descr'; }
<acc_descr>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; } <acc_descr>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; }
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");} "accDescr"\s*"{"\s* { this.pushState("acc_descr_multiline");}
<acc_descr_multiline>[\}] { this.popState(); } <acc_descr_multiline>[\}] { this.popState(); }
<acc_descr_multiline>[^\}]* { return "acc_descr_multiline_value"; } <acc_descr_multiline>[^\}]* { return "acc_descr_multiline_value"; }
"xychart-beta" {return 'XYCHART';} "xychart-beta" {return 'XYCHART';}
("vertical"|"horizontal") {return 'CHART_ORIENTATION'} ("vertical"|"horizontal") {return 'CHART_ORIENTATION'}
"x-axis" { this.begin("axis_data"); return "X_AXIS"; } "x-axis" { this.pushState("axis_data"); return "X_AXIS"; }
"y-axis" { this.begin("axis_data"); return "Y_AXIS"; } "y-axis" { this.pushState("axis_data"); return "Y_AXIS"; }
<axis_data>[\[] { this.popState(); return 'SQUARE_BRACES_START'; } <axis_data>[\[] { this.popState(); return 'SQUARE_BRACES_START'; }
<axis_data>[+-]?\d+(?:\.\d+)? { return 'NUMBER_WITH_DECIMAL'; } <axis_data>[+-]?(?:\d+(?:\.\d+)?|\.\d+) { return 'NUMBER_WITH_DECIMAL'; }
<axis_data>"-->" { return 'ARROW_DELIMITER'; } <axis_data>"-->" { return 'ARROW_DELIMITER'; }
"line" { this.begin("data"); return 'LINE'; } "line" { this.pushState("data"); return 'LINE'; }
"bar" { this.begin("data"); return 'BAR'; } "bar" { this.pushState("data"); return 'BAR'; }
<data>[\[] { this.popState(); this.begin("data_inner"); return 'SQUARE_BRACES_START'; } <data>[\[] { this.pushState("data_inner"); return 'SQUARE_BRACES_START'; }
<data_inner>[+-]?\d+(?:\.\d+)? { return 'NUMBER_WITH_DECIMAL';} <data_inner>[+-]?(?:\d+(?:\.\d+)?|\.\d+) { return 'NUMBER_WITH_DECIMAL';}
<data_inner>[\]] { this.popState(); return 'SQUARE_BRACES_END'; } <data_inner>[\]] { this.popState(); return 'SQUARE_BRACES_END'; }
["][`] { this.begin("md_string");} ["][`] { this.pushState("md_string");}
<md_string>[^`"]+ { return "MD_STR";} <md_string>[^`"]+ { return "MD_STR";}
<md_string>[`]["] { this.popState();} <md_string>[`]["] { this.popState();}
["] this.begin("string"); ["] this.pushState("string");
<string>["] this.popState(); <string>["] this.popState();
<string>[^"]* return "STR"; <string>[^"]* return "STR";
@@ -72,7 +70,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
":" return 'COLON'; ":" return 'COLON';
\+ return 'PLUS'; \+ return 'PLUS';
"," return 'COMMA'; "," return 'COMMA';
"=" return 'EQUALS';
\= return 'EQUALS'; \= return 'EQUALS';
"*" return 'MULT'; "*" return 'MULT';
\# return 'BRKT'; \# return 'BRKT';
@@ -81,9 +78,8 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
"&" return 'AMP'; "&" return 'AMP';
\- return 'MINUS'; \- return 'MINUS';
[0-9]+ return 'NUM'; [0-9]+ return 'NUM';
\s /* skip */ \s+ /* skip */
";" return 'SEMI'; ";" return 'SEMI';
[!"#$%&'*+,-.`?\\_/] return 'PUNCTUATION';
<<EOF>> return 'EOF'; <<EOF>> return 'EOF';
/lex /lex
@@ -96,61 +92,79 @@ start
: eol start : eol start
| directive start | directive start
| XYCHART chartConfig start | XYCHART chartConfig start
| XYCHART start | XYCHART start
| document | document
; ;
chartConfig chartConfig
: CHART_ORIENTATION { yy.setOrientation($1); } : CHART_ORIENTATION { yy.setOrientation($1); }
; ;
document document
: /* empty */ : /* empty */
| document statement | document statement
; ;
line
: statement
;
statement statement
: statement eol : statement eol
| title title_value { yy.setDiagramTitle($title_value.trim()); } | title text { yy.setDiagramTitle($text.text.trim()); }
| X_AXIS parseXAxis | X_AXIS parseXAxis
| Y_AXIS parseYAxis | Y_AXIS parseYAxis
| LINE parseLineData { yy.setLineData({text: '', type: 'text'}, $parseLineData); } | LINE plotData { yy.setLineData({text: '', type: 'text'}, $plotData); }
| LINE text parseLineData { yy.setLineData($text, $parseLineData); } | LINE text plotData { yy.setLineData($text, $plotData); }
| BAR parseBarData { yy.setBarData({text: '', type: 'text'}, $parseBarData); } | BAR plotData { yy.setBarData({text: '', type: 'text'}, $plotData); }
| BAR text parseBarData { yy.setBarData($text, $parseBarData); } | BAR text plotData { yy.setBarData($text, $plotData); }
;
parseLineData
: SQUARE_BRACES_START NUMBER_WITH_DECIMAL parseLineData {$parseLineData.unshift(Number($NUMBER_WITH_DECIMAL)); $$ = $parseLineData}
| COMMA NUMBER_WITH_DECIMAL parseLineData {$parseLineData.unshift(Number($NUMBER_WITH_DECIMAL)); $$ = $parseLineData;}
| SQUARE_BRACES_END {$$ = []}
; ;
parseBarData plotData
: SQUARE_BRACES_START NUMBER_WITH_DECIMAL parseBarData {$parseBarData.unshift(Number($NUMBER_WITH_DECIMAL)); $$ = $parseBarData} : SQUARE_BRACES_START commaSeperateNumber SQUARE_BRACES_END { $$ = $commaSeperateNumber }
| COMMA NUMBER_WITH_DECIMAL parseBarData {$parseBarData.unshift(Number($NUMBER_WITH_DECIMAL)); $$ = $parseBarData;}
| SQUARE_BRACES_END {$$ = []}
; ;
commaSeperateNumber
: NUMBER_WITH_DECIMAL commaSeperateNumberValues {
$commaSeperateNumberValues = $commaSeperateNumberValues || [];
$commaSeperateNumberValues.unshift(Number($NUMBER_WITH_DECIMAL));
$$ = $commaSeperateNumberValues
}
;
commaSeperateNumberValues
: COMMA NUMBER_WITH_DECIMAL commaSeperateNumberValues {
$commaSeperateNumberValues = $commaSeperateNumberValues || [];
$commaSeperateNumberValues.unshift(Number($NUMBER_WITH_DECIMAL));
$$ = $commaSeperateNumberValues
}
|;
parseXAxis parseXAxis
: text {yy.setXAxisTitle($text);} : text {yy.setXAxisTitle($text);}
| text xAxisBandData {yy.setXAxisTitle($text); yy.setXAxisBand($xAxisBandData);} | text bandData {yy.setXAxisTitle($text); yy.setXAxisBand($bandData);}
| text NUMBER_WITH_DECIMAL ARROW_DELIMITER NUMBER_WITH_DECIMAL {yy.setXAxisTitle($text); yy.setXAxisRangeData(Number($2), Number($4));} | text NUMBER_WITH_DECIMAL ARROW_DELIMITER NUMBER_WITH_DECIMAL {yy.setXAxisTitle($text); yy.setXAxisRangeData(Number($NUMBER_WITH_DECIMAL1), Number($NUMBER_WITH_DECIMAL2));}
; ;
xAxisBandData bandData
: SQUARE_BRACES_START text xAxisBandData {$xAxisBandData.unshift($text); $$ = $xAxisBandData} : SQUARE_BRACES_START commaSeperateText SQUARE_BRACES_END {$$ = $commaSeperateText}
| COMMA text xAxisBandData {$xAxisBandData.unshift($text); $$ = $xAxisBandData;}
| SQUARE_BRACES_END {$$ = []}
; ;
commaSeperateText
: text commaSeperateTextValues {
$commaSeperateTextValues = $commaSeperateTextValues || [];
$commaSeperateTextValues.unshift($text);
$$ = $commaSeperateTextValues
}
;
commaSeperateTextValues
: COMMA text commaSeperateTextValues {
$commaSeperateTextValues = $commaSeperateTextValues || [];
$commaSeperateTextValues.unshift($text);
$$ = $commaSeperateTextValues
}
|;
parseYAxis parseYAxis
: text {yy.setYAxisTitle($text);} : text {yy.setYAxisTitle($text);}
| text NUMBER_WITH_DECIMAL ARROW_DELIMITER NUMBER_WITH_DECIMAL {yy.setYAxisTitle($text); yy.setYAxisRangeData(Number($2), Number($4));} | text NUMBER_WITH_DECIMAL ARROW_DELIMITER NUMBER_WITH_DECIMAL {yy.setYAxisTitle($text); yy.setYAxisRangeData(Number($NUMBER_WITH_DECIMAL1), Number($NUMBER_WITH_DECIMAL2));}
; ;
directive directive
@@ -180,22 +194,17 @@ closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'xychart'); } : close_directive { yy.parseDirective('}%%', 'close_directive', 'xychart'); }
; ;
text: alphaNum text: alphaNum { $$={text:$alphaNum, type: 'text'};}
{ $$={text:$alphaNum, type: 'text'};} | STR { $$={text: $STR, type: 'text'};}
| STR | MD_STR { $$={text: $MD_STR, type: 'markdown'};}
{ $$={text: $STR, type: 'text'};}
| MD_STR
{ $$={text: $MD_STR, type: 'markdown'};}
; ;
alphaNum alphaNum
: alphaNumToken : alphaNumToken {$$=$alphaNumToken;}
{$$=$alphaNumToken;} | alphaNum alphaNumToken {$$=$alphaNum+''+$alphaNumToken;}
| alphaNum alphaNumToken
{$$=$alphaNum+''+$alphaNumToken;}
; ;
alphaNumToken : PUNCTUATION | AMP | NUM | ALPHA | PLUS | EQUALS | MULT | DOT | BRKT| UNDERSCORE ; alphaNumToken : AMP | NUM | ALPHA | PLUS | EQUALS | MULT | DOT | BRKT| MINUS | UNDERSCORE ;
%% %%

View File

@@ -43,11 +43,16 @@ describe('Testing xychart jison file', () => {
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
}); });
it('parse title of the chart', () => { it('parse title of the chart within "', () => {
const str = 'xychart-beta \n title This is a title'; const str = 'xychart-beta \n title "This is a title"';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setDiagramTitle).toHaveBeenCalledWith('This is a title'); 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('should be able to parse directive', () => { it('should be able to parse directive', () => {
const str = const str =
@@ -63,13 +68,13 @@ describe('Testing xychart jison file', () => {
}); });
it('parse chart orientation', () => { it('parse chart orientation', () => {
let str = 'xychart-beta vertical'; const str = 'xychart-beta vertical';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setOrientation).toHaveBeenCalledWith('vertical'); expect(mockDB.setOrientation).toHaveBeenCalledWith('vertical');
});
clearMocks(); it('parse chart orientation with spaces', () => {
let str = 'xychart-beta horizontal ';
str = 'xychart-beta horizontal ';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setOrientation).toHaveBeenCalledWith('horizontal'); expect(mockDB.setOrientation).toHaveBeenCalledWith('horizontal');
@@ -78,52 +83,66 @@ describe('Testing xychart jison file', () => {
}); });
it('parse x-axis', () => { it('parse x-axis', () => {
let str = 'xychart-beta \nx-axis xAxisName\n'; const str = 'xychart-beta \nx-axis xAxisName\n';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
text: 'xAxisName', text: 'xAxisName',
type: 'text', type: 'text',
}); });
});
clearMocks(); it('parse x-axis with axis name without "', () => {
const str = 'xychart-beta \nx-axis xAxisName \n';
str = 'xychart-beta \nx-axis xAxisName \n';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
text: 'xAxisName', text: 'xAxisName',
type: 'text', type: 'text',
}); });
});
clearMocks(); it('parse x-axis with axis name with "', () => {
const str = 'xychart-beta \n x-axis "xAxisName has space"\n';
str = 'xychart-beta \n x-axis "xAxisName has space"\n';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
text: 'xAxisName has space', text: 'xAxisName has space',
type: 'text', type: 'text',
}); });
});
clearMocks(); it('parse x-axis with axis name with " with spaces', () => {
const str = 'xychart-beta \n x-axis " xAxisName has space " \n';
str = 'xychart-beta \n x-axis " xAxisName has space " \n';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
text: ' xAxisName has space ', text: ' xAxisName has space ',
type: 'text', type: 'text',
}); });
});
clearMocks(); it('parse x-axis with axis name and range data', () => {
str = 'xychart-beta \nx-axis xAxisName 45.5 --> 33 \n'; const str = 'xychart-beta \nx-axis xAxisName 45.5 --> 33 \n';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
text: 'xAxisName', text: 'xAxisName',
type: 'text', type: 'text',
}); });
expect(mockDB.setXAxisRangeData).toHaveBeenCalledWith(45.5, 33); 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);
});
clearMocks(); it('parse x-axis with axis name and category data', () => {
const str = 'xychart-beta \nx-axis xAxisName [ "cat1" , cat2a ] \n ';
str = 'xychart-beta \nx-axis xAxisName [ "cat1" , cat2a ] \n ';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
text: 'xAxisName', text: 'xAxisName',
@@ -136,9 +155,17 @@ describe('Testing xychart jison file', () => {
}, },
{ text: 'cat2a', type: 'text' }, { text: 'cat2a', type: 'text' },
]); ]);
clearMocks(); });
str = `xychart-beta \n x-axis "this is x axis" [category1, "category 2", category3]\n`; 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(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'this is x axis', type: 'text' }); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'this is x axis', type: 'text' });
expect(mockDB.setXAxisBand).toHaveBeenCalledWith([ expect(mockDB.setXAxisBand).toHaveBeenCalledWith([
@@ -146,9 +173,11 @@ describe('Testing xychart jison file', () => {
{ text: 'category 2', type: 'text' }, { text: 'category 2', type: 'text' },
{ text: 'category3', type: 'text' }, { text: 'category3', type: 'text' },
]); ]);
clearMocks(); });
str = 'xychart-beta \nx-axis xAxisName [ "cat1 with space" , cat2 , cat3] \n '; 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(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' }); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
expect(mockDB.setXAxisBand).toHaveBeenCalledWith([ expect(mockDB.setXAxisBand).toHaveBeenCalledWith([
@@ -156,9 +185,11 @@ describe('Testing xychart jison file', () => {
{ text: 'cat2', type: 'text' }, { text: 'cat2', type: 'text' },
{ text: 'cat3', type: 'text' }, { text: 'cat3', type: 'text' },
]); ]);
clearMocks(); });
str = 'xychart-beta \nx-axis xAxisName [ "cat1 with space" , cat2 asdf , cat3] \n '; 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(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' }); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
expect(mockDB.setXAxisBand).toHaveBeenCalledWith([ expect(mockDB.setXAxisBand).toHaveBeenCalledWith([
@@ -167,77 +198,61 @@ describe('Testing xychart jison file', () => {
{ text: 'cat3', type: 'text' }, { text: 'cat3', type: 'text' },
]); ]);
}); });
it('parse y-axis', () => {
let str = 'xychart-beta \ny-axis yAxisName\n'; it('parse y-axis with axis name', () => {
const str = 'xychart-beta \ny-axis yAxisName\n';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
});
clearMocks(); it('parse y-axis with axis name with spaces', () => {
const str = 'xychart-beta \ny-axis yAxisName \n';
str = 'xychart-beta \ny-axis yAxisName \n';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
});
clearMocks(); it('parse y-axis with axis name with "', () => {
const str = 'xychart-beta \n y-axis "yAxisName has space"\n';
str = 'xychart-beta \n y-axis "yAxisName has space"\n';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({
text: 'yAxisName has space', text: 'yAxisName has space',
type: 'text', type: 'text',
}); });
});
clearMocks(); it('parse y-axis with axis name with " and spaces', () => {
const str = 'xychart-beta \n y-axis " yAxisName has space " \n';
str = 'xychart-beta \n y-axis " yAxisName has space " \n';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({
text: ' yAxisName has space ', text: ' yAxisName has space ',
type: 'text', type: 'text',
}); });
});
clearMocks(); it('parse y-axis with axis name with range data', () => {
str = 'xychart-beta \ny-axis yAxisName 45.5 --> 33 \n'; const str = 'xychart-beta \ny-axis yAxisName 45.5 --> 33 \n';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
expect(mockDB.setYAxisRangeData).toHaveBeenCalledWith(45.5, 33); expect(mockDB.setYAxisRangeData).toHaveBeenCalledWith(45.5, 33);
});
clearMocks(); 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';
str = 'xychart-beta \ny-axis yAxisName [ 45.3, 33 ] \n';
expect(parserFnConstructor(str)).toThrow();
clearMocks();
str = 'xychart-beta \ny-axis yAxisName\n';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
expect(mockDB.setYAxisRangeData).toHaveBeenCalledWith(45.5, 0.33);
}); });
it('parse both axis', () => { it('parse y-axis throw error for invalid number in range data', () => {
let str = 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n'; 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(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' }); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
clearMocks();
str = 'xychart-beta \nx-axis xAxisName\n';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
text: 'xAxisName',
type: 'text',
});
clearMocks();
str = 'xychart-beta \ny-axis yAxisName';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
clearMocks();
}); });
it('parse line Data', () => { it('parse line Data', () => {
let str = 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line lineTitle [23, 45, 56.6]'; const str = 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line lineTitle [23, 45, 56.6]';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setLineData).toHaveBeenCalledWith( expect(mockDB.setLineData).toHaveBeenCalledWith(
{ text: 'lineTitle', type: 'text' }, { text: 'lineTitle', type: 'text' },
@@ -245,10 +260,9 @@ describe('Testing xychart jison file', () => {
); );
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' }); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
});
clearMocks(); it('parse line Data with spaces and +,- symbols', () => {
const str =
str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , -45 , 56.6 ] '; 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , -45 , 56.6 ] ';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
@@ -257,32 +271,58 @@ describe('Testing xychart jison file', () => {
{ text: 'lineTitle with space', type: 'text' }, { text: 'lineTitle with space', type: 'text' },
[23, -45, 56.6] [23, -45, 56.6]
); );
});
// set line data without title it('parse line Data without title', () => {
str = 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line [ +23 , -45 , 56.6 ] '; const str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line [ +23 , -45 , 56.6 , .33] ';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' }); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
expect(mockDB.setLineData).toHaveBeenCalledWith({ text: '', type: 'text' }, [23, -45, 56.6]); expect(mockDB.setLineData).toHaveBeenCalledWith(
{ text: '', type: 'text' },
clearMocks(); [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 = 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 ] '; 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , -4aa5 , 56.6 ] ';
expect(parserFnConstructor(str)).toThrow(); expect(parserFnConstructor(str)).toThrow();
}); });
it('parse bar Data', () => { it('parse bar Data', () => {
let str = 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar barTitle [23, 45, 56.6]'; const str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar barTitle [23, 45, 56.6, .22]';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' }); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
expect(mockDB.setBarData).toHaveBeenCalledWith( expect(mockDB.setBarData).toHaveBeenCalledWith(
{ text: 'barTitle', type: 'text' }, { text: 'barTitle', type: 'text' },
[23, 45, 56.6] [23, 45, 56.6, 0.22]
); );
});
clearMocks(); it('parse bar Data spaces and +,- symbol', () => {
const str =
str =
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , -45 , 56.6 ] '; 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , -45 , 56.6 ] ';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
@@ -291,22 +331,45 @@ describe('Testing xychart jison file', () => {
{ text: 'barTitle with space', type: 'text' }, { text: 'barTitle with space', type: 'text' },
[23, -45, 56.6] [23, -45, 56.6]
); );
clearMocks(); });
it('parse bar Data without plot title', () => {
// set bar data without title const str =
str = 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar [ +23 , -45 , 56.6 ] '; 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar [ +23 , -45 , 56.6 ] ';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' }); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
expect(mockDB.setBarData).toHaveBeenCalledWith({ text: '', type: 'text' }, [23, -45, 56.6]); expect(mockDB.setBarData).toHaveBeenCalledWith({ text: '', type: 'text' }, [23, -45, 56.6]);
clearMocks(); clearMocks();
});
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 = 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 ] '; 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , -4aa5 , 56.6 ] ';
expect(parserFnConstructor(str)).toThrow(); expect(parserFnConstructor(str)).toThrow();
}); });
it('parse multiple bar and line', () => { it('parse multiple bar and line varient 1', () => {
let str = 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]'; '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(parserFnConstructor(str)).not.toThrow();
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
@@ -327,9 +390,9 @@ describe('Testing xychart jison file', () => {
{ text: 'lineTitle2', type: 'text' }, { text: 'lineTitle2', type: 'text' },
[45, 99, 12] [45, 99, 12]
); );
clearMocks(); });
it('parse multiple bar and line varient 2', () => {
str = ` const str = `
xychart-beta horizontal xychart-beta horizontal
title Basic xychart title Basic xychart
x-axis "this is x axis" [category1, "category 2", category3] x-axis "this is x axis" [category1, "category 2", category3]

View File

@@ -1,5 +1,6 @@
// @ts-ignore: TODO Fix ts errors // @ts-ignore: TODO Fix ts errors
import { adjust, channel } from 'khroma'; import { adjust, channel } from 'khroma';
import { Selection } from 'd3-selection';
import mermaidAPI from '../../mermaidAPI.js'; import mermaidAPI from '../../mermaidAPI.js';
import * as configApi from '../../config.js'; import * as configApi from '../../config.js';
import defaultConfig from '../../defaultConfig.js'; import defaultConfig from '../../defaultConfig.js';
@@ -25,12 +26,30 @@ import {
} from './chartBuilder/Interfaces.js'; } from './chartBuilder/Interfaces.js';
import { getThemeVariables } from '../../themes/theme-default.js'; import { getThemeVariables } from '../../themes/theme-default.js';
export type SVGGType = Selection<SVGGElement, unknown, HTMLElement, any>;
const defaultThemeVariables = getThemeVariables(); const defaultThemeVariables = getThemeVariables();
const config = configApi.getConfig(); const config = configApi.getConfig();
let plotIndex = 0; let plotIndex = 0;
let tmpSVGGElem: SVGGType;
let xyChartConfig: XYChartConfig = getChartDefaultConfig();
let xyChartThemeConfig: XYChartThemeConfig = getChartDefaultThemeConfig();
let xyChartData: XYChartData = getChartDefalutData();
let plotColorPalette = Array.isArray(xyChartThemeConfig.plotBaseColor)
? xyChartThemeConfig.plotBaseColor
: plotColorPaletteGenerator(xyChartThemeConfig.plotBaseColor);
let hasSetXAxis = false;
let hasSetYAxis = false;
interface NormalTextType {
type: 'text';
text: string;
}
function plotColorPaletteGenerator(baseColor: string, noOfColorNeeded = 15): string[] { function plotColorPaletteGenerator(baseColor: string, noOfColorNeeded = 15): string[] {
const colors = []; const colors = [];
const MAX_HUE_VALUE = 360; const MAX_HUE_VALUE = 360;
@@ -110,20 +129,6 @@ function getChartDefalutData(): XYChartData {
}; };
} }
let xyChartConfig: XYChartConfig = getChartDefaultConfig();
let xyChartThemeConfig: XYChartThemeConfig = getChartDefaultThemeConfig();
let xyChartData: XYChartData = getChartDefalutData();
let plotColorPalette = Array.isArray(xyChartThemeConfig.plotBaseColor)
? xyChartThemeConfig.plotBaseColor
: plotColorPaletteGenerator(xyChartThemeConfig.plotBaseColor);
let hasSetXAxis = false;
let hasSetYAxis = false;
interface NormalTextType {
type: 'text';
text: string;
}
function textSanitizer(text: string) { function textSanitizer(text: string) {
return sanitizeText(text.trim(), config); return sanitizeText(text.trim(), config);
} }
@@ -133,6 +138,9 @@ function parseDirective(statement: string, context: string, type: string) {
mermaidAPI.parseDirective(this, statement, context, type); mermaidAPI.parseDirective(this, statement, context, type);
} }
function setTmpSVGG(SVGG: SVGGType) {
tmpSVGGElem = SVGG;
}
function setOrientation(oriantation: string) { function setOrientation(oriantation: string) {
if (oriantation === 'horizontal') { if (oriantation === 'horizontal') {
xyChartConfig.chartOrientation = 'horizontal'; xyChartConfig.chartOrientation = 'horizontal';
@@ -177,7 +185,7 @@ function setYAxisRangeFromPlotData(data: number[]) {
}; };
} }
function transformDataWithOutCategory(data: number[]): SimplePlotDataType { function transformDataWithoutCategory(data: number[]): SimplePlotDataType {
let retData: SimplePlotDataType = []; let retData: SimplePlotDataType = [];
if (data.length === 0) { if (data.length === 0) {
return retData; return retData;
@@ -214,7 +222,7 @@ function getPlotColorFromPalette(plotIndex: number): string {
} }
function setLineData(title: NormalTextType, data: number[]) { function setLineData(title: NormalTextType, data: number[]) {
const plotData = transformDataWithOutCategory(data); const plotData = transformDataWithoutCategory(data);
xyChartData.plots.push({ xyChartData.plots.push({
type: 'line', type: 'line',
strokeFill: getPlotColorFromPalette(plotIndex), strokeFill: getPlotColorFromPalette(plotIndex),
@@ -225,7 +233,7 @@ function setLineData(title: NormalTextType, data: number[]) {
} }
function setBarData(title: NormalTextType, data: number[]) { function setBarData(title: NormalTextType, data: number[]) {
const plotData = transformDataWithOutCategory(data); const plotData = transformDataWithoutCategory(data);
xyChartData.plots.push({ xyChartData.plots.push({
type: 'bar', type: 'bar',
fill: getPlotColorFromPalette(plotIndex), fill: getPlotColorFromPalette(plotIndex),
@@ -239,7 +247,7 @@ function getDrawableElem(): DrawableElem[] {
throw Error('No Plot to render, please provide a plot with some data'); throw Error('No Plot to render, please provide a plot with some data');
} }
xyChartData.title = getDiagramTitle(); xyChartData.title = getDiagramTitle();
return XYChartBuilder.build(xyChartConfig, xyChartData, xyChartThemeConfig); return XYChartBuilder.build(xyChartConfig, xyChartData, xyChartThemeConfig, tmpSVGGElem);
} }
function setHeight(height: number) { function setHeight(height: number) {
@@ -283,4 +291,5 @@ export default {
setYAxisRangeData, setYAxisRangeData,
setLineData, setLineData,
setBarData, setBarData,
setTmpSVGG,
}; };

View File

@@ -8,5 +8,4 @@ export const diagram: DiagramDefinition = {
parser, parser,
db, db,
renderer, renderer,
styles: () => '',
}; };

View File

@@ -9,6 +9,7 @@ import {
TextHorizontalPos, TextHorizontalPos,
TextVerticalPos, TextVerticalPos,
} from './chartBuilder/Interfaces.js'; } from './chartBuilder/Interfaces.js';
import XYChartDB from './xychartDb.js';
export const draw = (txt: string, id: string, _version: string, diagObj: Diagram) => { export const draw = (txt: string, id: string, _version: string, diagObj: Diagram) => {
function getDominantBaseLine(horizontalPos: TextHorizontalPos) { function getDominantBaseLine(horizontalPos: TextHorizontalPos) {
@@ -46,13 +47,13 @@ export const draw = (txt: string, id: string, _version: string, diagObj: Diagram
svg.attr('viewBox', '0 0 ' + width + ' ' + height); svg.attr('viewBox', '0 0 ' + width + ' ' + height);
// @ts-ignore: TODO Fix ts errors const db = diagObj.db as typeof XYChartDB;
diagObj.db.setHeight(height);
// @ts-ignore: TODO Fix ts errors
diagObj.db.setWidth(width);
// @ts-ignore: TODO Fix ts errors db.setHeight(height);
const shapes: DrawableElem[] = diagObj.db.getDrawableElem(); db.setWidth(width);
db.setTmpSVGG(svg.append('g').attr('class', 'mermaid-tmp-group'));
const shapes: DrawableElem[] = db.getDrawableElem();
const groups: Record<string, any> = {}; const groups: Record<string, any> = {};

View File

@@ -94,6 +94,22 @@ function computeWidthOfText(parentNode, lineHeight, text) {
return textLength; return textLength;
} }
/**
* Compute the width of rendered text
* @param {object} parentNode
* @param {number} lineHeight
* @param {string} text
* @returns {{width: number, height: number}}
*/
export function computeDimensionOfText(parentNode, lineHeight, text) {
const testElement = parentNode.append('text');
const testSpan = createTspan(testElement, 1, lineHeight);
updateTextContentAndStyles(testSpan, [{ content: text, type: 'normal' }]);
const textDimension = testSpan.node().getBoundingClientRect();
testElement.remove();
return textDimension;
}
/** /**
* Creates a formatted text element by breaking lines and applying styles based on * Creates a formatted text element by breaking lines and applying styles based on
* the given structuredText. * the given structuredText.