Generated the base architecture

This commit is contained in:
Subhash Halder
2023-06-08 22:00:02 +05:30
parent 14e290bf1a
commit 93697b74f4
18 changed files with 835 additions and 14 deletions

View File

@@ -82,6 +82,8 @@
"@types/d3": "^7.4.0",
"@types/d3-sankey": "^0.12.1",
"@types/d3-selection": "^3.0.5",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/dompurify": "^3.0.2",
"@types/jsdom": "^21.1.1",
"@types/lodash-es": "^4.17.7",

View File

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

View File

@@ -0,0 +1,151 @@
export enum ChartPlotEnum {
LINE = 'line',
}
export enum ChartLayoutElem {
NULL = 'null',
CHART = 'chart',
TITLE = 'title',
XAXISLABEL = 'xaxislabel',
XAXISTITLE = 'xaxistitle',
YAXISLABEL = 'yaxislabel',
YAXISTITLE = 'yaxistitle',
}
export enum XYChartYAxisPosition {
LEFT = 'left',
RIGHT = 'right',
}
export enum OrientationEnum {
VERTICAL = 'vertical',
HORIZONTAL = 'horizontal',
}
export type ChartLayout = ChartLayoutElem[][];
export type VisibilityOption = {
chartTitle: boolean;
xAxisTitle: boolean;
xAxisLabel: boolean;
yAxisTitle: boolean;
yAxisLabel: boolean;
};
export interface XYChartConfig {
width: number;
height: number;
fontFamily: string;
titleFontSize: number;
titleFill: string;
titlePadding: number;
xAxisFontSize: number;
xAxisTitleFontSize: number;
yAxisFontSize: number;
yAxisTitleFontSize: number;
yAxisPosition: XYChartYAxisPosition;
showChartTitle: boolean;
showXAxisLable: boolean;
showXAxisTitle: boolean;
showYAxisLabel: boolean;
showYAxisTitle: boolean;
chartOrientation: OrientationEnum;
plotReservedSpacePercent: number;
}
export type SimplePlotDataType = [string | number, number][];
export interface LinePlotData {
type: ChartPlotEnum.LINE;
data: SimplePlotDataType;
}
export type PlotData = LinePlotData;
export interface BandAxisDataType {
title: string;
categories: string[];
}
export interface LinearAxisDataType{
title: string;
min: number;
max: number;
}
export type AxisDataType = LinearAxisDataType | BandAxisDataType;
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 XYChartSpaceProperty extends BoundingRect {
orientation: OrientationEnum;
}
export interface XYChartSpace {
chart: XYChartSpaceProperty;
title: XYChartSpaceProperty;
xAxisLabels: XYChartSpaceProperty;
xAxisTitle: XYChartSpaceProperty;
yAxisLabel: XYChartSpaceProperty;
yAxisTitle: XYChartSpaceProperty;
}
export interface Point {
x: number;
y: number;
}
export type TextVerticalPos = 'left' | 'center' | 'right';
export type TextHorizontalPos = 'top' | 'middle' | 'bottom';
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 =
| {
groupText: string;
type: 'rect';
data: RectElem[];
}
| {
groupText: string;
type: 'text';
data: TextElem[];
}
| {
groupText: string;
type: 'path';
data: PathElem[];
};

View File

@@ -0,0 +1,66 @@
import { DrawableElem, XYChartConfig, XYChartData } from './Interfaces.js';
import { getChartTitleComponent } from './components/ChartTitle.js';
import { ChartComponent } from './components/Interfaces.js';
import { IAxis, getAxis } from './components/axis/index.js';
import { IPlot, getPlotComponent, isTypeIPlot } from './components/plot/index.js';
export class Orchestrator {
private componentStore: {
title: ChartComponent,
plot: IPlot,
xAxis: IAxis,
yAxis: IAxis,
};
constructor(private chartConfig: XYChartConfig, chartData: XYChartData) {
this.componentStore = {
title: getChartTitleComponent(chartConfig, chartData),
plot: getPlotComponent(chartConfig, chartData),
xAxis: getAxis(chartData.xAxis, chartConfig),
yAxis: getAxis(chartData.yAxis, chartConfig),
};
}
private calculateSpace() {
let availableWidth = this.chartConfig.width;
let availableHeight = this.chartConfig.height;
let chartX = 0;
let chartY = 0;
const chartWidth = Math.floor((availableWidth * this.chartConfig.plotReservedSpacePercent) / 100);
const 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,
});
chartY = spaceUsed.height;
availableWidth -= spaceUsed.width;
availableHeight -= spaceUsed.height;
//
// spaceUsed = this.componentStore.xAxis.calculateSpace({
// width: availableWidth,
// height: availableHeight,
// });
// availableWidth -= spaceUsed.width;
// availableHeight -= spaceUsed.height;
this.componentStore.plot.setBoundingBoxXY({x: chartX, y: chartY});
this.componentStore.xAxis.setRange([chartX, chartX + chartWidth]);
this.componentStore.yAxis.setRange([chartY, chartY + chartHeight]);
}
getDrawableElement() {
this.calculateSpace();
const drawableElem: DrawableElem[] = [];
this.componentStore.plot.setAxes(this.componentStore.xAxis, this.componentStore.yAxis);
for (const component of Object.values(this.componentStore)) {
drawableElem.push(...component.getDrawableElements());
}
return drawableElem;
}
}

View File

@@ -0,0 +1,15 @@
import { Dimension } from './Interfaces.js';
export interface ITextDimensionCalculator {
getDimension(text: string): Dimension;
}
export class TextDimensionCalculator implements ITextDimensionCalculator {
constructor(private fontSize: number, private fontFamily: string) {}
getDimension(text: string): Dimension {
return {
width: text.length * this.fontSize,
height: this.fontSize,
};
}
}

View File

@@ -0,0 +1,89 @@
import { ITextDimensionCalculator, TextDimensionCalculator } from '../TextDimensionCalculator.js';
import {
XYChartConfig,
XYChartData,
Dimension,
BoundingRect,
DrawableElem,
Point,
OrientationEnum,
} from '../Interfaces.js';
import { ChartComponent } from './Interfaces.js';
export class ChartTitle implements ChartComponent {
private boundingRect: BoundingRect;
private showChartTitle: boolean;
private orientation: OrientationEnum;
constructor(
private textDimensionCalculator: ITextDimensionCalculator,
private chartConfig: XYChartConfig,
private chartData: XYChartData
) {
this.boundingRect = {
x: 0,
y: 0,
width: 0,
height: 0,
};
this.showChartTitle = !!(this.chartData.title && this.chartConfig.showChartTitle);
this.orientation = OrientationEnum.VERTICAL;
}
setOrientation(orientation: OrientationEnum): void {
this.orientation = orientation;
}
setBoundingBoxXY(point: Point): void {
this.boundingRect.x = point.x;
this.boundingRect.y = point.y;
}
calculateSpace(availableSpace: Dimension): Dimension {
const titleDimension = this.textDimensionCalculator.getDimension(this.chartData.title);
const widthRequired = Math.max(titleDimension.width, availableSpace.width);
const heightRequired = titleDimension.height + 2 * this.chartConfig.titlePadding;
if (
titleDimension.width <= widthRequired &&
titleDimension.height <= heightRequired &&
this.showChartTitle
) {
this.boundingRect.width = widthRequired;
this.boundingRect.height = heightRequired;
}
return {
width: this.boundingRect.width,
height: this.boundingRect.height,
};
}
getDrawableElements(): DrawableElem[] {
const drawableElem: DrawableElem[] = [];
if (this.boundingRect.height > 0 && this.boundingRect.width > 0) {
drawableElem.push({
groupText: 'chart-title',
type: 'text',
data: [
{
fontSize: this.chartConfig.titleFontSize,
text: this.chartData.title,
verticalPos: 'center',
horizontalPos: 'middle',
x: this.boundingRect.x + this.boundingRect.width / 2,
y: this.boundingRect.y + this.boundingRect.height / 2,
fill: this.chartConfig.titleFill,
rotation: 0,
},
],
});
}
return drawableElem;
}
}
export function getChartTitleComponent(
chartConfig: XYChartConfig,
chartData: XYChartData
): ChartComponent {
const textDimensionCalculator = new TextDimensionCalculator(
chartConfig.titleFontSize,
chartConfig.fontFamily
);
return new ChartTitle(textDimensionCalculator, chartConfig, chartData);
}

View File

@@ -0,0 +1,8 @@
import { Dimension, DrawableElem, OrientationEnum, Point } from '../Interfaces.js';
export interface ChartComponent {
setOrientation(orientation: OrientationEnum): void;
calculateSpace(availableSpace: Dimension): Dimension;
setBoundingBoxXY(point: Point): void;
getDrawableElements(): DrawableElem[];
}

View File

@@ -0,0 +1,54 @@
import { ScaleBand, scaleBand } from 'd3';
import {
Dimension,
Point,
DrawableElem,
BoundingRect,
OrientationEnum,
XYChartConfig,
} from '../../Interfaces.js';
import { IAxis } from './index.js';
export class BandAxis implements IAxis {
private scale: ScaleBand<string>;
private range: [number, number];
private boundingRect: BoundingRect;
private orientation: OrientationEnum;
private categories: string[];
constructor(private chartConfig: XYChartConfig, categories: string[]) {
this.range = [0, 10];
this.categories = categories;
this.scale = scaleBand().domain(this.categories).range(this.range);
this.boundingRect = { x: 0, y: 0, width: 0, height: 0 };
this.orientation = OrientationEnum.VERTICAL;
}
setRange(range: [number, number]): void {
this.range = range;
this.scale = scaleBand().domain(this.categories).range(this.range);
}
setOrientation(orientation: OrientationEnum): void {
this.orientation = orientation;
}
getScaleValue(value: string): number {
return this.scale(value) || this.range[0];
}
calculateSpace(availableSpace: Dimension): Dimension {
return {
width: availableSpace.width,
height: availableSpace.height,
};
}
setBoundingBoxXY(point: Point): void {
this.boundingRect.x = point.x;
this.boundingRect.y = point.y;
}
getDrawableElements(): DrawableElem[] {
return [];
}
}

View File

@@ -0,0 +1,55 @@
import { scaleLinear, ScaleLinear } from 'd3';
import {
Dimension,
Point,
DrawableElem,
BoundingRect,
OrientationEnum,
XYChartConfig,
} from '../../Interfaces.js';
import { IAxis } from './index.js';
export class LinearAxis implements IAxis {
private scale: ScaleLinear<number, number>;
private boundingRect: BoundingRect;
private orientation: OrientationEnum;
private domain: [number, number];
private range: [number, number];
constructor(private chartConfig: XYChartConfig, domain: [number, number]) {
this.domain = domain;
this.range = [0, 10];
this.scale = scaleLinear().domain(this.domain).range(this.range);
this.boundingRect = { x: 0, y: 0, width: 0, height: 0 };
this.orientation = OrientationEnum.VERTICAL;
}
setRange(range: [number, number]): void {
this.range = range;
this.scale = scaleLinear().domain(this.domain).range(this.range);
}
setOrientation(orientation: OrientationEnum): void {
this.orientation = orientation;
}
getScaleValue(value: number): number {
return this.scale(value);
}
calculateSpace(availableSpace: Dimension): Dimension {
return {
width: availableSpace.width,
height: availableSpace.height,
};
}
setBoundingBoxXY(point: Point): void {
this.boundingRect.x = point.x;
this.boundingRect.y = point.y;
}
getDrawableElements(): DrawableElem[] {
return [];
}
}

View File

@@ -0,0 +1,31 @@
import {
AxisDataType,
BandAxisDataType,
BoundingRect,
LinearAxisDataType,
XYChartConfig,
XYChartData,
} from '../../Interfaces.js';
import { ChartComponent } from '../Interfaces.js';
import { BandAxis } from './BandAxis.js';
import { LinearAxis } from './LinearAxis.js';
export interface IAxis extends ChartComponent {
getScaleValue(value: string | number): number;
setRange(range: [number, number]): void;
}
function isLinearAxisData(data: any): data is LinearAxisDataType {
return !(Number.isNaN(data.min) || Number.isNaN(data.max));
}
function isBandAxisData(data: any): data is BandAxisDataType {
return data.categories && Array.isArray(data.categories);
}
export function getAxis(data: AxisDataType, chartConfig: XYChartConfig): IAxis {
if (isBandAxisData(data)) {
return new BandAxis(chartConfig, data.categories);
}
return new LinearAxis(chartConfig, [data.min, data.max]);
}

View File

@@ -0,0 +1,34 @@
import { line } from 'd3';
import { DrawableElem, SimplePlotDataType } from '../../Interfaces.js';
import { IAxis } from '../axis/index.js';
export class LinePlot {
constructor(private data: SimplePlotDataType, private xAxis: IAxis, private yAxis: IAxis) {}
getDrawableElement(): DrawableElem[] {
const finalData: [number, number][] = this.data.map((d) => [
this.xAxis.getScaleValue(d[0]),
this.yAxis.getScaleValue(d[1]),
]);
const path = line()
.x((d) => d[0])
.y((d) => d[1])(finalData);
if (!path) {
return [];
}
return [
{
groupText: 'line-plot',
type: 'path',
data: [
{
path,
strokeFill: '#0000ff',
strokeWidth: 2,
},
],
},
];
}
}

View File

@@ -0,0 +1,81 @@
import {
XYChartConfig,
XYChartData,
Dimension,
BoundingRect,
DrawableElem,
Point,
OrientationEnum,
ChartPlotEnum,
} from '../../Interfaces.js';
import { IAxis } from '../axis/index.js';
import { ChartComponent } from './../Interfaces.js';
import { LinePlot } from './LinePlot.js';
export interface IPlot extends ChartComponent {
setAxes(xAxis: IAxis, yAxis: IAxis): void
}
export class Plot implements IPlot {
private boundingRect: BoundingRect;
private orientation: OrientationEnum;
private xAxis?: IAxis;
private yAxis?: IAxis;
constructor(
private chartConfig: XYChartConfig,
private chartData: XYChartData,
) {
this.boundingRect = {
x: 0,
y: 0,
width: 0,
height: 0,
};
this.orientation = OrientationEnum.VERTICAL;
}
setAxes(xAxis: IAxis, yAxis: IAxis) {
this.xAxis = xAxis;
this.yAxis = yAxis;
}
setOrientation(orientation: OrientationEnum): void {
this.orientation = orientation;
}
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 plot of this.chartData.plots) {
switch(plot.type) {
case ChartPlotEnum.LINE: {
const linePlot = new LinePlot(plot.data, this.xAxis, this.yAxis);
drawableElem.push(...linePlot.getDrawableElement())
}
break;
}
}
return drawableElem;
}
}
export function getPlotComponent(
chartConfig: XYChartConfig,
chartData: XYChartData,
): IPlot {
return new Plot(chartConfig, chartData);
}

View File

@@ -0,0 +1,77 @@
// @ts-ignore: TODO Fix ts errors
import { defaultConfig } from '../../../config.js';
import { log } from '../../../logger.js';
import {
ChartPlotEnum,
DrawableElem,
XYChartConfig,
XYChartData,
OrientationEnum,
XYChartYAxisPosition,
} from './Interfaces.js';
import { Orchestrator } from './Orchestrator.js';
export class XYChartBuilder {
private config: XYChartConfig;
private chartData: XYChartData;
constructor() {
this.config = {
width: 500,
height: 500,
fontFamily: defaultConfig.fontFamily || 'Sans',
titleFontSize: 16,
titleFill: '#000000',
titlePadding: 5,
xAxisFontSize: 14,
xAxisTitleFontSize: 16,
yAxisFontSize: 14,
yAxisTitleFontSize: 16,
yAxisPosition: XYChartYAxisPosition.LEFT,
showChartTitle: true,
showXAxisLable: true,
showXAxisTitle: true,
showYAxisLabel: true,
showYAxisTitle: true,
chartOrientation: OrientationEnum.HORIZONTAL,
plotReservedSpacePercent: 50,
};
this.chartData = {
yAxis: {
title: 'yAxis1',
min: 0,
max: 100,
},
xAxis: {
title: 'xAxis',
categories: ['category1', 'category2', 'category3'],
},
title: 'this is a sample task',
plots: [
{
type: ChartPlotEnum.LINE,
data: [
['category1', 33],
['category2', 45],
['category3', 65],
],
},
],
};
}
setWidth(width: number) {
this.config.width = width;
}
setHeight(height: number) {
this.config.height = height;
}
build(): DrawableElem[] {
log.trace(`Build start with Config: ${JSON.stringify(this.config, null, 2)}`);
log.trace(`Build start with ChartData: ${JSON.stringify(this.chartData, null, 2)}`);
const orchestrator = new Orchestrator(this.config, this.chartData);
return orchestrator.getDrawableElement();
}
}

View File

@@ -1,5 +0,0 @@
// @ts-ignore: TODO Fix ts errors
import { scaleLinear } from 'd3';
import { log } from '../../logger.js';
export class XYChartBuilder {}

View File

@@ -11,6 +11,8 @@ import {
setAccDescription,
clear as commonClear,
} from '../../commonDb.js';
import { XYChartBuilder } from './chartBuilder/index.js';
import { DrawableElem } from './chartBuilder/Interfaces.js';
const config = configApi.getConfig();
@@ -18,16 +20,33 @@ function textSanitizer(text: string) {
return sanitizeText(text.trim(), config);
}
export const parseDirective = function (statement: string, context: string, type: string) {
function parseDirective(statement: string, context: string, type: string) {
// @ts-ignore: TODO Fix ts errors
mermaidAPI.parseDirective(this, statement, context, type);
};
const xyChartBuilder = new XYChartBuilder();
function getDrawableElem(): DrawableElem[] {
return xyChartBuilder.build();
}
function setHeight(height: number) {
xyChartBuilder.setHeight(height);
}
function setWidth(width: number) {
xyChartBuilder.setWidth(width);
}
const clear = function () {
commonClear();
};
export default {
setWidth,
setHeight,
getDrawableElem,
parseDirective,
clear,
setAccTitle,

View File

@@ -1,11 +1,27 @@
// @ts-ignore: TODO Fix ts errors
import { select } from 'd3';
import { select, scaleOrdinal, scaleLinear, axisBottom, line } from 'd3';
import * as configApi from '../../config.js';
import { log } from '../../logger.js';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import { Diagram } from '../../Diagram.js';
import {
DrawableElem,
TextElem,
TextHorizontalPos,
TextVerticalPos,
} from './chartBuilder/Interfaces.js';
export const draw = (txt: string, id: string, _version: string, diagObj: Diagram) => {
function getDominantBaseLine(horizontalPos: TextHorizontalPos) {
return horizontalPos === 'top' ? 'hanging' : 'middle';
}
function getTextAnchor(verticalPos: TextVerticalPos) {
return verticalPos === 'left' ? 'start' : 'middle';
}
function getTextTransformation(data: TextElem) {
return `translate(${data.x}, ${data.y}) rotate(${data.rotation || 0})`;
}
const conf = configApi.getConfig();
log.debug('Rendering xychart chart\n' + txt);
@@ -17,8 +33,8 @@ export const draw = (txt: string, id: string, _version: string, diagObj: Diagram
sandboxElement = select('#i' + id);
}
const root =
securityLevel === 'sandbox'
? select(sandboxElement.nodes()[0].contentDocument.body)
sandboxElement
? sandboxElement
: select('body');
const svg = root.select(`[id="${id}"]`);
@@ -28,9 +44,80 @@ export const draw = (txt: string, id: string, _version: string, diagObj: Diagram
const width = 500;
const height = 500;
// @ts-ignore: TODO Fix ts errors
configureSvgSize(svg, height, width, true);
svg.attr('viewBox', '0 0 ' + width + ' ' + height);
// @ts-ignore: TODO Fix ts errors
diagObj.db.setHeight(height);
// @ts-ignore: TODO Fix ts errors
diagObj.db.setWidth(width);
// @ts-ignore: TODO Fix ts errors
const shapes: DrawableElem[] = diagObj.db.getDrawableElem();
for (const shape of shapes) {
if (shape.data.length === 0) {
log.trace(
`Skipped drawing of shape of type: ${shape.type} with data: ${JSON.stringify(
shape.data,
null,
2
)}`
);
continue;
}
log.trace(
`Drawing shape of type: ${shape.type} with data: ${JSON.stringify(shape.data, null, 2)}`
);
const shapeGroup = group.append('g').attr('class', shape.groupText);
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.horizontalPos))
.attr('text-anchor', data => getTextAnchor(data.verticalPos))
.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 {