@jrkasprzyk/parcoord-es 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/dist/parcoords.css +61 -0
  4. package/dist/parcoords.esm.js +4132 -0
  5. package/dist/parcoords.esm.js.map +1 -0
  6. package/dist/parcoords.js +4130 -0
  7. package/dist/parcoords.js.map +1 -0
  8. package/dist/parcoords.standalone.js +11233 -0
  9. package/dist/parcoords.standalone.js.map +1 -0
  10. package/package.json +89 -0
  11. package/src/api/adjacentPairs.js +10 -0
  12. package/src/api/applyAxisConfig.js +35 -0
  13. package/src/api/applyDimensionDefaults.js +25 -0
  14. package/src/api/autoscale.js +133 -0
  15. package/src/api/axisDots.js +27 -0
  16. package/src/api/brushMode.js +50 -0
  17. package/src/api/brushReset.js +34 -0
  18. package/src/api/brushable.js +81 -0
  19. package/src/api/clear.js +21 -0
  20. package/src/api/commonScale.js +45 -0
  21. package/src/api/computeRealCentroids.js +8 -0
  22. package/src/api/createAxes.js +101 -0
  23. package/src/api/detectDimensionTypes.js +12 -0
  24. package/src/api/detectDimensions.js +7 -0
  25. package/src/api/flip.js +11 -0
  26. package/src/api/getOrderedDimensionKeys.js +8 -0
  27. package/src/api/highlight.js +26 -0
  28. package/src/api/init.js +53 -0
  29. package/src/api/interactive.js +7 -0
  30. package/src/api/intersection.js +16 -0
  31. package/src/api/mark.js +20 -0
  32. package/src/api/mergeParcoords.js +86 -0
  33. package/src/api/removeAxes.js +9 -0
  34. package/src/api/render.js +26 -0
  35. package/src/api/renderBrushed.js +39 -0
  36. package/src/api/renderDefault.js +27 -0
  37. package/src/api/renderMarked.js +34 -0
  38. package/src/api/reorder.js +37 -0
  39. package/src/api/reorderable.js +41 -0
  40. package/src/api/resize.js +36 -0
  41. package/src/api/scale.js +10 -0
  42. package/src/api/selected.js +209 -0
  43. package/src/api/shadows.js +9 -0
  44. package/src/api/sortDimensions.js +13 -0
  45. package/src/api/sortDimensionsByRowData.js +20 -0
  46. package/src/api/toString.js +11 -0
  47. package/src/api/toType.js +9 -0
  48. package/src/api/toTypeCoerceNumbers.js +7 -0
  49. package/src/api/unhighlight.js +12 -0
  50. package/src/api/unmark.js +12 -0
  51. package/src/api/updateAxes.js +103 -0
  52. package/src/bindEvents.js +60 -0
  53. package/src/brush/1d/brushExtents.js +73 -0
  54. package/src/brush/1d/brushFor.js +92 -0
  55. package/src/brush/1d/brushReset.js +36 -0
  56. package/src/brush/1d/index.js +20 -0
  57. package/src/brush/1d/install.js +41 -0
  58. package/src/brush/1d/selected.js +79 -0
  59. package/src/brush/1d/uninstall.js +12 -0
  60. package/src/brush/1d-multi/brushExtents.js +101 -0
  61. package/src/brush/1d-multi/brushFor.js +13 -0
  62. package/src/brush/1d-multi/brushReset.js +53 -0
  63. package/src/brush/1d-multi/drawBrushes.js +36 -0
  64. package/src/brush/1d-multi/index.js +20 -0
  65. package/src/brush/1d-multi/install.js +29 -0
  66. package/src/brush/1d-multi/newBrush.js +102 -0
  67. package/src/brush/1d-multi/selected.js +160 -0
  68. package/src/brush/1d-multi/uninstall.js +12 -0
  69. package/src/brush/angular/brushReset.js +14 -0
  70. package/src/brush/angular/index.js +19 -0
  71. package/src/brush/angular/install.js +141 -0
  72. package/src/brush/angular/onDrag.js +109 -0
  73. package/src/brush/angular/onDragEnd.js +30 -0
  74. package/src/brush/angular/onDragStart.js +38 -0
  75. package/src/brush/angular/removeStrum.js +12 -0
  76. package/src/brush/angular/selected.js +63 -0
  77. package/src/brush/angular/uninstall.js +17 -0
  78. package/src/brush/angular/util/hypothenuse.js +3 -0
  79. package/src/brush/consecutive.js +11 -0
  80. package/src/brush/dimensionsForPoint.js +32 -0
  81. package/src/brush/invertByScale.js +24 -0
  82. package/src/brush/strums/brushReset.js +14 -0
  83. package/src/brush/strums/index.js +19 -0
  84. package/src/brush/strums/install.js +81 -0
  85. package/src/brush/strums/onDrag.js +83 -0
  86. package/src/brush/strums/onDragEnd.js +20 -0
  87. package/src/brush/strums/onDragStart.js +33 -0
  88. package/src/brush/strums/removeStrum.js +10 -0
  89. package/src/brush/strums/selected.js +52 -0
  90. package/src/brush/strums/uninstall.js +16 -0
  91. package/src/index.js +226 -0
  92. package/src/parallel-coordinates.css +61 -0
  93. package/src/state/defaultConfig.js +36 -0
  94. package/src/state/index.js +86 -0
  95. package/src/state/sideEffects.js +89 -0
  96. package/src/util/colorPath.js +71 -0
  97. package/src/util/computeCentroids.js +44 -0
  98. package/src/util/computeClusterCentroids.js +33 -0
  99. package/src/util/computeControlPoints.js +38 -0
  100. package/src/util/dimensionLabels.js +11 -0
  101. package/src/util/flipAxisAndUpdatePCP.js +19 -0
  102. package/src/util/functor.js +3 -0
  103. package/src/util/getRange.js +24 -0
  104. package/src/util/getset.js +23 -0
  105. package/src/util/height.js +3 -0
  106. package/src/util/isBrushed.js +15 -0
  107. package/src/util/renderQueue.js +67 -0
  108. package/src/util/rotateLabels.js +18 -0
  109. package/src/util/width.js +3 -0
@@ -0,0 +1,7 @@
1
+ const detectDimensions = pc =>
2
+ function() {
3
+ pc.dimensions(pc.applyDimensionDefaults());
4
+ return this;
5
+ };
6
+
7
+ export default detectDimensions;
@@ -0,0 +1,11 @@
1
+ const flip = config =>
2
+ function(d) {
3
+ //__.dimensions[d].yscale.domain().reverse(); // does not work
4
+ config.dimensions[d].yscale.domain(
5
+ config.dimensions[d].yscale.domain().reverse()
6
+ ); // works
7
+
8
+ return this;
9
+ };
10
+
11
+ export default flip;
@@ -0,0 +1,8 @@
1
+ import { ascending } from 'd3-array';
2
+
3
+ const getOrderedDimensionKeys = config => () =>
4
+ Object.keys(config.dimensions).sort((x, y) =>
5
+ ascending(config.dimensions[x].index, config.dimensions[y].index)
6
+ );
7
+
8
+ export default getOrderedDimensionKeys;
@@ -0,0 +1,26 @@
1
+ import { selectAll } from 'd3-selection';
2
+
3
+ import colorPath from '../util/colorPath';
4
+ import functor from '../util/functor';
5
+
6
+ const pathHighlight = (config, ctx, position) => (d, i) => {
7
+ ctx.highlight.strokeStyle = functor(config.color)(d, i);
8
+ return colorPath(config, position, d, ctx.highlight);
9
+ };
10
+
11
+ // highlight an array of data
12
+ const highlight = (config, pc, canvas, events, ctx, position) =>
13
+ function(data = null) {
14
+ if (data === null) {
15
+ return config.highlighted;
16
+ }
17
+
18
+ config.highlighted = data;
19
+ pc.clear('highlight');
20
+ selectAll([canvas.foreground, canvas.brushed]).classed('faded', true);
21
+ data.forEach(pathHighlight(config, ctx, position));
22
+ events.call('highlight', this, data);
23
+ return this;
24
+ };
25
+
26
+ export default highlight;
@@ -0,0 +1,53 @@
1
+ import { select } from 'd3-selection';
2
+
3
+ /**
4
+ * Setup a new parallel coordinates chart.
5
+ *
6
+ * @param config
7
+ * @param canvas
8
+ * @param ctx
9
+ * @returns {pc} a parcoords closure
10
+ */
11
+ const init = (config, canvas, ctx) => {
12
+ /**
13
+ * Create the chart within a container. The selector can also be a d3 selection.
14
+ *
15
+ * @param selection a d3 selection
16
+ * @returns {pc} instance for chained api
17
+ */
18
+ const pc = function(selection) {
19
+ selection = pc.selection = select(selection);
20
+
21
+ config.width = selection.node().clientWidth;
22
+ config.height = selection.node().clientHeight;
23
+ // canvas data layers
24
+ ['dots', 'foreground', 'brushed', 'marked', 'highlight'].forEach(layer => {
25
+ canvas[layer] = selection
26
+ .append('canvas')
27
+ .attr('class', layer)
28
+ .node();
29
+ ctx[layer] = canvas[layer].getContext('2d');
30
+ });
31
+
32
+ // svg tick and brush layers
33
+ pc.svg = selection
34
+ .append('svg')
35
+ .attr('width', config.width)
36
+ .attr('height', config.height)
37
+ .style('font', '14px sans-serif')
38
+ .style('position', 'absolute')
39
+
40
+ .append('svg:g')
41
+ .attr(
42
+ 'transform',
43
+ 'translate(' + config.margin.left + ',' + config.margin.top + ')'
44
+ );
45
+ // for chained api
46
+ return pc;
47
+ };
48
+
49
+ // for partial-application style programming
50
+ return pc;
51
+ };
52
+
53
+ export default init;
@@ -0,0 +1,7 @@
1
+ const interactive = flags =>
2
+ function() {
3
+ flags.interactive = true;
4
+ return this;
5
+ };
6
+
7
+ export default interactive;
@@ -0,0 +1,16 @@
1
+ // calculate 2d intersection of line a->b with line c->d
2
+ // points are objects with x and y properties
3
+ const intersection = (a, b, c, d) => {
4
+ return {
5
+ x:
6
+ ((a.x * b.y - a.y * b.x) * (c.x - d.x) -
7
+ (a.x - b.x) * (c.x * d.y - c.y * d.x)) /
8
+ ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x)),
9
+ y:
10
+ ((a.x * b.y - a.y * b.x) * (c.y - d.y) -
11
+ (a.y - b.y) * (c.x * d.y - c.y * d.x)) /
12
+ ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x)),
13
+ };
14
+ };
15
+
16
+ export default intersection;
@@ -0,0 +1,20 @@
1
+ import { selectAll } from 'd3-selection';
2
+
3
+ import { pathMark } from './renderMarked';
4
+
5
+ // mark an array of data
6
+ const mark = (config, pc, canvas, events, ctx, position) =>
7
+ function(data = null) {
8
+ if (data === null) {
9
+ return config.marked;
10
+ }
11
+
12
+ // add array to already marked data
13
+ config.marked = config.marked.concat(data);
14
+ selectAll([canvas.foreground, canvas.brushed]).classed('dimmed', true);
15
+ data.forEach(pathMark(config, ctx, position));
16
+ events.call('mark', this, data);
17
+ return this;
18
+ };
19
+
20
+ export default mark;
@@ -0,0 +1,86 @@
1
+ import { select, selectAll } from 'd3-selection';
2
+
3
+ // Merges the canvases and SVG elements into one canvas element which is then passed into the callback
4
+ // (so you can choose to save it to disk, etc.)
5
+ const mergeParcoords = pc => callback => {
6
+ // Retina display, etc.
7
+ const devicePixelRatio = window.devicePixelRatio || 1;
8
+
9
+ // Create a canvas element to store the merged canvases
10
+ const mergedCanvas = document.createElement('canvas');
11
+
12
+ const foregroundCanvas = pc.canvas.foreground;
13
+ // We will need to adjust for canvas margins to align the svg and canvas
14
+ const canvasMarginLeft = Number(
15
+ foregroundCanvas.style.marginLeft.replace('px', '')
16
+ );
17
+
18
+ const textTopAdjust = 15;
19
+ const canvasMarginTop =
20
+ Number(foregroundCanvas.style.marginTop.replace('px', '')) + textTopAdjust;
21
+ const width =
22
+ (foregroundCanvas.clientWidth + canvasMarginLeft) * devicePixelRatio;
23
+ const height =
24
+ (foregroundCanvas.clientHeight + canvasMarginTop) * devicePixelRatio;
25
+ mergedCanvas.width = width + 50; // pad so that svg labels at right will not get cut off
26
+ mergedCanvas.height = height + 30; // pad so that svg labels at bottom will not get cut off
27
+ mergedCanvas.style.width = mergedCanvas.width / devicePixelRatio + 'px';
28
+ mergedCanvas.style.height = mergedCanvas.height / devicePixelRatio + 'px';
29
+
30
+ // Give the canvas a white background
31
+ const context = mergedCanvas.getContext('2d');
32
+ context.fillStyle = '#ffffff';
33
+ context.fillRect(0, 0, mergedCanvas.width, mergedCanvas.height);
34
+
35
+ // Merge all the canvases
36
+ for (const key in pc.canvas) {
37
+ context.drawImage(
38
+ pc.canvas[key],
39
+ canvasMarginLeft * devicePixelRatio,
40
+ canvasMarginTop * devicePixelRatio,
41
+ width - canvasMarginLeft * devicePixelRatio,
42
+ height - canvasMarginTop * devicePixelRatio
43
+ );
44
+ }
45
+
46
+ // Add SVG elements to canvas
47
+ const DOMURL = window.URL || window.webkitURL || window;
48
+ const serializer = new XMLSerializer();
49
+ // axis labels are translated (0,-5) so we will clone the svg
50
+ // and translate down so the labels are drawn on the canvas
51
+ const svgNodeCopy = pc.selection
52
+ .select('svg')
53
+ .node()
54
+ .cloneNode(true);
55
+ svgNodeCopy.setAttribute('transform', 'translate(0,' + textTopAdjust + ')');
56
+ svgNodeCopy.setAttribute(
57
+ 'height',
58
+ svgNodeCopy.getAttribute('height') + textTopAdjust
59
+ );
60
+ // text will need fill attribute since css styles will not get picked up
61
+ // this is not sophisticated since it doesn't look up css styles
62
+ // if the user changes
63
+ select(svgNodeCopy)
64
+ .selectAll('text')
65
+ .attr('fill', 'black');
66
+ const svgStr = serializer.serializeToString(svgNodeCopy);
67
+
68
+ // Create a Data URI.
69
+ const src = 'data:image/svg+xml;base64,' + window.btoa(svgStr);
70
+ const img = new Image();
71
+ img.onload = () => {
72
+ context.drawImage(
73
+ img,
74
+ 0,
75
+ 0,
76
+ img.width * devicePixelRatio,
77
+ img.height * devicePixelRatio
78
+ );
79
+ if (typeof callback === 'function') {
80
+ callback(mergedCanvas);
81
+ }
82
+ };
83
+ img.src = src;
84
+ };
85
+
86
+ export default mergeParcoords;
@@ -0,0 +1,9 @@
1
+ const removeAxes = pc =>
2
+ function() {
3
+ pc._g.remove();
4
+
5
+ delete pc._g;
6
+ return this;
7
+ };
8
+
9
+ export default removeAxes;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Renders the polylines.
3
+ * If no dimensions have been specified, it will attempt to detect quantitative
4
+ * dimensions based on the first data entry. If scales haven't been set, it will
5
+ * autoscale based on the extent for each dimension.
6
+ *
7
+ * @param config
8
+ * @param pc
9
+ * @param events
10
+ * @returns {Function}
11
+ */
12
+ const render = (config, pc, events) =>
13
+ function() {
14
+ // try to autodetect dimensions and create scales
15
+ if (!Object.keys(config.dimensions).length) {
16
+ pc.detectDimensions();
17
+ }
18
+ pc.autoscale();
19
+
20
+ pc.render[config.mode]();
21
+
22
+ events.call('render', this);
23
+ return this;
24
+ };
25
+
26
+ export default render;
@@ -0,0 +1,39 @@
1
+ import isBrushed from '../util/isBrushed';
2
+ import colorPath from '../util/colorPath';
3
+ import functor from '../util/functor';
4
+
5
+ const pathBrushed = (config, ctx, position) => (d, i) => {
6
+ if (config.brushedColor !== null) {
7
+ ctx.brushed.strokeStyle = functor(config.brushedColor)(d, i);
8
+ } else {
9
+ ctx.brushed.strokeStyle = functor(config.color)(d, i);
10
+ }
11
+ return colorPath(config, position, d, ctx.brushed);
12
+ };
13
+
14
+ const renderBrushedDefault = (config, ctx, position, pc, brushGroup) => () => {
15
+ pc.clear('brushed');
16
+
17
+ if (isBrushed(config, brushGroup) && config.brushed !== false) {
18
+ config.brushed.forEach(pathBrushed(config, ctx, position));
19
+ }
20
+ };
21
+
22
+ const renderBrushedQueue = (config, brushGroup, brushedQueue) => () => {
23
+ if (isBrushed(config, brushGroup)) {
24
+ brushedQueue(config.brushed);
25
+ } else {
26
+ brushedQueue([]); // This is needed to clear the currently brushed items
27
+ }
28
+ };
29
+
30
+ const renderBrushed = (config, pc, events) =>
31
+ function() {
32
+ if (!Object.keys(config.dimensions).length) pc.detectDimensions();
33
+
34
+ pc.renderBrushed[config.mode]();
35
+ events.call('render', this);
36
+ return this;
37
+ };
38
+
39
+ export { pathBrushed, renderBrushed, renderBrushedDefault, renderBrushedQueue };
@@ -0,0 +1,27 @@
1
+ import colorPath from '../util/colorPath';
2
+ import functor from '../util/functor';
3
+
4
+ const pathForeground = (config, ctx, position) => (d, i) => {
5
+ ctx.foreground.strokeStyle = functor(config.color)(d, i);
6
+ return colorPath(config, position, d, ctx.foreground);
7
+ };
8
+
9
+ const renderDefault = (config, pc, ctx, position) => () => {
10
+ pc.clear('foreground');
11
+ pc.clear('highlight');
12
+
13
+ pc.renderBrushed.default();
14
+ pc.renderMarked.default();
15
+
16
+ config.data.forEach(pathForeground(config, ctx, position));
17
+ };
18
+
19
+ const renderDefaultQueue = (config, pc, foregroundQueue) => () => {
20
+ pc.renderBrushed.queue();
21
+ pc.renderMarked.queue();
22
+ foregroundQueue(config.data);
23
+ };
24
+
25
+ export default renderDefault;
26
+
27
+ export { pathForeground, renderDefaultQueue };
@@ -0,0 +1,34 @@
1
+ import colorPath from '../util/colorPath';
2
+ import functor from '../util/functor';
3
+
4
+ const pathMark = (config, ctx, position) => (d, i) => {
5
+ ctx.marked.strokeStyle = functor(config.color)(d, i);
6
+ return colorPath(config, position, d, ctx.marked);
7
+ };
8
+
9
+ const renderMarkedDefault = (config, pc, ctx, position) => () => {
10
+ pc.clear('marked');
11
+
12
+ if (config.marked.length) {
13
+ config.marked.forEach(pathMark(config, ctx, position));
14
+ }
15
+ };
16
+
17
+ const renderMarkedQueue = (config, markedQueue) => () => {
18
+ if (config.marked) {
19
+ markedQueue(config.marked);
20
+ } else {
21
+ markedQueue([]); // This is needed to clear the currently marked items
22
+ }
23
+ };
24
+
25
+ const renderMarked = (config, pc, events) =>
26
+ function() {
27
+ if (!Object.keys(config.dimensions).length) pc.detectDimensions();
28
+
29
+ pc.renderMarked[config.mode]();
30
+ events.call('render', this);
31
+ return this;
32
+ };
33
+
34
+ export { pathMark, renderMarked, renderMarkedDefault, renderMarkedQueue };
@@ -0,0 +1,37 @@
1
+ // Reorder dimensions, such that the highest value (visually) is on the left and
2
+ // the lowest on the right. Visual values are determined by the data values in
3
+ // the given row.
4
+ const reorder = (config, pc, xscale) => rowdata => {
5
+ const firstDim = pc.getOrderedDimensionKeys()[0];
6
+
7
+ pc.sortDimensionsByRowData(rowdata);
8
+ // NOTE: this is relatively cheap given that:
9
+ // number of dimensions < number of data items
10
+ // Thus we check equality of order to prevent rerendering when this is the case.
11
+ const reordered = firstDim !== pc.getOrderedDimensionKeys()[0];
12
+
13
+ if (reordered) {
14
+ xscale.domain(pc.getOrderedDimensionKeys());
15
+ const highlighted = config.highlighted.slice(0);
16
+ pc.unhighlight();
17
+
18
+ const marked = config.marked.slice(0);
19
+ pc.unmark();
20
+
21
+ const g = pc.g();
22
+ g.transition()
23
+ .duration(1500)
24
+ .attr('transform', d => 'translate(' + xscale(d) + ')');
25
+ pc.render();
26
+
27
+ // pc.highlight() does not check whether highlighted is length zero, so we do that here.
28
+ if (highlighted.length !== 0) {
29
+ pc.highlight(highlighted);
30
+ }
31
+ if (marked.length !== 0) {
32
+ pc.mark(marked);
33
+ }
34
+ }
35
+ };
36
+
37
+ export default reorder;
@@ -0,0 +1,41 @@
1
+ import { drag } from 'd3-drag';
2
+ import { select } from 'd3-selection';
3
+
4
+ import w from '../util/width';
5
+
6
+ // Jason Davies, http://bl.ocks.org/1341281
7
+ const reorderable = (config, pc, xscale, position, dragging, flags) =>
8
+ function() {
9
+ if (pc.g() === undefined) pc.createAxes();
10
+ const g = pc.g();
11
+
12
+ g.style('cursor', 'move').call(
13
+ drag()
14
+ .on('start', function(event, d) {
15
+ dragging[d] = this.__origin__ = xscale(d);
16
+ })
17
+ .on('drag', function(event, d) {
18
+ dragging[d] = Math.min(
19
+ w(config),
20
+ Math.max(0, (this.__origin__ += event.dx))
21
+ );
22
+ pc.sortDimensions();
23
+ xscale.domain(pc.getOrderedDimensionKeys());
24
+ pc.render();
25
+ g.attr('transform', d => 'translate(' + position(d) + ')');
26
+ })
27
+ .on('end', function(event, d) {
28
+ delete this.__origin__;
29
+ delete dragging[d];
30
+ select(this)
31
+ .transition()
32
+ .attr('transform', 'translate(' + xscale(d) + ')');
33
+ pc.render();
34
+ pc.renderMarked();
35
+ })
36
+ );
37
+ flags.reorderable = true;
38
+ return this;
39
+ };
40
+
41
+ export default reorderable;
@@ -0,0 +1,36 @@
1
+ // rescale for height, width and margins
2
+ // TODO currently assumes chart is brushable, and destroys old brushes
3
+ const resize = (config, pc, flags, events) => {
4
+ return function() {
5
+ // selection size
6
+ pc.selection
7
+ .select('svg')
8
+ .attr('width', config.width)
9
+ .attr('height', config.height);
10
+ pc.svg.attr(
11
+ 'transform',
12
+ 'translate(' + config.margin.left + ',' + config.margin.top + ')'
13
+ );
14
+
15
+ // FIXME: the current brush state should pass through
16
+ if (flags.brushable) pc.brushReset();
17
+
18
+ // scales
19
+ pc.autoscale();
20
+
21
+ // axes, destroys old brushes.
22
+ if (pc.g()) pc.createAxes();
23
+ if (flags.brushable) pc.brushable();
24
+ if (flags.reorderable) pc.reorderable();
25
+
26
+ events.call('resize', this, {
27
+ width: config.width,
28
+ height: config.height,
29
+ margin: config.margin,
30
+ });
31
+
32
+ return this;
33
+ };
34
+ };
35
+
36
+ export default resize;
@@ -0,0 +1,10 @@
1
+ const scale = (config, pc) =>
2
+ function(d, domain) {
3
+ config.dimensions[d].yscale.domain(domain);
4
+ pc.render.default();
5
+ pc.updateAxes();
6
+
7
+ return this;
8
+ };
9
+
10
+ export default scale;