Tuesday, 29 May 2018

D3 custom curve: bundle interpolation for areas

Consider this D3JS graph which uses a basis interpolation:

D3JS v3, basis interpolation

In D3JS v3, I could use bundle interpolation (.interpolate("bundle").tension(0)) on areas to achieve this type of rendering instead:

D3JS v3, bundle interpolation

Notice how each segment of the graph fits nicely with its neighbors. This is what I need.

With D3JS v4 and v5, the syntax for bundle interpolation is now this: .curve(d3.curveBundle). However, it's now "intended to work with d3.line, not d3.area."

I recently upgraded from v3 to v5, and so I'm trying to create a custom bundle curve that will work with areas too, to keep the interpolation type I enjoyed with v3.

I'm very close. This is what I have so far:

///////////////////// Custom curves.

/** Bundle-ish.
 * Trying to adapt curveBundle for use with areas…
 */
function myBundle(context, beta) {
        this._basis = new d3.curveBasis(context);
        this._beta = beta;

        this._context = context; // temporary. shouldn't be needed for bundle.
}
myBundle.prototype = {

        areaStart: function() {
                this._line = 0;
        },
        areaEnd: function() {
                this._line = NaN;
        },
        lineStart: function() {
                this._x = [];
                this._y = [];
                this._basis.lineStart();
        },
        lineEnd: function() {
                var x = this._x,
                                y = this._y,
                                j = x.length - 1;

                if (j > 0) {
                        var x0 = x[0],
                                        y0 = y[0],
                                        dx = x[j] - x0,
                                        dy = y[j] - y0,
                                        i = -1,
                                        t;

                        while (++i <= j) {
                                t = i / j;
                                this._basis.point(
                                        this._beta * x[i] + (1 - this._beta) * (x0 + t * dx),
                                        this._beta * y[i] + (1 - this._beta) * (y0 + t * dy)
                                );
                        }
                }

                this._x = this._y = null;
                this._basis.lineEnd();
        },
        point: function(x, y) {
                this._x.push(+x);
                this._y.push(+y);
                // console.log( this._x.push(+x), this._y.push(+y) );
        }
};
myCurveBundle = (function custom(beta) {
        function myCurveBundle(context) {
                return beta === 1 ? new myBasis(context) : new myBundle(context, beta);
        }

        myCurveBundle.beta = function(beta) {
                return custom(+beta);
        };

        return myCurveBundle;
})(0.85);






///////////////////// The chart.

        var width = 960;
        var height = 540;
        var data = [];
        data.prosody = [116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.473, 116.578, 125.552, 134.888, 144.225, 153.561, 162.898, 172.235, 181.571, 190.908, 200.244, 209.581, 218.917, 227.715, 218.849, 209.591, 200.333, 191.076, 181.818, 172.560, 163.302, 154.044, 144.787, 135.529, 126.271, 117.013, 107.755, 98.498, 89.240, 97.511, 118.857, 140.202, 161.547, 182.893, 192.100, 188.997, 185.895, 182.792, 179.690, 176.587, 173.485, 170.382, 167.280, 164.177, 161.075, 157.972, 154.870, 151.767, 148.665, 145.562, 142.460, 139.357, 136.255, 133.152, 130.050, 126.947, 124.244, 122.275, 120.307, 118.338, 116.369, 114.400, 112.431, 110.462, 108.493, 106.524, 104.555, 102.586, 100.617, 98.648, 99.659, 101.531, 103.402, 105.273, 107.145, 109.016, 110.887, 112.758, 114.630, 116.501, 118.372, 120.244, 122.115, 123.986, 125.857, 127.729, 129.600, 131.471, 133.343, 135.214, 137.085, 138.956, 140.828, 142.699, 144.570, 146.442, 148.313, 150.184, 149.175, 146.384, 143.594, 140.803, 138.013, 135.222, 132.432, 129.642, 126.851, 124.061, 121.270, 118.480, 115.689, 112.899, 110.109, 107.318, 104.528, 101.737, 98.947, 96.156, 93.366, 90.576, 87.785, 84.995, 82.204, 79.414, 76.623, 0, 0, 0, 0, 0, 0, 76.601, 78.414, 80.227, 82.041, 83.854, 85.667, 87.480, 89.294, 91.107, 92.920, 94.733, 96.547, 98.360, 100.173, 101.986, 103.800, 105.613, 107.426, 109.239, 111.053, 112.866, 114.679, 116.492, 115.917, 114.338, 112.760, 111.181, 109.602, 108.023, 106.444, 104.865, 103.286, 101.707, 100.128, 98.549, 96.970, 95.391, 93.812, 92.233, 90.654, 89.075, 87.534, 88.055, 88.646, 89.237, 89.827, 90.418, 91.009, 91.600, 92.191, 92.782, 93.373, 93.964, 94.555, 95.146, 95.737, 96.328, 96.919, 97.509, 98.100, 98.691, 99.282, 99.873, 100.062, 98.230, 96.399, 94.567, 92.736, 90.904, 89.072, 87.241, 85.409, 83.578, 81.746, 79.914, 78.083, 78.839, 80.880, 82.922, 84.964, 87.006, 89.048, 91.090, 93.132, 95.174, 97.216, 99.257, 101.299, 103.341, 105.383, 107.425, 109.467, 111.509, 113.551, 112.633, 110.755, 108.877, 106.999, 105.121, 103.243, 101.365, 99.487, 97.609, 95.731, 93.853, 91.975, 90.097, 88.219, 86.341, 84.463, 82.585, 80.707, 78.829, 76.951, 78.067, 81.290, 84.513, 87.736, 90.958, 94.181, 97.404, 100.627, 103.849, 107.072, 110.295, 113.517, 116.740, 119.963, 123.186, 126.408, 129.631, 132.854, 136.077, 139.299, 142.522, 145.745, 148.968, 152.190, 155.413, 154.840, 152.899, 150.958, 149.017, 147.076, 145.135, 143.194, 141.253, 139.312, 137.371, 135.429, 133.488, 131.547, 129.606, 127.665, 125.724, 124.874, 126.734, 128.594, 130.454, 132.314, 134.174, 136.034, 137.894, 139.754, 141.614, 143.474, 145.334, 147.194, 149.054, 150.914, 152.774, 154.634, 156.494, 158.354, 160.214, 162.074, 163.934, 165.664, 161.795, 157.761, 153.726, 149.692, 145.658, 141.624, 137.589, 133.555, 129.521, 125.487, 121.452, 117.418, 113.384, 109.350, 105.316, 101.281, 97.247, 93.213, 89.179, 85.144, 81.110, 77.076, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
        data.TextGrid = {  "phone" : [ /** segment type, beginning, and end of each segment **/ [ "sp", 0.0124716553288, 0.271882086168 ], [ "M", 0.271882086168, 0.401587301587 ], [ "OW", 0.401587301587, 0.521315192744 ], [ "S", 0.521315192744, 0.660997732426 ], [ "T", 0.660997732426, 0.710884353741 ], [ "AH", 0.710884353741, 0.760770975057 ], [ "V", 0.760770975057, 0.820634920635 ], [ "DH", 0.820634920635, 0.860544217687 ], [ "IY", 0.860544217687, 0.940362811791 ], [ "AH", 0.940362811791, 0.980272108844 ], [ "D", 0.980272108844, 1.04013605442 ], [ "V", 1.04013605442, 1.10997732426 ], [ "EH", 1.10997732426, 1.21972789116 ], [ "N", 1.21972789116, 1.289569161 ], [ "CH", 1.289569161, 1.42925170068 ], [ "ER", 1.42925170068, 1.51904761905 ], [ "Z", 1.51904761905, 1.57891156463 ], [ "R", 1.57891156463, 1.66870748299 ], [ "AH", 1.66870748299, 1.69863945578 ], [ "K", 1.69863945578, 1.75850340136 ], [ "AO", 1.75850340136, 1.88820861678 ], [ "R", 1.88820861678, 1.91814058957 ], [ "D", 1.91814058957, 1.95804988662 ], [ "AH", 1.95804988662, 1.99795918367 ], [ "D", 1.99795918367, 2.07777777778 ], [ "AH", 2.07777777778, 2.10770975057 ], [ "N", 2.10770975057, 2.18752834467 ], [ "DH", 2.18752834467, 2.22743764172 ], [ "AH", 2.22743764172, 2.2873015873 ], [ "S", 2.2873015873, 2.42698412698 ], [ "B", 2.42698412698, 2.51678004535 ], [ "UH", 2.51678004535, 2.68639455782 ], [ "K", 2.68639455782, 2.79614512472 ], [ "sp", 2.79614512472, 2.81609977324 ], [ "R", 2.81609977324, 2.95578231293 ], [ "IY", 2.95578231293, 3.00566893424 ], [ "L", 3.00566893424, 3.09546485261 ], [ "IY", 3.09546485261, 3.23514739229 ], [ "AH", 3.23514739229, 3.27505668934 ], [ "K", 3.27505668934, 3.41473922902 ], [ "ER", 3.41473922902, 3.68412698413 ], [ "D", 3.68412698413, 3.75396825397 ], [ "sp", 3.75396825397, 4.01337868481 ] ] }


        /**
         * Set up D3JS
         */
        var x = d3.scaleLinear()
                .domain([0, 401])
                .range([0, width]);
        var y = d3.scaleLinear()
                .domain([0, 800])
                .range([height, 0]);

        /** Center the stream vertically **/
        var shift = d3.scaleLinear()
                .domain([0, 0])
                .range([-height/2, 0]);

        /** Draw a stream segment **/
        var pathGenerator = d3.area()
                .curve( myCurveBundle.beta(0) )
                .x(function(d, i) { return x(i); })
                .y1(function(d) { return y(d + 72 ); }) /** 72 is just some arbitrary thickess given to the graph **/
                .y0(function(d) { return y(d); });

        var svg = d3.select("body").append("svg")
                .attr("width", width)
                .attr("height", height);



        /**
         * Render the chart
         */
        
        /** Draw the stream, on a per-segment basis **/
        var path = svg.selectAll("path")
                .data(data.TextGrid.phone)
                .enter().append("path")
                        .attr("transform", function(d, i) { return "translate(" + x(Math.floor(d[1]*100)) + ", " + shift(i) + ")"; })
                        .attr("class", function(d) { return "segment " + d[0]; })
                        .on('click', function(d, i) { playFromTo(Math.floor(d[1] * 1000), Math.floor(d[2] * 1000)); })
                        .attr("d", function(d) { return pathGenerator(data.prosody.slice( Math.floor(d[1]*100), Math.floor(d[2]*100)+1)); });
.segment { fill: #ccc; }
.segment.sp { display: none; }

/** Adapted from Margaret Horrigan for American English **/
.segment.IY { fill: #7AC141; }
.segment.IH { fill: #F9C5DC; }
.segment.UH { fill: #FF00FF; }
.segment.UW { fill: #0153A5; }
.segment.EY { fill: #8B8C90; }
.segment.EH { fill: #E61A25; }
.segment.AX { fill: #DF5435; }
.segment.ER { fill: #805EAA; }
.segment.AO { fill: #E2A856; }
.segment.OY { fill: #2E3094; }
.segment.OW { fill: #FC2B1C; }
.segment.AE { fill: #21201E; }
.segment.AH { fill: #DF5435; }
.segment.AA { fill: #bf181f; }
.segment.AY { fill: #FFFFFF; }
.segment.AW { fill: #7C4540; }
<script src="https://cdn.jsdelivr.net/npm/d3@5.4.0/dist/d3.min.js"></script>

(The bundle code is adapted from bundle.js in d3-shape.)

I'm very close: if you inspect the SVG, you'll see that, even though nothing is shown, paths actually do get created.

If you look at the first "visible" segment (class segment M) you'll see that it contains a move command somewhere in the middle:

M31.122194513715712,398.532825

If I rename it to a line command, like so:

L31.122194513715712,398.532825

…then that segment will show.

I'm confused as to which part of the custom curve is responsible for that. How can I turn that M into an L?

The resulting paths also happen to lack final Zs. How would I go about handling that?

I haven't found much help regarding custom curves in D3JS. Any help is welcome.



from D3 custom curve: bundle interpolation for areas

No comments:

Post a Comment