var micropolar = { version: "0.2.2" }; var µ = micropolar; µ.Axis = function module() { var config = { data: [], layout: {} }, inputConfig = {}, liveConfig = {}; var svg, container, dispatch = d3.dispatch("hover"), radialScale, angularScale; var angularTooltip, radialTooltip, geometryTooltip; var exports = {}; function render(_container) { container = _container || container; var data = config.data; var axisConfig = config.layout; if (typeof container === "string" || container.nodeName) { container = d3.select(container); } container.datum(data).each(function(_data, _index) { var dataOriginal = _data.slice(); liveConfig = { data: µ.util.cloneJson(dataOriginal), layout: µ.util.cloneJson(axisConfig) }; var colorIndex = 0; dataOriginal.forEach(function(d, i) { if (!d.color) { d.color = axisConfig.defaultColorRange[colorIndex]; colorIndex = (colorIndex + 1) % axisConfig.defaultColorRange.length; } if (!d.strokeColor) { d.strokeColor = d.geometry === "LinePlot" ? d.color : d3.rgb(d.color).darker().toString(); } liveConfig.data[i].color = d.color; liveConfig.data[i].strokeColor = d.strokeColor; liveConfig.data[i].strokeDash = d.strokeDash; liveConfig.data[i].strokeSize = d.strokeSize; }); var data = dataOriginal.filter(function(d, i) { var visible = d.visible; return typeof visible === "undefined" || visible === true; }); var isStacked = false; var dataWithGroupId = data.map(function(d, i) { isStacked = isStacked || typeof d.groupId !== "undefined"; return d; }); var dataYStack = []; if (isStacked) { var grouped = d3.nest().key(function(d, i) { return typeof d.groupId !== "undefined" ? d.groupId : "unstacked"; }).entries(dataWithGroupId); var stacked = grouped.map(function(d, i) { if (d.key === "unstacked") { return d.values; } else { var prevArray = d.values[0].r.map(function(d, i) { return 0; }); d.values.forEach(function(d, i, a) { d.yStack = [ prevArray ]; dataYStack.push(prevArray); prevArray = µ.util.sumArrays(d.r, prevArray); }); return d.values; } }); data = d3.merge(stacked); } data.forEach(function(d, i) { d.t = Array.isArray(d.t[0]) ? d.t : [ d.t ]; d.r = Array.isArray(d.r[0]) ? d.r : [ d.r ]; }); var radius = Math.min(axisConfig.width - axisConfig.margin.left - axisConfig.margin.right, axisConfig.height - axisConfig.margin.top - axisConfig.margin.bottom) / 2; radius = Math.max(10, radius); var chartCenter = [ axisConfig.margin.left + radius, axisConfig.margin.top + radius ]; var extent; if (isStacked) { var highestStackedValue = d3.max(µ.util.sumArrays(µ.util.arrayLast(data).r[0], µ.util.arrayLast(dataYStack))); extent = [ 0, highestStackedValue ]; } else { extent = d3.extent(µ.util.flattenArray(data.map(function(d, i) { return d.r; }))); } if (axisConfig.radialAxis.domain !== µ.DATAEXTENT) { extent[0] = 0; } radialScale = d3.scale.linear().domain(axisConfig.radialAxis.domain !== µ.DATAEXTENT && axisConfig.radialAxis.domain ? axisConfig.radialAxis.domain : extent).range([ 0, radius ]); liveConfig.layout.radialAxis.domain = radialScale.domain(); var angularDataMerged = µ.util.flattenArray(data.map(function(d, i) { return d.t; })); var isOrdinal = typeof angularDataMerged[0] === "string"; var ticks; if (isOrdinal) { if (isStacked) { angularDataMerged = µ.util.deduplicate(angularDataMerged); } ticks = angularDataMerged.slice(); angularDataMerged = d3.range(angularDataMerged.length); data = data.map(function(d, i) { var result = d; d.t = [ angularDataMerged ]; if (isStacked) { result.yStack = d.yStack; } return result; }); } var hasOnlyLineOrDotPlot = data.filter(function(d, i) { return d.geometry === "LinePlot" || d.geometry === "DotPlot"; }).length === data.length; var needsEndSpacing = axisConfig.needsEndSpacing === null ? isOrdinal || !hasOnlyLineOrDotPlot : axisConfig.needsEndSpacing; var useProvidedDomain = axisConfig.angularAxis.domain && axisConfig.angularAxis.domain !== µ.DATAEXTENT && !isOrdinal && axisConfig.angularAxis.domain[0] >= 0; var angularDomain = useProvidedDomain ? axisConfig.angularAxis.domain : d3.extent(angularDataMerged); var angularDomainStep = Math.abs(angularDataMerged[1] - angularDataMerged[0]); if (hasOnlyLineOrDotPlot && !isOrdinal) { angularDomainStep = 0; } var angularDomainWithPadding = angularDomain.slice(); if (needsEndSpacing && isOrdinal) { angularDomainWithPadding[1] += angularDomainStep; } var tickCount = axisConfig.angularAxis.ticksCount || 4; if (tickCount > 8) { tickCount = tickCount / (tickCount / 8) + tickCount % 8; } if (axisConfig.angularAxis.ticksStep) { tickCount = (angularDomainWithPadding[1] - angularDomainWithPadding[0]) / tickCount; } var angularTicksStep = axisConfig.angularAxis.ticksStep || (angularDomainWithPadding[1] - angularDomainWithPadding[0]) / (tickCount * (axisConfig.minorTicks + 1)); if (ticks) { angularTicksStep = Math.max(Math.round(angularTicksStep), 1); } if (!angularDomainWithPadding[2]) { angularDomainWithPadding[2] = angularTicksStep; } var angularAxisRange = d3.range.apply(this, angularDomainWithPadding); angularAxisRange = angularAxisRange.map(function(d, i) { return parseFloat(d.toPrecision(12)); }); angularScale = d3.scale.linear().domain(angularDomainWithPadding.slice(0, 2)).range(axisConfig.direction === "clockwise" ? [ 0, 360 ] : [ 360, 0 ]); liveConfig.layout.angularAxis.domain = angularScale.domain(); liveConfig.layout.angularAxis.endPadding = needsEndSpacing ? angularDomainStep : 0; svg = d3.select(this).select("svg.chart-root"); if (typeof svg === "undefined" || svg.empty()) { var skeleton = '' + '' + '' + '' + '' + '' + '' + "" + '' + '' + "" + '' + '' + '' + "" + ""; var doc = new DOMParser().parseFromString(skeleton, "application/xml"); var newSvg = this.appendChild(this.ownerDocument.importNode(doc.documentElement, true)); svg = d3.select(newSvg); } svg.select(".guides-group").style({ "pointer-events": "none" }); svg.select(".angular.axis-group").style({ "pointer-events": "none" }); svg.select(".radial.axis-group").style({ "pointer-events": "none" }); var chartGroup = svg.select(".chart-group"); var lineStyle = { fill: "none", stroke: axisConfig.tickColor }; var fontStyle = { "font-size": axisConfig.font.size, "font-family": axisConfig.font.family, fill: axisConfig.font.color, "text-shadow": [ "-1px 0px", "1px -1px", "-1px 1px", "1px 1px" ].map(function(d, i) { return " " + d + " 0 " + axisConfig.font.outlineColor; }).join(",") }; var legendContainer, legendBBox; if (axisConfig.showLegend) { legendContainer = svg.select(".legend-group").attr({ transform: "translate(" + [ radius, axisConfig.margin.top ] + ")" }).style({ display: "block" }); var elements = data.map(function(d, i) { var datumClone = µ.util.cloneJson(d); datumClone.symbol = d.geometry === "DotPlot" ? d.dotType || "circle" : d.geometry !== "LinePlot" ? "square" : "line"; datumClone.visibleInLegend = typeof d.visibleInLegend === "undefined" || d.visibleInLegend; datumClone.color = d.geometry === "LinePlot" ? d.strokeColor : d.color; return datumClone; }); var legendConfigMixin1 = µ.util.deepExtend({}, µ.Legend.defaultConfig().legendConfig); var legendConfigMixin2 = µ.util.deepExtend(legendConfigMixin1, { container: legendContainer, elements: elements, reverseOrder: axisConfig.legend.reverseOrder }); var legendConfigMixin3 = { data: data.map(function(d, i) { return d.name || "Element" + i; }), legendConfig: legendConfigMixin2 }; µ.Legend().config(legendConfigMixin3)(); legendBBox = legendContainer.node().getBBox(); radius = Math.min(axisConfig.width - legendBBox.width - axisConfig.margin.left - axisConfig.margin.right, axisConfig.height - axisConfig.margin.top - axisConfig.margin.bottom) / 2; radius = Math.max(10, radius); chartCenter = [ axisConfig.margin.left + radius, axisConfig.margin.top + radius ]; radialScale.range([ 0, radius ]); liveConfig.layout.radialAxis.domain = radialScale.domain(); legendContainer.attr("transform", "translate(" + [ chartCenter[0] + radius, chartCenter[1] - radius ] + ")"); } else { legendContainer = svg.select(".legend-group").style({ display: "none" }); } svg.attr({ width: axisConfig.width, height: axisConfig.height }).style({ opacity: axisConfig.opacity }); chartGroup.attr("transform", "translate(" + chartCenter + ")").style({ cursor: "crosshair" }); var centeringOffset = [ (axisConfig.width - (axisConfig.margin.left + axisConfig.margin.right + radius * 2 + (legendBBox ? legendBBox.width : 0))) / 2, (axisConfig.height - (axisConfig.margin.top + axisConfig.margin.bottom + radius * 2)) / 2 ]; centeringOffset[0] = Math.max(0, centeringOffset[0]); centeringOffset[1] = Math.max(0, centeringOffset[1]); svg.select(".outer-group").attr("transform", "translate(" + centeringOffset + ")"); if (axisConfig.title) { var title = svg.select("g.title-group text").style(fontStyle).text(axisConfig.title); var titleBBox = title.node().getBBox(); title.attr({ x: chartCenter[0] - titleBBox.width / 2, y: chartCenter[1] - radius - 20 }); } var radialAxis = svg.select(".radial.axis-group"); if (axisConfig.radialAxis.gridLinesVisible) { var gridCircles = radialAxis.selectAll("circle.grid-circle").data(radialScale.ticks(5)); gridCircles.enter().append("circle").attr({ "class": "grid-circle" }).style(lineStyle); gridCircles.attr("r", radialScale); gridCircles.exit().remove(); } radialAxis.select("circle.outside-circle").attr({ r: radius }).style(lineStyle); var backgroundCircle = svg.select("circle.background-circle").attr({ r: radius }).style({ fill: axisConfig.backgroundColor, stroke: axisConfig.stroke }); function currentAngle(d, i) { return angularScale(d) % 360 + axisConfig.orientation; } if (axisConfig.radialAxis.visible) { var axis = d3.svg.axis().scale(radialScale).ticks(5).tickSize(5); radialAxis.call(axis).attr({ transform: "rotate(" + axisConfig.radialAxis.orientation + ")" }); radialAxis.selectAll(".domain").style(lineStyle); radialAxis.selectAll("g>text").text(function(d, i) { return this.textContent + axisConfig.radialAxis.ticksSuffix; }).style(fontStyle).style({ "text-anchor": "start" }).attr({ x: 0, y: 0, dx: 0, dy: 0, transform: function(d, i) { if (axisConfig.radialAxis.tickOrientation === "horizontal") { return "rotate(" + -axisConfig.radialAxis.orientation + ") translate(" + [ 0, fontStyle["font-size"] ] + ")"; } else { return "translate(" + [ 0, fontStyle["font-size"] ] + ")"; } } }); radialAxis.selectAll("g>line").style({ stroke: "black" }); } var angularAxis = svg.select(".angular.axis-group").selectAll("g.angular-tick").data(angularAxisRange); var angularAxisEnter = angularAxis.enter().append("g").classed("angular-tick", true); angularAxis.attr({ transform: function(d, i) { return "rotate(" + currentAngle(d, i) + ")"; } }).style({ display: axisConfig.angularAxis.visible ? "block" : "none" }); angularAxis.exit().remove(); angularAxisEnter.append("line").classed("grid-line", true).classed("major", function(d, i) { return i % (axisConfig.minorTicks + 1) === 0; }).classed("minor", function(d, i) { return i % (axisConfig.minorTicks + 1) !== 0; }).style(lineStyle); angularAxisEnter.selectAll(".minor").style({ stroke: axisConfig.minorTickColor }); angularAxis.select("line.grid-line").attr({ x1: axisConfig.tickLength ? radius - axisConfig.tickLength : 0, x2: radius }).style({ display: axisConfig.angularAxis.gridLinesVisible ? "block" : "none" }); angularAxisEnter.append("text").classed("axis-text", true).style(fontStyle); var ticksText = angularAxis.select("text.axis-text").attr({ x: radius + axisConfig.labelOffset, dy: ".35em", transform: function(d, i) { var angle = currentAngle(d, i); var rad = radius + axisConfig.labelOffset; var orient = axisConfig.angularAxis.tickOrientation; if (orient === "horizontal") { return "rotate(" + -angle + " " + rad + " 0)"; } else if (orient === "radial") { return angle < 270 && angle > 90 ? "rotate(180 " + rad + " 0)" : null; } else { return "rotate(" + (angle <= 180 && angle > 0 ? -90 : 90) + " " + rad + " 0)"; } } }).style({ "text-anchor": "middle", display: axisConfig.angularAxis.labelsVisible ? "block" : "none" }).text(function(d, i) { if (i % (axisConfig.minorTicks + 1) !== 0) { return ""; } if (ticks) { return ticks[d] + axisConfig.angularAxis.ticksSuffix; } else { return d + axisConfig.angularAxis.ticksSuffix; } }).style(fontStyle); if (axisConfig.angularAxis.rewriteTicks) { ticksText.text(function(d, i) { if (i % (axisConfig.minorTicks + 1) !== 0) { return ""; } return axisConfig.angularAxis.rewriteTicks(this.textContent, i); }); } var rightmostTickEndX = d3.max(chartGroup.selectAll(".angular-tick text")[0].map(function(d, i) { return d.getCTM().e + d.getBBox().width; })); legendContainer.attr({ transform: "translate(" + [ radius + rightmostTickEndX, axisConfig.margin.top ] + ")" }); var hasGeometry = svg.select("g.geometry-group").selectAll("g").size() > 0; var geometryContainer = svg.select("g.geometry-group").selectAll("g.geometry").data(data); geometryContainer.enter().append("g").attr({ "class": function(d, i) { return "geometry geometry" + i; } }); geometryContainer.exit().remove(); if (data[0] || hasGeometry) { var geometryConfigs = []; data.forEach(function(d, i) { var geometryConfig = {}; geometryConfig.radialScale = radialScale; geometryConfig.angularScale = angularScale; geometryConfig.container = geometryContainer.filter(function(dB, iB) { return iB === i; }); geometryConfig.geometry = d.geometry; geometryConfig.orientation = axisConfig.orientation; geometryConfig.direction = axisConfig.direction; geometryConfig.index = i; geometryConfigs.push({ data: d, geometryConfig: geometryConfig }); }); var geometryConfigsGrouped = d3.nest().key(function(d, i) { return typeof d.data.groupId !== "undefined" || "unstacked"; }).entries(geometryConfigs); var geometryConfigsGrouped2 = []; geometryConfigsGrouped.forEach(function(d, i) { if (d.key === "unstacked") { geometryConfigsGrouped2 = geometryConfigsGrouped2.concat(d.values.map(function(d, i) { return [ d ]; })); } else { geometryConfigsGrouped2.push(d.values); } }); geometryConfigsGrouped2.forEach(function(d, i) { var geometry; if (Array.isArray(d)) { geometry = d[0].geometryConfig.geometry; } else { geometry = d.geometryConfig.geometry; } var finalGeometryConfig = d.map(function(dB, iB) { return µ.util.deepExtend(µ[geometry].defaultConfig(), dB); }); µ[geometry]().config(finalGeometryConfig)(); }); } var guides = svg.select(".guides-group"); var tooltipContainer = svg.select(".tooltips-group"); angularTooltip = angularTooltip || µ.tooltipPanel().config({ container: tooltipContainer, fontSize: 8 })(); radialTooltip = radialTooltip || µ.tooltipPanel().config({ container: tooltipContainer, fontSize: 8 })(); geometryTooltip = geometryTooltip || µ.tooltipPanel().config({ container: tooltipContainer, fontSize: 8 })(); var angularValue, radialValue; if (!isOrdinal) { var angularGuideLine = guides.select("line").attr({ x1: 0, y1: 0, y2: 0 }).style({ stroke: "grey", "pointer-events": "none" }); // chartGroup.on("mousemove.angular-guide", function(d, i) { // var mouseAngle = µ.util.getMousePos(backgroundCircle).angle; // angularGuideLine.attr({ // x2: -radius, // transform: "rotate(" + mouseAngle + ")" // }).style({ // opacity: .5 // }); // var angleWithOriginOffset = (mouseAngle + 180 + 360 - axisConfig.orientation) % 360; // angularValue = angularScale.invert(angleWithOriginOffset); // var pos = µ.util.convertToCartesian(radius + 12, mouseAngle + 180); // angularTooltip.text(µ.util.round(angularValue)).move([ pos[0] + chartCenter[0], pos[1] + chartCenter[1] ]); // }).on("mouseout.angular-guide", function(d, i) { // guides.select("line").style({ // opacity: 0 // }); // }); } if (axisConfig.radialAxis.visible !== false) { var angularGuideCircle = guides.select("circle").style({ stroke: "grey", fill: "none" }); // chartGroup.on("mousemove.radial-guide", function(d, i) { // var r = µ.util.getMousePos(backgroundCircle).radius; // angularGuideCircle.attr({ // r: r // }).style({ // opacity: .5 // }); // radialValue = radialScale.invert(µ.util.getMousePos(backgroundCircle).radius); // var pos = µ.util.convertToCartesian(r, axisConfig.radialAxis.orientation); // radialTooltip.text(µ.util.round(radialValue)).move([ pos[0] + chartCenter[0], pos[1] + chartCenter[1] ]); // }).on("mouseout.radial-guide", function(d, i) { // angularGuideCircle.style({ // opacity: 0 // }); // geometryTooltip.hide(); // angularTooltip.hide(); // radialTooltip.hide(); // }); } // svg.selectAll(".geometry-group .mark").on("mouseover.tooltip", function(d, i) { // var el = d3.select(this); // var color = el.style("fill"); // var newColor = "black"; // var opacity = el.style("opacity") || 1; // el.attr({ // "data-opacity": opacity // }); // if (color !== "none") { // el.attr({ // "data-fill": color // }); // newColor = d3.hsl(color).darker().toString(); // el.style({ // fill: newColor, // opacity: 1 // }); // var textData = { // t: µ.util.round(d[0]), // r: µ.util.round(d[1]) // }; // if (isOrdinal) { // textData.t = ticks[d[0]]; // } // var text = "t: " + textData.t + ", r: " + textData.r; // var bbox = this.getBoundingClientRect(); // var svgBBox = svg.node().getBoundingClientRect(); // var pos = [ bbox.left + bbox.width / 2 - centeringOffset[0] - svgBBox.left, bbox.top + bbox.height / 2 - centeringOffset[1] - svgBBox.top ]; // geometryTooltip.config({ // color: newColor // }).text(text); // geometryTooltip.move(pos); // } else { // color = el.style("stroke"); // el.attr({ // "data-stroke": color // }); // newColor = d3.hsl(color).darker().toString(); // el.style({ // stroke: newColor, // opacity: 1 // }); // } // }).on("mousemove.tooltip", function(d, i) { // if (d3.event.which !== 0) { // return false; // } // if (d3.select(this).attr("data-fill")) { // geometryTooltip.show(); // } // }).on("mouseout.tooltip", function(d, i) { // geometryTooltip.hide(); // var el = d3.select(this); // var fillColor = el.attr("data-fill"); // if (fillColor) { // el.style({ // fill: fillColor, // opacity: el.attr("data-opacity") // }); // } else { // el.style({ // stroke: el.attr("data-stroke"), // opacity: el.attr("data-opacity") // }); // } // }); }); return exports; } exports.render = function(_container) { render(_container); return this; }; exports.config = function(_x) { if (!arguments.length) { return config; } var xClone = µ.util.cloneJson(_x); xClone.data.forEach(function(d, i) { if (!config.data[i]) { config.data[i] = {}; } µ.util.deepExtend(config.data[i], µ.Axis.defaultConfig().data[0]); µ.util.deepExtend(config.data[i], d); }); µ.util.deepExtend(config.layout, µ.Axis.defaultConfig().layout); µ.util.deepExtend(config.layout, xClone.layout); return this; }; exports.getLiveConfig = function() { return liveConfig; }; exports.getinputConfig = function() { return inputConfig; }; exports.radialScale = function(_x) { return radialScale; }; exports.angularScale = function(_x) { return angularScale; }; exports.svg = function() { return svg; }; d3.rebind(exports, dispatch, "on"); return exports; }; µ.Axis.defaultConfig = function(d, i) { var config = { data: [ { t: [ 1, 2, 3, 4 ], r: [ 10, 11, 12, 13 ], name: "Line1", geometry: "LinePlot", color: null, strokeDash: "solid", strokeColor: null, strokeSize: "1", visibleInLegend: true, opacity: 1 } ], layout: { defaultColorRange: d3.scale.category10().range(), title: null, height: 450, width: 500, margin: { top: 40, right: 40, bottom: 40, left: 40 }, font: { size: 12, color: "gray", outlineColor: "white", family: "Tahoma, sans-serif" }, direction: "clockwise", orientation: 0, labelOffset: 10, radialAxis: { domain: null, orientation: -45, ticksSuffix: "", visible: true, gridLinesVisible: true, tickOrientation: "horizontal", rewriteTicks: null }, angularAxis: { domain: [ 0, 360 ], ticksSuffix: "", visible: true, gridLinesVisible: true, labelsVisible: true, tickOrientation: "horizontal", rewriteTicks: null, ticksCount: null, ticksStep: null }, minorTicks: 0, tickLength: null, tickColor: "silver", minorTickColor: "#eee", backgroundColor: "none", needsEndSpacing: null, showLegend: true, legend: { reverseOrder: false }, opacity: 1 } }; return config; }; µ.util = {}; µ.DATAEXTENT = "dataExtent"; µ.AREA = "AreaChart"; µ.LINE = "LinePlot"; µ.DOT = "DotPlot"; µ.BAR = "BarChart"; µ.PIE = "PieChart"; µ.util._override = function(_objA, _objB) { for (var x in _objA) { if (x in _objB) { _objB[x] = _objA[x]; } } }; µ.util._extend = function(_objA, _objB) { for (var x in _objA) { _objB[x] = _objA[x]; } }; µ.util._rndSnd = function() { return Math.random() * 2 - 1 + (Math.random() * 2 - 1) + (Math.random() * 2 - 1); }; µ.util.dataFromEquation2 = function(_equation, _step) { var step = _step || 6; var data = d3.range(0, 360 + step, step).map(function(deg, index) { var theta = deg * Math.PI / 180; var radius = _equation(theta); return [ deg, radius ]; }); return data; }; µ.util.dataFromEquation = function(_equation, _step, _name) { var step = _step || 6; var t = [], r = []; d3.range(0, 360 + step, step).forEach(function(deg, index) { var theta = deg * Math.PI / 180; var radius = _equation(theta); t.push(deg); r.push(radius); }); var result = { t: t, r: r }; if (_name) { result.name = _name; } return result; }; µ.util.ensureArray = function(_val, _count) { if (typeof _val === "undefined") { return null; } var arr = [].concat(_val); return d3.range(_count).map(function(d, i) { return arr[i] || arr[0]; }); }; µ.util.fillArrays = function(_obj, _valueNames, _count) { _valueNames.forEach(function(d, i) { _obj[d] = µ.util.ensureArray(_obj[d], _count); }); return _obj; }; µ.util.cloneJson = function(json) { return JSON.parse(JSON.stringify(json)); }; µ.util.deepExtend = function(destination, source) { for (var property in source) { if (source[property] && source[property].constructor && source[property].constructor === Object) { destination[property] = destination[property] || {}; arguments.callee(destination[property], source[property]); } else { destination[property] = source[property]; } } return destination; }; µ.util.validateKeys = function(obj, keys) { if (typeof keys === "string") { keys = keys.split("."); } var next = keys.shift(); return obj[next] && (!keys.length || objHasKeys(obj[next], keys)); }; µ.util.sumArrays = function(a, b) { return d3.zip(a, b).map(function(d, i) { return d3.sum(d); }); }; µ.util.arrayLast = function(a) { return a[a.length - 1]; }; µ.util.arrayEqual = function(a, b) { var i = Math.max(a.length, b.length, 1); while (i-- >= 0 && a[i] === b[i]) ; return i === -2; }; µ.util.flattenArray = function(arr) { var r = []; while (!µ.util.arrayEqual(r, arr)) { r = arr; arr = [].concat.apply([], arr); } return arr; }; µ.util.deduplicate = function(arr) { return arr.filter(function(v, i, a) { return a.indexOf(v) === i; }); }; µ.util.convertToCartesian = function(radius, theta) { var thetaRadians = theta * Math.PI / 180; var x = radius * Math.cos(thetaRadians); var y = radius * Math.sin(thetaRadians); return [ x, y ]; }; µ.util.round = function(_value, _digits) { var digits = _digits || 2; var mult = Math.pow(10, digits); return Math.round(_value * mult) / mult; }; µ.util.getMousePos = function(_referenceElement) { var mousePos = d3.mouse(_referenceElement.node()); var mouseX = mousePos[0]; var mouseY = mousePos[1]; var mouse = {}; mouse.x = mouseX; mouse.y = mouseY; mouse.pos = mousePos; mouse.angle = (Math.atan2(mouseY, mouseX) + Math.PI) * 180 / Math.PI; mouse.radius = Math.sqrt(mouseX * mouseX + mouseY * mouseY); return mouse; }; µ.util.duplicatesCount = function(arr) { var uniques = {}, val; var dups = {}; for (var i = 0, len = arr.length; i < len; i++) { val = arr[i]; if (val in uniques) { uniques[val]++; dups[val] = uniques[val]; } else { uniques[val] = 1; } } return dups; }; µ.util.duplicates = function(arr) { return Object.keys(µ.util.duplicatesCount(arr)); }; µ.util.translator = function(obj, sourceBranch, targetBranch, reverse) { if (reverse) { var targetBranchCopy = targetBranch.slice(); targetBranch = sourceBranch; sourceBranch = targetBranchCopy; } var value = sourceBranch.reduce(function(previousValue, currentValue) { if (typeof previousValue !== "undefined") { return previousValue[currentValue]; } }, obj); if (typeof value === "undefined") { return; } sourceBranch.reduce(function(previousValue, currentValue, index) { if (typeof previousValue === "undefined") { return; } if (index === sourceBranch.length - 1) { delete previousValue[currentValue]; } return previousValue[currentValue]; }, obj); targetBranch.reduce(function(previousValue, currentValue, index) { if (typeof previousValue[currentValue] === "undefined") { previousValue[currentValue] = {}; } if (index === targetBranch.length - 1) { previousValue[currentValue] = value; } return previousValue[currentValue]; }, obj); }; µ.PolyChart = function module() { var config = [ µ.PolyChart.defaultConfig() ]; var dispatch = d3.dispatch("hover"); var dashArray = { solid: "none", dash: [ 5, 2 ], dot: [ 2, 5 ] }; var colorScale; function exports() { var geometryConfig = config[0].geometryConfig; var container = geometryConfig.container; if (typeof container === "string") { container = d3.select(container); } container.datum(config).each(function(_config, _index) { var isStack = !!_config[0].data.yStack; var data = _config.map(function(d, i) { if (isStack) { return d3.zip(d.data.t[0], d.data.r[0], d.data.yStack[0]); } else { return d3.zip(d.data.t[0], d.data.r[0]); } }); var angularScale = geometryConfig.angularScale; var domainMin = geometryConfig.radialScale.domain()[0]; var generator = {}; generator.bar = function(d, i, pI) { var dataConfig = _config[pI].data; var h = geometryConfig.radialScale(d[1]) - geometryConfig.radialScale(0); var stackTop = geometryConfig.radialScale(d[2] || 0); var w = dataConfig.barWidth; d3.select(this).attr({ "class": "mark bar", d: "M" + [ [ h + stackTop, -w / 2 ], [ h + stackTop, w / 2 ], [ stackTop, w / 2 ], [ stackTop, -w / 2 ] ].join("L") + "Z", transform: function(d, i) { return "rotate(" + (geometryConfig.orientation + angularScale(d[0])) + ")"; } }); }; generator.dot = function(d, i, pI) { var stackedData = d[2] ? [ d[0], d[1] + d[2] ] : d; var symbol = d3.svg.symbol().size(_config[pI].data.dotSize).type(_config[pI].data.dotType)(d, i); d3.select(this).attr({ "class": "mark dot", d: symbol, transform: function(d, i) { var coord = convertToCartesian(getPolarCoordinates(stackedData)); return "translate(" + [ coord.x, coord.y ] + ")"; } }); }; var line = d3.svg.line.radial().interpolate(_config[0].data.lineInterpolation).radius(function(d) { return geometryConfig.radialScale(d[1]); }).angle(function(d) { return geometryConfig.angularScale(d[0]) * Math.PI / 180; }); generator.line = function(d, i, pI) { var lineData = d[2] ? data[pI].map(function(d, i) { return [ d[0], d[1] + d[2] ]; }) : data[pI]; d3.select(this).each(generator.dot).style({ opacity: function(dB, iB) { return +_config[pI].data.dotVisible; }, fill: markStyle.stroke(d, i, pI) }).attr({ "class": "mark dot" }); if (i > 0) { return; } var lineSelection = d3.select(this.parentNode).selectAll("path.line").data([ 0 ]); lineSelection.enter().insert("path"); lineSelection.attr({ "class": "line", d: line(lineData), transform: function(dB, iB) { return "rotate(" + (geometryConfig.orientation + 90) + ")"; }, "pointer-events": "none" }).style({ fill: function(dB, iB) { return markStyle.fill(d, i, pI); }, "fill-opacity": 0, stroke: function(dB, iB) { return markStyle.stroke(d, i, pI); }, "stroke-width": function(dB, iB) { return markStyle["stroke-width"](d, i, pI); }, "stroke-dasharray": function(dB, iB) { return markStyle["stroke-dasharray"](d, i, pI); }, opacity: function(dB, iB) { return markStyle.opacity(d, i, pI); }, display: function(dB, iB) { return markStyle.display(d, i, pI); } }); }; var angularRange = geometryConfig.angularScale.range(); var triangleAngle = Math.abs(angularRange[1] - angularRange[0]) / data[0].length * Math.PI / 180; var arc = d3.svg.arc().startAngle(function(d) { return -triangleAngle / 2; }).endAngle(function(d) { return triangleAngle / 2; }).innerRadius(function(d) { return geometryConfig.radialScale(domainMin + (d[2] || 0)); }).outerRadius(function(d) { return geometryConfig.radialScale(domainMin + (d[2] || 0)) + geometryConfig.radialScale(d[1]); }); generator.arc = function(d, i, pI) { d3.select(this).attr({ "class": "mark arc", d: arc, transform: function(d, i) { return "rotate(" + (geometryConfig.orientation + angularScale(d[0]) + 90) + ")"; } }); }; var pieArc = d3.svg.arc().outerRadius(geometryConfig.radialScale.range()[1]); var pie = d3.layout.pie().value(function(d) { return d[1]; }); var pieData = pie(data[0]); generator.pie = function(d, i, pI) { d3.select(this).attr({ "class": "mark arc", d: pieArc(pieData[i], i) }); }; var markStyle = { fill: function(d, i, pI) { return _config[pI].data.color; }, stroke: function(d, i, pI) { return _config[pI].data.strokeColor; }, "stroke-width": function(d, i, pI) { return _config[pI].data.strokeSize + "px"; }, "stroke-dasharray": function(d, i, pI) { return dashArray[_config[pI].data.strokeDash]; }, opacity: function(d, i, pI) { return _config[pI].data.opacity; }, display: function(d, i, pI) { return typeof _config[pI].data.visible === "undefined" || _config[pI].data.visible ? "block" : "none"; } }; var geometryLayer = d3.select(this).selectAll("g.layer").data(data); geometryLayer.enter().append("g").attr({ "class": "layer" }); var geometry = geometryLayer.selectAll("path.mark").data(function(d, i) { return d; }); geometry.enter().append("path").attr({ "class": "mark" }); geometry.style(markStyle).each(generator[geometryConfig.geometryType]); geometry.exit().remove(); geometryLayer.exit().remove(); function getPolarCoordinates(d, i) { var r = geometryConfig.radialScale(d[1]); var t = (geometryConfig.angularScale(d[0]) + geometryConfig.orientation) * Math.PI / 180; return { r: r, t: t }; } function convertToCartesian(polarCoordinates) { var x = polarCoordinates.r * Math.cos(polarCoordinates.t); var y = polarCoordinates.r * Math.sin(polarCoordinates.t); return { x: x, y: y }; } }); } exports.config = function(_x) { if (!arguments.length) { return config; } _x.forEach(function(d, i) { if (!config[i]) { config[i] = {}; } µ.util.deepExtend(config[i], µ.PolyChart.defaultConfig()); µ.util.deepExtend(config[i], d); }); return this; }; exports.getColorScale = function() { return colorScale; }; d3.rebind(exports, dispatch, "on"); return exports; }; µ.PolyChart.defaultConfig = function() { var config = { data: { name: "geom1", t: [ [ 1, 2, 3, 4 ] ], r: [ [ 1, 2, 3, 4 ] ], dotType: "circle", dotSize: 64, dotVisible: false, barWidth: 20, color: "#ffa500", strokeSize: 1, strokeColor: "silver", strokeDash: "solid", opacity: 1, index: 0, visible: true, visibleInLegend: true }, geometryConfig: { geometry: "LinePlot", geometryType: "arc", direction: "clockwise", orientation: 0, container: "body", radialScale: null, angularScale: null, colorScale: d3.scale.category20() } }; return config; }; µ.BarChart = function module() { return µ.PolyChart(); }; µ.BarChart.defaultConfig = function() { var config = { geometryConfig: { geometryType: "bar" } }; return config; }; µ.AreaChart = function module() { return µ.PolyChart(); }; µ.AreaChart.defaultConfig = function() { var config = { geometryConfig: { geometryType: "arc" } }; return config; }; µ.DotPlot = function module() { return µ.PolyChart(); }; µ.DotPlot.defaultConfig = function() { var config = { geometryConfig: { geometryType: "dot", dotType: "circle" } }; return config; }; µ.LinePlot = function module() { return µ.PolyChart(); }; µ.LinePlot.defaultConfig = function() { var config = { geometryConfig: { geometryType: "line" } }; return config; }; µ.Legend = function module() { var config = µ.Legend.defaultConfig(); var dispatch = d3.dispatch("hover"); function exports() { var legendConfig = config.legendConfig; var flattenData = config.data.map(function(d, i) { return [].concat(d).map(function(dB, iB) { var element = µ.util.deepExtend({}, legendConfig.elements[i]); element.name = dB; element.color = [].concat(legendConfig.elements[i].color)[iB]; return element; }); }); var data = d3.merge(flattenData); data = data.filter(function(d, i) { return legendConfig.elements[i] && (legendConfig.elements[i].visibleInLegend || typeof legendConfig.elements[i].visibleInLegend === "undefined"); }); if (legendConfig.reverseOrder) { data = data.reverse(); } var container = legendConfig.container; if (typeof container === "string" || container.nodeName) { container = d3.select(container); } var colors = data.map(function(d, i) { return d.color; }); var lineHeight = legendConfig.fontSize; var isContinuous = legendConfig.isContinuous === null ? typeof data[0] === "number" : legendConfig.isContinuous; var height = isContinuous ? legendConfig.height : lineHeight * data.length; var legendContainerGroup = container.classed("legend-group", true); var svg = legendContainerGroup.selectAll("svg").data([ 0 ]); var svgEnter = svg.enter().append("svg").attr({ width: 300, height: height + lineHeight, xmlns: "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", version: "1.1" }); svgEnter.append("g").classed("legend-axis", true); svgEnter.append("g").classed("legend-marks", true); var dataNumbered = d3.range(data.length); var colorScale = d3.scale[isContinuous ? "linear" : "ordinal"]().domain(dataNumbered).range(colors); var dataScale = d3.scale[isContinuous ? "linear" : "ordinal"]().domain(dataNumbered)[isContinuous ? "range" : "rangePoints"]([ 0, height ]); var shapeGenerator = function(_type, _size) { var squareSize = _size * 3; if (_type === "line") { return "M" + [ [ -_size / 2, -_size / 12 ], [ _size / 2, -_size / 12 ], [ _size / 2, _size / 12 ], [ -_size / 2, _size / 12 ] ] + "Z"; } else if (d3.svg.symbolTypes.indexOf(_type) !== -1) { return d3.svg.symbol().type(_type).size(squareSize)(); } else { return d3.svg.symbol().type("square").size(squareSize)(); } }; if (isContinuous) { var gradient = svg.select(".legend-marks").append("defs").append("linearGradient").attr({ id: "grad1", x1: "0%", y1: "0%", x2: "0%", y2: "100%" }).selectAll("stop").data(colors); gradient.enter().append("stop"); gradient.attr({ offset: function(d, i) { return i / (colors.length - 1) * 100 + "%"; } }).style({ "stop-color": function(d, i) { return d; } }); svg.append("rect").classed("legend-mark", true).attr({ height: legendConfig.height, width: legendConfig.colorBandWidth, fill: "url(#grad1)" }); } else { var legendElement = svg.select(".legend-marks").selectAll("path.legend-mark").data(data); legendElement.enter().append("path").classed("legend-mark", true); legendElement.attr({ transform: function(d, i) { return "translate(" + [ lineHeight / 2, dataScale(i) + lineHeight / 2 ] + ")"; }, d: function(d, i) { var symbolType = d.symbol; return shapeGenerator(symbolType, lineHeight); }, fill: function(d, i) { return colorScale(i); } }); legendElement.exit().remove(); } var legendAxis = d3.svg.axis().scale(dataScale).orient("right"); var axis = svg.select("g.legend-axis").attr({ transform: "translate(" + [ isContinuous ? legendConfig.colorBandWidth : lineHeight, lineHeight / 2 ] + ")" }).call(legendAxis); axis.selectAll(".domain").style({ fill: "none", stroke: "none" }); axis.selectAll("line").style({ fill: "none", stroke: isContinuous ? legendConfig.textColor : "none" }); axis.selectAll("text").style({ fill: legendConfig.textColor, "font-size": legendConfig.fontSize }).text(function(d, i) { return data[i].name; }); return exports; } exports.config = function(_x) { if (!arguments.length) { return config; } µ.util.deepExtend(config, _x); return this; }; d3.rebind(exports, dispatch, "on"); return exports; }; µ.Legend.defaultConfig = function(d, i) { var config = { data: [ "a", "b", "c" ], legendConfig: { elements: [ { symbol: "line", color: "red" }, { symbol: "square", color: "yellow" }, { symbol: "diamond", color: "limegreen" } ], height: 150, colorBandWidth: 30, fontSize: 12, container: "body", isContinuous: null, textColor: "grey", reverseOrder: false } }; return config; }; µ.tooltipPanel = function() { var tooltipEl, tooltipTextEl, backgroundEl; var config = { container: null, hasTick: false, fontSize: 12, color: "white", padding: 5 }; var id = "tooltip-" + µ.tooltipPanel.uid++; var tickSize = 10; var exports = function() { tooltipEl = config.container.selectAll("g." + id).data([ 0 ]); var tooltipEnter = tooltipEl.enter().append("g").classed(id, true).style({ "pointer-events": "none", display: "none" }); backgroundEl = tooltipEnter.append("path").style({ fill: "white", "fill-opacity": .9 }).attr({ d: "M0 0" }); tooltipTextEl = tooltipEnter.append("text").attr({ dx: config.padding + tickSize, dy: +config.fontSize * .3 }); return exports; }; exports.text = function(_text) { var l = d3.hsl(config.color).l; var strokeColor = l >= .5 ? "#aaa" : "white"; var fillColor = l >= .5 ? "black" : "white"; var text = _text || ""; tooltipTextEl.style({ fill: fillColor, "font-size": config.fontSize + "px" }).text(text); var padding = config.padding; var bbox = tooltipTextEl.node().getBBox(); var boxStyle = { fill: config.color, stroke: strokeColor, "stroke-width": "2px" }; var backGroundW = bbox.width + padding * 2 + tickSize; var backGroundH = bbox.height + padding * 2; backgroundEl.attr({ d: "M" + [ [ tickSize, -backGroundH / 2 ], [ tickSize, -backGroundH / 4 ], [ config.hasTick ? 0 : tickSize, 0 ], [ tickSize, backGroundH / 4 ], [ tickSize, backGroundH / 2 ], [ backGroundW, backGroundH / 2 ], [ backGroundW, -backGroundH / 2 ] ].join("L") + "Z" }).style(boxStyle); tooltipEl.attr({ transform: "translate(" + [ tickSize, -backGroundH / 2 + padding * 2 ] + ")" }); tooltipEl.style({ display: "block" }); return exports; }; exports.move = function(_pos) { if (!tooltipEl) { return; } tooltipEl.attr({ transform: "translate(" + [ _pos[0], _pos[1] ] + ")" }).style({ display: "block" }); return exports; }; exports.hide = function() { if (!tooltipEl) { return; } tooltipEl.style({ display: "none" }); return exports; }; exports.show = function() { if (!tooltipEl) { return; } tooltipEl.style({ display: "block" }); return exports; }; exports.config = function(_x) { µ.util.deepExtend(config, _x); return exports; }; return exports; }; µ.tooltipPanel.uid = 1; µ.adapter = {}; µ.adapter.plotly = function module() { var exports = {}; exports.convert = function(_inputConfig, reverse) { var outputConfig = {}; if (_inputConfig.data) { outputConfig.data = _inputConfig.data.map(function(d, i) { var r = µ.util.deepExtend({}, d); var toTranslate = [ [ r, [ "marker", "color" ], [ "color" ] ], [ r, [ "marker", "opacity" ], [ "opacity" ] ], [ r, [ "marker", "line", "color" ], [ "strokeColor" ] ], [ r, [ "marker", "line", "dash" ], [ "strokeDash" ] ], [ r, [ "marker", "line", "width" ], [ "strokeSize" ] ], [ r, [ "marker", "symbol" ], [ "dotType" ] ], [ r, [ "marker", "size" ], [ "dotSize" ] ], [ r, [ "marker", "barWidth" ], [ "barWidth" ] ], [ r, [ "line", "interpolation" ], [ "lineInterpolation" ] ], [ r, [ "showlegend" ], [ "visibleInLegend" ] ] ]; toTranslate.forEach(function(d, i) { µ.util.translator.apply(null, d.concat(reverse)); }); if (!reverse) { delete r.marker; } if (reverse) { delete r.groupId; } if (!reverse) { if (r.type === "scatter") { if (r.mode === "lines") { r.geometry = "LinePlot"; } else if (r.mode === "markers") { r.geometry = "DotPlot"; } else if (r.mode === "lines+markers") { r.geometry = "LinePlot"; r.dotVisible = true; } } else if (r.type === "area") { r.geometry = "AreaChart"; } else if (r.type === "bar") { r.geometry = "BarChart"; } delete r.mode; delete r.type; } else { if (r.geometry === "LinePlot") { r.type = "scatter"; if (r.dotVisible === true) { delete r.dotVisible; r.mode = "lines+markers"; } else { r.mode = "lines"; } } else if (r.geometry === "DotPlot") { r.type = "scatter"; r.mode = "markers"; } else if (r.geometry === "AreaChart") { r.type = "area"; } else if (r.geometry === "BarChart") { r.type = "bar"; } delete r.geometry; } return r; }); if (!reverse && _inputConfig.layout && _inputConfig.layout.barmode === "stack") { var duplicates = µ.util.duplicates(outputConfig.data.map(function(d, i) { return d.geometry; })); outputConfig.data.forEach(function(d, i) { var idx = duplicates.indexOf(d.geometry); if (idx !== -1) { outputConfig.data[i].groupId = idx; } }); } } if (_inputConfig.layout) { var r = µ.util.deepExtend({}, _inputConfig.layout); var toTranslate = [ [ r, [ "plot_bgcolor" ], [ "backgroundColor" ] ], [ r, [ "showlegend" ], [ "showLegend" ] ], [ r, [ "radialaxis" ], [ "radialAxis" ] ], [ r, [ "angularaxis" ], [ "angularAxis" ] ], [ r.angularaxis, [ "showline" ], [ "gridLinesVisible" ] ], [ r.angularaxis, [ "showticklabels" ], [ "labelsVisible" ] ], [ r.angularaxis, [ "nticks" ], [ "ticksCount" ] ], [ r.angularaxis, [ "tickorientation" ], [ "tickOrientation" ] ], [ r.angularaxis, [ "ticksuffix" ], [ "ticksSuffix" ] ], [ r.angularaxis, [ "range" ], [ "domain" ] ], [ r.angularaxis, [ "endpadding" ], [ "endPadding" ] ], [ r.radialaxis, [ "showline" ], [ "gridLinesVisible" ] ], [ r.radialaxis, [ "tickorientation" ], [ "tickOrientation" ] ], [ r.radialaxis, [ "ticksuffix" ], [ "ticksSuffix" ] ], [ r.radialaxis, [ "range" ], [ "domain" ] ], [ r.angularAxis, [ "showline" ], [ "gridLinesVisible" ] ], [ r.angularAxis, [ "showticklabels" ], [ "labelsVisible" ] ], [ r.angularAxis, [ "nticks" ], [ "ticksCount" ] ], [ r.angularAxis, [ "tickorientation" ], [ "tickOrientation" ] ], [ r.angularAxis, [ "ticksuffix" ], [ "ticksSuffix" ] ], [ r.angularAxis, [ "range" ], [ "domain" ] ], [ r.angularAxis, [ "endpadding" ], [ "endPadding" ] ], [ r.radialAxis, [ "showline" ], [ "gridLinesVisible" ] ], [ r.radialAxis, [ "tickorientation" ], [ "tickOrientation" ] ], [ r.radialAxis, [ "ticksuffix" ], [ "ticksSuffix" ] ], [ r.radialAxis, [ "range" ], [ "domain" ] ], [ r.font, [ "outlinecolor" ], [ "outlineColor" ] ], [ r.legend, [ "traceorder" ], [ "reverseOrder" ] ], [ r, [ "labeloffset" ], [ "labelOffset" ] ], [ r, [ "defaultcolorrange" ], [ "defaultColorRange" ] ] ]; toTranslate.forEach(function(d, i) { µ.util.translator.apply(null, d.concat(reverse)); }); if (!reverse) { if (r.angularAxis && typeof r.angularAxis.ticklen !== "undefined") { r.tickLength = r.angularAxis.ticklen; } if (r.angularAxis && typeof r.angularAxis.tickcolor !== "undefined") { r.tickColor = r.angularAxis.tickcolor; } } else { if (typeof r.tickLength !== "undefined") { r.angularaxis.ticklen = r.tickLength; delete r.tickLength; } if (r.tickColor) { r.angularaxis.tickcolor = r.tickColor; delete r.tickColor; } } if (r.legend && typeof r.legend.reverseOrder !== "boolean") { r.legend.reverseOrder = r.legend.reverseOrder !== "normal"; } if (r.legend && typeof r.legend.traceorder === "boolean") { r.legend.traceorder = r.legend.traceorder ? "reversed" : "normal"; delete r.legend.reverseOrder; } if (r.margin && typeof r.margin.t !== "undefined") { var source = [ "t", "r", "b", "l", "pad" ]; var target = [ "top", "right", "bottom", "left", "pad" ]; var margin = {}; d3.entries(r.margin).forEach(function(dB, iB) { margin[target[source.indexOf(dB.key)]] = dB.value; }); r.margin = margin; } if (reverse) { delete r.needsEndSpacing; delete r.minorTickColor; delete r.minorTicks; if (r.angularaxis) { delete r.angularaxis.ticksCount; delete r.angularaxis.ticksCount; delete r.angularaxis.ticksStep; delete r.angularaxis.rewriteTicks; delete r.angularaxis.nticks; } if (r.radialaxis) { delete r.radialaxis.ticksCount; delete r.radialaxis.ticksCount; delete r.radialaxis.ticksStep; delete r.radialaxis.rewriteTicks; delete r.radialaxis.nticks; } } outputConfig.layout = r; } return outputConfig; }; return exports; };