Skip to content

Commit

Permalink
refactor(frontend): improve visualization text alignment
Browse files Browse the repository at this point in the history
  • Loading branch information
kris7t committed Nov 2, 2024
1 parent d44fbf4 commit 2cf2431
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 23 deletions.
6 changes: 6 additions & 0 deletions subprojects/frontend/src/graph/GraphTheme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ export function createGraphTheme({
'& text': {
fontFamily: theme.typography.fontFamily,
fill: theme.palette.text.primary,
fontOpticalSizing: 'none',
letterSpacing: 0,
textRendering: 'geometricPrecision',
},
'.node-outline': {
stroke: theme.palette.text.primary,
Expand Down Expand Up @@ -137,6 +140,9 @@ export function createGraphTheme({
'& text': {
fontFamily: theme.typography.fontFamily,
fill: theme.palette.text.primary,
fontOpticalSizing: 'none',
letterSpacing: 0,
textRendering: 'geometricPrecision',
},
'.edge-line': {
stroke: theme.palette.text.primary,
Expand Down
12 changes: 8 additions & 4 deletions subprojects/frontend/src/graph/dotSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,19 +160,23 @@ function setLabelWidth(
measureDiv.style.position = 'absolute';
measureDiv.style.left = '-1000rem';
measureDiv.style.top = '-1000rem';
measureDiv.style.opacity = '0';
measureDiv.style.pointerEvents = 'none';
measureDiv.style.visibility = 'hidden';
measureDiv.style.width = 'auto';
measureDiv.style.height = 'auto';
measureDiv.style.padding = '0';
measureDiv.style.whiteSpace = 'pre';
measureDiv.style.lineHeight = String(14 / 12);
measureDiv.style.fontOpticalSizing = 'none';
measureDiv.style.letterSpacing = '0';
measureDiv.style.textRendering = 'geometricPreicision';
document.body.appendChild(measureDiv);
}
measureDiv.style.fontSize = `${size}px`;
measureDiv.style.fontSize = `${size}pt`;
measureDiv.innerHTML = text;
const { width, height } = measureDiv.getBoundingClientRect();
cached = `<table align="${align}" fixedsize="TRUE" width="${width}" height="${height}" border="0" cellborder="0" cellpadding="0" cellspacing="0">
// Rounding the length (converted to points) to 1 decimal precision seems to yield the best alignment.
// The rounding matters here, because Graphviz will also apply some rounding internall.
cached = `<table align="${align}" fixedsize="TRUE" width="${Math.ceil(width * 7.5) / 10}" height="${Math.ceil(height * 7.5) / 10}" border="0" cellborder="0" cellpadding="0" cellspacing="0">
<tr><td>${text}</td></tr>
</table>`;
sizeCache.set(key, cached);
Expand Down
4 changes: 2 additions & 2 deletions subprojects/frontend/src/graph/parseBBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export interface BBox {
}

function parsePoints(points: string[]): BBox {
const x = points.map((p) => Number(p.split(',')[0] ?? 0));
const y = points.map((p) => Number(p.split(',')[1] ?? 0));
const x = points.map((p) => parseFloat(p.split(',')[0] ?? '0'));
const y = points.map((p) => parseFloat(p.split(',')[1] ?? '0'));
const xmin = Math.min.apply(null, x);
const xmax = Math.max.apply(null, x);
const ymin = Math.min.apply(null, y);
Expand Down
59 changes: 42 additions & 17 deletions subprojects/frontend/src/graph/postProcessSVG.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@ import { type BBox, parsePolygonBBox, parsePathBBox } from './parseBBox';
export const SVG_NS = 'http://www.w3.org/2000/svg';
export const XLINK_NS = 'http://www.w3.org/1999/xlink';

function modifyAttribute(element: Element, attribute: string, change: number) {
const valueString = element.getAttribute(attribute);
if (valueString === null) {
return;
}
const value = parseInt(valueString, 10);
element.setAttribute(attribute, String(value + change));
function parseCoordinate(element: SVGElement, attribute: string): number {
return parseFloat(element.getAttribute(attribute)?.replace('px', '') ?? '0');
}

function modifyAttribute(
element: SVGElement,
attribute: string,
change: number,
): number {
const value = parseCoordinate(element, attribute);
const newValue = value + change;
element.setAttribute(attribute, String(newValue));
return value;
}

function addShadow(
Expand Down Expand Up @@ -59,18 +65,26 @@ function clipCompartmentBackground(node: SVGGElement) {
node.appendChild(clipPath);
compartment.setAttribute('clip-path', `url(#${clipId})`);
// Enlarge the compartment to completely cover the background.
modifyAttribute(compartment, 'y', -5);
modifyAttribute(compartment, 'x', -5);
modifyAttribute(compartment, 'width', 10);
const y = modifyAttribute(compartment, 'y', -5);
const x = modifyAttribute(compartment, 'x', -5);
const width = modifyAttribute(compartment, 'width', 10);
const isEmpty = node.classList.contains('node-empty');
// Make sure that empty nodes are fully filled.
modifyAttribute(compartment, 'height', isEmpty ? 10 : 5);
const height = modifyAttribute(compartment, 'height', isEmpty ? 10 : 5);
if (node.classList.contains('node-equalsSelf-UNKNOWN')) {
addShadow(node, container, 6);
}
container.id = `${node.id},container`;
compartment.id = `${node.id},compartment`;
border.id = `${node.id},border`;
const label = node.querySelector(':scope > text');
if (label === null) {
return;
}
label.setAttribute('dominant-baseline', 'central');
label.setAttribute('text-anchor', 'middle');
label.setAttribute('x', String(x + width / 2));
label.setAttribute('y', String(y + height / 2 - 0.75));
}

function createRect(
Expand Down Expand Up @@ -135,10 +149,6 @@ function hrefToClass(node: SVGGElement) {
});
}

function parseCoordinate(element: SVGElement, attribute: string): number {
return Number(element.getAttribute(attribute)?.replace('px', '') ?? '0');
}

function replaceImages(node: SVGGElement) {
node.querySelectorAll<SVGImageElement>('image').forEach((image) => {
const href =
Expand All @@ -153,8 +163,10 @@ function replaceImages(node: SVGGElement) {
const size = Math.min(width, height);
const sizeString = String(size);
const use = document.createElementNS(SVG_NS, 'use');
use.setAttribute('x', String(x + (width - size) / 2));
use.setAttribute('y', String(y + (height - size) / 2));
const xOffset = x + (width - size) / 2;
use.setAttribute('x', String(xOffset));
const yOffset = y + (height - size) / 2;
use.setAttribute('y', String(yOffset));
use.setAttribute('width', sizeString);
use.setAttribute('height', sizeString);
const iconName = `icon-${href.replace('#', '')}`;
Expand All @@ -169,6 +181,19 @@ function replaceImages(node: SVGGElement) {
sibling.id !== ''
) {
use.id = `${sibling.id},icon`;
sibling.querySelectorAll('text').forEach((textElement) => {
// Fix rounded text placement by dot.
textElement.setAttribute('dominant-baseline', 'central');
textElement.setAttribute('text-anchor', 'start');
textElement.setAttribute('x', String(xOffset + size + 4));
textElement.setAttribute('y', String(y + height / 2 - 0.75));
});
} else if (sibling !== null && sibling.tagName.toLowerCase() === 'text') {
// Fix rounded text placement of error labels by dot.
sibling.setAttribute('dominant-baseline', 'central');
sibling.setAttribute('text-anchor', 'start');
sibling.setAttribute('x', String(xOffset + size + 3));
sibling.setAttribute('y', String(y + height / 2));
}
image.parentNode?.replaceChild(use, image);
});
Expand Down

0 comments on commit 2cf2431

Please sign in to comment.