D3js - Elliptic Speech Bubble

697 Views Asked by At

I've been reading a lot lately about ellipsis and Bezier Curves, in order to be able to draw a "Speech Bubble" with d3js: This bubble has to be contained inside a rectangular or square container like so:

http://edn.embarcadero.com/article/images/10277/D15PX16.jpg

But of course it would contain a small arrow on the bottom left to give that "speech bubble" effect. Since this container will be resizable, I need the points to be recalculated when it happens.

Given those conditions, the bubble could be elliptic or an almost perfect circle in case of a square container. I'm guessing I can't use ellipsis, because drawing the arrow of the Speech Bubble would not be possible in that case.

While doing research, I've stumbled upon http://spencermortensen.com/articles/bezier-circle/ which I thought would be a good place to start but I can't really grasp how to start translating this into code, and most importantly how to use d3js to help me render it, espcially since I have the difficulty of having to draw an arrow somewhere on left bottom part of my ellipse.

I'd be really thankful if somebody could explain to me how to calculate the points I need to draw that speech bubble, and how D3js can draw it.

Thanks !

PS: Sorry I couldnt be more specific and add images, but my current reputation doesn't allow me to.

2

There are 2 best solutions below

1
On BEST ANSWER
function callout(parameters) {
    var w = parameters.width || 200,
            h = parameters.height || 100,
            a = w / 2,
            b = h / 2,
            o_x = parameters.x0 || 100,
            o_y = parameters.y0 || 100,
            m_r = parameters.l || 300,
            m_w = 10,
            m_q = parameters.angle * Math.PI / 180 || 50 * Math.PI / 180,
            m_q_delta = Math.atan(m_w / (2 * Math.min(w, h)));

    var d = "M", x, y, 
            d_q = Math.PI / 30; // 1/30 -- precision of drawing

    // now, we are drawing the path step by step
    for (var alpha = 0; alpha < 2 * Math.PI; alpha += d_q) {

        if (alpha > m_q - m_q_delta && alpha < m_q + m_q_delta) { //edge
            x = o_x + m_r * Math.cos(m_q);
            y = o_y + m_r * Math.sin(m_q);
            d += "L" + x + "," + y;
            alpha = m_q + m_q_delta;
        } else { // ellipse
            x = a * Math.cos(alpha) + o_x;
            y = b * Math.sin(alpha) + o_y;
            d += "L" + x + "," + y + " ";
        }
    }
    d += "Z";
    return(d.replace(/^ML/, "M").replace(/ Z$/, "Z"));
}

var styles = {
    board: {width: 1000, height: 1000},
    callout: {stroke: "black", "stroke-width": 1, fill: "snow"}
};

var callout_params = {
    width: 300,
    height: 100,
    angle: 20,
    l: 250,
    x0: 200,
    y0: 200
};

var board = d3.select("body").append("svg:svg").attr(styles.board);
var defs = board.append("svg:defs");
var callout = board.append("svg:path").attr(styles.callout).attr("d", callout(callout_params));

DEMO: http://jsbin.com/kalijumepe/1/

0
On

DEMO: http://jsbin.com/firacaredi/2/

You can use just markers and not reconstruct intersections of ellipse with two vectors to produce precisely correct bezier path:

var styles = {
  board: {width: 500, height: 400},

  bubble: {id: "bubble", refX: 0, refY: 0, markerWidth: 200, markerHeight: 200, viewBox: "-4 -4 8 4"},
  bubble_ellipse: {fill: "snow", stroke: "none", cx: 0, cy: 0, rx:4, ry: 2},

  handle: {id: "handle", refX: 0, refY: 2, "markerWidth": 100, "markerHeight": 20, orient: "auto", viewBox: "0 0 4 4" },
  handle_path: {"d": "M 0,0 V 4 L10,1", fill: "snow"},

  text: {fill: "black", x: 190, y: 110},
  callout: {fill: "snow", stroke: "snow", "stroke-width": 2, "d": "M200,100L130,160", "marker-end": "url(#handle)", "marker-start": "url(#bubble)",}
};

var board = d3.select("body").append("svg:svg").attr(styles.board);
var defs = board.append("svg:defs");
defs.append("svg:marker").attr(styles.handle).append("svg:path").attr(styles.handle_path);
defs.append("svg:marker").attr(styles.bubble).append("svg:ellipse").attr(styles.bubble_ellipse);

board.append("svg:path").attr(styles.callout);
board.append("svg:text").attr(styles.text).text("HI!");

Then, if you need a border, you can drop one pixel shadow or place dark, two pixels bigger callout shape under the light one.