From 31faa69b275f82cafd64ed300f6aa387f1be45dc Mon Sep 17 00:00:00 2001 From: skanaar Date: Thu, 20 Aug 2020 21:12:25 +0200 Subject: [PATCH] shift graphs so relationship labels fit within bounding box layouter responsible for relation labels layout --- dist/nomnoml.js | 182 ++++++++++++++++++++++++------------------- src/domain.ts | 13 +++- src/layouter.ts | 94 ++++++++++++++++++---- src/parser.ts | 4 +- src/renderer.ts | 65 ++++------------ src/skanaar.util.ts | 9 --- test/nomnoml.spec.js | 18 ++--- 7 files changed, 211 insertions(+), 174 deletions(-) diff --git a/dist/nomnoml.js b/dist/nomnoml.js index e420530..6a7c334 100644 --- a/dist/nomnoml.js +++ b/dist/nomnoml.js @@ -46,6 +46,13 @@ }()); nomnoml.Classifier = Classifier; })(nomnoml || (nomnoml = {})); +var __spreadArrays = (this && this.__spreadArrays) || function () { + for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; + for (var r = Array(s), k = 0, i = 0; i < il; i++) + for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) + r[k] = a[j]; + return r; +}; var nomnoml; (function (nomnoml) { function layout(measurer, config, ast) { @@ -54,16 +61,18 @@ var nomnoml; return { width: 0, height: config.padding }; measurer.setFont(config, fontWeight, 'normal'); return { - width: Math.round(nomnoml.skanaar.max(lines.map(measurer.textWidth)) + 2 * config.padding), + width: Math.round(Math.max.apply(Math, lines.map(measurer.textWidth)) + 2 * config.padding), height: Math.round(measurer.textHeight() * lines.length + 2 * config.padding) }; } function layoutCompartment(c, compartmentIndex, style) { var textSize = measureLines(c.lines, compartmentIndex ? 'normal' : 'bold'); - c.width = textSize.width; - c.height = textSize.height; - if (!c.nodes.length && !c.relations.length) + if (!c.nodes.length && !c.relations.length) { + c.width = textSize.width; + c.height = textSize.height; + c.offset = { x: config.padding, y: config.padding }; return; + } c.nodes.forEach(layoutClassifier); var g = new dagre.graphlib.Graph(); g.setGraph({ @@ -74,38 +83,95 @@ var nomnoml; acyclicer: config.acyclicer, ranker: config.ranker }); - c.nodes.forEach(function (e) { + for (var _i = 0, _a = c.nodes; _i < _a.length; _i++) { + var e = _a[_i]; g.setNode(e.name, { width: e.layoutWidth, height: e.layoutHeight }); - }); - c.relations.forEach(function (r) { + } + for (var _b = 0, _c = c.relations; _b < _c.length; _b++) { + var r = _c[_b]; g.setEdge(r.start, r.end, { id: r.id }); - }); + } dagre.layout(g); var rels = nomnoml.skanaar.indexBy(c.relations, 'id'); var nodes = nomnoml.skanaar.indexBy(c.nodes, 'name'); - function toPoint(o) { return { x: o.x, y: o.y }; } g.nodes().forEach(function (name) { var node = g.node(name); nodes[name].x = node.x; nodes[name].y = node.y; }); - var edgeWidth = 0; - var edgeHeight = 0; + var left = 0; + var right = 0; + var top = 0; + var bottom = 0; g.edges().forEach(function (edgeObj) { var edge = g.edge(edgeObj); var start = nodes[edgeObj.v]; var end = nodes[edgeObj.w]; - rels[edge.id].path = nomnoml.skanaar.flatten([[start], edge.points, [end]]).map(toPoint); - edgeWidth = nomnoml.skanaar.max(edge.points.map(function (e) { return e.x; })); - edgeHeight = nomnoml.skanaar.max(edge.points.map(function (e) { return e.y; })); + var rel = rels[edge.id]; + rel.path = nomnoml.skanaar.flatten([[start], edge.points, [end]]).map(toPoint); + var startP = rel.path[1]; + var endP = rel.path[rel.path.length - 2]; + layoutLabel(rel.startLabel, startP, adjustQuadrant(quadrant(startP, start, 4), start, end)); + layoutLabel(rel.endLabel, endP, adjustQuadrant(quadrant(endP, end, 2), end, start)); + left = Math.min.apply(Math, __spreadArrays([left, rel.startLabel.x, rel.endLabel.x], edge.points.map(function (e) { return e.x; }), edge.points.map(function (e) { return e.x; }))); + right = Math.max.apply(Math, __spreadArrays([right, rel.startLabel.x + rel.startLabel.width, rel.endLabel.x + rel.endLabel.width], edge.points.map(function (e) { return e.x; }))); + top = Math.min.apply(Math, __spreadArrays([top, rel.startLabel.y, rel.endLabel.y], edge.points.map(function (e) { return e.y; }))); + bottom = Math.max.apply(Math, __spreadArrays([bottom, rel.startLabel.y + rel.startLabel.height, rel.endLabel.y + rel.endLabel.height], edge.points.map(function (e) { return e.y; }))); }); var graph = g.graph(); - var width = Math.max(graph.width, edgeWidth); - var height = Math.max(graph.height, edgeHeight); + var width = Math.max(graph.width, right - left); + var height = Math.max(graph.height, bottom - top); var graphHeight = height ? height + 2 * config.gutter : 0; var graphWidth = width ? width + 2 * config.gutter : 0; c.width = Math.max(textSize.width, graphWidth) + 2 * config.padding; c.height = textSize.height + graphHeight + config.padding; + c.offset = { x: config.padding - left, y: config.padding - top }; + } + function toPoint(o) { + return { x: o.x, y: o.y }; + } + function layoutLabel(label, point, quadrant) { + if (!label.text) { + label.width = 0; + label.height = 0; + label.x = point.x; + label.y = point.y; + } + else { + var fontSize = config.fontSize; + var lines = label.text.split('`'); + label.width = Math.max.apply(Math, lines.map(function (l) { return measurer.textWidth(l); })), + label.height = fontSize * lines.length; + label.x = point.x + ((quadrant == 1 || quadrant == 4) ? config.padding : -label.width - config.padding), + label.y = point.y + ((quadrant == 3 || quadrant == 4) ? config.padding : -label.height - config.padding); + } + } + function quadrant(point, node, fallback) { + if (point.x < node.x && point.y < node.y) + return 1; + if (point.x > node.x && point.y < node.y) + return 2; + if (point.x > node.x && point.y > node.y) + return 3; + if (point.x < node.x && point.y > node.y) + return 4; + return fallback; + } + function adjustQuadrant(quadrant, point, opposite) { + if ((opposite.x == point.x) || (opposite.y == point.y)) + return quadrant; + var flipHorizontally = [4, 3, 2, 1]; + var flipVertically = [2, 1, 4, 3]; + var oppositeQuadrant = (opposite.y < point.y) ? + ((opposite.x < point.x) ? 2 : 1) : + ((opposite.x < point.x) ? 3 : 4); + if (oppositeQuadrant === quadrant) { + if (config.direction === 'LR') + return flipHorizontally[quadrant - 1]; + if (config.direction === 'TB') + return flipVertically[quadrant - 1]; + } + return quadrant; } function layoutClassifier(clas) { var layout = getLayouter(clas); @@ -126,7 +192,7 @@ var nomnoml; }; default: return function (clas) { clas.compartments.forEach(function (co, i) { layoutCompartment(co, i, style); }); - clas.width = nomnoml.skanaar.max(clas.compartments, 'width'); + clas.width = Math.max.apply(Math, clas.compartments.map(function (e) { return e.width; })); clas.height = nomnoml.skanaar.sum(clas.compartments, 'height'); clas.x = clas.layoutWidth / 2; clas.y = clas.layoutHeight / 2; @@ -143,7 +209,7 @@ var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { @@ -367,8 +433,8 @@ var nomnoml; assoc: p.assoc, start: p.start.parts[0][0], end: p.end.parts[0][0], - startLabel: p.startLabel, - endLabel: p.endLabel + startLabel: { text: p.startLabel }, + endLabel: { text: p.endLabel } }); } if (isAstClassifier(p)) { @@ -404,16 +470,15 @@ var nomnoml; var nomnoml; (function (nomnoml) { function render(graphics, config, compartment, setFont) { - var padding = config.padding; var g = graphics; var vm = nomnoml.skanaar.vector; function renderCompartment(compartment, style, level) { g.save(); - g.translate(padding, padding); + g.translate(compartment.offset.x, compartment.offset.y); g.fillStyle(style.stroke || config.stroke); compartment.lines.forEach(function (text, i) { g.textAlign(style.center ? 'center' : 'left'); - var x = style.center ? compartment.width / 2 - padding : 0; + var x = style.center ? compartment.width / 2 - config.padding : 0; var y = (0.5 + (i + 0.5) * config.leading) * config.fontSize; if (text) { g.fillText(text, x, y); @@ -426,7 +491,7 @@ var nomnoml; } }); g.translate(config.gutter, config.gutter); - compartment.relations.forEach(function (r) { renderRelation(r, compartment); }); + compartment.relations.forEach(function (r) { renderRelation(r); }); compartment.nodes.forEach(function (n) { renderNode(n, level); }); g.restore(); } @@ -443,7 +508,7 @@ var nomnoml; var drawNode = nomnoml.visualizers[style.visual] || nomnoml.visualizers["class"]; drawNode(node, x, y, config, g); g.setLineDash([]); - var yDivider = (style.visual === 'actor' ? y + padding * 3 / 4 : y); + var yDivider = (style.visual === 'actor' ? y + config.padding * 3 / 4 : y); node.compartments.forEach(function (part, i) { var s = i > 0 ? nomnoml.buildStyle({ stroke: style.stroke }) : style; if (s.empty) @@ -457,7 +522,7 @@ var nomnoml; return; yDivider += part.height; if (style.visual === 'frame' && i === 0) { - var w = g.measureText(node.name).width + part.height / 2 + padding; + var w = g.measureText(node.name).width + part.height / 2 + config.padding; g.path([ { x: x, y: yDivider }, { x: x + w - part.height / 2, y: yDivider }, @@ -485,58 +550,21 @@ var nomnoml; g.path(p).stroke(); } var empty = false, filled = true, diamond = true; - function renderLabel(text, pos, quadrant) { - if (text) { - var fontSize = config.fontSize; - var lines = text.split('`'); - var area = { - width: nomnoml.skanaar.max(lines.map(function (l) { return g.measureText(l).width; })), - height: fontSize * lines.length - }; - var origin = { - x: pos.x + ((quadrant == 1 || quadrant == 4) ? padding : -area.width - padding), - y: pos.y + ((quadrant == 3 || quadrant == 4) ? padding : -area.height - padding) - }; - lines.forEach(function (l, i) { g.fillText(l, origin.x, origin.y + fontSize * (i + 1)); }); - } - } - function quadrant(point, node, fallback) { - if (point.x < node.x && point.y < node.y) - return 1; - if (point.x > node.x && point.y < node.y) - return 2; - if (point.x > node.x && point.y > node.y) - return 3; - if (point.x < node.x && point.y > node.y) - return 4; - return fallback; - } - function adjustQuadrant(quadrant, point, opposite) { - if ((opposite.x == point.x) || (opposite.y == point.y)) - return quadrant; - var flipHorizontally = [4, 3, 2, 1]; - var flipVertically = [2, 1, 4, 3]; - var oppositeQuadrant = (opposite.y < point.y) ? - ((opposite.x < point.x) ? 2 : 1) : - ((opposite.x < point.x) ? 3 : 4); - if (oppositeQuadrant === quadrant) { - if (config.direction === 'LR') - return flipHorizontally[quadrant - 1]; - if (config.direction === 'TB') - return flipVertically[quadrant - 1]; - } - return quadrant; + function renderLabel(label) { + if (!label || !label.text) + return; + var fontSize = config.fontSize; + var lines = label.text.split('`'); + lines.forEach(function (l, i) { return g.fillText(l, label.x, label.y + fontSize * (i + 1)); }); } - function renderRelation(r, compartment) { - var startNode = nomnoml.skanaar.find(compartment.nodes, function (e) { return e.name == r.start; }); - var endNode = nomnoml.skanaar.find(compartment.nodes, function (e) { return e.name == r.end; }); + function renderRelation(r) { var start = r.path[1]; var end = r.path[r.path.length - 2]; var path = r.path.slice(1, -1); g.fillStyle(config.stroke); setFont(config, 'normal'); - renderLabel(r.startLabel, start, adjustQuadrant(quadrant(start, startNode, 4), start, end)); - renderLabel(r.endLabel, end, adjustQuadrant(quadrant(end, endNode, 2), end, start)); + renderLabel(r.startLabel); + renderLabel(r.endLabel); if (r.assoc !== '-/-') { if (nomnoml.skanaar.hasSubstring(r.assoc, '--')) { var dash = Math.max(4, 2 * config.lineWidth); @@ -1006,16 +1034,6 @@ var nomnoml; } } skanaar.plucker = plucker; - function max(list, plucker) { - var transform = skanaar.plucker(plucker); - var maximum = transform(list[0]); - for (var i = 0; i < list.length; i++) { - var item = transform(list[i]); - maximum = (item > maximum) ? item : maximum; - } - return maximum; - } - skanaar.max = max; function sum(list, plucker) { var transform = skanaar.plucker(plucker); for (var i = 0, summation = 0, len = list.length; i < len; i++) diff --git a/src/domain.ts b/src/domain.ts index e157a3f..840939c 100644 --- a/src/domain.ts +++ b/src/domain.ts @@ -68,20 +68,29 @@ namespace nomnoml { export class Compartment { width: number height: number + offset: Vector constructor( public lines: string[], public nodes: Classifier[], public relations: Relation[] ){} } + + export interface RelationLabel { + x?: number + y?: number + width?: number + height?: number + text: string + } export class Relation { id: number path?: Vector[] start: string end: string - startLabel: string - endLabel: string + startLabel: RelationLabel + endLabel: RelationLabel assoc: string } diff --git a/src/layouter.ts b/src/layouter.ts index b60ea4f..12b1685 100644 --- a/src/layouter.ts +++ b/src/layouter.ts @@ -1,4 +1,6 @@ namespace nomnoml { + + type Quadrant = 1|2|3|4 export function layout(measurer: Measurer, config: Config, ast: Compartment): Compartment { @@ -7,18 +9,20 @@ namespace nomnoml { return { width: 0, height: config.padding } measurer.setFont(config, fontWeight, 'normal') return { - width: Math.round(skanaar.max(lines.map(measurer.textWidth)) + 2*config.padding), + width: Math.round(Math.max(...lines.map(measurer.textWidth)) + 2*config.padding), height: Math.round(measurer.textHeight() * lines.length + 2*config.padding) } } function layoutCompartment(c: Compartment, compartmentIndex: number, style: Style){ var textSize = measureLines(c.lines, compartmentIndex ? 'normal' : 'bold') - c.width = textSize.width - c.height = textSize.height - if (!c.nodes.length && !c.relations.length) + if (!c.nodes.length && !c.relations.length){ + c.width = textSize.width + c.height = textSize.height + c.offset = { x: config.padding, y: config.padding } return + } c.nodes.forEach(layoutClassifier) @@ -34,40 +38,96 @@ namespace nomnoml { acyclicer: config.acyclicer, ranker: config.ranker }); - c.nodes.forEach(function (e){ + for(var e of c.nodes){ g.setNode(e.name, { width: e.layoutWidth, height: e.layoutHeight }) - }) - c.relations.forEach(function (r){ + } + for(var r of c.relations){ g.setEdge(r.start, r.end, { id: r.id }) - }) + } dagre.layout(g) var rels = skanaar.indexBy(c.relations, 'id') var nodes = skanaar.indexBy(c.nodes, 'name') - function toPoint(o:{x:number, y:number}){ return {x:o.x, y:o.y} } g.nodes().forEach(function(name) { var node = g.node(name) nodes[name].x = node.x nodes[name].y = node.y }) - var edgeWidth = 0; - var edgeHeight = 0; + var left = 0 + var right = 0 + var top = 0 + var bottom = 0 g.edges().forEach(function(edgeObj) { var edge = g.edge(edgeObj) var start = nodes[edgeObj.v] var end = nodes[edgeObj.w] - rels[edge.id].path = skanaar.flatten([[start], edge.points, [end]]).map(toPoint) - edgeWidth = skanaar.max(edge.points.map(e => e.x)) - edgeHeight = skanaar.max(edge.points.map(e => e.y)) + var rel = rels[edge.id] + rel.path = skanaar.flatten([[start], edge.points, [end]]).map(toPoint) + + var startP = rel.path[1]; + var endP = rel.path[rel.path.length - 2]; + layoutLabel(rel.startLabel, startP, adjustQuadrant(quadrant(startP, start, 4), start, end)); + layoutLabel(rel.endLabel, endP, adjustQuadrant(quadrant(endP, end, 2), end, start)); + left = Math.min(left, rel.startLabel.x, rel.endLabel.x, ...edge.points.map(e => e.x), ...edge.points.map(e => e.x)) + right = Math.max(right, rel.startLabel.x + rel.startLabel.width, rel.endLabel.x + rel.endLabel.width, ...edge.points.map(e => e.x)) + top = Math.min(top, rel.startLabel.y, rel.endLabel.y, ...edge.points.map(e => e.y)) + bottom = Math.max(bottom, rel.startLabel.y + rel.startLabel.height, rel.endLabel.y + rel.endLabel.height, ...edge.points.map(e => e.y)) }) var graph = g.graph() - var width = Math.max(graph.width, edgeWidth) - var height = Math.max(graph.height, edgeHeight) + var width = Math.max(graph.width, right - left) + var height = Math.max(graph.height, bottom - top) var graphHeight = height ? height + 2*config.gutter : 0 var graphWidth = width ? width + 2*config.gutter : 0 c.width = Math.max(textSize.width, graphWidth) + 2*config.padding c.height = textSize.height + graphHeight + config.padding + c.offset = { x: config.padding - left, y: config.padding - top } + } + + function toPoint(o: Vec): Vec { + return { x:o.x, y:o.y } + } + + function layoutLabel(label: RelationLabel, point: Vector, quadrant: Quadrant) { + if (!label.text) { + label.width = 0 + label.height = 0 + label.x = point.x + label.y = point.y + } else { + var fontSize = config.fontSize + var lines = label.text.split('`') + label.width = Math.max(...lines.map(function(l){ return measurer.textWidth(l) })), + label.height = fontSize*lines.length + label.x = point.x + ((quadrant==1 || quadrant==4) ? config.padding : -label.width - config.padding), + label.y = point.y + ((quadrant==3 || quadrant==4) ? config.padding : -label.height - config.padding) + } + } + + // find basic quadrant using relative position of endpoint and block rectangle + function quadrant(point: Vector, node: Classifier, fallback: Quadrant): Quadrant { + if (point.x < node.x && point.y < node.y) return 1; + if (point.x > node.x && point.y < node.y) return 2; + if (point.x > node.x && point.y > node.y) return 3; + if (point.x < node.x && point.y > node.y) return 4; + return fallback; + } + + // Flip basic label quadrant if needed, to avoid crossing a bent relationship line + function adjustQuadrant(quadrant: Quadrant, point: Vector, opposite: Vector): Quadrant { + if ((opposite.x == point.x) || (opposite.y == point.y)) return quadrant; + var flipHorizontally: Quadrant[] = [4, 3, 2, 1] + var flipVertically: Quadrant[] = [2, 1, 4, 3] + var oppositeQuadrant = (opposite.y < point.y) ? + ((opposite.x < point.x) ? 2 : 1) : + ((opposite.x < point.x) ? 3 : 4); + // if an opposite relation end is in the same quadrant as a label, we need to flip the label + if (oppositeQuadrant === quadrant) { + if (config.direction === 'LR') return flipHorizontally[quadrant-1]; + if (config.direction === 'TB') return flipVertically[quadrant-1]; + } + return quadrant; + } function layoutClassifier(clas: Classifier): void { @@ -90,7 +150,7 @@ namespace nomnoml { } default: return function (clas){ clas.compartments.forEach(function(co,i){ layoutCompartment(co, i, style) }) - clas.width = skanaar.max(clas.compartments, 'width') + clas.width = Math.max(...clas.compartments.map(e => e.width)) clas.height = skanaar.sum(clas.compartments, 'height') clas.x = clas.layoutWidth/2 clas.y = clas.layoutHeight/2 diff --git a/src/parser.ts b/src/parser.ts index 50cf2db..cf752b3 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -148,8 +148,8 @@ namespace nomnoml { assoc: p.assoc, start: p.start.parts[0][0] as string, end: p.end.parts[0][0] as string, - startLabel: p.startLabel, - endLabel: p.endLabel + startLabel: { text: p.startLabel }, + endLabel: { text: p.endLabel } }) } if (isAstClassifier(p)){ diff --git a/src/renderer.ts b/src/renderer.ts index 9feff15..3d269fb 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -1,20 +1,17 @@ namespace nomnoml { - type Quadrant = 1|2|3|4 - export function render(graphics: Graphics, config: Config, compartment: Compartment, setFont: nomnoml.SetFont){ - var padding = config.padding var g = graphics var vm = nomnoml.skanaar.vector function renderCompartment(compartment: Compartment, style: Style, level: number){ g.save() - g.translate(padding, padding) + g.translate(compartment.offset.x, compartment.offset.y) g.fillStyle(style.stroke || config.stroke) compartment.lines.forEach(function (text, i){ g.textAlign(style.center ? 'center' : 'left') - var x = style.center ? compartment.width/2 - padding : 0 + var x = style.center ? compartment.width/2 - config.padding : 0 var y = (0.5+(i+0.5)*config.leading)*config.fontSize if (text){ g.fillText(text, x, y) @@ -27,7 +24,7 @@ namespace nomnoml { } }) g.translate(config.gutter, config.gutter) - compartment.relations.forEach(function (r){ renderRelation(r, compartment) }) + compartment.relations.forEach(function (r){ renderRelation(r) }) compartment.nodes.forEach(function (n){ renderNode(n, level) }) g.restore() } @@ -47,7 +44,7 @@ namespace nomnoml { drawNode(node, x, y, config, g) g.setLineDash([]) - var yDivider = (style.visual === 'actor' ? y + padding*3/4 : y) + var yDivider = (style.visual === 'actor' ? y + config.padding*3/4 : y) node.compartments.forEach(function (part: Compartment, i: number){ var s = i > 0 ? buildStyle({ stroke: style.stroke }) : style; // only style node title if (s.empty) return @@ -59,7 +56,7 @@ namespace nomnoml { if (i+1 === node.compartments.length) return yDivider += part.height if (style.visual === 'frame' && i === 0){ - var w = g.measureText(node.name).width+part.height/2+padding + var w = g.measureText(node.name).width+part.height/2+config.padding g.path([ {x:x, y:yDivider}, {x:x+w-part.height/2, y:yDivider}, @@ -90,50 +87,14 @@ namespace nomnoml { var empty = false, filled = true, diamond = true - function renderLabel(text: string, pos: Vector, quadrant: Quadrant){ - if (text) { - var fontSize = config.fontSize - var lines = text.split('`') - var area = { - width : skanaar.max(lines.map(function(l){ return g.measureText(l).width })), - height : fontSize*lines.length - } - var origin = { - x: pos.x + ((quadrant==1 || quadrant==4) ? padding : -area.width - padding), - y: pos.y + ((quadrant==3 || quadrant==4) ? padding : -area.height - padding) - } - lines.forEach(function(l, i){ g.fillText(l, origin.x, origin.y + fontSize*(i+1)) }) - } - } - - // find basic quadrant using relative position of endpoint and block rectangle - function quadrant(point: Vector, node: Classifier, fallback: Quadrant): Quadrant { - if (point.x < node.x && point.y < node.y) return 1; - if (point.x > node.x && point.y < node.y) return 2; - if (point.x > node.x && point.y > node.y) return 3; - if (point.x < node.x && point.y > node.y) return 4; - return fallback; - } - - // Flip basic label quadrant if needed, to avoid crossing a bent relationship line - function adjustQuadrant(quadrant: Quadrant, point: Vector, opposite: Vector): Quadrant { - if ((opposite.x == point.x) || (opposite.y == point.y)) return quadrant; - var flipHorizontally: Quadrant[] = [4, 3, 2, 1] - var flipVertically: Quadrant[] = [2, 1, 4, 3] - var oppositeQuadrant = (opposite.y < point.y) ? - ((opposite.x < point.x) ? 2 : 1) : - ((opposite.x < point.x) ? 3 : 4); - // if an opposite relation end is in the same quadrant as a label, we need to flip the label - if (oppositeQuadrant === quadrant) { - if (config.direction === 'LR') return flipHorizontally[quadrant-1]; - if (config.direction === 'TB') return flipVertically[quadrant-1]; - } - return quadrant; + function renderLabel(label: RelationLabel){ + if (!label || !label.text) return + var fontSize = config.fontSize + var lines = label.text.split('`') + lines.forEach((l, i) => g.fillText(l, label.x, label.y + fontSize*(i+1))) } - function renderRelation(r: Relation, compartment: Compartment){ - var startNode = skanaar.find(compartment.nodes, function(e: Classifier){ return e.name == r.start }) - var endNode = skanaar.find(compartment.nodes, function(e: Classifier){ return e.name == r.end }) + function renderRelation(r: Relation){ var start = r.path[1] var end = r.path[r.path.length-2] var path = r.path.slice(1, -1) @@ -141,8 +102,8 @@ namespace nomnoml { g.fillStyle(config.stroke) setFont(config, 'normal') - renderLabel(r.startLabel, start, adjustQuadrant(quadrant(start, startNode, 4), start, end)) - renderLabel(r.endLabel, end, adjustQuadrant(quadrant(end, endNode, 2), end, start)) + renderLabel(r.startLabel) + renderLabel(r.endLabel) if (r.assoc !== '-/-'){ if (skanaar.hasSubstring(r.assoc, '--')){ diff --git a/src/skanaar.util.ts b/src/skanaar.util.ts index 61a1907..1b15a52 100644 --- a/src/skanaar.util.ts +++ b/src/skanaar.util.ts @@ -7,15 +7,6 @@ namespace nomnoml.skanaar { case 'function': return pluckerDef } } - export function max(list: any[], plucker?: any): number { - var transform = skanaar.plucker(plucker) - var maximum = transform(list[0]) - for(var i=0; i maximum) ? item : maximum - } - return maximum - } export function sum(list: { length: number, [i: number]: T }, plucker?: any){ var transform = skanaar.plucker(plucker) for(var i=0, summation=0, len=list.length; i e.a), 10) - assertEqual(nomnoml.skanaar.max([7, 10, 6]), 10) -}) - suite.test('skanaar.flatten', function(){ assertEqual(nomnoml.skanaar.flatten([[4, 5]]), [4, 5]) assertEqual(nomnoml.skanaar.flatten([[7], [4, 5]]), [7, 4, 5]) @@ -127,8 +121,8 @@ suite.test('astBuilder should handle single association', function(){ assoc: '->', start: 'apa', end: 'banan', - startLabel: '', - endLabel: '' + startLabel: { text: '' }, + endLabel: { text: '' } } ])) }) @@ -232,7 +226,9 @@ suite.test('layout [apa|[flea]->[dandruff]] vertically stacked inner classes', f id: 0, type: 'association', start: 'flea', - end: 'dandruff' + end: 'dandruff', + startLabel: { text: '' }, + endLabel: { text: '' }, }] ) ]) @@ -268,7 +264,9 @@ suite.test('layouter should handle [apa|[flea]->[dandruff]] relation placement', id: 0, type: 'association', start: 'flea', - end: 'dandruff' + end: 'dandruff', + startLabel: { text: '' }, + endLabel: { text: '' }, }] ) ])