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
| license: mit |
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
| <!DOCTYPE html> | |
| <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> |