Home > Software engineering >  Why d3 line generator fails with the usage of scale around null values
Why d3 line generator fails with the usage of scale around null values

Time:06-09

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>

  • Related