Cmagelssen
Cmagelssen

Reputation: 660

Create a tooltip that displays Y-position on a line (d3.js)

I have managed to drag a skier along a line. The final big step is to create a tooltip that calculates the Y-location of the skier as I drag him up on the line. Let's say that the top on the line corresponds to 100 meter and the buttom of the line to be 0. Is this possible? If so, how can I accomplish this?

draganddrop skier

const height = 500;
const width = 960;
const skierIconSvg = 'https://image.flaticon.com/icons/svg/94/94150.svg';

const [p1, p2, p3] = [
    [width / 3, 213],
    [(2 * width) / 3, 300],
    [width / 2, 132],
];

const svg = d3.select('svg');

const line = svg.append('line').attr('stroke', 'black');

const projection = svg
    .append('circle')
    .attr('r', 5)
    .attr('stroke', 'red')
    .attr('fill', 'none');

const g = svg
    .append('g')
    .attr('cursor', 'move')
    .attr('pointer-events', 'all')
    .attr('stroke', 'transparent')
    .attr('stroke-width', 30);

const skier = g
    .append('image')
    .attr('id', 'skier')
    .datum(p3)
    .attr('href', skierIconSvg)
    .attr('width', 100)
    .attr('height', 100)
    .attr('transform', 'translate(-50, 40)')
    .call(
        d3
            .drag()
            .subject(([x, y]) => ({
                x,
                y,
            }))
            .on('drag', dragged)
    );
// create a tooltip

update();

function dragged(d) {
    d[0] = d3.event.x;
    d[1] = d3.event.y;
    update();
}

function update() {
    const t = (width + height) / distance(p1, p2);
    const l1 = interpolate(p1, p2, t);
    const l2 = interpolate(p2, p1, t);
    const p = interpolate(p1, p2, project(p1, p2, p3));
    projection.attr('cx', p[0]).attr('cy', p[1]);
    line.attr('x1', l1[0]).attr('y1', l1[1]);
    line.attr('x2', l2[0]).attr('y2', l2[1]);
    skier.attr('x', (d) => d[0]).attr('y', (d) => d[1]);
}

function distance([x1, y1], [x2, y2]) {
    return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}

function interpolate([x1, y1], [x2, y2], t) {
    return [x1 + (x2 - x1) * t, y1 + (y2 - y1) * t];
}

function project([x1, y1], [x2, y2], [x3, y3]) {
    const x21 = x2 - x1,
        y21 = y2 - y1;
    const x31 = x3 - x1,
        y31 = y3 - y1;
    return (x31 * x21 + y31 * y21) / (x21 * x21 + y21 * y21);
}
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <script src="https://d3js.org/d3.v5.js"></script>
        <script src="https://d3js.org/d3-path.v1.min.js"></script>
        <script src="https://d3js.org/d3-shape.v1.min.js"></script>
        <script src="https://d3js.org/d3-scale.v3.min.js"></script>
        <script src="https://d3js.org/d3-axis.v1.min.js"></script>
        <script src="https://d3js.org/d3-dispatch.v1.min.js"></script>
        <script src="https://d3js.org/d3-selection.v1.min.js"></script>

        <link
            href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@300&display=swap"
            rel="stylesheet"
        />
        <link
            href="https://fonts.googleapis.com/css2?family=Amatic+SC:wght@700&display=swap"
            rel="stylesheet"
        />

        <style>
            * {
                font-family: 'Amatic SC', cursive;
                text-align: center;
            }
            h1 {
                font-size: 50px;
            }
            p {
                font-size: 20px;
            }

            path {
                fill: none;
                stroke: #000;
                stroke-width: 4px;
            }

            circle {
                fill: steelblue;
                stroke: #fff;
                stroke-width: 3px;
            }
        </style>
    </head>

    <body>
        <h1>Forsøk på å lage en tutorial i JavaScript og D3.js</h1>
        <svg width="960" height="500"></svg>

        <script src="main.js"></script>
    </body>
</html>

Upvotes: 0

Views: 33

Answers (1)

Ruben Helsloot
Ruben Helsloot

Reputation: 13129

Yes, this is very possible, since you know the properties of p (the projection) and p2 (the bottom right point) right when you're about to update them, the perfect moment to display it.

You do seem to have removed the other points though - compared to your last question, so it's difficult to use them as a reference point. That means that technically, the skier can go beyond 100 metres high or even below 0 metres.

I used toFixed() to format the number, but you could easily use d3-format if you'd prefer of course.

const height = 500;
const width = 960;
const skierIconSvg = 'https://image.flaticon.com/icons/svg/94/94150.svg';

const [p1, p2, p3] = [
  [width / 3, 213],
  [(2 * width) / 3, 300],
  [width / 2, 132],
];

const svg = d3.select('svg');

const line = svg.append('line').attr('stroke', 'black');

// Store a reference to the span we're going to update
const skierHeight = d3.select("#skier-height");

const projection = svg
  .append('circle')
  .attr('r', 5)
  .attr('stroke', 'red')
  .attr('fill', 'none');

const g = svg
  .append('g')
  .attr('cursor', 'move')
  .attr('pointer-events', 'all')
  .attr('stroke', 'transparent')
  .attr('stroke-width', 30);

const skier = g
  .append('image')
  .attr('id', 'skier')
  .datum(p3)
  .attr('href', skierIconSvg)
  .attr('width', 100)
  .attr('height', 100)
  .attr('transform', 'translate(-50, 40)')
  .call(
    d3
    .drag()
    .subject(([x, y]) => ({
      x,
      y,
    }))
    .on('drag', dragged)
  );
// create a tooltip

update();

function dragged(d) {
  d[0] = d3.event.x;
  d[1] = d3.event.y;
  update();
}

function update() {
  const t = (width + height) / distance(p1, p2);
  const l1 = interpolate(p1, p2, t);
  const l2 = interpolate(p2, p1, t);
  const p = interpolate(p1, p2, project(p1, p2, p3));
  projection.attr('cx', p[0]).attr('cy', p[1]);
  line.attr('x1', l1[0]).attr('y1', l1[1]);
  line.attr('x2', l2[0]).attr('y2', l2[1]);
  skier.attr('x', (d) => d[0]).attr('y', (d) => d[1]);

  skierHeight.text(`${getHeight(p, p1, p2).toFixed(2)} metres`);
}

function distance([x1, y1], [x2, y2]) {
  return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}

function interpolate([x1, y1], [x2, y2], t) {
  return [x1 + (x2 - x1) * t, y1 + (y2 - y1) * t];
}

function project([x1, y1], [x2, y2], [x3, y3]) {
  const x21 = x2 - x1,
    y21 = y2 - y1;
  const x31 = x3 - x1,
    y31 = y3 - y1;
  return (x31 * x21 + y31 * y21) / (x21 * x21 + y21 * y21);
}

function getHeight([xp, yp], [x1, y1], [x2, y2]) {
  // Note that y is counted from top to bottom, so higher y means
  // a point is actually lower.

  // First, the total height is 100 metres.
  const pxPerMeter = (y2 - y1) / 100;
  
  // Calculate the height diff in pixels
  const heightDiffPx = (y2 - yp);
  
  // Now transform it to meters
  return heightDiffPx / pxPerMeter;
}
* {
  font-family: 'Amatic SC', cursive;
  text-align: center;
}

h1 {
  font-size: 50px;
}

p {
  font-size: 20px;
}

path {
  fill: none;
  stroke: #000;
  stroke-width: 4px;
}

circle {
  fill: steelblue;
  stroke: #fff;
  stroke-width: 3px;
}
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <script src="https://d3js.org/d3.v5.js"></script>
  <script src="https://d3js.org/d3-path.v1.min.js"></script>
  <script src="https://d3js.org/d3-shape.v1.min.js"></script>
  <script src="https://d3js.org/d3-scale.v3.min.js"></script>
  <script src="https://d3js.org/d3-axis.v1.min.js"></script>
  <script src="https://d3js.org/d3-dispatch.v1.min.js"></script>
  <script src="https://d3js.org/d3-selection.v1.min.js"></script>

  <link href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@300&display=swap" rel="stylesheet" />
  <link href="https://fonts.googleapis.com/css2?family=Amatic+SC:wght@700&display=swap" rel="stylesheet" />
</head>

<body>
  <h1>Forsøk på å lage en tutorial i JavaScript og D3.js</h1>
  <h2>Height: <span id="skier-height"></span></h2>
  <svg width="960" height="500"></svg>

  <script src="main.js"></script>
</body>

</html>

Upvotes: 1

Related Questions