var Plotly = require('@lib/index');
var Lib = require('@src/lib');
var setConvert = require('@src/plots/cartesian/set_convert');

var supplyDefaults = require('@src/traces/histogram/defaults');
var supplyDefaults2D = require('@src/traces/histogram2d/defaults');
var supplyDefaults2DC = require('@src/traces/histogram2dcontour/defaults');
var calc = require('@src/traces/histogram/calc');
var getBinSpanLabelRound = require('@src/traces/histogram/bin_label_vals');

var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');
var supplyAllDefaults = require('../assets/supply_defaults');


describe('Test histogram', function() {
    'use strict';

    describe('supplyDefaults', function() {
        var traceIn,
            traceOut;

        beforeEach(function() {
            traceOut = {};
        });

        it('should set visible to false when x or y is empty', function() {
            traceIn = {
                x: []
            };
            supplyDefaults(traceIn, traceOut, '', {});
            expect(traceOut.visible).toBe(false);

            traceIn = {
                y: []
            };
            traceOut = {};
            supplyDefaults(traceIn, traceOut, '', {});
            expect(traceOut.visible).toBe(false);
        });

        it('should set visible to false when x or y is empty AND the other is present', function() {
            traceIn = {
                x: [],
                y: [1, 2, 2]
            };
            supplyDefaults(traceIn, traceOut, '', {});
            expect(traceOut.visible).toBe(false);

            traceIn = {
                x: [1, 2, 2],
                y: []
            };
            traceOut = {};
            supplyDefaults(traceIn, traceOut, '', {});
            expect(traceOut.visible).toBe(false);
        });

        it('should set visible to false when type is histogram2d(contour) and x or y are empty', function() {
            traceIn = {
                x: [],
                y: [1, 2, 2]
            };
            supplyDefaults2D(traceIn, traceOut, '', {});
            expect(traceOut.visible).toBe(false);

            traceIn = {
                x: [1, 2, 2],
                y: []
            };
            traceOut = {};
            supplyDefaults2D(traceIn, traceOut, '', {});
            expect(traceOut.visible).toBe(false);

            traceIn = {
                x: [],
                y: []
            };
            traceOut = {};
            supplyDefaults2D(traceIn, traceOut, '', {});
            expect(traceOut.visible).toBe(false);

            traceIn = {
                x: [],
                y: [1, 2, 2]
            };
            traceOut = {};
            supplyDefaults2DC(traceIn, traceOut, '', {});
            expect(traceOut.visible).toBe(false);
        });

        it('should set orientation to v by default', function() {
            traceIn = {
                x: [1, 2, 2]
            };
            supplyDefaults(traceIn, traceOut, '', {});
            expect(traceOut.orientation).toBe('v');

            traceIn = {
                x: [1, 2, 2],
                y: [1, 2, 2]
            };
            traceOut = {};
            supplyDefaults(traceIn, traceOut, '', {});
            expect(traceOut.orientation).toBe('v');
        });

        it('should set orientation to h when only y is supplied', function() {
            traceIn = {
                y: [1, 2, 2]
            };
            supplyDefaults(traceIn, traceOut, '', {});
            expect(traceOut.orientation).toBe('h');

        });

        // coercing bin attributes got moved to calc because it needs
        // axis type - so here we just test that it's NOT happening

        it('should not coerce autobinx regardless of xbins', function() {
            traceIn = {
                x: [1, 2, 2],
                xbins: {
                    start: 1,
                    end: 3,
                    size: 1
                }
            };
            supplyDefaults(traceIn, traceOut, '', {});
            expect(traceOut.autobinx).toBeUndefined();

            traceIn = {
                x: [1, 2, 2]
            };
            traceOut = {};
            supplyDefaults(traceIn, traceOut, '', {});
            expect(traceOut.autobinx).toBeUndefined();
        });

        it('should not coerce autobiny regardless of ybins', function() {
            traceIn = {
                y: [1, 2, 2],
                ybins: {
                    start: 1,
                    end: 3,
                    size: 1
                }
            };
            supplyDefaults(traceIn, traceOut, '', {});
            expect(traceOut.autobiny).toBeUndefined();

            traceIn = {
                y: [1, 2, 2]
            };
            traceOut = {};
            supplyDefaults(traceIn, traceOut, '', {});
            expect(traceOut.autobiny).toBeUndefined();
        });

        it('should inherit layout.calendar', function() {
            traceIn = {
                x: [1, 2, 3]
            };
            supplyDefaults(traceIn, traceOut, '', {calendar: 'islamic'});

            // we always fill calendar attributes, because it's hard to tell if
            // we're on a date axis at this point.
            // size axis calendar is weird, but *might* be able to happen if
            // we're using histfunc=min or max (does this work?)
            expect(traceOut.xcalendar).toBe('islamic');
            expect(traceOut.ycalendar).toBe('islamic');
        });

        it('should take its own calendars', function() {
            traceIn = {
                x: [1, 2, 3],
                xcalendar: 'coptic',
                ycalendar: 'nepali'
            };
            supplyDefaults(traceIn, traceOut, '', {calendar: 'islamic'});

            expect(traceOut.xcalendar).toBe('coptic');
            expect(traceOut.ycalendar).toBe('nepali');
        });
    });


    describe('calc', function() {
        function _calc(opts, extraTraces, layout) {
            var base = { type: 'histogram' };
            var trace = Lib.extendFlat({}, base, opts);
            var gd = { data: [trace] };

            if(layout) gd.layout = layout;

            if(Array.isArray(extraTraces)) {
                extraTraces.forEach(function(extraTrace) {
                    gd.data.push(Lib.extendFlat({}, base, extraTrace));
                });
            }

            supplyAllDefaults(gd);
            var fullTrace = gd._fullData[0];

            var out = calc(gd, fullTrace);
            delete out[0].trace;

            // this is dumb - but some of the `p0` values are `-0` which doesn't match `0`
            // even though -0 === 0
            out.forEach(function(cdi) {
                for(var key in cdi) {
                    if(cdi[key] === 0) cdi[key] = 0;
                }
            });

            return out;
        }

        var oneDay = 24 * 3600000;

        it('should handle auto dates with nonuniform (month) bins', function() {
            // All data on exact years: shift so bin center is an
            // exact year, except on leap years
            var out = _calc({
                x: ['1970-01-01', '1970-01-01', '1971-01-01', '1973-01-01'],
                nbinsx: 4
            });

            // TODO: this gives half-day gaps between all but the first two
            // bars. Now that we have explicit per-bar positioning, perhaps
            // we should fill the space, rather than insisting on equal-width
            // bars?
            var d70 = Date.UTC(1970, 0, 1);
            var d71 = Date.UTC(1971, 0, 1);
            var d72 = Date.UTC(1972, 0, 1, 12);
            var d73 = Date.UTC(1973, 0, 1);
            expect(out).toEqual([
                // full calcdata has x and y too (and t in the first one),
                // but those come later from setPositions.
                {i: 0, b: 0, p: d70, s: 2, pts: [0, 1], p0: d70, p1: d70},
                {i: 1, b: 0, p: d71, s: 1, pts: [2], p0: d71, p1: d71},
                {i: 2, b: 0, p: d72, s: 0, pts: [], p0: d72, p1: d72},
                {i: 3, b: 0, p: d73, s: 1, pts: [3], p0: d73, p1: d73}
            ]);

            // All data on exact months: shift so bin center is on (31-day months)
            // or in (shorter months) that month
            out = _calc({
                x: ['1970-01-01', '1970-01-01', '1970-02-01', '1970-04-01'],
                nbinsx: 4
            });

            var d70feb = Date.UTC(1970, 1, 1);
            var d70mar = Date.UTC(1970, 2, 2, 12);
            var d70apr = Date.UTC(1970, 3, 1);
            expect(out).toEqual([
                {i: 0, b: 0, p: d70, s: 2, pts: [0, 1], p0: d70, p1: d70},
                {i: 1, b: 0, p: d70feb, s: 1, pts: [2], p0: d70feb, p1: d70feb},
                {i: 2, b: 0, p: d70mar, s: 0, pts: [], p0: d70mar, p1: d70mar},
                {i: 3, b: 0, p: d70apr, s: 1, pts: [3], p0: d70apr, p1: d70apr}
            ]);

            // data on exact days: shift so each bin goes from noon to noon
            // even though this gives kind of odd bin centers since the bins
            // are months... but the important thing is it's unambiguous which
            // bin any given day is in.
            out = _calc({
                x: ['1970-01-02', '1970-01-31', '1970-02-13', '1970-04-19'],
                nbinsx: 4
            });

            expect(out).toEqual([
                // dec 31 12:00 -> jan 31 12:00, middle is jan 16
                {i: 0, b: 0, p: Date.UTC(1970, 0, 16), s: 2, pts: [0, 1], p0: Date.UTC(1970, 0, 1), p1: Date.UTC(1970, 0, 31)},
                // jan 31 12:00 -> feb 28 12:00, middle is feb 14 12:00
                {i: 1, b: 0, p: Date.UTC(1970, 1, 14, 12), s: 1, pts: [2], p0: Date.UTC(1970, 1, 1), p1: Date.UTC(1970, 1, 28)},
                {i: 2, b: 0, p: Date.UTC(1970, 2, 16), s: 0, pts: [], p0: Date.UTC(1970, 2, 1), p1: Date.UTC(1970, 2, 31)},
                {i: 3, b: 0, p: Date.UTC(1970, 3, 15, 12), s: 1, pts: [3], p0: Date.UTC(1970, 3, 1), p1: Date.UTC(1970, 3, 30)}
            ]);
        });

        it('should handle auto dates with uniform (day) bins', function() {
            var out = _calc({
                x: ['1970-01-01', '1970-01-01', '1970-01-02', '1970-01-04'],
                nbinsx: 4
            });

            var x0 = 0,
                x1 = x0 + oneDay,
                x2 = x1 + oneDay,
                x3 = x2 + oneDay;

            expect(out).toEqual([
                {i: 0, b: 0, p: x0, s: 2, pts: [0, 1], p0: x0, p1: x0},
                {i: 1, b: 0, p: x1, s: 1, pts: [2], p0: x1, p1: x1},
                {i: 2, b: 0, p: x2, s: 0, pts: [], p0: x2, p1: x2},
                {i: 3, b: 0, p: x3, s: 1, pts: [3], p0: x3, p1: x3}
            ]);
        });

        it('should handle very small bins', function() {
            var out = _calc({
                x: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
                xbins: {
                    start: 0,
                    end: 10,
                    size: 0.001
                }
            });

            expect(out.length).toEqual(9001);
        });

        it('handles single-bin data without extra bins', function() {
            var out = _calc({
                x: [2.1, 3, 3.9],
                xbins: {start: 0, end: 10, size: 2}
            });

            expect(out).toEqual([
                {i: 0, b: 0, p: 3, s: 3, width1: 2, pts: [0, 1, 2], p0: 2, p1: 3.9}
            ]);
        });

        it('handles single-value overlaid autobinned data with other manual bins', function() {
            var out = _calc({x: [1.1, 1.1, 1.1]}, [
                {x: [1, 2, 3, 4], xbins: {start: 0.5, end: 4.5, size: 2}},
                {x: [10, 10.5, 11, 11.5], xbins: {start: 9.8, end: 11.8, size: 0.5}}
            ], {
                barmode: 'overlay'
            });

            expect(out).toEqual([
                {i: 0, b: 0, p: 1.1, s: 3, width1: 0.5, pts: [0, 1, 2], p0: 1.1, p1: 1.1}
            ]);
        });

        it('handles single-value overlaid autobinned data with other auto bins', function() {
            var out = _calc({x: ['', null, 17, '', 17]}, [
                {x: [10, 20, 30, 40]},
                {x: [100, 101, 102, 103]}
            ], {
                barmode: 'overlay'
            });

            expect(out).toEqual([
                {i: 0, b: 0, p: 17, s: 2, width1: 2, pts: [2, 4], p0: 17, p1: 17}
            ]);
        });

        it('handles multiple single-valued overlaid autobinned traces with different values', function() {
            var out = _calc({x: [null, 13, '', 13]}, [
                {x: [5]},
                {x: [null, 29, 29, 29, null]}
            ], {
                barmode: 'overlay'
            });

            expect(out).toEqual([
                {i: 0, b: 0, p: 13, s: 2, width1: 8, pts: [1, 3], p0: 13, p1: 13}
            ]);
        });

        it('handles multiple single-date overlaid autobinned traces with different values', function() {
            var out = _calc({x: [null, '2011-02-03', '', '2011-02-03']}, [
                {x: ['2011-02-05']},
                {x: [null, '2015-05-05', '2015-05-05', '2015-05-05', null]}
            ], {
                barmode: 'overlay'
            });

            var p = 1296691200000;
            expect(out).toEqual([
                {i: 0, b: 0, p: p, s: 2, width1: 2 * 24 * 3600 * 1000, pts: [1, 3], p0: p, p1: p}
            ]);
        });

        it('handles several overlaid autobinned traces with only one value total', function() {
            var out = _calc({x: [null, 97, '', 97]}, [
                {x: [97]},
                {x: [null, 97, 97, 97, null]}
            ], {
                barmode: 'overlay'
            });

            expect(out).toEqual([
                {i: 0, b: 0, p: 97, s: 2, width1: 1, pts: [1, 3], p0: 97, p1: 97}
            ]);
        });

        it('can tell the difference between single-bin and single-value histograms', function() {
            var out = _calc({x: [1, 4]}, [], {barmode: 'overlay'});

            expect(out).toEqual([
                {i: 0, b: 0, p: 2, s: 2, width1: 5, pts: [0, 1], p0: 0, p1: 4}
            ]);

            // real single-valued trace inherits bar width from the simply single-bin trace
            out = _calc({x: [5]}, [
                {x: [1, 4]}
            ], {
                barmode: 'overlay'
            });

            expect(out).toEqual([
                {i: 0, b: 0, p: 5, s: 1, width1: 5, pts: [0], p0: 5, p1: 5}
            ]);
        });

        function calcPositions(opts, extraTraces) {
            return _calc(opts, extraTraces).map(function(v) { return v.p; });
        }

        it('harmonizes autobins when all traces are autobinned', function() {
            var trace1 = {x: [1, 2, 3, 4]};
            var trace2 = {x: [5, 5.5, 6, 6.5]};

            expect(calcPositions(trace1)).toBeCloseToArray([0.5, 2.5, 4.5], 5);

            expect(calcPositions(trace2)).toBeCloseToArray([5.5, 6.5], 5);

            expect(calcPositions(trace1, [trace2])).toEqual([1, 2, 3, 4]);
            // huh, turns out even this one is an example of "unexpected bin positions"
            // (see another example below) - in this case it's because trace1 gets
            // autoshifted to keep integers off the bin edges, whereas trace2 doesn't
            // because there are as many integers as half-integers.
            // In this case though, it's unexpected but arguably better than the
            // "expected" result.
            expect(calcPositions(trace2, [trace1])).toEqual([5, 6, 7]);
        });

        it('can sometimes give unexpected bin positions', function() {
            // documenting an edge case that might not be desirable but for now
            // we've decided to ignore: a larger bin sets the bin start, but then it
            // doesn't quite make sense with the smaller bin we end up with
            // we *could* fix this by ensuring that the bin start is based on the
            // same bin spec that gave the minimum bin size, but incremented down to
            // include the minimum start... but that would have awkward edge cases
            // involving month bins so for now we're ignoring it.

            // all integers, so all autobins should get shifted to start 0.5 lower
            // than they otherwise would.
            var trace1 = {x: [1, 2, 3, 4]};
            var trace2 = {x: [-2, 1, 4, 7]};

            // as above... size: 2
            expect(calcPositions(trace1)).toBeCloseToArray([0.5, 2.5, 4.5], 5);

            // {size: 5, start: -5.5}: -5..-1, 0..4, 5..9
            expect(calcPositions(trace2)).toEqual([-3, 2, 7]);

            // unexpected behavior when we put these together,
            // because 2 and 5 are mutually prime. Normally you could never get
            // groupings 1&2, 3&4... you'd always get 0&1, 2&3...
            expect(calcPositions(trace1, [trace2])).toBeCloseToArray([1.5, 3.5], 5);
            expect(calcPositions(trace2, [trace1])).toBeCloseToArray([
                -2.5, -0.5, 1.5, 3.5, 5.5, 7.5
            ], 5);
        });

        it('harmonizes autobins with smaller manual bins', function() {
            var trace1 = {x: [1, 2, 3, 4]};
            var trace2 = {x: [5, 6, 7, 8], xbins: {start: 4.3, end: 7.1, size: 0.4}};

            expect(calcPositions(trace1, [trace2])).toBeCloseToArray([
                0.9, 1.3, 1.7, 2.1, 2.5, 2.9, 3.3, 3.7, 4.1
            ], 5);
        });

        it('harmonizes autobins with larger manual bins', function() {
            var trace1 = {x: [1, 2, 3, 4]};
            var trace2 = {x: [5, 6, 7, 8], xbins: {start: 4.3, end: 15, size: 7}};

            expect(calcPositions(trace1, [trace2])).toBeCloseToArray([
                0.8, 2.55, 4.3
            ], 5);
        });

        it('ignores traces on other axes', function() {
            var trace1 = {x: [1, 2, 3, 4]};
            var trace2 = {x: [5, 5.5, 6, 6.5]};
            var trace3 = {x: [1, 1.1, 1.2, 1.3], xaxis: 'x2'};
            var trace4 = {x: [1, 1.2, 1.4, 1.6], yaxis: 'y2'};

            expect(calcPositions(trace1, [trace2, trace3, trace4])).toEqual([1, 2, 3, 4]);
            expect(calcPositions(trace3)).toBeCloseToArray([1.1, 1.3], 5);
        });

        describe('cumulative distribution functions', function() {
            var base = {
                x: [0, 5, 10, 15, 5, 10, 15, 10, 15, 15],
                y: [2, 2, 2, 14, 6, 6, 6, 10, 10, 2]
            };

            it('makes the right base histogram', function() {
                var baseOut = _calc(base);
                expect(baseOut).toEqual([
                    {i: 0, b: 0, p: 2, s: 1, pts: [0], p0: 0, p1: 0},
                    {i: 1, b: 0, p: 7, s: 2, pts: [1, 4], p0: 5, p1: 5},
                    {i: 2, b: 0, p: 12, s: 3, pts: [2, 5, 7], p0: 10, p1: 10},
                    {i: 3, b: 0, p: 17, s: 4, pts: [3, 6, 8, 9], p0: 15, p1: 15},
                ]);
            });

            // p0, p1, and pts have been omitted from CDFs for now
            var CDFs = [
                {p: [2, 7, 12, 17], s: [1, 3, 6, 10]},
                {
                    direction: 'decreasing',
                    p: [2, 7, 12, 17], s: [10, 9, 7, 4]
                },
                {
                    currentbin: 'exclude',
                    p: [7, 12, 17, 22], s: [1, 3, 6, 10]
                },
                {
                    direction: 'decreasing', currentbin: 'exclude',
                    p: [-3, 2, 7, 12], s: [10, 9, 7, 4]
                },
                {
                    currentbin: 'half',
                    p: [2, 7, 12, 17, 22], s: [0.5, 2, 4.5, 8, 10]
                },
                {
                    direction: 'decreasing', currentbin: 'half',
                    p: [-3, 2, 7, 12, 17], s: [10, 9.5, 8, 5.5, 2]
                },
                {
                    direction: 'decreasing', currentbin: 'half', histnorm: 'percent',
                    p: [-3, 2, 7, 12, 17], s: [100, 95, 80, 55, 20]
                },
                {
                    currentbin: 'exclude', histnorm: 'probability',
                    p: [7, 12, 17, 22], s: [0.1, 0.3, 0.6, 1]
                },
                {
                    // behaves the same as without *density*
                    direction: 'decreasing', currentbin: 'half', histnorm: 'density',
                    p: [-3, 2, 7, 12, 17], s: [10, 9.5, 8, 5.5, 2]
                },
                {
                    // behaves the same as without *density*, only *probability*
                    direction: 'decreasing', currentbin: 'half', histnorm: 'probability density',
                    p: [-3, 2, 7, 12, 17], s: [1, 0.95, 0.8, 0.55, 0.2]
                },
                {
                    currentbin: 'half', histfunc: 'sum',
                    p: [2, 7, 12, 17, 22], s: [1, 6, 19, 44, 60]
                },
                {
                    currentbin: 'half', histfunc: 'sum', histnorm: 'probability',
                    p: [2, 7, 12, 17, 22], s: [0.5 / 30, 0.1, 9.5 / 30, 22 / 30, 1]
                },
                {
                    direction: 'decreasing', currentbin: 'half', histfunc: 'max', histnorm: 'percent',
                    p: [-3, 2, 7, 12, 17], s: [100, 3100 / 32, 2700 / 32, 1900 / 32, 700 / 32]
                },
                {
                    direction: 'decreasing', currentbin: 'half', histfunc: 'min', histnorm: 'density',
                    p: [-3, 2, 7, 12, 17], s: [8, 7, 5, 3, 1]
                },
                {
                    currentbin: 'exclude', histfunc: 'avg', histnorm: 'probability density',
                    p: [7, 12, 17, 22], s: [0.1, 0.3, 0.6, 1]
                }
            ];

            CDFs.forEach(function(CDF) {
                var p = CDF.p,
                    s = CDF.s;

                it('handles direction=' + CDF.direction + ', currentbin=' + CDF.currentbin +
                        ', histnorm=' + CDF.histnorm + ', histfunc=' + CDF.histfunc, function() {
                    var traceIn = Lib.extendFlat({}, base, {
                        cumulative: {
                            enabled: true,
                            direction: CDF.direction,
                            currentbin: CDF.currentbin
                        },
                        histnorm: CDF.histnorm,
                        histfunc: CDF.histfunc
                    });
                    var out = _calc(traceIn);

                    expect(out.length).toBe(p.length);
                    out.forEach(function(outi, i) {
                        expect(outi.p).toBe(p[i]);
                        expect(outi.s).toBeCloseTo(s[i], 6);
                        expect(outi.b).toBe(0);
                    });
                });
            });
        });

    });

    describe('plot / restyle', function() {
        var gd;

        beforeEach(function() {
            gd = createGraphDiv();
        });

        afterEach(destroyGraphDiv);

        it('should update autobins correctly when restyling', function() {
            // note: I'm *not* testing what this does to gd.data, as that's
            // really a matter of convenience and will perhaps change later (v2?)
            var data1 = [1.5, 2, 2, 3, 3, 3, 4, 4, 5];
            Plotly.plot(gd, [{x: data1, type: 'histogram' }]);
            expect(gd._fullData[0].xbins).toEqual({start: 1, end: 6, size: 1});
            expect(gd._fullData[0].autobinx).toBe(true);

            // same range but fewer samples changes autobin size
            var data2 = [1.5, 5];
            Plotly.restyle(gd, 'x', [data2]);
            expect(gd._fullData[0].xbins).toEqual({start: -2.5, end: 7.5, size: 5});
            expect(gd._fullData[0].autobinx).toBe(true);

            // different range
            var data3 = [10, 20.2, 20, 30, 30, 30, 40, 40, 50];
            Plotly.restyle(gd, 'x', [data3]);
            expect(gd._fullData[0].xbins).toEqual({start: 5, end: 55, size: 10});
            expect(gd._fullData[0].autobinx).toBe(true);

            // explicit change to a bin attribute clears autobin
            Plotly.restyle(gd, 'xbins.start', 3);
            expect(gd._fullData[0].xbins).toEqual({start: 3, end: 55, size: 10});
            expect(gd._fullData[0].autobinx).toBe(false);

            // restart autobin
            Plotly.restyle(gd, 'autobinx', true);
            expect(gd._fullData[0].xbins).toEqual({start: 5, end: 55, size: 10});
            expect(gd._fullData[0].autobinx).toBe(true);
        });

        it('respects explicit autobin: false as a one-time autobin', function() {
            var data1 = [1.5, 2, 2, 3, 3, 3, 4, 4, 5];
            Plotly.plot(gd, [{x: data1, type: 'histogram', autobinx: false }]);
            // we have no bins, so even though autobin is false we have to autobin once
            expect(gd._fullData[0].xbins).toEqual({start: 1, end: 6, size: 1});
            expect(gd._fullData[0].autobinx).toBe(false);

            // since autobin is false, this will not change the bins
            var data2 = [1.5, 5];
            Plotly.restyle(gd, 'x', [data2]);
            expect(gd._fullData[0].xbins).toEqual({start: 1, end: 6, size: 1});
            expect(gd._fullData[0].autobinx).toBe(false);
        });

        it('allows changing axis type with new x data', function() {
            var x1 = [1, 1, 1, 1, 2, 2, 2, 3, 3, 4];
            var x2 = ['2017-01-01', '2017-01-01', '2017-01-01', '2017-01-02', '2017-01-02', '2017-01-03'];

            Plotly.newPlot(gd, [{x: x1, type: 'histogram'}]);
            expect(gd._fullLayout.xaxis.type).toBe('linear');
            expect(gd._fullLayout.xaxis.range).toBeCloseToArray([0.5, 4.5], 3);

            Plotly.restyle(gd, {x: [x2]});
            expect(gd._fullLayout.xaxis.type).toBe('date');
            expect(gd._fullLayout.xaxis.range).toEqual(['2016-12-31 12:00', '2017-01-03 12:00']);
        });

        it('can resize a plot with several histograms', function(done) {
            Plotly.newPlot(gd, [{
                type: 'histogram',
                x: [1, 1, 1, 1, 2, 2, 2, 3, 3, 4]
            }, {
                type: 'histogram',
                x: [1, 1, 1, 1, 2, 2, 2, 3, 3, 4]
            }], {
                width: 400,
                height: 400
            })
            .then(function() {
                expect(gd._fullLayout.width).toBe(400);
                expect(gd._fullLayout.height).toBe(400);

                gd._fullData.forEach(function(trace, i) {
                    expect(trace._autoBinFinished).toBeUndefined(i);
                });

                return Plotly.relayout(gd, {width: 500, height: 500});
            })
            .then(function() {
                expect(gd._fullLayout.width).toBe(500);
                expect(gd._fullLayout.height).toBe(500);

                gd._fullData.forEach(function(trace, i) {
                    expect(trace._autoBinFinished).toBeUndefined(i);
                });
            })
            .catch(fail)
            .then(done);
        });

        it('gives the right bar width for single-value histograms', function(done) {
            Plotly.newPlot(gd, [{
                type: 'histogram',
                x: [3, 3, 3],
                xbins: {start: 0, end: 10, size: 2}
            }])
            .then(function() {
                expect(gd._fullLayout.xaxis.range).toBeCloseToArray([2, 4], 3);
            })
            .catch(fail)
            .then(done);
        });
    });
});

describe('getBinSpanLabelRound', function() {
    function _test(leftGap, rightGap, edges, calendar, expected) {
        var ax = {type: 'not date'};
        // only date axes have any different treatment here. We could explicitly
        // test category
        if(calendar) {
            ax = {type: 'date', calendar: 'gregorian', range: [0, 1e7]};
            setConvert(ax);
        }

        var roundFn = getBinSpanLabelRound(leftGap, rightGap, edges, ax, calendar);

        var j = 0;
        var PREC = calendar ? 1 : 6;
        edges.forEach(function(edge, i) {
            if(i) {
                expect(roundFn(edge, true)).toBeCloseTo(expected[j], PREC, 'right ' + i);
                j++;
            }
            if(i < edges.length - 1) {
                expect(roundFn(edge)).toBeCloseTo(expected[j], PREC, 'left ' + i);
                j++;
            }
        });
    }

    it('works when the bin edges are round numbers and data are "continuous"', function() {
        _test(0.05, 0.3, [0, 2, 4, 6], false, [0, 1.9, 2, 3.9, 4, 5.9]);
        _test(0, 0.1, [0, 1, 2], false, [0, 0.9, 1, 1.9]);
        _test(0, 0.001, [0, 1, 2], false, [0, 0.999, 1, 1.999]);
        _test(0.1, 0.1, [115, 125, 135], false, [115, 124.9, 125, 134.9]);
        _test(0.1, 0.01, [115, 125, 135], false, [115, 124.99, 125, 134.99]);
        _test(10, 100, [5000, 6000, 7000], false, [5000, 5900, 6000, 6900]);

        // too small of a right gap: stop disambiguating data at the edge
        _test(0, 0.0009, [0, 1, 2], false, [0, 1, 1, 2]);
        _test(0.1, 0.009, [115, 125, 135], false, [115, 125, 125, 135]);
    });

    it('works when the bins are shifted to be less round than the data', function() {
        // integer or category data look like this - though categories don't even
        // get here normally, unless you explicitly ask to bin multiple categories
        // together, because uniqueValsPerBin will be true
        _test(0.5, 0.5, [-0.5, 4.5, 9.5], false, [0, 4, 5, 9]);

        _test(0.013, 0.087, [-0.013, 0.987, 1.987], false, [0, 0.9, 1, 1.9]);
        _test(500, 500, [4500, 9500, 14500], false, [5000, 9000, 10000, 14000]);
    });

    var jan17 = Date.UTC(2017, 0, 1);
    var feb17 = Date.UTC(2017, 1, 1);
    var mar17 = Date.UTC(2017, 2, 1);
    var jan18 = Date.UTC(2018, 0, 1);
    var jan19 = Date.UTC(2019, 0, 1);
    var j2116 = Date.UTC(2116, 0, 1);
    var j2117 = Date.UTC(2117, 0, 1);
    var j2216 = Date.UTC(2216, 0, 1);
    var j2217 = Date.UTC(2217, 0, 1);
    var sec = 1000;
    var min = 60 * sec;
    var hr = 60 * min;
    var day = 24 * hr;

    it('rounds dates to full fields (if larger than seconds) - round bin edges case', function() {
        // sub-second & 1-second resolution
        _test(0, 0.1, [jan17, jan17 + 1, jan17 + 2], 'gregorian',
            [jan17, jan17 + 0.9, jan17 + 1, jan17 + 1.9]);
        _test(1, 3, [jan17, jan17 + 20, jan17 + 40], 'gregorian',
            [jan17, jan17 + 19, jan17 + 20, jan17 + 39]);
        _test(5, 35, [jan17, jan17 + 1000, jan17 + 2000], 'gregorian',
            [jan17, jan17 + 990, jan17 + 1000, jan17 + 1990]);
        _test(0, 100, [jan17, jan17 + 20000, jan17 + 40000], 'gregorian',
            [jan17, jan17 + 19900, jan17 + 20000, jan17 + 39900]);
        _test(100, 2000, [jan17, jan17 + 10000, jan17 + 20000], 'gregorian',
            [jan17, jan17 + 9000, jan17 + 10000, jan17 + 19000]);

        // > second, only go to the next full field
        // 30 sec gap - still show seconds
        _test(0, 30 * sec, [jan17, jan17 + 5 * min, jan17 + 10 * min], 'gregorian',
            [jan17, jan17 + 5 * min - sec, jan17 + 5 * min, jan17 + 10 * min - sec]);
        // 1-minute gap round to minutes
        _test(10 * sec, min, [jan17, jan17 + 5 * min, jan17 + 10 * min], 'gregorian',
            [jan17, jan17 + 4 * min, jan17 + 5 * min, jan17 + 9 * min]);
        // 30-minute gap - still show minutes
        _test(0, 30 * min, [jan17, jan17 + day, jan17 + 2 * day], 'gregorian',
            [jan17, jan17 + day - min, jan17 + day, jan17 + 2 * day - min]);
        // 1-hour gap - round to hours
        _test(0, hr, [jan17, jan17 + day, jan17 + 2 * day], 'gregorian',
            [jan17, jan17 + day - hr, jan17 + day, jan17 + 2 * day - hr]);
        // 12-hour gap - still hours
        _test(0, 12 * hr, [jan17, feb17, mar17], 'gregorian',
            [jan17, feb17 - hr, feb17, mar17 - hr]);
        // 1-day gap - round to days
        _test(0, day, [jan17, feb17, mar17], 'gregorian',
            [jan17, feb17 - day, feb17, mar17 - day]);
        // 15-day gap - still days
        _test(0, 15 * day, [jan17, jan18, jan19], 'gregorian',
            [jan17, jan18 - day, jan18, jan19 - day]);
        // 28-day gap STILL gets days - in principle this might happen with data
        // that's actually monthly, if the bins edges fall on different months
        // (ie not full years) but that's a very weird edge case so I'll ignore it!
        _test(0, 28 * day, [jan17, jan18, jan19], 'gregorian',
            [jan17, jan18 - day, jan18, jan19 - day]);
        // 31-day gap - months
        _test(0, 31 * day, [jan17, jan18, jan19], 'gregorian',
            [jan17, Date.UTC(2017, 11, 1), jan18, Date.UTC(2018, 11, 1)]);
        // 365-day gap - years have enough buffer to handle leap vs nonleap years
        _test(0, 365 * day, [jan17, j2117, j2217], 'gregorian',
            [jan17, j2116, j2117, j2216]);
        // no bigger rounding than years
        _test(0, 40 * 365 * day, [jan17, j2117, j2217], 'gregorian',
            [jan17, j2116, j2117, j2216]);
    });

    it('rounds dates to full fields (if larger than seconds) - round data case', function() {
        // sub-second & 1-second resolution
        _test(0.05, 0.05, [jan17 - 0.05, jan17 + 0.95, jan17 + 1.95], 'gregorian',
            [jan17, jan17 + 0.9, jan17 + 1, jan17 + 1.9]);
        _test(0.5, 3.5, [jan17 - 0.5, jan17 + 19.5, jan17 + 39.5], 'gregorian',
            [jan17, jan17 + 19, jan17 + 20, jan17 + 39]);
        _test(5, 35, [jan17 - 5, jan17 + 995, jan17 + 1995], 'gregorian',
            [jan17, jan17 + 990, jan17 + 1000, jan17 + 1990]);
        _test(50, 50, [jan17 - 50, jan17 + 19950, jan17 + 39950], 'gregorian',
            [jan17, jan17 + 19900, jan17 + 20000, jan17 + 39900]);
        _test(500, 2500, [jan17 - 500, jan17 + 9500, jan17 + 19500], 'gregorian',
            [jan17, jan17 + 9000, jan17 + 10000, jan17 + 19000]);

        // > second, only go to the next full field
        // 30 sec gap - still show seconds
        _test(15 * sec, 15 * sec, [jan17 - 15 * sec, jan17 + 5 * min - 15 * sec, jan17 + 10 * min - 15 * sec], 'gregorian',
            [jan17 - 15 * sec, jan17 + 5 * min - 16 * sec, jan17 + 5 * min - 15 * sec, jan17 + 10 * min - 16 * sec]);
        // 1-minute gap round to minutes
        _test(30 * sec, 30 * sec, [jan17 - 30 * sec, jan17 + 5 * min - 30 * sec, jan17 + 10 * min - 30 * sec], 'gregorian',
            [jan17, jan17 + 4 * min, jan17 + 5 * min, jan17 + 9 * min]);
        // 30-minute gap - still show minutes
        _test(15 * min, 15 * min, [jan17 - 15 * min, jan17 + day - 15 * min, jan17 + 2 * day - 15 * min], 'gregorian',
            [jan17 - 15 * min, jan17 + day - 16 * min, jan17 + day - 15 * min, jan17 + 2 * day - 16 * min]);
        // 1-hour gap - round to hours
        _test(30 * min, 30 * min, [jan17 - 30 * min, jan17 + day - 30 * min, jan17 + 2 * day - 30 * min], 'gregorian',
            [jan17, jan17 + day - hr, jan17 + day, jan17 + 2 * day - hr]);
        // 12-hour gap - still hours
        _test(6 * hr, 6 * hr, [jan17 - 6 * hr, feb17 - 6 * hr, mar17 - 6 * hr], 'gregorian',
            [jan17 - 6 * hr, feb17 - 7 * hr, feb17 - 6 * hr, mar17 - 7 * hr]);
        // 1-day gap - round to days
        _test(12 * hr, 12 * hr, [jan17 - 12 * hr, feb17 - 12 * hr, mar17 - 12 * hr], 'gregorian',
            [jan17, feb17 - day, feb17, mar17 - day]);
        // 15-day gap - still days
        _test(7 * day, 8 * day, [jan17 - 7 * day, jan18 - 7 * day, jan19 - 7 * day], 'gregorian',
            [jan17 - 7 * day, jan18 - 8 * day, jan18 - 7 * day, jan19 - 8 * day]);
        // 28-day gap STILL gets days - in principle this might happen with data
        // that's actually monthly, if the bins edges fall on different months
        // (ie not full years) but that's a very weird edge case so I'll ignore it!
        _test(14 * day, 14 * day, [jan17 - 14 * day, jan18 - 14 * day, jan19 - 14 * day], 'gregorian',
            [jan17 - 14 * day, jan18 - 15 * day, jan18 - 14 * day, jan19 - 15 * day]);
        // 31-day gap - months
        _test(15 * day, 16 * day, [jan17 - 15 * day, jan18 - 15 * day, jan19 - 15 * day], 'gregorian',
            [jan17, Date.UTC(2017, 11, 1), jan18, Date.UTC(2018, 11, 1)]);
        // 365-day gap - years have enough buffer to handle leap vs nonleap years
        _test(182 * day, 183 * day, [jan17 - 182 * day, j2117 - 182 * day, j2217 - 182 * day], 'gregorian',
            [jan17, j2116, j2117, j2216]);
        // no bigger rounding than years
        _test(20 * 365 * day, 20 * 365 * day, [jan17, j2117, j2217], 'gregorian',
            [jan17, j2116, j2117, j2216]);
    });

    it('rounds (mostly) correctly when using world calendars', function() {
        var cn = 'chinese';
        var cn8 = Lib.dateTime2ms('1995-08-01', cn);
        var cn8i = Lib.dateTime2ms('1995-08i-01', cn);
        var cn9 = Lib.dateTime2ms('1995-09-01', cn);

        var cn1_00 = Lib.dateTime2ms('2000-01-01', cn);
        var cn1_01 = Lib.dateTime2ms('2001-01-01', cn);
        var cn1_02 = Lib.dateTime2ms('2002-01-01', cn);
        var cn1_10 = Lib.dateTime2ms('2010-01-01', cn);
        var cn1_20 = Lib.dateTime2ms('2020-01-01', cn);

        _test(100, 2000, [cn8i, cn8i + 10000, cn8i + 20000], cn,
            [cn8i, cn8i + 9000, cn8i + 10000, cn8i + 19000]);
        _test(500, 2500, [cn8i - 500, cn8i + 9500, cn8i + 19500], cn,
            [cn8i, cn8i + 9000, cn8i + 10000, cn8i + 19000]);

        _test(0, day, [cn8, cn8i, cn9], cn,
            [cn8, cn8i - day, cn8i, cn9 - day]);
        _test(12 * hr, 12 * hr, [cn8 - 12 * hr, cn8i - 12 * hr, cn9 - 12 * hr], cn,
            [cn8, cn8i - day, cn8i, cn9 - day]);

        _test(0, 28 * day, [cn1_00, cn1_01, cn1_02], cn,
            [cn1_00, Lib.dateTime2ms('2000-12-01', cn), cn1_01, Lib.dateTime2ms('2001-12-01', cn)]);
        _test(14 * day, 14 * day, [cn1_00 - 14 * day, cn1_01 - 14 * day, cn1_02 - 14 * day], cn,
            [cn1_00, Lib.dateTime2ms('2000-12-01', cn), cn1_01, Lib.dateTime2ms('2001-12-01', cn)]);

        _test(0, 353 * day, [cn1_00, cn1_10, cn1_20], cn,
            [cn1_00, Lib.dateTime2ms('2009-01-01', cn), cn1_10, Lib.dateTime2ms('2019-01-01', cn)]);
        // occasionally we give extra precision for world dates (month when it should be year
        // or day when it should be month). That's better than doing the opposite... not going
        // to fix now, too many edge cases, better not to complicate the logic for them all.
        _test(176 * day, 177 * day, [cn1_00 - 176 * day, cn1_10 - 176 * day, cn1_20 - 176 * day], cn, [
            Lib.dateTime2ms('1999-08-01', cn), Lib.dateTime2ms('2009-07-01', cn),
            Lib.dateTime2ms('2009-08-01', cn), Lib.dateTime2ms('2019-07-01', cn)
        ]);
    });
});
