yavg
yavg

Reputation: 3051

how can I make the shape of a medition using d3.js?

basically I'm trying to do something like the image, I'm basing myself on the image on the left, my goal is to have something like the image on the right.

enter image description here

This is the most I have achieved:

enter image description here

I am new to d3.js, I don't know how to make the thermometer shape, I have tried many things but maybe because of my inexperience I get too complicated.

this is my live code:

https://jsfiddle.net/zukdymvf/1/

function generateNumber() {
  let number = parseInt(Math.random() * 100)
  //let number= 100

  d3.select("#indicador").attr("y", yScale(number) - 3); // to center the indicador image
  d3.select("#indicadorText").attr("y", yScale(number) + 5).text(number);
}

let heightRectangle = 10;
d3.select("#visualization").append('svg')
var vis = d3.select("svg").attr("width", 800).attr("height", 614).style("border", "1px solid red");

let g = vis.append("g").style("transform", "translate(0px, 10px)").style("transform", "translate(10px, 10px)");


g.append("image")
  .attr("id", "indicador")
  .attr("href", "https://www.shareicon.net/data/256x256/2015/08/17/86784_left_512x512.png")
  .attr("width", 15)
  .attr("height", 15)
  .attr("y", 403)
  .style("transform", "translate(100px)")

g.append("text")
  .attr("id", "indicadorText")
  .text("0")
  .attr("x", 120)
  .attr("y", 413)
  .attr("alignment-baseline", "middle")

g.append("circle")
  .attr("id", "mycircle")
  .attr("r", 55)
  .attr("cx", 52)
  .attr("cy", 450)
  .attr("width", "100")
  .attr("height", "100")
  .attr("fill", "red")
  .attr("stroke", "black")
//Doing my color bar
var arr = d3.range(101)
let maxRange = 410; // instead of 600
var yScale = d3.scaleLinear().domain([0, 100]).range([maxRange, 0])
let borderRadius = 5;
var colorScale = d3.scaleLinear().domain([0, 50, 100])
  .range(["green", "yellow", "red"])

g.selectAll('.rect').data(arr).enter()
  .append('rect')
  .attr("class", "rect")
  .attr("rx", borderRadius)

  .attr("ry", borderRadius)

  .attr("y", function(d, i) {
    return i * 4
  })
  .attr("x", 20)
  .attr("height", heightRectangle)
  .attr("width", 60)
  .attr("fill", function(d) {
    return colorScale(d)
  })


var y = d3.scaleLinear()
  .domain([0, 500])
  .range([maxRange, 0])


var yAxisBar = d3.axisLeft()
  .scale(y)
  .ticks(15);
//.tickFormat(function(d,i){ return "" });
g.append("g")
  .attr("class", "y axis")
  .attr("transform", "translate(150 , 0)")
  .call(yAxisBar)
  .append("text")
  .attr("transform", "rotate(-90)")
  .attr("y", 0)
  .attr("dy", ".71em")
  .style("text-anchor", "end")
  .text("axis title");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="visualization"></div>
<button onclick="generateNumber()">
 Generate number
</button>

Upvotes: 1

Views: 341

Answers (1)

Ruben Helsloot
Ruben Helsloot

Reputation: 13129

I wrote a plain JavaScript function that returns the shape of a thermometer. I find that this tutorial and this documentation are very valuable in understanding how to write SVG paths like that.

The arguments to pass are radius - the radius of the bulb, width - the width of the shaft, and height - the height of the shaft.

Hopefully, you can use it to render your scale inside it, and adjust its height with the new value

function getThermometerOutline(shape) {
  const height = shape.height;
  const width = shape.width;
  const radius = shape.radius;

  // Draw it in two parts, first the left, then the right
  // Start at the very top, the total width is 2x the radius
  let result = `M${radius},0 `;

  // Then draw the first rounding
  const offset = radius - (width / 2);
  result += `A ${width / 2} ${width / 2} 0 0 0 ${offset},${offset} `;

  // Go down the shaft to the base of the bulb
  const shaftHeight = height - offset - (2 * radius); // more or less
  result += `V${shaftHeight + offset} `;

  // Draw the bulb in two half circles, first we end at the bottom
  const endPoint = {
    x: radius,
    y: height
  };
  result += `a ${radius} ${radius} 0 1 0 `
  result += `${width},0 `;

  // Now move back up to the very top
  result += `V${offset} `;
  result += `A ${width / 2} ${width / 2} 0 0 0 ${radius},0 `;

  return result + 'Z';
}

function generateNumber() {
  let number = parseInt(Math.random() * 100)
  //let number= 100

  d3.select("#indicador").attr("y", yScale(number) - 3); // to center the indicador image
  d3.select("#indicadorText").attr("y", yScale(number) + 5).text(number);
}

let heightRectangle = 10;
const vis = d3.select("#visualization")
  .append('svg')
  .attr("width", 800)
  .attr("height", 614)
  .style("border", "1px solid red");

let g = vis.append("g")
  .style("transform", "translate(10px, 10px)");

g.append("image")
  .attr("id", "indicador")
  .attr("href", "https://www.shareicon.net/data/256x256/2015/08/17/86784_left_512x512.png")
  .attr("width", 15)
  .attr("height", 15)
  .attr("y", 403)
  .style("transform", "translate(100px)")

g.append("text")
  .attr("id", "indicadorText")
  .text("0")
  .attr("x", 120)
  .attr("y", 413)
  .attr("alignment-baseline", "middle")

const thermometer = vis.append("g")
  .attr("id", "thermometer")
  .attr('transform', 'translate(10, 10)');

thermometer.append("circle")
  .attr("id", "mycircle")
  .attr("r", 55)
  .attr("cx", 52)
  .attr("cy", 450)
  .attr("width", "100")
  .attr("height", "100")
  .attr("fill", "red")
  .attr("stroke", "black")
//Doing my color bar
var arr = d3.range(101)
let maxRange = 410; // instead of 600
var yScale = d3.scaleLinear().domain([0, 100]).range([maxRange, 0])
let borderRadius = 5;
var colorScale = d3.scaleLinear().domain([0, 50, 100])
  .range(["green", "yellow", "red"])

thermometer.selectAll('.rect')
  .data(arr)
  .enter()
  .append('rect')
  .attr("class", "rect")
  .attr("y", function(d, i) {
    return i * 4
  })
  .attr("x", 20)
  .attr("height", heightRectangle)
  .attr("width", 60)
  .attr("fill", function(d) {
    return colorScale(d)
  })

const outline = thermometer.append("path")
  .attr("id", "outline")
  .attr("d", getThermometerOutline({
    width: 62,
    height: 532,
    radius: 56
  }))
  .attr("transform", "translate(-5, -16)")

var y = d3.scaleLinear()
  .domain([0, 500])
  .range([maxRange, 0])


var yAxisBar = d3.axisLeft()
  .scale(y)
  .ticks(15);
//.tickFormat(function(d,i){ return "" });

g.append("g")
  .attr("class", "y axis")
  .attr("transform", "translate(150 , 0)")
  .call(yAxisBar)
  .append("text")
  .attr("transform", "rotate(-90)")
  .attr("y", 0)
  .attr("dy", ".71em")
  .style("text-anchor", "end")
  .text("axis title");
#thermometer #outline {
  fill: none;
  stroke: black;
  stroke-width: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="visualization"></div>
<button onclick="generateNumber()">
 Generate number
</button>


Edit to fill up the thermometer completely:

function getThermometerOutline(shape) {
  const height = shape.height;
  const width = shape.width;
  const radius = shape.radius;

  // Draw it in two parts, first the left, then the right
  // Start at the very top, the total width is 2x the radius
  let result = `M${radius},0 `;

  // Then draw the first rounding
  const offset = radius - (width / 2);
  result += `A ${width / 2} ${width / 2} 0 0 0 ${offset},${offset} `;

  // Go down the shaft to the base of the bulb
  const shaftHeight = height - offset - (2 * radius); // more or less
  result += `V${shaftHeight + offset} `;

  // Draw the bulb in two half circles, first we end at the bottom
  const endPoint = {
    x: radius,
    y: height
  };
  result += `a ${radius} ${radius} 0 1 0 `
  result += `${width},0 `;

  // Now move back up to the very top
  result += `V${offset} `;
  result += `A ${width / 2} ${width / 2} 0 0 0 ${radius},0 `;

  return result + 'Z';
}

function generateNumber() {
  let number = parseInt(Math.random() * 100)
  //let number= 100

  d3.select("#indicador").attr("y", yScale(number) - 3); // to center the indicador image
  d3.select("#indicadorText").attr("y", yScale(number) + 5).text(number);
}

const vis = d3.select("#visualization")
  .append('svg')
  .attr("width", 800)
  .attr("height", 614)
  .style("border", "1px solid red");

const gradient = vis.append('defs')
  .append('linearGradient')
  .attr('id', 'temperature')
  .attr('x1', '0%')
  .attr('x2', '0%')
  .attr('y1', '100%')
  .attr('y2', '0%');

// Start at red
gradient.append('stop')
  .attr('offset', '0%')
  .style('stop-color', 'red');

// The entire bulb should be red
// a little less than radius / (height + radius)
gradient.append('stop')
  .attr('offset', '8%')
  .style('stop-color', 'red');


gradient.append('stop')
  .attr('offset', '50%')
  .style('stop-color', 'yellow');

gradient.append('stop')
  .attr('offset', '100%')
  .style('stop-color', 'green');

let g = vis.append("g")
  .style("transform", "translate(10px, 10px)");

g.append("image")
  .attr("id", "indicador")
  .attr("href", "https://www.shareicon.net/data/256x256/2015/08/17/86784_left_512x512.png")
  .attr("width", 15)
  .attr("height", 15)
  .attr("y", 403)
  .style("transform", "translate(100px)")

g.append("text")
  .attr("id", "indicadorText")
  .text("0")
  .attr("x", 120)
  .attr("y", 413)
  .attr("alignment-baseline", "middle")

const thermometer = vis.append("g")
  .attr("id", "thermometer")
  .attr('transform', 'translate(10, 10)');

const outline = thermometer.append("path")
  .attr("id", "outline")
  .attr("d", getThermometerOutline({
    width: 62,
    height: 532,
    radius: 56
  }))
  .attr("transform", "translate(-5, -16)")
  .style("fill", "url(#temperature)");

let maxRange = 410; // instead of 600
var y = d3.scaleLinear()
  .domain([0, 500])
  .range([maxRange, 0])


var yAxisBar = d3.axisLeft()
  .scale(y)
  .ticks(15);
//.tickFormat(function(d,i){ return "" });

g.append("g")
  .attr("class", "y axis")
  .attr("transform", "translate(150 , 0)")
  .call(yAxisBar)
  .append("text")
  .attr("transform", "rotate(-90)")
  .attr("y", 0)
  .attr("dy", ".71em")
  .style("text-anchor", "end")
  .text("axis title");
#thermometer #outline {
  fill: none;
  stroke: black;
  stroke-width: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="visualization"></div>
<button onclick="generateNumber()">
 Generate number
</button>

Upvotes: 3

Related Questions