Create a grid of triangles, used for creating a ternary color scale
forked from widged's block: Rapid implementation of a ternary plot with d3js
forked from tomshanley's block: Ternary background
xxxxxxxxxx
<html lang="en">
<head>
<meta charset="UTF-8">
<title>D3 Ternary Plot</title>
<style>
.border {
fill: none;
stroke-width: 2;
stroke: #363636;
}
.triangle {
stroke-width: 1;
stroke: #FFF;
}
circle {
stroke: #111
}
text.tick-text {
font-family: "sans-serif";
font-size: 10px;
fill: #000;
}
</style>
</head>
<body>
<div id="plot"></div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
(function() {
console.clear()
var width = 400;
var height = triangleHeight(width)
var margin = 20;
var radius = 4
var rows = 4
var rotateHSL = 180
//////////////////////////////////////////
// Overall triangle geometry
var maxDistanceToCentre = Math.ceil(2 * (height/3))
var corners = {
"left": {},
"top": {},
"right": {}
}
corners.left.x = margin
corners.left.y = height + margin
corners.top.x = (width/2) + margin
corners.top.y = margin
corners.right.x = width + margin
corners.right.y = height + margin
var centre = {
"x": (width/2) + margin,
"y": margin + maxDistanceToCentre
}
//////////////////////////////////////////
// Create random data
var data = d3.range(100).map(function(d){
let dataPoint = {}
dataPoint.a = Math.random() * 100
dataPoint.b = (100 - dataPoint.a) * Math.random()
dataPoint.c = 100 - dataPoint.a - dataPoint.b
let p = coord(dataPoint.a, dataPoint.b, dataPoint.c)
dataPoint.x = p.x
dataPoint.y = p.y
return dataPoint
})
//////////////////////////////////////////
// Create grid data
var triangles = createTriangleGrid(rows, width, height)
//////////////////////////////////////////
// Draw the SVG and shapes
var svg = d3.select('#plot').append('svg')
.attr("width", 800)
.attr("height", 800);
var grid = svg.append("g")
.attr("id", "grid")
let t = grid.selectAll("path")
.data(triangles)
.enter()
.append("g")
t.append("path")
.attr("class", "triangle")
.attr("d", function(d){
return trianglePath(d.corners)
})
.style("fill", function(d){ return d.color })
.style("opacity", 0.1)
/*
t.append("circle")
.attr("class", "centre")
.attr("cx", d => d.centre[0])
.attr("cy", d => d.centre[1])
.attr("r", 5)
*/
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", radius)
.style("fill", function(d){
return ternaryFill(d.x, d.y)
})
//////////////////////////////////////////////
// Return a x/y coordinate for ternary data point
function coord(a, b, c){
var sum
var pos = {};
pos.a = a
pos.b = b
pos.c = c
sum = a + b + c;
if(sum !== 0) {
a /= sum;
b /= sum;
c /= sum;
pos.x = corners.left.x * a + corners.right.x * b + corners.top.x * c;
pos.y = corners.left.y * a + corners.right.y * b + corners.top.y * c;
}
return pos;
}
//////////////////////////////////////////////
// Functions to draw the data
function ternaryFill(x, y) {
let point = [x, y]
let fillColor = ""
triangles.some(function(t){
if (d3.polygonContains(t.corners, point)) {
fillColor = t.color
return true
}
})
return fillColor
}
//////////////////////////////////////////////
// Triangle functions
function triangleHeight(width){
return Math.sqrt((width * width) - (width/2 * width/2));
}
function angleTan(opposite, adjacent) {
return Math.atan(opposite/adjacent);
}
function triangleHypotenuse(sideA, sideB) {
return Math.sqrt(Math.pow(sideA, 2) + Math.pow(sideB, 2))
};
function distanceRatio(x, y){
return triangleHypotenuse(x, y) / maxDistanceToCentre
}
//////////////////////////////////////////////
// Functions to draw the grid
function trianglePath(corners) {
return "M " + corners[0][0]
+ " " + corners[0][1]
+ " L " + corners[1][0]
+ " " + corners[1][1]
+ " L " + corners[2][0]
+ " " + corners[2][1]
+ " z"
}
//////////////////////////////////////////////
// Functions to create the grid data
function createTriangleGrid(_rows, _width, _height) {
let trianglesHeight = _height/_rows
let trianglesWidth = _width/_rows
let arrGrid = []
for (var row = 0; row < _rows; row++) {
for (var col = 0; col < ((row * 2) + 1); col++) {
let t = {}
t.row = row
t.col = col
t.corners = []
let top = corners.top.y + (row * trianglesHeight)
let mid = corners.top.x - (row * (trianglesWidth/2)) + (col * (trianglesWidth/2))
let right = mid + (trianglesWidth/2)
let left = mid - (trianglesWidth/2)
let bottom = top + trianglesHeight
if ((col % 2) == 0) {
t.corners[0] = [mid, top] // top
t.corners[1] = [left, bottom] // bottom left
t.corners[2] = [right, bottom] // bottom right
t.centre = [mid, (top + (2 * (trianglesHeight/3)))]
} else {
t.corners[0] = [left, top] // top left
t.corners[1] = [right, top] // left right
t.corners[2] = [mid, bottom] // right
t.centre = [mid, (top + (trianglesHeight/3))]
}
t.color = colorStep(t)
arrGrid.push(t)
}
}
return arrGrid
}
function colorStep(d) {
let dx = d.centre[0]
let dy = d.centre[1]
let x = Math.abs(dx - centre.x)
let y = Math.abs(dy - centre.y)
if (dy < centre.y && dx > centre.x) {
d.angle = angleTan(x,y) * (180 / Math.PI)
}
if (dy <= centre.y && dx <= centre.x) {
d.angle = 360 - (angleTan(x,y) * (180 / Math.PI))
}
if (dy > centre.y && dx < centre.x) {
d.angle = 180 + (angleTan(x,y) * (180 / Math.PI))
}
if (dy >= centre.y && dx >= centre.x) {
d.angle = 180 - (angleTan(x,y) * (180 / Math.PI))
}
if (d.angle <= 60 || d.angle >= 300 ) {
x = Math.abs(dx - corners.top.x)
y = Math.abs(dy - corners.top.y)
d.distance = distanceRatio(x, y)
} else if (d.angle >= 60 && d.angle <= 180 ) {
x = Math.abs(dx - corners.right.x)
y = Math.abs(dy - corners.right.y)
d.distance = distanceRatio(x, y)
} else if (d.angle >= 180 && d.angle <= 300 ) {
x = Math.abs(dx - corners.left.x)
y = Math.abs(dy - corners.left.y)
d.distance = distanceRatio(x, y)
}
d.hAngle = Math.floor(d.angle + rotateHSL)
d.sat = 0.6 - (d.distance / 2)
d.lum = 0.2 + (d.distance * 0.8)
let hslColor = d3.hsl(d.hAngle, d.sat, d.lum)
return hslColor
}
})()
</script>
</body>
</html>
https://d3js.org/d3.v4.min.js