starburst
--- /dev/null
+++ b/json.php
@@ -1,1 +1,80 @@
+<?php
+/**
+ * @author Razvan Stanga <git@razvi.ro>
+ */
+
+set_time_limit (0);
+
+$root = __DIR__;
+chdir ($root);
+define ("appRoot", $root."/");
+
+require (appRoot."app/autoload.php");
+
+function randomColor ($minVal = 0, $maxVal = 255)
+{
+
+ // Make sure the parameters will result in valid colours
+ $minVal = $minVal < 0 || $minVal > 255 ? 0 : $minVal;
+ $maxVal = $maxVal < 0 || $maxVal > 255 ? 255 : $maxVal;
+
+ // Generate 3 values
+ $r = mt_rand($minVal, $maxVal);
+ $g = mt_rand($minVal, $maxVal);
+ $b = mt_rand($minVal, $maxVal);
+
+ // Return a hex colour ID string
+ return sprintf('#%02X%02X%02X', $r, $g, $b);
+
+}
+
+use carstats\Database\Mysql as Db;
+
+$json = array ();
+$q = "SELECT b.`id`, b.`bodyName`, AVG(p.`price`) as `avgprice` FROM `prices` as p LEFT JOIN `body` as b ON p.`body_id`=b.`id` WHERE b.`active`=1 GROUP BY p.`body_id` ORDER BY b.`bodyName` ASC";
+$b = Db::query ($q);
+while ( $row = Db::fetchArray($b) ) {
+ $brands = array ();
+ $q1 = "SELECT b.`id`, b.`brandName`, AVG(p.`price`) as `avgprice`, p.`brand_id` FROM `prices` as p LEFT JOIN `brand` as b ON p.`brand_id`=b.`id` WHERE b.`active`=1 AND p.`body_id`='".$row['id']."' GROUP BY p.`brand_id` ORDER BY b.`brandName`";
+ $br = Db::query ($q1);
+ while ( $row1 = Db::fetchArray($br) ) {
+ $models = array ();
+ $q2 = "SELECT m.`id`, m.`modelName`, AVG(p.`price`) as `avgprice`, p.`brand_id` FROM `prices` as p LEFT JOIN `model` as m ON p.`model_id`=m.`id` WHERE p.`brand_id`='".$row1['brand_id']."' AND m.`active`=1 AND p.`body_id`='".$row['id']."' GROUP BY p.`model_id` ORDER BY m.`modelName`";
+ //$q2 = "SELECT * FROM `model` WHERE `brand_id`='".$row1['brand_id']."' AND `body_id`='".$row['id']."' ORDER BY `modelName` ASC";
+ $bm = Db::query ($q2);
+ while ( $row2 = Db::fetchArray($bm) ) {
+
+ $years = array ();
+ $q3 = "SELECT `year`, AVG(price) as `avgprice` FROM `prices` WHERE `brand_id`='".$row1['brand_id']."' AND `body_id`='".$row['id']."' AND `model_id`='".$row2['id']."' GROUP BY `year`";
+ $by = Db::query ($q3);
+ while ( $row4 = Db::fetchArray($by) ) {
+ $years[] = array (
+ "name" => $row4['year']."_".intval ($row4['avgprice'])." EUR",
+ "colour" => randomColor (100, 255),
+ "children" => array ()
+ );
+ }
+
+ $models[] = array (
+ "name" => $row2['modelName']."_".intval ($row2['avgprice'])." EUR",
+ "colour" => randomColor (180, 255),
+ "children" => $years
+ );
+ }
+ $brands[] = array (
+ "name" => $row1['brandName']."_".intval ($row1['avgprice'])." EUR",
+ "colour" => randomColor (140, 180),
+ "children" => $models
+ );
+ }
+ $json[] = array (
+ "name" => $row['bodyName']."_".intval ($row['avgprice'])." EUR",
+ "colour" => randomColor (100, 140),
+ "children" => $brands
+ );
+}
+
+echo json_encode ($json);
+
+?>
--- /dev/null
+++ b/wwwroot/demo.json
@@ -1,1 +1,378 @@
-
+[
+ {
+ "name": "Aromas",
+ "children": [
+ {
+ "name": "Enzymatic",
+ "children": [
+ {
+ "name": "Flowery",
+ "children": [
+ {
+ "name": "Floral",
+ "children": [
+ {
+ "name": "Coffee Blossom", "colour": "#f9f0ab"
+ },
+ {
+ "name": "Tea Rose", "colour": "#e8e596"
+ }
+ ]
+ },
+ {
+ "name": "Fragrant",
+ "children": [
+ {
+ "name": "Cardamon Caraway", "colour": "#f0e2a3"
+ },
+ {
+ "name": "Coriander Seeds", "colour": "#ede487"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Fruity",
+ "children": [
+ {
+ "name": "Citrus",
+ "children": [
+ {
+ "name": "Lemon", "colour": "#efd580"
+ },
+ {
+ "name": "Apple", "colour": "#f1cb82"
+ }
+ ]
+ },
+ {
+ "name": "Berry-like",
+ "children": [
+ {
+ "name": "Apricot", "colour": "#f1c298"
+ },
+ {
+ "name": "Blackberry", "colour": "#e8b598"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Herby",
+ "children": [
+ {
+ "name": "Alliaceous",
+ "children": [
+ {
+ "name": "Onion", "colour": "#d5dda1"
+ },
+ {
+ "name": "Garlic", "colour": "#c9d2b5"
+ }
+ ]
+ },
+ {
+ "name": "Leguminous",
+ "children": [
+ {
+ "name": "Cucumber", "colour": "#aec1ad"
+ },
+ {
+ "name": "Garden Peas", "colour": "#a7b8a8"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Sugar Browning",
+ "children": [
+ {
+ "name": "Nutty",
+ "children": [
+ {
+ "name": "Nut-like",
+ "children": [
+ {
+ "name": "Roasted Peanuts", "colour": "#b49a3d"
+ },
+ {
+ "name": "Walnuts", "colour": "#b28647"
+ }
+ ]
+ },
+ {
+ "name": "Malt-like",
+ "children": [
+ {
+ "name": "Balsamic Rice", "colour": "#a97d32"
+ },
+ {
+ "name": "Toast", "colour": "#b68334"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Carmelly",
+ "children": [
+ {
+ "name": "Candy-like",
+ "children": [
+ {
+ "name": "Roasted Hazelnut", "colour": "#d6a680"
+ },
+ {
+ "name": "Roasted Almond", "colour": "#dfad70"
+ }
+ ]
+ },
+ {
+ "name": "Syrup-like",
+ "children": [
+ {
+ "name": "Honey", "colour": "#a2765d"
+ },
+ {
+ "name": "Maple Syrup", "colour": "#9f6652"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Chocolatey",
+ "children": [
+ {
+ "name": "Chocolate-like",
+ "children": [
+ {
+ "name": "Bakers", "colour": "#b9763f"
+ },
+ {
+ "name": "Dark Chocolate", "colour": "#bf6e5d"
+ }
+ ]
+ },
+ {
+ "name": "Vanilla-like",
+ "children": [
+ {
+ "name": "Swiss", "colour": "#af643c"
+ },
+ {
+ "name": "Butter", "colour": "#9b4c3f"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Dry Distillation",
+ "children": [
+ {
+ "name": "Resinous",
+ "children": [
+ {
+ "name": "Turpeny",
+ "children": [
+ {
+ "name": "Piney", "colour": "#72659d"
+ },
+ {
+ "name": "Blackcurrant-like", "colour": "#8a6e9e"
+ }
+ ]
+ },
+ {
+ "name": "Medicinal",
+ "children": [
+ {
+ "name": "Camphoric", "colour": "#8f5c85"
+ },
+ {
+ "name": "Cineolic", "colour": "#934b8b"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Spicy",
+ "children": [
+ {
+ "name": "Warming",
+ "children": [
+ {
+ "name": "Cedar", "colour": "#9d4e87"
+ },
+ {
+ "name": "Pepper", "colour": "#92538c"
+ }
+ ]
+ },
+ {
+ "name": "Pungent",
+ "children": [
+ {
+ "name": "Clove", "colour": "#8b6397"
+ },
+ {
+ "name": "Thyme", "colour": "#716084"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Carbony",
+ "children": [
+ {
+ "name": "Smokey",
+ "children": [
+ {
+ "name": "Tarry", "colour": "#2e6093"
+ },
+ {
+ "name": "Pipe Tobacco", "colour": "#3a5988"
+ }
+ ]
+ },
+ {
+ "name": "Ashy",
+ "children": [
+ {
+ "name": "Burnt", "colour": "#4a5072"
+ },
+ {
+ "name": "Charred", "colour": "#393e64"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Tastes",
+ "children": [
+ {
+ "name": "Bitter",
+ "children": [
+ {
+ "name": "Pungent",
+ "children": [
+ {
+ "name": "Creosol", "colour": "#aaa1cc"
+ },
+ {
+ "name": "Phenolic", "colour": "#e0b5c9"
+ }
+ ]
+ },
+ {
+ "name": "Harsh",
+ "children": [
+ {
+ "name": "Caustic", "colour": "#e098b0"
+ },
+ {
+ "name": "Alkaline", "colour": "#ee82a2"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Salt",
+ "children": [
+ {
+ "name": "Sharp",
+ "children": [
+ {
+ "name": "Astringent", "colour": "#ef91ac"
+ },
+ {
+ "name": "Rough", "colour": "#eda994"
+ }
+ ]
+ },
+ {
+ "name": "Bland",
+ "children": [
+ {
+ "name": "Neutral", "colour": "#eeb798"
+ },
+ {
+ "name": "Soft", "colour": "#ecc099"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Sweet",
+ "children": [
+ {
+ "name": "Mellow",
+ "children": [
+ {
+ "name": "Delicate", "colour": "#f6d5aa"
+ },
+ {
+ "name": "Mild", "colour": "#f0d48a"
+ }
+ ]
+ },
+ {
+ "name": "Acidy",
+ "children": [
+ {
+ "name": "Nippy", "colour": "#efd95f"
+ },
+ {
+ "name": "Piquant", "colour": "#eee469"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Sour",
+ "children": [
+ {
+ "name": "Winey",
+ "children": [
+ {
+ "name": "Tangy", "colour": "#dbdc7f"
+ },
+ {
+ "name": "Tart", "colour": "#dfd961"
+ }
+ ]
+ },
+ {
+ "name": "Soury",
+ "children": [
+ {
+ "name": "Hard", "colour": "#ebe378"
+ },
+ {
+ "name": "Acrid", "colour": "#f5e351"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+]
--- /dev/null
+++ b/wwwroot/index.php
@@ -1,1 +1,180 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Demo</title>
+
+<style>
+@import url(style.css);
+path {
+ stroke: #000;
+ stroke-width: 1.5;
+ cursor: pointer;
+}
+
+text {
+ font: 11px sans-serif;
+ cursor: pointer;
+}
+
+body {
+ width: 1280px;
+ margin: 0 auto;
+}
+
+h1 {
+ text-align: center;
+ margin: .5em 0;
+}
+
+p#intro {
+ text-align: center;
+ margin: 1em 0;
+}
+</style>
+<h1>Demo</h1>
+<div id="vis"><img src="full.png"></div>
+
+<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.4/d3.min.js"></script>
+<script>
+var width = 1280,
+ height = width,
+ radius = width / 2,
+ x = d3.scale.linear().range([0, 2 * Math.PI]),
+ y = d3.scale.pow().exponent(1.3).domain([0, 1]).range([0, radius]),
+ padding = 5,
+ duration = 1000;
+
+var div = d3.select("#vis");
+
+div.select("img").remove();
+
+var vis = div.append("svg")
+ .attr("width", width + padding * 2)
+ .attr("height", height + padding * 2)
+ .append("g")
+ .attr("transform", "translate(" + [radius + padding, radius + padding] + ")");
+
+div.append("p")
+ .attr("id", "intro")
+ .text("Click to zoom!");
+
+var partition = d3.layout.partition()
+ .sort(null)
+ .value(function(d) { return 5.8 - d.depth; });
+
+var arc = d3.svg.arc()
+ .startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
+ .endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
+ .innerRadius(function(d) { return Math.max(0, d.y ? y(d.y) : d.y); })
+ .outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)); });
+
+d3.json("../json.php", function(error, json) {
+ var nodes = partition.nodes({children: json});
+
+ var path = vis.selectAll("path").data(nodes);
+ path.enter().append("path")
+ .attr("id", function(d, i) { return "path-" + i; })
+ .attr("d", arc)
+ .attr("fill-rule", "evenodd")
+ .style("fill", colour)
+ .on("click", click);
+
+ var text = vis.selectAll("text").data(nodes);
+ var textEnter = text.enter().append("text")
+ .style("fill-opacity", 1)
+ .style("fill", function(d) {
+ return brightness(d3.rgb(colour(d))) < 125 ? "#eee" : "#000";
+ })
+ .attr("text-anchor", function(d) {
+ return x(d.x + d.dx / 2) > Math.PI ? "end" : "start";
+ })
+ .attr("dy", ".2em")
+ .attr("transform", function(d) {
+ var multiline = (d.name || "").split(" ").length > 3,
+ angle = x(d.x + d.dx / 2) * 180 / Math.PI - 90,
+ rotate = angle + (multiline ? -.5 : 0);
+ return "rotate(" + rotate + ")translate(" + (y(d.y) + padding) + ")rotate(" + (angle > 90 ? -180 : 0) + ")";
+ })
+ .on("click", click);
+ textEnter.append("tspan")
+ .attr("x", 0)
+ .text(function(d) { return d.depth ? d.name.split("_")[0] : ""; });
+ textEnter.append("tspan")
+ .attr("x", 0)
+ .attr("dy", "1em")
+ .text(function(d) { return d.depth ? d.name.split("_")[1] || "" : ""; });
+
+ function click(d) {
+ path.transition()
+ .duration(duration)
+ .attrTween("d", arcTween(d));
+
+ // Somewhat of a hack as we rely on arcTween updating the scales.
+ text.style("visibility", function(e) {
+ return isParentOf(d, e) ? null : d3.select(this).style("visibility");
+ })
+ .transition()
+ .duration(duration)
+ .attrTween("text-anchor", function(d) {
+ return function() {
+ return x(d.x + d.dx / 2) > Math.PI ? "end" : "start";
+ };
+ })
+ .attrTween("transform", function(d) {
+ var multiline = (d.name || "").split(" ").length > 3;
+ return function() {
+ var angle = x(d.x + d.dx / 2) * 180 / Math.PI - 90,
+ rotate = angle + (multiline ? -.5 : 0);
+ return "rotate(" + rotate + ")translate(" + (y(d.y) + padding) + ")rotate(" + (angle > 90 ? -180 : 0) + ")";
+ };
+ })
+ .style("fill-opacity", function(e) { return isParentOf(d, e) ? 1 : 1e-6; })
+ .each("end", function(e) {
+ d3.select(this).style("visibility", isParentOf(d, e) ? null : "hidden");
+ });
+ }
+});
+
+function isParentOf(p, c) {
+ if (p === c) return true;
+ if (p.children) {
+ return p.children.some(function(d) {
+ return isParentOf(d, c);
+ });
+ }
+ return false;
+}
+
+function colour(d) {
+ if (d.children) {
+ // There is a maximum of two children!
+ var colours = d.children.map(colour),
+ a = d3.hsl(colours[0]),
+ b = d3.hsl(colours[1]);
+ // L*a*b* might be better here...
+ return d3.hsl((a.h + b.h) / 2, a.s * 1.2, a.l / 1.2);
+ }
+ return d.colour || "#fff";
+}
+
+// Interpolate the scales!
+function arcTween(d) {
+ var my = maxY(d),
+ xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
+ yd = d3.interpolate(y.domain(), [d.y, my]),
+ yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
+ return function(d) {
+ return function(t) { x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); return arc(d); };
+ };
+}
+
+function maxY(d) {
+ return d.children ? Math.max.apply(Math, d.children.map(maxY)) : d.y + d.dy;
+}
+
+// http://www.w3.org/WAI/ER/WD-AERT/#color-contrast
+function brightness(rgb) {
+ return rgb.r * .299 + rgb.g * .587 + rgb.b * .114;
+}
+</script>
+
--- /dev/null
+++ b/wwwroot/style.css
@@ -1,1 +1,73 @@
+.date {
+ font-size: small;
+ font-style: italic;
+ margin: 0;
+ width: 100%;
+ border-top: solid #eee 1px;
+}
+
+.breadcrumbs {
+ font-size: small;
+ font-family: sans-serif;
+ width: 100%;
+ margin: 0;
+ padding-top: 3px;
+ font-style: italic;
+ border-bottom: solid #eee 1px;
+ color: #999;
+}
+
+a {
+ color: steelblue;
+}
+
+a:not(:hover) {
+ text-decoration: none;
+}
+
+body {
+ margin: 0 auto;
+ font-family: 'PT Serif', Georgia, Times, 'Times New Roman', serif;
+ line-height: 1.5em;
+ color: #333;
+ background: #fefefe;
+ width: 960px;
+ position: relative;
+}
+
+p {
+ width: 720px;
+}
+
+h1, h2, h3 {
+ font-weight: 300;
+ text-rendering: optimizeLegibility;
+}
+
+h1 {
+ font-size: 2.5em;
+}
+
+aside {
+ font-size: small;
+ position: absolute;
+ right: 0;
+ width: 180px;
+}
+
+aside p {
+ width: auto;
+}
+
+p.cite, p.caption {
+ color: #666;
+}
+
+p.caption {
+ text-align: center;
+ font-size: small;
+ font-style: italic;
+ width: auto;
+}
+