Can someone guide me towards creating a honeycomb like below, where I can add labels both outside and inside the shapes? I do not need any colors or fills for the moment.
Are you aware of the polygon
function and CeTZ packageâs cetz.draw.line()
? It should be relatively easy to use either to draw this (CeTZ is probably a bit easier), using a bit of trigonometry. For this, youâll also want to use the calc.sin()
and calc.cos()
functions to determine coordinates.
Basically:
- choose the coordinates for where to have the first hexagonâs center (
cx
,cy
), and a âradiusâ - the six corners are each shifted by 60°, and you get them by
x = cx + cos(angle)*radius
andy = cy + sin(angle)*radius
(the Wikipedia article on the unit circle may help making it more clear why this works) - the next hexagon has
cx2 = cx1 + 1.5*radius
, which is pretty easy to see.cy2
is the same as the first hexagonâs lower cornersây
. - to place the texts, add some extra distance to the upper/lower corners, and center text at
cx
.
I think this should get you going. Feel free to ask follow-up questions, and when youâre done please post your result so that others can easily reuse it :)
Thank you @SillyFreak, I was not aware. I will try your suggestion, and return with the result.
#import "@preview/cetz:0.2.2"
#set page(height: auto, width: auto, margin: 0pt)
#let background-color = gray
#let fill-color = white
#let stroke-color = black
#let labels = ("STR", "DEX", "END", "INT", "EDU", "SOC")
// #let labels = ("STR", "DEX", "END") // This also works!
#let label-formatter(label) = text(font: "Liberation Sans", emph(strong(label)))
#let d = 1.8cm // The diameter of the circumcircle of the hexagon.
#let r = d / 2 // The radius of the circumcircle of the hexagon.
// The height of one of 6 triangles inside hexagon, i.e., half-height of hexagon.
#let h = r * calc.sqrt(3) / 2
// _____
// /| \
// / | \
//|\ | /
//| \|____/
//| |
//|ââ|
// Small height (of a small triangle inside the hexagon).
#let hh = calc.sqrt(r.cm() * r.cm() - h.cm() * h.cm()) * 1cm
#let hexagon-shape = polygon.regular(
vertices: 6,
size: d,
fill: fill-color,
stroke: stroke-color,
)
#let hexagon(rel-pos, name, label-pos, label) = {
import cetz.draw: *
let rel-to = if name == 1 { () } else { str(name - 1) }
let position = (rel: rel-pos, to: rel-to)
content(position, name: str(name), hexagon-shape)
let text-offset = 0.2
let text-offset = text-offset * if label-pos == top { 1 } else { -1 }
let side = if label-pos == top { "north" } else { "south" }
content(
(rel: (0, text-offset), to: str(name) + "." + side),
label-formatter(label),
)
}
#let hexagon-right-top(name, label) = {
hexagon((hh + r, h), name, top, label)
}
#let hexagon-right-bottom(name, label) = {
hexagon((hh + r, -h), name, bottom, label)
}
#let img = cetz.canvas(background: background-color, {
import cetz.draw: *
let last = labels.len()
// Draw each hexagon and its label.
for (i, label) in labels.enumerate() {
if calc.odd(i + 1) {
hexagon-right-top(i + 1, label)
} else {
hexagon-right-bottom(i + 1, label)
}
}
// Draw background horizontal stripe.
on-layer(-1, {
let extend = 1
line(
(rel: (-extend, 0), to: "1.west"),
if calc.even(labels.len()) {
(rel: (extend, 0), to: str(last) + ".north-east")
} else { () },
(rel: (extend, 0), to: str(last) + ".east"),
if calc.odd(labels.len()) {
(rel: (extend, 0), to: str(last) + ".south-east")
} else { () },
(rel: (-extend, 0), to: "1.south-west"),
close: true,
fill: fill-color,
stroke: fill-color,
)
})
})
// Add vertical padding.
#let img = block(inset: (y: 2mm), fill: background-color, img)
#img
I tried to optimize the code, but I have very little overall cetz experience. Maybe @jwolf or someone else more experienced can get a cleaner/shorter solution, but I think the algorithm more or less will be the same.
Thanks a lot for contributing @Andrew your solution seems slightly simpler than the one I came up with
But now that I have spent all that time Iâm going to post my solution anyways.
First I tried the solution with sin() and cos() that @SillyFreak posted, but I think my math was way off because I never ended up with a real hexagon. But instead someone had posted the vertices on a webpage: (1, 0), (1/2, sqrt3 / 2), (-1/2, sqrt3 / 2), (-1, 0), (-1/2, -sqrt3 / 2), (1/2, -sqrt3 / 2)
So with this I succeeded.
Progress
And here is my code
#import "@preview/cetz:0.2.2"
#set page(
paper: "a3",
flipped: true,
)
#set text(
font: "IBM Plex Sans",
)
#cetz.canvas(
length: 20pt,
{
import cetz.draw: *
// grid((-1.5,-3), (9,2), stroke: luma(240), step: .25)
// grid((-1.5, -3), (9, 2), stroke: luma(160), step: 5)
let centery = 0
let centerx = 0
let angle = 60
let radius = 0.1
// Vertices at
// (1, 0)
// (1/2, sqrt3 / 2)
// (-1/2, sqrt3 / 2)
// (-1, 0)
// (-1/2, -sqrt3 / 2)
// (1/2, -sqrt3 / 2)
let hex(x, y, property, level) = {
let ax = 1
let ay = 0
let a = (x + ax, y + ay)
let bx = 1 / 2
let by = calc.sqrt(3) / 2
let b = (x + bx, y + by)
let cx = -(1 / 2)
let cy = by
let c = (x + cx, y + cy)
let dx = -1
let dy = 0
let d = (x + dx, y + dy)
let ex = -(1 / 2)
let ey = -by
let e = (x + ex, y + ey)
let fx = 1 / 2
let fy = -by
let f = (x + fx, y + fy)
// circle(a, radius: radius, stroke: 1pt + blue)
// circle(b, radius: radius, stroke: blue)
// circle(c, radius: radius, stroke: blue)
// circle(d, radius: radius, stroke: purple)
// circle(e, radius: radius, stroke: purple)
// circle(f, radius: radius, stroke: purple)
line(a, b, c, d, e, f, stroke: 1.5pt, close: true)
content((x + 0,y + 0), [#level])
content((x + 0,y + 1.2), [#text(style: "italic", weight: "bold", [#property])])
}
hex(0,0, "STR", "10")
hex(1.5,-calc.sqrt(3)/2, "DEX", "10")
hex(3,0, "END", "10")
hex(4.5,-calc.sqrt(3)/2, "INT", "10")
hex(6.0,0, "EDU", "10")
hex(7.5,-calc.sqrt(3)/2, "SOC", "10")
})
Youâre welcome. I can make my solution even simpler if you donât need documentation, readability, and flexibility (but I donât recommend it because it lacks all of that):
#set page(height: auto, width: auto, margin: 0pt)
#import "@preview/cetz:0.2.2"
#let d = 1.8cm
#let hexagon(rel-pos, name, label) = {
cetz.draw.content(
(rel: rel-pos, to: if name == 1 { () } else { str(name - 1) }),
name: str(name),
polygon.regular(vertices: 6, size: d, fill: white, stroke: black),
)
let text-offset = 0.2 * if calc.odd(name) { 1 } else { -1 }
let side = if calc.odd(name) { "north" } else { "south" }
cetz.draw.content(
(rel: (0, text-offset), to: str(name) + "." + side),
text(font: "Liberation Sans", emph(strong(label))),
)
}
#block(inset: (y: 2mm), fill: gray, cetz.canvas(background: gray, {
let r = d / 2
let h = r * calc.sqrt(3) / 2
let hh = calc.sqrt(r.cm() * r.cm() - h.cm() * h.cm()) * 1cm
hexagon((hh + r, h), 1)[STR]
hexagon((hh + r, -h), 2)[DEX]
hexagon((hh + r, h), 3)[END]
hexagon((hh + r, -h), 4)[INT]
hexagon((hh + r, h), 5)[EDU]
hexagon((hh + r, -h), 6)[SOC]
cetz.draw.on-layer(-1, {
cetz.draw.rect(
(rel: (-1, 0), to: "1.west"),
(rel: (1, 0), to: "6.east"),
fill: white,
stroke: white,
)
})
}))
I also saw some optimizations like not needing the label-pos
parameter and substituting line()
with rect()
.