How do I make a honeycomb?

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.

2024-09-27 13_13_53-Trav2022CharSheet.pdf - Adobe Acrobat Reader (32-bit)

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 and y = 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.

1 Like
#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

out

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 :smiley:

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")
})
1 Like

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().