Skip to content

Support gaps in data (null, NaN, etc) #46

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions __tests__/DataProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}]}]);
});
});
});
20 changes: 20 additions & 0 deletions __tests__/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
]
}
7 changes: 6 additions & 1 deletion __tests__/fixtures.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions bootstrap-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
35 changes: 35 additions & 0 deletions demo/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () =>
<Sparklines data={sampleData} width={300} height={50}>
Expand All @@ -41,11 +42,21 @@ const Simple = () =>
<SparklinesLine />
</Sparklines>

const SimpleGaps = () =>
<Sparklines data={sampleDataGaps} gaps={true}>
<SparklinesLine />
</Sparklines>

const SimpleCurve = () =>
<Sparklines data={sampleData}>
<SparklinesCurve />
</Sparklines>

const SimpleCurveGaps = () =>
<Sparklines data={sampleDataGaps} gaps={true}>
<SparklinesCurve />
</Sparklines>

const Customizable1 = () =>
<Sparklines data={sampleData}>
<SparklinesLine color="#1c8cdc" />
Expand Down Expand Up @@ -110,6 +121,12 @@ const Bars2 = () =>
<SparklinesLine style={{ stroke: "#41c3f9", fill: "none" }} />
</Sparklines>

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

class Dynamic1 extends Component {

constructor(props) {
Expand Down Expand Up @@ -230,6 +247,12 @@ const ReferenceLine6 = () =>
<SparklinesReferenceLine />
</Sparklines>

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

const NormalBand1 = () =>
<Sparklines data={sampleData}>
<SparklinesLine style={{ fill: "none" }} />
Expand All @@ -243,6 +266,13 @@ const NormalBand2 = () =>
<SparklinesReferenceLine type="mean" />
</Sparklines>

const NormalBandGaps = () =>
<Sparklines data={sampleDataGaps} gaps={true}>
<SparklinesLine style={{ fill: "none" }}/>
<SparklinesNormalBand />
<SparklinesReferenceLine type="mean" />
</Sparklines>

const RealWorld1 = () =>
<Sparklines data={sampleData}>
<SparklinesLine style={{ strokeWidth: 3, stroke: "#336aff", fill: "none" }} />
Expand Down Expand Up @@ -295,7 +325,9 @@ const RealWorld9 = () =>
const demos = {
'headersparklines': Header,
'simple': Simple,
'simpleGaps': SimpleGaps,
'simpleCurve': SimpleCurve,
'simpleCurveGaps': SimpleCurveGaps,
'customizable1': Customizable1,
'customizable2': Customizable2,
'customizable3': Customizable3,
Expand All @@ -308,6 +340,7 @@ const demos = {
'bounds1': Bounds1,
'bars1': Bars1,
'bars2': Bars2,
'barsGaps': BarsGaps,
'dynamic1': Dynamic1,
'dynamic2': Dynamic2,
'dynamic3': Dynamic3,
Expand All @@ -318,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,
Expand Down
53 changes: 53 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,17 @@ <h2>Simple</h2>
</xmp></pre>
</div>

<h2>Simple w/Gaps</h2>

<div class="row">
<div id="simpleGaps"></div>
<pre class="prettyprint"><xmp>
<Sparklines data={sampleDataGaps}>
<SparklinesLine />
</Sparklines>
</xmp></pre>
</div>

<h2>Simple Curve</h2>

<div class="row">
Expand All @@ -125,6 +136,17 @@ <h2>Simple Curve</h2>
</xmp></pre>
</div>

<h2>Simple Curve w/Gaps</h2>

<div class="row">
<div id="simpleCurveGaps"></div>
<pre class="prettyprint"><xmp>
<Sparklines data={sampleDataGaps}>
<SparklinesCurve />
</Sparklines>
</xmp></pre>
</div>

<h2>Customizable</h2>

<div class="row">
Expand Down Expand Up @@ -208,6 +230,16 @@ <h2>Bars</h2>
</xmp></pre>
</div>

<div class="row">
<div id="barsGaps"></div>
<pre class="prettyprint"><xmp>
<Sparklines data={sampleDataGaps}>
<SparklinesBars style={{ stroke: "white", fill: "#41c3f9", fillOpacity: ".25" }} />
<SparklinesLine style={{ stroke: "#41c3f9", fill: "none" }} />
</Sparklines>
</xmp></pre>
</div>

<h2>Dynamic</h2>

<div class="row">
Expand Down Expand Up @@ -310,6 +342,16 @@ <h2>Reference Line</h2>
</xmp></pre>
</div>

<div class="row">
<div id="referencelineGaps"></div>
<pre class="prettyprint"><xmp>
<Sparklines data={sampleDataGaps}>
<SparklinesBars style={{ fill: 'slategray', fillOpacity: ".5" }} />
<SparklinesReferenceLine />
</Sparklines>
</xmp></pre>
</div>

<h2>Normal Band</h2>

<div class="row">
Expand All @@ -333,6 +375,17 @@ <h2>Normal Band</h2>
</xmp></pre>
</div>

<div class="row">
<div id="normalbandGaps"></div>
<pre class="prettyprint"><xmp>
<Sparklines data={sampleData}>
<SparklinesLine style={{ fill: "none" }}/>
<SparklinesNormalBand />
<SparklinesReferenceLine type="mean" />
</Sparklines>
</xmp></pre>
</div>

<h2>Real world examples</h2>

<div class="row">
Expand Down
39 changes: 36 additions & 3 deletions src/DataProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,38 +14,47 @@ export default class DataProcessor {
return data.map((d, i) => {
return {
x: i * hfactor + margin,
y: (max === min ? 1 : (max - d)) * vfactor + margin
y: this.isGapValue(d) ? d : ((max === min ? 1 : (max - d)) * vfactor + margin)
}
});
}

static nonGapValues(data) {
return data.filter(d => !this.isGapValue(d));
}

static max(data) {
return Math.max.apply(Math, data);
return Math.max.apply(Math, this.nonGapValues(data));
}

static min(data) {
return Math.min.apply(Math, data);
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);
Expand All @@ -55,4 +64,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);
}
}
4 changes: 2 additions & 2 deletions src/Sparklines.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,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;

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 );

return (
<svg width={width} height={height} style={style} viewBox={`0 0 ${width} ${height}`}>
Expand Down
5 changes: 3 additions & 2 deletions src/SparklinesBars.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import DataProcessor from './DataProcessor';

export default class SparklinesBars extends React.Component {

Expand All @@ -22,13 +23,13 @@ export default class SparklinesBars extends React.Component {
return (
<g>
{points.map((p, i) =>
<rect
DataProcessor.isGapValue(p.y) ? null : (<rect
key={i}
x={Math.ceil(p.x - strokeWidth * i)}
y={Math.ceil(p.y)}
width={Math.ceil(width)}
height={Math.ceil(Math.max(0, height - p.y))}
style={style} />
style={style} />)
)}
</g>
)
Expand Down
Loading