//- UNCLASSIFIED

extends base
append base_help
	:markdown
		Display [d3 sun-burst](https://bl.ocks.org/denjn5/e1cdbbe586ac31747b4a304f8f86e
		chart using parameters:

			src = source returning NODES = [ { name: "...", value: N, doc: "...", children: NODES }, ... ]
			name = "name" || "name%..." || "" of source record(s)
			pivots = "key,key,..." || "" source record grouping keys
			w = drawing width
			h = drawing height
			debug = level of debugging alerts
			node,children,value,doc = src keys

append base_parms
	- tech = "d3v5"

append base_head
	style.
		body {
			font-family: 'Open Sans', sans-serif;
			font-size: 12px;
			font-weight: 400;
			background-color: #fff;
			width: 960px;
			height: 700px;
			margin-top: 10px;
		}
		svg {
			font: 10px sans-serif;
		}

		#main {
			float: left;
			width: 750px;
		}

		#sidebar {
			float: right;
			width: 100px;
		}

		#sequence {
			width: 600px;
			height: 70px;
		}

		#legend {
			padding: 10px 0 0 3px;
		}

		#sequence text, #legend text {
			font-weight: 600;
			fill: #fff;
		}

		#chart {
			position: relative;
		}

		#chart path {
		stroke: #fff;
		}

		#explanation {
			position: absolute;
			top: 260px;
			left: 305px;
			width: 140px;
			text-align: center;
			color: #666;
			z-index: -1;
		}

		#percentage {
			font-size: 2.5em;
		}

	script.
		var opts = {
			ds: "!{query.src}" || "/stores/flare.json",
			
			/*"#{query.pivots}"
				? "!{query.src}.tree?name=#{query[NAME]}&_sort=#{query.pivots}"
				: "#{query[NAME]}"
							? "!{query.src}.schema?name=#{query[NAME]}"
							: "!{query.src}" || "/stores/flare.json", */

			url: "#{url}",
			family: "cluster",
			
			dims: {
				margin: {top: 20, right: 90, bottom: 30, left: 90},
				width: parseInt("#{query.w}") || 1200,
				height: parseInt("#{query.h}") || 500
			},
			debug: parseInt("#{query.debug}"),
			
			keys: {
				NODE: "#{query.node}" || "name",
				CHILDREN: "#{query.children}" || "children",
				VALUE: "#{query.value}" || "value",
				DOC: "#{query.doc}" || "doc"
			}
		};

		const {NODE, CHILDREN, VALUE, DOC} = opts.keys;

append base_body
	script.
		function partition(data) {
			/*
			The first thing we need to do with our data is to find the root node--that's the very first node in our hierarchical data.

				<1	The sunburst layout (or any hierarchical layout in d3) needs a root node. Happily, our data is already in a hierarchical pattern and has a root node ("name" : "TOPICS"). So we can pass our data directly to d3.partition with no preliminary reformatting. We use d3.hierarchy(nodeData) to tell d3, "Hey d3, our data is stored in the nodeData variable. It's already shaped in a hierarchy."

				<2	sum() iterates through each node in our data and adds a "value" attribute to each one. The value stored in the "value" attribute is the combined sum of d[VALUE] (since that's what we've asked for) for this node and all of its children nodes. It will be used later to determine arc / slice sizes.
					Example: If the current node has no size attribute of its own, but it has 2 children, each size = 4, then .sum() will create a "value = 8" attribute for this node.
					See the d3 documentation for node.sum(value)
					
				<3 	The partition command is a special tool that will help organize our data into the sunburst pattern, and ensure things are properly sized (e.g., that we use all 360 degrees of the circle, and that each slice is sized relative to the other slices.) So far, this is about structure, since we haven't linked it to our actual data yet.

				<4	size sets this partition's overall size "width" and "height". But we've shifted from an [x,y] coordinate system (where a box could be 25 by 25] to a system where we size each part of our sunburst in radians (how much of the 360 the shape will consume) and depth (distance from center to full radius):

						2 * Math.PI tells d3 the number of radians our sunburst will consume. Remember from middle-school geometry that a circle has a circumference of 2πr (2 * pi * r). This coordinate tells d3 how big our sunburst is in "radiuses". The answer is that it's 2π radiuses (or radians). So it's a full circle.
							Want a sunburst that's a ½ circle? Delete the 2 *.
							Want to better understand radians and how they map to degrees? Try mathisfun: radians or Intuitive Guide to Angles, Degrees and Radians. 

						radius takes our variable, set above, and tells d3 that this is the distance from the center to the outside of the sunburst.
			*/			
			const root = d3.hierarchy( data, d => d[CHILDREN] )  //<-- 1
						.sum(d => d[VALUE])  //<-- 2
						.sort((a, b) => b[VALUE] - a[VALUE]);
			return d3.partition()  //<-- 3
						.size([2 * Math.PI, root.height + 1])	//<-- 4
						(root);
		}
		
		Fetch( opts, (data,svg) => {
			var
				width = svg.attr("width"),
				height = svg.attr("height");

			var
				/*
					var width = 500 creates a variable set to 500; it does not actually set the width of anything, yet. Below we'll apply this variable to the <svg> element's width attribute. We could set width directly (<svg width=500>). But we'll use this values a few times. If we coded it directly, we'd then need to change each occurrences every time. Mistakes will happen.

					var radius = Math.min(width, height) / 2 determines which is smaller (the min), the width or height. Then it divides that value by 2 (since the radius is 1/2 of the circle's diameter). Then we store that value as our radius. This optimizes the size of our viz within the <svg> element (since we don't want to leak past the edges, but we also don't want a bunch of wasted white space). Since width and height are both 500, the radius variable will equal 250.

					d3.scaleOrdinal: d3 scales help us map our data to something in our visual. Outside of d3, ordinal scales indicate the direction of the underlying data and provide nominal information (e.g., low, medium, high). In the same way, scaleOrdinal in d3 allows us to relate a part of our data to something that has a series of named values (like an array of colors).
						schemeCategory20b is a d3 command that returns an array of colors. d3 has several similar options that are specifically designed to work with d3.scaleOrdinal(). The result of this line is that we'll have a variable ("color") that will return a rainbow of options for our sunburst.
				*/	
				radius = width / 20; //Math.round(Math.sqrt( width**2  ));
			
			var 
				/*
				<1 combines our partition variable (which creates the data structure) with our root node (the actual data). This line sets us up for the arc statement. Advanced Note: Inspecting "d" in our functions (e.g., function (d) { return d.x0 }) before and after this partition line yields an interesting finding:

				Before this line, "d" for a particular node returns an object that looks just like our underlying json: {name: "Sub A1", size: 4}.
				After this partition line, "d" for a particular node returns a d3-shaped object: {data: Object, height: 0, depth: 2, parent: qo, value: 4…}. And our json attributes are tucked into the data attribute.
				*/
				root = partition( data[0] || data ),	//<-- 1
				format = d3.format(",d"),
				color = d3.scaleOrdinal(d3.quantize(d3.interpolateRainbow, root.children.length + 1));

			var
				/*
				d3.arc() calculates the size of each arc based on our JSON data. Each of the 4 variables below are staples in d3 sunbursts. They define the 4 outside lines for each arc.

				d.x0 is the radian location for the start of the arc, as we traverse around the circle.
				d.x1 is the radian location for the end of the arc. If x0 and x1 are the same, our arc will be invisible. If x0 = 0 and x1 = 2, our arc will encompass a bit less than 1/3 of our circle.
				d.y0 is the radian location for the inside arc.
				d.y1 is the radian location for the outside arc. If y0 and y1 are the same, our arc will be invisible. 
				*/
				arc = d3.arc()
					.startAngle(d => d.x0)
					.endAngle(d => d.x1)
					.padAngle(d => Math.min((d.x1 - d.x0) / 2, 0.005))
					.padRadius(radius * 1.5)
					.innerRadius(d => d.y0 * radius)
					.outerRadius(d => Math.max(d.y0 * radius, d.y1 * radius - 1));
	
			root.each(d => d.current = d);

			var
				/*
					<1 d3.select('svg') selects our <svg></svg> element so that we can work with it. The d3.select() command finds the first element (and only the first, if there are multiple) that matches the specified string. If the select command does not find a match, it returns an empty selection.

					<2	.attr('width', width) sets the width attribute of our <svg> element.

					<3	.append('g') adds a <g> element to our SVG. <g> does not do much directly, it's is a special SVG element that acts as a container; it groups other SVG elements. And transformations applied to the <g> element are performed on all of its child elements. And its attributes are inherited by its children. That'll be helpful later.

					<4	.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')') sets the value for the transform attribute (as we did with width above). SVG's transform attribute allows us to scale, translate (move), and rotate our <g> element (and it's children). There's a longer conversation to be had about the SVG coordinate system (See Sara Soueidan's article helps clarify the mechanics.). For now, we'll simply say that we'll use this transform attribute to move the "center" [0,0] of 
					our <g> element from the upper-left to the actual center of our <svg> element:

						'translate(' + width / 2 + ',' + height / 2 + ')' will resolves to translate(250, 250). This command moves our coordinate system (for <g>) 250 units right (x-axis) and 250 units down (y-axis). 
				*/
				g = svg.append("g")	//<-- 3
						.attr("transform", `translate(${width / 2},${height / 2})`),	//<-- 4

				/*
				d3's "update pattern" operates as following:

					<1 	g.selectAll('path') starts with the g variable that we created way above; it references the <g> element that we originally appended to our <svg> element. selectAll gets a reference to all existing <path> elements within our <g> element. "That's odd," you say, "since we know that there are no <path> elements in <g>." You are right. They don't yet exist. For now, we'll just say that d3 uses this step to establish where the new <path> elements will fit on the page (in the svg object model).

					<2  .data(root.descendants()) tells d3 what about the <path> elements that we want to exist by passing it our data. We're passing in our root variable with its descendants.

					<3	.enter() tells d3 to "connect" the originally selected <path> element with our data so that we can…

					<4	.append('path') actually creates one new, but empty, <path> element for each node under our <g> element. See Chris Givens' Update Pattern tutorial for another look at steps 1-4 above.

					<5	.attr("display", function (d) { return d.depth ? null : "none"; }) sets the display attribute of the <path> element for our root node to "none". (display="none" tells SVG that this element will not be rendered.)
						d.depth will equal 0 for the root node, 1 for its children, 2 for "grandchildren", etc.
						Want more layers in your sunburst? This visualization will add as many layers as you have in your data. We've limited to just 2 layers for simplicity in our explanation. (Advanced Idea: Imagine you've added many additional layers in the json, and maybe you don't always want to show all of those layers. You could use a d.depth test to limit the number of rings that actually appear on your viz.)

					<6 .attr("d", arc) fills in all the "d" attributes of each <path> element with the values from our arc variable. Two important notes here:
						The d attribute contains the actual directions for each line of this svg <path> element, see the example below.
						Don't confuse the the <path d=""> attribute with the d variable that represents the data within or d3 script. 

				If you stopped here, you'd see a sunburst with each slice drawn, but all black with barely visible gray lines separating the slices. Let's add some color:

					<7	 .style('stroke', '#fff') add style="stroke: rgb(255, 255, 255);" to our <path> element. Now the lines between our slices are white.

					<8	.style("fill", function (d) { return color((d.children ? d : d.parent).data.name); }) combines the color variable we defined at the beginning (which returns an array of colors that we can step through) with our data.
						(d.children ? d : d.parent) is a javascript inline if in the form of (condition ? expr1 : expr2) that says, if the current node has children, return the current node, otherwise, return its parent.
						That node's name will be passed to our color variable and then returned to the style attribute within each <path> element.
				*/
				path = g.append("g")
						.selectAll("path")	//<-- 1
						//.data(root.descendants().slice(1))
						.data( root.descendants() )	//<-- 2
						.enter()  // <-- 3
							.append('path')  // <-- 4
							.attr("display", function (d) { return d.depth ? null : "none"; })  // <-- 5
							.attr("d", arc)  // <-- 6
							.style('stroke', '#fff')  // <-- 7
							.style("fill", function (d) { return color((d.children ? d : d.parent).data[NODE]); });  // <-- 8			
				/*.join("path")
				.attr("fill", d => { while (d.depth > 1) d = d.parent; return color(d.data[NODE]); })
				.attr("fill-opacity", d => arcVisible(d.current) ? (d.children ? 0.6 : 0.4) : 0)
				.attr("d", d => arc(d.current));
				*/
			
			path.filter(d => d.children)
				.style("cursor", "pointer")
				.on("click", clicked);

			path.append("title")
				.text(d => `${d.ancestors().map(d => d.data[NODE]).reverse().join("/")}\n${format(d.value)}`);

			var 
				label = g.append("g")
					.attr("pointer-events", "none")
					.attr("text-anchor", "middle")
					.style("user-select", "none")
					.selectAll("text")
					.data(root.descendants().slice(1))
					.join("text")
					.attr("dy", "0.35em")
					.attr("fill-opacity", d => +labelVisible(d.current))
					.attr("transform", d => labelTransform(d.current))
					.text(d => d.data[NODE] ),
				
				parent = g.append("circle")
					.datum(root)
					.attr("r", radius)
					.attr("fill", "none")
					.attr("pointer-events", "all")
					.on("click", clicked);

			function clicked(p) {
				parent.datum(p.parent || root);

				root.each(d => d.target = {
					x0: Math.max(0, Math.min(1, (d.x0 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI,
					x1: Math.max(0, Math.min(1, (d.x1 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI,
					y0: Math.max(0, d.y0 - p.depth),
					y1: Math.max(0, d.y1 - p.depth)
				});

				const t = g.transition().duration(750);

				// Transition the data on all arcs, even the ones that aren’t visible,
				// so that if this transition is interrupted, entering arcs will start
				// the next transition from the desired position.
				path.transition(t)
				.tween("data", d => {
					const i = d3.interpolate(d.current, d.target);
					return t => d.current = i(t);
				})
				.filter(function(d) {
					return +this.getAttribute("fill-opacity") || arcVisible(d.target);
				})
				.attr("fill-opacity", d => arcVisible(d.target) ? (d.children ? 0.6 : 0.4) : 0)
				.attrTween("d", d => () => arc(d.current));

				label.filter(function(d) {
					return +this.getAttribute("fill-opacity") || labelVisible(d.target);
				}).transition(t)
					.attr("fill-opacity", d => +labelVisible(d.target))
					.attrTween("transform", d => () => labelTransform(d.current));
			}

			function arcVisible(d) {
				return d.y1 <= 3 && d.y0 >= 1 && d.x1 > d.x0;
			}

			function labelVisible(d) {
				return d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.03;
			}

			function labelTransform(d) {
				const x = (d.x0 + d.x1) / 2 * 180 / Math.PI;
				const y = (d.y0 + d.y1) / 2 * radius;
				return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`;
			}
		});

// UNCLASSIFIED
