Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New: Add support for label styling options #63

Open
wants to merge 4 commits into
base: release/0.5.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/styles.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ the following properties:
| `colorHover` | Color | string | Node background color on mouse hover event. If not defined `color` is used. |
| `colorSelected` | Color | string | Node background color on mouse click event. If not defined `color` is used. |
| `fontBackgroundColor` | Color | string | Node text (label) background color. |
| `fontBackgroundBorderWidth` | number | Node text (label) background border width. |
| `fontBackgroundBorderColor` | Color | string | Node text (label) background border color. |
| `fontBackgroundBorderRadius` | number | Node text (label) background border radius. |
| `fontBackgroundPadding` | number | Node text (label) background padding. |
| `fontColor` | Color | string | Node text (label) font color. The default is `#000000`. |
| `fontFamily` | string | Node text (label) font family. The default is `"Roboto, sans-serif"`. |
| `fontSize` | number | Node text (label) font size. The default is `4`. |
Expand Down
48 changes: 38 additions & 10 deletions examples/example-custom-styled-graph.html
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
<!DOCTYPE html>
<html lang='en' type="module">

<head>
<base href=".">
<meta charset='UTF-8'>
<title>Orb | Simple graph with custom default style on default canvas</title>
<script type="text/javascript" src="./orb.js"></script>
</head>
<style>
html, body {
html,
body {
height: 100%;
margin: 0;
}
</style>

<body>
<div style="display: flex; flex-direction: column; align-items: center; height: 100%;">
<h1>Example 2 - Basic + Custom default style</h1>
Expand All @@ -30,13 +33,13 @@ <h1>Example 2 - Basic + Custom default style</h1>

const nodes = [
{ id: 0, label: 'Node A' },
{ id: 1, label: 'Node B' },
{ id: 1, label: 'Node B - A Special Node\nWith Some Special\nProperties' },
{ id: 2, label: 'Node C' },
];
const edges = [
{ id: 0, start: 0, end: 0, label: 'A -> A' },
{ id: 1, start: 0, end: 1, label: 'A -> B' },
{ id: 2, start: 0, end: 2, label: 'A -> C' },
{ id: 2, start: 0, end: 2, label: 'New Property\nA -> C' },
{ id: 3, start: 1, end: 2, label: 'B -> C' },
{ id: 4, start: 2, end: 2, label: 'C -> C' },
{ id: 5, start: 0, end: 1, label: 'A -> B' },
Expand All @@ -54,7 +57,7 @@ <h1>Example 2 - Basic + Custom default style</h1>
getNodeStyle(node) {
const basicStyle = {
borderColor: '#1d1d1d',
borderWidth: 0.6,
borderWidth: 0.1,
color: '#DD2222',
colorHover: '#e7644e',
colorSelected: '#e7644e',
Expand All @@ -63,12 +66,20 @@ <h1>Example 2 - Basic + Custom default style</h1>
size: 6,
};

if (node.data.label === 'Node A') {
if (node.data.id === 1) {
return {
...basicStyle,
size: 10,
size: 4,
borderWidth: 0.3,
color: '#00FF2B',
borderColor: "purple",
zIndex: 1,
fontBackgroundColor: 'rgb(0, 255, 0)',
fontBackgroundBorderWidth: 0.3,
fontBackgroundBorderColor: 'purple',
fontBackgroundBorderRadius: 1.5,
fontBackgroundPadding: 1,
fontSize: 1.5,
};
}

Expand All @@ -77,16 +88,30 @@ <h1>Example 2 - Basic + Custom default style</h1>
};
},
getEdgeStyle(edge) {
return {
const basicStyles = {
color: '#999999',
colorHover: '#1d1d1d',
colorSelected: '#1d1d1d',
fontSize: 3,
fontSize: 2,
width: 0.3,
widthHover: 0.9,
widthSelected: 0.9,
label: edge.data.label,
};
}

if (edge.data.id === 2) {
return {
...basicStyles,
fontBackgroundColor: 'rgb(0, 255, 0)',
fontBackgroundBorderWidth: 0.2,
fontBackgroundBorderColor: 'grey',
fontBackgroundBorderRadius: 10,
fontBackgroundPadding: 1,
fontSize: 1.5,
}
}

else return { ...basicStyles };
},
});

Expand All @@ -96,6 +121,9 @@ <h1>Example 2 - Basic + Custom default style</h1>
orb.view.render(() => {
orb.view.recenter();
});


</script>
</body>
</html>

</html>
4 changes: 4 additions & 0 deletions src/models/edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export type IEdgeStyle = Partial<{
colorHover: Color | string;
colorSelected: Color | string;
fontBackgroundColor: Color | string;
fontBackgroundBorderWidth: number;
fontBackgroundBorderColor: Color | string;
fontBackgroundBorderRadius: number;
fontBackgroundPadding: number;
fontColor: Color | string;
fontFamily: string;
fontSize: number;
Expand Down
4 changes: 4 additions & 0 deletions src/models/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ export type INodeStyle = Partial<{
colorHover: Color | string;
colorSelected: Color | string;
fontBackgroundColor: Color | string;
fontBackgroundBorderWidth: number;
fontBackgroundBorderColor: Color | string;
fontBackgroundBorderRadius: number;
fontBackgroundPadding: number;
fontColor: Color | string;
fontFamily: string;
fontSize: number;
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/canvas/edge/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ const drawEdgeLabel = <N extends INodeBase, E extends IEdgeBase>(
textBaseline: LabelTextBaseline.MIDDLE,
properties: {
fontBackgroundColor: edge.style.fontBackgroundColor,
fontBackgroundBorderWidth: edge.style.fontBackgroundBorderWidth,
fontBackgroundBorderColor: edge.style.fontBackgroundBorderColor,
fontBackgroundBorderRadius: edge.style.fontBackgroundBorderRadius,
fontBackgroundPadding: edge.style.fontBackgroundPadding,
fontColor: edge.style.fontColor,
fontFamily: edge.style.fontFamily,
fontSize: edge.style.fontSize,
Expand Down
152 changes: 133 additions & 19 deletions src/renderer/canvas/label.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { IPosition, Color } from '../../common';
import { IPosition, Color, IRectangle } from '../../common';
import { drawRoundRect, getMaxValidBorderRadius } from './shapes';

const DEFAULT_FONT_FAMILY = 'Roboto, sans-serif';
const DEFAULT_FONT_SIZE = 4;
const DEFAULT_FONT_COLOR = '#000000';
const DEFAULT_FONT_BORDER_COLOR = '#000000';

const FONT_BACKGROUND_MARGIN = 0.12;
const FONT_LINE_SPACING = 1.2;
Expand All @@ -14,6 +16,10 @@ export enum LabelTextBaseline {

export interface ILabelProperties {
fontBackgroundColor: Color | string;
fontBackgroundBorderWidth: number;
fontBackgroundBorderColor: Color | string;
fontBackgroundBorderRadius: number;
fontBackgroundPadding: number;
fontColor: Color | string;
fontFamily: string;
fontSize: number;
Expand Down Expand Up @@ -64,34 +70,138 @@ export const drawLabel = (context: CanvasRenderingContext2D, label: Label) => {
return;
}

drawTextBackground(context, label);
drawText(context, label);
const borderPoints: IRectangle[] | undefined = getLabelBoundaryPoints(context, label);

if (!borderPoints) {
return;
}

drawTextBackground(context, borderPoints, label);
drawTextBackgroundBorder(context, borderPoints, label);
drawText(context, borderPoints, label);
};

const drawTextBackground = (context: CanvasRenderingContext2D, label: Label) => {
if (!label.properties.fontBackgroundColor || !label.position) {
const getLabelBoundaryPoints = (context: CanvasRenderingContext2D, label: Label): IRectangle[] | undefined => {
if (!label.position) {
return;
}

context.fillStyle = label.properties.fontBackgroundColor.toString();
const margin = label.fontSize * FONT_BACKGROUND_MARGIN;
const height = label.fontSize + 2 * margin;

const lineHeight = label.fontSize * FONT_LINE_SPACING;

const baselineHeight = label.textBaseline === LabelTextBaseline.MIDDLE ? label.fontSize / 2 : 0;

const borderPoints: IRectangle[] = [];

for (let i = 0; i < label.textLines.length; i++) {
const line = label.textLines[i];
const width = context.measureText(line).width + 2 * margin;
context.fillRect(
label.position.x - width / 2,
label.position.y - baselineHeight - margin + i * lineHeight,
width,
height,
);

context.font = label.fontFamily;
let width = context.measureText(line).width + 2 * margin;
let x = label.position.x - width / 2;
let y = label.position.y - baselineHeight - margin + i * lineHeight;
let height = label.fontSize + 2 * margin;

if (label.properties.fontBackgroundPadding) {
width += label.properties.fontBackgroundPadding * 2;
height += label.properties.fontBackgroundPadding * 2;
x -= label.properties.fontBackgroundPadding;

if (label.textBaseline === LabelTextBaseline.TOP) {
y += label.properties.fontBackgroundPadding * 2 * i;
} else {
if (i === 0) {
y -= label.properties.fontBackgroundPadding;
} else {
y += label.properties.fontBackgroundPadding;
}
}
}

borderPoints.push({ x, y, width, height });
}

return borderPoints;
};

const drawTextBackground = (context: CanvasRenderingContext2D, borderPoints: IRectangle[], label: Label) => {
if (!label.properties.fontBackgroundColor) {
return;
}

const backgroundColor = label.properties.fontBackgroundColor;
const fontBorderRadius = label.properties.fontBackgroundBorderRadius ?? 0;

context.fillStyle = backgroundColor.toString();

for (let i = 0; i < borderPoints.length; i++) {
const { x, y, width, height } = borderPoints[i];
const borderRadius = getMaxValidBorderRadius(fontBorderRadius, width, height);
drawRoundRect(context, x, y, width, height, borderRadius, { isTopRounded: i === 0, isBottomRounded: true });
context.fill();
}
};

const drawTextBackgroundBorder = (context: CanvasRenderingContext2D, borderPoints: IRectangle[], label: Label) => {
if (!label.properties.fontBackgroundBorderWidth) {
return;
}

const borderWidth = label.properties.fontBackgroundBorderWidth;
const borderColor = label.properties.fontBackgroundBorderColor ?? DEFAULT_FONT_BORDER_COLOR;
const fontBorderRadius = label.properties.fontBackgroundBorderRadius ?? 0;
context.lineWidth = borderWidth;
context.strokeStyle = borderColor.toString();

context.beginPath();

if (borderPoints.length === 1) {
const { x, y, height, width } = borderPoints[0];
const borderRadius = getMaxValidBorderRadius(fontBorderRadius, width, height);
drawRoundRect(context, x, y, width, height, borderRadius);
} else {
for (let i = 0; i < borderPoints.length; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not 100% sure why you need this logic here. I feel like all this can be done using 3 functions which are: drawRoundRect + context.fill() and context.stroke().

Here is the idea:

  • There are two stages: draw background and draw foreground (the actual text)
  • Stage 1: Background
    • baseline height in case if text baseline is TOP should incorporate the padding (so it doesn't overlay what is above it, e.g. node circle)
    • for each line of text get the bounding boxes (what you called BorderPoints) - check the context.font for context.measureText (it is in another comment)
    • for each line call drawRoundRect - what is cool is that it will make a path and it will not fill nor stroke it - which is great
    • after all lines are called with drawRoundRect we can just do 2 calls: context.stroke() (if backgroundBorderWidth > 0) and context.fill() - this fill will override the overlapping strokes.

You can test this last piece with this little sample:

<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>Canvas</title>
    <style>
      canvas {
        border: 1px solid black;
      }
    </style>
  </head>
  <body>
    <canvas id="tutorial" width="550" height="550"></canvas>
    <script>
      function draw() {
        const canvas = document.getElementById("tutorial");
        if (canvas.getContext) {
          const ctx = canvas.getContext("2d");
          ctx.fillStyle = 'yellow';
          ctx.lineWidth = 1;
          ctx.strokeStyle = 'black';
          ctx.rect(100, 100, 20, 20);
          ctx.rect(110, 110, 20, 20);
          ctx.stroke();
          ctx.fill();
        }
      }
      draw();
    </script>
  </body>
</html>
  • Stage 2: Foreground
    • Everything should stay the same as before, the only difference is the starting y when the text baseline is TOP (in the case of the nodes) because of the padding - so the baseline height needs to incorporate the padding.

What do you think?

Copy link
Contributor Author

@shashankshukla96 shashankshukla96 Jul 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there will be one issue here if there is no backgroundColor i.e. label.properties.fontBackgroundColor is not set, in this case with the above method, we will get overlapping borders around the label. meaning if we don't apply context.fill() then it will look something like this.

image

So, the border has to be drawn independently of the background.

Please let me know if my understanding is not correct.

const { x, y, height, width } = borderPoints[i];
const { x: nextX, width: nextWidth } = i !== borderPoints.length - 1 ? borderPoints[i + 1] : { x: 0, width: 0 };
const borderRadius = getMaxValidBorderRadius(fontBorderRadius, width, height);

if (i === 0) {
context.moveTo(x + borderRadius, y);
context.lineTo(x + width - borderRadius, y);
context.quadraticCurveTo(x + width, y, x + width, y + borderRadius);
context.lineTo(x + width, y + height - borderRadius);
context.quadraticCurveTo(x + width, y + height, x + width - borderRadius, y + height);
context.lineTo(nextX + nextWidth, y + height);
context.moveTo(x + borderRadius, y);
context.quadraticCurveTo(x, y, x, y + borderRadius);
context.lineTo(x, y + height - borderRadius);
context.quadraticCurveTo(x, y + height, x + borderRadius, y + height);
context.lineTo(nextX, y + height);
} else if (i === borderPoints.length - 1) {
context.moveTo(x + width, y);
context.lineTo(x + width, y + height - borderRadius);
context.quadraticCurveTo(x + width, y + height, x + width - borderRadius, y + height);
context.lineTo(x + borderRadius, y + height);
context.quadraticCurveTo(x, y + height, x, y + height - borderRadius);
context.lineTo(x, y);
} else {
context.moveTo(x + width, y);
context.lineTo(x + width, y + height - borderRadius);
context.quadraticCurveTo(x + width, y + height, x + width - borderRadius, y + height);
context.lineTo(nextX + nextWidth, y + height);
context.moveTo(x, y);
context.lineTo(x, y + height - borderRadius);
context.quadraticCurveTo(x, y + height, x + borderRadius, y + height);
context.lineTo(nextX, y + height);
}
}
}

context.stroke();
};

const drawText = (context: CanvasRenderingContext2D, label: Label) => {
const drawText = (context: CanvasRenderingContext2D, borderPoints: IRectangle[], label: Label) => {
if (!label.position) {
return;
}
Expand All @@ -100,11 +210,15 @@ const drawText = (context: CanvasRenderingContext2D, label: Label) => {
context.font = label.fontFamily;
context.textBaseline = label.textBaseline;
context.textAlign = 'center';
const lineHeight = label.fontSize * FONT_LINE_SPACING;

for (let i = 0; i < label.textLines.length; i++) {
const line = label.textLines[i];
context.fillText(line, label.position.x, label.position.y + i * lineHeight);
for (let i = 0; i < borderPoints.length; i++) {
const { x, y, width, height } = borderPoints[i];
const textLine = label.textLines[i];
if (label.textBaseline === LabelTextBaseline.TOP) {
context.fillText(textLine, x + width / 2, y + (label.properties.fontBackgroundPadding ?? 0));
} else {
context.fillText(textLine, x + width / 2, y + height / 2);
}
}
};

Expand Down
4 changes: 4 additions & 0 deletions src/renderer/canvas/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ const drawNodeLabel = <N extends INodeBase, E extends IEdgeBase>(
textBaseline: LabelTextBaseline.TOP,
properties: {
fontBackgroundColor: node.style.fontBackgroundColor,
fontBackgroundBorderWidth: node.style.fontBackgroundBorderWidth,
fontBackgroundBorderColor: node.style.fontBackgroundBorderColor,
fontBackgroundBorderRadius: node.style.fontBackgroundBorderRadius,
fontBackgroundPadding: node.style.fontBackgroundPadding,
fontColor: node.style.fontColor,
fontFamily: node.style.fontFamily,
fontSize: node.style.fontSize,
Expand Down
Loading