0

I need to do a circular menu, with an unknown/variable number of elements (I underline this point because not interested in 3 or 4 elements static solutions).

I decided to use SVG. As I build the HTML/SVG with the server side code, having a SVG path, I can put the text on that path with startOffset = "@(100/items.Count)%"

.container { width: 300px; }
svg { border: 1px solid; }
a:hover { fill: red; }
<div class="container">
  <svg viewBox="0 0 200 200">
<defs>
   <desc>The path used for the text</desc>
   <path id="c" d="M150,100 A50,50 0 1 1 150,99.99z" />
</defs>
     <use xlink:href="#c" stroke="#d9d9d9" fill="none"/>
     <text font-size="20" >
      <textPath xlink:href="#c" startOffset="33%">
            <a xlink:href="https://stackoverflow.com">Our products</a>
      </textPath>
  </text>
  
  <text font-size="20" text-anchor="middle">
      <textPath xlink:href="#c" startOffset="66%">
            <a xlink:href="https://stackoverflow.com">Services</a>
      </textPath>
  </text>
  
  <text font-size="20" text-anchor="end">
      <textPath xlink:href="#c" startOffset="99%">
            <a xlink:href="https://stackoverflow.com">Achievements</a>
      </textPath>
  </text>
</svg>
</div>

My question is how to make the text readable for the user (not be superposed, inverted, hard to read) ? The concrete question would be how to make the text horizontal, keeping its position/"base point" on the circle's path? After that I will use something like

.textbox    { 
    max-width: 200px; 
    white-space: nowrap; 
    overflow: hidden;
    text-overflow: ellipsis;
}

PS. I am interested more in SVG/CSS/HTML simple solutions rather that JS complex drawing libraries.

serge
  • 13,940
  • 35
  • 121
  • 205

2 Answers2

2

Something like this?

let links = [
  {
    text: "Our products",
    url: "https://stackoverflow.com"
  },
  {
    text: "Services",
    url: "https://stackoverflow.com"
  },
  {
    text: "Achievements",
    url: "https://stackoverflow.com"
  },
  {
    text: "something else",
    url: "https://stackoverflow.com"
  },
  {
    text: "test",
    url: "https://stackoverflow.com"
  },
  {
    text: "more stuff",
    url: "https://stackoverflow.com"
  }
];


const RADIUS_PADDING = 10;



function makeMenu(circleElementId, linksData)
{
  var circle = document.getElementById(circleElementId);
  var svg = circle.ownerSVGElement;
  var r = circle.r.baseVal.value + RADIUS_PADDING;
  var cx = circle.cx.baseVal.value;
  var cy = circle.cx.baseVal.value;

  for (var i = 0; i < linksData.length; i++)
  {
    var angle = i * 2 * Math.PI / linksData.length;
    var o = {'x': cx + r * Math.sin(angle),
             'y': cy - r * Math.cos(angle),
             'text-anchor': (angle <= Math.PI) ? "start" : "end"};


    // Make a link (a) element
    var aLink = addLink(linksData[i].url, svg);
    
    // Make a text element for the link text
    addText(o, linksData[i].text, aLink);
    
  }
}


function addLink(url, parent)
{
  var link = document.createElementNS(parent.namespaceURI, "a");
  link.setAttributeNS("http://www.w3.org/1999/xlink", "href", url);
  parent.appendChild(link);
  return link;
}


function addText(o, txt, parent)
{
  var text = document.createElementNS(parent.namespaceURI, "text");
  for (var name in o) {
    if (o.hasOwnProperty(name)) {
      text.setAttribute(name, o[name]);
    }
    text.textContent = txt;
  }
  parent.appendChild(text);
  return text;
}



makeMenu("menu-circle", links)
svg {
  border: 1px solid;
  width:90vh;
}
circle {
  fill: none;
  stroke: #d9d9d9;
}
text {
  font-size: 8px;
  font-family:consolas;
  dominant-baseline: middle;
}
a:hover{fill:red}
<div class="container">
  <svg viewBox="-10 0 220 200">

   <circle id="menu-circle" r="60" cx="100" cy="100" stroke="black" fill="none"/>

  </svg>
</div>
Paul LeBeau
  • 97,474
  • 9
  • 154
  • 181
  • ) maybe just center the top and bottom elements? ) great work – serge Dec 06 '18 at 16:03
  • `'text-anchor': (angle % Math.PI == 0) ? "middle" : (angle < Math.PI) ? "start" : "end"};` – serge Dec 06 '18 at 16:10
  • I knew you could do it ;) – Paul LeBeau Dec 06 '18 at 16:11
  • what if I need some little dots on the circle? ) – serge Dec 06 '18 at 16:11
  • Then make a `` element. The code for that will look very similar to the `addText()` function. – Paul LeBeau Dec 06 '18 at 16:13
  • one more, last, question, I try to add `text { fill: blue; max-width: 50px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }` only the "fill" is taken into consideration (is not css), could I apply some CSS to the text elements? – serge Dec 06 '18 at 16:17
  • 1
    All the CSS properties in that list (apart from `fill`) are HTML-only properties. They have no meaning in SVG. SVG does not have automatic text wrapping. – Paul LeBeau Dec 06 '18 at 16:33
  • your answer fits me better than @enxaneta's one ... however I should admit your code is just a improvement of the his one... if you want ot mark your post as answer, you could mention it in your text ;) – serge Dec 06 '18 at 16:37
  • as for CSS, I heard (https://stackoverflow.com/a/15977587/961631) I could use "foreignObject" to keep the CSS – serge Dec 06 '18 at 16:53
  • You can style text elements (font, weight, etc) in SVG. You just can't do automatic wrapping. You can use ``, but you may strike some issues. For example foreignObject doesn't work in IE. – Paul LeBeau Dec 06 '18 at 17:07
1

No complex JS libraries, however there is some JS used to calculate the position of the text. The text is horizontal. I don't really like the outcome (aesthetically speaking). For a circular menu I would use icons.

const SVG_NS = "http://www.w3.org/2000/svg";
const SVG_XLINK = "http://www.w3.org/1999/xlink";
let links = [
  {
    text: "Our products",
    parent: "_a"
  },
  {
    text: "Services",
    parent: "_b"
  },
  {
    text: "Achievements",
    parent: "_c"
  },
  {
    text: "something else",
    parent: "_d"
  },
  {
    text: "test",
    parent: "_e"
  }
];

let R = 60;
let center = { x: 100, y: 100 };

for (let i = 0; i < links.length; i++) {
  let angle = i * 2 * Math.PI / links.length;
  let o = {};
  o.x = center.x + R * Math.cos(angle);
  o.y = center.y + R * Math.sin(angle);

  let theparent = document.querySelector("#" + links[i].parent);

  drawText(o, links[i].text, theparent);
}

function drawText(o, txt, parent) {
  var text = document.createElementNS(SVG_NS, "text");
  for (var name in o) {
    if (o.hasOwnProperty(name)) {
      text.setAttributeNS(null, name, o[name]);
    }
    text.textContent = txt;
  }

  parent.appendChild(text);
  return text;
}
svg {
  border: 1px solid;
  width:90vh;
}
circle {
  fill: none;
  stroke: #d9d9d9;
}
text {
  font-size: 12px;
  font-family:consolas;
  dominant-baseline: middle;
  text-anchor: middle;
}
a:hover{fill:red}
<div class="container">
  <svg viewBox="-10 0 220 200">

   <circle r="60" cx="100" cy="100" stroke="black" fill="none"/>

<a xlink:href="https://stackoverflow.com" id="_a"></a>
<a xlink:href="https://stackoverflow.com" id="_b"></a>
<a xlink:href="https://stackoverflow.com" id="_c"></a>
<a xlink:href="https://stackoverflow.com" id="_d"></a>
<a xlink:href="https://stackoverflow.com" id="_e"></a>
</svg>
</div>

See a codepen demo

enxaneta
  • 31,608
  • 5
  • 29
  • 42
  • is a little bit hard to automate this... I mean, if I have 100 links in JS, I need to respectively generate 100 `a` tags in html and also generate the ids a, b c... z... aa bb? why do not use the startOffeset instead? – serge Dec 06 '18 at 13:33
  • Because startOffeset is a `` attribute. – enxaneta Dec 06 '18 at 14:02
  • why do not use textpath? – serge Dec 06 '18 at 14:09
  • When you use `` text is rendered along the path. You wanted `to make the text horizontal, keeping its position/"base point" on the circle's path`. Maybe I didn't understood your your question. Maybe you should edit it by adding an image with the desired output. – enxaneta Dec 06 '18 at 14:13
  • I want the text be linked with the path... more that that, if the picture is rezised (say, the window became smaller), the text should follow the path... its base point should be on the path. Just force textPath to be horizontally drawn – serge Dec 06 '18 at 14:16
  • Maybe you should take a look at this pen: [circular menu](https://codepen.io/AndrewHaze/pen/PRaZEZ?editors=1000). It may give you some ideas. Also please search for [circular menu in codepen](https://codepen.io/search/pens?q=circular%20menu&page=1&order=popularity&depth=everything&show_forks=false) – enxaneta Dec 06 '18 at 14:24
  • the first pen is great, however it's hardly coded for 13 elements – serge Dec 06 '18 at 14:53