I am trying to understand why d3.line
behaves differently around null values when combined with scale.
To elaborate, my dataset looks like this and it will always contain some null
const data = [
{ "x": 50, "y": 97.04013083865155 },
{ "x": 100, "y": null },
{ "x": 150, "y": 98.62594214598816 },
{ "x": 200, "y": 76.49419950954189 },
{ "x": 250, "y": 29.30639006661442 },
{ "x": 300, "y": 29.366842697148176 },
{ "x": 350, "y": 51.587600132998325 },
{ "x": 400, "y": null },
{ "x": 450, "y": null },
{ "x": 500, "y": 26.90860254816283 },
{ "x": 550, "y": null },
{ "x": 600, "y": 99.1622268038577 }
]
With Scale null
If I want to generate a line, and not use scale
, I get the following
////////////////////////////////////////////////////////////
//////////////////////// 1 DATA ///////////////////////////
////////////////////////////////////////////////////////////
const data = [
{ "x": 50, "y": 97.04013083865155 },
{ "x": 100, "y": null },
{ "x": 150, "y": 98.62594214598816 },
{ "x": 200, "y": 76.49419950954189 },
{ "x": 250, "y": 29.30639006661442 },
{ "x": 300, "y": 29.366842697148176 },
{ "x": 350, "y": 51.587600132998325 },
{ "x": 400, "y": null },
{ "x": 450, "y": null },
{ "x": 500, "y": 26.90860254816283 },
{ "x": 550, "y": null },
{ "x": 600, "y": 99.1622268038577 }
]
height = 400,
width = 720;
padding = {
top: 70,
bottom: 50,
left: 70,
right: 70
}
const boundHeight = height - padding.top - padding.bottom;
const boundWidth = width - padding.right - padding.left;
////////////////////////////////////////////////////////////
//////////////////////// 2 CREATE SCALE ////////////////////
////////////////////////////////////////////////////////////
const scaleX = d3.scaleLinear()
.range([0, boundWidth])
.domain(d3.extent(data, d => d.x))
const scaleY = d3.scaleLinear()
.range([boundHeight, 0])
.domain(d3.extent(data, d => d.y))
////////////////////////////////////////////////////////////
//////////////////////// 3 SVG// ///////////////////////////
////////////////////////////////////////////////////////////
const svgns = 'http://www.w3.org/2000/svg'
const svg = d3.select('svg')
svg
.attr('xmlns', svgns)
.attr('viewBox', `0 0 ${width} ${height}`)
svg.append('rect')
.attr('class', 'vBoxRect')
.style("overflow", "visible")
.attr('width', `${width}`)
.attr('height', `${height}`)
.attr('stroke', 'red')
.attr('fill', 'none')
//create BOUND rect -- to be deleted later
svg.append('rect')
.attr('class', 'boundRect')
.attr('x', `${padding.left}`)
.attr('y', `${padding.top}`)
.attr('width', `${boundWidth}`)
.attr('height', `${boundHeight}`)
.attr('fill', 'none')
.attr('stroke', 'black')
//create bound element
bound = svg.append('g')
.attr('class', 'bound')
.style('transform', `translate(${padding.left}px,${padding.top}px)`)
//constrcuct line generators
////////////////////////////////////////////////////////////
////////////////////////NO SCALE// /////////////////////////
////////////////////////////////////////////////////////////
noScale = d3.line()
.x(d => d.x)
.y(d => d.y)
(data)
//no Scale
bound.append('path')
.attr('class', 'Black-noScale')
.attr('d', noScale)
.attr('fill', 'none')
.attr('stroke', 'black')
.attr('stroke-width', '2')
console.log(noScale)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>
<body>
<svg>
</svg>
<div id="container" ></div>
<script src="next.js"></script>
</body>
</html>
No Scale null
However, if I want to generate a line, and use scale
, the generator fails
////////////////////////////////////////////////////////////
//////////////////////// 1 DATA ///////////////////////////
////////////////////////////////////////////////////////////
const data = [
{ "x": 50, "y": 97.04013083865155 },
{ "x": 100, "y": null },
{ "x": 150, "y": 98.62594214598816 },
{ "x": 200, "y": 76.49419950954189 },
{ "x": 250, "y": 29.30639006661442 },
{ "x": 300, "y": 29.366842697148176 },
{ "x": 350, "y": 51.587600132998325 },
{ "x": 400, "y": null },
{ "x": 450, "y": null },
{ "x": 500, "y": 26.90860254816283 },
{ "x": 550, "y": null },
{ "x": 600, "y": 99.1622268038577 }
]
height = 400,
width = 720;
padding = {
top: 70,
bottom: 50,
left: 70,
right: 70
}
const boundHeight = height - padding.top - padding.bottom;
const boundWidth = width - padding.right - padding.left;
////////////////////////////////////////////////////////////
//////////////////////// 2 CREATE SCALE ////////////////////
////////////////////////////////////////////////////////////
const scaleX = d3.scaleLinear()
.range([0, boundWidth])
.domain(d3.extent(data, d => d.x))
const scaleY = d3.scaleLinear()
.range([boundHeight, 0])
.domain(d3.extent(data, d => d.y))
////////////////////////////////////////////////////////////
//////////////////////// 3 SVG// ///////////////////////////
////////////////////////////////////////////////////////////
const svgns = 'http://www.w3.org/2000/svg'
const svg = d3.select('svg')
svg
.attr('xmlns', svgns)
.attr('viewBox', `0 0 ${width} ${height}`)
svg.append('rect')
.attr('class', 'vBoxRect')
.style("overflow", "visible")
.attr('width', `${width}`)
.attr('height', `${height}`)
.attr('stroke', 'red')
.attr('fill', 'none')
//create BOUND rect -- to be deleted later
svg.append('rect')
.attr('class', 'boundRect')
.attr('x', `${padding.left}`)
.attr('y', `${padding.top}`)
.attr('width', `${boundWidth}`)
.attr('height', `${boundHeight}`)
.attr('fill', 'none')
.attr('stroke', 'black')
//create bound element
bound = svg.append('g')
.attr('class', 'bound')
.style('transform', `translate(${padding.left}px,${padding.top}px)`)
//constrcuct line generators
////////////////////////////////////////////////////////////
////////////////////////WITH SCALE// ////////////////////////
////////////////////////////////////////////////////////////
withScale = d3.line()
.x(d => scaleX(d.x))
.y(d => scaleY(d.y))
(data)
bound.append('path')
.attr('class', 'Orange-withScale')
.attr('d', withScale)
.attr('fill', 'none')
.attr('stroke', 'orange')
.attr('stroke-width', '2')
console.log(withScale);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>
<body>
<svg>
</svg>
<div id="container" ></div>
<script src="next.js"></script>
</body>
</html>
On the other hand, the genrator works well with defined whether scale used or not
////////////////////////////////////////////////////////////
//////////////////////// 1 DATA ///////////////////////////
////////////////////////////////////////////////////////////
const data = [
{ "x": 50, "y": 97.04013083865155 },
{ "x": 100, "y": null },
{ "x": 150, "y": 98.62594214598816 },
{ "x": 200, "y": 76.49419950954189 },
{ "x": 250, "y": 29.30639006661442 },
{ "x": 300, "y": 29.366842697148176 },
{ "x": 350, "y": 51.587600132998325 },
{ "x": 400, "y": null },
{ "x": 450, "y": null },
{ "x": 500, "y": 26.90860254816283 },
{ "x": 550, "y": null },
{ "x": 600, "y": 99.1622268038577 }
]
height = 400,
width = 720;
padding = {
top: 70,
bottom: 50,
left: 70,
right: 70
}
const boundHeight = height - padding.top - padding.bottom;
const boundWidth = width - padding.right - padding.left;
////////////////////////////////////////////////////////////
//////////////////////// 2 CREATE SCALE ////////////////////
////////////////////////////////////////////////////////////
const scaleX = d3.scaleLinear()
.range([0, boundWidth])
.domain(d3.extent(data, d => d.x))
const scaleY = d3.scaleLinear()
.range([boundHeight, 0])
.domain(d3.extent(data, d => d.y))
////////////////////////////////////////////////////////////
//////////////////////// 3 SVG// ///////////////////////////
////////////////////////////////////////////////////////////
const svgns = 'http://www.w3.org/2000/svg'
const svg = d3.select('svg')
svg
.attr('xmlns', svgns)
.attr('viewBox', `0 0 ${width} ${height}`)
svg.append('rect')
.attr('class', 'vBoxRect')
.style("overflow", "visible")
.attr('width', `${width}`)
.attr('height', `${height}`)
.attr('stroke', 'red')
.attr('fill', 'none')
//create BOUND rect -- to be deleted later
svg.append('rect')
.attr('class', 'boundRect')
.attr('x', `${padding.left}`)
.attr('y', `${padding.top}`)
.attr('width', `${boundWidth}`)
.attr('height', `${boundHeight}`)
.attr('fill', 'none')
.attr('stroke', 'black')
//create bound element
bound = svg.append('g')
.attr('class', 'bound')
.style('transform', `translate(${padding.left}px,${padding.top}px)`)
//constrcuct line generators
////////////////////////////////////////////////////////////
////////////////////////NO SCALE// /////////////////////////
////////////////////////////////////////////////////////////
noScaleWithDefinedFilter = d3.line()
.x(d => d.x)
.y(d => d.y)
.defined(d => d.y)
(data.filter((a) => a.y !== null))
noScaleWithDefined = d3.line()
.x(d => d.x)
.y(d => d.y)
.defined(d => d.y)
(data)
//shows complete line
bound.append('path')
.attr('class', 'Violet-noScale defined filter')
.attr('d', noScaleWithDefinedFilter)
.attr('fill', 'none')
.attr('stroke', 'violet')
.attr('stroke-width', '2')
.style('transform', 'translateY(50px)')
//does not show null Y
bound.append('path')
.attr('class', 'Red-noScale defined')
.attr('d', noScaleWithDefined)
.attr('fill', 'none')
.attr('stroke', 'red')
.attr('stroke-width', '2')
.style('transform', 'translateY(75px)')
////////////////////////////////////////////////////////////
////////////////////////WITH SCALE// /////////////////////////
////////////////////////////////////////////////////////////
withScaleWithDefined = d3.line()
.x(d => scaleX(d.x))
.y(d => scaleY(d.y))
.defined(d => d.y)
(data)
withScaleWithDefinedFilter = d3.line()
.x(d => scaleX(d.x))
.y(d => scaleY(d.y))
.defined(d => d.y)
(data.filter((a) => a.y !== null))
bound.append('path')
.attr('class', 'Salmon-withScale Defined Filter')
.attr('d', withScaleWithDefinedFilter)
.attr('fill', 'none')
.attr('stroke', 'salmon')
.attr('stroke-width', '2')
bound.append('path')
.attr('class', 'Blue-withScale Defined')
.attr('d', withScaleWithDefined)
.attr('fill', 'none')
.attr('stroke', 'blue')
.attr('stroke-width', '2')
.style('transform', 'translateY(-25px)')
console.log(noScaleWithDefinedFilter);
console.log(noScaleWithDefined);
console.log(withScaleWithDefined);
console.log(withScaleWithDefinedFilter);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>
<body>
<svg>
</svg>
<div id="container" ></div>
<script src="next.js"></script>
</body>
</html>
How can I achieve, what the following does
const noScale = d3.line()
.x(d => d.x)
.y(d => d.y)
(data)
but with scaling
const withScale = d3.line()
.x(d => scaleX(d.x))
.y(d => scaleY(d.y))
(data)
Can this be achieved without destroying the data?
CodePudding user response:
This is by design and documented: if you pass a null
value to that linear scale it will return undefined
, and obviously you cannot do anything with undefined
inside the d
attribute.
The solution for the scale itself is using unknown
, which sets the unknown returned value to any value you want, for instance zero:
const scaleY = d3.scaleLinear()
.unknown(0)
etc...
If, on the other hand, you want to skip the null values, you have to change the line generator itself with defined
, not the scale.
Here's your code with unknown(0)
:
////////////////////////////////////////////////////////////
//////////////////////// 1 DATA ///////////////////////////
////////////////////////////////////////////////////////////
const data = [
{ "x": 50, "y": 97.04013083865155 },
{ "x": 100, "y": null },
{ "x": 150, "y": 98.62594214598816 },
{ "x": 200, "y": 76.49419950954189 },
{ "x": 250, "y": 29.30639006661442 },
{ "x": 300, "y": 29.366842697148176 },
{ "x": 350, "y": 51.587600132998325 },
{ "x": 400, "y": null },
{ "x": 450, "y": null },
{ "x": 500, "y": 26.90860254816283 },
{ "x": 550, "y": null },
{ "x": 600, "y": 99.1622268038577 }
]
height = 400,
width = 720;
padding = {
top: 70,
bottom: 50,
left: 70,
right: 70
}
const boundHeight = height - padding.top - padding.bottom;
const boundWidth = width - padding.right - padding.left;
////////////////////////////////////////////////////////////
//////////////////////// 2 CREATE SCALE ////////////////////
////////////////////////////////////////////////////////////
const scaleX = d3.scaleLinear()
.range([0, boundWidth])
.domain(d3.extent(data, d => d.x))
const scaleY = d3.scaleLinear()
.unknown(0)
.range([boundHeight, 0])
.domain(d3.extent(data, d => d.y))
////////////////////////////////////////////////////////////
//////////////////////// 3 SVG// ///////////////////////////
////////////////////////////////////////////////////////////
const svgns = 'http://www.w3.org/2000/svg'
const svg = d3.select('svg')
svg
.attr('xmlns', svgns)
.attr('viewBox', `0 0 ${width} ${height}`)
svg.append('rect')
.attr('class', 'vBoxRect')
.style("overflow", "visible")
.attr('width', `${width}`)
.attr('height', `${height}`)
.attr('stroke', 'red')
.attr('fill', 'none')
//create BOUND rect -- to be deleted later
svg.append('rect')
.attr('class', 'boundRect')
.attr('x', `${padding.left}`)
.attr('y', `${padding.top}`)
.attr('width', `${boundWidth}`)
.attr('height', `${boundHeight}`)
.attr('fill', 'none')
.attr('stroke', 'black')
//create bound element
bound = svg.append('g')
.attr('class', 'bound')
.style('transform', `translate(${padding.left}px,${padding.top}px)`)
//constrcuct line generators
////////////////////////////////////////////////////////////
////////////////////////WITH SCALE// ////////////////////////
////////////////////////////////////////////////////////////
withScale = d3.line()
.x(d => scaleX(d.x))
.y(d => scaleY(d.y))
(data)
bound.append('path')
.attr('class', 'Orange-withScale')
.attr('d', withScale)
.attr('fill', 'none')
.attr('stroke', 'orange')
.attr('stroke-width', '2')
console.log(withScale);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>
<body>
<svg>
</svg>
<div id="container" ></div>
<script src="next.js"></script>
</body>
</html>