mirror of
https://github.com/morrisjs/morris.js.git
synced 2024-11-10 21:36:34 +01:00
Refactor.
- Test paths as rendered in SVG. - More exact unit tests for createPath. - Catch some more edge case bugs in createPath. - Refactor createPath to handle null values better.
This commit is contained in:
parent
b0e99f7ca9
commit
d41bea2e23
@ -95,7 +95,7 @@ class Morris.Grid extends Morris.EventEmitter
|
||||
ret.y = for ykey, idx in @options.ykeys
|
||||
yval = row[ykey]
|
||||
yval = parseFloat(yval) if typeof yval is 'string'
|
||||
yval = null unless typeof yval is 'number'
|
||||
yval = null if yval? and typeof yval isnt 'number'
|
||||
if yval?
|
||||
if @cumulative
|
||||
total += yval
|
||||
|
@ -71,7 +71,7 @@ class Morris.Line extends Morris.Grid
|
||||
for row in @data
|
||||
row._x = @transX(row.x)
|
||||
row._y = for y in row.y
|
||||
if y? then @transY(y) else null
|
||||
if y? then @transY(y) else y
|
||||
|
||||
# calculate hover margins
|
||||
#
|
||||
@ -85,11 +85,11 @@ class Morris.Line extends Morris.Grid
|
||||
generatePaths: ->
|
||||
@paths = for i in [0...@options.ykeys.length]
|
||||
smooth = @options.smooth is true or @options.ykeys[i] in @options.smooth
|
||||
coords = ({x: r._x, y: r._y[i]} for r in @data )
|
||||
coords = (c for c in coords when c.y != null) if @options.continuousLine
|
||||
coords = ({x: r._x, y: r._y[i]} for r in @data when r._y[i] isnt undefined)
|
||||
coords = (c for c in coords when c.y isnt null) if @options.continuousLine
|
||||
|
||||
if coords.length > 1
|
||||
@createPath coords, smooth
|
||||
Morris.Line.createPath coords, smooth, @bottom
|
||||
else
|
||||
null
|
||||
|
||||
@ -160,54 +160,48 @@ class Morris.Line extends Morris.Grid
|
||||
# create a path for a data series
|
||||
#
|
||||
# @private
|
||||
createPath: (coords, smooth) ->
|
||||
@createPath: (coords, smooth, bottom) ->
|
||||
path = ""
|
||||
grads = @gradients coords if smooth
|
||||
grads = Morris.Line.gradients(coords) if smooth
|
||||
|
||||
nextPathType = "M"
|
||||
for i in [0..coords.length-1]
|
||||
c = coords[i]
|
||||
if c.y == null
|
||||
nextPathType = "M"
|
||||
continue
|
||||
|
||||
if nextPathType == "M"
|
||||
path += "M#{c.x},#{c.y}"
|
||||
nextPathType = "CorL"
|
||||
else
|
||||
if smooth
|
||||
g = grads[i]
|
||||
lc = coords[i - 1]
|
||||
lg = grads[i - 1]
|
||||
ix = (c.x - lc.x) / 4
|
||||
x1 = lc.x + ix
|
||||
y1 = Math.min(@bottom, lc.y + ix * lg)
|
||||
x2 = c.x - ix
|
||||
y2 = Math.min(@bottom, c.y - ix * g)
|
||||
path += "C#{x1},#{y1},#{x2},#{y2},#{c.x},#{c.y}"
|
||||
prevCoord = {y: null}
|
||||
for coord, i in coords
|
||||
if coord.y?
|
||||
if prevCoord.y?
|
||||
if smooth
|
||||
g = grads[i]
|
||||
lg = grads[i - 1]
|
||||
ix = (coord.x - prevCoord.x) / 4
|
||||
x1 = prevCoord.x + ix
|
||||
y1 = Math.max(bottom, prevCoord.y + ix * lg)
|
||||
x2 = coord.x - ix
|
||||
y2 = Math.max(bottom, coord.y - ix * g)
|
||||
path += "C#{x1},#{y1},#{x2},#{y2},#{coord.x},#{coord.y}"
|
||||
else
|
||||
path += "L#{coord.x},#{coord.y}"
|
||||
else
|
||||
path += "L#{c.x},#{c.y}"
|
||||
if not smooth or grads[i]?
|
||||
path += "M#{coord.x},#{coord.y}"
|
||||
prevCoord = coord
|
||||
return path
|
||||
|
||||
# calculate a gradient at each point for a series of points
|
||||
#
|
||||
# @private
|
||||
gradients: (coords) ->
|
||||
coordA = null
|
||||
coordB = null
|
||||
for c, i in coords
|
||||
if i is 0
|
||||
coordA = coords[1]
|
||||
coordB = c
|
||||
else if i is (coords.length - 1)
|
||||
coordA = c
|
||||
coordB = coords[i - 1]
|
||||
else
|
||||
coordA = coords[i + 1]
|
||||
coordB = coords[i - 1]
|
||||
|
||||
if coordA.y != null and coordB.y != null and coordA.x != null and coordB.x != null
|
||||
(coordA.y - coordB.y) / (coordA.x - coordB.x)
|
||||
@gradients: (coords) ->
|
||||
grad = (a, b) -> (a.y - b.y) / (a.x - b.x)
|
||||
for coord, i in coords
|
||||
if coord.y?
|
||||
nextCoord = coords[i + 1] or {y: null}
|
||||
prevCoord = coords[i - 1] or {y: null}
|
||||
if prevCoord.y? and nextCoord.y?
|
||||
grad(prevCoord, nextCoord)
|
||||
else if prevCoord.y?
|
||||
grad(prevCoord, coord)
|
||||
else if nextCoord.y?
|
||||
grad(coord, nextCoord)
|
||||
else
|
||||
null
|
||||
else
|
||||
null
|
||||
|
||||
|
110
morris.js
110
morris.js
@ -155,7 +155,7 @@
|
||||
if (typeof yval === 'string') {
|
||||
yval = parseFloat(yval);
|
||||
}
|
||||
if (typeof yval !== 'number') {
|
||||
if ((yval != null) && typeof yval !== 'number') {
|
||||
yval = null;
|
||||
}
|
||||
if (yval != null) {
|
||||
@ -484,7 +484,8 @@
|
||||
smooth: true,
|
||||
hideHover: false,
|
||||
xLabels: 'auto',
|
||||
xLabelFormat: null
|
||||
xLabelFormat: null,
|
||||
continuousLine: true
|
||||
};
|
||||
|
||||
Line.prototype.calc = function() {
|
||||
@ -509,7 +510,7 @@
|
||||
if (y != null) {
|
||||
_results1.push(this.transY(y));
|
||||
} else {
|
||||
_results1.push(null);
|
||||
_results1.push(y);
|
||||
}
|
||||
}
|
||||
return _results1;
|
||||
@ -533,7 +534,7 @@
|
||||
};
|
||||
|
||||
Line.prototype.generatePaths = function() {
|
||||
var coords, i, r, smooth;
|
||||
var c, coords, i, r, smooth;
|
||||
return this.paths = (function() {
|
||||
var _i, _ref, _ref1, _results;
|
||||
_results = [];
|
||||
@ -545,7 +546,7 @@
|
||||
_results1 = [];
|
||||
for (_j = 0, _len = _ref2.length; _j < _len; _j++) {
|
||||
r = _ref2[_j];
|
||||
if (r._y[i] !== null) {
|
||||
if (r._y[i] !== void 0) {
|
||||
_results1.push({
|
||||
x: r._x,
|
||||
y: r._y[i]
|
||||
@ -554,8 +555,21 @@
|
||||
}
|
||||
return _results1;
|
||||
}).call(this);
|
||||
if (this.options.continuousLine) {
|
||||
coords = (function() {
|
||||
var _j, _len, _results1;
|
||||
_results1 = [];
|
||||
for (_j = 0, _len = coords.length; _j < _len; _j++) {
|
||||
c = coords[_j];
|
||||
if (c.y !== null) {
|
||||
_results1.push(c);
|
||||
}
|
||||
}
|
||||
return _results1;
|
||||
})();
|
||||
}
|
||||
if (coords.length > 1) {
|
||||
_results.push(this.createPath(coords, smooth));
|
||||
_results.push(Morris.Line.createPath(coords, smooth, this.bottom));
|
||||
} else {
|
||||
_results.push(null);
|
||||
}
|
||||
@ -651,52 +665,68 @@
|
||||
return _results;
|
||||
};
|
||||
|
||||
Line.prototype.createPath = function(coords, smooth) {
|
||||
var c, g, grads, i, ix, lc, lg, path, x1, x2, y1, y2, _i, _ref;
|
||||
Line.createPath = function(coords, smooth, bottom) {
|
||||
var coord, g, grads, i, ix, lg, path, prevCoord, x1, x2, y1, y2, _i, _len;
|
||||
path = "";
|
||||
if (smooth) {
|
||||
grads = this.gradients(coords);
|
||||
for (i = _i = 0, _ref = coords.length - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) {
|
||||
c = coords[i];
|
||||
if (i === 0) {
|
||||
path += "M" + c.x + "," + c.y;
|
||||
grads = Morris.Line.gradients(coords);
|
||||
}
|
||||
prevCoord = {
|
||||
y: null
|
||||
};
|
||||
for (i = _i = 0, _len = coords.length; _i < _len; i = ++_i) {
|
||||
coord = coords[i];
|
||||
if (coord.y != null) {
|
||||
if (prevCoord.y != null) {
|
||||
if (smooth) {
|
||||
g = grads[i];
|
||||
lg = grads[i - 1];
|
||||
ix = (coord.x - prevCoord.x) / 4;
|
||||
x1 = prevCoord.x + ix;
|
||||
y1 = Math.max(bottom, prevCoord.y + ix * lg);
|
||||
x2 = coord.x - ix;
|
||||
y2 = Math.max(bottom, coord.y - ix * g);
|
||||
path += "C" + x1 + "," + y1 + "," + x2 + "," + y2 + "," + coord.x + "," + coord.y;
|
||||
} else {
|
||||
path += "L" + coord.x + "," + coord.y;
|
||||
}
|
||||
} else {
|
||||
g = grads[i];
|
||||
lc = coords[i - 1];
|
||||
lg = grads[i - 1];
|
||||
ix = (c.x - lc.x) / 4;
|
||||
x1 = lc.x + ix;
|
||||
y1 = Math.min(this.bottom, lc.y + ix * lg);
|
||||
x2 = c.x - ix;
|
||||
y2 = Math.min(this.bottom, c.y - ix * g);
|
||||
path += "C" + x1 + "," + y1 + "," + x2 + "," + y2 + "," + c.x + "," + c.y;
|
||||
if (!smooth || (grads[i] != null)) {
|
||||
path += "M" + coord.x + "," + coord.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
path = "M" + ((function() {
|
||||
var _j, _len, _results;
|
||||
_results = [];
|
||||
for (_j = 0, _len = coords.length; _j < _len; _j++) {
|
||||
c = coords[_j];
|
||||
_results.push("" + c.x + "," + c.y);
|
||||
}
|
||||
return _results;
|
||||
})()).join("L");
|
||||
prevCoord = coord;
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
Line.prototype.gradients = function(coords) {
|
||||
var c, i, _i, _len, _results;
|
||||
Line.gradients = function(coords) {
|
||||
var coord, grad, i, nextCoord, prevCoord, _i, _len, _results;
|
||||
grad = function(a, b) {
|
||||
return (a.y - b.y) / (a.x - b.x);
|
||||
};
|
||||
_results = [];
|
||||
for (i = _i = 0, _len = coords.length; _i < _len; i = ++_i) {
|
||||
c = coords[i];
|
||||
if (i === 0) {
|
||||
_results.push((coords[1].y - c.y) / (coords[1].x - c.x));
|
||||
} else if (i === (coords.length - 1)) {
|
||||
_results.push((c.y - coords[i - 1].y) / (c.x - coords[i - 1].x));
|
||||
coord = coords[i];
|
||||
if (coord.y != null) {
|
||||
nextCoord = coords[i + 1] || {
|
||||
y: null
|
||||
};
|
||||
prevCoord = coords[i - 1] || {
|
||||
y: null
|
||||
};
|
||||
if ((prevCoord.y != null) && (nextCoord.y != null)) {
|
||||
_results.push(grad(prevCoord, nextCoord));
|
||||
} else if (prevCoord.y != null) {
|
||||
_results.push(grad(prevCoord, coord));
|
||||
} else if (nextCoord.y != null) {
|
||||
_results.push(grad(coord, nextCoord));
|
||||
} else {
|
||||
_results.push(null);
|
||||
}
|
||||
} else {
|
||||
_results.push((coords[i + 1].y - coords[i - 1].y) / (coords[i + 1].x - coords[i - 1].x));
|
||||
_results.push(null);
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
|
2
morris.min.js
vendored
2
morris.min.js
vendored
File diff suppressed because one or more lines are too long
@ -68,80 +68,76 @@ describe 'Morris.Line', ->
|
||||
"#{x.getYear()}/#{x.getMonth()+1}/#{x.getDay()}"
|
||||
chart.data.map((x) -> x.label).should == ['2012/1/1', '2012/1/2']
|
||||
|
||||
describe '#generatePaths', ->
|
||||
TestDefaults = {}
|
||||
describe 'rendering lines', ->
|
||||
beforeEach ->
|
||||
TestDefaults = {element: 'graph', xkey: 'x', ykeys: ['y'], labels: ['dontcare']}
|
||||
@defaults =
|
||||
element: 'graph'
|
||||
data: [{x:0, y:1, z:0}, {x:1, y:0, z:1}, {x:2, y:1, z:0}, {x:3, y:0, z:1}, {x:4, y:1, z:0}]
|
||||
xkey: 'x'
|
||||
ykeys: ['y', 'z']
|
||||
labels: ['y', 'z']
|
||||
lineColors: ['#abcdef', '#fedcba']
|
||||
smooth: true
|
||||
continuousLine: false
|
||||
|
||||
shouldHavePath = (regex, color = '#abcdef') ->
|
||||
# Matches an SVG path element within the rendered chart.
|
||||
#
|
||||
# Sneakily uses line colors to differentiate between paths within
|
||||
# the chart.
|
||||
$('#graph').find("path[stroke='#{color}']").attr('d').should.match regex
|
||||
|
||||
it 'should generate smooth lines when options.smooth is true', ->
|
||||
testData = [{x: 1, y: 1}, {x: 3, y: 1 }]
|
||||
chart = Morris.Line(TestDefaults extends {data: testData, continuousLine: true})
|
||||
path = chart.generatePaths()[0]
|
||||
path.match(/[A-Z]/g).should.deep.equal ['M', 'C']
|
||||
Morris.Line @defaults
|
||||
shouldHavePath /M[\d\.]+,[\d\.]+(C[\d\.]+(,[\d\.]+){5}){4}/
|
||||
|
||||
it 'should generate jagged, continuous lines when options.smooth is false and options.continuousLine is true', ->
|
||||
testData = [{x: 1, y: 1}, {x: 2, y: null }, {x: 3, y: 1}]
|
||||
chart = Morris.Line(TestDefaults extends {data: testData, smooth: false, continuousLine: true})
|
||||
path = chart.generatePaths()[0]
|
||||
path.match(/[A-Z]/g).should.deep.equal ['M', 'L']
|
||||
|
||||
it 'should generate jagged, discontinuous lines when options.smooth is false and options.continuousLine is false', ->
|
||||
testData = [{x: 1, y: 1}, {x: 2, y: null }, {x: 3, y: 1}, {x: 4, y: 1}]
|
||||
chart = Morris.Line(TestDefaults extends {data: testData, smooth: false, continuousLine: false})
|
||||
path = chart.generatePaths()[0]
|
||||
path.match(/[A-Z]/g).should.deep.equal ['M', 'M', 'L']
|
||||
it 'should generate jagged lines when options.smooth is false', ->
|
||||
Morris.Line $.extend(@defaults, smooth: false)
|
||||
shouldHavePath /M[\d\.]+,[\d\.]+(L[\d\.]+,[\d\.]+){4}/
|
||||
|
||||
it 'should generate smooth/jagged lines according to the value for each series when options.smooth is an array', ->
|
||||
testData = [{x: 1, a: 1, b: 1}, {x: 3, a: 1, b: 1}]
|
||||
chart = Morris.Line(TestDefaults extends {data: testData, smooth: ['a'], ykeys: ['a', 'b']})
|
||||
pathA = chart.generatePaths()[0]
|
||||
pathA.match(/[A-Z]/g).should.deep.equal ['M', 'C']
|
||||
Morris.Line $.extend(@defaults, smooth: ['y'])
|
||||
shouldHavePath /M[\d\.]+,[\d\.]+(C[\d\.]+(,[\d\.]+){5}){4}/, '#abcdef'
|
||||
shouldHavePath /M[\d\.]+,[\d\.]+(L[\d\.]+,[\d\.]+){4}/, '#fedcba'
|
||||
|
||||
pathB = chart.generatePaths()[1]
|
||||
pathB.match(/[A-Z]/g).should.deep.equal ['M', 'L']
|
||||
it 'should ignore undefined values', ->
|
||||
@defaults.data[2].y = undefined
|
||||
Morris.Line @defaults
|
||||
shouldHavePath /M[\d\.]+,[\d\.]+(C[\d\.]+(,[\d\.]+){5}){3}/
|
||||
|
||||
#skipping because undefined values are converted to nulls in the setData method morris.grid line#98
|
||||
it.skip 'should filter undefined values from series', ->
|
||||
testData = [{x: 1, y: 1}, {x: 2, y: undefined}, {x: 3, y: 1}]
|
||||
options =
|
||||
data: testData
|
||||
continuousLine: false #doesn't matter for undefined values
|
||||
it 'should ignore null values when options.continuousLine is true', ->
|
||||
@defaults.data[2].y = null
|
||||
Morris.Line $.extend(@defaults, continuousLine: true)
|
||||
shouldHavePath /M[\d\.]+,[\d\.]+(C[\d\.]+(,[\d\.]+){5}){3}/
|
||||
|
||||
chart = Morris.Line(TestDefaults extends options)
|
||||
path = chart.generatePaths()[0]
|
||||
path.match(/[A-Z]/g).should.deep.equal ['M', 'C']
|
||||
|
||||
it 'should filter null values from series only when options.continuousLine is true', ->
|
||||
testData = [{x: 1, y: 1}, {x: 2, y: null}, {x: 3, y: 1}]
|
||||
chart = Morris.Line(TestDefaults extends {data: testData, continuousLine: true})
|
||||
path = chart.generatePaths()[0]
|
||||
path.match(/[A-Z]/g).should.deep.equal ['M', 'C']
|
||||
|
||||
it 'should not filter null values from series when options.continuousLine is false', ->
|
||||
testData = [{x: 1, y: 1}, {x: 2, y: null}, {x: 3, y: 1}, {x: 4, y: 1}]
|
||||
chart = Morris.Line(TestDefaults extends {data: testData, continuousLine: false})
|
||||
path = chart.generatePaths()[0]
|
||||
path.match(/[A-Z]/g).should.deep.equal ['M', 'M', 'C']
|
||||
it 'should break the line at null values when options.continuousLine is false', ->
|
||||
@defaults.data[2].y = null
|
||||
Morris.Line @defaults
|
||||
shouldHavePath /(M[\d\.]+,[\d\.]+C[\d\.]+(,[\d\.]+){5}){2}/
|
||||
|
||||
describe '#createPath', ->
|
||||
TestDefaults = {}
|
||||
beforeEach ->
|
||||
TestDefaults = {element: 'graph', xkey: 'x', ykeys: ['y'], labels: ['dontcare']}
|
||||
|
||||
it 'should generate a smooth line', ->
|
||||
testData = [{x: 1, y: 1}, {x: 3, y: 1}]
|
||||
chart = Morris.Line(TestDefaults extends {data: testData})
|
||||
path = chart.createPath(testData, true)
|
||||
path.match(/[A-Z]/g).should.deep.equal ['M', 'C']
|
||||
testData = [{x: 0, y: 10}, {x: 10, y: 0}, {x: 20, y: 10}]
|
||||
path = Morris.Line.createPath(testData, true, 0)
|
||||
path.should.equal 'M0,10C2.5,7.5,7.5,0,10,0C12.5,0,17.5,7.5,20,10'
|
||||
|
||||
it 'should generate a jagged line', ->
|
||||
testData = [{x: 1, y: 1}, {x: 3, y: 1}]
|
||||
chart = Morris.Line(TestDefaults extends {data: testData})
|
||||
path = chart.createPath(testData, false)
|
||||
path.match(/[A-Z]/g).should.deep.equal ['M', 'L']
|
||||
testData = [{x: 0, y: 10}, {x: 10, y: 0}, {x: 20, y: 10}]
|
||||
path = Morris.Line.createPath(testData, false, 0)
|
||||
path.should.equal 'M0,10L10,0L20,10'
|
||||
|
||||
it 'should prevent paths from descending below the bottom of the chart', ->
|
||||
testData = [{x: 0, y: 20}, {x: 10, y: 10}, {x: 20, y: 30}]
|
||||
path = Morris.Line.createPath(testData, true, 10)
|
||||
path.should.equal 'M0,20C2.5,17.5,7.5,10,10,10C12.5,11.25,17.5,25,20,30'
|
||||
|
||||
it 'should break the line at null values', ->
|
||||
testData = [{x: 1, y: 1}, {x: 2, y: null}, {x: 3, y: 1}, {x: 4, y: 1}]
|
||||
chart = Morris.Line(TestDefaults extends {data: testData})
|
||||
path = chart.createPath(testData, true)
|
||||
path.match(/[A-Z]/g).should.deep.equal ['M', 'M', 'C']
|
||||
testData = [{x: 0, y: 10}, {x: 10, y: 0}, {x: 20, y: null}, {x: 30, y: 10}, {x: 40, y: 0}]
|
||||
path = Morris.Line.createPath(testData, true, 0)
|
||||
path.should.equal 'M0,10C2.5,7.5,7.5,2.5,10,0M30,10C32.5,7.5,37.5,2.5,40,0'
|
||||
|
||||
it 'should ignore leading and trailing null values', ->
|
||||
testData = [{x: 0, y: null}, {x: 10, y: 10}, {x: 20, y: 0}, {x: 30, y: 10}, {x: 40, y: null}]
|
||||
path = Morris.Line.createPath(testData, true, 0)
|
||||
path.should.equal 'M10,10C12.5,7.5,17.5,0,20,0C22.5,0,27.5,7.5,30,10'
|
||||
|
Loading…
Reference in New Issue
Block a user