function cocktail(json) { "use strict"; var mapping = { // These are the ingredients we're going to display in the chord diagram apple_brandy: "Fruit Brandy", apricot_brandy: "Fruit Brandy", blackberry_brandy: "Fruit Brandy", bourbon: "Whisky", brandy: "Brandy", cherry_brandy: "Fruit Brandy", coffee_liqueur: "Coffee Liqueur", creme_de_noyaux: "Creme de Noyaux", curacao: "Curacao", dark_creme_de_cacao: "Creme de Cacao", dark_rum: "Rum", dry_vermouth: "Vermouth", gin: "Gin", gold_rum: "Rum", green_creme_de_menthe: "Creme de Menthe", light_rum: "Rum", orange_cognac: "Fruit Brandy", peach_brandy: "Fruit Brandy", pernod: "Pernod", plum_brandy: "Fruit Brandy", sloe_gin: "Gin", sweet_vermouth: "Vermouth", tequila: "Tequila", vodka: "Vodka", whisky: "Whisky", white_creme_de_cacao: "Creme de Cacao", white_creme_de_menthe: "Creme de Menthe" }; function ConstructKnownCocktails(cocktails) { var i, j, k, known = {}; for (i in mapping) { // Construct a dictionary of known ingredients k = mapping[i]; if (!(k in known)) { known[k] = { name: k, cocktails: [], cross: {}, endpoint: {}, width: 0 }; } } for (var cocktail in cocktails) { // Search all the cocktails and add each to their known ingredients var ingredients = {}; for (var ingredient in cocktails[cocktail].ingredients) { // Make sure we don't double-count ingredients such as "Light Rum" and "Dark Rum" i = mapping[ingredient]; if (i) { ingredients[i] = true; } } ingredients = Object.keys(ingredients); for (i = 0; i < ingredients.length; ++i) { // Cross-reference pairs of ingredients var a = ingredients[i]; known[a].cocktails.push(cocktail); for (j = i + 1; j < ingredients.length; ++j) { var b = ingredients[j]; known[a].cross[b] = (known[a].cross[b] || 0) + 1; known[b].cross[a] = (known[b].cross[a] || 0) + 1; } } } var total = 0; for (i in known) { // Work out the relative width of each ingredient in the chord diagram k = known[i]; for (j in k.cross) { k.width += k.cross[j]; } total += k.width; } var ordered = Object.keys(known); ordered.sort(function (a, b) { return known[b].width - known[a].width; }); var used = 5; var gap = (2 * Math.PI - used) / ordered.length; var angle = gap / 2; var scale = used / total; for (i = 0; i < ordered.length; ++i) { // Work out the angles for each of the chords k = known[ordered[i]]; k.lbound = angle; for (j = ordered.length - 1; j > 0; --j) { var name = ordered[(i + j) % ordered.length]; var w = k.cross[name]; if (w) { k.endpoint[name] = { name: name, lbound: angle, ubound: angle + w * scale }; angle += w * scale; } } k.ubound = angle; angle += gap; k.hue = i * 360 / ordered.length; k.style = function (amount) { return "hsl(" + this.hue + ",60%," + (amount || "50%") + ")"; }; } return known; } var known = ConstructKnownCocktails(json.cocktails); var active = null; var highlight = null; var canvas = document.getElementById('canvas'); var context = canvas.getContext('2d'); var chart = { x: canvas.clientWidth / 2, y: canvas.clientHeight / 2, r: canvas.clientWidth / 2.7 }; function RedrawCanvas() { function Transform(angle, radius) { // Transform polar coordinates to canvas pixel coordinates return { x: chart.x + Math.sin(angle) * radius * chart.r, y: chart.y - Math.cos(angle) * radius * chart.r }; } function Join(a, b, amount) { // Join two ingredients with a chord var sa = known[b.name].style(amount); var sb = known[a.name].style(amount); var w = a.ubound - a.lbound; var aa = (a.ubound + a.lbound) / 2; var ab = (b.ubound + b.lbound) / 2; // Heuristic for finding the two inner control points of a Bezier that mitigates overlaps var q = (1 - Math.sin((aa - ab) / 2)) * 0.8; var p0 = Transform(aa, 1); var p1 = Transform(aa, q); var p2 = Transform(ab, q); var p3 = Transform(ab, 1); // The closest we can get to a length-wise gradient is a linear gradient between the endpoints var gradient = context.createLinearGradient(p0.x, p0.y, p3.x, p3.y); gradient.addColorStop(0, sa); gradient.addColorStop(1, sb); context.beginPath(); context.moveTo(p0.x, p0.y); context.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); context.lineWidth = w * chart.r; context.strokeStyle = gradient; context.stroke(); } function Label(k) { // Draw a label for a known ingredient var halfpi = 0.5 * Math.PI; context.save(); var a = (k.ubound + k.lbound) / 2; var p = Transform(a, 1.05); context.translate(p.x, p.y); if (a < Math.PI) { context.rotate(a - halfpi); context.textAlign = "left"; } else { context.rotate(a + halfpi); context.textAlign = "right"; } var h = 20; context.fillStyle = k.style(); context.font = h + "px sans-serif"; context.textBaseline = "middle"; var text = k.name.split(" "); var y = (1 - text.length) * h / 2; for (var i = 0; i < text.length; ++i) { context.fillText(text[i], 0, y); y += h; } context.restore(); context.beginPath(); context.strokeStyle = k.style(); context.lineCap = "round"; context.lineWidth = 5; context.arc(chart.x, chart.y, chart.r, k.lbound - halfpi, k.ubound - halfpi); context.stroke(); } // Redraw the canvas from scratch context.save(); context.clearRect(0, 0, canvas.width, canvas.height); var i, j, deferred = []; for (i in known) { for (j in known[i].endpoint) { // Draw each chord, but only once var a = known[i].endpoint[j]; var b = known[j].endpoint[i]; if (a.lbound > b.ubound) { if ((a.name === highlight) || (b.name === highlight)) { // Draw this chord later deferred.push([a, b]); } else { // Draw this chord feint Join(a, b, "75%"); } } } } for (i in deferred) { // Draw this chord highlighted Join(deferred[i][0], deferred[i][1]); } for (i in known) { // Draw all the labels Label(known[i]); } context.restore(); } function MakeCocktail(name) { function Ingredient(kind, what, quantity) { var k = known[mapping[kind]]; if (k) { var background = k.style("90%"); var foreground = k.style(); var style = "background:" + background + ";border-color:" + foreground; return "