diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..45f1dc0 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,9 @@ +parserOptions: + sourceType: module + +extends: + "eslint:recommended" + +rules: + no-cond-assign: 0 + no-fallthrough: 0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..75704c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.sublime-workspace +.DS_Store +build/ +node_modules +npm-debug.log diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..8c9eb1d --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +*.sublime-* +build/*.zip +img/ +test/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4f0b022 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright 2010-2015 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..acfe066 --- /dev/null +++ b/README.md @@ -0,0 +1,1146 @@ +# d3-shape + +Visualizations typically consist of discrete graphical marks, such as [symbols](#symbols), [arcs](#arcs), [lines](#lines) and [areas](#areas). While the rectangles of a bar chart may be easy enough to generate directly using [SVG](http://www.w3.org/TR/SVG/paths.html#PathData) or [Canvas](http://www.w3.org/TR/2dcontext/#canvaspathmethods), other shapes are complex, such as rounded annular sectors and centripetal Catmull–Rom splines. This module provides a variety of shape generators for your convenience. + +As with other aspects of D3, these shapes are driven by data: each shape generator exposes accessors that control how the input data are mapped to a visual representation. For example, you might define a line generator for a time series by [scaling](https://github.com/d3/d3-scale) fields of your data to fit the chart: + +```js +var line = d3.line() + .x(function(d) { return x(d.date); }) + .y(function(d) { return y(d.value); }); +``` + +This line generator can then be used to compute the `d` attribute of an SVG path element: + +```js +path.datum(data).attr("d", line); +``` + +Or you can use it to render to a Canvas 2D context: + +```js +line.context(context)(data); +``` + +For more, read [Introducing d3-shape](https://medium.com/@mbostock/introducing-d3-shape-73f8367e6d12). + +## Installing + +If you use NPM, `npm install d3-shape`. Otherwise, download the [latest release](https://github.com/d3/d3-shape/releases/latest). You can also load directly from [d3js.org](https://d3js.org), either as a [standalone library](https://d3js.org/d3-shape.v1.min.js) or as part of [D3 4.0](https://github.com/d3/d3). AMD, CommonJS, and vanilla environments are supported. In vanilla, a `d3` global is exported: + +```html + + + +``` + +[Try d3-shape in your browser.](https://tonicdev.com/npm/d3-shape) + +## API Reference + +* [Arcs](#arcs) +* [Pies](#pies) +* [Lines](#lines) +* [Areas](#areas) +* [Curves](#curves) +* [Custom Curves](#custom-curves) +* [Links](#links) +* [Symbols](#symbols) +* [Custom Symbol Types](#custom-symbol-types) +* [Stacks](#stacks) + +### Arcs + +[Pie Chart](http://bl.ocks.org/mbostock/8878e7fd82034f1d63cf)[Donut Chart](http://bl.ocks.org/mbostock/2394b23da1994fc202e1) + +The arc generator produces a [circular](https://en.wikipedia.org/wiki/Circular_sector) or [annular](https://en.wikipedia.org/wiki/Annulus_\(mathematics\)) sector, as in a pie or donut chart. If the difference between the [start](#arc_startAngle) and [end](#arc_endAngle) angles (the *angular span*) is greater than [τ](https://en.wikipedia.org/wiki/Turn_\(geometry\)#Tau_proposal), the arc generator will produce a complete circle or annulus. If it is less than τ, arcs may have [rounded corners](#arc_cornerRadius) and [angular padding](#arc_padAngle). Arcs are always centered at ⟨0,0⟩; use a transform (see: [SVG](http://www.w3.org/TR/SVG/coords.html#TransformAttribute), [Canvas](http://www.w3.org/TR/2dcontext/#transformations)) to move the arc to a different position. + +See also the [pie generator](#pies), which computes the necessary angles to represent an array of data as a pie or donut chart; these angles can then be passed to an arc generator. + +# d3.arc() [<>](https://github.com/d3/d3-shape/blob/master/src/arc.js "Source") + +Constructs a new arc generator with the default settings. + +# arc(arguments…) [<>](https://github.com/d3/d3-shape/blob/master/src/arc.js#L89 "Source") + +Generates an arc for the given *arguments*. The *arguments* are arbitrary; they are simply propagated to the arc generator’s accessor functions along with the `this` object. For example, with the default settings, an object with radii and angles is expected: + +```js +var arc = d3.arc(); + +arc({ + innerRadius: 0, + outerRadius: 100, + startAngle: 0, + endAngle: Math.PI / 2 +}); // "M0,-100A100,100,0,0,1,100,0L0,0Z" +``` + +If the radii and angles are instead defined as constants, you can generate an arc without any arguments: + +```js +var arc = d3.arc() + .innerRadius(0) + .outerRadius(100) + .startAngle(0) + .endAngle(Math.PI / 2); + +arc(); // "M0,-100A100,100,0,0,1,100,0L0,0Z" +``` + +If the arc generator has a [context](#arc_context), then the arc is rendered to this context as a sequence of [path method](http://www.w3.org/TR/2dcontext/#canvaspathmethods) calls and this function returns void. Otherwise, a [path data](http://www.w3.org/TR/SVG/paths.html#PathData) string is returned. + +# arc.centroid(arguments…) [<>](https://github.com/d3/d3-shape/blob/master/src/arc.js#L224 "Source") + +Computes the midpoint [*x*, *y*] of the center line of the arc that would be [generated](#_arc) by the given *arguments*. The *arguments* are arbitrary; they are simply propagated to the arc generator’s accessor functions along with the `this` object. To be consistent with the generated arc, the accessors must be deterministic, *i.e.*, return the same value given the same arguments. The midpoint is defined as ([startAngle](#arc_startAngle) + [endAngle](#arc_endAngle)) / 2 and ([innerRadius](#arc_innerRadius) + [outerRadius](#arc_outerRadius)) / 2. For example: + +[Circular Sector Centroids](http://bl.ocks.org/mbostock/9b5a2fd1ce1a146f27e4)[Annular Sector Centroids](http://bl.ocks.org/mbostock/c274877f647361f3df7d) + +Note that this is **not the geometric center** of the arc, which may be outside the arc; this method is merely a convenience for positioning labels. + +# arc.innerRadius([radius]) [<>](https://github.com/d3/d3-shape/blob/master/src/arc.js#L230 "Source") + +If *radius* is specified, sets the inner radius to the specified function or number and returns this arc generator. If *radius* is not specified, returns the current inner radius accessor, which defaults to: + +```js +function innerRadius(d) { + return d.innerRadius; +} +``` + +Specifying the inner radius as a function is useful for constructing a stacked polar bar chart, often in conjunction with a [sqrt scale](https://github.com/d3/d3-scale#sqrt). More commonly, a constant inner radius is used for a donut or pie chart. If the outer radius is smaller than the inner radius, the inner and outer radii are swapped. A negative value is treated as zero. + +# arc.outerRadius([radius]) [<>](https://github.com/d3/d3-shape/blob/master/src/arc.js#L234 "Source") + +If *radius* is specified, sets the outer radius to the specified function or number and returns this arc generator. If *radius* is not specified, returns the current outer radius accessor, which defaults to: + +```js +function outerRadius(d) { + return d.outerRadius; +} +``` + +Specifying the outer radius as a function is useful for constructing a coxcomb or polar bar chart, often in conjunction with a [sqrt scale](https://github.com/d3/d3-scale#sqrt). More commonly, a constant outer radius is used for a pie or donut chart. If the outer radius is smaller than the inner radius, the inner and outer radii are swapped. A negative value is treated as zero. + +# arc.cornerRadius([radius]) [<>](https://github.com/d3/d3-shape/blob/master/src/arc.js#L238 "Source") + +If *radius* is specified, sets the corner radius to the specified function or number and returns this arc generator. If *radius* is not specified, returns the current corner radius accessor, which defaults to: + +```js +function cornerRadius() { + return 0; +} +``` + +If the corner radius is greater than zero, the corners of the arc are rounded using circles of the given radius. For a circular sector, the two outer corners are rounded; for an annular sector, all four corners are rounded. The corner circles are shown in this diagram: + +[Rounded Circular Sectors](http://bl.ocks.org/mbostock/e5e3680f3079cf5c3437)[Rounded Annular Sectors](http://bl.ocks.org/mbostock/f41f50e06a6c04828b6e) + +The corner radius may not be larger than ([outerRadius](#arc_outerRadius) - [innerRadius](#arc_innerRadius)) / 2. In addition, for arcs whose angular span is less than π, the corner radius may be reduced as two adjacent rounded corners intersect. This is occurs more often with the inner corners. See the [arc corners animation](http://bl.ocks.org/mbostock/b7671cb38efdfa5da3af) for illustration. + +# arc.startAngle([angle]) [<>](https://github.com/d3/d3-shape/blob/master/src/arc.js#L246 "Source") + +If *angle* is specified, sets the start angle to the specified function or number and returns this arc generator. If *angle* is not specified, returns the current start angle accessor, which defaults to: + +```js +function startAngle(d) { + return d.startAngle; +} +``` + +The *angle* is specified in radians, with 0 at -*y* (12 o’clock) and positive angles proceeding clockwise. If |endAngle - startAngle| ≥ τ, a complete circle or annulus is generated rather than a sector. + +# arc.endAngle([angle]) [<>](https://github.com/d3/d3-shape/blob/master/src/arc.js#L250 "Source") + +If *angle* is specified, sets the end angle to the specified function or number and returns this arc generator. If *angle* is not specified, returns the current end angle accessor, which defaults to: + +```js +function endAngle(d) { + return d.endAngle; +} +``` + +The *angle* is specified in radians, with 0 at -*y* (12 o’clock) and positive angles proceeding clockwise. If |endAngle - startAngle| ≥ τ, a complete circle or annulus is generated rather than a sector. + +# arc.padAngle([angle]) [<>](https://github.com/d3/d3-shape/blob/master/src/arc.js#L254 "Source") + +If *angle* is specified, sets the pad angle to the specified function or number and returns this arc generator. If *angle* is not specified, returns the current pad angle accessor, which defaults to: + +```js +function padAngle() { + return d && d.padAngle; +} +``` + +The pad angle is converted to a fixed linear distance separating adjacent arcs, defined as [padRadius](#arc_padRadius) * padAngle. This distance is subtracted equally from the [start](#arc_startAngle) and [end](#arc_endAngle) of the arc. If the arc forms a complete circle or annulus, as when |endAngle - startAngle| ≥ τ, the pad angle is ignored. + +If the [inner radius](#arc_innerRadius) or angular span is small relative to the pad angle, it may not be possible to maintain parallel edges between adjacent arcs. In this case, the inner edge of the arc may collapse to a point, similar to a circular sector. For this reason, padding is typically only applied to annular sectors (*i.e.*, when innerRadius is positive), as shown in this diagram: + +[Padded Circular Sectors](http://bl.ocks.org/mbostock/f37b07b92633781a46f7)[Padded Annular Sectors](http://bl.ocks.org/mbostock/99f0a6533f7c949cf8b8) + +The recommended minimum inner radius when using padding is outerRadius \* padAngle / sin(θ), where θ is the angular span of the smallest arc before padding. For example, if the outer radius is 200 pixels and the pad angle is 0.02 radians, a reasonable θ is 0.04 radians, and a reasonable inner radius is 100 pixels. See the [arc padding animation](http://bl.ocks.org/mbostock/053fcc2295a445afab07) for illustration. + +Often, the pad angle is not set directly on the arc generator, but is instead computed by the [pie generator](#pies) so as to ensure that the area of padded arcs is proportional to their value; see [*pie*.padAngle](#pie_padAngle). See the [pie padding animation](http://bl.ocks.org/mbostock/3e961b4c97a1b543fff2) for illustration. If you apply a constant pad angle to the arc generator directly, it tends to subtract disproportionately from smaller arcs, introducing distortion. + +# arc.padRadius([radius]) [<>](https://github.com/d3/d3-shape/blob/master/src/arc.js#L242 "Source") + +If *radius* is specified, sets the pad radius to the specified function or number and returns this arc generator. If *radius* is not specified, returns the current pad radius accessor, which defaults to null, indicating that the pad radius should be automatically computed as sqrt([innerRadius](#arc_innerRadius) * innerRadius + [outerRadius](#arc_outerRadius) * outerRadius). The pad radius determines the fixed linear distance separating adjacent arcs, defined as padRadius * [padAngle](#arc_padAngle). + +# arc.context([context]) [<>](https://github.com/d3/d3-shape/blob/master/src/arc.js#L258 "Source") + +If *context* is specified, sets the context and returns this arc generator. If *context* is not specified, returns the current context, which defaults to null. If the context is not null, then the [generated arc](#_arc) is rendered to this context as a sequence of [path method](http://www.w3.org/TR/2dcontext/#canvaspathmethods) calls. Otherwise, a [path data](http://www.w3.org/TR/SVG/paths.html#PathData) string representing the generated arc is returned. + +### Pies + +The pie generator does not produce a shape directly, but instead computes the necessary angles to represent a tabular dataset as a pie or donut chart; these angles can then be passed to an [arc generator](#arcs). + +# d3.pie() [<>](https://github.com/d3/d3-shape/blob/master/src/pie.js "Source") + +Constructs a new pie generator with the default settings. + +# pie(data[, arguments…]) [<>](https://github.com/d3/d3-shape/blob/master/src/pie.js#L14 "Source") + +Generates a pie for the given array of *data*, returning an array of objects representing each datum’s arc angles. Any additional *arguments* are arbitrary; they are simply propagated to the pie generator’s accessor functions along with the `this` object. The length of the returned array is the same as *data*, and each element *i* in the returned array corresponds to the element *i* in the input data. Each object in the returned array has the following properties: + +* `data` - the input datum; the corresponding element in the input data array. +* `value` - the numeric [value](#pie_value) of the arc. +* `index` - the zero-based [sorted index](#pie_sort) of the arc. +* `startAngle` - the [start angle](#pie_startAngle) of the arc. +* `endAngle` - the [end angle](#pie_endAngle) of the arc. +* `padAngle` - the [pad angle](#pie_padAngle) of the arc. + +This representation is designed to work with the arc generator’s default [startAngle](#arc_startAngle), [endAngle](#arc_endAngle) and [padAngle](#arc_padAngle) accessors. The angular units are arbitrary, but if you plan to use the pie generator in conjunction with an [arc generator](#arcs), you should specify angles in radians, with 0 at -*y* (12 o’clock) and positive angles proceeding clockwise. + +Given a small dataset of numbers, here is how to compute the arc angles to render this data as a pie chart: + +```js +var data = [1, 1, 2, 3, 5, 8, 13, 21]; +var arcs = d3.pie()(data); +``` + +The first pair of parens, `pie()`, [constructs](#pie) a default pie generator. The second, `pie()(data)`, [invokes](#_pie) this generator on the dataset, returning an array of objects: + +```json +[ + {"data": 1, "value": 1, "index": 6, "startAngle": 6.050474740247008, "endAngle": 6.166830023713296, "padAngle": 0}, + {"data": 1, "value": 1, "index": 7, "startAngle": 6.166830023713296, "endAngle": 6.283185307179584, "padAngle": 0}, + {"data": 2, "value": 2, "index": 5, "startAngle": 5.817764173314431, "endAngle": 6.050474740247008, "padAngle": 0}, + {"data": 3, "value": 3, "index": 4, "startAngle": 5.468698322915565, "endAngle": 5.817764173314431, "padAngle": 0}, + {"data": 5, "value": 5, "index": 3, "startAngle": 4.886921905584122, "endAngle": 5.468698322915565, "padAngle": 0}, + {"data": 8, "value": 8, "index": 2, "startAngle": 3.956079637853813, "endAngle": 4.886921905584122, "padAngle": 0}, + {"data": 13, "value": 13, "index": 1, "startAngle": 2.443460952792061, "endAngle": 3.956079637853813, "padAngle": 0}, + {"data": 21, "value": 21, "index": 0, "startAngle": 0.000000000000000, "endAngle": 2.443460952792061, "padAngle": 0} +] +``` + +Note that the returned array is in the same order as the data, even though this pie chart is [sorted](#pie_sortValues) by descending value, starting with the arc for the last datum (value 21) at 12 o’clock. + +# pie.value([value]) [<>](https://github.com/d3/d3-shape/blob/master/src/pie.js#L54 "Source") + +If *value* is specified, sets the value accessor to the specified function or number and returns this pie generator. If *value* is not specified, returns the current value accessor, which defaults to: + +```js +function value(d) { + return d; +} +``` + +When a pie is [generated](#_pie), the value accessor will be invoked for each element in the input data array, being passed the element `d`, the index `i`, and the array `data` as three arguments. The default value accessor assumes that the input data are numbers, or that they are coercible to numbers using [valueOf](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/valueOf). If your data are not simply numbers, then you should specify an accessor that returns the corresponding numeric value for a given datum. For example: + +```js +var data = [ + {"number": 4, "name": "Locke"}, + {"number": 8, "name": "Reyes"}, + {"number": 15, "name": "Ford"}, + {"number": 16, "name": "Jarrah"}, + {"number": 23, "name": "Shephard"}, + {"number": 42, "name": "Kwon"} +]; + +var arcs = d3.pie() + .value(function(d) { return d.number; }) + (data); +``` + +This is similar to [mapping](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) your data to values before invoking the pie generator: + +```js +var arcs = d3.pie()(data.map(function(d) { return d.number; })); +``` + +The benefit of an accessor is that the input data remains associated with the returned objects, thereby making it easier to access other fields of the data, for example to set the color or to add text labels. + +# pie.sort([compare]) [<>](https://github.com/d3/d3-shape/blob/master/src/pie.js#L62 "Source") + +If *compare* is specified, sets the data comparator to the specified function and returns this pie generator. If *compare* is not specified, returns the current data comparator, which defaults to null. If both the data comparator and the value comparator are null, then arcs are positioned in the original input order. Otherwise, the data is sorted according to the data comparator, and the resulting order is used. Setting the data comparator implicitly sets the [value comparator](#pie_sortValues) to null. + +The *compare* function takes two arguments *a* and *b*, each elements from the input data array. If the arc for *a* should be before the arc for *b*, then the comparator must return a number less than zero; if the arc for *a* should be after the arc for *b*, then the comparator must return a number greater than zero; returning zero means that the relative order of *a* and *b* is unspecified. For example, to sort arcs by their associated name: + +```js +pie.sort(function(a, b) { return a.name.localeCompare(b.name); }); +``` + +Sorting does not affect the order of the [generated arc array](#_pie) which is always in the same order as the input data array; it merely affects the computed angles of each arc. The first arc starts at the [start angle](#pie_startAngle) and the last arc ends at the [end angle](#pie_endAngle). + +# pie.sortValues([compare]) [<>](https://github.com/d3/d3-shape/blob/master/src/pie.js#L58 "Source") + +If *compare* is specified, sets the value comparator to the specified function and returns this pie generator. If *compare* is not specified, returns the current value comparator, which defaults to descending value. The default value comparator is implemented as: + +```js +function compare(a, b) { + return b - a; +} +``` + +If both the data comparator and the value comparator are null, then arcs are positioned in the original input order. Otherwise, the data is sorted according to the data comparator, and the resulting order is used. Setting the value comparator implicitly sets the [data comparator](#pie_sort) to null. + +The value comparator is similar to the [data comparator](#pie_sort), except the two arguments *a* and *b* are values derived from the input data array using the [value accessor](#pie_value), not the data elements. If the arc for *a* should be before the arc for *b*, then the comparator must return a number less than zero; if the arc for *a* should be after the arc for *b*, then the comparator must return a number greater than zero; returning zero means that the relative order of *a* and *b* is unspecified. For example, to sort arcs by ascending value: + +```js +pie.sortValues(function(a, b) { return a - b; }); +``` + +Sorting does not affect the order of the [generated arc array](#_pie) which is always in the same order as the input data array; it merely affects the computed angles of each arc. The first arc starts at the [start angle](#pie_startAngle) and the last arc ends at the [end angle](#pie_endAngle). + +# pie.startAngle([angle]) [<>](https://github.com/d3/d3-shape/blob/master/src/pie.js#L66 "Source") + +If *angle* is specified, sets the overall start angle of the pie to the specified function or number and returns this pie generator. If *angle* is not specified, returns the current start angle accessor, which defaults to: + +```js +function startAngle() { + return 0; +} +``` + +The start angle here means the *overall* start angle of the pie, *i.e.*, the start angle of the first arc. The start angle accessor is invoked once, being passed the same arguments and `this` context as the [pie generator](#_pie). The units of *angle* are arbitrary, but if you plan to use the pie generator in conjunction with an [arc generator](#arcs), you should specify an angle in radians, with 0 at -*y* (12 o’clock) and positive angles proceeding clockwise. + +# pie.endAngle([angle]) [<>](https://github.com/d3/d3-shape/blob/master/src/pie.js#L70 "Source") + +If *angle* is specified, sets the overall end angle of the pie to the specified function or number and returns this pie generator. If *angle* is not specified, returns the current end angle accessor, which defaults to: + +```js +function endAngle() { + return 2 * Math.PI; +} +``` + +The end angle here means the *overall* end angle of the pie, *i.e.*, the end angle of the last arc. The end angle accessor is invoked once, being passed the same arguments and `this` context as the [pie generator](#_pie). The units of *angle* are arbitrary, but if you plan to use the pie generator in conjunction with an [arc generator](#arcs), you should specify an angle in radians, with 0 at -*y* (12 o’clock) and positive angles proceeding clockwise. + +The value of the end angle is constrained to [startAngle](#pie_startAngle) ± τ, such that |endAngle - startAngle| ≤ τ. + +# pie.padAngle([angle]) [<>](https://github.com/d3/d3-shape/blob/master/src/pie.js#L74 "Source") + +If *angle* is specified, sets the pad angle to the specified function or number and returns this pie generator. If *angle* is not specified, returns the current pad angle accessor, which defaults to: + +```js +function padAngle() { + return 0; +} +``` + +The pad angle here means the angular separation between each adjacent arc. The total amount of padding reserved is the specified *angle* times the number of elements in the input data array, and at most |endAngle - startAngle|; the remaining space is then divided proportionally by [value](#pie_value) such that the relative area of each arc is preserved. See the [pie padding animation](http://bl.ocks.org/mbostock/3e961b4c97a1b543fff2) for illustration. The pad angle accessor is invoked once, being passed the same arguments and `this` context as the [pie generator](#_pie). The units of *angle* are arbitrary, but if you plan to use the pie generator in conjunction with an [arc generator](#arcs), you should specify an angle in radians. + +### Lines + +[Line Chart](http://bl.ocks.org/mbostock/1550e57e12e73b86ad9e) + +The line generator produces a [spline](https://en.wikipedia.org/wiki/Spline_\(mathematics\)) or [polyline](https://en.wikipedia.org/wiki/Polygonal_chain), as in a line chart. Lines also appear in many other visualization types, such as the links in [hierarchical edge bundling](http://bl.ocks.org/mbostock/7607999). + +# d3.line() [<>](https://github.com/d3/d3-shape/blob/master/src/line.js "Source") + +Constructs a new line generator with the default settings. + +# line(data) [<>](https://github.com/d3/d3-shape/blob/master/src/line.js#L14 "Source") + +Generates a line for the given array of *data*. Depending on this line generator’s associated [curve](#line_curve), the given input *data* may need to be sorted by *x*-value before being passed to the line generator. If the line generator has a [context](#line_context), then the line is rendered to this context as a sequence of [path method](http://www.w3.org/TR/2dcontext/#canvaspathmethods) calls and this function returns void. Otherwise, a [path data](http://www.w3.org/TR/SVG/paths.html#PathData) string is returned. + +# line.x([x]) [<>](https://github.com/d3/d3-shape/blob/master/src/line.js#L34 "Source") + +If *x* is specified, sets the x accessor to the specified function or number and returns this line generator. If *x* is not specified, returns the current x accessor, which defaults to: + +```js +function x(d) { + return d[0]; +} +``` + +When a line is [generated](#_line), the x accessor will be invoked for each [defined](#line_defined) element in the input data array, being passed the element `d`, the index `i`, and the array `data` as three arguments. The default x accessor assumes that the input data are two-element arrays of numbers. If your data are in a different format, or if you wish to transform the data before rendering, then you should specify a custom accessor. For example, if `x` is a [time scale](https://github.com/d3/d3-scale#time-scales) and `y` is a [linear scale](https://github.com/d3/d3-scale#linear-scales): + +```js +var data = [ + {date: new Date(2007, 3, 24), value: 93.24}, + {date: new Date(2007, 3, 25), value: 95.35}, + {date: new Date(2007, 3, 26), value: 98.84}, + {date: new Date(2007, 3, 27), value: 99.92}, + {date: new Date(2007, 3, 30), value: 99.80}, + {date: new Date(2007, 4, 1), value: 99.47}, + … +]; + +var line = d3.line() + .x(function(d) { return x(d.date); }) + .y(function(d) { return y(d.value); }); +``` + +# line.y([y]) [<>](https://github.com/d3/d3-shape/blob/master/src/line.js#L38 "Source") + +If *y* is specified, sets the y accessor to the specified function or number and returns this line generator. If *y* is not specified, returns the current y accessor, which defaults to: + +```js +function y(d) { + return d[1]; +} +``` + +When a line is [generated](#_line), the y accessor will be invoked for each [defined](#line_defined) element in the input data array, being passed the element `d`, the index `i`, and the array `data` as three arguments. The default y accessor assumes that the input data are two-element arrays of numbers. See [*line*.x](#line_x) for more information. + +# line.defined([defined]) [<>](https://github.com/d3/d3-shape/blob/master/src/line.js#L42 "Source") + +If *defined* is specified, sets the defined accessor to the specified function or boolean and returns this line generator. If *defined* is not specified, returns the current defined accessor, which defaults to: + +```js +function defined() { + return true; +} +``` + +The default accessor thus assumes that the input data is always defined. When a line is [generated](#_line), the defined accessor will be invoked for each element in the input data array, being passed the element `d`, the index `i`, and the array `data` as three arguments. If the given element is defined (*i.e.*, if the defined accessor returns a truthy value for this element), the [x](#line_x) and [y](#line_y) accessors will subsequently be evaluated and the point will be added to the current line segment. Otherwise, the element will be skipped, the current line segment will be ended, and a new line segment will be generated for the next defined point. As a result, the generated line may have several discrete segments. For example: + +[Line with Missing Data](http://bl.ocks.org/mbostock/0533f44f2cfabecc5e3a) + +Note that if a line segment consists of only a single point, it may appear invisible unless rendered with rounded or square [line caps](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-linecap). In addition, some curves such as [curveCardinalOpen](#curveCardinalOpen) only render a visible segment if it contains multiple points. + +# line.curve([curve]) [<>](https://github.com/d3/d3-shape/blob/master/src/line.js#L46 "Source") + +If *curve* is specified, sets the [curve factory](#curves) and returns this line generator. If *curve* is not specified, returns the current curve factory, which defaults to [curveLinear](#curveLinear). + +# line.context([context]) [<>](https://github.com/d3/d3-shape/blob/master/src/line.js#L50 "Source") + +If *context* is specified, sets the context and returns this line generator. If *context* is not specified, returns the current context, which defaults to null. If the context is not null, then the [generated line](#_line) is rendered to this context as a sequence of [path method](http://www.w3.org/TR/2dcontext/#canvaspathmethods) calls. Otherwise, a [path data](http://www.w3.org/TR/SVG/paths.html#PathData) string representing the generated line is returned. + +# d3.lineRadial() [<>](https://github.com/d3/d3-shape/blob/master/src/lineRadial.js "Source") + +Radial Line + +Constructs a new radial line generator with the default settings. A radial line generator is equivalent to the standard Cartesian [line generator](#line), except the [x](#line_x) and [y](#line_y) accessors are replaced with [angle](#lineRadial_angle) and [radius](#lineRadial_radius) accessors. Radial lines are always positioned relative to ⟨0,0⟩; use a transform (see: [SVG](http://www.w3.org/TR/SVG/coords.html#TransformAttribute), [Canvas](http://www.w3.org/TR/2dcontext/#transformations)) to change the origin. + +# lineRadial(data) [<>](https://github.com/d3/d3-shape/blob/master/src/lineRadial.js#L4 "Source") + +Equivalent to [*line*](#_line). + +# lineRadial.angle([angle]) [<>](https://github.com/d3/d3-shape/blob/master/src/lineRadial.js#L7 "Source") + +Equivalent to [*line*.x](#line_x), except the accessor returns the angle in radians, with 0 at -*y* (12 o’clock). + +# lineRadial.radius([radius]) [<>](https://github.com/d3/d3-shape/blob/master/src/lineRadial.js#L8 "Source") + +Equivalent to [*line*.y](#line_y), except the accessor returns the radius: the distance from the origin ⟨0,0⟩. + +# lineRadial.defined([defined]) + +Equivalent to [*line*.defined](#line_defined). + +# lineRadial.curve([curve]) [<>](https://github.com/d3/d3-shape/blob/master/src/lineRadial.js#L10 "Source") + +Equivalent to [*line*.curve](#line_curve). Note that [curveMonotoneX](#curveMonotoneX) or [curveMonotoneY](#curveMonotoneY) are not recommended for radial lines because they assume that the data is monotonic in *x* or *y*, which is typically untrue of radial lines. + +# lineRadial.context([context]) + +Equivalent to [*line*.context](#line_context). + +### Areas + +[Area Chart](http://bl.ocks.org/mbostock/3883195)[Stacked Area Chart](http://bl.ocks.org/mbostock/3885211)[Difference Chart](http://bl.ocks.org/mbostock/3894205) + +The area generator produces an area, as in an area chart. An area is defined by two bounding [lines](#lines), either splines or polylines. Typically, the two lines share the same [*x*-values](#area_x) ([x0](#area_x0) = [x1](#area_x1)), differing only in *y*-value ([y0](#area_y0) and [y1](#area_y1)); most commonly, y0 is defined as a constant representing [zero](http://www.vox.com/2015/11/19/9758062/y-axis-zero-chart). The first line (the topline) is defined by x1 and y1 and is rendered first; the second line (the baseline) is defined by x0 and y0 and is rendered second, with the points in reverse order. With a [curveLinear](#curveLinear) [curve](#area_curve), this produces a clockwise polygon. + +# d3.area() [<>](https://github.com/d3/d3-shape/blob/master/src/area.js "Source") + +Constructs a new area generator with the default settings. + +# area(data) [<>](https://github.com/d3/d3-shape/blob/master/src/area.js#L17 "Source") + +Generates an area for the given array of *data*. Depending on this area generator’s associated [curve](#area_curve), the given input *data* may need to be sorted by *x*-value before being passed to the area generator. If the area generator has a [context](#line_context), then the area is rendered to this context as a sequence of [path method](http://www.w3.org/TR/2dcontext/#canvaspathmethods) calls and this function returns void. Otherwise, a [path data](http://www.w3.org/TR/SVG/paths.html#PathData) string is returned. + +# area.x([x]) [<>](https://github.com/d3/d3-shape/blob/master/src/area.js#L59 "Source") + +If *x* is specified, sets [x0](#area_x0) to *x* and [x1](#area_x1) to null and returns this area generator. If *x* is not specified, returns the current x0 accessor. + +# area.x0([x]) [<>](https://github.com/d3/d3-shape/blob/master/src/area.js#L63 "Source") + +If *x* is specified, sets the x0 accessor to the specified function or number and returns this area generator. If *x* is not specified, returns the current x0 accessor, which defaults to: + +```js +function x(d) { + return d[0]; +} +``` + +When an area is [generated](#_area), the x0 accessor will be invoked for each [defined](#area_defined) element in the input data array, being passed the element `d`, the index `i`, and the array `data` as three arguments. The default x0 accessor assumes that the input data are two-element arrays of numbers. If your data are in a different format, or if you wish to transform the data before rendering, then you should specify a custom accessor. For example, if `x` is a [time scale](https://github.com/d3/d3-scale#time-scales) and `y` is a [linear scale](https://github.com/d3/d3-scale#linear-scales): + +```js +var data = [ + {date: new Date(2007, 3, 24), value: 93.24}, + {date: new Date(2007, 3, 25), value: 95.35}, + {date: new Date(2007, 3, 26), value: 98.84}, + {date: new Date(2007, 3, 27), value: 99.92}, + {date: new Date(2007, 3, 30), value: 99.80}, + {date: new Date(2007, 4, 1), value: 99.47}, + … +]; + +var area = d3.area() + .x(function(d) { return x(d.date); }) + .y1(function(d) { return y(d.value); }) + .y0(y(0)); +``` + +# area.x1([x]) [<>](https://github.com/d3/d3-shape/blob/master/src/area.js#L67 "Source") + +If *x* is specified, sets the x1 accessor to the specified function or number and returns this area generator. If *x* is not specified, returns the current x1 accessor, which defaults to null, indicating that the previously-computed [x0](#area_x0) value should be reused for the x1 value. + +When an area is [generated](#_area), the x1 accessor will be invoked for each [defined](#area_defined) element in the input data array, being passed the element `d`, the index `i`, and the array `data` as three arguments. See [*area*.x0](#area_x0) for more information. + +# area.y([y]) [<>](https://github.com/d3/d3-shape/blob/master/src/area.js#L71 "Source") + +If *y* is specified, sets [y0](#area_y0) to *y* and [y1](#area_y1) to null and returns this area generator. If *y* is not specified, returns the current y0 accessor. + +# area.y0([y]) [<>](https://github.com/d3/d3-shape/blob/master/src/area.js#L75 "Source") + +If *y* is specified, sets the y0 accessor to the specified function or number and returns this area generator. If *y* is not specified, returns the current y0 accessor, which defaults to: + +```js +function y() { + return 0; +} +``` + +When an area is [generated](#_area), the y0 accessor will be invoked for each [defined](#area_defined) element in the input data array, being passed the element `d`, the index `i`, and the array `data` as three arguments. See [*area*.x0](#area_x0) for more information. + +# area.y1([y]) [<>](https://github.com/d3/d3-shape/blob/master/src/area.js#L79 "Source") + +If *y* is specified, sets the y1 accessor to the specified function or number and returns this area generator. If *y* is not specified, returns the current y1 accessor, which defaults to: + +```js +function y(d) { + return d[1]; +} +``` + +A null accessor is also allowed, indicating that the previously-computed [y0](#area_y0) value should be reused for the y1 value. When an area is [generated](#_area), the y1 accessor will be invoked for each [defined](#area_defined) element in the input data array, being passed the element `d`, the index `i`, and the array `data` as three arguments. See [*area*.x0](#area_x0) for more information. + +# area.defined([defined]) [<>](https://github.com/d3/d3-shape/blob/master/src/area.js#L96 "Source") + +If *defined* is specified, sets the defined accessor to the specified function or boolean and returns this area generator. If *defined* is not specified, returns the current defined accessor, which defaults to: + +```js +function defined() { + return true; +} +``` + +The default accessor thus assumes that the input data is always defined. When an area is [generated](#_area), the defined accessor will be invoked for each element in the input data array, being passed the element `d`, the index `i`, and the array `data` as three arguments. If the given element is defined (*i.e.*, if the defined accessor returns a truthy value for this element), the [x0](#area_x0), [x1](#area_x1), [y0](#area_y0) and [y1](#area_y1) accessors will subsequently be evaluated and the point will be added to the current area segment. Otherwise, the element will be skipped, the current area segment will be ended, and a new area segment will be generated for the next defined point. As a result, the generated area may have several discrete segments. For example: + +[Area with Missing Data](http://bl.ocks.org/mbostock/3035090) + +Note that if an area segment consists of only a single point, it may appear invisible unless rendered with rounded or square [line caps](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-linecap). In addition, some curves such as [curveCardinalOpen](#curveCardinalOpen) only render a visible segment if it contains multiple points. + +# area.curve([curve]) [<>](https://github.com/d3/d3-shape/blob/master/src/area.js#L100 "Source") + +If *curve* is specified, sets the [curve factory](#curves) and returns this area generator. If *curve* is not specified, returns the current curve factory, which defaults to [curveLinear](#curveLinear). + +# area.context([context]) [<>](https://github.com/d3/d3-shape/blob/master/src/area.js#L104 "Source") + +If *context* is specified, sets the context and returns this area generator. If *context* is not specified, returns the current context, which defaults to null. If the context is not null, then the [generated area](#_area) is rendered to this context as a sequence of [path method](http://www.w3.org/TR/2dcontext/#canvaspathmethods) calls. Otherwise, a [path data](http://www.w3.org/TR/SVG/paths.html#PathData) string representing the generated area is returned. + +# area.lineX0() [<>](https://github.com/d3/d3-shape/blob/master/src/area.js#L83 "Source") +
# area.lineY0() [<>](https://github.com/d3/d3-shape/blob/master/src/area.js#L84 "Source") + +Returns a new [line generator](#lines) that has this area generator’s current [defined accessor](#area_defined), [curve](#area_curve) and [context](#area_context). The line’s [*x*-accessor](#line_x) is this area’s [*x0*-accessor](#area_x0), and the line’s [*y*-accessor](#line_y) is this area’s [*y0*-accessor](#area_y0). + +# area.lineX1() [<>](https://github.com/d3/d3-shape/blob/master/src/area.js#L92 "Source") + +Returns a new [line generator](#lines) that has this area generator’s current [defined accessor](#area_defined), [curve](#area_curve) and [context](#area_context). The line’s [*x*-accessor](#line_x) is this area’s [*x1*-accessor](#area_x1), and the line’s [*y*-accessor](#line_y) is this area’s [*y0*-accessor](#area_y0). + +# area.lineY1() [<>](https://github.com/d3/d3-shape/blob/master/src/area.js#L88 "Source") + +Returns a new [line generator](#lines) that has this area generator’s current [defined accessor](#area_defined), [curve](#area_curve) and [context](#area_context). The line’s [*x*-accessor](#line_x) is this area’s [*x0*-accessor](#area_x0), and the line’s [*y*-accessor](#line_y) is this area’s [*y1*-accessor](#area_y1). + +# d3.areaRadial() [<>](https://github.com/d3/d3-shape/blob/master/src/areaRadial.js "Source") + +Radial Area + +Constructs a new radial area generator with the default settings. A radial area generator is equivalent to the standard Cartesian [area generator](#area), except the [x](#area_x) and [y](#area_y) accessors are replaced with [angle](#areaRadial_angle) and [radius](#areaRadial_radius) accessors. Radial areas are always positioned relative to ⟨0,0⟩; use a transform (see: [SVG](http://www.w3.org/TR/SVG/coords.html#TransformAttribute), [Canvas](http://www.w3.org/TR/2dcontext/#transformations)) to change the origin. + +# areaRadial(data) + +Equivalent to [*area*](#_area). + +# areaRadial.angle([angle]) [<>](https://github.com/d3/d3-shape/blob/master/src/areaRadial.js#L13 "Source") + +Equivalent to [*area*.x](#area_x), except the accessor returns the angle in radians, with 0 at -*y* (12 o’clock). + +# areaRadial.startAngle([angle]) [<>](https://github.com/d3/d3-shape/blob/master/src/areaRadial.js#L14 "Source") + +Equivalent to [*area*.x0](#area_x0), except the accessor returns the angle in radians, with 0 at -*y* (12 o’clock). Note: typically [angle](#areaRadial_angle) is used instead of setting separate start and end angles. + +# areaRadial.endAngle([angle]) [<>](https://github.com/d3/d3-shape/blob/master/src/areaRadial.js#L15 "Source") + +Equivalent to [*area*.x1](#area_x1), except the accessor returns the angle in radians, with 0 at -*y* (12 o’clock). Note: typically [angle](#areaRadial_angle) is used instead of setting separate start and end angles. + +# areaRadial.radius([radius]) [<>](https://github.com/d3/d3-shape/blob/master/src/areaRadial.js#L16 "Source") + +Equivalent to [*area*.y](#area_y), except the accessor returns the radius: the distance from the origin ⟨0,0⟩. + +# areaRadial.innerRadius([radius]) [<>](https://github.com/d3/d3-shape/blob/master/src/areaRadial.js#L17 "Source") + +Equivalent to [*area*.y0](#area_y0), except the accessor returns the radius: the distance from the origin ⟨0,0⟩. + +# areaRadial.outerRadius([radius]) [<>](https://github.com/d3/d3-shape/blob/master/src/areaRadial.js#L18 "Source") + +Equivalent to [*area*.y1](#area_y1), except the accessor returns the radius: the distance from the origin ⟨0,0⟩. + +# areaRadial.defined([defined]) + +Equivalent to [*area*.defined](#area_defined). + +# areaRadial.curve([curve]) [<>](https://github.com/d3/d3-shape/blob/master/src/areaRadial.js#L24 "Source") + +Equivalent to [*area*.curve](#area_curve). Note that [curveMonotoneX](#curveMonotoneX) or [curveMonotoneY](#curveMonotoneY) are not recommended for radial areas because they assume that the data is monotonic in *x* or *y*, which is typically untrue of radial areas. + +# areaRadial.context([context]) + +Equivalent to [*line*.context](#line_context). + +# areaRadial.lineStartAngle() [<>](https://github.com/d3/d3-shape/blob/master/src/areaRadial.js#L19 "Source") +
# areaRadial.lineInnerRadius() [<>](https://github.com/d3/d3-shape/blob/master/src/areaRadial.js#L21 "Source") + +Returns a new [radial line generator](#lineRadial) that has this radial area generator’s current [defined accessor](#areaRadial_defined), [curve](#areaRadial_curve) and [context](#areaRadial_context). The line’s [angle accessor](#lineRadial_angle) is this area’s [start angle accessor](#areaRadial_startAngle), and the line’s [radius accessor](#lineRadial_radius) is this area’s [inner radius accessor](#areaRadial_innerRadius). + +# areaRadial.lineEndAngle() [<>](https://github.com/d3/d3-shape/blob/master/src/areaRadial.js#L20 "Source") + +Returns a new [radial line generator](#lineRadial) that has this radial area generator’s current [defined accessor](#areaRadial_defined), [curve](#areaRadial_curve) and [context](#areaRadial_context). The line’s [angle accessor](#lineRadial_angle) is this area’s [end angle accessor](#areaRadial_endAngle), and the line’s [radius accessor](#lineRadial_radius) is this area’s [inner radius accessor](#areaRadial_innerRadius). + +# areaRadial.lineOuterRadius() [<>](https://github.com/d3/d3-shape/blob/master/src/areaRadial.js#L22 "Source") + +Returns a new [radial line generator](#lineRadial) that has this radial area generator’s current [defined accessor](#areaRadial_defined), [curve](#areaRadial_curve) and [context](#areaRadial_context). The line’s [angle accessor](#lineRadial_angle) is this area’s [start angle accessor](#areaRadial_startAngle), and the line’s [radius accessor](#lineRadial_radius) is this area’s [outer radius accessor](#areaRadial_outerRadius). + +### Curves + +While [lines](#lines) are defined as a sequence of two-dimensional [*x*, *y*] points, and [areas](#areas) are similarly defined by a topline and a baseline, there remains the task of transforming this discrete representation into a continuous shape: *i.e.*, how to interpolate between the points. A variety of curves are provided for this purpose. + +Curves are typically not constructed or used directly, instead being passed to [*line*.curve](#line_curve) and [*area*.curve](#area_curve). For example: + +```js +var line = d3.line() + .x(function(d) { return x(d.date); }) + .y(function(d) { return y(d.value); }) + .curve(d3.curveCatmullRom.alpha(0.5)); +``` + +# d3.curveBasis(context) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/basis.js#L12 "Source") + +basis + +Produces a cubic [basis spline](https://en.wikipedia.org/wiki/B-spline) using the specified control points. The first and last points are triplicated such that the spline starts at the first point and ends at the last point, and is tangent to the line between the first and second points, and to the line between the penultimate and last points. + +# d3.curveBasisClosed(context) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/basisClosed.js "Source") + +basisClosed + +Produces a closed cubic [basis spline](https://en.wikipedia.org/wiki/B-spline) using the specified control points. When a line segment ends, the first three control points are repeated, producing a closed loop with C2 continuity. + +# d3.curveBasisOpen(context) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/basisOpen.js "Source") + +basisOpen + +Produces a cubic [basis spline](https://en.wikipedia.org/wiki/B-spline) using the specified control points. Unlike [basis](#basis), the first and last points are not repeated, and thus the curve typically does not intersect these points. + +# d3.curveBundle(context) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/bundle.js "Source") + +bundle + +Produces a straightened cubic [basis spline](https://en.wikipedia.org/wiki/B-spline) using the specified control points, with the spline straightened according to the curve’s [*beta*](#curveBundle_beta), which defaults to 0.85. This curve is typically used in [hierarchical edge bundling](http://bl.ocks.org/mbostock/7607999) to disambiguate connections, as proposed by [Danny Holten](https://www.win.tue.nl/vis1/home/dholten/) in [Hierarchical Edge Bundles: Visualization of Adjacency Relations in Hierarchical Data](https://www.win.tue.nl/vis1/home/dholten/papers/bundles_infovis.pdf). This curve does not implement [*curve*.areaStart](#curve_areaStart) and [*curve*.areaEnd](#curve_areaEnd); it is intended to work with [d3.line](#lines), not [d3.area](#areas). + +# bundle.beta(beta) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/bundle.js#L51 "Source") + +Returns a bundle curve with the specified *beta* in the range [0, 1], representing the bundle strength. If *beta* equals zero, a straight line between the first and last point is produced; if *beta* equals one, a standard [basis](#basis) spline is produced. For example: + +```js +var line = d3.line().curve(d3.curveBundle.beta(0.5)); +``` + +# d3.curveCardinal(context) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/cardinal.js "Source") + +cardinal + +Produces a cubic [cardinal spline](https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Cardinal_spline) using the specified control points, with one-sided differences used for the first and last piece. The default [tension](#curveCardinal_tension) is 0. + +# d3.curveCardinalClosed(context) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/cardinalClosed.js "Source") + +cardinalClosed + +Produces a closed cubic [cardinal spline](https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Cardinal_spline) using the specified control points. When a line segment ends, the first three control points are repeated, producing a closed loop. The default [tension](#curveCardinal_tension) is 0. + +# d3.curveCardinalOpen(context) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/cardinalOpen.js "Source") + +cardinalOpen + +Produces a cubic [cardinal spline](https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Cardinal_spline) using the specified control points. Unlike [curveCardinal](#curveCardinal), one-sided differences are not used for the first and last piece, and thus the curve starts at the second point and ends at the penultimate point. The default [tension](#curveCardinal_tension) is 0. + +# cardinal.tension(tension) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/cardinalOpen.js#L44 "Source") + +Returns a cardinal curve with the specified *tension* in the range [0, 1]. The *tension* determines the length of the tangents: a *tension* of one yields all zero tangents, equivalent to [curveLinear](#curveLinear); a *tension* of zero produces a uniform [Catmull–Rom](#curveCatmullRom) spline. For example: + +```js +var line = d3.line().curve(d3.curveCardinal.tension(0.5)); +``` + +# d3.curveCatmullRom(context) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/catmullRom.js "Source") + +catmullRom + +Produces a cubic Catmull–Rom spline using the specified control points and the parameter [*alpha*](#catmullRom_alpha), which defaults to 0.5, as proposed by Yuksel et al. in [On the Parameterization of Catmull–Rom Curves](http://www.cemyuksel.com/research/catmullrom_param/), with one-sided differences used for the first and last piece. + +# d3.curveCatmullRomClosed(context) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/catmullRomClosed.js "Source") + +catmullRomClosed + +Produces a closed cubic Catmull–Rom spline using the specified control points and the parameter [*alpha*](#catmullRom_alpha), which defaults to 0.5, as proposed by Yuksel et al. When a line segment ends, the first three control points are repeated, producing a closed loop. + +# d3.curveCatmullRomOpen(context) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/catmullRomOpen.js "Source") + +catmullRomOpen + +Produces a cubic Catmull–Rom spline using the specified control points and the parameter [*alpha*](#catmullRom_alpha), which defaults to 0.5, as proposed by Yuksel et al. Unlike [curveCatmullRom](#curveCatmullRom), one-sided differences are not used for the first and last piece, and thus the curve starts at the second point and ends at the penultimate point. + +# catmullRom.alpha(alpha) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/catmullRom.js#L83 "Source") + +Returns a cubic Catmull–Rom curve with the specified *alpha* in the range [0, 1]. If *alpha* is zero, produces a uniform spline, equivalent to [curveCardinal](#curveCardinal) with a tension of zero; if *alpha* is one, produces a chordal spline; if *alpha* is 0.5, produces a [centripetal spline](https://en.wikipedia.org/wiki/Centripetal_Catmull–Rom_spline). Centripetal splines are recommended to avoid self-intersections and overshoot. For example: + +```js +var line = d3.line().curve(d3.curveCatmullRom.alpha(0.5)); +``` + +# d3.curveLinear(context) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/linear.js "Source") + +linear + +Produces a polyline through the specified points. + +# d3.curveLinearClosed(context) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/linearClosed.js "Source") + +linearClosed + +Produces a closed polyline through the specified points by repeating the first point when the line segment ends. + +# d3.curveMonotoneX(context) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/monotone.js#L98 "Source") + +monotoneX + +Produces a cubic spline that [preserves monotonicity](https://en.wikipedia.org/wiki/Monotone_cubic_interpolation) in *y*, assuming monotonicity in *x*, as proposed by Steffen in [A simple method for monotonic interpolation in one dimension](http://adsabs.harvard.edu/full/1990A%26A...239..443S): “a smooth curve with continuous first-order derivatives that passes through any given set of data points without spurious oscillations. Local extrema can occur only at grid points where they are given by the data, but not in between two adjacent grid points.” + +# d3.curveMonotoneY(context) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/monotone.js#L102 "Source") + +monotoneY + +Produces a cubic spline that [preserves monotonicity](https://en.wikipedia.org/wiki/Monotone_cubic_interpolation) in *x*, assuming monotonicity in *y*, as proposed by Steffen in [A simple method for monotonic interpolation in one dimension](http://adsabs.harvard.edu/full/1990A%26A...239..443S): “a smooth curve with continuous first-order derivatives that passes through any given set of data points without spurious oscillations. Local extrema can occur only at grid points where they are given by the data, but not in between two adjacent grid points.” + +# d3.curveNatural(context) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/natural.js "Source") + +natural + +Produces a [natural](https://en.wikipedia.org/wiki/Spline_interpolation) [cubic spline](http://mathworld.wolfram.com/CubicSpline.html) with the second derivative of the spline set to zero at the endpoints. + +# d3.curveStep(context) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/step.js "Source") + +step + +Produces a piecewise constant function (a [step function](https://en.wikipedia.org/wiki/Step_function)) consisting of alternating horizontal and vertical lines. The *y*-value changes at the midpoint of each pair of adjacent *x*-values. + +# d3.curveStepAfter(context) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/step.js#L51 "Source") + +stepAfter + +Produces a piecewise constant function (a [step function](https://en.wikipedia.org/wiki/Step_function)) consisting of alternating horizontal and vertical lines. The *y*-value changes after the *x*-value. + +# d3.curveStepBefore(context) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/step.js#L47 "Source") + +stepBefore + +Produces a piecewise constant function (a [step function](https://en.wikipedia.org/wiki/Step_function)) consisting of alternating horizontal and vertical lines. The *y*-value changes before the *x*-value. + +### Custom Curves + +Curves are typically not used directly, instead being passed to [*line*.curve](#line_curve) and [*area*.curve](#area_curve). However, you can define your own curve implementation should none of the built-in curves satisfy your needs using the following interface. You can also use this low-level interface with a built-in curve type as an alternative to the line and area generators. + +# curve.areaStart() [<>](https://github.com/d3/d3-shape/blob/master/src/curve/step.js#L7 "Source") + +Indicates the start of a new area segment. Each area segment consists of exactly two [line segments](#curve_lineStart): the topline, followed by the baseline, with the baseline points in reverse order. + +# curve.areaEnd() [<>](https://github.com/d3/d3-shape/blob/master/src/curve/step.js#L10 "Source") + +Indicates the end of the current area segment. + +# curve.lineStart() [<>](https://github.com/d3/d3-shape/blob/master/src/curve/step.js#L13 "Source") + +Indicates the start of a new line segment. Zero or more [points](#curve_point) will follow. + +# curve.lineEnd() [<>](https://github.com/d3/d3-shape/blob/master/src/curve/step.js#L17 "Source") + +Indicates the end of the current line segment. + +# curve.point(x, y) [<>](https://github.com/d3/d3-shape/blob/master/src/curve/step.js#L22 "Source") + +Indicates a new point in the current line segment with the given *x*- and *y*-values. + +### Links + +[Tidy Tree](http://bl.ocks.org/mbostock/9d0899acb5d3b8d839d9d613a9e1fe04) + +The **link** shape generates a smooth cubic Bézier curve from a source point to a target point. The tangents of the curve at the start and end are either [vertical](#linkVertical), [horizontal](#linkHorizontal) or [radial](#linkRadial). + +# d3.linkVertical() [<>](https://github.com/d3/d3-shape/blob/master/src/link/index.js#L74 "Source") + +Returns a new [link generator](#_link) with vertical tangents. For example, to visualize [links](https://github.com/d3/d3-hierarchy/blob/master/README.md#node_links) in a [tree diagram](https://github.com/d3/d3-hierarchy/blob/master/README.md#tree) rooted on the top edge of the display, you might say: + +```js +var link = d3.linkVertical() + .x(function(d) { return d.x; }) + .y(function(d) { return d.y; }); +``` + +# d3.linkHorizontal() [<>](https://github.com/d3/d3-shape/blob/master/src/link/index.js#L70 "Source") + +Returns a new [link generator](#_link) with horizontal tangents. For example, to visualize [links](https://github.com/d3/d3-hierarchy/blob/master/README.md#node_links) in a [tree diagram](https://github.com/d3/d3-hierarchy/blob/master/README.md#tree) rooted on the left edge of the display, you might say: + +```js +var link = d3.linkHorizontal() + .x(function(d) { return d.y; }) + .y(function(d) { return d.x; }); +``` + +# link(arguments…) [<>](https://github.com/d3/d3-shape/blob/master/src/link/index.js#L21 "Source") + +Generates a link for the given *arguments*. The *arguments* are arbitrary; they are simply propagated to the link generator’s accessor functions along with the `this` object. For example, with the default settings, an object expected: + +```js +link({ + source: [100, 100], + target: [300, 300] +}); +``` + +# link.source([source]) [<>](https://github.com/d3/d3-shape/blob/master/src/link/index.js#L28 "Source") + +If *source* is specified, sets the source accessor to the specified function and returns this link generator. If *source* is not specified, returns the current source accessor, which defaults to: + +```js +function source(d) { + return d.source; +} +``` + +# link.target([target]) [<>](https://github.com/d3/d3-shape/blob/master/src/link/index.js#L32 "Source") + +If *target* is specified, sets the target accessor to the specified function and returns this link generator. If *target* is not specified, returns the current target accessor, which defaults to: + +```js +function target(d) { + return d.target; +} +``` + +# link.x([x]) [<>](https://github.com/d3/d3-shape/blob/master/src/link/index.js#L36 "Source") + +If *x* is specified, sets the *x*-accessor to the specified function or number and returns this link generator. If *x* is not specified, returns the current *x*-accessor, which defaults to: + +```js +function x(d) { + return d[0]; +} +``` + +# link.y([y]) [<>](https://github.com/d3/d3-shape/blob/master/src/link/index.js#L40 "Source") + +If *y* is specified, sets the *y*-accessor to the specified function or number and returns this link generator. If *y* is not specified, returns the current *y*-accessor, which defaults to: + +```js +function y(d) { + return d[1]; +} +``` + +# link.context([context]) [<>](https://github.com/d3/d3-shape/blob/master/src/link/index.js#L44 "Source") + +If *context* is specified, sets the context and returns this link generator. If *context* is not specified, returns the current context, which defaults to null. If the context is not null, then the [generated link](#_link) is rendered to this context as a sequence of [path method](http://www.w3.org/TR/2dcontext/#canvaspathmethods) calls. Otherwise, a [path data](http://www.w3.org/TR/SVG/paths.html#PathData) string representing the generated link is returned. See also [d3-path](https://github.com/d3/d3-path). + +# d3.linkRadial() [<>](https://github.com/d3/d3-shape/blob/master/src/link/index.js#L78 "Source") + +Returns a new [link generator](#_link) with radial tangents. For example, to visualize [links](https://github.com/d3/d3-hierarchy/blob/master/README.md#node_links) in a [tree diagram](https://github.com/d3/d3-hierarchy/blob/master/README.md#tree) rooted in the center of the display, you might say: + +```js +var link = d3.linkRadial() + .angle(function(d) { return d.x; }) + .radius(function(d) { return d.y; }); +``` + +# linkRadial.angle([angle]) [<>](https://github.com/d3/d3-shape/blob/master/src/link/index.js#L80 "Source") + +Equivalent to [*link*.x](#link_x), except the accessor returns the angle in radians, with 0 at -*y* (12 o’clock). + +# linkRadial.radius([radius]) [<>](https://github.com/d3/d3-shape/blob/master/src/link/index.js#L81 "Source") + +Equivalent to [*link*.y](#link_y), except the accessor returns the radius: the distance from the origin ⟨0,0⟩. + +### Symbols + + + +Symbols provide a categorical shape encoding as is commonly used in scatterplots. Symbols are always centered at ⟨0,0⟩; use a transform (see: [SVG](http://www.w3.org/TR/SVG/coords.html#TransformAttribute), [Canvas](http://www.w3.org/TR/2dcontext/#transformations)) to move the symbol to a different position. + +# d3.symbol() [<>](https://github.com/d3/d3-shape/blob/master/src/symbol.js "Source") + +Constructs a new symbol generator with the default settings. + +# symbol(arguments…) [<>](https://github.com/d3/d3-shape/blob/master/src/symbol.js#L11 "Source") + +Generates a symbol for the given *arguments*. The *arguments* are arbitrary; they are simply propagated to the symbol generator’s accessor functions along with the `this` object. For example, with the default settings, no arguments are needed to produce a circle with area 64 square pixels. If the symbol generator has a [context](#symbol_context), then the symbol is rendered to this context as a sequence of [path method](http://www.w3.org/TR/2dcontext/#canvaspathmethods) calls and this function returns void. Otherwise, a [path data](http://www.w3.org/TR/SVG/paths.html#PathData) string is returned. + +# symbol.type([type]) [<>](https://github.com/d3/d3-shape/blob/master/src/symbol.js#L33 "Source") + +If *type* is specified, sets the symbol type to the specified function or symbol type and returns this line generator. If *type* is not specified, returns the current symbol type accessor, which defaults to: + +```js +function type() { + return circle; +} +``` + +See [symbols](#symbols) for the set of built-in symbol types. To implement a custom symbol type, pass an object that implements [*symbolType*.draw](#symbolType_draw). + +# symbol.size([size]) [<>](https://github.com/d3/d3-shape/blob/master/src/symbol.js#L37 "Source") + +If *size* is specified, sets the size to the specified function or number and returns this symbol generator. If *size* is not specified, returns the current size accessor, which defaults to: + +```js +function size() { + return 64; +} +``` + +Specifying the size as a function is useful for constructing a scatterplot with a size encoding. If you wish to scale the symbol to fit a given bounding box, rather than by area, try [SVG’s getBBox](http://bl.ocks.org/mbostock/3dd515e692504c92ab65). + +# symbol.context([context]) [<>](https://github.com/d3/d3-shape/blob/master/src/symbol.js#L41 "Source") + +If *context* is specified, sets the context and returns this symbol generator. If *context* is not specified, returns the current context, which defaults to null. If the context is not null, then the [generated symbol](#_symbol) is rendered to this context as a sequence of [path method](http://www.w3.org/TR/2dcontext/#canvaspathmethods) calls. Otherwise, a [path data](http://www.w3.org/TR/SVG/paths.html#PathData) string representing the generated symbol is returned. + +# d3.symbols + +An array containing the set of all built-in symbol types: [circle](#symbolCircle), [cross](#symbolCross), [diamond](#symbolDiamond), [square](#symbolSquare), [star](#symbolStar), [triangle](#symbolTriangle), and [wye](#symbolWye). Useful for constructing the range of an [ordinal scale](https://github.com/d3/d3-scale#ordinal-scales) should you wish to use a shape encoding for categorical data. + +# d3.symbolCircle [<>](https://github.com/d3/d3-shape/blob/master/src/symbol/circle.js "Source") + +The circle symbol type. + +# d3.symbolCross [<>](https://github.com/d3/d3-shape/blob/master/src/symbol/cross.js "Source") + +The Greek cross symbol type, with arms of equal length. + +# d3.symbolDiamond [<>](https://github.com/d3/d3-shape/blob/master/src/symbol/diamond.js "Source") + +The rhombus symbol type. + +# d3.symbolSquare [<>](https://github.com/d3/d3-shape/blob/master/src/symbol/square.js "Source") + +The square symbol type. + +# d3.symbolStar [<>](https://github.com/d3/d3-shape/blob/master/src/symbol/star.js "Source") + +The pentagonal star (pentagram) symbol type. + +# d3.symbolTriangle [<>](https://github.com/d3/d3-shape/blob/master/src/symbol/triangle.js "Source") + +The up-pointing triangle symbol type. + +# d3.symbolWye [<>](https://github.com/d3/d3-shape/blob/master/src/symbol/wye.js "Source") + +The Y-shape symbol type. + +# d3.pointRadial(angle, radius) [<>](https://github.com/d3/d3-shape/blob/master/src/pointRadial.js "Source") + +Returns the point [x, y] for the given *angle* in radians, with 0 at -*y* (12 o’clock) and positive angles proceeding clockwise, and the given *radius*. + +### Custom Symbol Types + +Symbol types are typically not used directly, instead being passed to [*symbol*.type](#symbol_type). However, you can define your own symbol type implementation should none of the built-in types satisfy your needs using the following interface. You can also use this low-level interface with a built-in symbol type as an alternative to the symbol generator. + +# symbolType.draw(context, size) + +Renders this symbol type to the specified *context* with the specified *size* in square pixels. The *context* implements the [CanvasPathMethods](http://www.w3.org/TR/2dcontext/#canvaspathmethods) interface. (Note that this is a subset of the CanvasRenderingContext2D interface!) + +### Stacks + +[Stacked Bar Chart](http://bl.ocks.org/mbostock/3886208)[Streamgraph](http://bl.ocks.org/mbostock/4060954) + +Some shape types can be stacked, placing one shape adjacent to another. For example, a bar chart of monthly sales might be broken down into a multi-series bar chart by product category, stacking bars vertically. This is equivalent to subdividing a bar chart by an ordinal dimension (such as product category) and applying a color encoding. + +Stacked charts can show overall value and per-category value simultaneously; however, it is typically harder to compare across categories, as only the bottom layer of the stack is aligned. So, chose the [stack order](#stack_order) carefully, and consider a [streamgraph](#stackOffsetWiggle). (See also [grouped charts](http://bl.ocks.org/mbostock/3887051).) + +Like the [pie generator](#pies), the stack generator does not produce a shape directly. Instead it computes positions which you can then pass to an [area generator](#areas) or use directly, say to position bars. + +# d3.stack() [<>](https://github.com/d3/d3-shape/blob/master/src/stack.js "Source") + +Constructs a new stack generator with the default settings. + +# stack(data[, arguments…]) [<>](https://github.com/d3/d3-shape/blob/master/src/stack.js#L16 "Source") + +Generates a stack for the given array of *data*, returning an array representing each series. Any additional *arguments* are arbitrary; they are simply propagated to accessors along with the `this` object. + +The series are determined by the [keys accessor](#stack_keys); each series *i* in the returned array corresponds to the *i*th key. Each series is an array of points, where each point *j* corresponds to the *j*th element in the input *data*. Lastly, each point is represented as an array [*y0*, *y1*] where *y0* is the lower value (baseline) and *y1* is the upper value (topline); the difference between *y0* and *y1* corresponds to the computed [value](#stack_value) for this point. The key for each series is available as *series*.key, and the [index](#stack_order) as *series*.index. The input data element for each point is available as *point*.data. + +For example, consider the following table representing monthly sales of fruits: + +Month | Apples | Bananas | Cherries | Dates +--------|--------|---------|----------|------- + 1/2015 | 3840 | 1920 | 960 | 400 + 2/2015 | 1600 | 1440 | 960 | 400 + 3/2015 | 640 | 960 | 640 | 400 + 4/2015 | 320 | 480 | 640 | 400 + +This might be represented in JavaScript as an array of objects: + +```js +var data = [ + {month: new Date(2015, 0, 1), apples: 3840, bananas: 1920, cherries: 960, dates: 400}, + {month: new Date(2015, 1, 1), apples: 1600, bananas: 1440, cherries: 960, dates: 400}, + {month: new Date(2015, 2, 1), apples: 640, bananas: 960, cherries: 640, dates: 400}, + {month: new Date(2015, 3, 1), apples: 320, bananas: 480, cherries: 640, dates: 400} +]; +``` + +To produce a stack for this data: + +```js +var stack = d3.stack() + .keys(["apples", "bananas", "cherries", "dates"]) + .order(d3.stackOrderNone) + .offset(d3.stackOffsetNone); + +var series = stack(data); +``` + +The resulting array has one element per *series*. Each series has one point per month, and each point has a lower and upper value defining the baseline and topline: + +```js +[ + [[ 0, 3840], [ 0, 1600], [ 0, 640], [ 0, 320]], // apples + [[3840, 5760], [1600, 3040], [ 640, 1600], [ 320, 800]], // bananas + [[5760, 6720], [3040, 4000], [1600, 2240], [ 800, 1440]], // cherries + [[6720, 7120], [4000, 4400], [2240, 2640], [1440, 1840]], // dates +] +``` + +Each series in then typically passed to an [area generator](#areas) to render an area chart, or used to construct rectangles for a bar chart. + +# stack.keys([keys]) [<>](https://github.com/d3/d3-shape/blob/master/src/stack.js#L40 "Source") + +If *keys* is specified, sets the keys accessor to the specified function or array and returns this stack generator. If *keys* is not specified, returns the current keys accessor, which defaults to the empty array. A series (layer) is [generated](#_stack) for each key. Keys are typically strings, but they may be arbitrary values. The series’ key is passed to the [value accessor](#stack_value), along with each data point, to compute the point’s value. + +# stack.value([value]) [<>](https://github.com/d3/d3-shape/blob/master/src/stack.js#L44 "Source") + +If *value* is specified, sets the value accessor to the specified function or number and returns this stack generator. If *value* is not specified, returns the current value accessor, which defaults to: + +```js +function value(d, key) { + return d[key]; +} +``` + +Thus, by default the stack generator assumes that the input data is an array of objects, with each object exposing named properties with numeric values; see [*stack*](#_stack) for an example. + +# stack.order([order]) [<>](https://github.com/d3/d3-shape/blob/master/src/stack.js#L48 "Source") + +If *order* is specified, sets the order accessor to the specified function or array and returns this stack generator. If *order* is not specified, returns the current order acccesor, which defaults to [stackOrderNone](#stackOrderNone); this uses the order given by the [key accessor](#stack_key). See [stack orders](#stack-orders) for the built-in orders. + +If *order* is a function, it is passed the generated series array and must return an array of numeric indexes representing the stack order. For example, the default order is defined as: + +```js +function orderNone(series) { + var n = series.length, o = new Array(n); + while (--n >= 0) o[n] = n; + return o; +} +``` + +The stack order is computed prior to the [offset](#stack_offset); thus, the lower value for all points is zero at the time the order is computed. The index attribute for each series is also not set until after the order is computed. + +# stack.offset([offset]) [<>](https://github.com/d3/d3-shape/blob/master/src/stack.js#L52 "Source") + +If *offset* is specified, sets the offset accessor to the specified function or array and returns this stack generator. If *offset* is not specified, returns the current offset acccesor, which defaults to [stackOffsetNone](#stackOffsetNone); this uses a zero baseline. See [stack offsets](#stack-offsets) for the built-in offsets. + +If *offset* is a function, it is passed the generated series array and the order index array. The offset function is then responsible for updating the lower and upper values in the series array to layout the stack. For example, the default offset is defined as: + +```js +function offsetNone(series, order) { + if (!((n = series.length) > 1)) return; + for (var i = 1, s0, s1 = series[order[0]], n, m = s1.length; i < n; ++i) { + s0 = s1, s1 = series[order[i]]; + for (var j = 0; j < m; ++j) { + s1[j][1] += s1[j][0] = s0[j][1]; + } + } +} +``` + +### Stack Orders + +Stack orders are typically not used directly, but are instead passed to [*stack*.order](#stack_order). + +# d3.stackOrderAscending(series) [<>](https://github.com/d3/d3-shape/blob/master/src/order/ascending.js "Source") + +Returns a series order such that the smallest series (according to the sum of values) is at the bottom. + +# d3.stackOrderDescending(series) [<>](https://github.com/d3/d3-shape/blob/master/src/order/descending.js "Source") + +Returns a series order such that the largest series (according to the sum of values) is at the bottom. + +# d3.stackOrderInsideOut(series) [<>](https://github.com/d3/d3-shape/blob/master/src/order/insideOut.js "Source") + +Returns a series order such that the larger series (according to the sum of values) are on the inside and the smaller series are on the outside. This order is recommended for streamgraphs in conjunction with the [wiggle offset](#stackOffsetWiggle). See [Stacked Graphs—Geometry & Aesthetics](http://leebyron.com/streamgraph/) by Byron & Wattenberg for more information. + +# d3.stackOrderNone(series) [<>](https://github.com/d3/d3-shape/blob/master/src/order/none.js "Source") + +Returns the given series order [0, 1, … *n* - 1] where *n* is the number of elements in *series*. Thus, the stack order is given by the [key accessor](#stack_keys). + +# d3.stackOrderReverse(series) [<>](https://github.com/d3/d3-shape/blob/master/src/order/reverse.js "Source") + +Returns the reverse of the given series order [*n* - 1, *n* - 2, … 0] where *n* is the number of elements in *series*. Thus, the stack order is given by the reverse of the [key accessor](#stack_keys). + +### Stack Offsets + +Stack offsets are typically not used directly, but are instead passed to [*stack*.offset](#stack_offset). + +# d3.stackOffsetExpand(series, order) [<>](https://github.com/d3/d3-shape/blob/master/src/offset/expand.js "Source") + +Applies a zero baseline and normalizes the values for each point such that the topline is always one. + +# d3.stackOffsetDiverging(series, order) [<>](https://github.com/d3/d3-shape/blob/master/src/offset/diverging.js "Source") + +Positive values are stacked above zero, while negative values are [stacked below zero](https://bl.ocks.org/mbostock/b5935342c6d21928111928401e2c8608). + +# d3.stackOffsetNone(series, order) [<>](https://github.com/d3/d3-shape/blob/master/src/offset/none.js "Source") + +Applies a zero baseline. + +# d3.stackOffsetSilhouette(series, order) [<>](https://github.com/d3/d3-shape/blob/master/src/offset/silhouette.js "Source") + +Shifts the baseline down such that the center of the streamgraph is always at zero. + +# d3.stackOffsetWiggle(series, order) [<>](https://github.com/d3/d3-shape/blob/master/src/offset/wiggle.js "Source") + +Shifts the baseline so as to minimize the weighted wiggle of layers. This offset is recommended for streamgraphs in conjunction with the [inside-out order](#stackOrderInsideOut). See [Stacked Graphs—Geometry & Aesthetics](http://leebyron.com/streamgraph/) by Bryon & Wattenberg for more information. diff --git a/d3-shape.sublime-project b/d3-shape.sublime-project new file mode 100644 index 0000000..6e0c2d4 --- /dev/null +++ b/d3-shape.sublime-project @@ -0,0 +1,13 @@ +{ + "folders": [ + { + "path": ".", + "file_exclude_patterns": [ + "*.sublime-workspace" + ], + "folder_exclude_patterns": [ + "build" + ] + } + ] +} diff --git a/img/area-defined.png b/img/area-defined.png new file mode 100644 index 0000000..a72642b Binary files /dev/null and b/img/area-defined.png differ diff --git a/img/area-difference.png b/img/area-difference.png new file mode 100644 index 0000000..7a7e147 Binary files /dev/null and b/img/area-difference.png differ diff --git a/img/area-radial.png b/img/area-radial.png new file mode 100644 index 0000000..a700557 Binary files /dev/null and b/img/area-radial.png differ diff --git a/img/area-stacked.png b/img/area-stacked.png new file mode 100644 index 0000000..dfcc2e0 Binary files /dev/null and b/img/area-stacked.png differ diff --git a/img/area.png b/img/area.png new file mode 100644 index 0000000..500e3b4 Binary files /dev/null and b/img/area.png differ diff --git a/img/basis.png b/img/basis.png new file mode 100644 index 0000000..bf40ead Binary files /dev/null and b/img/basis.png differ diff --git a/img/basisClosed.png b/img/basisClosed.png new file mode 100644 index 0000000..c01644b Binary files /dev/null and b/img/basisClosed.png differ diff --git a/img/basisOpen.png b/img/basisOpen.png new file mode 100644 index 0000000..57705ed Binary files /dev/null and b/img/basisOpen.png differ diff --git a/img/bundle.png b/img/bundle.png new file mode 100644 index 0000000..430281b Binary files /dev/null and b/img/bundle.png differ diff --git a/img/cardinal.png b/img/cardinal.png new file mode 100644 index 0000000..c9479bd Binary files /dev/null and b/img/cardinal.png differ diff --git a/img/cardinalClosed.png b/img/cardinalClosed.png new file mode 100644 index 0000000..3167771 Binary files /dev/null and b/img/cardinalClosed.png differ diff --git a/img/cardinalOpen.png b/img/cardinalOpen.png new file mode 100644 index 0000000..1e2a1a0 Binary files /dev/null and b/img/cardinalOpen.png differ diff --git a/img/catmullRom.png b/img/catmullRom.png new file mode 100644 index 0000000..c991fc7 Binary files /dev/null and b/img/catmullRom.png differ diff --git a/img/catmullRomClosed.png b/img/catmullRomClosed.png new file mode 100644 index 0000000..91e7a08 Binary files /dev/null and b/img/catmullRomClosed.png differ diff --git a/img/catmullRomOpen.png b/img/catmullRomOpen.png new file mode 100644 index 0000000..1cba0f6 Binary files /dev/null and b/img/catmullRomOpen.png differ diff --git a/img/centroid-annular-sector.png b/img/centroid-annular-sector.png new file mode 100644 index 0000000..7583607 Binary files /dev/null and b/img/centroid-annular-sector.png differ diff --git a/img/centroid-circular-sector.png b/img/centroid-circular-sector.png new file mode 100644 index 0000000..f20043d Binary files /dev/null and b/img/centroid-circular-sector.png differ diff --git a/img/circle.png b/img/circle.png new file mode 100644 index 0000000..9dedc6f Binary files /dev/null and b/img/circle.png differ diff --git a/img/cross.png b/img/cross.png new file mode 100644 index 0000000..529c3d5 Binary files /dev/null and b/img/cross.png differ diff --git a/img/diamond.png b/img/diamond.png new file mode 100644 index 0000000..8b2c988 Binary files /dev/null and b/img/diamond.png differ diff --git a/img/donut.png b/img/donut.png new file mode 100644 index 0000000..e5ce56a Binary files /dev/null and b/img/donut.png differ diff --git a/img/line-defined.png b/img/line-defined.png new file mode 100644 index 0000000..2c86e15 Binary files /dev/null and b/img/line-defined.png differ diff --git a/img/line-radial.png b/img/line-radial.png new file mode 100644 index 0000000..eacb775 Binary files /dev/null and b/img/line-radial.png differ diff --git a/img/line.png b/img/line.png new file mode 100644 index 0000000..501eb28 Binary files /dev/null and b/img/line.png differ diff --git a/img/linear.png b/img/linear.png new file mode 100644 index 0000000..4a774b4 Binary files /dev/null and b/img/linear.png differ diff --git a/img/linearClosed.png b/img/linearClosed.png new file mode 100644 index 0000000..02201cf Binary files /dev/null and b/img/linearClosed.png differ diff --git a/img/monotoneX.png b/img/monotoneX.png new file mode 100644 index 0000000..7610fc8 Binary files /dev/null and b/img/monotoneX.png differ diff --git a/img/monotoneY.png b/img/monotoneY.png new file mode 100644 index 0000000..7dfcaca Binary files /dev/null and b/img/monotoneY.png differ diff --git a/img/natural.png b/img/natural.png new file mode 100644 index 0000000..04176d9 Binary files /dev/null and b/img/natural.png differ diff --git a/img/padded-annular-sector.png b/img/padded-annular-sector.png new file mode 100644 index 0000000..3c208a9 Binary files /dev/null and b/img/padded-annular-sector.png differ diff --git a/img/padded-circular-sector.png b/img/padded-circular-sector.png new file mode 100644 index 0000000..ca8c06a Binary files /dev/null and b/img/padded-circular-sector.png differ diff --git a/img/pie.png b/img/pie.png new file mode 100644 index 0000000..430be50 Binary files /dev/null and b/img/pie.png differ diff --git a/img/rounded-annular-sector.png b/img/rounded-annular-sector.png new file mode 100644 index 0000000..ec4a658 Binary files /dev/null and b/img/rounded-annular-sector.png differ diff --git a/img/rounded-circular-sector.png b/img/rounded-circular-sector.png new file mode 100644 index 0000000..94f4085 Binary files /dev/null and b/img/rounded-circular-sector.png differ diff --git a/img/square.png b/img/square.png new file mode 100644 index 0000000..393c806 Binary files /dev/null and b/img/square.png differ diff --git a/img/stacked-bar.png b/img/stacked-bar.png new file mode 100644 index 0000000..236e739 Binary files /dev/null and b/img/stacked-bar.png differ diff --git a/img/stacked-stream.png b/img/stacked-stream.png new file mode 100644 index 0000000..72b0e85 Binary files /dev/null and b/img/stacked-stream.png differ diff --git a/img/star.png b/img/star.png new file mode 100644 index 0000000..d2fbdd9 Binary files /dev/null and b/img/star.png differ diff --git a/img/step.png b/img/step.png new file mode 100644 index 0000000..5ae8341 Binary files /dev/null and b/img/step.png differ diff --git a/img/stepAfter.png b/img/stepAfter.png new file mode 100644 index 0000000..365a1ac Binary files /dev/null and b/img/stepAfter.png differ diff --git a/img/stepBefore.png b/img/stepBefore.png new file mode 100644 index 0000000..e659d3a Binary files /dev/null and b/img/stepBefore.png differ diff --git a/img/triangle.png b/img/triangle.png new file mode 100644 index 0000000..7c504f8 Binary files /dev/null and b/img/triangle.png differ diff --git a/img/wye.png b/img/wye.png new file mode 100644 index 0000000..b667108 Binary files /dev/null and b/img/wye.png differ diff --git a/index.js b/index.js new file mode 100644 index 0000000..903c1e3 --- /dev/null +++ b/index.js @@ -0,0 +1,45 @@ +export {default as arc} from "./src/arc"; +export {default as area} from "./src/area"; +export {default as line} from "./src/line"; +export {default as pie} from "./src/pie"; +export {default as areaRadial, default as radialArea} from "./src/areaRadial"; // Note: radialArea is deprecated! +export {default as lineRadial, default as radialLine} from "./src/lineRadial"; // Note: radialLine is deprecated! +export {default as pointRadial} from "./src/pointRadial"; +export {linkHorizontal, linkVertical, linkRadial} from "./src/link/index"; + +export {default as symbol, symbols} from "./src/symbol"; +export {default as symbolCircle} from "./src/symbol/circle"; +export {default as symbolCross} from "./src/symbol/cross"; +export {default as symbolDiamond} from "./src/symbol/diamond"; +export {default as symbolSquare} from "./src/symbol/square"; +export {default as symbolStar} from "./src/symbol/star"; +export {default as symbolTriangle} from "./src/symbol/triangle"; +export {default as symbolWye} from "./src/symbol/wye"; + +export {default as curveBasisClosed} from "./src/curve/basisClosed"; +export {default as curveBasisOpen} from "./src/curve/basisOpen"; +export {default as curveBasis} from "./src/curve/basis"; +export {default as curveBundle} from "./src/curve/bundle"; +export {default as curveCardinalClosed} from "./src/curve/cardinalClosed"; +export {default as curveCardinalOpen} from "./src/curve/cardinalOpen"; +export {default as curveCardinal} from "./src/curve/cardinal"; +export {default as curveCatmullRomClosed} from "./src/curve/catmullRomClosed"; +export {default as curveCatmullRomOpen} from "./src/curve/catmullRomOpen"; +export {default as curveCatmullRom} from "./src/curve/catmullRom"; +export {default as curveLinearClosed} from "./src/curve/linearClosed"; +export {default as curveLinear} from "./src/curve/linear"; +export {monotoneX as curveMonotoneX, monotoneY as curveMonotoneY} from "./src/curve/monotone"; +export {default as curveNatural} from "./src/curve/natural"; +export {default as curveStep, stepAfter as curveStepAfter, stepBefore as curveStepBefore} from "./src/curve/step"; + +export {default as stack} from "./src/stack"; +export {default as stackOffsetExpand} from "./src/offset/expand"; +export {default as stackOffsetDiverging} from "./src/offset/diverging"; +export {default as stackOffsetNone} from "./src/offset/none"; +export {default as stackOffsetSilhouette} from "./src/offset/silhouette"; +export {default as stackOffsetWiggle} from "./src/offset/wiggle"; +export {default as stackOrderAscending} from "./src/order/ascending"; +export {default as stackOrderDescending} from "./src/order/descending"; +export {default as stackOrderInsideOut} from "./src/order/insideOut"; +export {default as stackOrderNone} from "./src/order/none"; +export {default as stackOrderReverse} from "./src/order/reverse"; diff --git a/package.json b/package.json new file mode 100644 index 0000000..63f796f --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "d3-shape", + "version": "1.2.0", + "description": "Graphical primitives for visualization, such as lines and areas.", + "keywords": [ + "d3", + "d3-module", + "graphics", + "visualization", + "canvas", + "svg" + ], + "homepage": "https://d3js.org/d3-shape/", + "license": "BSD-3-Clause", + "author": { + "name": "Mike Bostock", + "url": "http://bost.ocks.org/mike" + }, + "main": "build/d3-shape.js", + "module": "index", + "jsnext:main": "index", + "repository": { + "type": "git", + "url": "https://github.com/d3/d3-shape.git" + }, + "scripts": { + "pretest": "rm -rf build && mkdir build && rollup --banner \"$(preamble)\" -f umd -g d3-path:d3 -n d3 -o build/d3-shape.js -- index.js", + "test": "tape 'test/**/*-test.js' && eslint index.js src", + "prepublish": "npm run test && uglifyjs --preamble \"$(preamble)\" build/d3-shape.js -c -m -o build/d3-shape.min.js", + "postpublish": "git push && git push --tags && cd ../d3.github.com && git pull && cp ../d3-shape/build/d3-shape.js d3-shape.v1.js && cp ../d3-shape/build/d3-shape.min.js d3-shape.v1.min.js && git add d3-shape.v1.js d3-shape.v1.min.js && git commit -m \"d3-shape ${npm_package_version}\" && git push && cd - && zip -j build/d3-shape.zip -- LICENSE README.md build/d3-shape.js build/d3-shape.min.js" + }, + "dependencies": { + "d3-path": "1" + }, + "devDependencies": { + "d3-polygon": "1", + "eslint": "3", + "package-preamble": "0.1", + "rollup": "0.41", + "tape": "4", + "uglify-js": "^2.8.11" + } +} diff --git a/src/arc.js b/src/arc.js new file mode 100644 index 0000000..d98e6d3 --- /dev/null +++ b/src/arc.js @@ -0,0 +1,259 @@ +import {path} from "d3-path"; +import constant from "./constant"; +import {abs, acos, asin, atan2, cos, epsilon, halfPi, max, min, pi, sin, sqrt, tau} from "./math"; + +function arcInnerRadius(d) { + return d.innerRadius; +} + +function arcOuterRadius(d) { + return d.outerRadius; +} + +function arcStartAngle(d) { + return d.startAngle; +} + +function arcEndAngle(d) { + return d.endAngle; +} + +function arcPadAngle(d) { + return d && d.padAngle; // Note: optional! +} + +function intersect(x0, y0, x1, y1, x2, y2, x3, y3) { + var x10 = x1 - x0, y10 = y1 - y0, + x32 = x3 - x2, y32 = y3 - y2, + t = (x32 * (y0 - y2) - y32 * (x0 - x2)) / (y32 * x10 - x32 * y10); + return [x0 + t * x10, y0 + t * y10]; +} + +// Compute perpendicular offset line of length rc. +// http://mathworld.wolfram.com/Circle-LineIntersection.html +function cornerTangents(x0, y0, x1, y1, r1, rc, cw) { + var x01 = x0 - x1, + y01 = y0 - y1, + lo = (cw ? rc : -rc) / sqrt(x01 * x01 + y01 * y01), + ox = lo * y01, + oy = -lo * x01, + x11 = x0 + ox, + y11 = y0 + oy, + x10 = x1 + ox, + y10 = y1 + oy, + x00 = (x11 + x10) / 2, + y00 = (y11 + y10) / 2, + dx = x10 - x11, + dy = y10 - y11, + d2 = dx * dx + dy * dy, + r = r1 - rc, + D = x11 * y10 - x10 * y11, + d = (dy < 0 ? -1 : 1) * sqrt(max(0, r * r * d2 - D * D)), + cx0 = (D * dy - dx * d) / d2, + cy0 = (-D * dx - dy * d) / d2, + cx1 = (D * dy + dx * d) / d2, + cy1 = (-D * dx + dy * d) / d2, + dx0 = cx0 - x00, + dy0 = cy0 - y00, + dx1 = cx1 - x00, + dy1 = cy1 - y00; + + // Pick the closer of the two intersection points. + // TODO Is there a faster way to determine which intersection to use? + if (dx0 * dx0 + dy0 * dy0 > dx1 * dx1 + dy1 * dy1) cx0 = cx1, cy0 = cy1; + + return { + cx: cx0, + cy: cy0, + x01: -ox, + y01: -oy, + x11: cx0 * (r1 / r - 1), + y11: cy0 * (r1 / r - 1) + }; +} + +export default function() { + var innerRadius = arcInnerRadius, + outerRadius = arcOuterRadius, + cornerRadius = constant(0), + padRadius = null, + startAngle = arcStartAngle, + endAngle = arcEndAngle, + padAngle = arcPadAngle, + context = null; + + function arc() { + var buffer, + r, + r0 = +innerRadius.apply(this, arguments), + r1 = +outerRadius.apply(this, arguments), + a0 = startAngle.apply(this, arguments) - halfPi, + a1 = endAngle.apply(this, arguments) - halfPi, + da = abs(a1 - a0), + cw = a1 > a0; + + if (!context) context = buffer = path(); + + // Ensure that the outer radius is always larger than the inner radius. + if (r1 < r0) r = r1, r1 = r0, r0 = r; + + // Is it a point? + if (!(r1 > epsilon)) context.moveTo(0, 0); + + // Or is it a circle or annulus? + else if (da > tau - epsilon) { + context.moveTo(r1 * cos(a0), r1 * sin(a0)); + context.arc(0, 0, r1, a0, a1, !cw); + if (r0 > epsilon) { + context.moveTo(r0 * cos(a1), r0 * sin(a1)); + context.arc(0, 0, r0, a1, a0, cw); + } + } + + // Or is it a circular or annular sector? + else { + var a01 = a0, + a11 = a1, + a00 = a0, + a10 = a1, + da0 = da, + da1 = da, + ap = padAngle.apply(this, arguments) / 2, + rp = (ap > epsilon) && (padRadius ? +padRadius.apply(this, arguments) : sqrt(r0 * r0 + r1 * r1)), + rc = min(abs(r1 - r0) / 2, +cornerRadius.apply(this, arguments)), + rc0 = rc, + rc1 = rc, + t0, + t1; + + // Apply padding? Note that since r1 ≥ r0, da1 ≥ da0. + if (rp > epsilon) { + var p0 = asin(rp / r0 * sin(ap)), + p1 = asin(rp / r1 * sin(ap)); + if ((da0 -= p0 * 2) > epsilon) p0 *= (cw ? 1 : -1), a00 += p0, a10 -= p0; + else da0 = 0, a00 = a10 = (a0 + a1) / 2; + if ((da1 -= p1 * 2) > epsilon) p1 *= (cw ? 1 : -1), a01 += p1, a11 -= p1; + else da1 = 0, a01 = a11 = (a0 + a1) / 2; + } + + var x01 = r1 * cos(a01), + y01 = r1 * sin(a01), + x10 = r0 * cos(a10), + y10 = r0 * sin(a10); + + // Apply rounded corners? + if (rc > epsilon) { + var x11 = r1 * cos(a11), + y11 = r1 * sin(a11), + x00 = r0 * cos(a00), + y00 = r0 * sin(a00); + + // Restrict the corner radius according to the sector angle. + if (da < pi) { + var oc = da0 > epsilon ? intersect(x01, y01, x00, y00, x11, y11, x10, y10) : [x10, y10], + ax = x01 - oc[0], + ay = y01 - oc[1], + bx = x11 - oc[0], + by = y11 - oc[1], + kc = 1 / sin(acos((ax * bx + ay * by) / (sqrt(ax * ax + ay * ay) * sqrt(bx * bx + by * by))) / 2), + lc = sqrt(oc[0] * oc[0] + oc[1] * oc[1]); + rc0 = min(rc, (r0 - lc) / (kc - 1)); + rc1 = min(rc, (r1 - lc) / (kc + 1)); + } + } + + // Is the sector collapsed to a line? + if (!(da1 > epsilon)) context.moveTo(x01, y01); + + // Does the sector’s outer ring have rounded corners? + else if (rc1 > epsilon) { + t0 = cornerTangents(x00, y00, x01, y01, r1, rc1, cw); + t1 = cornerTangents(x11, y11, x10, y10, r1, rc1, cw); + + context.moveTo(t0.cx + t0.x01, t0.cy + t0.y01); + + // Have the corners merged? + if (rc1 < rc) context.arc(t0.cx, t0.cy, rc1, atan2(t0.y01, t0.x01), atan2(t1.y01, t1.x01), !cw); + + // Otherwise, draw the two corners and the ring. + else { + context.arc(t0.cx, t0.cy, rc1, atan2(t0.y01, t0.x01), atan2(t0.y11, t0.x11), !cw); + context.arc(0, 0, r1, atan2(t0.cy + t0.y11, t0.cx + t0.x11), atan2(t1.cy + t1.y11, t1.cx + t1.x11), !cw); + context.arc(t1.cx, t1.cy, rc1, atan2(t1.y11, t1.x11), atan2(t1.y01, t1.x01), !cw); + } + } + + // Or is the outer ring just a circular arc? + else context.moveTo(x01, y01), context.arc(0, 0, r1, a01, a11, !cw); + + // Is there no inner ring, and it’s a circular sector? + // Or perhaps it’s an annular sector collapsed due to padding? + if (!(r0 > epsilon) || !(da0 > epsilon)) context.lineTo(x10, y10); + + // Does the sector’s inner ring (or point) have rounded corners? + else if (rc0 > epsilon) { + t0 = cornerTangents(x10, y10, x11, y11, r0, -rc0, cw); + t1 = cornerTangents(x01, y01, x00, y00, r0, -rc0, cw); + + context.lineTo(t0.cx + t0.x01, t0.cy + t0.y01); + + // Have the corners merged? + if (rc0 < rc) context.arc(t0.cx, t0.cy, rc0, atan2(t0.y01, t0.x01), atan2(t1.y01, t1.x01), !cw); + + // Otherwise, draw the two corners and the ring. + else { + context.arc(t0.cx, t0.cy, rc0, atan2(t0.y01, t0.x01), atan2(t0.y11, t0.x11), !cw); + context.arc(0, 0, r0, atan2(t0.cy + t0.y11, t0.cx + t0.x11), atan2(t1.cy + t1.y11, t1.cx + t1.x11), cw); + context.arc(t1.cx, t1.cy, rc0, atan2(t1.y11, t1.x11), atan2(t1.y01, t1.x01), !cw); + } + } + + // Or is the inner ring just a circular arc? + else context.arc(0, 0, r0, a10, a00, cw); + } + + context.closePath(); + + if (buffer) return context = null, buffer + "" || null; + } + + arc.centroid = function() { + var r = (+innerRadius.apply(this, arguments) + +outerRadius.apply(this, arguments)) / 2, + a = (+startAngle.apply(this, arguments) + +endAngle.apply(this, arguments)) / 2 - pi / 2; + return [cos(a) * r, sin(a) * r]; + }; + + arc.innerRadius = function(_) { + return arguments.length ? (innerRadius = typeof _ === "function" ? _ : constant(+_), arc) : innerRadius; + }; + + arc.outerRadius = function(_) { + return arguments.length ? (outerRadius = typeof _ === "function" ? _ : constant(+_), arc) : outerRadius; + }; + + arc.cornerRadius = function(_) { + return arguments.length ? (cornerRadius = typeof _ === "function" ? _ : constant(+_), arc) : cornerRadius; + }; + + arc.padRadius = function(_) { + return arguments.length ? (padRadius = _ == null ? null : typeof _ === "function" ? _ : constant(+_), arc) : padRadius; + }; + + arc.startAngle = function(_) { + return arguments.length ? (startAngle = typeof _ === "function" ? _ : constant(+_), arc) : startAngle; + }; + + arc.endAngle = function(_) { + return arguments.length ? (endAngle = typeof _ === "function" ? _ : constant(+_), arc) : endAngle; + }; + + arc.padAngle = function(_) { + return arguments.length ? (padAngle = typeof _ === "function" ? _ : constant(+_), arc) : padAngle; + }; + + arc.context = function(_) { + return arguments.length ? ((context = _ == null ? null : _), arc) : context; + }; + + return arc; +} diff --git a/src/area.js b/src/area.js new file mode 100644 index 0000000..9eb795c --- /dev/null +++ b/src/area.js @@ -0,0 +1,109 @@ +import {path} from "d3-path"; +import constant from "./constant"; +import curveLinear from "./curve/linear"; +import line from "./line"; +import {x as pointX, y as pointY} from "./point"; + +export default function() { + var x0 = pointX, + x1 = null, + y0 = constant(0), + y1 = pointY, + defined = constant(true), + context = null, + curve = curveLinear, + output = null; + + function area(data) { + var i, + j, + k, + n = data.length, + d, + defined0 = false, + buffer, + x0z = new Array(n), + y0z = new Array(n); + + if (context == null) output = curve(buffer = path()); + + for (i = 0; i <= n; ++i) { + if (!(i < n && defined(d = data[i], i, data)) === defined0) { + if (defined0 = !defined0) { + j = i; + output.areaStart(); + output.lineStart(); + } else { + output.lineEnd(); + output.lineStart(); + for (k = i - 1; k >= j; --k) { + output.point(x0z[k], y0z[k]); + } + output.lineEnd(); + output.areaEnd(); + } + } + if (defined0) { + x0z[i] = +x0(d, i, data), y0z[i] = +y0(d, i, data); + output.point(x1 ? +x1(d, i, data) : x0z[i], y1 ? +y1(d, i, data) : y0z[i]); + } + } + + if (buffer) return output = null, buffer + "" || null; + } + + function arealine() { + return line().defined(defined).curve(curve).context(context); + } + + area.x = function(_) { + return arguments.length ? (x0 = typeof _ === "function" ? _ : constant(+_), x1 = null, area) : x0; + }; + + area.x0 = function(_) { + return arguments.length ? (x0 = typeof _ === "function" ? _ : constant(+_), area) : x0; + }; + + area.x1 = function(_) { + return arguments.length ? (x1 = _ == null ? null : typeof _ === "function" ? _ : constant(+_), area) : x1; + }; + + area.y = function(_) { + return arguments.length ? (y0 = typeof _ === "function" ? _ : constant(+_), y1 = null, area) : y0; + }; + + area.y0 = function(_) { + return arguments.length ? (y0 = typeof _ === "function" ? _ : constant(+_), area) : y0; + }; + + area.y1 = function(_) { + return arguments.length ? (y1 = _ == null ? null : typeof _ === "function" ? _ : constant(+_), area) : y1; + }; + + area.lineX0 = + area.lineY0 = function() { + return arealine().x(x0).y(y0); + }; + + area.lineY1 = function() { + return arealine().x(x0).y(y1); + }; + + area.lineX1 = function() { + return arealine().x(x1).y(y0); + }; + + area.defined = function(_) { + return arguments.length ? (defined = typeof _ === "function" ? _ : constant(!!_), area) : defined; + }; + + area.curve = function(_) { + return arguments.length ? (curve = _, context != null && (output = curve(context)), area) : curve; + }; + + area.context = function(_) { + return arguments.length ? (_ == null ? context = output = null : output = curve(context = _), area) : context; + }; + + return area; +} diff --git a/src/areaRadial.js b/src/areaRadial.js new file mode 100644 index 0000000..d8235c4 --- /dev/null +++ b/src/areaRadial.js @@ -0,0 +1,29 @@ +import curveRadial, {curveRadialLinear} from "./curve/radial"; +import area from "./area"; +import {lineRadial} from "./lineRadial" + +export default function() { + var a = area().curve(curveRadialLinear), + c = a.curve, + x0 = a.lineX0, + x1 = a.lineX1, + y0 = a.lineY0, + y1 = a.lineY1; + + a.angle = a.x, delete a.x; + a.startAngle = a.x0, delete a.x0; + a.endAngle = a.x1, delete a.x1; + a.radius = a.y, delete a.y; + a.innerRadius = a.y0, delete a.y0; + a.outerRadius = a.y1, delete a.y1; + a.lineStartAngle = function() { return lineRadial(x0()); }, delete a.lineX0; + a.lineEndAngle = function() { return lineRadial(x1()); }, delete a.lineX1; + a.lineInnerRadius = function() { return lineRadial(y0()); }, delete a.lineY0; + a.lineOuterRadius = function() { return lineRadial(y1()); }, delete a.lineY1; + + a.curve = function(_) { + return arguments.length ? c(curveRadial(_)) : c()._curve; + }; + + return a; +} diff --git a/src/array.js b/src/array.js new file mode 100644 index 0000000..8eeac16 --- /dev/null +++ b/src/array.js @@ -0,0 +1 @@ +export var slice = Array.prototype.slice; diff --git a/src/constant.js b/src/constant.js new file mode 100644 index 0000000..6fa95b7 --- /dev/null +++ b/src/constant.js @@ -0,0 +1,5 @@ +export default function(x) { + return function constant() { + return x; + }; +} diff --git a/src/curve/basis.js b/src/curve/basis.js new file mode 100644 index 0000000..b186bed --- /dev/null +++ b/src/curve/basis.js @@ -0,0 +1,51 @@ +export function point(that, x, y) { + that._context.bezierCurveTo( + (2 * that._x0 + that._x1) / 3, + (2 * that._y0 + that._y1) / 3, + (that._x0 + 2 * that._x1) / 3, + (that._y0 + 2 * that._y1) / 3, + (that._x0 + 4 * that._x1 + x) / 6, + (that._y0 + 4 * that._y1 + y) / 6 + ); +} + +export function Basis(context) { + this._context = context; +} + +Basis.prototype = { + areaStart: function() { + this._line = 0; + }, + areaEnd: function() { + this._line = NaN; + }, + lineStart: function() { + this._x0 = this._x1 = + this._y0 = this._y1 = NaN; + this._point = 0; + }, + lineEnd: function() { + switch (this._point) { + case 3: point(this, this._x1, this._y1); // proceed + case 2: this._context.lineTo(this._x1, this._y1); break; + } + if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath(); + this._line = 1 - this._line; + }, + point: function(x, y) { + x = +x, y = +y; + switch (this._point) { + case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break; + case 1: this._point = 2; break; + case 2: this._point = 3; this._context.lineTo((5 * this._x0 + this._x1) / 6, (5 * this._y0 + this._y1) / 6); // proceed + default: point(this, x, y); break; + } + this._x0 = this._x1, this._x1 = x; + this._y0 = this._y1, this._y1 = y; + } +}; + +export default function(context) { + return new Basis(context); +} diff --git a/src/curve/basisClosed.js b/src/curve/basisClosed.js new file mode 100644 index 0000000..522e305 --- /dev/null +++ b/src/curve/basisClosed.js @@ -0,0 +1,52 @@ +import noop from "../noop"; +import {point} from "./basis"; + +function BasisClosed(context) { + this._context = context; +} + +BasisClosed.prototype = { + areaStart: noop, + areaEnd: noop, + lineStart: function() { + this._x0 = this._x1 = this._x2 = this._x3 = this._x4 = + this._y0 = this._y1 = this._y2 = this._y3 = this._y4 = NaN; + this._point = 0; + }, + lineEnd: function() { + switch (this._point) { + case 1: { + this._context.moveTo(this._x2, this._y2); + this._context.closePath(); + break; + } + case 2: { + this._context.moveTo((this._x2 + 2 * this._x3) / 3, (this._y2 + 2 * this._y3) / 3); + this._context.lineTo((this._x3 + 2 * this._x2) / 3, (this._y3 + 2 * this._y2) / 3); + this._context.closePath(); + break; + } + case 3: { + this.point(this._x2, this._y2); + this.point(this._x3, this._y3); + this.point(this._x4, this._y4); + break; + } + } + }, + point: function(x, y) { + x = +x, y = +y; + switch (this._point) { + case 0: this._point = 1; this._x2 = x, this._y2 = y; break; + case 1: this._point = 2; this._x3 = x, this._y3 = y; break; + case 2: this._point = 3; this._x4 = x, this._y4 = y; this._context.moveTo((this._x0 + 4 * this._x1 + x) / 6, (this._y0 + 4 * this._y1 + y) / 6); break; + default: point(this, x, y); break; + } + this._x0 = this._x1, this._x1 = x; + this._y0 = this._y1, this._y1 = y; + } +}; + +export default function(context) { + return new BasisClosed(context); +} diff --git a/src/curve/basisOpen.js b/src/curve/basisOpen.js new file mode 100644 index 0000000..dac5d4e --- /dev/null +++ b/src/curve/basisOpen.js @@ -0,0 +1,39 @@ +import {point} from "./basis"; + +function BasisOpen(context) { + this._context = context; +} + +BasisOpen.prototype = { + areaStart: function() { + this._line = 0; + }, + areaEnd: function() { + this._line = NaN; + }, + lineStart: function() { + this._x0 = this._x1 = + this._y0 = this._y1 = NaN; + this._point = 0; + }, + lineEnd: function() { + if (this._line || (this._line !== 0 && this._point === 3)) this._context.closePath(); + this._line = 1 - this._line; + }, + point: function(x, y) { + x = +x, y = +y; + switch (this._point) { + case 0: this._point = 1; break; + case 1: this._point = 2; break; + case 2: this._point = 3; var x0 = (this._x0 + 4 * this._x1 + x) / 6, y0 = (this._y0 + 4 * this._y1 + y) / 6; this._line ? this._context.lineTo(x0, y0) : this._context.moveTo(x0, y0); break; + case 3: this._point = 4; // proceed + default: point(this, x, y); break; + } + this._x0 = this._x1, this._x1 = x; + this._y0 = this._y1, this._y1 = y; + } +}; + +export default function(context) { + return new BasisOpen(context); +} diff --git a/src/curve/bundle.js b/src/curve/bundle.js new file mode 100644 index 0000000..10d5307 --- /dev/null +++ b/src/curve/bundle.js @@ -0,0 +1,56 @@ +import {Basis} from "./basis"; + +function Bundle(context, beta) { + this._basis = new Basis(context); + this._beta = beta; +} + +Bundle.prototype = { + lineStart: function() { + this._x = []; + this._y = []; + this._basis.lineStart(); + }, + lineEnd: function() { + var x = this._x, + y = this._y, + j = x.length - 1; + + if (j > 0) { + var x0 = x[0], + y0 = y[0], + dx = x[j] - x0, + dy = y[j] - y0, + i = -1, + t; + + while (++i <= j) { + t = i / j; + this._basis.point( + this._beta * x[i] + (1 - this._beta) * (x0 + t * dx), + this._beta * y[i] + (1 - this._beta) * (y0 + t * dy) + ); + } + } + + this._x = this._y = null; + this._basis.lineEnd(); + }, + point: function(x, y) { + this._x.push(+x); + this._y.push(+y); + } +}; + +export default (function custom(beta) { + + function bundle(context) { + return beta === 1 ? new Basis(context) : new Bundle(context, beta); + } + + bundle.beta = function(beta) { + return custom(+beta); + }; + + return bundle; +})(0.85); diff --git a/src/curve/cardinal.js b/src/curve/cardinal.js new file mode 100644 index 0000000..3ab1070 --- /dev/null +++ b/src/curve/cardinal.js @@ -0,0 +1,61 @@ +export function point(that, x, y) { + that._context.bezierCurveTo( + that._x1 + that._k * (that._x2 - that._x0), + that._y1 + that._k * (that._y2 - that._y0), + that._x2 + that._k * (that._x1 - x), + that._y2 + that._k * (that._y1 - y), + that._x2, + that._y2 + ); +} + +export function Cardinal(context, tension) { + this._context = context; + this._k = (1 - tension) / 6; +} + +Cardinal.prototype = { + areaStart: function() { + this._line = 0; + }, + areaEnd: function() { + this._line = NaN; + }, + lineStart: function() { + this._x0 = this._x1 = this._x2 = + this._y0 = this._y1 = this._y2 = NaN; + this._point = 0; + }, + lineEnd: function() { + switch (this._point) { + case 2: this._context.lineTo(this._x2, this._y2); break; + case 3: point(this, this._x1, this._y1); break; + } + if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath(); + this._line = 1 - this._line; + }, + point: function(x, y) { + x = +x, y = +y; + switch (this._point) { + case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break; + case 1: this._point = 2; this._x1 = x, this._y1 = y; break; + case 2: this._point = 3; // proceed + default: point(this, x, y); break; + } + this._x0 = this._x1, this._x1 = this._x2, this._x2 = x; + this._y0 = this._y1, this._y1 = this._y2, this._y2 = y; + } +}; + +export default (function custom(tension) { + + function cardinal(context) { + return new Cardinal(context, tension); + } + + cardinal.tension = function(tension) { + return custom(+tension); + }; + + return cardinal; +})(0); diff --git a/src/curve/cardinalClosed.js b/src/curve/cardinalClosed.js new file mode 100644 index 0000000..20516af --- /dev/null +++ b/src/curve/cardinalClosed.js @@ -0,0 +1,61 @@ +import noop from "../noop"; +import {point} from "./cardinal"; + +export function CardinalClosed(context, tension) { + this._context = context; + this._k = (1 - tension) / 6; +} + +CardinalClosed.prototype = { + areaStart: noop, + areaEnd: noop, + lineStart: function() { + this._x0 = this._x1 = this._x2 = this._x3 = this._x4 = this._x5 = + this._y0 = this._y1 = this._y2 = this._y3 = this._y4 = this._y5 = NaN; + this._point = 0; + }, + lineEnd: function() { + switch (this._point) { + case 1: { + this._context.moveTo(this._x3, this._y3); + this._context.closePath(); + break; + } + case 2: { + this._context.lineTo(this._x3, this._y3); + this._context.closePath(); + break; + } + case 3: { + this.point(this._x3, this._y3); + this.point(this._x4, this._y4); + this.point(this._x5, this._y5); + break; + } + } + }, + point: function(x, y) { + x = +x, y = +y; + switch (this._point) { + case 0: this._point = 1; this._x3 = x, this._y3 = y; break; + case 1: this._point = 2; this._context.moveTo(this._x4 = x, this._y4 = y); break; + case 2: this._point = 3; this._x5 = x, this._y5 = y; break; + default: point(this, x, y); break; + } + this._x0 = this._x1, this._x1 = this._x2, this._x2 = x; + this._y0 = this._y1, this._y1 = this._y2, this._y2 = y; + } +}; + +export default (function custom(tension) { + + function cardinal(context) { + return new CardinalClosed(context, tension); + } + + cardinal.tension = function(tension) { + return custom(+tension); + }; + + return cardinal; +})(0); diff --git a/src/curve/cardinalOpen.js b/src/curve/cardinalOpen.js new file mode 100644 index 0000000..69070d5 --- /dev/null +++ b/src/curve/cardinalOpen.js @@ -0,0 +1,49 @@ +import {point} from "./cardinal"; + +export function CardinalOpen(context, tension) { + this._context = context; + this._k = (1 - tension) / 6; +} + +CardinalOpen.prototype = { + areaStart: function() { + this._line = 0; + }, + areaEnd: function() { + this._line = NaN; + }, + lineStart: function() { + this._x0 = this._x1 = this._x2 = + this._y0 = this._y1 = this._y2 = NaN; + this._point = 0; + }, + lineEnd: function() { + if (this._line || (this._line !== 0 && this._point === 3)) this._context.closePath(); + this._line = 1 - this._line; + }, + point: function(x, y) { + x = +x, y = +y; + switch (this._point) { + case 0: this._point = 1; break; + case 1: this._point = 2; break; + case 2: this._point = 3; this._line ? this._context.lineTo(this._x2, this._y2) : this._context.moveTo(this._x2, this._y2); break; + case 3: this._point = 4; // proceed + default: point(this, x, y); break; + } + this._x0 = this._x1, this._x1 = this._x2, this._x2 = x; + this._y0 = this._y1, this._y1 = this._y2, this._y2 = y; + } +}; + +export default (function custom(tension) { + + function cardinal(context) { + return new CardinalOpen(context, tension); + } + + cardinal.tension = function(tension) { + return custom(+tension); + }; + + return cardinal; +})(0); diff --git a/src/curve/catmullRom.js b/src/curve/catmullRom.js new file mode 100644 index 0000000..fff05e6 --- /dev/null +++ b/src/curve/catmullRom.js @@ -0,0 +1,88 @@ +import {epsilon} from "../math"; +import {Cardinal} from "./cardinal"; + +export function point(that, x, y) { + var x1 = that._x1, + y1 = that._y1, + x2 = that._x2, + y2 = that._y2; + + if (that._l01_a > epsilon) { + var a = 2 * that._l01_2a + 3 * that._l01_a * that._l12_a + that._l12_2a, + n = 3 * that._l01_a * (that._l01_a + that._l12_a); + x1 = (x1 * a - that._x0 * that._l12_2a + that._x2 * that._l01_2a) / n; + y1 = (y1 * a - that._y0 * that._l12_2a + that._y2 * that._l01_2a) / n; + } + + if (that._l23_a > epsilon) { + var b = 2 * that._l23_2a + 3 * that._l23_a * that._l12_a + that._l12_2a, + m = 3 * that._l23_a * (that._l23_a + that._l12_a); + x2 = (x2 * b + that._x1 * that._l23_2a - x * that._l12_2a) / m; + y2 = (y2 * b + that._y1 * that._l23_2a - y * that._l12_2a) / m; + } + + that._context.bezierCurveTo(x1, y1, x2, y2, that._x2, that._y2); +} + +function CatmullRom(context, alpha) { + this._context = context; + this._alpha = alpha; +} + +CatmullRom.prototype = { + areaStart: function() { + this._line = 0; + }, + areaEnd: function() { + this._line = NaN; + }, + lineStart: function() { + this._x0 = this._x1 = this._x2 = + this._y0 = this._y1 = this._y2 = NaN; + this._l01_a = this._l12_a = this._l23_a = + this._l01_2a = this._l12_2a = this._l23_2a = + this._point = 0; + }, + lineEnd: function() { + switch (this._point) { + case 2: this._context.lineTo(this._x2, this._y2); break; + case 3: this.point(this._x2, this._y2); break; + } + if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath(); + this._line = 1 - this._line; + }, + point: function(x, y) { + x = +x, y = +y; + + if (this._point) { + var x23 = this._x2 - x, + y23 = this._y2 - y; + this._l23_a = Math.sqrt(this._l23_2a = Math.pow(x23 * x23 + y23 * y23, this._alpha)); + } + + switch (this._point) { + case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break; + case 1: this._point = 2; break; + case 2: this._point = 3; // proceed + default: point(this, x, y); break; + } + + this._l01_a = this._l12_a, this._l12_a = this._l23_a; + this._l01_2a = this._l12_2a, this._l12_2a = this._l23_2a; + this._x0 = this._x1, this._x1 = this._x2, this._x2 = x; + this._y0 = this._y1, this._y1 = this._y2, this._y2 = y; + } +}; + +export default (function custom(alpha) { + + function catmullRom(context) { + return alpha ? new CatmullRom(context, alpha) : new Cardinal(context, 0); + } + + catmullRom.alpha = function(alpha) { + return custom(+alpha); + }; + + return catmullRom; +})(0.5); diff --git a/src/curve/catmullRomClosed.js b/src/curve/catmullRomClosed.js new file mode 100644 index 0000000..eb79521 --- /dev/null +++ b/src/curve/catmullRomClosed.js @@ -0,0 +1,74 @@ +import {CardinalClosed} from "./cardinalClosed"; +import noop from "../noop"; +import {point} from "./catmullRom"; + +function CatmullRomClosed(context, alpha) { + this._context = context; + this._alpha = alpha; +} + +CatmullRomClosed.prototype = { + areaStart: noop, + areaEnd: noop, + lineStart: function() { + this._x0 = this._x1 = this._x2 = this._x3 = this._x4 = this._x5 = + this._y0 = this._y1 = this._y2 = this._y3 = this._y4 = this._y5 = NaN; + this._l01_a = this._l12_a = this._l23_a = + this._l01_2a = this._l12_2a = this._l23_2a = + this._point = 0; + }, + lineEnd: function() { + switch (this._point) { + case 1: { + this._context.moveTo(this._x3, this._y3); + this._context.closePath(); + break; + } + case 2: { + this._context.lineTo(this._x3, this._y3); + this._context.closePath(); + break; + } + case 3: { + this.point(this._x3, this._y3); + this.point(this._x4, this._y4); + this.point(this._x5, this._y5); + break; + } + } + }, + point: function(x, y) { + x = +x, y = +y; + + if (this._point) { + var x23 = this._x2 - x, + y23 = this._y2 - y; + this._l23_a = Math.sqrt(this._l23_2a = Math.pow(x23 * x23 + y23 * y23, this._alpha)); + } + + switch (this._point) { + case 0: this._point = 1; this._x3 = x, this._y3 = y; break; + case 1: this._point = 2; this._context.moveTo(this._x4 = x, this._y4 = y); break; + case 2: this._point = 3; this._x5 = x, this._y5 = y; break; + default: point(this, x, y); break; + } + + this._l01_a = this._l12_a, this._l12_a = this._l23_a; + this._l01_2a = this._l12_2a, this._l12_2a = this._l23_2a; + this._x0 = this._x1, this._x1 = this._x2, this._x2 = x; + this._y0 = this._y1, this._y1 = this._y2, this._y2 = y; + } +}; + +export default (function custom(alpha) { + + function catmullRom(context) { + return alpha ? new CatmullRomClosed(context, alpha) : new CardinalClosed(context, 0); + } + + catmullRom.alpha = function(alpha) { + return custom(+alpha); + }; + + return catmullRom; +})(0.5); diff --git a/src/curve/catmullRomOpen.js b/src/curve/catmullRomOpen.js new file mode 100644 index 0000000..217ddb8 --- /dev/null +++ b/src/curve/catmullRomOpen.js @@ -0,0 +1,62 @@ +import {CardinalOpen} from "./cardinalOpen"; +import {point} from "./catmullRom"; + +function CatmullRomOpen(context, alpha) { + this._context = context; + this._alpha = alpha; +} + +CatmullRomOpen.prototype = { + areaStart: function() { + this._line = 0; + }, + areaEnd: function() { + this._line = NaN; + }, + lineStart: function() { + this._x0 = this._x1 = this._x2 = + this._y0 = this._y1 = this._y2 = NaN; + this._l01_a = this._l12_a = this._l23_a = + this._l01_2a = this._l12_2a = this._l23_2a = + this._point = 0; + }, + lineEnd: function() { + if (this._line || (this._line !== 0 && this._point === 3)) this._context.closePath(); + this._line = 1 - this._line; + }, + point: function(x, y) { + x = +x, y = +y; + + if (this._point) { + var x23 = this._x2 - x, + y23 = this._y2 - y; + this._l23_a = Math.sqrt(this._l23_2a = Math.pow(x23 * x23 + y23 * y23, this._alpha)); + } + + switch (this._point) { + case 0: this._point = 1; break; + case 1: this._point = 2; break; + case 2: this._point = 3; this._line ? this._context.lineTo(this._x2, this._y2) : this._context.moveTo(this._x2, this._y2); break; + case 3: this._point = 4; // proceed + default: point(this, x, y); break; + } + + this._l01_a = this._l12_a, this._l12_a = this._l23_a; + this._l01_2a = this._l12_2a, this._l12_2a = this._l23_2a; + this._x0 = this._x1, this._x1 = this._x2, this._x2 = x; + this._y0 = this._y1, this._y1 = this._y2, this._y2 = y; + } +}; + +export default (function custom(alpha) { + + function catmullRom(context) { + return alpha ? new CatmullRomOpen(context, alpha) : new CardinalOpen(context, 0); + } + + catmullRom.alpha = function(alpha) { + return custom(+alpha); + }; + + return catmullRom; +})(0.5); diff --git a/src/curve/linear.js b/src/curve/linear.js new file mode 100644 index 0000000..6245493 --- /dev/null +++ b/src/curve/linear.js @@ -0,0 +1,31 @@ +function Linear(context) { + this._context = context; +} + +Linear.prototype = { + areaStart: function() { + this._line = 0; + }, + areaEnd: function() { + this._line = NaN; + }, + lineStart: function() { + this._point = 0; + }, + lineEnd: function() { + if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath(); + this._line = 1 - this._line; + }, + point: function(x, y) { + x = +x, y = +y; + switch (this._point) { + case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break; + case 1: this._point = 2; // proceed + default: this._context.lineTo(x, y); break; + } + } +}; + +export default function(context) { + return new Linear(context); +} diff --git a/src/curve/linearClosed.js b/src/curve/linearClosed.js new file mode 100644 index 0000000..3e63e63 --- /dev/null +++ b/src/curve/linearClosed.js @@ -0,0 +1,25 @@ +import noop from "../noop"; + +function LinearClosed(context) { + this._context = context; +} + +LinearClosed.prototype = { + areaStart: noop, + areaEnd: noop, + lineStart: function() { + this._point = 0; + }, + lineEnd: function() { + if (this._point) this._context.closePath(); + }, + point: function(x, y) { + x = +x, y = +y; + if (this._point) this._context.lineTo(x, y); + else this._point = 1, this._context.moveTo(x, y); + } +}; + +export default function(context) { + return new LinearClosed(context); +} diff --git a/src/curve/monotone.js b/src/curve/monotone.js new file mode 100644 index 0000000..2599031 --- /dev/null +++ b/src/curve/monotone.js @@ -0,0 +1,104 @@ +function sign(x) { + return x < 0 ? -1 : 1; +} + +// Calculate the slopes of the tangents (Hermite-type interpolation) based on +// the following paper: Steffen, M. 1990. A Simple Method for Monotonic +// Interpolation in One Dimension. Astronomy and Astrophysics, Vol. 239, NO. +// NOV(II), P. 443, 1990. +function slope3(that, x2, y2) { + var h0 = that._x1 - that._x0, + h1 = x2 - that._x1, + s0 = (that._y1 - that._y0) / (h0 || h1 < 0 && -0), + s1 = (y2 - that._y1) / (h1 || h0 < 0 && -0), + p = (s0 * h1 + s1 * h0) / (h0 + h1); + return (sign(s0) + sign(s1)) * Math.min(Math.abs(s0), Math.abs(s1), 0.5 * Math.abs(p)) || 0; +} + +// Calculate a one-sided slope. +function slope2(that, t) { + var h = that._x1 - that._x0; + return h ? (3 * (that._y1 - that._y0) / h - t) / 2 : t; +} + +// According to https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Representations +// "you can express cubic Hermite interpolation in terms of cubic Bézier curves +// with respect to the four values p0, p0 + m0 / 3, p1 - m1 / 3, p1". +function point(that, t0, t1) { + var x0 = that._x0, + y0 = that._y0, + x1 = that._x1, + y1 = that._y1, + dx = (x1 - x0) / 3; + that._context.bezierCurveTo(x0 + dx, y0 + dx * t0, x1 - dx, y1 - dx * t1, x1, y1); +} + +function MonotoneX(context) { + this._context = context; +} + +MonotoneX.prototype = { + areaStart: function() { + this._line = 0; + }, + areaEnd: function() { + this._line = NaN; + }, + lineStart: function() { + this._x0 = this._x1 = + this._y0 = this._y1 = + this._t0 = NaN; + this._point = 0; + }, + lineEnd: function() { + switch (this._point) { + case 2: this._context.lineTo(this._x1, this._y1); break; + case 3: point(this, this._t0, slope2(this, this._t0)); break; + } + if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath(); + this._line = 1 - this._line; + }, + point: function(x, y) { + var t1 = NaN; + + x = +x, y = +y; + if (x === this._x1 && y === this._y1) return; // Ignore coincident points. + switch (this._point) { + case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break; + case 1: this._point = 2; break; + case 2: this._point = 3; point(this, slope2(this, t1 = slope3(this, x, y)), t1); break; + default: point(this, this._t0, t1 = slope3(this, x, y)); break; + } + + this._x0 = this._x1, this._x1 = x; + this._y0 = this._y1, this._y1 = y; + this._t0 = t1; + } +} + +function MonotoneY(context) { + this._context = new ReflectContext(context); +} + +(MonotoneY.prototype = Object.create(MonotoneX.prototype)).point = function(x, y) { + MonotoneX.prototype.point.call(this, y, x); +}; + +function ReflectContext(context) { + this._context = context; +} + +ReflectContext.prototype = { + moveTo: function(x, y) { this._context.moveTo(y, x); }, + closePath: function() { this._context.closePath(); }, + lineTo: function(x, y) { this._context.lineTo(y, x); }, + bezierCurveTo: function(x1, y1, x2, y2, x, y) { this._context.bezierCurveTo(y1, x1, y2, x2, y, x); } +}; + +export function monotoneX(context) { + return new MonotoneX(context); +} + +export function monotoneY(context) { + return new MonotoneY(context); +} diff --git a/src/curve/natural.js b/src/curve/natural.js new file mode 100644 index 0000000..d51eb51 --- /dev/null +++ b/src/curve/natural.js @@ -0,0 +1,65 @@ +function Natural(context) { + this._context = context; +} + +Natural.prototype = { + areaStart: function() { + this._line = 0; + }, + areaEnd: function() { + this._line = NaN; + }, + lineStart: function() { + this._x = []; + this._y = []; + }, + lineEnd: function() { + var x = this._x, + y = this._y, + n = x.length; + + if (n) { + this._line ? this._context.lineTo(x[0], y[0]) : this._context.moveTo(x[0], y[0]); + if (n === 2) { + this._context.lineTo(x[1], y[1]); + } else { + var px = controlPoints(x), + py = controlPoints(y); + for (var i0 = 0, i1 = 1; i1 < n; ++i0, ++i1) { + this._context.bezierCurveTo(px[0][i0], py[0][i0], px[1][i0], py[1][i0], x[i1], y[i1]); + } + } + } + + if (this._line || (this._line !== 0 && n === 1)) this._context.closePath(); + this._line = 1 - this._line; + this._x = this._y = null; + }, + point: function(x, y) { + this._x.push(+x); + this._y.push(+y); + } +}; + +// See https://www.particleincell.com/2012/bezier-splines/ for derivation. +function controlPoints(x) { + var i, + n = x.length - 1, + m, + a = new Array(n), + b = new Array(n), + r = new Array(n); + a[0] = 0, b[0] = 2, r[0] = x[0] + 2 * x[1]; + for (i = 1; i < n - 1; ++i) a[i] = 1, b[i] = 4, r[i] = 4 * x[i] + 2 * x[i + 1]; + a[n - 1] = 2, b[n - 1] = 7, r[n - 1] = 8 * x[n - 1] + x[n]; + for (i = 1; i < n; ++i) m = a[i] / b[i - 1], b[i] -= m, r[i] -= m * r[i - 1]; + a[n - 1] = r[n - 1] / b[n - 1]; + for (i = n - 2; i >= 0; --i) a[i] = (r[i] - a[i + 1]) / b[i]; + b[n - 1] = (x[n] + a[n - 1]) / 2; + for (i = 0; i < n - 1; ++i) b[i] = 2 * x[i + 1] - a[i + 1]; + return [a, b]; +} + +export default function(context) { + return new Natural(context); +} diff --git a/src/curve/radial.js b/src/curve/radial.js new file mode 100644 index 0000000..fb359b5 --- /dev/null +++ b/src/curve/radial.js @@ -0,0 +1,36 @@ +import curveLinear from "./linear"; + +export var curveRadialLinear = curveRadial(curveLinear); + +function Radial(curve) { + this._curve = curve; +} + +Radial.prototype = { + areaStart: function() { + this._curve.areaStart(); + }, + areaEnd: function() { + this._curve.areaEnd(); + }, + lineStart: function() { + this._curve.lineStart(); + }, + lineEnd: function() { + this._curve.lineEnd(); + }, + point: function(a, r) { + this._curve.point(r * Math.sin(a), r * -Math.cos(a)); + } +}; + +export default function curveRadial(curve) { + + function radial(context) { + return new Radial(curve(context)); + } + + radial._curve = curve; + + return radial; +} diff --git a/src/curve/step.js b/src/curve/step.js new file mode 100644 index 0000000..e9fb78f --- /dev/null +++ b/src/curve/step.js @@ -0,0 +1,53 @@ +function Step(context, t) { + this._context = context; + this._t = t; +} + +Step.prototype = { + areaStart: function() { + this._line = 0; + }, + areaEnd: function() { + this._line = NaN; + }, + lineStart: function() { + this._x = this._y = NaN; + this._point = 0; + }, + lineEnd: function() { + if (0 < this._t && this._t < 1 && this._point === 2) this._context.lineTo(this._x, this._y); + if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath(); + if (this._line >= 0) this._t = 1 - this._t, this._line = 1 - this._line; + }, + point: function(x, y) { + x = +x, y = +y; + switch (this._point) { + case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break; + case 1: this._point = 2; // proceed + default: { + if (this._t <= 0) { + this._context.lineTo(this._x, y); + this._context.lineTo(x, y); + } else { + var x1 = this._x * (1 - this._t) + x * this._t; + this._context.lineTo(x1, this._y); + this._context.lineTo(x1, y); + } + break; + } + } + this._x = x, this._y = y; + } +}; + +export default function(context) { + return new Step(context, 0.5); +} + +export function stepBefore(context) { + return new Step(context, 0); +} + +export function stepAfter(context) { + return new Step(context, 1); +} diff --git a/src/descending.js b/src/descending.js new file mode 100644 index 0000000..a4e2d7f --- /dev/null +++ b/src/descending.js @@ -0,0 +1,3 @@ +export default function(a, b) { + return b < a ? -1 : b > a ? 1 : b >= a ? 0 : NaN; +} diff --git a/src/identity.js b/src/identity.js new file mode 100644 index 0000000..ca161ab --- /dev/null +++ b/src/identity.js @@ -0,0 +1,3 @@ +export default function(d) { + return d; +} diff --git a/src/line.js b/src/line.js new file mode 100644 index 0000000..16816ce --- /dev/null +++ b/src/line.js @@ -0,0 +1,55 @@ +import {path} from "d3-path"; +import constant from "./constant"; +import curveLinear from "./curve/linear"; +import {x as pointX, y as pointY} from "./point"; + +export default function() { + var x = pointX, + y = pointY, + defined = constant(true), + context = null, + curve = curveLinear, + output = null; + + function line(data) { + var i, + n = data.length, + d, + defined0 = false, + buffer; + + if (context == null) output = curve(buffer = path()); + + for (i = 0; i <= n; ++i) { + if (!(i < n && defined(d = data[i], i, data)) === defined0) { + if (defined0 = !defined0) output.lineStart(); + else output.lineEnd(); + } + if (defined0) output.point(+x(d, i, data), +y(d, i, data)); + } + + if (buffer) return output = null, buffer + "" || null; + } + + line.x = function(_) { + return arguments.length ? (x = typeof _ === "function" ? _ : constant(+_), line) : x; + }; + + line.y = function(_) { + return arguments.length ? (y = typeof _ === "function" ? _ : constant(+_), line) : y; + }; + + line.defined = function(_) { + return arguments.length ? (defined = typeof _ === "function" ? _ : constant(!!_), line) : defined; + }; + + line.curve = function(_) { + return arguments.length ? (curve = _, context != null && (output = curve(context)), line) : curve; + }; + + line.context = function(_) { + return arguments.length ? (_ == null ? context = output = null : output = curve(context = _), line) : context; + }; + + return line; +} diff --git a/src/lineRadial.js b/src/lineRadial.js new file mode 100644 index 0000000..92697b9 --- /dev/null +++ b/src/lineRadial.js @@ -0,0 +1,19 @@ +import curveRadial, {curveRadialLinear} from "./curve/radial"; +import line from "./line"; + +export function lineRadial(l) { + var c = l.curve; + + l.angle = l.x, delete l.x; + l.radius = l.y, delete l.y; + + l.curve = function(_) { + return arguments.length ? c(curveRadial(_)) : c()._curve; + }; + + return l; +} + +export default function() { + return lineRadial(line().curve(curveRadialLinear)); +} diff --git a/src/link/index.js b/src/link/index.js new file mode 100644 index 0000000..71f2786 --- /dev/null +++ b/src/link/index.js @@ -0,0 +1,84 @@ +import {path} from "d3-path"; +import {slice} from "../array"; +import constant from "../constant"; +import {x as pointX, y as pointY} from "../point"; +import pointRadial from "../pointRadial"; + +function linkSource(d) { + return d.source; +} + +function linkTarget(d) { + return d.target; +} + +function link(curve) { + var source = linkSource, + target = linkTarget, + x = pointX, + y = pointY, + context = null; + + function link() { + var buffer, argv = slice.call(arguments), s = source.apply(this, argv), t = target.apply(this, argv); + if (!context) context = buffer = path(); + curve(context, +x.apply(this, (argv[0] = s, argv)), +y.apply(this, argv), +x.apply(this, (argv[0] = t, argv)), +y.apply(this, argv)); + if (buffer) return context = null, buffer + "" || null; + } + + link.source = function(_) { + return arguments.length ? (source = _, link) : source; + }; + + link.target = function(_) { + return arguments.length ? (target = _, link) : target; + }; + + link.x = function(_) { + return arguments.length ? (x = typeof _ === "function" ? _ : constant(+_), link) : x; + }; + + link.y = function(_) { + return arguments.length ? (y = typeof _ === "function" ? _ : constant(+_), link) : y; + }; + + link.context = function(_) { + return arguments.length ? ((context = _ == null ? null : _), link) : context; + }; + + return link; +} + +function curveHorizontal(context, x0, y0, x1, y1) { + context.moveTo(x0, y0); + context.bezierCurveTo(x0 = (x0 + x1) / 2, y0, x0, y1, x1, y1); +} + +function curveVertical(context, x0, y0, x1, y1) { + context.moveTo(x0, y0); + context.bezierCurveTo(x0, y0 = (y0 + y1) / 2, x1, y0, x1, y1); +} + +function curveRadial(context, x0, y0, x1, y1) { + var p0 = pointRadial(x0, y0), + p1 = pointRadial(x0, y0 = (y0 + y1) / 2), + p2 = pointRadial(x1, y0), + p3 = pointRadial(x1, y1); + context.moveTo(p0[0], p0[1]); + context.bezierCurveTo(p1[0], p1[1], p2[0], p2[1], p3[0], p3[1]); +} + +export function linkHorizontal() { + return link(curveHorizontal); +} + +export function linkVertical() { + return link(curveVertical); +} + +export function linkRadial() { + var l = link(curveRadial); + l.angle = l.x, delete l.x; + l.radius = l.y, delete l.y; + return l; +} diff --git a/src/math.js b/src/math.js new file mode 100644 index 0000000..131af62 --- /dev/null +++ b/src/math.js @@ -0,0 +1,20 @@ +export var abs = Math.abs; +export var atan2 = Math.atan2; +export var cos = Math.cos; +export var max = Math.max; +export var min = Math.min; +export var sin = Math.sin; +export var sqrt = Math.sqrt; + +export var epsilon = 1e-12; +export var pi = Math.PI; +export var halfPi = pi / 2; +export var tau = 2 * pi; + +export function acos(x) { + return x > 1 ? 0 : x < -1 ? pi : Math.acos(x); +} + +export function asin(x) { + return x >= 1 ? halfPi : x <= -1 ? -halfPi : Math.asin(x); +} diff --git a/src/noop.js b/src/noop.js new file mode 100644 index 0000000..6ab80bc --- /dev/null +++ b/src/noop.js @@ -0,0 +1 @@ +export default function() {} diff --git a/src/offset/diverging.js b/src/offset/diverging.js new file mode 100644 index 0000000..fd20899 --- /dev/null +++ b/src/offset/diverging.js @@ -0,0 +1,14 @@ +export default function(series, order) { + if (!((n = series.length) > 1)) return; + for (var i, j = 0, d, dy, yp, yn, n, m = series[order[0]].length; j < m; ++j) { + for (yp = yn = 0, i = 0; i < n; ++i) { + if ((dy = (d = series[order[i]][j])[1] - d[0]) >= 0) { + d[0] = yp, d[1] = yp += dy; + } else if (dy < 0) { + d[1] = yn, d[0] = yn += dy; + } else { + d[0] = yp; + } + } + } +} diff --git a/src/offset/expand.js b/src/offset/expand.js new file mode 100644 index 0000000..3aa0389 --- /dev/null +++ b/src/offset/expand.js @@ -0,0 +1,10 @@ +import none from "./none"; + +export default function(series, order) { + if (!((n = series.length) > 0)) return; + for (var i, n, j = 0, m = series[0].length, y; j < m; ++j) { + for (y = i = 0; i < n; ++i) y += series[i][j][1] || 0; + if (y) for (i = 0; i < n; ++i) series[i][j][1] /= y; + } + none(series, order); +} diff --git a/src/offset/none.js b/src/offset/none.js new file mode 100644 index 0000000..d8e11dc --- /dev/null +++ b/src/offset/none.js @@ -0,0 +1,9 @@ +export default function(series, order) { + if (!((n = series.length) > 1)) return; + for (var i = 1, j, s0, s1 = series[order[0]], n, m = s1.length; i < n; ++i) { + s0 = s1, s1 = series[order[i]]; + for (j = 0; j < m; ++j) { + s1[j][1] += s1[j][0] = isNaN(s0[j][1]) ? s0[j][0] : s0[j][1]; + } + } +} diff --git a/src/offset/silhouette.js b/src/offset/silhouette.js new file mode 100644 index 0000000..31afd25 --- /dev/null +++ b/src/offset/silhouette.js @@ -0,0 +1,10 @@ +import none from "./none"; + +export default function(series, order) { + if (!((n = series.length) > 0)) return; + for (var j = 0, s0 = series[order[0]], n, m = s0.length; j < m; ++j) { + for (var i = 0, y = 0; i < n; ++i) y += series[i][j][1] || 0; + s0[j][1] += s0[j][0] = -y / 2; + } + none(series, order); +} diff --git a/src/offset/wiggle.js b/src/offset/wiggle.js new file mode 100644 index 0000000..0f8c12b --- /dev/null +++ b/src/offset/wiggle.js @@ -0,0 +1,24 @@ +import none from "./none"; + +export default function(series, order) { + if (!((n = series.length) > 0) || !((m = (s0 = series[order[0]]).length) > 0)) return; + for (var y = 0, j = 1, s0, m, n; j < m; ++j) { + for (var i = 0, s1 = 0, s2 = 0; i < n; ++i) { + var si = series[order[i]], + sij0 = si[j][1] || 0, + sij1 = si[j - 1][1] || 0, + s3 = (sij0 - sij1) / 2; + for (var k = 0; k < i; ++k) { + var sk = series[order[k]], + skj0 = sk[j][1] || 0, + skj1 = sk[j - 1][1] || 0; + s3 += skj0 - skj1; + } + s1 += sij0, s2 += s3 * sij0; + } + s0[j - 1][1] += s0[j - 1][0] = y; + if (s1) y -= s2 / s1; + } + s0[j - 1][1] += s0[j - 1][0] = y; + none(series, order); +} diff --git a/src/order/ascending.js b/src/order/ascending.js new file mode 100644 index 0000000..c97e118 --- /dev/null +++ b/src/order/ascending.js @@ -0,0 +1,12 @@ +import none from "./none"; + +export default function(series) { + var sums = series.map(sum); + return none(series).sort(function(a, b) { return sums[a] - sums[b]; }); +} + +export function sum(series) { + var s = 0, i = -1, n = series.length, v; + while (++i < n) if (v = +series[i][1]) s += v; + return s; +} diff --git a/src/order/descending.js b/src/order/descending.js new file mode 100644 index 0000000..c65b9a4 --- /dev/null +++ b/src/order/descending.js @@ -0,0 +1,5 @@ +import ascending from "./ascending"; + +export default function(series) { + return ascending(series).reverse(); +} diff --git a/src/order/insideOut.js b/src/order/insideOut.js new file mode 100644 index 0000000..459d21c --- /dev/null +++ b/src/order/insideOut.js @@ -0,0 +1,27 @@ +import none from "./none"; +import {sum} from "./ascending"; + +export default function(series) { + var n = series.length, + i, + j, + sums = series.map(sum), + order = none(series).sort(function(a, b) { return sums[b] - sums[a]; }), + top = 0, + bottom = 0, + tops = [], + bottoms = []; + + for (i = 0; i < n; ++i) { + j = order[i]; + if (top < bottom) { + top += sums[j]; + tops.push(j); + } else { + bottom += sums[j]; + bottoms.push(j); + } + } + + return bottoms.reverse().concat(tops); +} diff --git a/src/order/none.js b/src/order/none.js new file mode 100644 index 0000000..b0d7d6f --- /dev/null +++ b/src/order/none.js @@ -0,0 +1,5 @@ +export default function(series) { + var n = series.length, o = new Array(n); + while (--n >= 0) o[n] = n; + return o; +} diff --git a/src/order/reverse.js b/src/order/reverse.js new file mode 100644 index 0000000..b93af7c --- /dev/null +++ b/src/order/reverse.js @@ -0,0 +1,5 @@ +import none from "./none"; + +export default function(series) { + return none(series).reverse(); +} diff --git a/src/pie.js b/src/pie.js new file mode 100644 index 0000000..49cbc78 --- /dev/null +++ b/src/pie.js @@ -0,0 +1,79 @@ +import constant from "./constant"; +import descending from "./descending"; +import identity from "./identity"; +import {tau} from "./math"; + +export default function() { + var value = identity, + sortValues = descending, + sort = null, + startAngle = constant(0), + endAngle = constant(tau), + padAngle = constant(0); + + function pie(data) { + var i, + n = data.length, + j, + k, + sum = 0, + index = new Array(n), + arcs = new Array(n), + a0 = +startAngle.apply(this, arguments), + da = Math.min(tau, Math.max(-tau, endAngle.apply(this, arguments) - a0)), + a1, + p = Math.min(Math.abs(da) / n, padAngle.apply(this, arguments)), + pa = p * (da < 0 ? -1 : 1), + v; + + for (i = 0; i < n; ++i) { + if ((v = arcs[index[i] = i] = +value(data[i], i, data)) > 0) { + sum += v; + } + } + + // Optionally sort the arcs by previously-computed values or by data. + if (sortValues != null) index.sort(function(i, j) { return sortValues(arcs[i], arcs[j]); }); + else if (sort != null) index.sort(function(i, j) { return sort(data[i], data[j]); }); + + // Compute the arcs! They are stored in the original data's order. + for (i = 0, k = sum ? (da - n * pa) / sum : 0; i < n; ++i, a0 = a1) { + j = index[i], v = arcs[j], a1 = a0 + (v > 0 ? v * k : 0) + pa, arcs[j] = { + data: data[j], + index: i, + value: v, + startAngle: a0, + endAngle: a1, + padAngle: p + }; + } + + return arcs; + } + + pie.value = function(_) { + return arguments.length ? (value = typeof _ === "function" ? _ : constant(+_), pie) : value; + }; + + pie.sortValues = function(_) { + return arguments.length ? (sortValues = _, sort = null, pie) : sortValues; + }; + + pie.sort = function(_) { + return arguments.length ? (sort = _, sortValues = null, pie) : sort; + }; + + pie.startAngle = function(_) { + return arguments.length ? (startAngle = typeof _ === "function" ? _ : constant(+_), pie) : startAngle; + }; + + pie.endAngle = function(_) { + return arguments.length ? (endAngle = typeof _ === "function" ? _ : constant(+_), pie) : endAngle; + }; + + pie.padAngle = function(_) { + return arguments.length ? (padAngle = typeof _ === "function" ? _ : constant(+_), pie) : padAngle; + }; + + return pie; +} diff --git a/src/point.js b/src/point.js new file mode 100644 index 0000000..c345257 --- /dev/null +++ b/src/point.js @@ -0,0 +1,7 @@ +export function x(p) { + return p[0]; +} + +export function y(p) { + return p[1]; +} diff --git a/src/pointRadial.js b/src/pointRadial.js new file mode 100644 index 0000000..1cccb70 --- /dev/null +++ b/src/pointRadial.js @@ -0,0 +1,3 @@ +export default function(x, y) { + return [(y = +y) * Math.cos(x -= Math.PI / 2), y * Math.sin(x)]; +} diff --git a/src/stack.js b/src/stack.js new file mode 100644 index 0000000..e36e885 --- /dev/null +++ b/src/stack.js @@ -0,0 +1,57 @@ +import {slice} from "./array"; +import constant from "./constant"; +import offsetNone from "./offset/none"; +import orderNone from "./order/none"; + +function stackValue(d, key) { + return d[key]; +} + +export default function() { + var keys = constant([]), + order = orderNone, + offset = offsetNone, + value = stackValue; + + function stack(data) { + var kz = keys.apply(this, arguments), + i, + m = data.length, + n = kz.length, + sz = new Array(n), + oz; + + for (i = 0; i < n; ++i) { + for (var ki = kz[i], si = sz[i] = new Array(m), j = 0, sij; j < m; ++j) { + si[j] = sij = [0, +value(data[j], ki, j, data)]; + sij.data = data[j]; + } + si.key = ki; + } + + for (i = 0, oz = order(sz); i < n; ++i) { + sz[oz[i]].index = i; + } + + offset(sz, oz); + return sz; + } + + stack.keys = function(_) { + return arguments.length ? (keys = typeof _ === "function" ? _ : constant(slice.call(_)), stack) : keys; + }; + + stack.value = function(_) { + return arguments.length ? (value = typeof _ === "function" ? _ : constant(+_), stack) : value; + }; + + stack.order = function(_) { + return arguments.length ? (order = _ == null ? orderNone : typeof _ === "function" ? _ : constant(slice.call(_)), stack) : order; + }; + + stack.offset = function(_) { + return arguments.length ? (offset = _ == null ? offsetNone : _, stack) : offset; + }; + + return stack; +} diff --git a/src/symbol/circle.js b/src/symbol/circle.js new file mode 100644 index 0000000..ff8098d --- /dev/null +++ b/src/symbol/circle.js @@ -0,0 +1,9 @@ +import {pi, tau} from "../math"; + +export default { + draw: function(context, size) { + var r = Math.sqrt(size / pi); + context.moveTo(r, 0); + context.arc(0, 0, r, 0, tau); + } +}; diff --git a/src/symbol/cross.js b/src/symbol/cross.js new file mode 100644 index 0000000..a282f80 --- /dev/null +++ b/src/symbol/cross.js @@ -0,0 +1,18 @@ +export default { + draw: function(context, size) { + var r = Math.sqrt(size / 5) / 2; + context.moveTo(-3 * r, -r); + context.lineTo(-r, -r); + context.lineTo(-r, -3 * r); + context.lineTo(r, -3 * r); + context.lineTo(r, -r); + context.lineTo(3 * r, -r); + context.lineTo(3 * r, r); + context.lineTo(r, r); + context.lineTo(r, 3 * r); + context.lineTo(-r, 3 * r); + context.lineTo(-r, r); + context.lineTo(-3 * r, r); + context.closePath(); + } +}; diff --git a/src/symbol/diamond.js b/src/symbol/diamond.js new file mode 100644 index 0000000..9f4ff1a --- /dev/null +++ b/src/symbol/diamond.js @@ -0,0 +1,14 @@ +var tan30 = Math.sqrt(1 / 3), + tan30_2 = tan30 * 2; + +export default { + draw: function(context, size) { + var y = Math.sqrt(size / tan30_2), + x = y * tan30; + context.moveTo(0, -y); + context.lineTo(x, 0); + context.lineTo(0, y); + context.lineTo(-x, 0); + context.closePath(); + } +}; diff --git a/src/symbol/square.js b/src/symbol/square.js new file mode 100644 index 0000000..10beccd --- /dev/null +++ b/src/symbol/square.js @@ -0,0 +1,7 @@ +export default { + draw: function(context, size) { + var w = Math.sqrt(size), + x = -w / 2; + context.rect(x, x, w, w); + } +}; diff --git a/src/symbol/star.js b/src/symbol/star.js new file mode 100644 index 0000000..7d3c19a --- /dev/null +++ b/src/symbol/star.js @@ -0,0 +1,24 @@ +import {pi, tau} from "../math"; + +var ka = 0.89081309152928522810, + kr = Math.sin(pi / 10) / Math.sin(7 * pi / 10), + kx = Math.sin(tau / 10) * kr, + ky = -Math.cos(tau / 10) * kr; + +export default { + draw: function(context, size) { + var r = Math.sqrt(size * ka), + x = kx * r, + y = ky * r; + context.moveTo(0, -r); + context.lineTo(x, y); + for (var i = 1; i < 5; ++i) { + var a = tau * i / 5, + c = Math.cos(a), + s = Math.sin(a); + context.lineTo(s * r, -c * r); + context.lineTo(c * x - s * y, s * x + c * y); + } + context.closePath(); + } +}; diff --git a/src/symbol/triangle.js b/src/symbol/triangle.js new file mode 100644 index 0000000..a323d20 --- /dev/null +++ b/src/symbol/triangle.js @@ -0,0 +1,11 @@ +var sqrt3 = Math.sqrt(3); + +export default { + draw: function(context, size) { + var y = -Math.sqrt(size / (sqrt3 * 3)); + context.moveTo(0, y * 2); + context.lineTo(-sqrt3 * y, -y); + context.lineTo(sqrt3 * y, -y); + context.closePath(); + } +}; diff --git a/src/symbol/wye.js b/src/symbol/wye.js new file mode 100644 index 0000000..697f2c3 --- /dev/null +++ b/src/symbol/wye.js @@ -0,0 +1,26 @@ +var c = -0.5, + s = Math.sqrt(3) / 2, + k = 1 / Math.sqrt(12), + a = (k / 2 + 1) * 3; + +export default { + draw: function(context, size) { + var r = Math.sqrt(size / a), + x0 = r / 2, + y0 = r * k, + x1 = x0, + y1 = r * k + r, + x2 = -x1, + y2 = y1; + context.moveTo(x0, y0); + context.lineTo(x1, y1); + context.lineTo(x2, y2); + context.lineTo(c * x0 - s * y0, s * x0 + c * y0); + context.lineTo(c * x1 - s * y1, s * x1 + c * y1); + context.lineTo(c * x2 - s * y2, s * x2 + c * y2); + context.lineTo(c * x0 + s * y0, c * y0 - s * x0); + context.lineTo(c * x1 + s * y1, c * y1 - s * x1); + context.lineTo(c * x2 + s * y2, c * y2 - s * x2); + context.closePath(); + } +}; diff --git a/src/symbol.js b/src/symbol.js new file mode 100644 index 0000000..bd147ce --- /dev/null +++ b/src/symbol.js @@ -0,0 +1,46 @@ +import {path} from "d3-path"; +import circle from "./symbol/circle"; +import cross from "./symbol/cross"; +import diamond from "./symbol/diamond"; +import star from "./symbol/star"; +import square from "./symbol/square"; +import triangle from "./symbol/triangle"; +import wye from "./symbol/wye"; +import constant from "./constant"; + +export var symbols = [ + circle, + cross, + diamond, + square, + star, + triangle, + wye +]; + +export default function() { + var type = constant(circle), + size = constant(64), + context = null; + + function symbol() { + var buffer; + if (!context) context = buffer = path(); + type.apply(this, arguments).draw(context, +size.apply(this, arguments)); + if (buffer) return context = null, buffer + "" || null; + } + + symbol.type = function(_) { + return arguments.length ? (type = typeof _ === "function" ? _ : constant(_), symbol) : type; + }; + + symbol.size = function(_) { + return arguments.length ? (size = typeof _ === "function" ? _ : constant(+_), symbol) : size; + }; + + symbol.context = function(_) { + return arguments.length ? (context = _ == null ? null : _, symbol) : context; + }; + + return symbol; +} diff --git a/test/arc-test.js b/test/arc-test.js new file mode 100644 index 0000000..754ea06 --- /dev/null +++ b/test/arc-test.js @@ -0,0 +1,396 @@ +var tape = require("tape"), + shape = require("../"); + +require("./pathEqual"); + +tape("arc().innerRadius(f)(…) propagates the context and arguments to the specified function f", function(test) { + var expected = {that: {}, args: [42]}, actual; + shape.arc().innerRadius(function() { actual = {that: this, args: [].slice.call(arguments)}; }).apply(expected.that, expected.args); + test.deepEqual(actual, expected); + test.end(); +}); + +tape("arc().outerRadius(f)(…) propagates the context and arguments to the specified function f", function(test) { + var expected = {that: {}, args: [42]}, actual; + shape.arc().outerRadius(function() { actual = {that: this, args: [].slice.call(arguments)}; }).apply(expected.that, expected.args); + test.deepEqual(actual, expected); + test.end(); +}); + +tape("arc().cornerRadius(f)(…) propagates the context and arguments to the specified function f", function(test) { + var expected = {that: {}, args: [42]}, actual; + shape.arc().outerRadius(100).cornerRadius(function() { actual = {that: this, args: [].slice.call(arguments)}; }).apply(expected.that, expected.args); + test.deepEqual(actual, expected); + test.end(); +}); + +tape("arc().startAngle(f)(…) propagates the context and arguments to the specified function f", function(test) { + var expected = {that: {}, args: [42]}, actual; + shape.arc().startAngle(function() { actual = {that: this, args: [].slice.call(arguments)}; }).apply(expected.that, expected.args); + test.deepEqual(actual, expected); + test.end(); +}); + +tape("arc().endAngle(f)(…) propagates the context and arguments to the specified function f", function(test) { + var expected = {that: {}, args: [42]}, actual; + shape.arc().endAngle(function() { actual = {that: this, args: [].slice.call(arguments)}; }).apply(expected.that, expected.args); + test.deepEqual(actual, expected); + test.end(); +}); + +tape("arc().padAngle(f)(…) propagates the context and arguments to the specified function f", function(test) { + var expected = {that: {}, args: [42]}, actual; + shape.arc().outerRadius(100).startAngle(Math.PI / 2).padAngle(function() { actual = {that: this, args: [].slice.call(arguments)}; }).apply(expected.that, expected.args); + test.deepEqual(actual, expected); + test.end(); +}); + +tape("arc().padRadius(f)(…) propagates the context and arguments to the specified function f", function(test) { + var expected = {that: {}, args: [42]}, actual; + shape.arc().outerRadius(100).startAngle(Math.PI / 2).padAngle(0.1).padRadius(function() { actual = {that: this, args: [].slice.call(arguments)}; }).apply(expected.that, expected.args); + test.deepEqual(actual, expected); + test.end(); +}); + +tape("arc().centroid(…) computes the midpoint of the center line of the arc", function(test) { + var a = shape.arc(), round = function(x) { return Math.round(x * 1e6) / 1e6; }; + test.deepEqual(a.centroid({innerRadius: 0, outerRadius: 100, startAngle: 0, endAngle: Math.PI}).map(round), [50, 0]); + test.deepEqual(a.centroid({innerRadius: 0, outerRadius: 100, startAngle: 0, endAngle: Math.PI / 2}).map(round), [35.355339, -35.355339]); + test.deepEqual(a.centroid({innerRadius: 50, outerRadius: 100, startAngle: 0, endAngle: -Math.PI}).map(round), [-75, 0]); + test.deepEqual(a.centroid({innerRadius: 50, outerRadius: 100, startAngle: 0, endAngle: -Math.PI / 2}).map(round), [-53.033009, -53.033009]); + test.end(); +}); + +tape("arc().innerRadius(f).centroid(…) propagates the context and arguments to the specified function f", function(test) { + var expected = {that: {}, args: [42]}, actual; + shape.arc().innerRadius(function() { actual = {that: this, args: [].slice.call(arguments)}; }).centroid.apply(expected.that, expected.args); + test.deepEqual(actual, expected); + test.end(); +}); + +tape("arc().outerRadius(f).centroid(…) propagates the context and arguments to the specified function f", function(test) { + var expected = {that: {}, args: [42]}, actual; + shape.arc().outerRadius(function() { actual = {that: this, args: [].slice.call(arguments)}; }).centroid.apply(expected.that, expected.args); + test.deepEqual(actual, expected); + test.end(); +}); + +tape("arc().startAngle(f).centroid(…) propagates the context and arguments to the specified function f", function(test) { + var expected = {that: {}, args: [42]}, actual; + shape.arc().startAngle(function() { actual = {that: this, args: [].slice.call(arguments)}; }).centroid.apply(expected.that, expected.args); + test.deepEqual(actual, expected); + test.end(); +}); + +tape("arc().endAngle(f).centroid(…) propagates the context and arguments to the specified function f", function(test) { + var expected = {that: {}, args: [42]}, actual; + shape.arc().endAngle(function() { actual = {that: this, args: [].slice.call(arguments)}; }).centroid.apply(expected.that, expected.args); + test.deepEqual(actual, expected); + test.end(); +}); + +tape("arc().innerRadius(0).outerRadius(0) renders a point", function(test) { + var a = shape.arc().innerRadius(0).outerRadius(0); + test.pathEqual(a.startAngle(0).endAngle(2 * Math.PI)(), "M0,0Z"); + test.pathEqual(a.startAngle(0).endAngle(0)(), "M0,0Z"); + test.end(); +}); + +tape("arc().innerRadius(0).outerRadius(r).startAngle(θ₀).endAngle(θ₁) renders a clockwise circle if r > 0 and θ₁ - θ₀ ≥ τ", function(test) { + var a = shape.arc().innerRadius(0).outerRadius(100); + test.pathEqual(a.startAngle(0).endAngle(2 * Math.PI)(), "M0,-100A100,100,0,1,1,0,100A100,100,0,1,1,0,-100Z"); + test.pathEqual(a.startAngle(0).endAngle(3 * Math.PI)(), "M0,-100A100,100,0,1,1,0,100A100,100,0,1,1,0,-100Z"); + test.pathEqual(a.startAngle(-2 * Math.PI).endAngle(0)(), "M0,-100A100,100,0,1,1,0,100A100,100,0,1,1,0,-100Z"); + test.pathEqual(a.startAngle(-Math.PI).endAngle(Math.PI)(), "M0,100A100,100,0,1,1,0,-100A100,100,0,1,1,0,100Z"); + test.pathEqual(a.startAngle(-3 * Math.PI).endAngle(0)(), "M0,100A100,100,0,1,1,0,-100A100,100,0,1,1,0,100Z"); + test.end(); +}); + +tape("arc().innerRadius(0).outerRadius(r).startAngle(θ₀).endAngle(θ₁) renders an anticlockwise circle if r > 0 and θ₀ - θ₁ ≥ τ", function(test) { + var a = shape.arc().innerRadius(0).outerRadius(100); + test.pathEqual(a.startAngle(0).endAngle(-2 * Math.PI)(), "M0,-100A100,100,0,1,0,0,100A100,100,0,1,0,0,-100Z"); + test.pathEqual(a.startAngle(0).endAngle(-3 * Math.PI)(), "M0,-100A100,100,0,1,0,0,100A100,100,0,1,0,0,-100Z"); + test.pathEqual(a.startAngle(2 * Math.PI).endAngle(0)(), "M0,-100A100,100,0,1,0,0,100A100,100,0,1,0,0,-100Z"); + test.pathEqual(a.startAngle(Math.PI).endAngle(-Math.PI)(), "M0,100A100,100,0,1,0,0,-100A100,100,0,1,0,0,100Z"); + test.pathEqual(a.startAngle(3 * Math.PI).endAngle(0)(), "M0,100A100,100,0,1,0,0,-100A100,100,0,1,0,0,100Z"); + test.end(); +}); + +// Note: The outer ring starts and ends at θ₀, but the inner ring starts and ends at θ₁. +// Note: The outer ring is clockwise, but the inner ring is anticlockwise. +tape("arc().innerRadius(r₀).outerRadius(r₁).startAngle(θ₀).endAngle(θ₁) renders a clockwise annulus if r₀ > 0, r₁ > 0 and θ₀ - θ₁ ≥ τ", function(test) { + var a = shape.arc().innerRadius(50).outerRadius(100); + test.pathEqual(a.startAngle(0).endAngle(2 * Math.PI)(), "M0,-100A100,100,0,1,1,0,100A100,100,0,1,1,0,-100M0,-50A50,50,0,1,0,0,50A50,50,0,1,0,0,-50Z"); + test.pathEqual(a.startAngle(0).endAngle(3 * Math.PI)(), "M0,-100A100,100,0,1,1,0,100A100,100,0,1,1,0,-100M0,50A50,50,0,1,0,0,-50A50,50,0,1,0,0,50Z"); + test.pathEqual(a.startAngle(-2 * Math.PI).endAngle(0)(), "M0,-100A100,100,0,1,1,0,100A100,100,0,1,1,0,-100M0,-50A50,50,0,1,0,0,50A50,50,0,1,0,0,-50Z"); + test.pathEqual(a.startAngle(-Math.PI).endAngle(Math.PI)(), "M0,100A100,100,0,1,1,0,-100A100,100,0,1,1,0,100M0,50A50,50,0,1,0,0,-50A50,50,0,1,0,0,50Z"); + test.pathEqual(a.startAngle(-3 * Math.PI).endAngle(0)(), "M0,100A100,100,0,1,1,0,-100A100,100,0,1,1,0,100M0,-50A50,50,0,1,0,0,50A50,50,0,1,0,0,-50Z"); + test.end(); +}); + +// Note: The outer ring starts and ends at θ₀, but the inner ring starts and ends at θ₁. +// Note: The outer ring is anticlockwise, but the inner ring is clockwise. +tape("arc().innerRadius(r₀).outerRadius(r₁).startAngle(θ₀).endAngle(θ₁) renders an anticlockwise annulus if r₀ > 0, r₁ > 0 and θ₁ - θ₀ ≥ τ", function(test) { + var a = shape.arc().innerRadius(50).outerRadius(100); + test.pathEqual(a.startAngle(0).endAngle(-2 * Math.PI)(), "M0,-100A100,100,0,1,0,0,100A100,100,0,1,0,0,-100M0,-50A50,50,0,1,1,0,50A50,50,0,1,1,0,-50Z"); + test.pathEqual(a.startAngle(0).endAngle(-3 * Math.PI)(), "M0,-100A100,100,0,1,0,0,100A100,100,0,1,0,0,-100M0,50A50,50,0,1,1,0,-50A50,50,0,1,1,0,50Z"); + test.pathEqual(a.startAngle(2 * Math.PI).endAngle(0)(), "M0,-100A100,100,0,1,0,0,100A100,100,0,1,0,0,-100M0,-50A50,50,0,1,1,0,50A50,50,0,1,1,0,-50Z"); + test.pathEqual(a.startAngle(Math.PI).endAngle(-Math.PI)(), "M0,100A100,100,0,1,0,0,-100A100,100,0,1,0,0,100M0,50A50,50,0,1,1,0,-50A50,50,0,1,1,0,50Z"); + test.pathEqual(a.startAngle(3 * Math.PI).endAngle(0)(), "M0,100A100,100,0,1,0,0,-100A100,100,0,1,0,0,100M0,-50A50,50,0,1,1,0,50A50,50,0,1,1,0,-50Z"); + test.end(); +}); + +tape("arc().innerRadius(0).outerRadius(r).startAngle(θ₀).endAngle(θ₁) renders a small clockwise sector if r > 0 and π > θ₁ - θ₀ ≥ 0", function(test) { + var a = shape.arc().innerRadius(0).outerRadius(100); + test.pathEqual(a.startAngle(0).endAngle(Math.PI / 2)(), "M0,-100A100,100,0,0,1,100,0L0,0Z"); + test.pathEqual(a.startAngle(2 * Math.PI).endAngle(5 * Math.PI / 2)(), "M0,-100A100,100,0,0,1,100,0L0,0Z"); + test.pathEqual(a.startAngle(-Math.PI).endAngle(-Math.PI / 2)(), "M0,100A100,100,0,0,1,-100,0L0,0Z"); + test.end(); +}); + +tape("arc().innerRadius(0).outerRadius(r).startAngle(θ₀).endAngle(θ₁) renders a small anticlockwise sector if r > 0 and π > θ₀ - θ₁ ≥ 0", function(test) { + var a = shape.arc().innerRadius(0).outerRadius(100); + test.pathEqual(a.startAngle(0).endAngle(-Math.PI / 2)(), "M0,-100A100,100,0,0,0,-100,0L0,0Z"); + test.pathEqual(a.startAngle(-2 * Math.PI).endAngle(-5 * Math.PI / 2)(), "M0,-100A100,100,0,0,0,-100,0L0,0Z"); + test.pathEqual(a.startAngle(Math.PI).endAngle(Math.PI / 2)(), "M0,100A100,100,0,0,0,100,0L0,0Z"); + test.end(); +}); + +tape("arc().innerRadius(0).outerRadius(r).startAngle(θ₀).endAngle(θ₁) renders a large clockwise sector if r > 0 and τ > θ₁ - θ₀ ≥ π", function(test) { + var a = shape.arc().innerRadius(0).outerRadius(100); + test.pathEqual(a.startAngle(0).endAngle(3 * Math.PI / 2)(), "M0,-100A100,100,0,1,1,-100,0L0,0Z"); + test.pathEqual(a.startAngle(2 * Math.PI).endAngle(7 * Math.PI / 2)(), "M0,-100A100,100,0,1,1,-100,0L0,0Z"); + test.pathEqual(a.startAngle(-Math.PI).endAngle(Math.PI / 2)(), "M0,100A100,100,0,1,1,100,0L0,0Z"); + test.end(); +}); + +tape("arc().innerRadius(0).outerRadius(r).startAngle(θ₀).endAngle(θ₁) renders a large anticlockwise sector if r > 0 and τ > θ₀ - θ₁ ≥ π", function(test) { + var a = shape.arc().innerRadius(0).outerRadius(100); + test.pathEqual(a.startAngle(0).endAngle(-3 * Math.PI / 2)(), "M0,-100A100,100,0,1,0,100,0L0,0Z"); + test.pathEqual(a.startAngle(-2 * Math.PI).endAngle(-7 * Math.PI / 2)(), "M0,-100A100,100,0,1,0,100,0L0,0Z"); + test.pathEqual(a.startAngle(Math.PI).endAngle(-Math.PI / 2)(), "M0,100A100,100,0,1,0,-100,0L0,0Z"); + test.end(); +}); + +// Note: The outer ring is clockwise, but the inner ring is anticlockwise. +tape("arc().innerRadius(r₀).outerRadius(r₁).startAngle(θ₀).endAngle(θ₁) renders a small clockwise annular sector if r₀ > 0, r₁ > 0 and π > θ₁ - θ₀ ≥ 0", function(test) { + var a = shape.arc().innerRadius(50).outerRadius(100); + test.pathEqual(a.startAngle(0).endAngle(Math.PI / 2)(), "M0,-100A100,100,0,0,1,100,0L50,0A50,50,0,0,0,0,-50Z"); + test.pathEqual(a.startAngle(2 * Math.PI).endAngle(5 * Math.PI / 2)(), "M0,-100A100,100,0,0,1,100,0L50,0A50,50,0,0,0,0,-50Z"); + test.pathEqual(a.startAngle(-Math.PI).endAngle(-Math.PI / 2)(), "M0,100A100,100,0,0,1,-100,0L-50,0A50,50,0,0,0,0,50Z"); + test.end(); +}); + +// Note: The outer ring is anticlockwise, but the inner ring is clockwise. +tape("arc().innerRadius(r₀).outerRadius(r₁).startAngle(θ₀).endAngle(θ₁) renders a small anticlockwise annular sector if r₀ > 0, r₁ > 0 and π > θ₀ - θ₁ ≥ 0", function(test) { + var a = shape.arc().innerRadius(50).outerRadius(100); + test.pathEqual(a.startAngle(0).endAngle(-Math.PI / 2)(), "M0,-100A100,100,0,0,0,-100,0L-50,0A50,50,0,0,1,0,-50Z"); + test.pathEqual(a.startAngle(-2 * Math.PI).endAngle(-5 * Math.PI / 2)(), "M0,-100A100,100,0,0,0,-100,0L-50,0A50,50,0,0,1,0,-50Z"); + test.pathEqual(a.startAngle(Math.PI).endAngle(Math.PI / 2)(), "M0,100A100,100,0,0,0,100,0L50,0A50,50,0,0,1,0,50Z"); + test.end(); +}); + +// Note: The outer ring is clockwise, but the inner ring is anticlockwise. +tape("arc().innerRadius(r₀).outerRadius(r₁).startAngle(θ₀).endAngle(θ₁) renders a large clockwise annular sector if r₀ > 0, r₁ > 0 and τ > θ₁ - θ₀ ≥ π", function(test) { + var a = shape.arc().innerRadius(50).outerRadius(100); + test.pathEqual(a.startAngle(0).endAngle(3 * Math.PI / 2)(), "M0,-100A100,100,0,1,1,-100,0L-50,0A50,50,0,1,0,0,-50Z"); + test.pathEqual(a.startAngle(2 * Math.PI).endAngle(7 * Math.PI / 2)(), "M0,-100A100,100,0,1,1,-100,0L-50,0A50,50,0,1,0,0,-50Z"); + test.pathEqual(a.startAngle(-Math.PI).endAngle(Math.PI / 2)(), "M0,100A100,100,0,1,1,100,0L50,0A50,50,0,1,0,0,50Z"); + test.end(); +}); + +// Note: The outer ring is anticlockwise, but the inner ring is clockwise. +tape("arc().innerRadius(r₀).outerRadius(r₁).startAngle(θ₀).endAngle(θ₁) renders a large anticlockwise annular sector if r₀ > 0, r₁ > 0 and τ > θ₀ - θ₁ ≥ π", function(test) { + var a = shape.arc().innerRadius(50).outerRadius(100); + test.pathEqual(a.startAngle(0).endAngle(-3 * Math.PI / 2)(), "M0,-100A100,100,0,1,0,100,0L50,0A50,50,0,1,1,0,-50Z"); + test.pathEqual(a.startAngle(-2 * Math.PI).endAngle(-7 * Math.PI / 2)(), "M0,-100A100,100,0,1,0,100,0L50,0A50,50,0,1,1,0,-50Z"); + test.pathEqual(a.startAngle(Math.PI).endAngle(-Math.PI / 2)(), "M0,100A100,100,0,1,0,-100,0L-50,0A50,50,0,1,1,0,50Z"); + test.end(); +}); + +tape("arc().innerRadius(0).outerRadius(0).cornerRadius(r) renders a point", function(test) { + var a = shape.arc().innerRadius(0).outerRadius(0).cornerRadius(5); + test.pathEqual(a.startAngle(0).endAngle(2 * Math.PI)(), "M0,0Z"); + test.pathEqual(a.startAngle(0).endAngle(0)(), "M0,0Z"); + test.end(); +}); + +tape("arc().innerRadius(0).outerRadius(r).startAngle(θ₀).endAngle(θ₁).cornerRadius(rᵧ) renders a clockwise circle if r > 0 and θ₁ - θ₀ ≥ τ", function(test) { + var a = shape.arc().innerRadius(0).outerRadius(100).cornerRadius(5); + test.pathEqual(a.startAngle(0).endAngle(2 * Math.PI)(), "M0,-100A100,100,0,1,1,0,100A100,100,0,1,1,0,-100Z"); + test.pathEqual(a.startAngle(0).endAngle(3 * Math.PI)(), "M0,-100A100,100,0,1,1,0,100A100,100,0,1,1,0,-100Z"); + test.pathEqual(a.startAngle(-2 * Math.PI).endAngle(0)(), "M0,-100A100,100,0,1,1,0,100A100,100,0,1,1,0,-100Z"); + test.pathEqual(a.startAngle(-Math.PI).endAngle(Math.PI)(), "M0,100A100,100,0,1,1,0,-100A100,100,0,1,1,0,100Z"); + test.pathEqual(a.startAngle(-3 * Math.PI).endAngle(0)(), "M0,100A100,100,0,1,1,0,-100A100,100,0,1,1,0,100Z"); + test.end(); +}); + +tape("arc().innerRadius(0).outerRadius(r).startAngle(θ₀).endAngle(θ₁).cornerRadius(rᵧ) renders an anticlockwise circle if r > 0 and θ₀ - θ₁ ≥ τ", function(test) { + var a = shape.arc().innerRadius(0).outerRadius(100).cornerRadius(5); + test.pathEqual(a.startAngle(0).endAngle(-2 * Math.PI)(), "M0,-100A100,100,0,1,0,0,100A100,100,0,1,0,0,-100Z"); + test.pathEqual(a.startAngle(0).endAngle(-3 * Math.PI)(), "M0,-100A100,100,0,1,0,0,100A100,100,0,1,0,0,-100Z"); + test.pathEqual(a.startAngle(2 * Math.PI).endAngle(0)(), "M0,-100A100,100,0,1,0,0,100A100,100,0,1,0,0,-100Z"); + test.pathEqual(a.startAngle(Math.PI).endAngle(-Math.PI)(), "M0,100A100,100,0,1,0,0,-100A100,100,0,1,0,0,100Z"); + test.pathEqual(a.startAngle(3 * Math.PI).endAngle(0)(), "M0,100A100,100,0,1,0,0,-100A100,100,0,1,0,0,100Z"); + test.end(); +}); + +// Note: The outer ring starts and ends at θ₀, but the inner ring starts and ends at θ₁. +// Note: The outer ring is clockwise, but the inner ring is anticlockwise. +tape("arc().innerRadius(r₀).outerRadius(r₁).startAngle(θ₀).endAngle(θ₁).cornerRadius(rᵧ) renders a clockwise annulus if r₀ > 0, r₁ > 0 and θ₀ - θ₁ ≥ τ", function(test) { + var a = shape.arc().innerRadius(50).outerRadius(100).cornerRadius(5); + test.pathEqual(a.startAngle(0).endAngle(2 * Math.PI)(), "M0,-100A100,100,0,1,1,0,100A100,100,0,1,1,0,-100M0,-50A50,50,0,1,0,0,50A50,50,0,1,0,0,-50Z"); + test.pathEqual(a.startAngle(0).endAngle(3 * Math.PI)(), "M0,-100A100,100,0,1,1,0,100A100,100,0,1,1,0,-100M0,50A50,50,0,1,0,0,-50A50,50,0,1,0,0,50Z"); + test.pathEqual(a.startAngle(-2 * Math.PI).endAngle(0)(), "M0,-100A100,100,0,1,1,0,100A100,100,0,1,1,0,-100M0,-50A50,50,0,1,0,0,50A50,50,0,1,0,0,-50Z"); + test.pathEqual(a.startAngle(-Math.PI).endAngle(Math.PI)(), "M0,100A100,100,0,1,1,0,-100A100,100,0,1,1,0,100M0,50A50,50,0,1,0,0,-50A50,50,0,1,0,0,50Z"); + test.pathEqual(a.startAngle(-3 * Math.PI).endAngle(0)(), "M0,100A100,100,0,1,1,0,-100A100,100,0,1,1,0,100M0,-50A50,50,0,1,0,0,50A50,50,0,1,0,0,-50Z"); + test.end(); +}); + +// Note: The outer ring starts and ends at θ₀, but the inner ring starts and ends at θ₁. +// Note: The outer ring is anticlockwise, but the inner ring is clockwise. +tape("arc().innerRadius(r₀).outerRadius(r₁).startAngle(θ₀).endAngle(θ₁).cornerRadius(rᵧ) renders an anticlockwise annulus if r₀ > 0, r₁ > 0 and θ₁ - θ₀ ≥ τ", function(test) { + var a = shape.arc().innerRadius(50).outerRadius(100).cornerRadius(5); + test.pathEqual(a.startAngle(0).endAngle(-2 * Math.PI)(), "M0,-100A100,100,0,1,0,0,100A100,100,0,1,0,0,-100M0,-50A50,50,0,1,1,0,50A50,50,0,1,1,0,-50Z"); + test.pathEqual(a.startAngle(0).endAngle(-3 * Math.PI)(), "M0,-100A100,100,0,1,0,0,100A100,100,0,1,0,0,-100M0,50A50,50,0,1,1,0,-50A50,50,0,1,1,0,50Z"); + test.pathEqual(a.startAngle(2 * Math.PI).endAngle(0)(), "M0,-100A100,100,0,1,0,0,100A100,100,0,1,0,0,-100M0,-50A50,50,0,1,1,0,50A50,50,0,1,1,0,-50Z"); + test.pathEqual(a.startAngle(Math.PI).endAngle(-Math.PI)(), "M0,100A100,100,0,1,0,0,-100A100,100,0,1,0,0,100M0,50A50,50,0,1,1,0,-50A50,50,0,1,1,0,50Z"); + test.pathEqual(a.startAngle(3 * Math.PI).endAngle(0)(), "M0,100A100,100,0,1,0,0,-100A100,100,0,1,0,0,100M0,-50A50,50,0,1,1,0,50A50,50,0,1,1,0,-50Z"); + test.end(); +}); + +tape("arc().innerRadius(0).outerRadius(r).startAngle(θ₀).endAngle(θ₁).cornerRadius(rᵧ) renders a small clockwise sector if r > 0 and π > θ₁ - θ₀ ≥ 0", function(test) { + var a = shape.arc().innerRadius(0).outerRadius(100).cornerRadius(5); + test.pathEqual(a.startAngle(0).endAngle(Math.PI / 2)(), "M0,-94.868330A5,5,0,0,1,5.263158,-99.861400A100,100,0,0,1,99.861400,-5.263158A5,5,0,0,1,94.868330,0L0,0Z"); + test.pathEqual(a.startAngle(2 * Math.PI).endAngle(5 * Math.PI / 2)(), "M0,-94.868330A5,5,0,0,1,5.263158,-99.861400A100,100,0,0,1,99.861400,-5.263158A5,5,0,0,1,94.868330,0L0,0Z"); + test.pathEqual(a.startAngle(-Math.PI).endAngle(-Math.PI / 2)(), "M0,94.868330A5,5,0,0,1,-5.263158,99.861400A100,100,0,0,1,-99.861400,5.263158A5,5,0,0,1,-94.868330,0L0,0Z"); + test.end(); +}); + +tape("arc().innerRadius(0).outerRadius(r).startAngle(θ₀).endAngle(θ₁).cornerRadius(rᵧ) renders a small anticlockwise sector if r > 0 and π > θ₀ - θ₁ ≥ 0", function(test) { + var a = shape.arc().innerRadius(0).outerRadius(100).cornerRadius(5); + test.pathEqual(a.startAngle(0).endAngle(-Math.PI / 2)(), "M0,-94.868330A5,5,0,0,0,-5.263158,-99.861400A100,100,0,0,0,-99.861400,-5.263158A5,5,0,0,0,-94.868330,0L0,0Z"); + test.pathEqual(a.startAngle(-2 * Math.PI).endAngle(-5 * Math.PI / 2)(), "M0,-94.868330A5,5,0,0,0,-5.263158,-99.861400A100,100,0,0,0,-99.861400,-5.263158A5,5,0,0,0,-94.868330,0L0,0Z"); + test.pathEqual(a.startAngle(Math.PI).endAngle(Math.PI / 2)(), "M0,94.868330A5,5,0,0,0,5.263158,99.861400A100,100,0,0,0,99.861400,5.263158A5,5,0,0,0,94.868330,0L0,0Z"); + test.end(); +}); + +tape("arc().innerRadius(0).outerRadius(r).startAngle(θ₀).endAngle(θ₁).cornerRadius(rᵧ) renders a large clockwise sector if r > 0 and τ > θ₁ - θ₀ ≥ π", function(test) { + var a = shape.arc().innerRadius(0).outerRadius(100).cornerRadius(5); + test.pathEqual(a.startAngle(0).endAngle(3 * Math.PI / 2)(), "M0,-94.868330A5,5,0,0,1,5.263158,-99.861400A100,100,0,1,1,-99.861400,5.263158A5,5,0,0,1,-94.868330,0L0,0Z"); + test.pathEqual(a.startAngle(2 * Math.PI).endAngle(7 * Math.PI / 2)(), "M0,-94.868330A5,5,0,0,1,5.263158,-99.861400A100,100,0,1,1,-99.861400,5.263158A5,5,0,0,1,-94.868330,0L0,0Z"); + test.pathEqual(a.startAngle(-Math.PI).endAngle(Math.PI / 2)(), "M0,94.868330A5,5,0,0,1,-5.263158,99.861400A100,100,0,1,1,99.861400,-5.263158A5,5,0,0,1,94.868330,0L0,0Z"); + test.end(); +}); + +tape("arc().innerRadius(0).outerRadius(r).startAngle(θ₀).endAngle(θ₁).cornerRadius(rᵧ) renders a large anticlockwise sector if r > 0 and τ > θ₀ - θ₁ ≥ π", function(test) { + var a = shape.arc().innerRadius(0).outerRadius(100).cornerRadius(5); + test.pathEqual(a.startAngle(0).endAngle(-3 * Math.PI / 2)(), "M0,-94.868330A5,5,0,0,0,-5.263158,-99.861400A100,100,0,1,0,99.861400,5.263158A5,5,0,0,0,94.868330,0L0,0Z"); + test.pathEqual(a.startAngle(-2 * Math.PI).endAngle(-7 * Math.PI / 2)(), "M0,-94.868330A5,5,0,0,0,-5.263158,-99.861400A100,100,0,1,0,99.861400,5.263158A5,5,0,0,0,94.868330,0L0,0Z"); + test.pathEqual(a.startAngle(Math.PI).endAngle(-Math.PI / 2)(), "M0,94.868330A5,5,0,0,0,5.263158,99.861400A100,100,0,1,0,-99.861400,-5.263158A5,5,0,0,0,-94.868330,0L0,0Z"); + test.end(); +}); + +// Note: The outer ring is clockwise, but the inner ring is anticlockwise. +tape("arc().innerRadius(r₀).outerRadius(r₁).startAngle(θ₀).endAngle(θ₁).cornerRadius(rᵧ) renders a small clockwise annular sector if r₀ > 0, r₁ > 0 and π > θ₁ - θ₀ ≥ 0", function(test) { + var a = shape.arc().innerRadius(50).outerRadius(100).cornerRadius(5); + test.pathEqual(a.startAngle(0).endAngle(Math.PI / 2)(), "M0,-94.868330A5,5,0,0,1,5.263158,-99.861400A100,100,0,0,1,99.861400,-5.263158A5,5,0,0,1,94.868330,0L54.772256,0A5,5,0,0,1,49.792960,-4.545455A50,50,0,0,0,4.545455,-49.792960A5,5,0,0,1,0,-54.772256Z"); + test.pathEqual(a.startAngle(2 * Math.PI).endAngle(5 * Math.PI / 2)(), "M0,-94.868330A5,5,0,0,1,5.263158,-99.861400A100,100,0,0,1,99.861400,-5.263158A5,5,0,0,1,94.868330,0L54.772256,0A5,5,0,0,1,49.792960,-4.545455A50,50,0,0,0,4.545455,-49.792960A5,5,0,0,1,0,-54.772256Z"); + test.pathEqual(a.startAngle(-Math.PI).endAngle(-Math.PI / 2)(), "M0,94.868330A5,5,0,0,1,-5.263158,99.861400A100,100,0,0,1,-99.861400,5.263158A5,5,0,0,1,-94.868330,0L-54.772256,0A5,5,0,0,1,-49.792960,4.545455A50,50,0,0,0,-4.545455,49.792960A5,5,0,0,1,0,54.772256Z"); + test.end(); +}); + +// Note: The outer ring is anticlockwise, but the inner ring is clockwise. +tape("arc().innerRadius(r₀).outerRadius(r₁).startAngle(θ₀).endAngle(θ₁).cornerRadius(rᵧ) renders a small anticlockwise annular sector if r₀ > 0, r₁ > 0 and π > θ₀ - θ₁ ≥ 0", function(test) { + var a = shape.arc().innerRadius(50).outerRadius(100).cornerRadius(5); + test.pathEqual(a.startAngle(0).endAngle(-Math.PI / 2)(), "M0,-94.868330A5,5,0,0,0,-5.263158,-99.861400A100,100,0,0,0,-99.861400,-5.263158A5,5,0,0,0,-94.868330,0L-54.772256,0A5,5,0,0,0,-49.792960,-4.545455A50,50,0,0,1,-4.545455,-49.792960A5,5,0,0,0,0,-54.772256Z"); + test.pathEqual(a.startAngle(-2 * Math.PI).endAngle(-5 * Math.PI / 2)(), "M0,-94.868330A5,5,0,0,0,-5.263158,-99.861400A100,100,0,0,0,-99.861400,-5.263158A5,5,0,0,0,-94.868330,0L-54.772256,0A5,5,0,0,0,-49.792960,-4.545455A50,50,0,0,1,-4.545455,-49.792960A5,5,0,0,0,0,-54.772256Z"); + test.pathEqual(a.startAngle(Math.PI).endAngle(Math.PI / 2)(), "M0,94.868330A5,5,0,0,0,5.263158,99.861400A100,100,0,0,0,99.861400,5.263158A5,5,0,0,0,94.868330,0L54.772256,0A5,5,0,0,0,49.792960,4.545455A50,50,0,0,1,4.545455,49.792960A5,5,0,0,0,0,54.772256Z"); + test.end(); +}); + +// Note: The outer ring is clockwise, but the inner ring is anticlockwise. +tape("arc().innerRadius(r₀).outerRadius(r₁).startAngle(θ₀).endAngle(θ₁).cornerRadius(rᵧ) renders a large clockwise annular sector if r₀ > 0, r₁ > 0 and τ > θ₁ - θ₀ ≥ π", function(test) { + var a = shape.arc().innerRadius(50).outerRadius(100).cornerRadius(5); + test.pathEqual(a.startAngle(0).endAngle(3 * Math.PI / 2)(), "M0,-94.868330A5,5,0,0,1,5.263158,-99.861400A100,100,0,1,1,-99.861400,5.263158A5,5,0,0,1,-94.868330,0L-54.772256,0A5,5,0,0,1,-49.792960,4.545455A50,50,0,1,0,4.545455,-49.792960A5,5,0,0,1,0,-54.772256Z"); + test.pathEqual(a.startAngle(2 * Math.PI).endAngle(7 * Math.PI / 2)(), "M0,-94.868330A5,5,0,0,1,5.263158,-99.861400A100,100,0,1,1,-99.861400,5.263158A5,5,0,0,1,-94.868330,0L-54.772256,0A5,5,0,0,1,-49.792960,4.545455A50,50,0,1,0,4.545455,-49.792960A5,5,0,0,1,0,-54.772256Z"); + test.pathEqual(a.startAngle(-Math.PI).endAngle(Math.PI / 2)(), "M0,94.868330A5,5,0,0,1,-5.263158,99.861400A100,100,0,1,1,99.861400,-5.263158A5,5,0,0,1,94.868330,0L54.772256,0A5,5,0,0,1,49.792960,-4.545455A50,50,0,1,0,-4.545455,49.792960A5,5,0,0,1,0,54.772256Z"); + test.end(); +}); + +// Note: The outer ring is anticlockwise, but the inner ring is clockwise. +tape("arc().innerRadius(r₀).outerRadius(r₁).startAngle(θ₀).endAngle(θ₁).cornerRadius(rᵧ) renders a large anticlockwise annular sector if r₀ > 0, r₁ > 0 and τ > θ₀ - θ₁ ≥ π", function(test) { + var a = shape.arc().innerRadius(50).outerRadius(100).cornerRadius(5); + test.pathEqual(a.startAngle(0).endAngle(-3 * Math.PI / 2)(), "M0,-94.868330A5,5,0,0,0,-5.263158,-99.861400A100,100,0,1,0,99.861400,5.263158A5,5,0,0,0,94.868330,0L54.772256,0A5,5,0,0,0,49.792960,4.545455A50,50,0,1,1,-4.545455,-49.792960A5,5,0,0,0,0,-54.772256Z"); + test.pathEqual(a.startAngle(-2 * Math.PI).endAngle(-7 * Math.PI / 2)(), "M0,-94.868330A5,5,0,0,0,-5.263158,-99.861400A100,100,0,1,0,99.861400,5.263158A5,5,0,0,0,94.868330,0L54.772256,0A5,5,0,0,0,49.792960,4.545455A50,50,0,1,1,-4.545455,-49.792960A5,5,0,0,0,0,-54.772256Z"); + test.pathEqual(a.startAngle(Math.PI).endAngle(-Math.PI / 2)(), "M0,94.868330A5,5,0,0,0,5.263158,99.861400A100,100,0,1,0,-99.861400,-5.263158A5,5,0,0,0,-94.868330,0L-54.772256,0A5,5,0,0,0,-49.792960,-4.545455A50,50,0,1,1,4.545455,49.792960A5,5,0,0,0,0,54.772256Z"); + test.end(); +}); + +tape("arc().innerRadius(r₀).outerRadius(r₁).cornerRadius(rᵧ) restricts rᵧ to |r₁ - r₀| / 2", function(test) { + var a = shape.arc().cornerRadius(Infinity).startAngle(0).endAngle(Math.PI / 2); + test.pathEqual(a.innerRadius(90).outerRadius(100)(), "M0,-94.868330A5,5,0,0,1,5.263158,-99.861400A100,100,0,0,1,99.861400,-5.263158A5,5,0,0,1,94.868330,0L94.868330,0A5,5,0,0,1,89.875260,-4.736842A90,90,0,0,0,4.736842,-89.875260A5,5,0,0,1,0,-94.868330Z"); + test.pathEqual(a.innerRadius(100).outerRadius(90)(), "M0,-94.868330A5,5,0,0,1,5.263158,-99.861400A100,100,0,0,1,99.861400,-5.263158A5,5,0,0,1,94.868330,0L94.868330,0A5,5,0,0,1,89.875260,-4.736842A90,90,0,0,0,4.736842,-89.875260A5,5,0,0,1,0,-94.868330Z"); + test.end(); +}); + +tape("arc().innerRadius(r₀).outerRadius(r₁).cornerRadius(rᵧ) merges adjacent corners when rᵧ is relatively large", function(test) { + var a = shape.arc().cornerRadius(Infinity).startAngle(0).endAngle(Math.PI / 2); + test.pathEqual(a.innerRadius(10).outerRadius(100)(), "M0,-41.421356A41.421356,41.421356,0,1,1,41.421356,0L24.142136,0A24.142136,24.142136,0,0,1,0,-24.142136Z"); + test.pathEqual(a.innerRadius(100).outerRadius(10)(), "M0,-41.421356A41.421356,41.421356,0,1,1,41.421356,0L24.142136,0A24.142136,24.142136,0,0,1,0,-24.142136Z"); + test.end(); +}); + +tape("arc().innerRadius(0).outerRadius(0).startAngle(0).endAngle(τ).padAngle(δ) does not pad a point", function(test) { + var a = shape.arc().innerRadius(0).outerRadius(0).startAngle(0).endAngle(2 * Math.PI).padAngle(0.1); + test.pathEqual(a(), "M0,0Z"); + test.end(); +}); + +tape("arc().innerRadius(0).outerRadius(r).startAngle(0).endAngle(τ).padAngle(δ) does not pad a circle", function(test) { + var a = shape.arc().innerRadius(0).outerRadius(100).startAngle(0).endAngle(2 * Math.PI).padAngle(0.1); + test.pathEqual(a(), "M0,-100A100,100,0,1,1,0,100A100,100,0,1,1,0,-100Z"); + test.end(); +}); + +tape("arc().innerRadius(r₀).outerRadius(r₁).startAngle(0).endAngle(τ).padAngle(δ) does not pad an annulus", function(test) { + var a = shape.arc().innerRadius(50).outerRadius(100).startAngle(0).endAngle(2 * Math.PI).padAngle(0.1); + test.pathEqual(a(), "M0,-100A100,100,0,1,1,0,100A100,100,0,1,1,0,-100M0,-50A50,50,0,1,0,0,50A50,50,0,1,0,0,-50Z"); + test.end(); +}); + +tape("arc().innerRadius(0).outerRadius(r).startAngle(θ₀).endAngle(θ₁).padAngle(δ) pads the outside of a circular sector", function(test) { + var a = shape.arc().innerRadius(0).outerRadius(100).startAngle(0).endAngle(Math.PI / 2).padAngle(0.1); + test.pathEqual(a(), "M4.997917,-99.875026A100,100,0,0,1,99.875026,-4.997917L0,0Z"); + test.end(); +}); + +tape("arc().innerRadius(r₀).outerRadius(r₁).startAngle(θ₀).endAngle(θ₁).padAngle(δ) pads an annular sector", function(test) { + var a = shape.arc().innerRadius(50).outerRadius(100).startAngle(0).endAngle(Math.PI / 2).padAngle(0.1); + test.pathEqual(a(), "M5.587841,-99.843758A100,100,0,0,1,99.843758,-5.587841L49.686779,-5.587841A50,50,0,0,0,5.587841,-49.686779Z"); + test.end(); +}); + +tape("arc().innerRadius(r₀).outerRadius(r₁).startAngle(θ₀).endAngle(θ₁).padAngle(δ) may collapse the inside of an annular sector", function(test) { + var a = shape.arc().innerRadius(10).outerRadius(100).startAngle(0).endAngle(Math.PI / 2).padAngle(0.2); + test.pathEqual(a(), "M10.033134,-99.495408A100,100,0,0,1,99.495408,-10.033134L7.071068,-7.071068Z"); + test.end(); +}); + +tape("arc().innerRadius(0).outerRadius(r).startAngle(θ₀).endAngle(θ₁).padAngle(δ).cornerRadius(rᵧ) rounds and pads a circular sector", function(test) { + var a = shape.arc().innerRadius(0).outerRadius(100).startAngle(0).endAngle(Math.PI / 2).padAngle(0.1).cornerRadius(10); + test.pathEqual(a(), "M4.470273,-89.330939A10,10,0,0,1,16.064195,-98.701275A100,100,0,0,1,98.701275,-16.064195A10,10,0,0,1,89.330939,-4.470273L0,0Z"); + test.end(); +}); + +tape("arc().innerRadius(r₀).outerRadius(r₁).startAngle(θ₀).endAngle(θ₁).padAngle(δ).cornerRadius(rᵧ) rounds and pads an annular sector", function(test) { + var a = shape.arc().innerRadius(50).outerRadius(100).startAngle(0).endAngle(Math.PI / 2).padAngle(0.1).cornerRadius(10); + test.pathEqual(a(), "M5.587841,-88.639829A10,10,0,0,1,17.319823,-98.488698A100,100,0,0,1,98.488698,-17.319823A10,10,0,0,1,88.639829,-5.587841L57.939790,-5.587841A10,10,0,0,1,48.283158,-12.989867A50,50,0,0,0,12.989867,-48.283158A10,10,0,0,1,5.587841,-57.939790Z"); + test.end(); +}); + +tape("arc().innerRadius(r₀).outerRadius(r₁).startAngle(θ₀).endAngle(θ₁).padAngle(δ).cornerRadius(rᵧ) rounds and pads a collapsed annular sector", function(test) { + var a = shape.arc().innerRadius(10).outerRadius(100).startAngle(0).endAngle(Math.PI / 2).padAngle(0.2).cornerRadius(10); + test.pathEqual(a(), "M9.669396,-88.145811A10,10,0,0,1,21.849183,-97.583878A100,100,0,0,1,97.583878,-21.849183A10,10,0,0,1,88.145811,-9.669396L7.071068,-7.071068Z"); + test.end(); +}); diff --git a/test/area-test.js b/test/area-test.js new file mode 100644 index 0000000..8d4bf75 --- /dev/null +++ b/test/area-test.js @@ -0,0 +1,199 @@ +var tape = require("tape"), + shape = require("../"); + +require("./pathEqual"); + +tape("area() returns a default area shape", function(test) { + var a = shape.area(); + test.equal(a.x0()([42, 34]), 42); + test.equal(a.x1(), null); + test.equal(a.y0()([42, 34]), 0); + test.equal(a.y1()([42, 34]), 34); + test.equal(a.defined()([42, 34]), true); + test.equal(a.curve(), shape.curveLinear); + test.equal(a.context(), null); + test.pathEqual(a([[0, 1], [2, 3], [4, 5]]), "M0,1L2,3L4,5L4,0L2,0L0,0Z"); + test.end(); +}); + +tape("area.x(f)(data) passes d, i and data to the specified function f", function(test) { + var data = ["a", "b"], actual = []; + shape.area().x(function() { actual.push([].slice.call(arguments)); })(data); + test.deepEqual(actual, [["a", 0, data], ["b", 1, data]]); + test.end(); +}); + +tape("area.x0(f)(data) passes d, i and data to the specified function f", function(test) { + var data = ["a", "b"], actual = []; + shape.area().x0(function() { actual.push([].slice.call(arguments)); })(data); + test.deepEqual(actual, [["a", 0, data], ["b", 1, data]]); + test.end(); +}); + +tape("area.x1(f)(data) passes d, i and data to the specified function f", function(test) { + var data = ["a", "b"], actual = []; + shape.area().x1(function() { actual.push([].slice.call(arguments)); })(data); + test.deepEqual(actual, [["a", 0, data], ["b", 1, data]]); + test.end(); +}); + +tape("area.y(f)(data) passes d, i and data to the specified function f", function(test) { + var data = ["a", "b"], actual = []; + shape.area().y(function() { actual.push([].slice.call(arguments)); })(data); + test.deepEqual(actual, [["a", 0, data], ["b", 1, data]]); + test.end(); +}); + +tape("area.y0(f)(data) passes d, i and data to the specified function f", function(test) { + var data = ["a", "b"], actual = []; + shape.area().y0(function() { actual.push([].slice.call(arguments)); })(data); + test.deepEqual(actual, [["a", 0, data], ["b", 1, data]]); + test.end(); +}); + +tape("area.y1(f)(data) passes d, i and data to the specified function f", function(test) { + var data = ["a", "b"], actual = []; + shape.area().y1(function() { actual.push([].slice.call(arguments)); })(data); + test.deepEqual(actual, [["a", 0, data], ["b", 1, data]]); + test.end(); +}); + +tape("area.defined(f)(data) passes d, i and data to the specified function f", function(test) { + var data = ["a", "b"], actual = []; + shape.area().defined(function() { actual.push([].slice.call(arguments)); })(data); + test.deepEqual(actual, [["a", 0, data], ["b", 1, data]]); + test.end(); +}); + +tape("area.x(x)(data) observes the specified function", function(test) { + var x = function(d) { return d.x; }, + a = shape.area().x(x); + test.equal(a.x(), x); + test.equal(a.x0(), x); + test.equal(a.x1(), null); + test.pathEqual(a([{x: 0, 1: 1}, {x: 2, 1: 3}, {x: 4, 1: 5}]), "M0,1L2,3L4,5L4,0L2,0L0,0Z"); + test.end(); +}); + +tape("area.x(x)(data) observes the specified constant", function(test) { + var x = 0, + a = shape.area().x(x); + test.equal(a.x()(), 0); + test.equal(a.x0()(), 0); + test.equal(a.x1(), null); + test.pathEqual(a([{1: 1}, {1: 3}, {1: 5}]), "M0,1L0,3L0,5L0,0L0,0L0,0Z"); + test.end(); +}); + +tape("area.y(y)(data) observes the specified function", function(test) { + var y = function(d) { return d.y; }, + a = shape.area().y(y); + test.equal(a.y(), y); + test.equal(a.y0(), y); + test.equal(a.y1(), null); + test.pathEqual(a([{0: 0, y: 1}, {0: 2, y: 3}, {0: 4, y: 5}]), "M0,1L2,3L4,5L4,5L2,3L0,1Z"); + test.end(); +}); + +tape("area.y(y)(data) observes the specified constant", function(test) { + var a = shape.area().y(0); + test.equal(a.y()(), 0); + test.equal(a.y0()(), 0); + test.equal(a.y1(), null); + test.pathEqual(a([{0: 0}, {0: 2}, {0: 4}]), "M0,0L2,0L4,0L4,0L2,0L0,0Z"); + test.end(); +}); + +tape("area.curve(curve) sets the curve method", function(test) { + var a = shape.area().curve(shape.curveCardinal); + test.pathEqual(a([[0, 1], [1, 3], [2, 1], [3, 3]]), "M0,1C0,1,0.666667,3,1,3C1.333333,3,1.666667,1,2,1C2.333333,1,3,3,3,3L3,0C3,0,2.333333,0,2,0C1.666667,0,1.333333,0,1,0C0.666667,0,0,0,0,0Z"); + test.end(); +}); + +tape("area.curve(curveCardinal.tension(tension)) sets the cardinal spline tension", function(test) { + var a = shape.area().curve(shape.curveCardinal.tension(0.1)); + test.equal(a([]), null); + test.pathEqual(a([[0, 1]]), "M0,1L0,0Z"); + test.pathEqual(a([[0, 1], [1, 3]]), "M0,1L1,3L1,0L0,0Z"); + test.pathEqual(a([[0, 1], [1, 3], [2, 1]]), "M0,1C0,1,0.700000,3,1,3C1.300000,3,2,1,2,1L2,0C2,0,1.300000,0,1,0C0.700000,0,0,0,0,0Z"); + test.pathEqual(a([[0, 1], [1, 3], [2, 1], [3, 3]]), "M0,1C0,1,0.700000,3,1,3C1.300000,3,1.700000,1,2,1C2.300000,1,3,3,3,3L3,0C3,0,2.300000,0,2,0C1.700000,0,1.300000,0,1,0C0.700000,0,0,0,0,0Z"); + test.end(); +}); + +tape("area.curve(curveCardinal.tension(tension)) coerces the specified tension to a number", function(test) { + var a = shape.area().curve(shape.curveCardinal.tension("0.1")); + test.equal(a([]), null); + test.pathEqual(a([[0, 1]]), "M0,1L0,0Z"); + test.pathEqual(a([[0, 1], [1, 3]]), "M0,1L1,3L1,0L0,0Z"); + test.pathEqual(a([[0, 1], [1, 3], [2, 1]]), "M0,1C0,1,0.700000,3,1,3C1.300000,3,2,1,2,1L2,0C2,0,1.300000,0,1,0C0.700000,0,0,0,0,0Z"); + test.pathEqual(a([[0, 1], [1, 3], [2, 1], [3, 3]]), "M0,1C0,1,0.700000,3,1,3C1.300000,3,1.700000,1,2,1C2.300000,1,3,3,3,3L3,0C3,0,2.300000,0,2,0C1.700000,0,1.300000,0,1,0C0.700000,0,0,0,0,0Z"); + test.end(); +}); + +tape("area.lineX0() returns a line derived from the area", function(test) { + var defined = function() { return true; }, + curve = shape.curveCardinal, + context = {}, + x0 = function() {}, + x1 = function() {}, + y = function() {}, + a = shape.area().defined(defined).curve(curve).context(context).y(y).x0(x0).x1(x1), + l = a.lineX0(); + test.equal(l.defined(), defined); + test.equal(l.curve(), curve); + test.equal(l.context(), context); + test.equal(l.x(), x0); + test.equal(l.y(), y); + test.end(); +}); + +tape("area.lineX1() returns a line derived from the area", function(test) { + var defined = function() { return true; }, + curve = shape.curveCardinal, + context = {}, + x0 = function() {}, + x1 = function() {}, + y = function() {}, + a = shape.area().defined(defined).curve(curve).context(context).y(y).x0(x0).x1(x1), + l = a.lineX1(); + test.equal(l.defined(), defined); + test.equal(l.curve(), curve); + test.equal(l.context(), context); + test.equal(l.x(), x1); + test.equal(l.y(), y); + test.end(); +}); + +tape("area.lineY0() returns a line derived from the area", function(test) { + var defined = function() { return true; }, + curve = shape.curveCardinal, + context = {}, + x = function() {}, + y0 = function() {}, + y1 = function() {}, + a = shape.area().defined(defined).curve(curve).context(context).x(x).y0(y0).y1(y1), + l = a.lineY0(); + test.equal(l.defined(), defined); + test.equal(l.curve(), curve); + test.equal(l.context(), context); + test.equal(l.x(), x); + test.equal(l.y(), y0); + test.end(); +}); + +tape("area.lineY1() returns a line derived from the area", function(test) { + var defined = function() { return true; }, + curve = shape.curveCardinal, + context = {}, + x = function() {}, + y0 = function() {}, + y1 = function() {}, + a = shape.area().defined(defined).curve(curve).context(context).x(x).y0(y0).y1(y1), + l = a.lineY1(); + test.equal(l.defined(), defined); + test.equal(l.curve(), curve); + test.equal(l.context(), context); + test.equal(l.x(), x); + test.equal(l.y(), y1); + test.end(); +}); diff --git a/test/areaRadial-test.js b/test/areaRadial-test.js new file mode 100644 index 0000000..d9855fe --- /dev/null +++ b/test/areaRadial-test.js @@ -0,0 +1,85 @@ +var tape = require("tape"), + shape = require("../"); + +require("./pathEqual"); + +tape("areaRadial() returns a default radial area shape", function(test) { + var a = shape.areaRadial(); + test.equal(a.startAngle()([42, 34]), 42); + test.equal(a.endAngle(), null); + test.equal(a.innerRadius()([42, 34]), 0); + test.equal(a.outerRadius()([42, 34]), 34); + test.equal(a.defined()([42, 34]), true); + test.equal(a.curve(), shape.curveLinear); + test.equal(a.context(), null); + test.pathEqual(a([[0, 1], [2, 3], [4, 5]]), "M0,-1L2.727892,1.248441L-3.784012,3.268218L0,0L0,0L0,0Z"); + test.end(); +}); + +tape("areaRadial.lineStartAngle() returns a line derived from the area", function(test) { + var defined = function() { return true; }, + curve = shape.curveCardinal, + context = {}, + startAngle = function() {}, + endAngle = function() {}, + radius = function() {}, + a = shape.areaRadial().defined(defined).curve(curve).context(context).radius(radius).startAngle(startAngle).endAngle(endAngle), + l = a.lineStartAngle(); + test.equal(l.defined(), defined); + test.equal(l.curve(), curve); + test.equal(l.context(), context); + test.equal(l.angle(), startAngle); + test.equal(l.radius(), radius); + test.end(); +}); + +tape("areaRadial.lineEndAngle() returns a line derived from the area", function(test) { + var defined = function() { return true; }, + curve = shape.curveCardinal, + context = {}, + startAngle = function() {}, + endAngle = function() {}, + radius = function() {}, + a = shape.areaRadial().defined(defined).curve(curve).context(context).radius(radius).startAngle(startAngle).endAngle(endAngle), + l = a.lineEndAngle(); + test.equal(l.defined(), defined); + test.equal(l.curve(), curve); + test.equal(l.context(), context); + test.equal(l.angle(), endAngle); + test.equal(l.radius(), radius); + test.end(); +}); + +tape("areaRadial.lineInnerRadius() returns a line derived from the area", function(test) { + var defined = function() { return true; }, + curve = shape.curveCardinal, + context = {}, + angle = function() {}, + innerRadius = function() {}, + outerRadius = function() {}, + a = shape.areaRadial().defined(defined).curve(curve).context(context).angle(angle).innerRadius(innerRadius).outerRadius(outerRadius), + l = a.lineInnerRadius(); + test.equal(l.defined(), defined); + test.equal(l.curve(), curve); + test.equal(l.context(), context); + test.equal(l.angle(), angle); + test.equal(l.radius(), innerRadius); + test.end(); +}); + +tape("areaRadial.lineOuterRadius() returns a line derived from the area", function(test) { + var defined = function() { return true; }, + curve = shape.curveCardinal, + context = {}, + angle = function() {}, + innerRadius = function() {}, + outerRadius = function() {}, + a = shape.areaRadial().defined(defined).curve(curve).context(context).angle(angle).innerRadius(innerRadius).outerRadius(outerRadius), + l = a.lineOuterRadius(); + test.equal(l.defined(), defined); + test.equal(l.curve(), curve); + test.equal(l.context(), context); + test.equal(l.angle(), angle); + test.equal(l.radius(), outerRadius); + test.end(); +}); diff --git a/test/curve/basis-test.js b/test/curve/basis-test.js new file mode 100644 index 0000000..563a4f5 --- /dev/null +++ b/test/curve/basis-test.js @@ -0,0 +1,22 @@ +var tape = require("tape"), + shape = require("../../"); + +require("../pathEqual"); + +tape("line.curve(curveBasis)(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveBasis); + test.equal(l([]), null); + test.pathEqual(l([[0, 1]]), "M0,1Z"); + test.pathEqual(l([[0, 1], [1, 3]]), "M0,1L1,3"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1]]), "M0,1L0.166667,1.333333C0.333333,1.666667,0.666667,2.333333,1,2.333333C1.333333,2.333333,1.666667,1.666667,1.833333,1.333333L2,1"); + test.end(); +}); + +tape("area.curve(curveBasis)(data) generates the expected path", function(test) { + var a = shape.area().curve(shape.curveBasis); + test.equal(a([]), null); + test.pathEqual(a([[0, 1]]), "M0,1L0,0Z"); + test.pathEqual(a([[0, 1], [1, 3]]), "M0,1L1,3L1,0L0,0Z"); + test.pathEqual(a([[0, 1], [1, 3], [2, 1]]), "M0,1L0.166667,1.333333C0.333333,1.666667,0.666667,2.333333,1,2.333333C1.333333,2.333333,1.666667,1.666667,1.833333,1.333333L2,1L2,0L1.833333,0C1.666667,0,1.333333,0,1,0C0.666667,0,0.333333,0,0.166667,0L0,0Z"); + test.end(); +}); diff --git a/test/curve/basisClosed-test.js b/test/curve/basisClosed-test.js new file mode 100644 index 0000000..c8693e5 --- /dev/null +++ b/test/curve/basisClosed-test.js @@ -0,0 +1,15 @@ +var tape = require("tape"), + shape = require("../../"); + +require("../pathEqual"); + +tape("line.curve(curveBasisClosed)(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveBasisClosed); + test.equal(l([]), null); + test.pathEqual(l([[0, 0]]), "M0,0Z"); + test.pathEqual(l([[0, 0], [0, 10]]), "M0,6.666667L0,3.333333Z"); + test.pathEqual(l([[0, 0], [0, 10], [10, 10]]), "M1.666667,8.333333C3.333333,10,6.666667,10,6.666667,8.333333C6.666667,6.666667,3.333333,3.333333,1.666667,3.333333C0,3.333333,0,6.666667,1.666667,8.333333"); + test.pathEqual(l([[0, 0], [0, 10], [10, 10], [10, 0]]), "M1.666667,8.333333C3.333333,10,6.666667,10,8.333333,8.333333C10,6.666667,10,3.333333,8.333333,1.666667C6.666667,0,3.333333,0,1.666667,1.666667C0,3.333333,0,6.666667,1.666667,8.333333"); + test.pathEqual(l([[0, 0], [0, 10], [10, 10], [10, 0], [0, 0]]), "M1.666667,8.333333C3.333333,10,6.666667,10,8.333333,8.333333C10,6.666667,10,3.333333,8.333333,1.666667C6.666667,0,3.333333,0,1.666667,0C0,0,0,0,0,1.666667C0,3.333333,0,6.666667,1.666667,8.333333"); + test.end(); +}); diff --git a/test/curve/basisOpen-test.js b/test/curve/basisOpen-test.js new file mode 100644 index 0000000..8c0c7e6 --- /dev/null +++ b/test/curve/basisOpen-test.js @@ -0,0 +1,26 @@ +var tape = require("tape"), + shape = require("../../"); + +require("../pathEqual"); + +tape("line.curve(curveBasisOpen)(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveBasisOpen); + test.equal(l([]), null); + test.equal(l([[0, 0]]), null); + test.equal(l([[0, 0], [0, 10]]), null); + test.pathEqual(l([[0, 0], [0, 10], [10, 10]]), "M1.666667,8.333333Z"); + test.pathEqual(l([[0, 0], [0, 10], [10, 10], [10, 0]]), "M1.666667,8.333333C3.333333,10,6.666667,10,8.333333,8.333333"); + test.pathEqual(l([[0, 0], [0, 10], [10, 10], [10, 0], [0, 0]]), "M1.666667,8.333333C3.333333,10,6.666667,10,8.333333,8.333333C10,6.666667,10,3.333333,8.333333,1.666667"); + test.end(); +}); + +tape("area.curve(curveBasisOpen)(data) generates the expected path", function(test) { + var a = shape.area().curve(shape.curveBasisOpen); + test.equal(a([]), null); + test.equal(a([[0, 1]]), null); + test.equal(a([[0, 1], [1, 3]]), null); + test.pathEqual(a([[0, 0], [0, 10], [10, 10]]), "M1.666667,8.333333L1.666667,0Z"); + test.pathEqual(a([[0, 0], [0, 10], [10, 10], [10, 0]]), "M1.666667,8.333333C3.333333,10,6.666667,10,8.333333,8.333333L8.333333,0C6.666667,0,3.333333,0,1.666667,0Z"); + test.pathEqual(a([[0, 0], [0, 10], [10, 10], [10, 0], [0, 0]]), "M1.666667,8.333333C3.333333,10,6.666667,10,8.333333,8.333333C10,6.666667,10,3.333333,8.333333,1.666667L8.333333,0C10,0,10,0,8.333333,0C6.666667,0,3.333333,0,1.666667,0Z"); + test.end(); +}); diff --git a/test/curve/bundle-test.js b/test/curve/bundle-test.js new file mode 100644 index 0000000..48a6bb4 --- /dev/null +++ b/test/curve/bundle-test.js @@ -0,0 +1,21 @@ +var tape = require("tape"), + shape = require("../../"); + +require("../pathEqual"); + +tape("line.curve(curveBundle) uses a default beta of 0.85", function(test) { + var l = shape.line().curve(shape.curveBundle.beta(0.85)); + test.equal(shape.line().curve(shape.curveBundle)([[0, 1], [1, 3], [2, 1], [3, 3]]), l([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); + +tape("line.curve(curveBundle.beta(beta)) uses the specified beta", function(test) { + test.equal(shape.line().curve(shape.curveBundle.beta(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), "M0,1L0.16666666666666666,1.222222222222222C0.3333333333333333,1.4444444444444444,0.6666666666666666,1.8888888888888886,1,1.9999999999999998C1.3333333333333333,2.1111111111111107,1.6666666666666667,1.8888888888888886,2,2C2.3333333333333335,2.111111111111111,2.6666666666666665,2.5555555555555554,2.8333333333333335,2.7777777777777772L3,3"); + test.end(); +}); + +tape("line.curve(curveBundle.beta(beta)) coerces the specified beta to a number", function(test) { + var l = shape.line().curve(shape.curveBundle.beta("0.5")); + test.equal(shape.line().curve(shape.curveBundle.beta(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), l([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); diff --git a/test/curve/cardinal-test.js b/test/curve/cardinal-test.js new file mode 100644 index 0000000..dc26680 --- /dev/null +++ b/test/curve/cardinal-test.js @@ -0,0 +1,58 @@ +var tape = require("tape"), + shape = require("../../"); + +require("../pathEqual"); + +tape("line.curve(curveCardinal)(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveCardinal); + test.equal(l([]), null); + test.pathEqual(l([[0, 1]]), "M0,1Z"); + test.pathEqual(l([[0, 1], [1, 3]]), "M0,1L1,3"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1]]), "M0,1C0,1,0.666667,3,1,3C1.333333,3,2,1,2,1"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1], [3, 3]]), "M0,1C0,1,0.666667,3,1,3C1.333333,3,1.666667,1,2,1C2.333333,1,3,3,3,3"); + test.end(); +}); + +tape("line.curve(curveCardinal) uses a default tension of zero", function(test) { + var l = shape.line().curve(shape.curveCardinal.tension(0)); + test.equal(shape.line().curve(shape.curveCardinal)([[0, 1], [1, 3], [2, 1], [3, 3]]), l([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); + +tape("line.curve(curveCardinal.tension(tension)) uses the specified tension", function(test) { + test.pathEqual(shape.line().curve(shape.curveCardinal.tension(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), "M0,1C0,1,0.833333,3,1,3C1.166667,3,1.833333,1,2,1C2.166667,1,3,3,3,3"); + test.end(); +}); + +tape("line.curve(curveCardinal.tension(tension)) coerces the specified tension to a number", function(test) { + var l = shape.line().curve(shape.curveCardinal.tension("0.5")); + test.equal(shape.line().curve(shape.curveCardinal.tension(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), l([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); + +tape("area.curve(curveCardinal)(data) generates the expected path", function(test) { + var a = shape.area().curve(shape.curveCardinal); + test.equal(a([]), null); + test.pathEqual(a([[0, 1]]), "M0,1L0,0Z"); + test.pathEqual(a([[0, 1], [1, 3]]), "M0,1L1,3L1,0L0,0Z"); + test.pathEqual(a([[0, 1], [1, 3], [2, 1]]), "M0,1C0,1,0.666667,3,1,3C1.333333,3,2,1,2,1L2,0C2,0,1.333333,0,1,0C0.666667,0,0,0,0,0Z"); + test.pathEqual(a([[0, 1], [1, 3], [2, 1], [3, 3]]), "M0,1C0,1,0.666667,3,1,3C1.333333,3,1.666667,1,2,1C2.333333,1,3,3,3,3L3,0C3,0,2.333333,0,2,0C1.666667,0,1.333333,0,1,0C0.666667,0,0,0,0,0Z"); + test.end(); +}); + +tape("area.curve(curveCardinal) uses a default tension of zero", function(test) { + var a = shape.area().curve(shape.curveCardinal.tension(0)); + test.equal(shape.area().curve(shape.curveCardinal)([[0, 1], [1, 3], [2, 1], [3, 3]]), a([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); + +tape("area.curve(curveCardinal.tension(tension)) uses the specified tension", function(test) { + test.pathEqual(shape.area().curve(shape.curveCardinal.tension(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), "M0,1C0,1,0.833333,3,1,3C1.166667,3,1.833333,1,2,1C2.166667,1,3,3,3,3L3,0C3,0,2.166667,0,2,0C1.833333,0,1.166667,0,1,0C0.833333,0,0,0,0,0Z"); + test.end(); +}); + +tape("area.curve(curveCardinal.tension(tension)) coerces the specified tension to a number", function(test) { + var a = shape.area().curve(shape.curveCardinal.tension("0.5")); + test.equal(shape.area().curve(shape.curveCardinal.tension(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), a([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); diff --git a/test/curve/cardinalClosed-test.js b/test/curve/cardinalClosed-test.js new file mode 100644 index 0000000..36e52da --- /dev/null +++ b/test/curve/cardinalClosed-test.js @@ -0,0 +1,58 @@ +var tape = require("tape"), + shape = require("../../"); + +require("../pathEqual"); + +tape("line.curve(curveCardinalClosed)(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveCardinalClosed); + test.equal(l([]), null); + test.pathEqual(l([[0, 1]]), "M0,1Z"); + test.pathEqual(l([[0, 1], [1, 3]]), "M1,3L0,1Z"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1]]), "M1,3C1.333333,3,2.166667,1.333333,2,1C1.833333,0.666667,0.166667,0.666667,0,1C-0.166667,1.333333,0.666667,3,1,3"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1], [3, 3]]), "M1,3C1.333333,3,1.666667,1,2,1C2.333333,1,3.333333,3,3,3C2.666667,3,0.333333,1,0,1C-0.333333,1,0.666667,3,1,3"); + test.end(); +}); + +tape("line.curve(curveCardinalClosed) uses a default tension of zero", function(test) { + var l = shape.line().curve(shape.curveCardinalClosed.tension(0)); + test.equal(shape.line().curve(shape.curveCardinalClosed)([[0, 1], [1, 3], [2, 1], [3, 3]]), l([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); + +tape("line.curve(curveCardinalClosed.tension(tension)) uses the specified tension", function(test) { + test.pathEqual(shape.line().curve(shape.curveCardinalClosed.tension(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), "M1,3C1.166667,3,1.833333,1,2,1C2.166667,1,3.166667,3,3,3C2.833333,3,0.166667,1,0,1C-0.166667,1,0.833333,3,1,3"); + test.end(); +}); + +tape("line.curve(curveCardinalClosed.tension(tension)) coerces the specified tension to a number", function(test) { + var l = shape.line().curve(shape.curveCardinalClosed.tension("0.5")); + test.equal(shape.line().curve(shape.curveCardinalClosed.tension(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), l([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); + +tape("area.curve(curveCardinalClosed)(data) generates the expected path", function(test) { + var a = shape.area().curve(shape.curveCardinalClosed); + test.equal(a([]), null); + test.equal(a([[0, 1]]), "M0,1ZM0,0Z"); + test.equal(a([[0, 1], [1, 3]]), "M1,3L0,1ZM0,0L1,0Z"); + test.pathEqual(a([[0, 1], [1, 3], [2, 1]]), "M1,3C1.333333,3,2.166667,1.333333,2,1C1.833333,0.666667,0.166667,0.666667,0,1C-0.166667,1.333333,0.666667,3,1,3M1,0C0.666667,0,-0.166667,0,0,0C0.166667,0,1.833333,0,2,0C2.166667,0,1.333333,0,1,0"); + test.pathEqual(a([[0, 1], [1, 3], [2, 1], [3, 3]]), "M1,3C1.333333,3,1.666667,1,2,1C2.333333,1,3.333333,3,3,3C2.666667,3,0.333333,1,0,1C-0.333333,1,0.666667,3,1,3M2,0C1.666667,0,1.333333,0,1,0C0.666667,0,-0.333333,0,0,0C0.333333,0,2.666667,0,3,0C3.333333,0,2.333333,0,2,0"); + test.end(); +}); + +tape("area.curve(curveCardinalClosed) uses a default tension of zero", function(test) { + var a = shape.area().curve(shape.curveCardinalClosed.tension(0)); + test.equal(shape.area().curve(shape.curveCardinalClosed)([[0, 1], [1, 3], [2, 1], [3, 3]]), a([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); + +tape("area.curve(curveCardinalClosed.tension(tension)) uses the specified tension", function(test) { + test.pathEqual(shape.area().curve(shape.curveCardinalClosed.tension(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), "M1,3C1.166667,3,1.833333,1,2,1C2.166667,1,3.166667,3,3,3C2.833333,3,0.166667,1,0,1C-0.166667,1,0.833333,3,1,3M2,0C1.833333,0,1.166667,0,1,0C0.833333,0,-0.166667,0,0,0C0.166667,0,2.833333,0,3,0C3.166667,0,2.166667,0,2,0"); + test.end(); +}); + +tape("area.curve(curveCardinalClosed.tension(tension)) coerces the specified tension to a number", function(test) { + var a = shape.area().curve(shape.curveCardinalClosed.tension("0.5")); + test.equal(shape.area().curve(shape.curveCardinalClosed.tension(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), a([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); diff --git a/test/curve/cardinalOpen-test.js b/test/curve/cardinalOpen-test.js new file mode 100644 index 0000000..652cbe4 --- /dev/null +++ b/test/curve/cardinalOpen-test.js @@ -0,0 +1,58 @@ +var tape = require("tape"), + shape = require("../../"); + +require("../pathEqual"); + +tape("line.curve(curveCardinalOpen)(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveCardinalOpen); + test.equal(l([]), null); + test.equal(l([[0, 1]]), null); + test.equal(l([[0, 1], [1, 3]]), null); + test.pathEqual(l([[0, 1], [1, 3], [2, 1]]), "M1,3Z"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1], [3, 3]]), "M1,3C1.333333,3,1.666667,1,2,1"); + test.end(); +}); + +tape("line.curve(curveCardinalOpen) uses a default tension of zero", function(test) { + var l = shape.line().curve(shape.curveCardinalOpen.tension(0)); + test.equal(shape.line().curve(shape.curveCardinalOpen)([[0, 1], [1, 3], [2, 1], [3, 3]]), l([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); + +tape("line.curve(curveCardinalOpen.tension(tension)) uses the specified tension", function(test) { + test.pathEqual(shape.line().curve(shape.curveCardinalOpen.tension(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), "M1,3C1.166667,3,1.833333,1,2,1"); + test.end(); +}); + +tape("line.curve(curveCardinalOpen.tension(tension)) coerces the specified tension to a number", function(test) { + var l = shape.line().curve(shape.curveCardinalOpen.tension("0.5")); + test.equal(shape.line().curve(shape.curveCardinalOpen.tension(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), l([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); + +tape("area.curve(curveCardinalOpen)(data) generates the expected path", function(test) { + var a = shape.area().curve(shape.curveCardinalOpen); + test.equal(a([]), null); + test.equal(a([[0, 1]]), null); + test.equal(a([[0, 1], [1, 3]]), null); + test.pathEqual(a([[0, 1], [1, 3], [2, 1]]), "M1,3L1,0Z"); + test.pathEqual(a([[0, 1], [1, 3], [2, 1], [3, 3]]), "M1,3C1.333333,3,1.666667,1,2,1L2,0C1.666667,0,1.333333,0,1,0Z"); + test.end(); +}); + +tape("area.curve(curveCardinalOpen) uses a default tension of zero", function(test) { + var a = shape.area().curve(shape.curveCardinalOpen.tension(0)); + test.equal(shape.area().curve(shape.curveCardinalOpen)([[0, 1], [1, 3], [2, 1], [3, 3]]), a([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); + +tape("area.curve(curveCardinalOpen.tension(tension)) uses the specified tension", function(test) { + test.pathEqual(shape.area().curve(shape.curveCardinalOpen.tension(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), "M1,3C1.166667,3,1.833333,1,2,1L2,0C1.833333,0,1.166667,0,1,0Z"); + test.end(); +}); + +tape("area.curve(curveCardinalOpen.tension(tension)) coerces the specified tension to a number", function(test) { + var a = shape.area().curve(shape.curveCardinalOpen.tension("0.5")); + test.equal(shape.area().curve(shape.curveCardinalOpen.tension(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), a([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); diff --git a/test/curve/catmullRom-test.js b/test/curve/catmullRom-test.js new file mode 100644 index 0000000..b617b84 --- /dev/null +++ b/test/curve/catmullRom-test.js @@ -0,0 +1,58 @@ +var tape = require("tape"), + shape = require("../../"); + +require("../pathEqual"); + +tape("line.curve(curveCatmullRom)(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveCatmullRom); + test.equal(l([]), null); + test.pathEqual(l([[0, 1]]), "M0,1Z"); + test.pathEqual(l([[0, 1], [1, 3]]), "M0,1L1,3"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1]]), "M0,1C0,1,0.666667,3,1,3C1.333333,3,2,1,2,1"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1], [3, 3]]), "M0,1C0,1,0.666667,3,1,3C1.333333,3,1.666667,1,2,1C2.333333,1,3,3,3,3"); + test.end(); +}); + +tape("line.curve(curveCatmullRom.alpha(1))(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveCatmullRom.alpha(1)); + test.equal(l([]), null); + test.pathEqual(l([[0, 1]]), "M0,1Z"); + test.pathEqual(l([[0, 1], [1, 3]]), "M0,1L1,3"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1]]), "M0,1C0,1,0.666667,3,1,3C1.333333,3,2,1,2,1"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1], [3, 3]]), "M0,1C0,1,0.666667,3,1,3C1.333333,3,1.666667,1,2,1C2.333333,1,3,3,3,3"); + test.end(); +}); + +tape("line.curve(curveCatmullRom) uses a default alpha of 0.5 (centripetal)", function(test) { + var l = shape.line().curve(shape.curveCatmullRom.alpha(0.5)); + test.equal(shape.line().curve(shape.curveCatmullRom)([[0, 1], [1, 3], [2, 1], [3, 3]]), l([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); + +tape("line.curve(curveCatmullRom.alpha(alpha)) coerces the specified alpha to a number", function(test) { + var l = shape.line().curve(shape.curveCatmullRom.alpha("0.5")); + test.equal(shape.line().curve(shape.curveCatmullRom.alpha(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), l([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); + +tape("area.curve(curveCatmullRom.alpha(0))(data) generates the expected path", function(test) { + var a = shape.area().curve(shape.curveCatmullRom.alpha(0)); + test.equal(a([]), null); + test.pathEqual(a([[0, 1]]), "M0,1L0,0Z"); + test.pathEqual(a([[0, 1], [1, 3]]), "M0,1L1,3L1,0L0,0Z"); + test.pathEqual(a([[0, 1], [1, 3], [2, 1]]), "M0,1C0,1,0.666667,3,1,3C1.333333,3,2,1,2,1L2,0C2,0,1.333333,0,1,0C0.666667,0,0,0,0,0Z"); + test.pathEqual(a([[0, 1], [1, 3], [2, 1], [3, 3]]), "M0,1C0,1,0.666667,3,1,3C1.333333,3,1.666667,1,2,1C2.333333,1,3,3,3,3L3,0C3,0,2.333333,0,2,0C1.666667,0,1.333333,0,1,0C0.666667,0,0,0,0,0Z"); + test.end(); +}); + +tape("area.curve(curveCatmullRom) uses a default alpha of 0.5 (centripetal)", function(test) { + var a = shape.area().curve(shape.curveCatmullRom.alpha(0.5)); + test.equal(shape.area().curve(shape.curveCatmullRom)([[0, 1], [1, 3], [2, 1], [3, 3]]), a([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); + +tape("area.curve(curveCatmullRom.alpha(alpha)) coerces the specified alpha to a number", function(test) { + var a = shape.area().curve(shape.curveCatmullRom.alpha("0.5")); + test.equal(shape.area().curve(shape.curveCatmullRom.alpha(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), a([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); diff --git a/test/curve/catmullRomClosed-test.js b/test/curve/catmullRomClosed-test.js new file mode 100644 index 0000000..900dafb --- /dev/null +++ b/test/curve/catmullRomClosed-test.js @@ -0,0 +1,52 @@ +var tape = require("tape"), + shape = require("../../"); + +require("../pathEqual"); + +tape("line.curve(curveCatmullRomClosed)(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveCatmullRomClosed); + test.equal(l([]), null); + test.pathEqual(l([[0, 1]]), "M0,1Z"); + test.pathEqual(l([[0, 1], [1, 3]]), "M1,3L0,1Z"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1]]), "M1,3C1.333333,3,2.200267,1.324038,2,1C1.810600,0.693544,0.189400,0.693544,0,1C-0.200267,1.324038,0.666667,3,1,3"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1], [3, 3]]), "M1,3C1.333333,3,1.666667,1,2,1C2.333333,1,3.160469,2.858341,3,3C2.796233,3.179882,0.203767,0.820118,0,1C-0.160469,1.141659,0.666667,3,1,3"); + test.end(); +}); + +tape("line.curve(curveCatmullRomClosed.alpha(0))(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveCatmullRomClosed.alpha(0)); + test.equal(l([]), null); + test.pathEqual(l([[0, 1]]), "M0,1Z"); + test.pathEqual(l([[0, 1], [1, 3]]), "M1,3L0,1Z"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1]]), "M1,3C1.333333,3,2.166667,1.333333,2,1C1.833333,0.666667,0.166667,0.666667,0,1C-0.166667,1.333333,0.666667,3,1,3"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1], [3, 3]]), "M1,3C1.333333,3,1.666667,1,2,1C2.333333,1,3.333333,3,3,3C2.666667,3,0.333333,1,0,1C-0.333333,1,0.666667,3,1,3"); + test.end(); +}); + +tape("line.curve(curveCatmullRomClosed.alpha(1))(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveCatmullRomClosed.alpha(1)); + test.equal(l([]), null); + test.pathEqual(l([[0, 1]]), "M0,1Z"); + test.pathEqual(l([[0, 1], [1, 3]]), "M1,3L0,1Z"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1]]), "M1,3C1.333333,3,2.236068,1.314757,2,1C1.788854,0.718473,0.211146,0.718473,0,1C-0.236068,1.314757,0.666667,3,1,3"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1], [3, 3]]), "M1,3C1.333333,3,1.666667,1,2,1C2.333333,1,3.031652,2.746782,3,3C2.948962,3.408301,0.051038,0.591699,0,1C-0.031652,1.253218,0.666667,3,1,3"); + test.end(); +}); + +tape("line.curve(curveCatmullRomClosed) uses a default alpha of 0.5 (centripetal)", function(test) { + var l = shape.line().curve(shape.curveCatmullRomClosed.alpha(0.5)); + test.equal(shape.line().curve(shape.curveCatmullRomClosed)([[0, 1], [1, 3], [2, 1], [3, 3]]), l([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); + +tape("line.curve(curveCatmullRomClosed.alpha(alpha)) coerces the specified alpha to a number", function(test) { + var l = shape.line().curve(shape.curveCatmullRomClosed.alpha("0.5")); + test.equal(shape.line().curve(shape.curveCatmullRomClosed.alpha(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), l([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); + +tape("area.curve(curveCatmullRomClosed.alpha(alpha)) coerces the specified alpha to a number", function(test) { + var a = shape.area().curve(shape.curveCatmullRomClosed.alpha("0.5")); + test.equal(shape.area().curve(shape.curveCatmullRomClosed.alpha(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), a([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); diff --git a/test/curve/catmullRomOpen-test.js b/test/curve/catmullRomOpen-test.js new file mode 100644 index 0000000..a8acad4 --- /dev/null +++ b/test/curve/catmullRomOpen-test.js @@ -0,0 +1,58 @@ +var tape = require("tape"), + shape = require("../../"); + +require("../pathEqual"); + +tape("line.curve(curveCatmullRomOpen)(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveCatmullRomOpen); + test.equal(l([]), null); + test.equal(l([[0, 1]]), null); + test.equal(l([[0, 1], [1, 3]]), null); + test.pathEqual(l([[0, 1], [1, 3], [2, 1]]), "M1,3Z"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1], [3, 3]]), "M1,3C1.333333,3,1.666667,1,2,1"); + test.end(); +}); + +tape("line.curve(curveCatmullRomOpen.alpha(1))(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveCatmullRomOpen.alpha(1)); + test.equal(l([]), null); + test.equal(l([[0, 1]]), null); + test.equal(l([[0, 1], [1, 3]]), null); + test.pathEqual(l([[0, 1], [1, 3], [2, 1]]), "M1,3Z"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1], [3, 3]]), "M1,3C1.333333,3,1.666667,1,2,1"); + test.end(); +}); + +tape("line.curve(curveCatmullRomOpen) uses a default alpha of 0.5 (centripetal)", function(test) { + var l = shape.line().curve(shape.curveCatmullRomOpen.alpha(0.5)); + test.equal(shape.line().curve(shape.curveCatmullRomOpen)([[0, 1], [1, 3], [2, 1], [3, 3]]), l([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); + +tape("line.curve(curveCatmullRom.alpha(alpha)) coerces the specified alpha to a number", function(test) { + var l = shape.line().curve(shape.curveCatmullRom.alpha("0.5")); + test.equal(shape.line().curve(shape.curveCatmullRom.alpha(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), l([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); + +tape("area.curve(curveCatmullRomOpen.alpha(0.5))(data) generates the expected path", function(test) { + var a = shape.area().curve(shape.curveCatmullRomOpen, 0.5); + test.equal(a([]), null); + test.equal(a([[0, 1]]), null); + test.equal(a([[0, 1], [1, 3]]), null); + test.pathEqual(a([[0, 1], [1, 3], [2, 1]]), "M1,3L1,0Z"); + test.pathEqual(a([[0, 1], [1, 3], [2, 1], [3, 3]]), "M1,3C1.333333,3,1.666667,1,2,1L2,0C1.666667,0,1.333333,0,1,0Z"); + test.end(); +}); + +tape("area.curve(curveCatmullRomOpen) uses a default alpha of 0.5 (centripetal)", function(test) { + var a = shape.area().curve(shape.curveCatmullRomOpen, 0.5); + test.equal(shape.area().curve(shape.curveCatmullRomOpen)([[0, 1], [1, 3], [2, 1], [3, 3]]), a([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); + +tape("area.curve(curveCatmullRomOpen.alpha(alpha)) coerces the specified alpha to a number", function(test) { + var a = shape.area().curve(shape.curveCatmullRomOpen.alpha("0.5")); + test.equal(shape.area().curve(shape.curveCatmullRomOpen.alpha(0.5))([[0, 1], [1, 3], [2, 1], [3, 3]]), a([[0, 1], [1, 3], [2, 1], [3, 3]])); + test.end(); +}); diff --git a/test/curve/linear-test.js b/test/curve/linear-test.js new file mode 100644 index 0000000..3480dd8 --- /dev/null +++ b/test/curve/linear-test.js @@ -0,0 +1,22 @@ +var tape = require("tape"), + shape = require("../../"); + +require("../pathEqual"); + +tape("line.curve(curveLinear)(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveLinear); + test.equal(l([]), null); + test.pathEqual(l([[0, 1]]), "M0,1Z"); + test.pathEqual(l([[0, 1], [2, 3]]), "M0,1L2,3"); + test.pathEqual(l([[0, 1], [2, 3], [4, 5]]), "M0,1L2,3L4,5"); + test.end(); +}); + +tape("area.curve(curveLinear)(data) generates the expected path", function(test) { + var a = shape.area().curve(shape.curveLinear); + test.equal(a([]), null); + test.pathEqual(a([[0, 1]]), "M0,1L0,0Z"); + test.pathEqual(a([[0, 1], [2, 3]]), "M0,1L2,3L2,0L0,0Z"); + test.pathEqual(a([[0, 1], [2, 3], [4, 5]]), "M0,1L2,3L4,5L4,0L2,0L0,0Z"); + test.end(); +}); diff --git a/test/curve/linearClosed-test.js b/test/curve/linearClosed-test.js new file mode 100644 index 0000000..9312f6b --- /dev/null +++ b/test/curve/linearClosed-test.js @@ -0,0 +1,13 @@ +var tape = require("tape"), + shape = require("../../"); + +require("../pathEqual"); + +tape("line.curve(curveLinearClosed)(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveLinearClosed); + test.equal(l([]), null); + test.pathEqual(l([[0, 1]]), "M0,1Z"); + test.pathEqual(l([[0, 1], [2, 3]]), "M0,1L2,3Z"); + test.pathEqual(l([[0, 1], [2, 3], [4, 5]]), "M0,1L2,3L4,5Z"); + test.end(); +}); diff --git a/test/curve/monotoneX-test.js b/test/curve/monotoneX-test.js new file mode 100644 index 0000000..c5efb26 --- /dev/null +++ b/test/curve/monotoneX-test.js @@ -0,0 +1,56 @@ +var tape = require("tape"), + shape = require("../../"); + +require("../pathEqual"); + +tape("line.curve(curveMonotoneX)(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveMonotoneX); + test.equal(l([]), null); + test.pathEqual(l([[0, 1]]), "M0,1Z"); + test.pathEqual(l([[0, 1], [1, 3]]), "M0,1L1,3"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1]]), "M0,1C0.333333,2,0.666667,3,1,3C1.333333,3,1.666667,2,2,1"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1], [3, 3]]), "M0,1C0.333333,2,0.666667,3,1,3C1.333333,3,1.666667,1,2,1C2.333333,1,2.666667,2,3,3"); + test.end(); +}); + +tape("line.curve(curveMonotoneX)(data) preserves monotonicity in y", function(test) { + var l = shape.line().curve(shape.curveMonotoneX); + test.pathEqual(l([[0, 200], [100, 100], [200, 100], [300, 300], [400, 300]]), "M0,200C33.333333,150,66.666667,100,100,100C133.333333,100,166.666667,100,200,100C233.333333,100,266.666667,300,300,300C333.333333,300,366.666667,300,400,300"); + test.end(); +}); + +tape("line.curve(curveMonotoneX)(data) handles duplicate x-values", function(test) { + var l = shape.line().curve(shape.curveMonotoneX); + test.pathEqual(l([[0, 200], [0, 100], [100, 100], [200, 0]]), "M0,200C0,200,0,100,0,100C33.333333,100,66.666667,100,100,100C133.333333,100,166.666667,50,200,0"); + test.pathEqual(l([[0, 200], [100, 100], [100, 0], [200, 0]]), "M0,200C33.333333,183.333333,66.666667,166.666667,100,100C100,100,100,0,100,0C133.333333,0,166.666667,0,200,0"); + test.pathEqual(l([[0, 200], [100, 100], [200, 100], [200, 0]]), "M0,200C33.333333,150,66.666667,100,100,100C133.333333,100,166.666667,100,200,100C200,100,200,0,200,0"); + test.end(); +}); + +tape("line.curve(curveMonotoneX)(data) handles segments of infinite slope", function(test) { + var l = shape.line().curve(shape.curveMonotoneX); + test.pathEqual(l([[0, 200], [100, 150], [100, 50], [200, 0]]), "M0,200C33.333333,191.666667,66.666667,183.333333,100,150C100,150,100,50,100,50C133.333333,16.666667,166.666667,8.333333,200,0"); + test.pathEqual(l([[200, 0], [100, 50], [100, 150], [0, 200]]), "M200,0C166.666667,8.333333,133.333333,16.666667,100,50C100,50,100,150,100,150C66.666667,183.333333,33.333333,191.666667,0,200"); + test.end(); +}); + +tape("line.curve(curveMonotoneX)(data) ignores coincident points", function(test) { + var l = shape.line().curve(shape.curveMonotoneX), + p = l([[0, 200], [50, 200], [100, 100], [150, 0], [200, 0]]); + test.equal(l([[0, 200], [0, 200], [50, 200], [100, 100], [150, 0], [200, 0]]), p); + test.equal(l([[0, 200], [50, 200], [50, 200], [100, 100], [150, 0], [200, 0]]), p); + test.equal(l([[0, 200], [50, 200], [100, 100], [100, 100], [150, 0], [200, 0]]), p); + test.equal(l([[0, 200], [50, 200], [100, 100], [150, 0], [150, 0], [200, 0]]), p); + test.equal(l([[0, 200], [50, 200], [100, 100], [150, 0], [200, 0], [200, 0]]), p); + test.end(); +}); + +tape("area.curve(curveMonotoneX)(data) generates the expected path", function(test) { + var a = shape.area().curve(shape.curveMonotoneX); + test.equal(a([]), null); + test.pathEqual(a([[0, 1]]), "M0,1L0,0Z"); + test.pathEqual(a([[0, 1], [1, 3]]), "M0,1L1,3L1,0L0,0Z"); + test.pathEqual(a([[0, 1], [1, 3], [2, 1]]), "M0,1C0.333333,2,0.666667,3,1,3C1.333333,3,1.666667,2,2,1L2,0C1.666667,0,1.333333,0,1,0C0.666667,0,0.333333,0,0,0Z"); + test.pathEqual(a([[0, 1], [1, 3], [2, 1], [3, 3]]), "M0,1C0.333333,2,0.666667,3,1,3C1.333333,3,1.666667,1,2,1C2.333333,1,2.666667,2,3,3L3,0C2.666667,0,2.333333,0,2,0C1.666667,0,1.333333,0,1,0C0.666667,0,0.333333,0,0,0Z"); + test.end(); +}); diff --git a/test/curve/monotoneY-test.js b/test/curve/monotoneY-test.js new file mode 100644 index 0000000..690252a --- /dev/null +++ b/test/curve/monotoneY-test.js @@ -0,0 +1,60 @@ +var tape = require("tape"), + shape = require("../../"); + +require("../pathEqual"); + +tape("line.curve(curveMonotoneY)(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveMonotoneY); + test.equal(l([]), null); + test.pathEqual(l([[0, 1]].map(reflect)), "M1,0Z"); + test.pathEqual(l([[0, 1], [1, 3]].map(reflect)), "M1,0L3,1"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1]].map(reflect)), "M1,0C2,0.333333,3,0.666667,3,1C3,1.333333,2,1.666667,1,2"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1], [3, 3]].map(reflect)), "M1,0C2,0.333333,3,0.666667,3,1C3,1.333333,1,1.666667,1,2C1,2.333333,2,2.666667,3,3"); + test.end(); +}); + +tape("line.curve(curveMonotoneY)(data) preserves monotonicity in y", function(test) { + var l = shape.line().curve(shape.curveMonotoneY); + test.pathEqual(l([[0, 200], [100, 100], [200, 100], [300, 300], [400, 300]].map(reflect)), "M200,0C150,33.333333,100,66.666667,100,100C100,133.333333,100,166.666667,100,200C100,233.333333,300,266.666667,300,300C300,333.333333,300,366.666667,300,400"); + test.end(); +}); + +tape("line.curve(curveMonotoneY)(data) handles duplicate x-values", function(test) { + var l = shape.line().curve(shape.curveMonotoneY); + test.pathEqual(l([[0, 200], [0, 100], [100, 100], [200, 0]].map(reflect)), "M200,0C200,0,100,0,100,0C100,33.333333,100,66.666667,100,100C100,133.333333,50,166.666667,0,200"); + test.pathEqual(l([[0, 200], [100, 100], [100, 0], [200, 0]].map(reflect)), "M200,0C183.333333,33.333333,166.666667,66.666667,100,100C100,100,0,100,0,100C0,133.333333,0,166.666667,0,200"); + test.pathEqual(l([[0, 200], [100, 100], [200, 100], [200, 0]].map(reflect)), "M200,0C150,33.333333,100,66.666667,100,100C100,133.333333,100,166.666667,100,200C100,200,0,200,0,200"); + test.end(); +}); + +tape("line.curve(curveMonotoneY)(data) handles segments of infinite slope", function(test) { + var l = shape.line().curve(shape.curveMonotoneY); + test.pathEqual(l([[0, 200], [100, 150], [100, 50], [200, 0]].map(reflect)), "M200,0C191.666667,33.333333,183.333333,66.666667,150,100C150,100,50,100,50,100C16.666667,133.333333,8.333333,166.666667,0,200"); + test.pathEqual(l([[200, 0], [100, 50], [100, 150], [0, 200]].map(reflect)), "M0,200C8.333333,166.666667,16.666667,133.333333,50,100C50,100,150,100,150,100C183.333333,66.666667,191.666667,33.333333,200,0"); + test.end(); +}); + +tape("line.curve(curveMonotoneY)(data) ignores coincident points", function(test) { + var l = shape.line().curve(shape.curveMonotoneY), + p = l([[0, 200], [50, 200], [100, 100], [150, 0], [200, 0]].map(reflect)); + test.equal(l([[0, 200], [0, 200], [50, 200], [100, 100], [150, 0], [200, 0]].map(reflect)), p); + test.equal(l([[0, 200], [50, 200], [50, 200], [100, 100], [150, 0], [200, 0]].map(reflect)), p); + test.equal(l([[0, 200], [50, 200], [100, 100], [100, 100], [150, 0], [200, 0]].map(reflect)), p); + test.equal(l([[0, 200], [50, 200], [100, 100], [150, 0], [150, 0], [200, 0]].map(reflect)), p); + test.equal(l([[0, 200], [50, 200], [100, 100], [150, 0], [200, 0], [200, 0]].map(reflect)), p); + test.end(); +}); + +tape("area.curve(curveMonotoneY)(data) generates the expected path", function(test) { + var a = shape.area().curve(shape.curveMonotoneY); + test.equal(a([].map(reflect)), null); + test.pathEqual(a([[0, 1]].map(reflect)), "M1,0L1,0Z"); + test.pathEqual(a([[0, 1], [1, 3]].map(reflect)), "M1,0L3,1L3,0L1,0Z"); + test.pathEqual(a([[0, 1], [1, 3], [2, 1]].map(reflect)), "M1,0C2,0.333333,3,0.666667,3,1C3,1.333333,2,1.666667,1,2L1,0C1,0,3,0,3,0C3,0,1,0,1,0Z"); + test.pathEqual(a([[0, 1], [1, 3], [2, 1], [3, 3]].map(reflect)), "M1,0C2,0.333333,3,0.666667,3,1C3,1.333333,1,1.666667,1,2C1,2.333333,2,2.666667,3,3L3,0C3,0,1,0,1,0C1,0,3,0,3,0C3,0,1,0,1,0Z"); + test.end(); +}); + +function reflect(p) { + return [p[1], p[0]]; +} diff --git a/test/curve/natural-test.js b/test/curve/natural-test.js new file mode 100644 index 0000000..5664b2b --- /dev/null +++ b/test/curve/natural-test.js @@ -0,0 +1,24 @@ +var tape = require("tape"), + shape = require("../../"); + +require("../pathEqual"); + +tape("line.curve(curveNatural)(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveNatural); + test.equal(l([]), null); + test.pathEqual(l([[0, 1]]), "M0,1Z"); + test.pathEqual(l([[0, 1], [1, 3]]), "M0,1L1,3"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1]]), "M0,1C0.333333,2,0.666667,3,1,3C1.333333,3,1.666667,2,2,1"); + test.pathEqual(l([[0, 1], [1, 3], [2, 1], [3, 3]]), "M0,1C0.333333,2.111111,0.666667,3.222222,1,3C1.333333,2.777778,1.666667,1.222222,2,1C2.333333,0.777778,2.666667,1.888889,3,3"); + test.end(); +}); + +tape("area.curve(curveNatural)(data) generates the expected path", function(test) { + var a = shape.area().curve(shape.curveNatural); + test.equal(a([]), null); + test.pathEqual(a([[0, 1]]), "M0,1L0,0Z"); + test.pathEqual(a([[0, 1], [1, 3]]), "M0,1L1,3L1,0L0,0Z"); + test.pathEqual(a([[0, 1], [1, 3], [2, 1]]), "M0,1C0.333333,2,0.666667,3,1,3C1.333333,3,1.666667,2,2,1L2,0C1.666667,0,1.333333,0,1,0C0.666667,0,0.333333,0,0,0Z"); + test.pathEqual(a([[0, 1], [1, 3], [2, 1], [3, 3]]), "M0,1C0.333333,2.111111,0.666667,3.222222,1,3C1.333333,2.777778,1.666667,1.222222,2,1C2.333333,0.777778,2.666667,1.888889,3,3L3,0C2.666667,0,2.333333,0,2,0C1.666667,0,1.333333,0,1,0C0.666667,0,0.333333,0,0,0Z"); + test.end(); +}); diff --git a/test/curve/step-test.js b/test/curve/step-test.js new file mode 100644 index 0000000..e37e681 --- /dev/null +++ b/test/curve/step-test.js @@ -0,0 +1,22 @@ +var tape = require("tape"), + shape = require("../../"); + +require("../pathEqual"); + +tape("line.curve(curveStep)(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveStep); + test.equal(l([]), null); + test.pathEqual(l([[0, 1]]), "M0,1Z"); + test.pathEqual(l([[0, 1], [2, 3]]), "M0,1L1,1L1,3L2,3"); + test.pathEqual(l([[0, 1], [2, 3], [4, 5]]), "M0,1L1,1L1,3L3,3L3,5L4,5"); + test.end(); +}); + +tape("area.curve(curveStep)(data) generates the expected path", function(test) { + var a = shape.area().curve(shape.curveStep); + test.equal(a([]), null); + test.pathEqual(a([[0, 1]]), "M0,1L0,0Z"); + test.pathEqual(a([[0, 1], [2, 3]]), "M0,1L1,1L1,3L2,3L2,0L1,0L1,0L0,0Z"); + test.pathEqual(a([[0, 1], [2, 3], [4, 5]]), "M0,1L1,1L1,3L3,3L3,5L4,5L4,0L3,0L3,0L1,0L1,0L0,0Z"); + test.end(); +}); diff --git a/test/curve/stepAfter-test.js b/test/curve/stepAfter-test.js new file mode 100644 index 0000000..8466258 --- /dev/null +++ b/test/curve/stepAfter-test.js @@ -0,0 +1,22 @@ +var tape = require("tape"), + shape = require("../../"); + +require("../pathEqual"); + +tape("line.curve(curveStepAfter)(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveStepAfter); + test.equal(l([]), null); + test.pathEqual(l([[0, 1]]), "M0,1Z"); + test.pathEqual(l([[0, 1], [2, 3]]), "M0,1L2,1L2,3"); + test.pathEqual(l([[0, 1], [2, 3], [4, 5]]), "M0,1L2,1L2,3L4,3L4,5"); + test.end(); +}); + +tape("area.curve(curveStepAfter)(data) generates the expected path", function(test) { + var a = shape.area().curve(shape.curveStepAfter); + test.equal(a([]), null); + test.pathEqual(a([[0, 1]]), "M0,1L0,0Z"); + test.pathEqual(a([[0, 1], [2, 3]]), "M0,1L2,1L2,3L2,0L2,0L0,0Z"); + test.pathEqual(a([[0, 1], [2, 3], [4, 5]]), "M0,1L2,1L2,3L4,3L4,5L4,0L4,0L2,0L2,0L0,0Z"); + test.end(); +}); diff --git a/test/curve/stepBefore-test.js b/test/curve/stepBefore-test.js new file mode 100644 index 0000000..7a8470c --- /dev/null +++ b/test/curve/stepBefore-test.js @@ -0,0 +1,22 @@ +var tape = require("tape"), + shape = require("../../"); + +require("../pathEqual"); + +tape("line.curve(curveStepBefore)(data) generates the expected path", function(test) { + var l = shape.line().curve(shape.curveStepBefore); + test.equal(l([]), null); + test.pathEqual(l([[0, 1]]), "M0,1Z"); + test.pathEqual(l([[0, 1], [2, 3]]), "M0,1L0,3L2,3"); + test.pathEqual(l([[0, 1], [2, 3], [4, 5]]), "M0,1L0,3L2,3L2,5L4,5"); + test.end(); +}); + +tape("area.curve(curveStepBefore)(data) generates the expected path", function(test) { + var a = shape.area().curve(shape.curveStepBefore); + test.equal(a([]), null); + test.pathEqual(a([[0, 1]]), "M0,1L0,0Z"); + test.pathEqual(a([[0, 1], [2, 3]]), "M0,1L0,3L2,3L2,0L0,0L0,0Z"); + test.pathEqual(a([[0, 1], [2, 3], [4, 5]]), "M0,1L0,3L2,3L2,5L4,5L4,0L2,0L2,0L0,0L0,0Z"); + test.end(); +}); diff --git a/test/inDelta.js b/test/inDelta.js new file mode 100644 index 0000000..f3def21 --- /dev/null +++ b/test/inDelta.js @@ -0,0 +1,10 @@ +var tape = require("tape"); + +tape.Test.prototype.inDelta = function(actual, expected) { + this._assert(expected - 1e-6 < actual && actual < expected + 1e-6, { + message: "should be in delta", + operator: "inDelta", + actual: actual, + expected: expected + }); +}; diff --git a/test/line-test.js b/test/line-test.js new file mode 100644 index 0000000..3505d20 --- /dev/null +++ b/test/line-test.js @@ -0,0 +1,67 @@ +var tape = require("tape"), + shape = require("../"); + +require("./pathEqual"); + +tape("line() returns a default line shape", function(test) { + var l = shape.line(); + test.equal(l.x()([42, 34]), 42); + test.equal(l.y()([42, 34]), 34); + test.equal(l.defined()([42, 34]), true); + test.equal(l.curve(), shape.curveLinear); + test.equal(l.context(), null); + test.pathEqual(l([[0, 1], [2, 3], [4, 5]]), "M0,1L2,3L4,5"); + test.end(); +}); + +tape("line.x(f)(data) passes d, i and data to the specified function f", function(test) { + var data = ["a", "b"], actual = []; + shape.line().x(function() { actual.push([].slice.call(arguments)); })(data); + test.deepEqual(actual, [["a", 0, data], ["b", 1, data]]); + test.end(); +}); + +tape("line.y(f)(data) passes d, i and data to the specified function f", function(test) { + var data = ["a", "b"], actual = []; + shape.line().y(function() { actual.push([].slice.call(arguments)); })(data); + test.deepEqual(actual, [["a", 0, data], ["b", 1, data]]); + test.end(); +}); + +tape("line.defined(f)(data) passes d, i and data to the specified function f", function(test) { + var data = ["a", "b"], actual = []; + shape.line().defined(function() { actual.push([].slice.call(arguments)); })(data); + test.deepEqual(actual, [["a", 0, data], ["b", 1, data]]); + test.end(); +}); + +tape("line.x(x)(data) observes the specified function", function(test) { + var l = shape.line().x(function(d) { return d.x; }); + test.pathEqual(l([{x: 0, 1: 1}, {x: 2, 1: 3}, {x: 4, 1: 5}]), "M0,1L2,3L4,5"); + test.end(); +}); + +tape("line.x(x)(data) observes the specified constant", function(test) { + var l = shape.line().x(0); + test.pathEqual(l([{1: 1}, {1: 3}, {1: 5}]), "M0,1L0,3L0,5"); + test.end(); +}); + +tape("line.y(y)(data) observes the specified function", function(test) { + var l = shape.line().y(function(d) { return d.y; }); + test.pathEqual(l([{0: 0, y: 1}, {0: 2, y: 3}, {0: 4, y: 5}]), "M0,1L2,3L4,5"); + test.end(); +}); + +tape("line.y(y)(data) observes the specified constant", function(test) { + var l = shape.line().y(0); + test.pathEqual(l([{0: 0}, {0: 2}, {0: 4}]), "M0,0L2,0L4,0"); + test.end(); +}); + +tape("line.curve(curve) sets the curve method", function(test) { + var l = shape.line().curve(shape.curveLinearClosed); + test.equal(l([]), null); + test.pathEqual(l([[0, 1], [2, 3]]), "M0,1L2,3Z"); + test.end(); +}); diff --git a/test/lineRadial-test.js b/test/lineRadial-test.js new file mode 100644 index 0000000..20138ca --- /dev/null +++ b/test/lineRadial-test.js @@ -0,0 +1,15 @@ +var tape = require("tape"), + shape = require("../"); + +require("./pathEqual"); + +tape("lineRadial() returns a default radial line shape", function(test) { + var l = shape.lineRadial(); + test.equal(l.angle()([42, 34]), 42); + test.equal(l.radius()([42, 34]), 34); + test.equal(l.defined()([42, 34]), true); + test.equal(l.curve(), shape.curveLinear); + test.equal(l.context(), null); + test.pathEqual(l([[0, 1], [2, 3], [4, 5]]), "M0,-1L2.727892,1.248441L-3.784012,3.268218"); + test.end(); +}); diff --git a/test/offset/diverging-test.js b/test/offset/diverging-test.js new file mode 100644 index 0000000..04d0f40 --- /dev/null +++ b/test/offset/diverging-test.js @@ -0,0 +1,64 @@ +var tape = require("tape"), + shape = require("../../"); + +tape("stackOffsetDiverging(series, order) applies a zero baseline, ignoring existing offsets", function(test) { + var series = [ + [[1, 2], [2, 4], [3, 4]], + [[0, 3], [0, 4], [0, 2]], + [[0, 5], [0, 2], [0, 4]] + ]; + shape.stackOffsetDiverging(series, shape.stackOrderNone(series)); + test.deepEqual(series, [ + [[0, 1], [0, 2], [0, 1]], + [[1, 4], [2, 6], [1, 3]], + [[4, 9], [6, 8], [3, 7]] + ]); + test.end(); +}); + +tape("stackOffsetDiverging(series, order) treats NaN as zero", function(test) { + var series = [ + [[0, 1], [0, 2], [0, 1]], + [[0, 3], [0, NaN], [0, 2]], + [[0, 5], [0, 2], [0, 4]] + ]; + shape.stackOffsetDiverging(series, shape.stackOrderNone(series)); + test.ok(isNaN(series[1][1][1])); + series[1][1][1] = "NaN"; // can’t test.equal NaN + test.deepEqual(series, [ + [[0, 1], [0, 2], [0, 1]], + [[1, 4], [2, "NaN"], [1, 3]], + [[4, 9], [2, 4], [3, 7]] + ]); + test.end(); +}); + +tape("stackOffsetDiverging(series, order) observes the specified order", function(test) { + var series = [ + [[0, 1], [0, 2], [0, 1]], + [[0, 3], [0, 4], [0, 2]], + [[0, 5], [0, 2], [0, 4]] + ]; + shape.stackOffsetDiverging(series, shape.stackOrderReverse(series)); + test.deepEqual(series, [ + [[8, 9], [6, 8], [6, 7]], + [[5, 8], [2, 6], [4, 6]], + [[0, 5], [0, 2], [0, 4]] + ]); + test.end(); +}); + +tape("stackOffsetDiverging(series, order) puts negative values below zero, in order", function(test) { + var series = [ + [[0, 1], [0, -2], [0, -1]], + [[0, -3], [0, -4], [0, -2]], + [[0, -5], [0, -2], [0, 4]] + ]; + shape.stackOffsetDiverging(series, shape.stackOrderNone(series)); + test.deepEqual(series, [ + [[ 0, 1], [-2, 0], [-1, 0]], + [[-3, 0], [-6, -2], [-3, -1]], + [[-8, -3], [-8, -6], [ 0, 4]] + ]); + test.end(); +}); diff --git a/test/offset/expand-test.js b/test/offset/expand-test.js new file mode 100644 index 0000000..497c0c1 --- /dev/null +++ b/test/offset/expand-test.js @@ -0,0 +1,49 @@ +var tape = require("tape"), + shape = require("../../"); + +tape("stackOffsetExpand(series, order) expands to fill [0, 1]", function(test) { + var series = [ + [[0, 1], [0, 2], [0, 1]], + [[0, 3], [0, 4], [0, 2]], + [[0, 5], [0, 2], [0, 4]] + ]; + shape.stackOffsetExpand(series, shape.stackOrderNone(series)); + test.deepEqual(series, [ + [[0 / 9, 1 / 9], [0 / 8, 2 / 8], [0 / 7, 1 / 7]], + [[1 / 9, 4 / 9], [2 / 8, 6 / 8], [1 / 7, 3 / 7]], + [[4 / 9, 9 / 9], [6 / 8, 8 / 8], [3 / 7, 7 / 7]] + ]); + test.end(); +}); + +tape("stackOffsetExpand(series, order) treats NaN as zero", function(test) { + var series = [ + [[0, 1], [0, 2], [0, 1]], + [[0, 3], [0, NaN], [0, 2]], + [[0, 5], [0, 2], [0, 4]] + ]; + shape.stackOffsetExpand(series, shape.stackOrderNone(series)); + test.ok(isNaN(series[1][1][1])); + series[1][1][1] = "NaN"; // can’t test.equal NaN + test.deepEqual(series, [ + [[0 / 9, 1 / 9], [0 / 4, 2 / 4], [0 / 7, 1 / 7]], + [[1 / 9, 4 / 9], [2 / 4, "NaN"], [1 / 7, 3 / 7]], + [[4 / 9, 9 / 9], [2 / 4, 4 / 4], [3 / 7, 7 / 7]] + ]); + test.end(); +}); + +tape("stackOffsetExpand(series, order) observes the specified order", function(test) { + var series = [ + [[0, 1], [0, 2], [0, 1]], + [[0, 3], [0, 4], [0, 2]], + [[0, 5], [0, 2], [0, 4]] + ]; + shape.stackOffsetExpand(series, shape.stackOrderReverse(series)); + test.deepEqual(series, [ + [[8 / 9, 9 / 9], [6 / 8, 8 / 8], [6 / 7, 7 / 7]], + [[5 / 9, 8 / 9], [2 / 8, 6 / 8], [4 / 7, 6 / 7]], + [[0 / 9, 5 / 9], [0 / 8, 2 / 8], [0 / 7, 4 / 7]] + ]); + test.end(); +}); diff --git a/test/offset/none-test.js b/test/offset/none-test.js new file mode 100644 index 0000000..3faefb0 --- /dev/null +++ b/test/offset/none-test.js @@ -0,0 +1,49 @@ +var tape = require("tape"), + shape = require("../../"); + +tape("stackOffsetNone(series, order) stacks upon the first layer’s existing positions", function(test) { + var series = [ + [[1, 2], [2, 4], [3, 4]], + [[0, 3], [0, 4], [0, 2]], + [[0, 5], [0, 2], [0, 4]] + ]; + shape.stackOffsetNone(series, shape.stackOrderNone(series)); + test.deepEqual(series, [ + [[1, 2], [2, 4], [3, 4]], + [[2, 5], [4, 8], [4, 6]], + [[5, 10], [8, 10], [6, 10]] + ]); + test.end(); +}); + +tape("stackOffsetNone(series, order) treats NaN as zero", function(test) { + var series = [ + [[0, 1], [0, 2], [0, 1]], + [[0, 3], [0, NaN], [0, 2]], + [[0, 5], [0, 2], [0, 4]] + ]; + shape.stackOffsetNone(series, shape.stackOrderNone(series)); + test.ok(isNaN(series[1][1][1])); + series[1][1][1] = "NaN"; // can’t test.equal NaN + test.deepEqual(series, [ + [[0, 1], [0, 2], [0, 1]], + [[1, 4], [2, "NaN"], [1, 3]], + [[4, 9], [2, 4], [3, 7]] + ]); + test.end(); +}); + +tape("stackOffsetNone(series, order) observes the specified order", function(test) { + var series = [ + [[0, 1], [0, 2], [0, 1]], + [[0, 3], [0, 4], [0, 2]], + [[0, 5], [0, 2], [0, 4]] + ]; + shape.stackOffsetNone(series, shape.stackOrderReverse(series)); + test.deepEqual(series, [ + [[8, 9], [6, 8], [6, 7]], + [[5, 8], [2, 6], [4, 6]], + [[0, 5], [0, 2], [0, 4]] + ]); + test.end(); +}); diff --git a/test/offset/silhouette-test.js b/test/offset/silhouette-test.js new file mode 100644 index 0000000..5ab18bf --- /dev/null +++ b/test/offset/silhouette-test.js @@ -0,0 +1,49 @@ +var tape = require("tape"), + shape = require("../../"); + +tape("stackOffsetSilhouette(series, order) centers the stack around zero", function(test) { + var series = [ + [[0, 1], [0, 2], [0, 1]], + [[0, 3], [0, 4], [0, 2]], + [[0, 5], [0, 2], [0, 4]] + ]; + shape.stackOffsetSilhouette(series, shape.stackOrderNone(series)); + test.deepEqual(series, [ + [[0 - 9 / 2, 1 - 9 / 2], [0 - 8 / 2, 2 - 8 / 2], [0 - 7 / 2, 1 - 7 / 2]], + [[1 - 9 / 2, 4 - 9 / 2], [2 - 8 / 2, 6 - 8 / 2], [1 - 7 / 2, 3 - 7 / 2]], + [[4 - 9 / 2, 9 - 9 / 2], [6 - 8 / 2, 8 - 8 / 2], [3 - 7 / 2, 7 - 7 / 2]] + ]); + test.end(); +}); + +tape("stackOffsetSilhouette(series, order) treats NaN as zero", function(test) { + var series = [ + [[0, 1], [0, 2], [0, 1]], + [[0, 3], [0, NaN], [0, 2]], + [[0, 5], [0, 2], [0, 4]] + ]; + shape.stackOffsetSilhouette(series, shape.stackOrderNone(series)); + test.ok(isNaN(series[1][1][1])); + series[1][1][1] = "NaN"; // can’t test.equal NaN + test.deepEqual(series, [ + [[0 - 9 / 2, 1 - 9 / 2], [0 - 4 / 2, 2 - 4 / 2], [0 - 7 / 2, 1 - 7 / 2]], + [[1 - 9 / 2, 4 - 9 / 2], [2 - 4 / 2, "NaN"], [1 - 7 / 2, 3 - 7 / 2]], + [[4 - 9 / 2, 9 - 9 / 2], [2 - 4 / 2, 4 - 4 / 2], [3 - 7 / 2, 7 - 7 / 2]] + ]); + test.end(); +}); + +tape("stackOffsetSilhouette(series, order) observes the specified order", function(test) { + var series = [ + [[0, 1], [0, 2], [0, 1]], + [[0, 3], [0, 4], [0, 2]], + [[0, 5], [0, 2], [0, 4]] + ]; + shape.stackOffsetSilhouette(series, shape.stackOrderReverse(series)); + test.deepEqual(series, [ + [[8 - 9 / 2, 9 - 9 / 2], [6 - 8 / 2, 8 - 8 / 2], [6 - 7 / 2, 7 - 7 / 2]], + [[5 - 9 / 2, 8 - 9 / 2], [2 - 8 / 2, 6 - 8 / 2], [4 - 7 / 2, 6 - 7 / 2]], + [[0 - 9 / 2, 5 - 9 / 2], [0 - 8 / 2, 2 - 8 / 2], [0 - 7 / 2, 4 - 7 / 2]] + ]); + test.end(); +}); diff --git a/test/offset/wiggle-test.js b/test/offset/wiggle-test.js new file mode 100644 index 0000000..95d4a45 --- /dev/null +++ b/test/offset/wiggle-test.js @@ -0,0 +1,61 @@ +var tape = require("tape"), + shape = require("../../"); + +tape("stackOffsetWiggle(series, order) minimizes weighted wiggle", function(test) { + var series = [ + [[0, 1], [0, 2], [0, 1]], + [[0, 3], [0, 4], [0, 2]], + [[0, 5], [0, 2], [0, 4]] + ]; + shape.stackOffsetWiggle(series, shape.stackOrderNone(series)); + test.deepEqual(series.map(roundSeries), [ + [[0, 1], [-1, 1], [0.7857143, 1.7857143]], + [[1, 4], [ 1, 5], [1.7857143, 3.7857143]], + [[4, 9], [ 5, 7], [3.7857143, 7.7857143]] + ].map(roundSeries)); + test.end(); +}); + +tape("stackOffsetWiggle(series, order) treats NaN as zero", function(test) { + var series = [ + [[0, 1], [0, 2], [0, 1]], + [[0, NaN], [0, NaN], [0, NaN]], + [[0, 3], [0, 4], [0, 2]], + [[0, 5], [0, 2], [0, 4]] + ]; + shape.stackOffsetWiggle(series, shape.stackOrderNone(series)); + test.ok(isNaN(series[1][0][1])); + test.ok(isNaN(series[1][0][2])); + test.ok(isNaN(series[1][0][3])); + series[1][0][1] = series[1][1][1] = series[1][2][1] = "NaN"; // can’t test.equal NaN + test.deepEqual(series.map(roundSeries), [ + [[0, 1], [-1, 1], [0.7857143, 1.7857143]], + [[1, "NaN"], [ 1, "NaN"], [1.7857143, "NaN"]], + [[1, 4], [ 1, 5], [1.7857143, 3.7857143]], + [[4, 9], [ 5, 7], [3.7857143, 7.7857143]] + ].map(roundSeries)); + test.end(); +}); + +tape("stackOffsetWiggle(series, order) observes the specified order", function(test) { + var series = [ + [[0, 1], [0, 2], [0, 1]], + [[0, 3], [0, 4], [0, 2]], + [[0, 5], [0, 2], [0, 4]] + ]; + shape.stackOffsetWiggle(series, shape.stackOrderReverse(series)); + test.deepEqual(series.map(roundSeries), [ + [[8, 9], [8, 10], [7.21428571, 8.21428571]], + [[5, 8], [4, 8], [5.21428571, 7.21428571]], + [[0, 5], [2, 4], [1.21428571, 5.21428571]] + ].map(roundSeries)); + test.end(); +}); + +function roundSeries(series) { + return series.map(function(point) { + return point.map(function(value) { + return isNaN(value) ? value : Math.round(value * 1e6) / 1e6; + }); + }); +} diff --git a/test/order/ascending-test.js b/test/order/ascending-test.js new file mode 100644 index 0000000..a85d9eb --- /dev/null +++ b/test/order/ascending-test.js @@ -0,0 +1,20 @@ +var tape = require("tape"), + shape = require("../../"); + +tape("stackOrderAscending(series) returns an order by sum", function(test) { + test.deepEqual(shape.stackOrderAscending([ + [[0, 1], [0, 2], [0, 3]], + [[0, 2], [0, 3], [0, 4]], + [[0, 0], [0, 1], [0, 2]] + ]), [2, 0, 1]); + test.end(); +}); + +tape("stackOrderAscending(series) treats NaN values as zero", function(test) { + test.deepEqual(shape.stackOrderAscending([ + [[0, 1], [0, 2], [0, NaN], [0, 3]], + [[0, 2], [0, 3], [0, NaN], [0, 4]], + [[0, 0], [0, 1], [0, NaN], [0, 2]] + ]), [2, 0, 1]); + test.end(); +}); diff --git a/test/order/descending-test.js b/test/order/descending-test.js new file mode 100644 index 0000000..a5f5499 --- /dev/null +++ b/test/order/descending-test.js @@ -0,0 +1,20 @@ +var tape = require("tape"), + shape = require("../../"); + +tape("stackOrderDescending(series) returns an order by sum", function(test) { + test.deepEqual(shape.stackOrderDescending([ + [[0, 1], [0, 2], [0, 3]], + [[0, 2], [0, 3], [0, 4]], + [[0, 0], [0, 1], [0, 2]] + ]), [1, 0, 2]); + test.end(); +}); + +tape("stackOrderDescending(series) treats NaN values as zero", function(test) { + test.deepEqual(shape.stackOrderDescending([ + [[0, 1], [0, 2], [0, 3], [0, NaN]], + [[0, 2], [0, 3], [0, 4], [0, NaN]], + [[0, 0], [0, 1], [0, 2], [0, NaN]] + ]), [1, 0, 2]); + test.end(); +}); diff --git a/test/order/insideOut-test.js b/test/order/insideOut-test.js new file mode 100644 index 0000000..cbd4010 --- /dev/null +++ b/test/order/insideOut-test.js @@ -0,0 +1,28 @@ +var tape = require("tape"), + shape = require("../../"); + +tape("stackOrderInsideOut(series) returns an order by sum", function(test) { + test.deepEqual(shape.stackOrderInsideOut([ + [[0, 0]], + [[0, 1]], + [[0, 2]], + [[0, 3]], + [[0, 4]], + [[0, 5]], + [[0, 6]] + ]), [2, 3, 6, 5, 4, 1, 0]); + test.end(); +}); + +tape("stackOrderInsideOut(series) treats NaN values as zero", function(test) { + test.deepEqual(shape.stackOrderInsideOut([ + [[0, 0], [0, NaN]], + [[0, 1], [0, NaN]], + [[0, 2], [0, NaN]], + [[0, 3], [0, NaN]], + [[0, 4], [0, NaN]], + [[0, 5], [0, NaN]], + [[0, 6], [0, NaN]] + ]), [2, 3, 6, 5, 4, 1, 0]); + test.end(); +}); diff --git a/test/order/none-test.js b/test/order/none-test.js new file mode 100644 index 0000000..2f8f9b8 --- /dev/null +++ b/test/order/none-test.js @@ -0,0 +1,7 @@ +var tape = require("tape"), + shape = require("../../"); + +tape("stackOrderNone(series) returns [0, 1, … series.length - 1]", function(test) { + test.deepEqual(shape.stackOrderNone(new Array(4)), [0, 1, 2, 3]); + test.end(); +}); diff --git a/test/order/reverse-test.js b/test/order/reverse-test.js new file mode 100644 index 0000000..396df3a --- /dev/null +++ b/test/order/reverse-test.js @@ -0,0 +1,7 @@ +var tape = require("tape"), + shape = require("../../"); + +tape("stackOrderReverse(series) returns [series.length - 1, series.length - 2, … 0]", function(test) { + test.deepEqual(shape.stackOrderReverse(new Array(4)), [3, 2, 1, 0]); + test.end(); +}); diff --git a/test/pathEqual.js b/test/pathEqual.js new file mode 100644 index 0000000..1f064cf --- /dev/null +++ b/test/pathEqual.js @@ -0,0 +1,22 @@ +var tape = require("tape"); + +var reNumber = /[-+]?(?:\d+\.\d+|\d+\.|\.\d+|\d+)(?:[eE][-]?\d+)?/g; + +tape.Test.prototype.pathEqual = function(actual, expected) { + actual = normalizePath(actual + ""); + // expected = normalizePath(expected + ""); + this._assert(actual === expected, { + message: "should be equal", + operator: "pathEqual", + actual: actual, + expected: expected + }); +}; + +function normalizePath(path) { + return path.replace(reNumber, formatNumber); +} + +function formatNumber(s) { + return Math.abs((s = +s) - Math.round(s)) < 1e-6 ? Math.round(s) : s.toFixed(6); +} diff --git a/test/pie-test.js b/test/pie-test.js new file mode 100644 index 0000000..0a4aaa8 --- /dev/null +++ b/test/pie-test.js @@ -0,0 +1,209 @@ +var tape = require("tape"), + shape = require("../"); + +tape("pie() returns a default pie shape", function(test) { + var p = shape.pie(); + test.equal(p.value()(42), 42); + test.ok(p.sortValues()(1, 2) > 0); + test.ok(p.sortValues()(2, 1) < 0); + test.equal(p.sortValues()(1, 1), 0); + test.equal(p.sort(), null); + test.equal(p.startAngle()(), 0); + test.equal(p.endAngle()(), 2 * Math.PI); + test.equal(p.padAngle()(), 0); + test.end(); +}); + +tape("pie(data) returns arcs in input order", function(test) { + var p = shape.pie(); + test.deepEqual(p([1, 3, 2]), [ + {data: 1, value: 1, index: 2, startAngle: 5.235987755982988, endAngle: 6.283185307179585, padAngle: 0}, + {data: 3, value: 3, index: 0, startAngle: 0.000000000000000, endAngle: 3.141592653589793, padAngle: 0}, + {data: 2, value: 2, index: 1, startAngle: 3.141592653589793, endAngle: 5.235987755982988, padAngle: 0} + ]); + test.end(); +}); + +tape("pie(data) coerces the specified value to a number", function(test) { + var p = shape.pie(), three = {valueOf: function() { return 3; }}; + test.deepEqual(p(["1", three, "2"]), [ + {data: "1", value: 1, index: 2, startAngle: 5.235987755982988, endAngle: 6.283185307179585, padAngle: 0}, + {data: three, value: 3, index: 0, startAngle: 0.000000000000000, endAngle: 3.141592653589793, padAngle: 0}, + {data: "2", value: 2, index: 1, startAngle: 3.141592653589793, endAngle: 5.235987755982988, padAngle: 0} + ]); + test.end(); +}); + +tape("pie(data) treats negative values as zero", function(test) { + var p = shape.pie(); + test.deepEqual(p([1, 0, -1]), [ + {data: 1, value: 1, index: 0, startAngle: 0.000000000000000, endAngle: 6.283185307179586, padAngle: 0}, + {data: 0, value: 0, index: 1, startAngle: 6.283185307179586, endAngle: 6.283185307179586, padAngle: 0}, + {data: -1, value: -1, index: 2, startAngle: 6.283185307179586, endAngle: 6.283185307179586, padAngle: 0} + ]); + test.end(); +}); + +tape("pie(data) treats NaN values as zero", function(test) { + var p = shape.pie(), + actual = p([1, NaN, undefined]), + expected = [ + {data: 1, value: 1, index: 0, startAngle: 0.000000000000000, endAngle: 6.283185307179586, padAngle: 0}, + {data: NaN, value: NaN, index: 1, startAngle: 6.283185307179586, endAngle: 6.283185307179586, padAngle: 0}, + {data: undefined, value: NaN, index: 2, startAngle: 6.283185307179586, endAngle: 6.283185307179586, padAngle: 0} + ]; + test.ok(isNaN(actual[1].data)); + test.ok(isNaN(actual[1].value)); + test.ok(isNaN(actual[2].value)); + actual[1].data = actual[1].value = actual[2].value = + expected[1].data = expected[1].value = expected[2].value = {}; // deepEqual NaN + test.deepEqual(actual, expected); + test.end(); +}); + +tape("pie(data) puts everything at the startAngle when the sum is zero", function(test) { + var p = shape.pie(); + test.deepEqual(p([0, 0]), [ + {data: 0, value: 0, index: 0, startAngle: 0, endAngle: 0, padAngle: 0}, + {data: 0, value: 0, index: 1, startAngle: 0, endAngle: 0, padAngle: 0} + ]); + test.deepEqual(p.startAngle(1)([0, 0]), [ + {data: 0, value: 0, index: 0, startAngle: 1, endAngle: 1, padAngle: 0}, + {data: 0, value: 0, index: 1, startAngle: 1, endAngle: 1, padAngle: 0} + ]); + test.end(); +}); + +tape("pie(data) restricts |endAngle - startAngle| to τ", function(test) { + var p = shape.pie(); + test.deepEqual(p.startAngle(0).endAngle(7)([1, 2]), [ + {data: 1, value: 1, index: 1, startAngle: 4.1887902047863905, endAngle: 6.2831853071795860, padAngle: 0}, + {data: 2, value: 2, index: 0, startAngle: 0.0000000000000000, endAngle: 4.1887902047863905, padAngle: 0} + ]); + test.deepEqual(p.startAngle(7).endAngle(0)([1, 2]), [ + {data: 1, value: 1, index: 1, startAngle: 2.8112097952136095, endAngle: 0.7168146928204142, padAngle: 0}, + {data: 2, value: 2, index: 0, startAngle: 7.0000000000000000, endAngle: 2.8112097952136095, padAngle: 0} + ]); + test.deepEqual(p.startAngle(1).endAngle(8)([1, 2]), [ + {data: 1, value: 1, index: 1, startAngle: 5.1887902047863905, endAngle: 7.2831853071795860, padAngle: 0}, + {data: 2, value: 2, index: 0, startAngle: 1.0000000000000000, endAngle: 5.1887902047863905, padAngle: 0} + ]); + test.deepEqual(p.startAngle(8).endAngle(1)([1, 2]), [ + {data: 1, value: 1, index: 1, startAngle: 3.8112097952136095, endAngle: 1.7168146928204142, padAngle: 0}, + {data: 2, value: 2, index: 0, startAngle: 8.0000000000000000, endAngle: 3.8112097952136095, padAngle: 0} + ]); + test.end(); +}); + +tape("pie.value(value)(data) observes the specified value function", function(test) { + test.deepEqual(shape.pie().value(function(d, i) { return i; })(new Array(3)), [ + {data: undefined, value: 0, index: 2, startAngle: 6.2831853071795860, endAngle: 6.2831853071795860, padAngle: 0}, + {data: undefined, value: 1, index: 1, startAngle: 4.1887902047863905, endAngle: 6.2831853071795860, padAngle: 0}, + {data: undefined, value: 2, index: 0, startAngle: 0.0000000000000000, endAngle: 4.1887902047863905, padAngle: 0} + ]); + test.end(); +}); + +tape("pie.value(f)(data) passes d, i and data to the specified function f", function(test) { + var data = ["a", "b"], actual = []; + shape.pie().value(function() { actual.push([].slice.call(arguments)); })(data); + test.deepEqual(actual, [["a", 0, data], ["b", 1, data]]); + test.end(); +}); + +tape("pie().startAngle(f)(…) propagates the context and arguments to the specified function f", function(test) { + var expected = {that: {}, args: [42]}, actual; + shape.pie().startAngle(function() { actual = {that: this, args: [].slice.call(arguments)}; }).apply(expected.that, expected.args); + test.deepEqual(actual, expected); + test.end(); +}); + +tape("pie().startAngle(θ)(data) observes the specified start angle", function(test) { + test.deepEqual(shape.pie().startAngle(Math.PI)([1, 2, 3]), [ + {data: 1, value: 1, index: 2, startAngle: 5.759586531581287, endAngle: 6.283185307179586, padAngle: 0}, + {data: 2, value: 2, index: 1, startAngle: 4.712388980384690, endAngle: 5.759586531581287, padAngle: 0}, + {data: 3, value: 3, index: 0, startAngle: 3.141592653589793, endAngle: 4.712388980384690, padAngle: 0} + ]); + test.end(); +}); + +tape("pie().endAngle(θ)(data) observes the specified end angle", function(test) { + test.deepEqual(shape.pie().endAngle(Math.PI)([1, 2, 3]), [ + {data: 1, value: 1, index: 2, startAngle: 2.6179938779914940, endAngle: 3.1415926535897927, padAngle: 0}, + {data: 2, value: 2, index: 1, startAngle: 1.5707963267948966, endAngle: 2.6179938779914940, padAngle: 0}, + {data: 3, value: 3, index: 0, startAngle: 0.0000000000000000, endAngle: 1.5707963267948966, padAngle: 0} + ]); + test.end(); +}); + +tape("pie().padAngle(δ)(data) observes the specified pad angle", function(test) { + test.deepEqual(shape.pie().padAngle(0.1)([1, 2, 3]), [ + {data: 1, value: 1, index: 2, startAngle: 5.1859877559829880, endAngle: 6.2831853071795850, padAngle: 0.1}, + {data: 2, value: 2, index: 1, startAngle: 3.0915926535897933, endAngle: 5.1859877559829880, padAngle: 0.1}, + {data: 3, value: 3, index: 0, startAngle: 0.0000000000000000, endAngle: 3.0915926535897933, padAngle: 0.1} + ]); + test.end(); +}); + +tape("pie().endAngle(f)(…) propagates the context and arguments to the specified function f", function(test) { + var expected = {that: {}, args: [42]}, actual; + shape.pie().endAngle(function() { actual = {that: this, args: [].slice.call(arguments)}; }).apply(expected.that, expected.args); + test.deepEqual(actual, expected); + test.end(); +}); + +tape("pie().padAngle(f)(…) propagates the context and arguments to the specified function f", function(test) { + var expected = {that: {}, args: [42]}, actual; + shape.pie().padAngle(function() { actual = {that: this, args: [].slice.call(arguments)}; }).apply(expected.that, expected.args); + test.deepEqual(actual, expected); + test.end(); +}); + +tape("pie().startAngle(θ₀).endAngle(θ₁).padAngle(δ)(data) restricts the pad angle to |θ₁ - θ₀| / data.length", function(test) { + test.deepEqual(shape.pie().startAngle(0).endAngle(Math.PI).padAngle(Infinity)([1, 2, 3]), [ + {data: 1, value: 1, index: 2, startAngle: 2.0943951023931953, endAngle: 3.1415926535897930, padAngle: 1.0471975511965976}, + {data: 2, value: 2, index: 1, startAngle: 1.0471975511965976, endAngle: 2.0943951023931953, padAngle: 1.0471975511965976}, + {data: 3, value: 3, index: 0, startAngle: 0.0000000000000000, endAngle: 1.0471975511965976, padAngle: 1.0471975511965976} + ]); + test.deepEqual(shape.pie().startAngle(0).endAngle(-Math.PI).padAngle(Infinity)([1, 2, 3]), [ + {data: 1, value: 1, index: 2, startAngle: -2.0943951023931953, endAngle: -3.1415926535897930, padAngle: 1.0471975511965976}, + {data: 2, value: 2, index: 1, startAngle: -1.0471975511965976, endAngle: -2.0943951023931953, padAngle: 1.0471975511965976}, + {data: 3, value: 3, index: 0, startAngle: -0.0000000000000000, endAngle: -1.0471975511965976, padAngle: 1.0471975511965976} + ]); + test.end(); +}); + +tape("pie.sortValues(f) sorts arcs by value per the specified comparator function f", function(test) { + var p = shape.pie(); + test.deepEqual(p.sortValues(function(a, b) { return a - b; })([1, 3, 2]), [ + {data: 1, value: 1, index: 0, startAngle: 0.0000000000000000, endAngle: 1.0471975511965976, padAngle: 0}, + {data: 3, value: 3, index: 2, startAngle: 3.1415926535897930, endAngle: 6.2831853071795860, padAngle: 0}, + {data: 2, value: 2, index: 1, startAngle: 1.0471975511965976, endAngle: 3.1415926535897930, padAngle: 0} + ]); + test.deepEqual(p.sortValues(function(a, b) { return b - a; })([1, 3, 2]), [ + {data: 1, value: 1, index: 2, startAngle: 5.2359877559829880, endAngle: 6.2831853071795850, padAngle: 0}, + {data: 3, value: 3, index: 0, startAngle: 0.0000000000000000, endAngle: 3.1415926535897930, padAngle: 0}, + {data: 2, value: 2, index: 1, startAngle: 3.1415926535897930, endAngle: 5.2359877559829880, padAngle: 0} + ]); + test.equal(p.sort(), null); + test.end(); +}); + +tape("pie.sort(f) sorts arcs by data per the specified comparator function f", function(test) { + var a = {valueOf: function() { return 1; }, name: "a"}, + b = {valueOf: function() { return 2; }, name: "b"}, + c = {valueOf: function() { return 3; }, name: "c"}, + p = shape.pie(); + test.deepEqual(p.sort(function(a, b) { return a.name.localeCompare(b.name); })([a, c, b]), [ + {data: a, value: 1, index: 0, startAngle: 0.0000000000000000, endAngle: 1.0471975511965976, padAngle: 0}, + {data: c, value: 3, index: 2, startAngle: 3.1415926535897930, endAngle: 6.2831853071795860, padAngle: 0}, + {data: b, value: 2, index: 1, startAngle: 1.0471975511965976, endAngle: 3.1415926535897930, padAngle: 0} + ]); + test.deepEqual(p.sort(function(a, b) { return b.name.localeCompare(a.name); })([a, c, b]), [ + {data: a, value: 1, index: 2, startAngle: 5.2359877559829880, endAngle: 6.2831853071795850, padAngle: 0}, + {data: c, value: 3, index: 0, startAngle: 0.0000000000000000, endAngle: 3.1415926535897930, padAngle: 0}, + {data: b, value: 2, index: 1, startAngle: 3.1415926535897930, endAngle: 5.2359877559829880, padAngle: 0} + ]); + test.equal(p.sortValues(), null); + test.end(); +}); diff --git a/test/polygonContext.js b/test/polygonContext.js new file mode 100644 index 0000000..d3b6a83 --- /dev/null +++ b/test/polygonContext.js @@ -0,0 +1,12 @@ +var polygon = require("d3-polygon"); + +module.exports = function() { + return { + points: null, + area: function() { return Math.abs(polygon.polygonArea(this.points)); }, + moveTo: function(x, y) { this.points = [[x, y]]; }, + lineTo: function(x, y) { this.points.push([x, y]); }, + rect: function(x, y, w, h) { this.points = [[x, y], [x + w, y], [x + w, y + h], [x, y + h]]; }, + closePath: function() {} + }; +}; diff --git a/test/stack-test.js b/test/stack-test.js new file mode 100644 index 0000000..e3ce9b0 --- /dev/null +++ b/test/stack-test.js @@ -0,0 +1,139 @@ +var tape = require("tape"), + shape = require("../"); + +tape("stack() has the expected defaults", function(test) { + var s = shape.stack(); + test.deepEqual(s.keys()(), []); + test.equal(s.value()({foo: 42}, "foo"), 42); + test.equal(s.order(), shape.stackOrderNone); + test.equal(s.offset(), shape.stackOffsetNone); + test.end(); +}); + +tape("stack(data) computes the stacked series for the given data", function(test) { + var s = shape.stack().keys([0, 1, 2, 3]), + data = [[1, 3, 5, 1], [2, 4, 2, 3], [1, 2, 4, 2]]; + test.deepEqual(s(data), [ + series([[0, 1], [0, 2], [0, 1]], data, 0, 0), + series([[1, 4], [2, 6], [1, 3]], data, 1, 1), + series([[4, 9], [6, 8], [3, 7]], data, 2, 2), + series([[9, 10], [8, 11], [7, 9]], data, 3, 3) + ]); + test.end(); +}); + +tape("stack.keys(array) sets the array of constant keys", function(test) { + var s = shape.stack().keys(["0.0", "2.0", "4.0"]); + test.deepEqual(s.keys()(), ["0.0", "2.0", "4.0"]); + test.end(); +}); + +tape("stack.keys(function) sets the key accessor function", function(test) { + var s = shape.stack().keys(function() { return "abc".split(""); }); + test.deepEqual(s.keys()(), ["a", "b", "c"]); + test.end(); +}); + +tape("stack(data, arguments…) passes the key accessor any additional arguments", function(test) { + var A, + B, + k = function(data, a, b) { A = a, B = b; return Object.keys(data[0]); }, + s = shape.stack().keys(k), + data = [[1, 3, 5, 1], [2, 4, 2, 3], [1, 2, 4, 2]]; + test.deepEqual(s(data, "foo", "bar"), [ + series([[0, 1], [0, 2], [0, 1]], data, "0", 0), + series([[1, 4], [2, 6], [1, 3]], data, "1", 1), + series([[4, 9], [6, 8], [3, 7]], data, "2", 2), + series([[9, 10], [8, 11], [7, 9]], data, "3", 3) + ]); + test.equal(A, "foo"); + test.equal(B, "bar"); + test.end(); +}); + +tape("stack.value(number) sets the constant value", function(test) { + var s = shape.stack().value("42.0"); + test.equal(s.value()(), 42); + test.end(); +}); + +tape("stack.value(function) sets the value accessor function", function(test) { + var v = function() { return 42; }, + s = shape.stack().value(v); + test.equal(s.value(), v); + test.end(); +}); + +tape("stack(data) passes the value accessor datum, key, index and data", function(test) { + var actual, + v = function(d, k, i, data) { actual = {datum: d, key: k, index: i, data: data}; return 2; }, + s = shape.stack().keys(["foo"]).value(v), + data = [{foo: 1}]; + test.deepEqual(s(data), [series([[0, 2]], data, "foo", 0)]); + test.deepEqual(actual, {datum: data[0], key: "foo", index: 0, data: data}); + test.end(); +}); + +tape("stack(data) coerces the return value of the value accessor to a number", function(test) { + var actual, + v = function() { return "2.0"; }, + s = shape.stack().keys(["foo"]).value(v), + data = [{foo: 1}]; + test.deepEqual(s(data), [series([[0, 2]], data, "foo", 0)]); + test.end(); +}); + +tape("stack.order(null) is equivalent to stack.order(stackOrderNone)", function(test) { + var s = shape.stack().order(null); + test.equal(s.order(), shape.stackOrderNone); + test.equal(typeof s.order(), "function"); + test.end(); +}); + +tape("stack.order(function) sets the order function", function(test) { + var s = shape.stack().keys([0, 1, 2, 3]).order(shape.stackOrderReverse), + data = [[1, 3, 5, 1], [2, 4, 2, 3], [1, 2, 4, 2]]; + test.equal(s.order(), shape.stackOrderReverse); + test.deepEqual(s(data), [ + series([[9, 10], [9, 11], [8, 9]], data, 0, 3), + series([[6, 9], [5, 9], [6, 8]], data, 1, 2), + series([[1, 6], [3, 5], [2, 6]], data, 2, 1), + series([[0, 1], [0, 3], [0, 2]], data, 3, 0) + ]); + test.end(); +}); + +tape("stack.offset(null) is equivalent to stack.offset(stackOffsetNone)", function(test) { + var s = shape.stack().offset(null); + test.equal(s.offset(), shape.stackOffsetNone); + test.equal(typeof s.offset(), "function"); + test.end(); +}); + +tape("stack.offset(function) sets the offset function", function(test) { + var s = shape.stack().keys([0, 1, 2, 3]).offset(shape.stackOffsetExpand), + data = [[1, 3, 5, 1], [2, 4, 2, 3], [1, 2, 4, 2]]; + test.equal(s.offset(), shape.stackOffsetExpand); + test.deepEqual(s(data).map(roundSeries), [ + [[0 / 10, 1 / 10], [0 / 11, 2 / 11], [0 / 9, 1 / 9]], + [[1 / 10, 4 / 10], [2 / 11, 6 / 11], [1 / 9, 3 / 9]], + [[4 / 10, 9 / 10], [6 / 11, 8 / 11], [3 / 9, 7 / 9]], + [[9 / 10, 10 / 10], [8 / 11, 11 / 11], [7 / 9, 9 / 9]] + ].map(roundSeries)); + test.end(); +}); + +function series(series, data, key, index) { + data.forEach(function(d, i) { series[i].data = d; }); + series.key = key; + series.index = index; + return series; +} + +function roundSeries(series) { + return series.map(function(point) { + return point.map(function(value) { + return Math.round(value * 1e6) / 1e6; + }); + }); +} diff --git a/test/symbol-test.js b/test/symbol-test.js new file mode 100644 index 0000000..38dd91f --- /dev/null +++ b/test/symbol-test.js @@ -0,0 +1,142 @@ +var tape = require("tape"), + shape = require("../"), + polygonContext = require("./polygonContext"); + +require("./inDelta"); +require("./pathEqual"); + +tape("symbol() returns a default symbol shape", function(test) { + var s = shape.symbol(); + test.equal(s.type()(), shape.symbolCircle); + test.equal(s.size()(), 64); + test.equal(s.context(), null); + test.pathEqual(s(), "M4.513517,0A4.513517,4.513517,0,1,1,-4.513517,0A4.513517,4.513517,0,1,1,4.513517,0"); + test.end(); +}); + +tape("symbol().size(f)(…) propagates the context and arguments to the specified function", function(test) { + var expected = {that: {}, args: [42]}, actual; + shape.symbol().size(function() { actual = {that: this, args: [].slice.call(arguments)}; return 64; }).apply(expected.that, expected.args); + test.deepEqual(actual, expected); + test.end(); +}); + +tape("symbol().type(f)(…) propagates the context and arguments to the specified function", function(test) { + var expected = {that: {}, args: [42]}, actual; + shape.symbol().type(function() { actual = {that: this, args: [].slice.call(arguments)}; return shape.symbolCircle; }).apply(expected.that, expected.args); + test.deepEqual(actual, expected); + test.end(); +}); + +tape("symbol.size(size) observes the specified size function", function(test) { + var size = function(d, i) { return d.z * 2 + i; }, + s = shape.symbol().size(size); + test.equal(s.size(), size); + test.pathEqual(s({z: 0}, 0), "M0,0"); + test.pathEqual(s({z: Math.PI / 2}, 0), "M1,0A1,1,0,1,1,-1,0A1,1,0,1,1,1,0"); + test.pathEqual(s({z: 2 * Math.PI}, 0), "M2,0A2,2,0,1,1,-2,0A2,2,0,1,1,2,0"); + test.pathEqual(s({z: Math.PI}, 1), "M1.522600,0A1.522600,1.522600,0,1,1,-1.522600,0A1.522600,1.522600,0,1,1,1.522600,0"); + test.pathEqual(s({z: 4 * Math.PI}, 2), "M2.938813,0A2.938813,2.938813,0,1,1,-2.938813,0A2.938813,2.938813,0,1,1,2.938813,0"); + test.end(); +}); + +tape("symbol.size(size) observes the specified size constant", function(test) { + var s = shape.symbol(); + test.equal(s.size(42).size()(), 42); + test.pathEqual(s.size(0)(), "M0,0"); + test.pathEqual(s.size(Math.PI)(), "M1,0A1,1,0,1,1,-1,0A1,1,0,1,1,1,0"); + test.pathEqual(s.size(4 * Math.PI)(), "M2,0A2,2,0,1,1,-2,0A2,2,0,1,1,2,0"); + test.end(); +}); + +tape("symbol.type(symbolCircle) generates the expected path", function(test) { + var s = shape.symbol().type(shape.symbolCircle).size(function(d) { return d; }); + test.pathEqual(s(0), "M0,0"); + test.pathEqual(s(20), "M2.523133,0A2.523133,2.523133,0,1,1,-2.523133,0A2.523133,2.523133,0,1,1,2.523133,0"); + test.end(); +}); + +tape("symbol.type(symbolCross) generates a polygon with the specified size", function(test) { + var p = polygonContext(), s = shape.symbol().type(shape.symbolCross).context(p); + s.size(1)(); test.inDelta(p.area(), 1); + s.size(240)(); test.inDelta(p.area(), 240); + test.end(); +}); + +tape("symbol.type(symbolCross) generates the expected path", function(test) { + var s = shape.symbol().type(shape.symbolCross).size(function(d) { return d; }); + test.pathEqual(s(0), "M0,0L0,0L0,0L0,0L0,0L0,0L0,0L0,0L0,0L0,0L0,0L0,0Z"); + test.pathEqual(s(20), "M-3,-1L-1,-1L-1,-3L1,-3L1,-1L3,-1L3,1L1,1L1,3L-1,3L-1,1L-3,1Z"); + test.end(); +}); + +tape("symbol.type(symbolDiamond) generates a polygon with the specified size", function(test) { + var p = polygonContext(), s = shape.symbol().type(shape.symbolDiamond).context(p); + s.size(1)(); test.inDelta(p.area(), 1); + s.size(240)(); test.inDelta(p.area(), 240); + test.end(); +}); + +tape("symbol.type(symbolDiamond) generates the expected path", function(test) { + var s = shape.symbol().type(shape.symbolDiamond).size(function(d) { return d; }); + test.pathEqual(s(0), "M0,0L0,0L0,0L0,0Z"); + test.pathEqual(s(10), "M0,-2.942831L1.699044,0L0,2.942831L-1.699044,0Z"); + test.end(); +}); + +tape("symbol.type(symbolStar) generates a polygon with the specified size", function(test) { + var p = polygonContext(), s = shape.symbol().type(shape.symbolStar).context(p); + s.size(1)(); test.inDelta(p.area(), 1); + s.size(240)(); test.inDelta(p.area(), 240); + test.end(); +}); + +tape("symbol.type(symbolStar) generates the expected path", function(test) { + var s = shape.symbol().type(shape.symbolStar).size(function(d) { return d; }); + test.pathEqual(s(0), "M0,0L0,0L0,0L0,0L0,0L0,0L0,0L0,0L0,0L0,0Z"); + test.pathEqual(s(10), "M0,-2.984649L0.670095,-0.922307L2.838570,-0.922307L1.084237,0.352290L1.754333,2.414632L0,1.140035L-1.754333,2.414632L-1.084237,0.352290L-2.838570,-0.922307L-0.670095,-0.922307Z"); + test.end(); +}); + +tape("symbol.type(symbolSquare) generates a polygon with the specified size", function(test) { + var p = polygonContext(), s = shape.symbol().type(shape.symbolSquare).context(p); + s.size(1)(); test.inDelta(p.area(), 1); + s.size(240)(); test.inDelta(p.area(), 240); + test.end(); +}); + +tape("symbol.type(symbolSquare) generates the expected path", function(test) { + var s = shape.symbol().type(shape.symbolSquare).size(function(d) { return d; }); + test.pathEqual(s(0), "M0,0h0v0h0Z"); + test.pathEqual(s(4), "M-1,-1h2v2h-2Z"); + test.pathEqual(s(16), "M-2,-2h4v4h-4Z"); + test.end(); +}); + +tape("symbol.type(symbolTriangle) generates a polygon with the specified size", function(test) { + var p = polygonContext(), s = shape.symbol().type(shape.symbolTriangle).context(p); + s.size(1)(); test.inDelta(p.area(), 1); + s.size(240)(); test.inDelta(p.area(), 240); + test.end(); +}); + +tape("symbol.type(symbolTriangle) generates the expected path", function(test) { + var s = shape.symbol().type(shape.symbolTriangle).size(function(d) { return d; }); + test.pathEqual(s(0), "M0,0L0,0L0,0Z"); + test.pathEqual(s(10), "M0,-2.774528L2.402811,1.387264L-2.402811,1.387264Z"); + test.end(); +}); + +tape("symbol.type(symbolWye) generates a polygon with the specified size", function(test) { + var p = polygonContext(), s = shape.symbol().type(shape.symbolWye).context(p); + s.size(1)(); test.inDelta(p.area(), 1); + s.size(240)(); test.inDelta(p.area(), 240); + test.end(); +}); + +tape("symbol.type(symbolWye) generates the expected path", function(test) { + var s = shape.symbol().type(shape.symbolWye).size(function(d) { return d; }); + test.pathEqual(s(0), "M0,0L0,0L0,0L0,0L0,0L0,0L0,0L0,0L0,0Z"); + test.pathEqual(s(10), "M0.853360,0.492688L0.853360,2.199408L-0.853360,2.199408L-0.853360,0.492688L-2.331423,-0.360672L-1.478063,-1.838735L0,-0.985375L1.478063,-1.838735L2.331423,-0.360672Z"); + test.end(); +}); diff --git a/test/symbols-test.js b/test/symbols-test.js new file mode 100644 index 0000000..16a7790 --- /dev/null +++ b/test/symbols-test.js @@ -0,0 +1,15 @@ +var tape = require("tape"), + shape = require("../"); + +tape("symbols is the array of symbol types", function(test) { + test.deepEqual(shape.symbols, [ + shape.symbolCircle, + shape.symbolCross, + shape.symbolDiamond, + shape.symbolSquare, + shape.symbolStar, + shape.symbolTriangle, + shape.symbolWye + ]); + test.end(); +});