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

Conversation

shashankshukla96
Copy link
Contributor

This PR will add support to add label background styling options.

This will add style properties -

  • fontBackgroundBorderWidth
  • fontBackgroundBorderColor
  • fontBackgroundBorderRadius
  • fontBackgroundPadding

Copy link
Contributor

@tonilastre tonilastre left a comment

Choose a reason for hiding this comment

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

There is one additional bug. When using multi-line labels, there is no background. Just change the line 36 in the example-custom-styled-graph to be:

      { id: 1, label: 'Node B - A Special Node\nNew Line' },

And you will get this:

Screenshot 2023-06-09 at 18 20 43

The expected behavior is to have a background under each line.

And now comes an additional question which is how border color behaves when you have one line background touching the background from the line below (e.g. padding is larger than the default line spacing) because there can be two states: A or B. What we want, is the B.

Screenshot 2023-06-09 at 18 30 03

src/renderer/canvas/label.ts Show resolved Hide resolved
Comment on lines 107 to 116
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(x + borderRadius, y + height);
context.quadraticCurveTo(x, y + height, x, y + height - borderRadius);
context.lineTo(x, y + borderRadius);
context.quadraticCurveTo(x, y, x + borderRadius, y);
context.closePath();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you can use drawRoundRect from shapes here.

context.lineTo(x, y + borderRadius);
context.quadraticCurveTo(x, y, x + borderRadius, y);
context.closePath();
context.fillStyle = (label.properties.fontBackgroundColor ?? DEFAULT_FONT_COLOR).toString();
Copy link
Contributor

Choose a reason for hiding this comment

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

Are you sure you want to use DEFAULT_FONT_COLOR here? If default font color is black, and the background is black too, then by default, the text won't be visible.

context.strokeStyle = (label.properties.fontBackgroundBorderColor ?? DEFAULT_FONT_COLOR).toString();
context.fill();
context.stroke();
} else if (label.properties.fontBackgroundBorderWidth && !label.properties.fontBackgroundBorderRadius) {
Copy link
Contributor

Choose a reason for hiding this comment

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

You already have if(label.properties.fontBackgroundBorderRadius) on line 101, to the check for (!label.properties.fontBackgroundBorderRadius` is not needed here, it is irrelevant.

Copy link
Contributor

Choose a reason for hiding this comment

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

It is hard to figure out what will be the shape drawn from these two ifs and else so I propose splitting it so it is much easier to understand what final shape will be drawn, e.g.

// setup and preps

const isBackgroundShapeDrawn = ....
if (!isBackgroundShapeDrawn) {
  continue;
}

// I think this function will also handle radius = 0 to draw proper rect
drawRoundRect(...);

if (backgroundColor) {
  setFill = 
  fill();
}

If (borderWidth) {
  setStrokeStyle = 
  stroke();
}

@tonilastre tonilastre changed the base branch from main to release/0.5.0 July 13, 2023 08:23
Copy link
Contributor

@tonilastre tonilastre left a comment

Choose a reason for hiding this comment

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

First of all, thanks again for this feature request. It is a really cool feature. I've added several comments because I think we can remove some parts of the code and still do the same thing - which is easier to extend later on in the future.

Let me know what do you think of the comments.

Comment on lines 156 to 157
roundedTop: Boolean = true,
roundedBottom: Boolean = true,
Copy link
Contributor

Choose a reason for hiding this comment

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

Btw when there are optional params like this, it is good to combine them into a Partial object. It is easier to extend + the code is more readable (especially when calling it), e.g.

export interface IRoundRectOptions {
  isTopRounded: boolean;
  isBottomRounded: boolean;
}

const DEFAULT_ROUND_RECT_OPTIONS: IRoundRectOptions = {
  isTopRounded: true;
  isBottomRounded: true;
};

export const drawRoundRect = (..... r: number, options?: Partial<IRoundRectOptions>) {
  const config = { ...DEFAULT_ROUND_RECT_OPTIONS, ...options };
  ...
  if (config.isTopRounded) { ... }
}

And then when using this, you have the context of the object which is much easier to read:

// Before (rounded top = true)
drawRoundRect(context, 100, 100, 20, false)

// Before (if you just want to have rounded bottom = false, you still need to provide rounded top
drawRoundRect(context, 100, 100, 20, true, false)

// ---

// After, when calling a function, you already know what option you are overriding
drawRoundRect(context, 100, 100, 20, { isTopRounded: false });

// After, you can also just override the option you want to without noting all the previous params
drawRoundRect(context, 100, 100, 20, { isBottomRounded: false });
drawRoundRect(context, 100, 100, 20, { isTopRounded: false, isBottomRounded: false });

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@@ -58,40 +72,160 @@ export class Label {
}
}

export const drawLabel = (context: CanvasRenderingContext2D, label: Label) => {
export const drawLabel = (context: CanvasRenderingContext2D, label: Label, type: 'node' | 'edge') => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't really get why you need information about if it is a node or edge. Why does it matter?

Both can have multiple lines of text. As far as I can see, you probably wanted to handle the situation where padding is larger than the initial margin and in the node label case, the background color of the text would go over/under the node circle, right?

That's why we have this textBaseline property. Nodes have that set as TOP, and edges have it as MIDDLE. With that info, you have everything you need. No need to send 'node' and/or 'edge'.

fontColor: edge.style.fontColor,
fontFamily: edge.style.fontFamily,
fontSize: edge.style.fontSize,
},
});
drawLabel(context, label);
drawLabel(context, label, 'edge');
Copy link
Contributor

Choose a reason for hiding this comment

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

Please check that one other comment, this 'edge' part can be removed.

fontColor: node.style.fontColor,
fontFamily: node.style.fontFamily,
fontSize: node.style.fontSize,
},
});
drawLabel(context, label);
drawLabel(context, label, 'node');
Copy link
Contributor

Choose a reason for hiding this comment

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

Please check that one other comment, this 'node' part can be removed.

Comment on lines 238 to 240
if (label.textLines.includes('A -> B')) {
console.log(borderPoints, label);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be removed. You probably used it for testing.

};

const drawTextBackground = (context: CanvasRenderingContext2D, label: Label) => {
if (!label.properties.fontBackgroundColor || !label.position) {
const drawTextBackgroundAndBorder = (
Copy link
Contributor

Choose a reason for hiding this comment

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

The name of this function and what it does doesn't match. It tells that it is doing the drawing, but it is actually getting the points for the background of the text.
When I get into it much deeper, those are actually bounding boxes rather than border points. But this is nitpicking.

src/renderer/canvas/label.ts Show resolved Hide resolved
Comment on lines 139 to 151
const calculateBorderRadius = (borderRadius: number, width: number, height: number) => {
// ensure that the radius isn't too large for x
if (width - 2 * borderRadius < 0) {
borderRadius = width / 2;
}

// ensure that the radius isn't too large for y
if (height - 2 * borderRadius < 0) {
borderRadius = height / 2;
}

return borderRadius;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a cool function that could go to shapes.ts utility file, like getMaxValidBorderRadius.

const borderRadius = calculateBorderRadius(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.

@@ -25,6 +32,13 @@ export interface ILabelData {
properties: Partial<ILabelProperties>;
}

type BorderPoint = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Btw, we do all interfaces and types with the prefix I so we can differentiate it from the classes with the same name.

Copy link
Contributor

Choose a reason for hiding this comment

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

But, you already have a type you need, it is called IRectangle from common/rectangle.ts. Same fields, same types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants