chalk
chalk copied to clipboard
Examples drawing arrows using trails
There are currently no examples of using arrows with trails in ./examples/arrows.py, so I'm working off bits and pieces here and in haskell diagrams. I'm not very strong with haskell, arrow.html#lengths-and-gaps looks similar to my problem but I'm not sure how to proceed.
I'm trying to get A to connect to B, using arrows and trails.
code (click to expand)
from PIL import Image as PILImage
from chalk import *
from colour import Color
from copy import deepcopy
from argparse import ArgumentParser
EM = 12
LINE_WIDTH = 0.05
black = Color("#000000")
white = Color("#ffffff")
grey = Color("#cccccc")
green = Color("#00ff00")
def Block(
label: str,
width: int = 5 * EM,
height: int = 2 * EM,
stroke: Color = black,
fill: Color = white,
):
radius = min(width, height) / 20
font_size = min(width, height) / 2
container = (
rectangle(width, height, radius)
.line_color(stroke)
.fill_color(fill)
.line_width(LINE_WIDTH)
).center_xy()
bounding_rect = container.scale(0.9).center_xy()
render_label = (
text(label, font_size)
.line_color(black)
.line_width(0)
.fill_color(black)
.with_envelope(bounding_rect)
).translate(0, height / 16)
return container + render_label
if __name__ == "__main__":
parser = ArgumentParser()
parser.add_argument("--path", type=str, required=True)
args = parser.parse_args()
A_block = Block("A").named("A")
node = square(1).fill_color(None).line_color(None)
A = vcat([node.named("a"), A_block]).center_xy()
B_block = Block("B").named("B")
B = hcat([node.named("b"), B_block]).center_xy()
diagram = hcat([A, B], 2 * EM)
trail = Trail.from_offsets(
[
V2(0, -EM),
V2(EM, 0),
V2(0, EM),
V2(EM, 0),
]
)
arrow1_opts = ArrowOpts(
**{
"head_pad": 0,
"tail_pad": 0,
"trail": trail,
"arc_height": 0,
"shaft_style": Style.empty().line_color(grey),
}
)
arrow2_opts = ArrowOpts(
**{
"head_pad": 0,
"tail_pad": 0,
"trail": trail,
"arc_height": 0,
"shaft_style": Style.empty().line_color(green),
}
)
arrow_hidden_from_centers = diagram.connect("A", "B", arrow1_opts)
arrow_outside = diagram.connect("a", "b", arrow2_opts)
diagram = arrow_hidden_from_centers + diagram + arrow_outside
diagram.render_svg(args.path, height=512)
As visible from the picture, I'm not having much luck with this. I tried some rotation of the arrow (outside) but that ends up weird. I'm assuming the trail is placed on the line between the nodes a and b. Placing the arrow (inside, but atop) through A and B centers get me the desired result (grey), but in this case I find it difficult to place heads (dart) and tails without configuring pad.
The larger diagram I'm trying to solve the above for is below (trying to connect an encoder and decoder), and the endpoints are not parallel to horizontal:
translation (click to expand)
Hello Jerin! Thanks for the report! I'll try to have a look at the issue these days.
Hi, I have worked around this for the time being by just manually finding locations and adding an elbow connector myself.
snippet
eout = diagram.get_subdiagram("encoder_out").get_location()
d0in = diagram.get_subdiagram("decoder_0_in").get_location()
print(eout)
print(d0in)
# Create an elbow connector.
dx = -1 * (eout.x - d0in.x)
dy = -1 * (eout.y - d0in.y)
up = 4
p0 = V2(0, 0)
p1 = V2(0, -1 * up)
p2 = V2(dx / 2, 0)
p3 = V2(0, (dy + up))
p4 = V2(dx / 2, 0)
# print(eout + p1 + p2 + p3 + p4, d0in)
# assert p4 == d0in
elbow_connector = trail.stroke().line_color(grey).translate(eout.x, eout.y)
diagram = elbow_connector + diagram
render
Cool! I think your solution provides a good compromise for the current situation.
I thought about this issue as well, but I have failed to come up with a better answer. As you've noticed, the reason of the observed behavior is that the arrow's trail is relative to its orientation (the start-point to end-point vector). I've also tried getting some inspiration from the Haskell example that you've mentioned, but I had trouble understanding it.
Maybe flowcharts should use a different API, for example, something similar to the one in TikZ? Let me know if you have any suggestions on this matter.
While trying out a few more diagrams - I get a feeling a connect_elbow(source, target, *args), will be enough to create a few basic elbow connectors (⤷⤴⤵⤶ and rotations, reflections). I'm not sure what args look like at this point, I think it could be a V2(dx, 0) or V2(0, dy) which encodes a start direction, used in conjunction with boundary_from(...) to get boundary, and followed by automatic work out of a path constrained to use horizontal or vertical movements to generate an elbow connector.
While I'm familiar with Tikz, I have not used it enough to form an opinion. My preferred choices for diagrams at the moment are inkscape > excalidraw > ppt-software. I'm currently trying chalk to replace this with a chalk + finishing touch-ups by inkscape workflow.
Thanks for the feedback @jerinphilip! I'm currently on vacation, but I'll try to implement your suggestion when I get back.
I've added a connect_outside_elbow function here (on the 122-elbow-connections branch). The function supports two types of connections: "hv" (horizontal-then-vertical connection) or "vh" (vertical-then-horizontal connection).
An example would be:
from colour import Color
from chalk import *
from chalk.arrow import connect_outside_elbow
color = Color("pink")
def make_dia():
c1 = circle(0.75).fill_color(color).named("src") + text("src", 0.7)
c2 = circle(0.75).fill_color(color).named("tgt") + text("tgt", 0.7)
return c1 + c2.translate(3, 3)
dia1 = make_dia()
dia1 = connect_outside_elbow(dia1, "src", "tgt", "hv")
dia2 = make_dia()
dia2 = connect_outside_elbow(dia2, "src", "tgt", "vh")
dia = hcat([dia1, dia2], sep=2)
path = "examples/output/connect_elbow.svg"
dia.render_svg(path, height=256)
yielding
Is this similar to what you envisioned?
I get the following directly using your code:
Adding .line_width(0) to text gives me this:
I think this is an entirely different bug?
I'm happy with the results I can achieve with this convenience.
elbow.py
from colour import Color
from chalk import *
from chalk.arrow import connect_outside_elbow
from .cli import basic_parser
if __name__ == "__main__":
parser = basic_parser()
args = parser.parse_args()
color = Color("pink")
def node(label):
c = circle(0.75).fill_color(color).named(label) + text(label, 0.7).line_width(0)
return c
def make_diagram():
points = [V2(3, 0), V2(0, 3), V2(-3, 0), V2(0, -3)]
diagram = empty()
for idx, point in enumerate(points):
diagram = diagram + node(f"c{idx}").translate(point.x, point.y)
return diagram
clockwise = make_diagram()
for idx in range(1, 4):
direction = "hv" if idx % 2 == 0 else "vh"
clockwise = connect_outside_elbow(clockwise, f"c{idx-1}", f"c{idx}", direction)
direction = "hv"
clockwise = connect_outside_elbow(clockwise, f"c3", f"c0", direction)
counterclockwise = make_diagram()
for idy in range(1, 4):
idx = 4 - idy
direction = "hv" if idx % 2 == 1 else "vh"
counterclockwise = connect_outside_elbow(
counterclockwise, f"c{idx}", f"c{idx-1}", direction
)
direction = "vh"
counterclockwise = connect_outside_elbow(counterclockwise, f"c0", f"c3", direction)
# dia2 = make_diagram()
# dia2 = connect_outside_elbow(dia2, "src", "tgt", "vh")
# dia = hcat([dia1, dia2], sep=2)
diagram = hcat([clockwise, counterclockwise], sep=2)
diagram.render_svg(args.path, height=256)
Hmm... the first rendering looks unexpected. In this Colab it looks fine. What tool do you use to open and view the SVG? Can it be related to this?
I'm running ArchLinux, I suspect this could be due to a font difference (hence line_width(0) helping out). Colab runs in a google modified Ubuntu environment, so the difference could just be that. If we increase line-width in the colab, we can notice getting similar artifacts. Are there any settings to control font in the Cairo backend exposed?
I'm using Eye of Gnome (eog), the default image-viewer on Gnome.