<commit_before>/*
    Orginal work under
    Open source under BSD 2-clause
    Copyright (c) 2015, <NAME>
    All rights reserved.
*/

function liquidFill(_config, _elementId, _value) {
    var value = _value;
    var elementId = _elementId;
    var config = Object.assign({
        circleColor: '#78B', // The color of the outer circle.
        circleFillGap: 0.05, // The size of the gap between the outer circle and wave circle as a percentage of the outer circles radius.
        circleThickness: 0.05, // The outer circle thickness as a percentage of it's radius.
        displayPercent: true, // If true, a % symbol is displayed after the value.
        maxValue: 100, // The gauge maximum value.
        minValue: 0, // The gauge minimum value.
        textColor: '#456', // The color of the value text when the wave does not overlap it.
        textSize: 1, // The relative height of the text to display in the wave circle. 1 = 50%
        textVertPosition: .5, // The height at which to display the percentage text withing the wave circle. 0 = bottom, 1 = top.
        valueCountUp: true, // If true, the displayed value counts up from 0 to it's final value upon loading. If false, the final value is displayed.
        waveAnimate: true, // Controls if the wave scrolls or is static.
        waveAnimateTime: 18000, // The amount of time in milliseconds for a full wave to enter the wave circle.
        waveColor: '#78B', // The color of the fill wave.
        waveCount: 1, // The number of full waves per width of the wave circle.
        waveHeight: 0.05, // The wave height as a percentage of the radius of the wave circle.
        waveHeightScaling: false, // Controls wave size scaling at low and high fill percentages. When true, wave height reaches it's maximum at 50% fill, and minimum at 0% and 100% fill. This helps to prevent the wave from making the wave circle from appear totally full or empty when near it's minimum or maximum fill.
        waveOffset: 0, // The amount to initially offset the wave. 0 = no offset. 1 = offset of one full wave.
        waveRise: true, // Control if the wave should rise from 0 to it's full height, or start at it's full height.
        waveRiseTime: 1000, // The amount of time in milliseconds for the wave to rise from 0 to it's final height.
        waveTextColor: '#4DB' // The color of the value text when the wave overlaps it.
    }, _config);

    var element = d3.select(elementId);
    var radius = Math.min(parseInt(element.style('width')), parseInt(element.style('height'))) / 2;
    var locationX = parseInt(element.style('width')) / 2 - radius;
    var locationY = parseInt(element.style('height')) / 2 - radius;
    var waveHeightScale;

    if (config.waveHeightScaling) {
        waveHeightScale = d3.scaleLinear().range([0, config.waveHeight, 0]).domain([0, 50, 100]);
    } else {
        waveHeightScale = d3.scaleLinear().range([config.waveHeight, config.waveHeight]).domain([0, 100]);
    }

    var textPixels = (config.textSize * radius / 2);
    var textFinalValue = parseFloat(value).toFixed(2);
    var textStartValue = config.valueCountUp
        ? config.minValue
        : textFinalValue;
    var percentText = config.displayPercent
        ? '%'
        : '';
    var circleThickness = config.circleThickness * radius;
    var circleFillGap = config.circleFillGap * radius;
    var fillCircleMargin = circleThickness + circleFillGap;
    var fillCircleRadius = radius - fillCircleMargin;
    var waveHeight = fillCircleRadius * waveHeightScale(getFillLevel(value) * 100);

    var waveLength = fillCircleRadius * 2 / config.waveCount;
    var waveClipCount = 1 + config.waveCount;
    var waveClipWidth = waveLength * waveClipCount;

    // Rounding functions so that the correct number of decimal places is always displayed as the value counts up.
    var textRounder = function(value) {
        return Math.round(value);
    };
    if (parseFloat(textFinalValue) != parseFloat(textRounder(textFinalValue))) {
        textRounder = function(value) {
            return parseFloat(value).toFixed(1);
        };
    }
    if (parseFloat(textFinalValue) != parseFloat(textRounder(textFinalValue))) {
        textRounder = function(value) {
            return parseFloat(value).toFixed(2);
        };
    }

    // Data for building the clip wave area.
    var data = [];
    for (var i = 0; i <= 40 * waveClipCount; i++) {
        data.push({
            x: i / (40 * waveClipCount),
            y: (i / (40))
        });
    }

    // Scales for drawing the outer circle.
    var circleX = d3.scaleLinear().range([
        0, 2 * Math.PI
    ]).domain([0, 1]);
    var circleY = d3.scaleLinear().range([0, radius]).domain([0, radius]);

    // Scales for controlling the size of the clipping path.
    var waveScaleX = d3.scaleLinear().range([0, waveClipWidth]).domain([0, 1]);
    var waveScaleY = d3.scaleLinear().range([0, waveHeight]).domain([0, 1]);

    // Scales for controlling the position of the clipping path.
    var waveRiseScale = d3.scaleLinear()
    // The clipping area size is the height of the fill circle + the wave height, so we position the clip wave
    // such that the it will overlap the fill circle at all when at 0%, and will totally cover the fill
    // circle at 100%.
        .range([
        (fillCircleMargin + fillCircleRadius * 2 + waveHeight),
        (fillCircleMargin - waveHeight)
    ]).domain([0, 1]);
    var waveAnimateScale = d3.scaleLinear().range([
        0, waveClipWidth - fillCircleRadius * 2
    ]). // Push the clip area one full wave then snap back.
    domain([0, 1]);

    // Scale for controlling the position of the text within the element.
    var textRiseScaleY = d3.scaleLinear().range([
        fillCircleMargin + fillCircleRadius * 2,
        (fillCircleMargin + textPixels * 0.7)
    ]).domain([0, 1]);

    // Center the gauge within the parent SVG.
    var elementContainer = element.append('g').attr('transform', 'translate(' + locationX + ',' + locationY + ')');

    // Draw the outer circle.
    var circleArc = d3.arc().startAngle(circleX(0)).endAngle(circleX(1)).outerRadius(circleY(radius)).innerRadius(circleY(radius - circleThickness));
    elementContainer.append('path').attr('d', circleArc).style('fill', config.circleColor).attr('transform', 'translate(' + radius + ',' + radius + ')');

    // Text where the wave does not overlap.
    var text1 = elementContainer.append('text').text(textRounder(textStartValue) + percentText).attr('class', 'liquidFillGaugeText').attr('text-anchor', 'middle').attr('font-size', textPixels + 'px').style('fill', config.textColor).attr('transform', 'translate(' + radius + ',' + textRiseScaleY(config.textVertPosition) + ')');

    // The clipping wave area.
    var clipArea = d3.area().x(function(d) {
        return waveScaleX(d.x);
    }).y0(function(d) {
        return waveScaleY(Math.sin(Math.PI * 2 * config.waveOffset * -1 + Math.PI * 2 * (1 - config.waveCount) + d.y * 2 * Math.PI));
    }).y1(function(d) {
        return (fillCircleRadius * 2 + waveHeight);
    });
    var waveGroup = elementContainer.append('defs').append('clipPath').attr('id', 'clipWave' + elementId);
    var wave = waveGroup.append('path').datum(data).attr('d', clipArea).attr('T', 0);

    // The inner circle with the clipping wave attached.
    var fillCircleGroup = elementContainer.append('g').attr('clip-path', 'url(#clipWave' + elementId + ')');
    fillCircleGroup.append('circle').attr('cx', radius).attr('cy', radius).attr('r', fillCircleRadius).style('fill', config.waveColor);

    // Text where the wave does overlap.
    var text2 = fillCircleGroup.append('text').text(textRounder(textStartValue) + percentText).attr('class', 'liquidFillGaugeText').attr('text-anchor', 'middle').attr('font-size', textPixels + 'px').style('fill', config.waveTextColor).attr('transform', 'translate(' + radius + ',' + textRiseScaleY(config.textVertPosition) + ')');

    // Make the value count up.
    if (config.valueCountUp) {
        var format = d3.format(",d");
        var textTween = function() {
            var that = d3.select(this);
            var i = d3.interpolate(that.text(), textFinalValue);
            return function(t) {
                that.text(format(i(t)) + percentText);
            };
        };
        text1.transition().duration(config.waveRiseTime).tween('text', textTween);
        text2.transition().duration(config.waveRiseTime).tween('text', textTween);
    }

    // Make the wave rise. wave and waveGroup are separate so that horizontal and vertical movement can be controlled independently.
    var waveGroupXPosition = fillCircleMargin + fillCircleRadius * 2 - waveClipWidth;
    if (config.waveRise) {
        waveGroup.attr('transform', 'translate(' + waveGroupXPosition + ',' + waveRiseScale(0) + ')').transition().duration(config.waveRiseTime).attr('transform', 'translate(' + waveGroupXPosition + ',' + waveRiseScale(getFillLevel(value)) + ')').on('start', function() {
            wave.attr('transform', 'translate(1,0)');
        }); // This transform is necessary to get the clip wave positioned correctly when waveRise=true and waveAnimate=false. The wave will not position correctly without this, but it's not clear why this is actually necessary.
    } else {
        waveGroup.attr('transform', 'translate(' + waveGroupXPosition + ',' + waveRiseScale(getFillLevel(value)) + ')');
    }

    if (config.waveAnimate)
        animateWave();

    function animateWave() {
        wave.attr('transform', 'translate(' + waveAnimateScale(wave.attr('T')) + ',0)');
        wave.transition().duration(config.waveAnimateTime * (1 - wave.attr('T'))).ease(d3.easeLinear).attr('transform', 'translate(' + waveAnimateScale(1) + ',0)').attr('T', 1).on('end', function() {
            wave.attr('T', 0);
            animateWave(config.waveAnimateTime);
        });
    }

    function getFillLevel(value) {
        return Math.max(config.minValue, Math.min(config.maxValue, value)) / config.maxValue;
    }

    function updater() {
        this.update = function(value) {
            var newFinalValue = parseFloat(value).toFixed(2);
            var textRounderUpdater = function(value) {
                return Math.round(value);
            };
            if (parseFloat(newFinalValue) != parseFloat(textRounderUpdater(newFinalValue))) {
                textRounderUpdater = function(value) {
                    return parseFloat(value).toFixed(1);
                };
            }
            if (parseFloat(newFinalValue) != parseFloat(textRounderUpdater(newFinalValue))) {
                textRounderUpdater = function(value) {
                    return parseFloat(value).toFixed(2);
                };
            }

            var format = d3.format(",d");
            var textTween = function() {
                var that = d3.select(this);
                var i = d3.interpolate(that.text(), newFinalValue);
                return function(t) {
                    that.text(format(i(t)) + percentText);
                };
            };
            text1.transition().duration(config.waveRiseTime).tween('text', textTween);
            text2.transition().duration(config.waveRiseTime).tween('text', textTween);

            var fillPercent = Math.max(config.minValue, Math.min(config.maxValue, value)) / config.maxValue;
            var waveHeight = fillCircleRadius * waveHeightScale(fillPercent * 100);
            var waveRiseScale = d3.scaleLinear()
            // The clipping area size is the height of the fill circle + the wave height, so we position the clip wave
            // such that the it will overlap the fill circle at all when at 0%, and will totally cover the fill
            // circle at 100%.
                .range([
                (fillCircleMargin + fillCircleRadius * 2 + waveHeight),
                (fillCircleMargin - waveHeight)
            ]).domain([0, 1]);
            var newHeight = waveRiseScale(fillPercent);
            var waveScaleX = d3.scaleLinear().range([0, waveClipWidth]).domain([0, 1]);
            var waveScaleY = d3.scaleLinear().range([0, waveHeight]).domain([0, 1]);
            var newClipArea;
            if (config.waveHeightScaling) {
                newClipArea = d3.area().x(function(d) {
                    return waveScaleX(d.x);
                }).y0(function(d) {
                    return waveScaleY(Math.sin(Math.PI * 2 * config.waveOffset * -1 + Math.PI * 2 * (1 - config.waveCount) + d.y * 2 * Math.PI));
                }).y1(function(d) {
                    return (fillCircleRadius * 2 + waveHeight);
                });
            } else {
                newClipArea = clipArea;
            }

            var newWavePosition = config.waveAnimate
                ? waveAnimateScale(1)
                : 0;
            wave.transition().duration(0).transition().duration(config.waveAnimate
                ? (config.waveAnimateTime * (1 - wave.attr('T')))
                : (config.waveRiseTime)).ease(d3.easeLinear).attr('d', newClipArea).attr('transform', 'translate(' + newWavePosition + ',0)').attr('T', '1').on('end', function() {
                if (config.waveAnimate) {
                    wave.attr('transform', 'translate(' + waveAnimateScale(0) + ',0)');
                    animateWave(config.waveAnimateTime);
                }
            });
            waveGroup.transition().duration(config.waveRiseTime).attr('transform', 'translate(' + waveGroupXPosition + ',' + newHeight + ')')
        }
    }

    return new updater();
}<commit_msg>beautify<commit_after>/*
    Orginal work under
    Open source under BSD 2-clause
    Copyright (c) 2015, <NAME>
    All rights reserved.
*/

function liquidFill(_config, _elementId, _value) {
    var value = _value;
    var elementId = _elementId;
    var config = Object.assign({
        circleColor: '#78B', // The color of the outer circle.
        circleFillGap: 0.05, // The size of the gap between the outer circle and wave circle as a percentage of the outer circles radius.
        circleThickness: 0.05, // The outer circle thickness as a percentage of it's radius.
        displayPercent: true, // If true, a % symbol is displayed after the value.
        maxValue: 100, // The gauge maximum value.
        minValue: 0, // The gauge minimum value.
        textColor: '#456', // The color of the value text when the wave does not overlap it.
        textSize: 1, // The relative height of the text to display in the wave circle. 1 = 50%
        textVertPosition: .5, // The height at which to display the percentage text withing the wave circle. 0 = bottom, 1 = top.
        valueCountUp: true, // If true, the displayed value counts up from 0 to it's final value upon loading. If false, the final value is displayed.
        waveAnimate: true, // Controls if the wave scrolls or is static.
        waveAnimateTime: 18000, // The amount of time in milliseconds for a full wave to enter the wave circle.
        waveColor: '#78B', // The color of the fill wave.
        waveCount: 1, // The number of full waves per width of the wave circle.
        waveHeight: 0.05, // The wave height as a percentage of the radius of the wave circle.
        waveHeightScaling: false, // Controls wave size scaling at low and high fill percentages. When true, wave height reaches it's maximum at 50% fill, and minimum at 0% and 100% fill. This helps to prevent the wave from making the wave circle from appear totally full or empty when near it's minimum or maximum fill.
        waveOffset: 0, // The amount to initially offset the wave. 0 = no offset. 1 = offset of one full wave.
        waveRise: true, // Control if the wave should rise from 0 to it's full height, or start at it's full height.
        waveRiseTime: 1000, // The amount of time in milliseconds for the wave to rise from 0 to it's final height.
        waveTextColor: '#4DB' // The color of the value text when the wave overlaps it.
    }, _config);

    var element = d3.select(elementId);
    var radius = Math.min(parseInt(element.style('width')), parseInt(element.style('height'))) / 2;
    var locationX = parseInt(element.style('width')) / 2 - radius;
    var locationY = parseInt(element.style('height')) / 2 - radius;
    var waveHeightScale;

    if (config.waveHeightScaling) {
        waveHeightScale = d3.scaleLinear().range([0, config.waveHeight, 0]).domain([0, 50, 100]);
    } else {
        waveHeightScale = d3.scaleLinear().range([config.waveHeight, config.waveHeight]).domain([0, 100]);
    }

    var textPixels = (config.textSize * radius / 2);
    var textFinalValue = parseFloat(value).toFixed(2);
    var textStartValue = config.valueCountUp
        ? config.minValue
        : textFinalValue;
    var percentText = config.displayPercent
        ? '%'
        : '';
    var circleThickness = config.circleThickness * radius;
    var circleFillGap = config.circleFillGap * radius;
    var fillCircleMargin = circleThickness + circleFillGap;
    var fillCircleRadius = radius - fillCircleMargin;
    var waveHeight = fillCircleRadius * waveHeightScale(getFillLevel(value) * 100);

    var waveLength = fillCircleRadius * 2 / config.waveCount;
    var waveClipCount = 1 + config.waveCount;
    var waveClipWidth = waveLength * waveClipCount;

    // Rounding functions so that the correct number of decimal places is always displayed as the value counts up.
    var textRounder = function(value) {
        return Math.round(value);
    };
    if (parseFloat(textFinalValue) != parseFloat(textRounder(textFinalValue))) {
        textRounder = function(value) {
            return parseFloat(value).toFixed(1);
        };
    }
    if (parseFloat(textFinalValue) != parseFloat(textRounder(textFinalValue))) {
        textRounder = function(value) {
            return parseFloat(value).toFixed(2);
        };
    }

    // Data for building the clip wave area.
    var data = [];
    for (var i = 0; i <= 40 * waveClipCount; i++) {
        data.push({
            x: i / (40 * waveClipCount),
            y: (i / (40))
        });
    }

    // Scales for drawing the outer circle.
    var circleX = d3.scaleLinear().range([
        0, 2 * Math.PI
    ]).domain([0, 1]);
    var circleY = d3.scaleLinear().range([0, radius]).domain([0, radius]);

    // Scales for controlling the size of the clipping path.
    var waveScaleX = d3.scaleLinear().range([0, waveClipWidth]).domain([0, 1]);
    var waveScaleY = d3.scaleLinear().range([0, waveHeight]).domain([0, 1]);

    // Scales for controlling the position of the clipping path.
    var waveRiseScale = d3.scaleLinear()
    // The clipping area size is the height of the fill circle + the wave height, so we position the clip wave
    // such that the it will overlap the fill circle at all when at 0%, and will totally cover the fill
    // circle at 100%.
        .range([
        (fillCircleMargin + fillCircleRadius * 2 + waveHeight),
        (fillCircleMargin - waveHeight)
    ]).domain([0, 1]);
    var waveAnimateScale = d3.scaleLinear().range([
        0, waveClipWidth - fillCircleRadius * 2
    ]). // Push the clip area one full wave then snap back.
    domain([0, 1]);

    // Scale for controlling the position of the text within the element.
    var textRiseScaleY = d3.scaleLinear().range([
        fillCircleMargin + fillCircleRadius * 2,
        (fillCircleMargin + textPixels * 0.7)
    ]).domain([0, 1]);

    // Center the gauge within the parent SVG.
    var elementContainer = element.append('g').attr('transform', 'translate(' + locationX + ',' + locationY + ')');

    // Draw the outer circle.
    var circleArc = d3.arc().startAngle(circleX(0)).endAngle(circleX(1)).outerRadius(circleY(radius)).innerRadius(circleY(radius - circleThickness));
    elementContainer.append('path').attr('d', circleArc).style('fill', config.circleColor).attr('transform', 'translate(' + radius + ',' + radius + ')');

    // Text where the wave does not overlap.
    var text1 = elementContainer.append('text').text(textRounder(textStartValue) + percentText).attr('class', 'liquidFillGaugeText').attr('text-anchor', 'middle').attr('font-size', textPixels + 'px').style('fill', config.textColor).attr('transform', 'translate(' + radius + ',' + textRiseScaleY(config.textVertPosition) + ')');

    // The clipping wave area.
    var clipArea = d3.area().x(function(d) {
        return waveScaleX(d.x);
    }).y0(function(d) {
        return waveScaleY(Math.sin(Math.PI * 2 * config.waveOffset * -1 + Math.PI * 2 * (1 - config.waveCount) + d.y * 2 * Math.PI));
    }).y1(function(d) {
        return (fillCircleRadius * 2 + waveHeight);
    });
    var waveGroup = elementContainer.append('defs').append('clipPath').attr('id', 'clipWave' + elementId);
    var wave = waveGroup.append('path').datum(data).attr('d', clipArea).attr('T', 0);

    // The inner circle with the clipping wave attached.
    var fillCircleGroup = elementContainer.append('g').attr('clip-path', 'url(#clipWave' + elementId + ')');
    fillCircleGroup.append('circle').attr('cx', radius).attr('cy', radius).attr('r', fillCircleRadius).style('fill', config.waveColor);

    // Text where the wave does overlap.
    var text2 = fillCircleGroup.append('text').text(textRounder(textStartValue) + percentText).attr('class', 'liquidFillGaugeText').attr('text-anchor', 'middle').attr('font-size', textPixels + 'px').style('fill', config.waveTextColor).attr('transform', 'translate(' + radius + ',' + textRiseScaleY(config.textVertPosition) + ')');

    // Make the value count up.
    if (config.valueCountUp) {
        var format = d3.format(",d");
        var textTween = function() {
            var that = d3.select(this);
            var i = d3.interpolate(that.text(), textFinalValue);
            return function(t) {
                that.text(format(i(t)) + percentText);
            };
        };
        text1.transition().duration(config.waveRiseTime).tween('text', textTween);
        text2.transition().duration(config.waveRiseTime).tween('text', textTween);
    }

    // Make the wave rise. wave and waveGroup are separate so that horizontal and vertical movement can be controlled independently.
    var waveGroupXPosition = fillCircleMargin + fillCircleRadius * 2 - waveClipWidth;
    if (config.waveRise) {
        waveGroup.attr('transform', 'translate(' + waveGroupXPosition + ',' + waveRiseScale(0) + ')').transition().duration(config.waveRiseTime).attr('transform', 'translate(' + waveGroupXPosition + ',' + waveRiseScale(getFillLevel(value)) + ')').on('start', function() {
            wave.attr('transform', 'translate(1,0)');
        }); // This transform is necessary to get the clip wave positioned correctly when waveRise=true and waveAnimate=false. The wave will not position correctly without this, but it's not clear why this is actually necessary.
    } else {
        waveGroup.attr('transform', 'translate(' + waveGroupXPosition + ',' + waveRiseScale(getFillLevel(value)) + ')');
    }

    if (config.waveAnimate)
        animateWave();

    function animateWave() {
        wave.attr('transform', 'translate(' + waveAnimateScale(wave.attr('T')) + ',0)');
        wave.transition().duration(config.waveAnimateTime * (1 - wave.attr('T'))).ease(d3.easeLinear).attr('transform', 'translate(' + waveAnimateScale(1) + ',0)').attr('T', 1).on('end', function() {
            wave.attr('T', 0);
            animateWave(config.waveAnimateTime);
        });
    }

    function getFillLevel(value) {
        return Math.max(config.minValue, Math.min(config.maxValue, value)) / config.maxValue;
    }

    function updater() {
        this.update = function(value) {
            var newFinalValue = parseFloat(value).toFixed(2);
            var textRounderUpdater = function(value) {
                return Math.round(value);
            };
            if (parseFloat(newFinalValue) != parseFloat(textRounderUpdater(newFinalValue))) {
                textRounderUpdater = function(value) {
                    return parseFloat(value).toFixed(1);
                };
            }
            if (parseFloat(newFinalValue) != parseFloat(textRounderUpdater(newFinalValue))) {
                textRounderUpdater = function(value) {
                    return parseFloat(value).toFixed(2);
                };
            }

            var format = d3.format(",d");
            var textTween = function() {
                var that = d3.select(this);
                var i = d3.interpolate(that.text(), newFinalValue);
                return function(t) {
                    that.text(format(i(t)) + percentText);
                };
            };
            text1.transition().duration(config.waveRiseTime).tween('text', textTween);
            text2.transition().duration(config.waveRiseTime).tween('text', textTween);

            var fillPercent = Math.max(config.minValue, Math.min(config.maxValue, value)) / config.maxValue;
            var waveHeight = fillCircleRadius * waveHeightScale(fillPercent * 100);
            var waveRiseScale = d3.scaleLinear()
            // The clipping area size is the height of the fill circle + the wave height, so we position the clip wave
            // such that the it will overlap the fill circle at all when at 0%, and will totally cover the fill
            // circle at 100%.
                .range([
                (fillCircleMargin + fillCircleRadius * 2 + waveHeight),
                (fillCircleMargin - waveHeight)
            ]).domain([0, 1]);
            var newHeight = waveRiseScale(fillPercent);
            var waveScaleX = d3.scaleLinear().range([0, waveClipWidth]).domain([0, 1]);
            var waveScaleY = d3.scaleLinear().range([0, waveHeight]).domain([0, 1]);
            var newClipArea;
            if (config.waveHeightScaling) {
                newClipArea = d3.area().x(function(d) {
                    return waveScaleX(d.x);
                }).y0(function(d) {
                    return waveScaleY(Math.sin(Math.PI * 2 * config.waveOffset * -1 + Math.PI * 2 * (1 - config.waveCount) + d.y * 2 * Math.PI));
                }).y1(function(d) {
                    return (fillCircleRadius * 2 + waveHeight);
                });
            } else {
                newClipArea = clipArea;
            }

            var newWavePosition = config.waveAnimate
                ? waveAnimateScale(1)
                : 0;
            wave.transition().duration(0).transition().duration(config.waveAnimate
                ? (config.waveAnimateTime * (1 - wave.attr('T')))
                : (config.waveRiseTime)).ease(d3.easeLinear).attr('d', newClipArea).attr('transform', 'translate(' + newWavePosition + ',0)').attr('T', '1').on('end', function() {
                if (config.waveAnimate) {
                    wave.attr('transform', 'translate(' + waveAnimateScale(0) + ',0)');
                    animateWave(config.waveAnimateTime);
                }
            });
            waveGroup.transition().duration(config.waveRiseTime).attr('transform', 'translate(' + waveGroupXPosition + ',' + newHeight + ')')
        }
    }
    return new updater();
}
{'fraction_non_alphanumeric': 0.13006931131101607, 'fraction_numerical': 0.014747087450228579, 'mean_word_length': 2.6645501215887597, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3200134', 'n_tokens_mistral': 7952, 'n_tokens_neox': 7579, 'n_words': 2156}