0

I'm trying to identify the following parameters from the example for oval triangle but when i modify the line:

drawCircle(canvas.width / 3,canvas.height / 2,2.5,'red');
//want to replace with specific values but its not working
drawCircle(32 / 3,33 / 2,2.5,'red');

I want to identify the correct parameter so the demo can change the red point into other space inside the triangle

CH4=  20
C2H4= 70
C2H2= 20

Demo: https://codepen.io/Barak/pen/WwdPxQ

enter image description here

I read the post from stackoverflow community and cannot see values

how to create Duval Triangle in canvas

JMP
  • 4,417
  • 17
  • 30
  • 41

1 Answers1

1

In the post you've mentioned, markE did a great job replicating the look of a Duval triangle. The only problem is that the codepen including the drawCircle() function is just a dummy and does nothing more than placing a dot at an arbitrary position, given by it's x and y parameters.

To make this function show the correct position of actual data on the triangle - e.g. CH4=20 | C2H4=70 | C2H2=20 - there's a lot more involved.

Let's have a more in-depth look, if we were to solve this graphically on paper. (The following is based on this paper)

The Duval triangle is basically an equilateral triangle like this, where side b=%CH4, side a=%C2H4 and side c=%C2H2.

If we consider the following example data %CH4=25 | %C2H4=35 | %C2H2=40

we would have to go from point A to point C, 25% the length of side b and draw a line parallel to side c:

then from point C to point B, 35% the length of side a and draw a line parallel to side b:

and finally from point B to point A, 40% the length of side c and draw a line parallel to side a:

So where those three lines intersect - so the paper says - we have our target position indicating the status. This can be done programatically using plain trigonometry. Well, I'm not too sure why we need all three lines though. As th percentage for CH4 is always parallel to the triangle's base and C2H4 is always parallel to side b, we have an intersection yet and the line for C2H2 is given automatically.

Basically we just need a function which calculates the intersection between the CH4 and the C2H4 line.

I've taken markE's existing code and enhanced it by a function plotResult(), which takes three parameters for the CH4, C2H2 and C2H4 ppm values:

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

// https://www.researchgate.net/publication/4345236_A_Software_Implementation_of_the_Duval_Triangle_Method

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

var v0 = {
  x: 114,
  y: 366
};
var v1 = {
  x: 306,
  y: 30
};
var v2 = {
  x: 498,
  y: 366
};
var triangle = [v0, v1, v2];

// Define all your segments here
var segments = [{
    points: [{
      x: 114,
      y: 366
    }, {
      x: 281,
      y: 76
    }, {
      x: 324,
      y: 150
    }, {
      x: 201,
      y: 366
    }],
    fill: 'rgb(172,236,222)',
    label: {
      text: 'D1',
      cx: 200,
      cy: 290,
      withLine: false,
      endX: null,
      endY: null
    },
  },
  {
    points: [{
      x: 385,
      y: 366
    }, {
      x: 201,
      y: 366
    }, {
      x: 324,
      y: 150
    }, {
      x: 356,
      y: 204
    }, {
      x: 321,
      y: 256
    }],
    fill: 'deepskyblue',
    label: {
      text: 'D2',
      cx: 290,
      cy: 290,
      withLine: false,
      endX: null,
      endY: null
    },
  },
  {
    points: [{
      x: 297,
      y: 46
    }, {
      x: 392,
      y: 214
    }, {
      x: 372,
      y: 248
    }, {
      x: 441,
      y: 366
    }, {
      x: 385,
      y: 366
    }, {
      x: 321,
      y: 256
    }, {
      x: 356,
      y: 204
    }, {
      x: 281,
      y: 76
    }],
    fill: 'lightCyan',
    label: {
      text: 'DT',
      cx: 370,
      cy: 290,
      withLine: false,
      endX: 366,
      endY: 120
    },
  },
  {
    points: [{
      x: 306,
      y: 30
    }, {
      x: 312,
      y: 40
    }, {
      x: 300,
      y: 40
    }],
    fill: 'black',
    label: {
      text: 'PD',
      cx: 356,
      cy: 40,
      withLine: true,
      endX: 321,
      endY: 40
    },
  },
  {
    points: [{
      x: 312,
      y: 40
    }, {
      x: 348,
      y: 103
    }, {
      x: 337,
      y: 115
    }, {
      x: 297,
      y: 46
    }, {
      x: 300,
      y: 40
    }],
    fill: 'navajoWhite',
    label: {
      text: 'T1',
      cx: 375,
      cy: 70,
      withLine: true,
      endX: 340,
      endY: 75
    },
  },
  {
    points: [{
      x: 348,
      y: 103
    }, {
      x: 402,
      y: 199
    }, {
      x: 392,
      y: 214
    }, {
      x: 337,
      y: 115
    }],
    fill: 'tan',
    label: {
      text: 'T2',
      cx: 400,
      cy: 125,
      withLine: true,
      endX: 366,
      endY: 120
    },
  },
  {
    points: [{
      x: 402,
      y: 199
    }, {
      x: 498,
      y: 366
    }, {
      x: 441,
      y: 366
    }, {
      x: 372,
      y: 248
    }],
    fill: 'peru',
    label: {
      text: 'T3',
      cx: 425,
      cy: 290,
      withLine: false,
      endX: null,
      endY: null
    },
  },
];

// label styles
var labelfontsize = 12;
var labelfontface = 'verdana';
var labelpadding = 3;

// pre-create a canvas-image of the arrowhead
var arrowheadLength = 10;
var arrowheadWidth = 8;
var arrowhead = document.createElement('canvas');
premakeArrowhead();

var legendTexts = ['PD = Partial Discharge', 'T1 = Thermal fault < 300 celcius', '...'];


// start drawing
/////////////////////


// draw colored segments inside triangle
for (var i = 0; i < segments.length; i++) {
  drawSegment(segments[i]);
}
// draw ticklines
ticklines(v0, v1, 9, 0, 20);
ticklines(v1, v2, 9, Math.PI * 3 / 4, 20);
ticklines(v2, v0, 9, Math.PI * 5 / 4, 20);
// molecules
moleculeLabel(v0, v1, 100, Math.PI, '% CH4');
moleculeLabel(v1, v2, 100, 0, '% C2H4');
moleculeLabel(v2, v0, 75, Math.PI / 2, '% C2H2');
// draw outer triangle
drawTriangle(triangle);
// draw legend
drawLegend(legendTexts, 10, 10, 12.86);

plotResult(25, 40, 35);
// end drawing
/////////////////////

function drawSegment(s) {
  // draw and fill the segment path
  ctx.beginPath();
  ctx.moveTo(s.points[0].x, s.points[0].y);
  for (var i = 1; i < s.points.length; i++) {
    ctx.lineTo(s.points[i].x, s.points[i].y);
  }
  ctx.closePath();
  ctx.fillStyle = s.fill;
  ctx.fill();
  ctx.lineWidth = 2;
  ctx.strokeStyle = 'black';
  ctx.stroke();
  // draw segment's box label
  if (s.label.withLine) {
    lineBoxedLabel(s, labelfontsize, labelfontface, labelpadding);
  } else {
    boxedLabel(s, labelfontsize, labelfontface, labelpadding);
  }
}


function moleculeLabel(start, end, offsetLength, angle, text) {
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle'
  ctx.font = '14px verdana';
  var dx = end.x - start.x;
  var dy = end.y - start.y;
  var x0 = parseInt(start.x + dx * 0.50);
  var y0 = parseInt(start.y + dy * 0.50);
  var x1 = parseInt(x0 + offsetLength * Math.cos(angle));
  var y1 = parseInt(y0 + offsetLength * Math.sin(angle));
  ctx.fillStyle = 'black';
  ctx.fillText(text, x1, y1);
  // arrow
  var x0 = parseInt(start.x + dx * 0.35);
  var y0 = parseInt(start.y + dy * 0.35);
  var x1 = parseInt(x0 + 50 * Math.cos(angle));
  var y1 = parseInt(y0 + 50 * Math.sin(angle));
  var x2 = parseInt(start.x + dx * 0.65);
  var y2 = parseInt(start.y + dy * 0.65);
  var x3 = parseInt(x2 + 50 * Math.cos(angle));
  var y3 = parseInt(y2 + 50 * Math.sin(angle));
  ctx.beginPath();
  ctx.moveTo(x1, y1);
  ctx.lineTo(x3, y3);
  ctx.strokeStyle = 'black';
  ctx.lineWidth = 1;
  ctx.stroke();
  var angle = Math.atan2(dy, dx);
  ctx.translate(x3, y3);
  ctx.rotate(angle);
  ctx.drawImage(arrowhead, -arrowheadLength, -arrowheadWidth / 2);
  ctx.setTransform(1, 0, 0, 1, 0, 0);
}


function boxedLabel(s, fontsize, fontface, padding) {
  var centerX = s.label.cx;
  var centerY = s.label.cy;
  var text = s.label.text;
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle'
  ctx.font = fontsize + 'px ' + fontface
  var textwidth = ctx.measureText(text).width;
  var textheight = fontsize * 1.286;
  var leftX = centerX - textwidth / 2 - padding;
  var topY = centerY - textheight / 2 - padding;
  ctx.fillStyle = 'white';
  ctx.fillRect(leftX, topY, textwidth + padding * 2, textheight + padding * 2);
  ctx.lineWidth = 1;
  ctx.strokeRect(leftX, topY, textwidth + padding * 2, textheight + padding * 2);
  ctx.fillStyle = 'black';
  ctx.fillText(text, centerX, centerY);
}


function lineBoxedLabel(s, fontsize, fontface, padding) {
  var centerX = s.label.cx;
  var centerY = s.label.cy;
  var text = s.label.text;
  var lineToX = s.label.endX;
  var lineToY = s.label.endY;
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle'
  ctx.font = fontsize + 'px ' + fontface
  var textwidth = ctx.measureText(text).width;
  var textheight = fontsize * 1.286;
  var leftX = centerX - textwidth / 2 - padding;
  var topY = centerY - textheight / 2 - padding;
  // the line
  ctx.beginPath();
  ctx.moveTo(leftX, topY + textheight / 2);
  ctx.lineTo(lineToX, topY + textheight / 2);
  ctx.strokeStyle = 'black';
  ctx.lineWidth = 1;
  ctx.stroke();
  // the boxed text
  ctx.fillStyle = 'white';
  ctx.fillRect(leftX, topY, textwidth + padding * 2, textheight + padding * 2);
  ctx.strokeRect(leftX, topY, textwidth + padding * 2, textheight + padding * 2);
  ctx.fillStyle = 'black';
  ctx.fillText(text, centerX, centerY);
}


function ticklines(start, end, count, angle, length) {
  var dx = end.x - start.x;
  var dy = end.y - start.y;
  ctx.lineWidth = 1;
  for (var i = 1; i < count; i++) {
    var x0 = parseInt(start.x + dx * i / count);
    var y0 = parseInt(start.y + dy * i / count);
    var x1 = parseInt(x0 + length * Math.cos(angle));
    var y1 = parseInt(y0 + length * Math.sin(angle));
    ctx.beginPath();
    ctx.moveTo(x0, y0);
    ctx.lineTo(x1, y1);
    ctx.stroke();
    if (i == 2 || i == 4 || i == 6 || i == 8) {
      var labelOffset = length * 3 / 4;
      var x1 = parseInt(x0 - labelOffset * Math.cos(angle));
      var y1 = parseInt(y0 - labelOffset * Math.sin(angle));
      ctx.fillStyle = 'black';
      ctx.fillText(parseInt(i * 10), x1, y1);
    }
  }
}


function premakeArrowhead() {
  var actx = arrowhead.getContext('2d');
  arrowhead.width = arrowheadLength;
  arrowhead.height = arrowheadWidth;
  actx.beginPath();
  actx.moveTo(0, 0);
  actx.lineTo(arrowheadLength, arrowheadWidth / 2);
  actx.lineTo(0, arrowheadWidth);
  actx.closePath();
  actx.fillStyle = 'black';
  actx.fill();
}


function drawTriangle(t) {
  ctx.beginPath();
  ctx.moveTo(t[0].x, t[0].y);
  ctx.lineTo(t[1].x, t[1].y);
  ctx.lineTo(t[2].x, t[2].y);
  ctx.closePath();
  ctx.strokeStyle = 'black';
  ctx.lineWidth = 2;
  ctx.stroke();
}


function drawLegend(texts, x, y, lineheight) {
  ctx.textAlign = 'left';
  ctx.textBaseline = 'top';
  ctx.fillStyle = 'black';
  ctx.font = '12px arial';
  for (var i = 0; i < texts.length; i++) {
    ctx.fillText(texts[i], x, y + i * lineheight);
  }
}

function plotResult(val1, val2, val3) {
  let deltaX, length;
  let sum = val1 + val2 + val3;
  const cos60 = Math.cos(Math.PI / 3);
  const sin60 = Math.sin(Math.PI / 3);
  let ch4 = val1 / sum;
  let c2h2 = val2 / sum;
  let c2h4 = val3 / sum;

  length = Math.sqrt(Math.pow((v1.x - v0.x), 2) + Math.pow((v1.y - v0.y), 2));
  let ch4PointA = new Point(v0.x + (length * ch4) * cos60, v0.y - (length * ch4) * sin60);
  length = Math.sqrt(Math.pow((v2.x - v1.x), 2) + Math.pow((v2.y - v1.y), 2));
  let ch4PointB = new Point(v2.x - (length * ch4) * cos60, v2.y - (length * ch4) * sin60);

  length = Math.sqrt(Math.pow((v1.x - v2.x), 2) + Math.pow((v1.y - v2.y), 2));
  let c2h4PointA = new Point(v1.x + (length * c2h4) * cos60, v1.y + (length * c2h4) * sin60);

  deltaX = (v2.x - v0.x) * c2h4;
  let c2h4PointB = new Point(v0.x + deltaX, v0.y);

  let point = getIntersection(ch4PointA, ch4PointB, c2h4PointA, c2h4PointB);

  ctx.beginPath();
  ctx.arc(point.x, point.y, 5, 0, 2 * Math.PI, false);
  ctx.fillStyle = "red";
  ctx.fill();
}

function getIntersection(pointA, pointB, pointC, pointD) {
  let denominator, a, b, numeratorA, numeratorB;
  denominator = ((pointD.y - pointC.y) * (pointB.x - pointA.x)) - ((pointD.x - pointC.x) * (pointB.y - pointA.y));
  a = pointA.y - pointC.y;
  b = pointA.x - pointC.x;
  numeratorA = ((pointD.x - pointC.x) * a) - ((pointD.y - pointC.y) * b);
  numeratorB = ((pointB.x - pointA.x) * a) - ((pointB.y - pointA.y) * b);
  a = numeratorA / denominator;
  b = numeratorB / denominator;

  return new Point(pointA.x + (a * (pointB.x - pointA.x)), pointA.y + (a * (pointB.y - pointA.y)));
}
body {
  background-color: ivory;
  padding: 10px;
}

#canvas {
  border: 1px solid red;
  margin: 0 auto;
}
<canvas id="canvas" width=650 height=500></canvas>
obscure
  • 11,916
  • 2
  • 17
  • 36
  • OMG @obscure you are awwesome!! It worked for me, could be more powerfull if shows a result where the point is located, example "result: point located on D2" – Ezio Auditore Jan 16 '22 at 18:52