4
\$\begingroup\$

I've made a simple web plot with D3 JS version 4. I've drawn a discontinuous function:

$$f(x)=\frac{1}{x(x-2)}$$

which has 2 vertical asymptotes and 1 horizontal asymptote. I add a legend with LateX:

image of function plot

I'm not a master using JavaScript and D3, so I'd be thankful for any suggestion about a more efficient use with arrays.

var x = d3.range(-4., 4.1, 0.1)

fnorm = x => x == 0 ? (1. / (.09 * (.09 - 2.))) : x == 2 ? (1. / (2.09 * (2.09 - 2))) : (1. / (x * (x - 2)))

var y = new Array()
for (var i = 0 ; i < x.length ; i++) {
    y[i] = fnorm(x[i])
}
var dataset = []
for (var j = 0; j < x.length; j++) {
    dataset[j] = []
    dataset[j][0] = x[j]
    dataset[j][1] = y[j]
}
/*var dataset = []
for (var j = 1; j < x.length; j++) {
    dataset[j] =  {'x1': x[j - 1], 'y1': y[j - 1], 'x2': x[j], 'y2': y[j]}
}*/

var w = 500
var h = 500
var padding = 50

var text_function = 'f(x)=\\dfrac{1}{x(x-2)}'
var latex_render_url = 'http://latex.codecogs.com/gif.latex?'
var latex_query = encodeURI(text_function)

var xScale = d3.scaleLinear()
            .domain([d3.min(x, function(d) { return d }), d3.max(x, function(d) { return d })])
            .range([padding, w - padding])

var yScale = d3.scaleLinear()
             .domain([-6, 5])
             .range([h - padding, padding])

function mycanvas() {
    var svg = d3.select('body')
            .append('svg')
            .attr('width', w)
            .attr('height', h)
    svg.append('rect')
        .attr('width', '100%')
        .attr('height', '100%')
        .style('fill', '#ffcc99')

    // Define the axis
    var xAxis = d3.axisBottom().scale(xScale).ticks(9)
    var yAxis = d3.axisLeft().scale(yScale).ticks(9)

    // Create the axis
    svg.append('g')
        .attr('class', 'axis')
        .attr('transform', 'translate(0,' + (h - padding) + ')')
        .call(xAxis)
    svg.append('g')
        .attr('class', 'axis')
        .attr('transform', 'translate(' + padding + ', 0)')
        .call(yAxis)

    // Define and plotting the function
    var line = d3.line()
        .x(function(d) { return xScale(d[0])})
        .y(function(d) { return yScale(d[1])})

    svg.append('path')
    .attr('d', line(dataset.slice(0, (x.length - 1) / 2)))
    .attr('stroke', 'blue')
    .attr('fill', 'none')
    svg.append('path')
    .attr('d', line(dataset.slice(41, 60)))
    .attr('stroke', 'blue')
    .attr('fill', 'none')
    svg.append('path')
    .attr('d', line(dataset.slice(61, x.length)))
    .attr('stroke', 'blue')
    .attr('fill', 'none')

    // asymptotes
    svg.append('line')
      .attr('x1', xScale(0))
      .attr('y1', yScale(5))
      .attr('x2', xScale(0))
      .attr('y2', yScale(-6))
      .attr('stroke', 'red')
      .attr('stroke-width', 1.5)
      .attr('stroke-dasharray', '10,10')
      .attr('fill', 'none')
    svg.append('line')
      .attr('x1', xScale(2))
      .attr('y1', yScale(5))
      .attr('x2', xScale(2))
      .attr('y2', yScale(-6))
      .attr('stroke', 'red')
      .attr('stroke-width', 1.5)
      .attr('stroke-dasharray', '10, 10')
      .attr('fill', 'none')
    svg.append('line')
      .attr('x1', xScale(-4))
      .attr('y1', yScale(0))
      .attr('x2', xScale(4))
      .attr('y2', yScale(0))
      .attr('stroke', 'green')
      .attr('stroke-width', 1.5)
      .attr('stroke-dasharray', '10, 10')
      .attr('fill', 'none')

    // add legend
    svg.append('line')
      .attr('x1', xScale(-1))
      .attr('y1', yScale(5.75))
      .attr('x2', xScale(-0.7))
      .attr('y2', yScale(5.75))
      .attr('stroke', 'blue')
      .attr('stroke-width', 1.5)
      .attr('fill', 'none')
    svg.append('foreignObject') // We need a foreign object for text latex
        .attr('x', xScale(-0.5))
        .attr('y', yScale(6.25))
        .attr('width', 10)
        .attr('height', 10)
        .attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility')
        .append('xhtml:div')
            .attr('margin', 0)
            .attr('padding', 0)
            .attr('width', 10)
            .attr('height', 10)
        .append('img')
            .attr('src', latex_render_url + latex_query)
}

function main() {
    mycanvas()
}

window.onload = main
<!doctype html>
  <html lang='es' dir='ltr'>
    <head>
        <meta charset='utf-8'>
        <meta http-equiv='X-UA-Compatible' content='IE=edge,chrome=1'>
        <title>Hipérbola</title>
        <meta name='description' content='examples, gaussian'>
        <meta name='viewport' content='width=device-width, initial-scale=1'>

        <script src='https://d3js.org/d3.v4.min.js'></script>
        <script src='hiperbola.js'></script>
        <style>
          * {
            margin: 0 auto;
          }
          svg {
            display: block;
            margin: 100px auto;
          }
        </style>
    </head>
    <body>
    </body>
  </html>

\$\endgroup\$
3
\$\begingroup\$

The problem of using D3 to plot a function is that you are using the wrong tool for the task.

The explanation is simple: despite being a very powerful JS library, D3 is designed to create visualizations based on data, normally qualitative or discrete quantitative data sets.

According to Mike Bostock, D3 creator:

D3 is designed primarily for data visualization, mostly empirical datasets rather than continuous functions, and so there is no built-in method for generating abscissa values. (emphasis mine)

Therefore, when you try to plot a math function using D3, you have to do what you are doing here: creating a data array based on your function, and using D3 to plot that array. The reason for that is that the line generator creates an SVG path based on the data points, drawing straight lines (if you don't set the curve interpolator, more on that below) from one data point to the next one.

The problem with that approach is that, to create a nice, smooth and mathematically accurate line, you have to increase the data array length more and more... For instance, if we do...

var x = d3.range(-4., 4.1, 0.03)

... we will have around 30 data points, and the line will look terrible:

enter image description here

In your code, you have around 80 data points, and the line looks a little better.

If we do...

var x = d3.range(-4., 4.1, 0.03)

... we will have 810 data points (that is, we increased the number of points ten times), and the line will look way better:

enter image description here

Here is the running code with that change:

var x = d3.range(-4., 4.1, 0.01)

fnorm = x => x == 0 ? (1. / (.09 * (.09 - 2.))) : x == 2 ? (1. / (2.09 * (2.09 - 2))) : (1. / (x * (x - 2)))

var y = new Array()
for (var i = 0 ; i < x.length ; i++) {
    y[i] = fnorm(x[i])
}
var dataset = []
for (var j = 0; j < x.length; j++) {
    dataset[j] = []
    dataset[j][0] = x[j]
    dataset[j][1] = y[j]
}
/*var dataset = []
for (var j = 1; j < x.length; j++) {
    dataset[j] =  {'x1': x[j - 1], 'y1': y[j - 1], 'x2': x[j], 'y2': y[j]}
}*/

var w = 500
var h = 500
var padding = 50

var text_function = 'f(x)=\\dfrac{1}{x(x-2)}'
var latex_render_url = 'http://latex.codecogs.com/gif.latex?'
var latex_query = encodeURI(text_function)

var xScale = d3.scaleLinear()
            .domain([d3.min(x, function(d) { return d }), d3.max(x, function(d) { return d })])
            .range([padding, w - padding])

var yScale = d3.scaleLinear()
             .domain([-6, 5])
             .range([h - padding, padding])

function mycanvas() {
    var svg = d3.select('body')
            .append('svg')
            .attr('width', w)
            .attr('height', h)
    svg.append('rect')
        .attr('width', '100%')
        .attr('height', '100%')
        .style('fill', '#ffcc99')

    // Define the axis
    var xAxis = d3.axisBottom().scale(xScale).ticks(9)
    var yAxis = d3.axisLeft().scale(yScale).ticks(9)

    // Create the axis
    svg.append('g')
        .attr('class', 'axis')
        .attr('transform', 'translate(0,' + (h - padding) + ')')
        .call(xAxis)
    svg.append('g')
        .attr('class', 'axis')
        .attr('transform', 'translate(' + padding + ', 0)')
        .call(yAxis)

    // Define and plotting the function
    var line = d3.line()
        .x(function(d) { return xScale(d[0])})
        .y(function(d) { return yScale(d[1])})

    svg.append('path')
    .attr('d', line(dataset.slice(0, (x.length - 1) / 2)))
    .attr('stroke', 'blue')
    .attr('fill', 'none')
    svg.append('path')
    .attr('d', line(dataset.slice(41, 60)))
    .attr('stroke', 'blue')
    .attr('fill', 'none')
    svg.append('path')
    .attr('d', line(dataset.slice(61, x.length)))
    .attr('stroke', 'blue')
    .attr('fill', 'none')

    // asymptotes
    svg.append('line')
      .attr('x1', xScale(0))
      .attr('y1', yScale(5))
      .attr('x2', xScale(0))
      .attr('y2', yScale(-6))
      .attr('stroke', 'red')
      .attr('stroke-width', 1.5)
      .attr('stroke-dasharray', '10,10')
      .attr('fill', 'none')
    svg.append('line')
      .attr('x1', xScale(2))
      .attr('y1', yScale(5))
      .attr('x2', xScale(2))
      .attr('y2', yScale(-6))
      .attr('stroke', 'red')
      .attr('stroke-width', 1.5)
      .attr('stroke-dasharray', '10, 10')
      .attr('fill', 'none')
    svg.append('line')
      .attr('x1', xScale(-4))
      .attr('y1', yScale(0))
      .attr('x2', xScale(4))
      .attr('y2', yScale(0))
      .attr('stroke', 'green')
      .attr('stroke-width', 1.5)
      .attr('stroke-dasharray', '10, 10')
      .attr('fill', 'none')

    // add legend
    svg.append('line')
      .attr('x1', xScale(-1))
      .attr('y1', yScale(5.75))
      .attr('x2', xScale(-0.7))
      .attr('y2', yScale(5.75))
      .attr('stroke', 'blue')
      .attr('stroke-width', 1.5)
      .attr('fill', 'none')
    svg.append('foreignObject') // We need a foreign object for text latex
        .attr('x', xScale(-0.5))
        .attr('y', yScale(6.25))
        .attr('width', 10)
        .attr('height', 10)
        .attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility')
        .append('xhtml:div')
            .attr('margin', 0)
            .attr('padding', 0)
            .attr('width', 10)
            .attr('height', 10)
        .append('img')
            .attr('src', latex_render_url + latex_query)
}

function main() {
    mycanvas()
}

window.onload = main
<!doctype html>
  <html lang='es' dir='ltr'>
    <head>
        <meta charset='utf-8'>
        <meta http-equiv='X-UA-Compatible' content='IE=edge,chrome=1'>
        <title>Hipérbola</title>
        <meta name='description' content='examples, gaussian'>
        <meta name='viewport' content='width=device-width, initial-scale=1'>

        <script src='https://d3js.org/d3.v4.min.js'></script>
        <script src='hiperbola.js'></script>
        <style>
          * {
            margin: 0 auto;
          }
          svg {
            display: block;
            margin: 100px auto;
          }
        </style>
    </head>
    <body>
    </body>
  </html>

However, of course, such approach is definitely not a smart approach: instead of simply plotting a line based on a function, you are plotting a line based on a huge array of data, wasting resources.

An alternative is interpolating between the points using D3 curves. Doing that, you can substantially reduce the number of data points, and the curve will still look nice. The reason is that, using a D3 curve, you don't have straight lines between a point and the next one anymore, but a cubic Bézier curve.

For instance, using a d3.curveBasis:

var line = d3.line().curve(d3.curveBasis)

And here is your code with it, the same 80 data points:

var x = d3.range(-4., 4.1, 0.1)

fnorm = x => x == 0 ? (1. / (.09 * (.09 - 2.))) : x == 2 ? (1. / (2.09 * (2.09 - 2))) : (1. / (x * (x - 2)))

var y = new Array()
for (var i = 0 ; i < x.length ; i++) {
    y[i] = fnorm(x[i])
}
var dataset = []
for (var j = 0; j < x.length; j++) {
    dataset[j] = []
    dataset[j][0] = x[j]
    dataset[j][1] = y[j]
}
/*var dataset = []
for (var j = 1; j < x.length; j++) {
    dataset[j] =  {'x1': x[j - 1], 'y1': y[j - 1], 'x2': x[j], 'y2': y[j]}
}*/

var w = 500
var h = 500
var padding = 50

var text_function = 'f(x)=\\dfrac{1}{x(x-2)}'
var latex_render_url = 'http://latex.codecogs.com/gif.latex?'
var latex_query = encodeURI(text_function)

var xScale = d3.scaleLinear()
            .domain([d3.min(x, function(d) { return d }), d3.max(x, function(d) { return d })])
            .range([padding, w - padding])

var yScale = d3.scaleLinear()
             .domain([-6, 5])
             .range([h - padding, padding])

function mycanvas() {
    var svg = d3.select('body')
            .append('svg')
            .attr('width', w)
            .attr('height', h)
    svg.append('rect')
        .attr('width', '100%')
        .attr('height', '100%')
        .style('fill', '#ffcc99')

    // Define the axis
    var xAxis = d3.axisBottom().scale(xScale).ticks(9)
    var yAxis = d3.axisLeft().scale(yScale).ticks(9)

    // Create the axis
    svg.append('g')
        .attr('class', 'axis')
        .attr('transform', 'translate(0,' + (h - padding) + ')')
        .call(xAxis)
    svg.append('g')
        .attr('class', 'axis')
        .attr('transform', 'translate(' + padding + ', 0)')
        .call(yAxis)

    // Define and plotting the function
    var line = d3.line()
        .x(function(d) { return xScale(d[0])})
        .y(function(d) { return yScale(d[1])})
        .curve(d3.curveBasis)

    svg.append('path')
    .attr('d', line(dataset.slice(0, (x.length - 1) / 2)))
    .attr('stroke', 'blue')
    .attr('fill', 'none')
    svg.append('path')
    .attr('d', line(dataset.slice(41, 60)))
    .attr('stroke', 'blue')
    .attr('fill', 'none')
    svg.append('path')
    .attr('d', line(dataset.slice(61, x.length)))
    .attr('stroke', 'blue')
    .attr('fill', 'none')

    // asymptotes
    svg.append('line')
      .attr('x1', xScale(0))
      .attr('y1', yScale(5))
      .attr('x2', xScale(0))
      .attr('y2', yScale(-6))
      .attr('stroke', 'red')
      .attr('stroke-width', 1.5)
      .attr('stroke-dasharray', '10,10')
      .attr('fill', 'none')
    svg.append('line')
      .attr('x1', xScale(2))
      .attr('y1', yScale(5))
      .attr('x2', xScale(2))
      .attr('y2', yScale(-6))
      .attr('stroke', 'red')
      .attr('stroke-width', 1.5)
      .attr('stroke-dasharray', '10, 10')
      .attr('fill', 'none')
    svg.append('line')
      .attr('x1', xScale(-4))
      .attr('y1', yScale(0))
      .attr('x2', xScale(4))
      .attr('y2', yScale(0))
      .attr('stroke', 'green')
      .attr('stroke-width', 1.5)
      .attr('stroke-dasharray', '10, 10')
      .attr('fill', 'none')

    // add legend
    svg.append('line')
      .attr('x1', xScale(-1))
      .attr('y1', yScale(5.75))
      .attr('x2', xScale(-0.7))
      .attr('y2', yScale(5.75))
      .attr('stroke', 'blue')
      .attr('stroke-width', 1.5)
      .attr('fill', 'none')
    svg.append('foreignObject') // We need a foreign object for text latex
        .attr('x', xScale(-0.5))
        .attr('y', yScale(6.25))
        .attr('width', 10)
        .attr('height', 10)
        .attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility')
        .append('xhtml:div')
            .attr('margin', 0)
            .attr('padding', 0)
            .attr('width', 10)
            .attr('height', 10)
        .append('img')
            .attr('src', latex_render_url + latex_query)
}

function main() {
    mycanvas()
}

window.onload = main
<!doctype html>
  <html lang='es' dir='ltr'>
    <head>
        <meta charset='utf-8'>
        <meta http-equiv='X-UA-Compatible' content='IE=edge,chrome=1'>
        <title>Hipérbola</title>
        <meta name='description' content='examples, gaussian'>
        <meta name='viewport' content='width=device-width, initial-scale=1'>

        <script src='https://d3js.org/d3.v4.min.js'></script>
        <script src='hiperbola.js'></script>
        <style>
          * {
            margin: 0 auto;
          }
          svg {
            display: block;
            margin: 100px auto;
          }
        </style>
    </head>
    <body>
    </body>
  </html>

As you can see, you have better, smoother lines. Here is an image comparing your plot (without a curve interpolation), to the left, and the code using a curve interpolation, to the right:

enter image description here

In the interpolated path (right) there is no more the straight lines and kinks present in the normal path (left).

Also, it's worth mentioning that there are some libraries to plot functions using D3, as this one. According to its creator, it still uses the data array approach, but in a cleaver way:

Function Plot, unlike other plotters that use n-equally spaced points joined by line segments, uses interval-arithmetic to correctly determine sections of the screen that need to be plotted with a few samples.

Conclusion:

To increase the accuracy of the plot and improve the aspect of the line, you can increase the number of data points or, alternatively, finding a D3 curve that suits your plot.

Unfortunately, the best review advice is this: if you plan to plot math functions, do not use D3.

\$\endgroup\$
  • 1
    \$\begingroup\$ Ok, thanks a lot for your answer. But what library is perfect to plot math functions for html5 web-pages? I haven't found any library. I'm a mathematician and I've used matplotlib, mathematica, gnuplot, octave, matlab, etc etc and all plots are drawn with a set of points, and when you've got more points more exact is your math function plot. \$\endgroup\$ – Tobal Aug 4 '17 at 15:30
  • \$\begingroup\$ Thanks, I've tried function-plot during all the morning but it doesn't work, It's a discontinued and old project. And the documentation is very poor. \$\endgroup\$ – Tobal Aug 5 '17 at 10:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.