diff --git a/addon/components/e3-shape/arc.js b/addon/components/e3-shape/arc.js new file mode 100644 index 0000000..c946993 --- /dev/null +++ b/addon/components/e3-shape/arc.js @@ -0,0 +1,14 @@ +import e3AnimatedChild from '../e3-animated-child'; + +export default e3AnimatedChild.extend({ + shadowType: 'arc', + enterState: {}, + activeState: { + x: 0, + y: 0, + 'start-angle': 0, + 'angle': 0, + 'inner-radius': 0, + 'outer-radius': null + } +}); diff --git a/addon/helpers/e3-extent.js b/addon/helpers/e3-extent.js index 75b4187..5fd0a93 100644 --- a/addon/helpers/e3-extent.js +++ b/addon/helpers/e3-extent.js @@ -9,6 +9,7 @@ const {get} = Ember; Options { key => the property key of where the value sits on each of the items in the array padding => A percent (0-1) of the found extent to be "padded" to both ends of the extent + sum => A flag to determine wether we should take the sum of the values. min-value => The minimum possible value (clips actual values lower than it) max-value => The maximum possible value (clips values higher than it) min-delta => Forces the different between the min and max to be at least a certain value. @@ -24,6 +25,7 @@ export function e3Extent(params, hash) { } let key = hash.key; + let sum = hash['sum']; let nestedKey = hash['nested-key']; let nestedSum = hash['nested-sum']; let length = arr.length; @@ -57,12 +59,23 @@ export function e3Extent(params, hash) { length = arr.length; index = -1; - // Iterate over the array and figure out min/max values. - while(++index < length) { - // If there's a key to map, get that value. - let cur = key ? get(arr[index], key) : arr[index]; - result[0] = Math.min(result[0], cur); - result[1] = Math.max(result[1], cur); + if(sum) { + let sumVal = 0; + while(++index < length) { + let cur = key ? get(arr[index], key) : arr[index]; + sumVal += cur; + } + + result[0] = 0; + result[1] = Math.max(result[1], sumVal); + } else { + // Iterate over the array and figure out min/max values. + while(++index < length) { + // If there's a key to map, get that value. + let cur = key ? get(arr[index], key) : arr[index]; + result[0] = Math.min(result[0], cur); + result[1] = Math.max(result[1], cur); + } } // Apply the padding. diff --git a/addon/helpers/e3-radian-range.js b/addon/helpers/e3-radian-range.js new file mode 100644 index 0000000..2ebc209 --- /dev/null +++ b/addon/helpers/e3-radian-range.js @@ -0,0 +1,10 @@ +import Ember from 'ember'; +let twoPI = 2 * Math.PI; + +export function e3RadianRange(params, hash = {}) { + let startPercent = 'start' in hash ? hash.start : 0; + let endPercent = 'end' in hash ? hash.end : 1; + return [startPercent * twoPI, endPercent * twoPI]; +} + +export default Ember.Helper.helper(e3RadianRange); diff --git a/addon/mixins/e3-scale.js b/addon/mixins/e3-scale.js index 610dd82..d44ae9c 100644 --- a/addon/mixins/e3-scale.js +++ b/addon/mixins/e3-scale.js @@ -62,7 +62,13 @@ export default Ember.Mixin.create({ do something to this. */ getDomain() { - return this.getAttr('domain'); + let domain = this.getAttr('domain'); + + if(!isArray(domain)) { + domain = [0, domain]; + } + + return domain; }, /* diff --git a/addon/templates/components/e3-shape/path.hbs b/addon/templates/components/e3-shape/path.hbs deleted file mode 100644 index 889d9ee..0000000 --- a/addon/templates/components/e3-shape/path.hbs +++ /dev/null @@ -1 +0,0 @@ -{{yield}} diff --git a/addon/templates/components/e3-shape/text.hbs b/addon/templates/components/e3-shape/text.hbs deleted file mode 100644 index 889d9ee..0000000 --- a/addon/templates/components/e3-shape/text.hbs +++ /dev/null @@ -1 +0,0 @@ -{{yield}} diff --git a/addon/utils/shadow/line-interpolation/monotone.js b/addon/utils/shadow/line-interpolation/monotone.js index c7e59a9..c23a748 100644 --- a/addon/utils/shadow/line-interpolation/monotone.js +++ b/addon/utils/shadow/line-interpolation/monotone.js @@ -1,4 +1,5 @@ const {abs} = Math; +import { Point, SmoothCurve, BezierCurve } from '../types/commands'; /** * Returns an array of arguemnts for commands @@ -11,7 +12,10 @@ export default function monotone(points) { return points; } - return [points[0]].concat(d3_svg_lineHermite(points, d3_svg_lineMonotoneTangents(points))); + var commands = []; + commands.push(new Point(points[0][0], points[0][1])); + var hermite = d3_svg_lineHermite(points, d3_svg_lineMonotoneTangents(points)); + return commands.concat(hermite); } /* @@ -90,12 +94,14 @@ function d3_svg_lineHermite(points, tangents) { pi = 1; if (quad) { - commands.push([ - p[0] - t0[0] * 2 / 3, - p[1] - t0[1] * 2 / 3, - p[0], - p[1] - ]); + commands.push( + new SmoothCurve( + p[0] - t0[0] * 2 / 3, + p[1] - t0[1] * 2 / 3, + p[0], + p[1] + ) + ); p0 = points[1]; pi = 2; } @@ -104,14 +110,16 @@ function d3_svg_lineHermite(points, tangents) { t = tangents[1]; p = points[pi]; pi++; - commands.push([ - p0[0] + t0[0], - p0[1] + t0[1], - p[0] - t[0], - p[1] - t[1], - p[0], - p[1] - ]); + commands.push( + new BezierCurve( + p0[0] + t0[0], + p0[1] + t0[1], + p[0] - t[0], + p[1] - t[1], + p[0], + p[1] + ) + ); for (var i = 2; i < tangents.length; i++, pi++) { p = points[pi]; @@ -119,25 +127,29 @@ function d3_svg_lineHermite(points, tangents) { let lt = tangents[i - 1]; let lp = points[i - 1]; - commands.push([ - lp[0] + lt[0], // Add the last tangent but reflected - lp[1] + lt[1], - p[0] - t[0], - p[1] - t[1], - p[0], - p[1] - ]); + commands.push( + new BezierCurve( + lp[0] + lt[0], // Add the last tangent but reflected + lp[1] + lt[1], + p[0] - t[0], + p[1] - t[1], + p[0], + p[1] + ) + ); } } if (quad) { var lp = points[pi]; - commands.push([ - p[0] + t[0] * 2 / 3, - p[1] + t[1] * 2 / 3, - lp[0], - lp[1] - ]); + commands.push( + new SmoothCurve( + p[0] + t[0] * 2 / 3, + p[1] + t[1] * 2 / 3, + lp[0], + lp[1] + ) + ); } return commands; diff --git a/addon/utils/shadow/line-interpolation/path-commands.js b/addon/utils/shadow/line-interpolation/path-commands.js index 3157adf..c2dd621 100644 --- a/addon/utils/shadow/line-interpolation/path-commands.js +++ b/addon/utils/shadow/line-interpolation/path-commands.js @@ -1,4 +1,5 @@ import monotone from './monotone'; +import { Point } from './monotone'; export default function generatePath(xPoints, yPoints, interpolation = 'linear') { @@ -6,7 +7,9 @@ export default function generatePath(xPoints, yPoints, interpolation = 'linear') var commands; switch(interpolation) { case 'linear': - commands = points; + commands = points.map(point => { + return new Point(point[0], point[1]); + }); break; case 'monotone': commands = monotone(points); diff --git a/addon/utils/shadow/types/canvas.js b/addon/utils/shadow/types/canvas.js index a13cfa2..cf42c49 100644 --- a/addon/utils/shadow/types/canvas.js +++ b/addon/utils/shadow/types/canvas.js @@ -49,6 +49,25 @@ export default { return parentContext; }, + arc(parentContext, selfContext, attrs, matrix) { + preShape(parentContext, attrs, matrix); + /* + attrs = { + x: {X Center}, + y: {Y Center}, + 'start-angle': {In Radians}, + 'angle': {In Radians}, + 'inner-radius': {Defaults to 0}, + 'outer-radius': {Number} + } + + Should accept these as arguments to construct a path array (shared with SVG); + Path array converts into commands...maybe pathFromCommands should support Arc commands? + */ + postShape(parentContext, attrs, matrix); + return parentContext; + }, + stage(parentContext, selfContext, attrs) { if(!selfContext) { selfContext = parentContext.getContext('2d'); diff --git a/addon/utils/shadow/types/commands.js b/addon/utils/shadow/types/commands.js new file mode 100644 index 0000000..a3b1f9f --- /dev/null +++ b/addon/utils/shadow/types/commands.js @@ -0,0 +1,51 @@ +// gets set to move for the first command and line for all others +export function Point(x, y) { + this.type = 'point'; + this.x = x; + this.y = y; +} + +export function Move(x, y) { + this.type = 'move'; + this.x = x; + this.y = y; +} + +export function Line(x, y) { + this.type = 'line'; + this.x = x; + this.y = y; +} + +export function SmoothCurve(x2, y2, x, y) { + this.type = 'smooth-curve'; + this.x2 = x2; + this.y2 = y2; + this.x = x; + this.y = y; +} + +export function BezierCurve(x1, y1, x2, y2, x, y) { + this.type = 'bezier-curve'; + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + this.x = x; + this.y = y; +} + +export function Arc(rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y) { + this.type = 'arc'; + this.x = x; + this.y = y; + this.xAxisRotation = xAxisRotation; + this.largeArcFlag = largeArcFlag; + this.sweepFlag = sweepFlag; + this.rx = rx; + this.ry = ry; +} + +export function Close() { + this.type = 'close'; +} diff --git a/addon/utils/shadow/types/svg.js b/addon/utils/shadow/types/svg.js index bf30489..2647d3a 100644 --- a/addon/utils/shadow/types/svg.js +++ b/addon/utils/shadow/types/svg.js @@ -2,6 +2,7 @@ import Ember from 'ember'; import {toArray} from '../matrix-math'; import pathCommands from '../line-interpolation/path-commands'; import pathFromCommands from './svg/path-from-commands'; +import arcSvgCommands from './svg/arc-svg-commands'; import ATTRIBUTE_MAP from './svg-attribute-map'; const {keys} = Object; const {copy} = Ember; @@ -55,6 +56,21 @@ export default { return selfContext; }, + arc(parentContext, selfContext, attrs) { + selfContext = preRender(selfContext, parentContext, 'path'); + var commands = arcSvgCommands( + attrs.x, + attrs.y, + attrs['start-angle'], + attrs['angle'], + attrs['inner-radius'], + attrs['outer-radius'] + ); + attrs.d = pathFromCommands(commands); + renderAttributes('path', selfContext, attrs); + return selfContext; + }, + stage(parentContext/*, selfContext, attrs*/) { return parentContext; }, diff --git a/addon/utils/shadow/types/svg/arc-svg-commands.js b/addon/utils/shadow/types/svg/arc-svg-commands.js new file mode 100644 index 0000000..e5c1641 --- /dev/null +++ b/addon/utils/shadow/types/svg/arc-svg-commands.js @@ -0,0 +1,113 @@ +const {PI} = Math; +import { Line, Move, Arc, Close } from '../commands'; + +/* + * Lovingly plucked from d3.js + * + * https://github.com/mbostock/d3/blob/master/src/svg/arc.js + */ +export default function arcSvgCommands(x, y, startAngle, angle, innerRadius, outerRadius) { + + var pi = PI, + r0 = Math.max(0, +innerRadius), + r1 = Math.max(0, +outerRadius), + a0 = startAngle - pi / 2, + a1 = (startAngle + angle) - pi / 2, + da = Math.abs(a1 - a0), + cw = a0 > a1 ? 0 : 1, + commands = []; + + // Ensure that the outer radius is always larger than the inner radius. + if (r1 < r0) { + let rc = r1; + r1 = r0; + r0 = rc; + } + + // Special case for an arc that spans the full circle. + if (da >= 2 * pi - 0.000001) { + commands.push(circleSegment(r1, cw)); + if (r0) { + commands.push(circleSegment(r0, 1 - cw)); + } + commands.push(new Close()); + return commands; + } + + var p0 = 0, + p1 = 0, + x0, + y0, + x1, + y1, + x2, + y2, + x3, + y3, + l0, + l1; + + // Compute the two outer corners. + if (r1) { + x0 = r1 * Math.cos(a0 + p1); + y0 = r1 * Math.sin(a0 + p1); + x1 = r1 * Math.cos(a1 - p1); + y1 = r1 * Math.sin(a1 - p1); + + // Detect whether the outer corners are collapsed. + l1 = Math.abs(a1 - a0 - 2 * p1) <= pi ? 0 : 1; + if (p1 && d3_svg_arcSweep(x0, y0, x1, y1) === cw ^ l1) { + var h1 = (a0 + a1) / 2; + x0 = r1 * Math.cos(h1); + y0 = r1 * Math.sin(h1); + x1 = y1 = null; + } + } else { + x0 = y0 = 0; + } + + // Compute the two inner corners. + if (r0) { + x2 = r0 * Math.cos(a1 - p0); + y2 = r0 * Math.sin(a1 - p0); + x3 = r0 * Math.cos(a0 + p0); + y3 = r0 * Math.sin(a0 + p0); + + // Detect whether the inner corners are collapsed. + l0 = Math.abs(a0 - a1 + 2 * p0) <= pi ? 0 : 1; + if (p0 && d3_svg_arcSweep(x2, y2, x3, y3) === (1 - cw) ^ l0) { + var h0 = (a0 + a1) / 2; + x2 = r0 * Math.cos(h0); + y2 = r0 * Math.sin(h0); + x3 = y3 = null; + } + } else { + x2 = y2 = 0; + } + + commands.push(new Move(x, y)); + if (x1 != null) { + commands.push(new Arc(r1, r1, 0, l1, cw, x1, y1)); + } + + commands.push(new Line(x2, y2)); + if (x3 != null) { + commands.push(new Arc(r0, r0, 0, l0, 1 - cw, x3, y3)); + } + + commands.push(new Close()); + return commands; +} + +function circleSegment(r1, cw) { + return [ + new Move(0, r1), + new Arc(r1, r1, 0, 1, cw, 0, -r1), + new Arc(r1, r1, 0, 1, cw, 0, r1), + ]; +} + +// Note: similar to d3_cross2d, d3_geom_polygonInside +function d3_svg_arcSweep(x0, y0, x1, y1) { + return (x0 - x1) * y0 - (y0 - y1) * x0 > 0 ? 0 : 1; +} diff --git a/addon/utils/shadow/types/svg/path-from-commands.js b/addon/utils/shadow/types/svg/path-from-commands.js index 9bfde4a..04c6bd4 100644 --- a/addon/utils/shadow/types/svg/path-from-commands.js +++ b/addon/utils/shadow/types/svg/path-from-commands.js @@ -1,19 +1,38 @@ +import { Move, Line, Point, Arc, SmoothCurve, BezierCurve } from '../commands'; + export default function pathFromCommands(commands) { let svgCommands = commands.map((command, i) => { - let name = ''; - if (command.length === 2) { - name = i === 0 ? 'M' : 'L'; + + if (command instanceof Point) { + return (i === 0 ? 'M' : 'L') + command.x + ',' + command.y; + } + + if (command instanceof Move) { + return 'M' + command.x + ',' + command.y; } - if (command.length === 4) { - name = 'S'; + if (command instanceof Line) { + return 'L' + command.x + ',' + command.y; } - if (command.length === 6) { - name = 'C'; + if (command instanceof SmoothCurve) { + return 'S' + command.x2 + ',' + command.y2 + ' ' + + command.x + ',' + command.y; } - return name + command.join(','); + if (command instanceof BezierCurve) { + return 'C' + command.x1 + ',' + command.y1 + ' ' + + command.x2 + ',' + command.y2 + ' ' + + command.x + ',' + command.y; + } + + if (command instanceof Arc) { + return 'A' + command.rx + ',' + command.ry + ' ' + + command.xAxisRotation + ' ' + + command.largeArcFlag + ',' + + command.sweepFlag + ' ' + + command.x + ',' + command.y; + } }); return svgCommands.join(' '); diff --git a/app/components/e3-shape/arc.js b/app/components/e3-shape/arc.js new file mode 100644 index 0000000..7069127 --- /dev/null +++ b/app/components/e3-shape/arc.js @@ -0,0 +1 @@ +export { default } from 'ember-e3/components/e3-shape/arc'; \ No newline at end of file diff --git a/app/helpers/e3-radian-range.js b/app/helpers/e3-radian-range.js new file mode 100644 index 0000000..87bb8ef --- /dev/null +++ b/app/helpers/e3-radian-range.js @@ -0,0 +1 @@ +export { default, e3RadianRange } from 'ember-e3/helpers/e3-radian-range'; diff --git a/tests/dummy/app/router.js b/tests/dummy/app/router.js index 593863c..4089b3a 100644 --- a/tests/dummy/app/router.js +++ b/tests/dummy/app/router.js @@ -16,6 +16,7 @@ Router.map(function() { this.route('grouped-bars'); this.route('invert'); this.route('nytimes-strikeouts'); + this.route('pie-chart'); }); export default Router; diff --git a/tests/dummy/app/routes/pie-chart.js b/tests/dummy/app/routes/pie-chart.js new file mode 100644 index 0000000..5f9b5fc --- /dev/null +++ b/tests/dummy/app/routes/pie-chart.js @@ -0,0 +1,37 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({ + actions: { + addBar() { + let model = this.controller.get('model').slice(0); + model.unshift(o()); + this.controller.set('model', model); + }, + removeBar() { + let model = this.controller.get('model').slice(0); + model.pop(); + this.controller.set('model', model); + } + }, + + model() { + return g(10); + } +}); + +function g(number) { + let res = []; + while(--number >= 0) { + res.push(o()); + } + return res; +} + +let ID = 0; +function o() { + return { + id: ++ID, + value: Math.random() * 100, + temperature: Math.random() * 100 + }; +} \ No newline at end of file diff --git a/tests/dummy/app/templates/application.hbs b/tests/dummy/app/templates/application.hbs index 4cf6f60..4e0fe76 100644 --- a/tests/dummy/app/templates/application.hbs +++ b/tests/dummy/app/templates/application.hbs @@ -12,8 +12,9 @@ + - {{!-- Can't show this yet as it dependson the mock server. Blarg. --}} + {{!-- Can't show this yet as it depends on the mock server. Blarg. --}} {{!-- --}}
diff --git a/tests/dummy/app/templates/pie-chart.hbs b/tests/dummy/app/templates/pie-chart.hbs new file mode 100644 index 0000000..4c4f98b --- /dev/null +++ b/tests/dummy/app/templates/pie-chart.hbs @@ -0,0 +1,27 @@ +