From c0e1cee53a961b18488b45ef583a675a73cfdcb8 Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Tue, 12 Apr 2016 18:41:10 +0300 Subject: [PATCH 1/8] Make react and react-dom devDependencies as well as peerDependencies This helps development when using npm 3, which does not install peers automatically. --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index f01c185..4a25f88 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,9 @@ "enzyme": "^2.1.0", "jsdom": "^8.1.0", "mocha": "^2.4.5", + "react": "^0.14.7", "react-addons-test-utils": "^0.14.7", + "react-dom": "^0.14.7", "webpack": "^2.1.0-beta.4", "webpack-dev-server": "^2.0.0-beta" }, From de8870c38d215006bbd6dc92677c98ba2dde7a87 Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Tue, 12 Apr 2016 18:43:42 +0300 Subject: [PATCH 2/8] Add basic support for gapped data (DataProcessor, SparklinesLine) --- __tests__/DataProcessor.js | 39 ++++++++++++++++++ demo/demo.js | 7 ++++ demo/index.html | 11 ++++++ src/DataProcessor.js | 32 +++++++++++++-- src/Sparklines.js | 9 +++-- src/SparklinesLine.js | 81 ++++++++++++++++++++++++-------------- 6 files changed, 142 insertions(+), 37 deletions(-) diff --git a/__tests__/DataProcessor.js b/__tests__/DataProcessor.js index 42a9d32..4ab81f7 100644 --- a/__tests__/DataProcessor.js +++ b/__tests__/DataProcessor.js @@ -104,4 +104,43 @@ describe('DataProcessor', () => { expect(DataProcessor.median(data)).to.eq(3668.810244) }) }); + + describe('isGapValue', () => { + it('finite numbers are not gaps', () => { + expect(DataProcessor.isGapValue(0)).to.eq(false); + expect(DataProcessor.isGapValue(1.5)).to.eq(false); + }); + + it('anything else is a gap', () => { + expect(DataProcessor.isGapValue(Infinity)).to.eq(true); + expect(DataProcessor.isGapValue(-Infinity)).to.eq(true); + expect(DataProcessor.isGapValue(NaN)).to.eq(true); + expect(DataProcessor.isGapValue('0')).to.eq(true); + expect(DataProcessor.isGapValue('1.5')).to.eq(true); + expect(DataProcessor.isGapValue({})).to.eq(true); + expect(DataProcessor.isGapValue(null)).to.eq(true); + expect(DataProcessor.isGapValue(undefined)).to.eq(true); + expect(DataProcessor.isGapValue([])).to.eq(true); + }); + }); + + describe('pointsToSegments', () => { + it('without gap', () => { + expect(DataProcessor.pointsToSegments([{x: 0, y: 0}])).to.eql([{isGap: false, points: [{x: 0, y: 0}]}]); + expect(DataProcessor.pointsToSegments([{x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 1}])).to.eql([{isGap: false, points: [{x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 1}]}]); + }); + it('only gap', () => { + expect(DataProcessor.pointsToSegments([{x: 0, y: null}])).to.eql([{isGap: true, points: [{x: 0, y: null}]}]); + expect(DataProcessor.pointsToSegments([{x: 0, y: null}, {x: 1, y: null}, {x: 2, y: null}])).to.eql([{isGap: true, points: [{x: 0, y: null}, {x: 1, y: null}, {x: 2, y: null}]}]); + }); + it('gap-nongap', () => { + expect(DataProcessor.pointsToSegments([{x: 0, y: null}, {x: 1, y: 2}, {x: 2, y: 3}])).to.eql([{isGap: true, points: [{x: 0, y: null}]}, {isGap: false, points: [{x: 1, y: 2}, {x: 2, y: 3}]}]); + }); + it('nongap-gap', () => { + expect(DataProcessor.pointsToSegments([{x: 0, y: 1}, {x: 1, y: null}, {x: 2, y: null}])).to.eql([{isGap: false, points: [{x: 0, y: 1}]}, {isGap: true, points: [{x: 1, y: null}, {x: 2, y: null}]}]); + }); + it('gap-nongap-gap', () => { + expect(DataProcessor.pointsToSegments([{x: 0, y: null}, {x: 1, y: 2}, {x: 2, y: null}])).to.eql([{isGap: true, points: [{x: 0, y: null}]}, {isGap: false, points: [{x: 1, y: 2}]}, {isGap: true, points: [{x: 2, y: null}]}]); + }); + }); }); diff --git a/demo/demo.js b/demo/demo.js index 48f58b8..3422b06 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -29,6 +29,7 @@ function randomData(n = 30) { const sampleData = randomData(30); const sampleData100 = randomData(100); +const sampleDataGaps = sampleData.map(x => x < -1 ? NaN : x); const Header = () => @@ -41,6 +42,11 @@ const Simple = () => +const SimpleGaps = () => + + + + const SimpleCurve = () => @@ -295,6 +301,7 @@ const RealWorld9 = () => const demos = { 'headersparklines': Header, 'simple': Simple, + 'simpleGaps': SimpleGaps, 'simpleCurve': SimpleCurve, 'customizable1': Customizable1, 'customizable2': Customizable2, diff --git a/demo/index.html b/demo/index.html index f417aa7..e090965 100644 --- a/demo/index.html +++ b/demo/index.html @@ -114,6 +114,17 @@

Simple

+

Simple w/Gaps

+ +
+
+

+<Sparklines data={sampleDataGaps} gaps={true}>
+    <SparklinesLine />
+</Sparklines>
+            
+
+

Simple Curve

diff --git a/src/DataProcessor.js b/src/DataProcessor.js index 8914957..4fe1cd7 100644 --- a/src/DataProcessor.js +++ b/src/DataProcessor.js @@ -1,6 +1,6 @@ export default class DataProcessor { - static dataToPoints(data, limit, width = 1, height = 1, margin = 0, max = this.max(data), min = this.min(data)) { + static dataToPoints(data, limit, width = 1, height = 1, margin = 0, max = this.max(data), min = this.min(data), leaveGaps = false) { const len = data.length; @@ -14,17 +14,17 @@ export default class DataProcessor { return data.map((d, i) => { return { x: i * hfactor + margin, - y: (max === min ? 1 : (max - d)) * vfactor + margin + y: (leaveGaps && this.isGapValue(d)) ? d : ((max === min ? 1 : (max - d)) * vfactor + margin) } }); } static max(data) { - return Math.max.apply(Math, data); + return Math.max.apply(Math, data.filter(d => !this.isGapValue(d))); } static min(data) { - return Math.min.apply(Math, data); + return Math.min.apply(Math, data.filter(d => !this.isGapValue(d))); } static mean(data) { @@ -55,4 +55,28 @@ export default class DataProcessor { static calculateFromData(data, calculationType) { return this[calculationType].call(this, data); } + + static pointsToSegments(points) { + let segment, segments = []; + const newSegment = (isGap) => ({points: [], isGap}); + for (let point of points) { + if (!segment) + segment = newSegment(false); + const isGapHere = this.isGapValue(point.y); + if (segment.isGap != isGapHere) { + if (segment.points.length) + segments.push(segment); + segment = newSegment(isGapHere); + } + segment.points.push(point); + } + if (segment && segment.points.length) + segments.push(segment); + return segments; + } + + static isGapValue(y) + { + return !Number.isFinite(y); + } } diff --git a/src/Sparklines.js b/src/Sparklines.js index 32a28d8..1e34166 100644 --- a/src/Sparklines.js +++ b/src/Sparklines.js @@ -17,7 +17,8 @@ class Sparklines extends React.Component { margin: React.PropTypes.number, style: React.PropTypes.object, min: React.PropTypes.number, - max: React.PropTypes.number + max: React.PropTypes.number, + gaps: React.PropTypes.bool }; static defaultProps = { @@ -39,15 +40,15 @@ class Sparklines extends React.Component { nextProps.max != this.props.max || nextProps.limit != this.props.limit || nextProps.data.length != this.props.data.length || - nextProps.data.some((d, i) => d !== this.props.data[i]); + nextProps.data.some((d, i) => !Object.is(d, this.props.data[i])); } render() { - const { data, limit, width, height, margin, style, max, min } = this.props; + const { data, limit, width, height, margin, style, max, min, gaps } = this.props; if (data.length === 0) return null; - const points = DataProcessor.dataToPoints(data, limit, width, height, margin, max, min); + const points = DataProcessor.dataToPoints(data, limit, width, height, margin, max, min, gaps); return ( diff --git a/src/SparklinesLine.js b/src/SparklinesLine.js index dfc3f4d..1927293 100644 --- a/src/SparklinesLine.js +++ b/src/SparklinesLine.js @@ -1,4 +1,41 @@ import React from 'react'; +import DataProcessor from './DataProcessor'; + +function SparklinesLineSegment({points, width, height, margin, color, style}) { + if (!style) + style = {}; + + const linePoints = points + .map((p) => [p.x, p.y]) + .reduce((a, b) => a.concat(b)); + const closePolyPoints = [ + points[points.length - 1].x, height - margin, + points[0].x, height - margin, + points[0].x, points[0].y + ]; + const fillPoints = linePoints.concat(closePolyPoints); + + const lineStyle = { + stroke: color || style.stroke || 'slategray', + strokeWidth: style.strokeWidth || '1', + strokeLinejoin: style.strokeLinejoin || 'round', + strokeLinecap: style.strokeLinecap || 'round', + fill: 'none' + }; + const fillStyle = { + stroke: style.stroke || 'none', + strokeWidth: '0', + fillOpacity: style.fillOpacity || '.1', + fill: style.fill || color || 'slategray' + }; + + return ( + + + + + ) +} export default class SparklinesLine extends React.Component { @@ -12,37 +49,23 @@ export default class SparklinesLine extends React.Component { }; render() { - const { points, width, height, margin, color, style } = this.props; - - const linePoints = points - .map((p) => [p.x, p.y]) - .reduce((a, b) => a.concat(b)); - const closePolyPoints = [ - points[points.length - 1].x, height - margin, - margin, height - margin, - margin, points[0].y - ]; - const fillPoints = linePoints.concat(closePolyPoints); - - const lineStyle = { - stroke: color || style.stroke || 'slategray', - strokeWidth: style.strokeWidth || '1', - strokeLinejoin: style.strokeLinejoin || 'round', - strokeLinecap: style.strokeLinecap || 'round', - fill: 'none' - }; - const fillStyle = { - stroke: style.stroke || 'none', - strokeWidth: '0', - fillOpacity: style.fillOpacity || '.1', - fill: style.fill || color || 'slategray' - }; - + const { points, height, margin, color, style } = this.props; + if (DataProcessor.pointsToSegments(points).length !== 1) + console.log(points); return ( - - + {DataProcessor.pointsToSegments(points) + .filter(segment => !segment.isGap) + .map((segment, i) => ( + ) + )} - ) + ); } } From 3e587576980b42f25e7e3333e3cb42e46c5c76f0 Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Wed, 13 Apr 2016 20:07:55 +0300 Subject: [PATCH 3/8] Remove console.log --- src/SparklinesLine.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/SparklinesLine.js b/src/SparklinesLine.js index 1927293..c379627 100644 --- a/src/SparklinesLine.js +++ b/src/SparklinesLine.js @@ -50,8 +50,6 @@ export default class SparklinesLine extends React.Component { render() { const { points, height, margin, color, style } = this.props; - if (DataProcessor.pointsToSegments(points).length !== 1) - console.log(points); return ( {DataProcessor.pointsToSegments(points) From e2d5a0039930abf0c2940e0bfbcb2e6c2cf0d891 Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Wed, 13 Apr 2016 20:08:49 +0300 Subject: [PATCH 4/8] Add graphical test for SparklinesLine with gaps --- __tests__/data.json | 20 ++++++++++++++++++++ __tests__/fixtures.js | 3 ++- bootstrap-tests.js | 4 ++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/__tests__/data.json b/__tests__/data.json index 02de8e4..60201cb 100644 --- a/__tests__/data.json +++ b/__tests__/data.json @@ -63,5 +63,25 @@ 0.7154759857504911, -2.8727571205730915, -1.3501468729225303, -0.0865770144109483, 0.505651174224522, -2.2111682240498753, 2.035381345199811 + ], + "sampleDataGaps": [0.26789611283279424, + null, -0.46848826820269196, -0.02429709108986638, -0.07347501430506465, + 0.938722048681125, -0.02488170176918398, + 0.014511315562131895, + null, + null, + 0.6777968350341455, + null, + 1.402853678778828, + null, -0.37025235972161835, -0.10254054014867667, + 0.3709902985604339, + 2.5285657626539253, -0.18958673659343403, + 0.8578243085059141, + 1.7395812075504404, + 0.9723534409914075, -0.6799757002898873, + 1.153081489500828, + 1.3851189843556257, + 0.19355625368483506, + 1.262069965103209, -0.8628137671385424, -0.6118030618030503, -0.25257403618789087 ] } \ No newline at end of file diff --git a/__tests__/fixtures.js b/__tests__/fixtures.js index f178336..0207ae4 100644 --- a/__tests__/fixtures.js +++ b/__tests__/fixtures.js @@ -2,13 +2,14 @@ import React from 'react'; import { Sparklines, SparklinesBars, SparklinesLine, SparklinesCurve, SparklinesNormalBand, SparklinesReferenceLine, SparklinesSpots } from '../src/Sparklines'; -import { sampleData, sampleData100 } from './data.json'; +import { sampleData, sampleData100, sampleDataGaps } from './data.json'; export default { // AUTO-GENERATED PART STARTS HERE "Header": {jsx: (), svg: ""}, "Simple": {jsx: (), svg: ""}, "SimpleCurve": {jsx: (), svg: ""}, + "SimpleGaps": {jsx: (), svg: ""}, "Customizable1": {jsx: (), svg: ""}, "Customizable2": {jsx: (), svg: ""}, "Customizable3": {jsx: (), svg: ""}, diff --git a/bootstrap-tests.js b/bootstrap-tests.js index 10cd82a..b3756cf 100644 --- a/bootstrap-tests.js +++ b/bootstrap-tests.js @@ -23,9 +23,9 @@ const dynamicPartEndSignal = '// AUTO-GENERATED PART ENDS HERE'; const fixtures = require(fixturesFile).default; // Handle recurring data constants -import {sampleData, sampleData100} from './__tests__/data.json'; +import {sampleData, sampleData100, sampleDataGaps} from './__tests__/data.json'; const recognizedDataConstants = { - sampleData, sampleData100 + sampleData, sampleData100, sampleDataGaps }; const recognizedDataStrings = {}; for (let dataKey of Object.keys(recognizedDataConstants)) { From cd05e87808d6501d77d537c77e3a53b8b5b9ed60 Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Wed, 13 Apr 2016 20:09:21 +0300 Subject: [PATCH 5/8] Don't nest elements more than necessary in the common case --- src/SparklinesLine.js | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/SparklinesLine.js b/src/SparklinesLine.js index c379627..bbf0982 100644 --- a/src/SparklinesLine.js +++ b/src/SparklinesLine.js @@ -50,20 +50,17 @@ export default class SparklinesLine extends React.Component { render() { const { points, height, margin, color, style } = this.props; - return ( - - {DataProcessor.pointsToSegments(points) + const groups = DataProcessor.pointsToSegments(points) .filter(segment => !segment.isGap) - .map((segment, i) => ( - ) - )} - - ); + .map((segment, i) => ()); + if (groups.length === 1) + return groups[0]; + return {groups}; } } From 774e33e82ad02a2b9f34e79a177e3d69009bae6a Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Wed, 13 Apr 2016 21:10:15 +0300 Subject: [PATCH 6/8] Add gapped data support in bars, curve, normal band, reference line. SparklinesLine and SparklinesCurve received a DRY treatment via the addition of SparklinesSegmentContainer and defaults.js. Includes tests and an updated demo page. --- __tests__/fixtures.js | 4 ++ demo/demo.js | 28 ++++++++ demo/index.html | 42 +++++++++++ src/DataProcessor.js | 13 +++- src/SparklinesBars.js | 5 +- src/SparklinesCurve.js | 111 +++++++++++++++--------------- src/SparklinesLine.js | 37 +++------- src/SparklinesSegmentContainer.js | 18 +++++ src/defaults.js | 40 +++++++++++ 9 files changed, 209 insertions(+), 89 deletions(-) create mode 100644 src/SparklinesSegmentContainer.js create mode 100644 src/defaults.js diff --git a/__tests__/fixtures.js b/__tests__/fixtures.js index 0207ae4..1063c83 100644 --- a/__tests__/fixtures.js +++ b/__tests__/fixtures.js @@ -10,6 +10,7 @@ export default { "Simple": {jsx: (), svg: ""}, "SimpleCurve": {jsx: (), svg: ""}, "SimpleGaps": {jsx: (), svg: ""}, + "SimpleCurveGaps": {jsx: (), svg: ""}, "Customizable1": {jsx: (), svg: ""}, "Customizable2": {jsx: (), svg: ""}, "Customizable3": {jsx: (), svg: ""}, @@ -22,14 +23,17 @@ export default { "Spots3": {jsx: (), svg: ""}, "Bars1": {jsx: (), svg: ""}, "Bars2": {jsx: (), svg: ""}, + "BarsGaps": {jsx: (), svg: ""}, "ReferenceLine1": {jsx: (), svg: ""}, "ReferenceLine2": {jsx: (), svg: ""}, "ReferenceLine3": {jsx: (), svg: ""}, "ReferenceLine4": {jsx: (), svg: ""}, "ReferenceLine5": {jsx: (), svg: ""}, "ReferenceLine6": {jsx: (), svg: ""}, + "ReferenceLineGaps": {jsx: (), svg: ""}, "NormalBand1": {jsx: (), svg: ""}, "NormalBand2": {jsx: (), svg: ""}, + "NormalBandGaps": {jsx: (), svg: ""}, "RealWorld1": {jsx: (), svg: ""}, "RealWorld2": {jsx: (), svg: ""}, "RealWorld3": {jsx: (), svg: ""}, diff --git a/demo/demo.js b/demo/demo.js index 3422b06..0742898 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -52,6 +52,11 @@ const SimpleCurve = () => +const SimpleCurveGaps = () => + + + + const Customizable1 = () => @@ -116,6 +121,12 @@ const Bars2 = () => +const BarsGaps = () => + + + + + class Dynamic1 extends Component { constructor(props) { @@ -236,6 +247,12 @@ const ReferenceLine6 = () => +const ReferenceLineGaps = () => + + + + + const NormalBand1 = () => @@ -249,6 +266,13 @@ const NormalBand2 = () => +const NormalBandGaps = () => + + + + + + const RealWorld1 = () => @@ -303,6 +327,7 @@ const demos = { 'simple': Simple, 'simpleGaps': SimpleGaps, 'simpleCurve': SimpleCurve, + 'simpleCurveGaps': SimpleCurveGaps, 'customizable1': Customizable1, 'customizable2': Customizable2, 'customizable3': Customizable3, @@ -315,6 +340,7 @@ const demos = { 'bounds1': Bounds1, 'bars1': Bars1, 'bars2': Bars2, + 'barsGaps': BarsGaps, 'dynamic1': Dynamic1, 'dynamic2': Dynamic2, 'dynamic3': Dynamic3, @@ -325,8 +351,10 @@ const demos = { 'referenceline4': ReferenceLine4, 'referenceline5': ReferenceLine5, 'referenceline6': ReferenceLine6, + 'referencelineGaps': ReferenceLineGaps, 'normalband1': NormalBand1, 'normalband2': NormalBand2, + 'normalbandGaps': NormalBandGaps, 'realworld1': RealWorld1, 'realworld2': RealWorld2, 'realworld3': RealWorld3, diff --git a/demo/index.html b/demo/index.html index e090965..1c14d4e 100644 --- a/demo/index.html +++ b/demo/index.html @@ -136,6 +136,17 @@

Simple Curve

+

Simple Curve w/Gaps

+ +
+
+

+<Sparklines data={sampleDataGaps} gaps={true}>
+    <SparklinesCurve />
+</Sparklines>
+            
+
+

Customizable

@@ -219,6 +230,16 @@

Bars

+
+
+

+<Sparklines data={sampleDataGaps} gaps={true}>
+    <SparklinesBars style={{ stroke: "white", fill: "#41c3f9", fillOpacity: ".25" }} />
+    <SparklinesLine style={{ stroke: "#41c3f9", fill: "none" }} />
+</Sparklines>
+            
+
+

Dynamic

@@ -321,6 +342,16 @@

Reference Line

+
+
+

+<Sparklines data={sampleDataGaps} gaps={true}>
+    <SparklinesBars style={{ fill: 'slategray', fillOpacity: ".5" }} />
+    <SparklinesReferenceLine />
+</Sparklines>
+            
+
+

Normal Band

@@ -344,6 +375,17 @@

Normal Band

+
+
+

+<Sparklines data={sampleData} gaps={true}>
+    <SparklinesLine style={{ fill: "none" }}/>
+    <SparklinesNormalBand />
+    <SparklinesReferenceLine type="mean" />
+</Sparklines>
+            
+
+

Real world examples

diff --git a/src/DataProcessor.js b/src/DataProcessor.js index 4fe1cd7..49cb7db 100644 --- a/src/DataProcessor.js +++ b/src/DataProcessor.js @@ -19,33 +19,42 @@ export default class DataProcessor { }); } + static nonGapValues(data) { + return data.filter(d => !this.isGapValue(d)); + } + static max(data) { - return Math.max.apply(Math, data.filter(d => !this.isGapValue(d))); + return Math.max.apply(Math, this.nonGapValues(data)); } static min(data) { - return Math.min.apply(Math, data.filter(d => !this.isGapValue(d))); + return Math.min.apply(Math, this.nonGapValues(data)); } static mean(data) { + data = this.nonGapValues(data); return (this.max(data) - this.min(data)) / 2; } static avg(data) { + data = this.nonGapValues(data); return data.reduce((a, b) => a + b) / data.length; } static median(data) { + data = this.nonGapValues(data); return data.sort((a,b) => a - b)[Math.floor(data.length / 2)]; } static variance(data) { + data = this.nonGapValues(data); const mean = this.mean(data); const sq = data.map(n => Math.pow(n - mean, 2)); return this.mean(sq); } static stdev(data) { + data = this.nonGapValues(data); const mean = this.mean(data); const sqDiff = data.map(n => Math.pow(n - mean, 2)); const avgSqDiff = this.avg(sqDiff); diff --git a/src/SparklinesBars.js b/src/SparklinesBars.js index 2672188..921a779 100644 --- a/src/SparklinesBars.js +++ b/src/SparklinesBars.js @@ -1,4 +1,5 @@ import React from 'react'; +import DataProcessor from './DataProcessor'; export default class SparklinesBars extends React.Component { @@ -22,13 +23,13 @@ export default class SparklinesBars extends React.Component { return ( {points.map((p, i) => - + style={style} />) )} ) diff --git a/src/SparklinesCurve.js b/src/SparklinesCurve.js index 13bc69d..8419bfb 100644 --- a/src/SparklinesCurve.js +++ b/src/SparklinesCurve.js @@ -1,4 +1,55 @@ import React from 'react'; +import * as defaults from './defaults'; +import SparklinesSegmentContainer from './SparklinesSegmentContainer'; + +function SparklinesCurveSegment({ points, width, height, margin, color, style, divisor = 0.25 }) { + let prev; + const curve = (p) => { + let res; + if (!prev) { + res = [p.x, p.y] + } else { + const len = (p.x - prev.x) * divisor; + res = [ "C", + //x1 + prev.x + len, + //y1 + prev.y, + //x2, + p.x - len, + //y2, + p.y, + //x, + p.x, + //y + p.y + ]; + } + prev = p; + return res; + + } + const linePoints = points + .map((p) => curve(p)) + .reduce((a, b) => a.concat(b)); + const closePolyPoints = [ + "L" + points[points.length - 1].x, height - margin, + points[0].x, height - margin, + points[0].x, points[0].y + ]; + const fillPoints = linePoints.concat(closePolyPoints); + + const lineStyle = defaults.getLineStyle({color, style}); + + const fillStyle = defaults.getFillStyle({color, style}); + + return ( + + + + + ); +} export default class SparklinesCurve extends React.Component { @@ -12,62 +63,8 @@ export default class SparklinesCurve extends React.Component { }; render() { - const { points, width, height, margin, color, style, divisor = 0.25 } = this.props; - let prev; - const curve = (p) => { - let res; - if (!prev) { - res = [p.x, p.y] - } else { - const len = (p.x - prev.x) * divisor; - res = [ "C", - //x1 - prev.x + len, - //y1 - prev.y, - //x2, - p.x - len, - //y2, - p.y, - //x, - p.x, - //y - p.y - ]; - } - prev = p; - return res; - - } - const linePoints = points - .map((p) => curve(p)) - .reduce((a, b) => a.concat(b)); - const closePolyPoints = [ - "L" + points[points.length - 1].x, height - margin, - margin, height - margin, - margin, points[0].y - ]; - const fillPoints = linePoints.concat(closePolyPoints); - - const lineStyle = { - stroke: color || style.stroke || 'slategray', - strokeWidth: style.strokeWidth || '1', - strokeLinejoin: style.strokeLinejoin || 'round', - strokeLinecap: style.strokeLinecap || 'round', - fill: 'none' - }; - const fillStyle = { - stroke: style.stroke || 'none', - strokeWidth: '0', - fillOpacity: style.fillOpacity || '.1', - fill: style.fill || color || 'slategray' - }; - - return ( - - - - - ) + return ( + + ); } } diff --git a/src/SparklinesLine.js b/src/SparklinesLine.js index bbf0982..fae26ae 100644 --- a/src/SparklinesLine.js +++ b/src/SparklinesLine.js @@ -1,5 +1,6 @@ import React from 'react'; -import DataProcessor from './DataProcessor'; +import * as defaults from './defaults'; +import SparklinesSegmentContainer from './SparklinesSegmentContainer'; function SparklinesLineSegment({points, width, height, margin, color, style}) { if (!style) @@ -15,19 +16,9 @@ function SparklinesLineSegment({points, width, height, margin, color, style}) { ]; const fillPoints = linePoints.concat(closePolyPoints); - const lineStyle = { - stroke: color || style.stroke || 'slategray', - strokeWidth: style.strokeWidth || '1', - strokeLinejoin: style.strokeLinejoin || 'round', - strokeLinecap: style.strokeLinecap || 'round', - fill: 'none' - }; - const fillStyle = { - stroke: style.stroke || 'none', - strokeWidth: '0', - fillOpacity: style.fillOpacity || '.1', - fill: style.fill || color || 'slategray' - }; + const lineStyle = defaults.getLineStyle({color, style}); + + const fillStyle = defaults.getFillStyle({color, style}); return ( @@ -49,18 +40,8 @@ export default class SparklinesLine extends React.Component { }; render() { - const { points, height, margin, color, style } = this.props; - const groups = DataProcessor.pointsToSegments(points) - .filter(segment => !segment.isGap) - .map((segment, i) => ()); - if (groups.length === 1) - return groups[0]; - return {groups}; + return ( + + ); } -} +} \ No newline at end of file diff --git a/src/SparklinesSegmentContainer.js b/src/SparklinesSegmentContainer.js new file mode 100644 index 0000000..41dde98 --- /dev/null +++ b/src/SparklinesSegmentContainer.js @@ -0,0 +1,18 @@ +import React from 'react'; +import DataProcessor from './DataProcessor'; + +export default class SparklinesSegmentContainer extends React.Component { + render() { + const { points, children, ...props } = this.props; + const groups = DataProcessor.pointsToSegments(points) + .filter(segment => !segment.isGap) + .map((segment, i) => React.cloneElement(React.Children.only(children), { + key: i, + points: segment.points, + ...props + })); + if (groups.length === 1) + return groups[0]; + return {groups}; + } +} diff --git a/src/defaults.js b/src/defaults.js new file mode 100644 index 0000000..7ee47fa --- /dev/null +++ b/src/defaults.js @@ -0,0 +1,40 @@ +export const lineStyle = { + stroke: 'slategray', + strokeWidth: '1', + strokeLinejoin: 'round', + strokeLinecap: 'round', + fill: 'none' +}; + +export const fillStyle = { + stroke: 'none', + strokeWidth: '0', + fillOpacity: '.1', + fill: 'slategray' +}; + +export const getLineStyle = ({color, style}) => { + const defaultStyle = lineStyle; + style = style || {}; + + return { + stroke: color || style.stroke || defaultStyle.stroke, + strokeWidth: style.strokeWidth || defaultStyle.strokeWidth, + strokeLinejoin: style.strokeLinejoin || defaultStyle.strokeLinejoin, + strokeLinecap: style.strokeLinecap || defaultStyle.strokeLinecap, + fill: defaultStyle.fill + }; +} + +export const getFillStyle = ({color, style}) => { + const defaultStyle = fillStyle; + style = style || {}; + + return { + stroke: style.stroke || defaultStyle.stroke, + strokeWidth: defaultStyle.strokeWidth, + fillOpacity: style.fillOpacity || defaultStyle.fillOpacity, + fill: style.fill || color || defaultStyle.fill + }; +} + From c8ab48677e96abcf3ea002e353afe301fc2dfd81 Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Thu, 14 Apr 2016 17:00:41 +0300 Subject: [PATCH 7/8] Remove gaps prop and make gaps={true} the effective default --- __tests__/fixtures.js | 10 +++++----- demo/index.html | 15 ++++++++++----- src/DataProcessor.js | 4 ++-- src/Sparklines.js | 7 +++---- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/__tests__/fixtures.js b/__tests__/fixtures.js index 1063c83..a9a2349 100644 --- a/__tests__/fixtures.js +++ b/__tests__/fixtures.js @@ -9,8 +9,8 @@ export default { "Header": {jsx: (), svg: ""}, "Simple": {jsx: (), svg: ""}, "SimpleCurve": {jsx: (), svg: ""}, - "SimpleGaps": {jsx: (), svg: ""}, - "SimpleCurveGaps": {jsx: (), svg: ""}, + "SimpleGaps": {jsx: (), svg: ""}, + "SimpleCurveGaps": {jsx: (), svg: ""}, "Customizable1": {jsx: (), svg: ""}, "Customizable2": {jsx: (), svg: ""}, "Customizable3": {jsx: (), svg: ""}, @@ -23,17 +23,17 @@ export default { "Spots3": {jsx: (), svg: ""}, "Bars1": {jsx: (), svg: ""}, "Bars2": {jsx: (), svg: ""}, - "BarsGaps": {jsx: (), svg: ""}, + "BarsGaps": {jsx: (), svg: ""}, "ReferenceLine1": {jsx: (), svg: ""}, "ReferenceLine2": {jsx: (), svg: ""}, "ReferenceLine3": {jsx: (), svg: ""}, "ReferenceLine4": {jsx: (), svg: ""}, "ReferenceLine5": {jsx: (), svg: ""}, "ReferenceLine6": {jsx: (), svg: ""}, - "ReferenceLineGaps": {jsx: (), svg: ""}, + "ReferenceLineGaps": {jsx: (), svg: ""}, "NormalBand1": {jsx: (), svg: ""}, "NormalBand2": {jsx: (), svg: ""}, - "NormalBandGaps": {jsx: (), svg: ""}, + "NormalBandGaps": {jsx: (), svg: ""}, "RealWorld1": {jsx: (), svg: ""}, "RealWorld2": {jsx: (), svg: ""}, "RealWorld3": {jsx: (), svg: ""}, diff --git a/demo/index.html b/demo/index.html index 1c14d4e..05d09bf 100644 --- a/demo/index.html +++ b/demo/index.html @@ -119,7 +119,8 @@

Simple w/Gaps


-<Sparklines data={sampleDataGaps} gaps={true}>
+<Sparklines data={sampleDataGaps}
+,>
     <SparklinesLine />
 </Sparklines>
             
@@ -141,7 +142,8 @@

Simple Curve w/Gaps


-<Sparklines data={sampleDataGaps} gaps={true}>
+<Sparklines data={sampleDataGaps}
+,>
     <SparklinesCurve />
 </Sparklines>
             
@@ -233,7 +235,8 @@

Bars


-<Sparklines data={sampleDataGaps} gaps={true}>
+<Sparklines data={sampleDataGaps}
+,>
     <SparklinesBars style={{ stroke: "white", fill: "#41c3f9", fillOpacity: ".25" }} />
     <SparklinesLine style={{ stroke: "#41c3f9", fill: "none" }} />
 </Sparklines>
@@ -345,7 +348,8 @@ <h2>Reference Line</h2>
         <div class="row">
             <div id="referencelineGaps"></div>
             <pre class="prettyprint"><xmp>
-<Sparklines data={sampleDataGaps} gaps={true}>
+<Sparklines data={sampleDataGaps}
+,>
     <SparklinesBars style={{ fill: 'slategray', fillOpacity: ".5" }} />
     <SparklinesReferenceLine />
 </Sparklines>
@@ -378,7 +382,8 @@ <h2>Normal Band</h2>
         <div class="row">
             <div id="normalbandGaps"></div>
             <pre class="prettyprint"><xmp>
-<Sparklines data={sampleData} gaps={true}>
+<Sparklines data={sampleData}
+,>
     <SparklinesLine style={{ fill: "none" }}/>
     <SparklinesNormalBand />
     <SparklinesReferenceLine type="mean" />
diff --git a/src/DataProcessor.js b/src/DataProcessor.js
index 49cb7db..ce9cd1d 100644
--- a/src/DataProcessor.js
+++ b/src/DataProcessor.js
@@ -1,6 +1,6 @@
 export default class DataProcessor {
 
-    static dataToPoints(data, limit, width = 1, height = 1, margin = 0, max = this.max(data), min = this.min(data), leaveGaps = false) {
+    static dataToPoints(data, limit, width = 1, height = 1, margin = 0, max = this.max(data), min = this.min(data)) {
 
         const len = data.length;
 
@@ -14,7 +14,7 @@ export default class DataProcessor {
         return data.map((d, i) => {
             return {
                 x: i * hfactor + margin,
-                y: (leaveGaps && this.isGapValue(d)) ? d : ((max === min ? 1 : (max - d)) * vfactor + margin)
+                y: this.isGapValue(d) ? d : ((max === min ? 1 : (max - d)) * vfactor + margin)
             }
         });
     }
diff --git a/src/Sparklines.js b/src/Sparklines.js
index 1e34166..0128168 100644
--- a/src/Sparklines.js
+++ b/src/Sparklines.js
@@ -17,8 +17,7 @@ class Sparklines extends React.Component {
         margin: React.PropTypes.number,
         style: React.PropTypes.object,
         min: React.PropTypes.number,
-        max: React.PropTypes.number,
-        gaps: React.PropTypes.bool
+        max: React.PropTypes.number
     };
 
     static defaultProps = {
@@ -44,11 +43,11 @@ class Sparklines extends React.Component {
     }
 
     render() {
-        const { data, limit, width, height, margin, style, max, min, gaps } = this.props;
+        const { data, limit, width, height, margin, style, max, min } = this.props;
 
         if (data.length === 0) return null;
 
-        const points = DataProcessor.dataToPoints(data, limit, width, height, margin, max, min, gaps);
+        const points = DataProcessor.dataToPoints(data, limit, width, height, margin, max, min );
 
         return (
             <svg width={width} height={height} style={style} viewBox={`0 0 ${width} ${height}`}>

From d4735e8a0e9ecf5a3c8afb5e1ff780a88a5ff02e Mon Sep 17 00:00:00 2001
From: Moti Zilberman <motiz88@gmail.com>
Date: Thu, 14 Apr 2016 17:29:38 +0300
Subject: [PATCH 8/8] Fix mistake in demo/index.html

---
 demo/index.html | 15 +++++----------
 1 file changed, 5 insertions(+), 10 deletions(-)

diff --git a/demo/index.html b/demo/index.html
index 05d09bf..e72cd52 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -119,8 +119,7 @@ <h2>Simple w/Gaps</h2>
         <div class="row">
             <div id="simpleGaps"></div>
             <pre class="prettyprint"><xmp>
-<Sparklines data={sampleDataGaps}
-,>
+<Sparklines data={sampleDataGaps}>
     <SparklinesLine />
 </Sparklines>
             
@@ -142,8 +141,7 @@

Simple Curve w/Gaps


-<Sparklines data={sampleDataGaps}
-,>
+<Sparklines data={sampleDataGaps}>
     <SparklinesCurve />
 </Sparklines>
             
@@ -235,8 +233,7 @@

Bars


-<Sparklines data={sampleDataGaps}
-,>
+<Sparklines data={sampleDataGaps}>
     <SparklinesBars style={{ stroke: "white", fill: "#41c3f9", fillOpacity: ".25" }} />
     <SparklinesLine style={{ stroke: "#41c3f9", fill: "none" }} />
 </Sparklines>
@@ -348,8 +345,7 @@ <h2>Reference Line</h2>
         <div class="row">
             <div id="referencelineGaps"></div>
             <pre class="prettyprint"><xmp>
-<Sparklines data={sampleDataGaps}
-,>
+<Sparklines data={sampleDataGaps}>
     <SparklinesBars style={{ fill: 'slategray', fillOpacity: ".5" }} />
     <SparklinesReferenceLine />
 </Sparklines>
@@ -382,8 +378,7 @@ <h2>Normal Band</h2>
         <div class="row">
             <div id="normalbandGaps"></div>
             <pre class="prettyprint"><xmp>
-<Sparklines data={sampleData}
-,>
+<Sparklines data={sampleData}>
     <SparklinesLine style={{ fill: "none" }}/>
     <SparklinesNormalBand />
     <SparklinesReferenceLine type="mean" />