<commit_before>/**
* Copyright 2012-2019, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var d3 = require('d3');

var Lib = require('../../lib');
var Plots = require('../../plots/plots');
var Registry = require('../../registry');
var Events = require('../../lib/events');
var dragElement = require('../dragelement');
var Drawing = require('../drawing');
var Color = require('../color');
var svgTextUtils = require('../../lib/svg_text_utils');
var handleClick = require('./handle_click');

var constants = require('./constants');
var alignmentConstants = require('../../constants/alignment');
var LINE_SPACING = alignmentConstants.LINE_SPACING;
var FROM_TL = alignmentConstants.FROM_TL;
var FROM_BR = alignmentConstants.FROM_BR;

var getLegendData = require('./get_legend_data');
var style = require('./style');
var helpers = require('./helpers');

module.exports = function draw(gd) {
    var fullLayout = gd._fullLayout;
    var clipId = 'legend' + fullLayout._uid;

    if(!fullLayout._infolayer || !gd.calcdata) return;

    if(!gd._legendMouseDownTime) gd._legendMouseDownTime = 0;

    var opts = fullLayout.legend;
    var legendData = fullLayout.showlegend && getLegendData(gd.calcdata, opts);
    var hiddenSlices = fullLayout.hiddenlabels || [];

    if(!fullLayout.showlegend || !legendData.length) {
        fullLayout._infolayer.selectAll('.legend').remove();
        fullLayout._topdefs.select('#' + clipId).remove();
        return Plots.autoMargin(gd, 'legend');
    }

    var legend = Lib.ensureSingle(fullLayout._infolayer, 'g', 'legend', function(s) {
        s.attr('pointer-events', 'all');
    });

    var clipPath = Lib.ensureSingleById(fullLayout._topdefs, 'clipPath', clipId, function(s) {
        s.append('rect');
    });

    var bg = Lib.ensureSingle(legend, 'rect', 'bg', function(s) {
        s.attr('shape-rendering', 'crispEdges');
    });
    bg.call(Color.stroke, opts.bordercolor)
        .call(Color.fill, opts.bgcolor)
        .style('stroke-width', opts.borderwidth + 'px');

    var scrollBox = Lib.ensureSingle(legend, 'g', 'scrollbox');

    var scrollBar = Lib.ensureSingle(legend, 'rect', 'scrollbar', function(s) {
        s.attr(constants.scrollBarEnterAttrs)
         .call(Color.fill, constants.scrollBarColor);
    });

    var groups = scrollBox.selectAll('g.groups').data(legendData);
    groups.enter().append('g').attr('class', 'groups');
    groups.exit().remove();

    var traces = groups.selectAll('g.traces').data(Lib.identity);
    traces.enter().append('g').attr('class', 'traces');
    traces.exit().remove();

    traces.style('opacity', function(d) {
        var trace = d[0].trace;
        if(Registry.traceIs(trace, 'pie-like')) {
            return hiddenSlices.indexOf(d[0].label) !== -1 ? 0.5 : 1;
        } else {
            return trace.visible === 'legendonly' ? 0.5 : 1;
        }
    })
    .each(function() { d3.select(this).call(drawTexts, gd); })
    .call(style, gd)
    .each(function() { d3.select(this).call(setupTraceToggle, gd); });

    Lib.syncOrAsync([
        Plots.previousPromises,
        function() { return computeLegendDimensions(gd, groups, traces); },
        function() { return expandMargin(gd); },
        function() {
            // Position and size the legend
            var lxMin = 0;
            var lxMax = fullLayout.width;
            var lyMin = 0;
            var lyMax = fullLayout.height;

            // Scroll section must be executed after repositionLegend.
            // It requires the legend width, height, x and y to position the scrollbox
            // and these values are mutated in repositionLegend.
            var gs = fullLayout._size;
            var lx = gs.l + gs.w * opts.x;
            var ly = gs.t + gs.h * (1 - opts.y);

            if(Lib.isRightAnchor(opts)) {
                lx -= opts._width;
            } else if(Lib.isCenterAnchor(opts)) {
                lx -= opts._width / 2;
            }

            if(Lib.isBottomAnchor(opts)) {
                ly -= opts._height;
            } else if(Lib.isMiddleAnchor(opts)) {
                ly -= opts._height / 2;
            }

            // Make sure the legend left and right sides are visible
            var legendWidth = opts._width;
            var legendWidthMax = gs.w;

            if(legendWidth > legendWidthMax) {
                lx = gs.l;
                legendWidth = legendWidthMax;
            } else {
                if(lx + legendWidth > lxMax) lx = lxMax - legendWidth;
                if(lx < lxMin) lx = lxMin;
                legendWidth = Math.min(lxMax - lx, opts._width);
            }

            // Make sure the legend top and bottom are visible
            // (legends with a scroll bar are not allowed to stretch beyond the extended
            // margins)
            var legendHeight = opts._height;
            var legendHeightMax = gs.h;

            if(legendHeight > legendHeightMax) {
                ly = gs.t;
                legendHeight = legendHeightMax;
            } else {
                if(ly + legendHeight > lyMax) ly = lyMax - legendHeight;
                if(ly < lyMin) ly = lyMin;
                legendHeight = Math.min(lyMax - ly, opts._height);
            }

            // Set size and position of all the elements that make up a legend:
            // legend, background and border, scroll box and scroll bar
            Drawing.setTranslate(legend, lx, ly);

            // to be safe, remove previous listeners
            scrollBar.on('.drag', null);
            legend.on('wheel', null);

            if(opts._height <= legendHeight || gd._context.staticPlot) {
                // if scrollbar should not be shown.
                bg.attr({
                    width: legendWidth - opts.borderwidth,
                    height: legendHeight - opts.borderwidth,
                    x: opts.borderwidth / 2,
                    y: opts.borderwidth / 2
                });

                Drawing.setTranslate(scrollBox, 0, 0);

                clipPath.select('rect').attr({
                    width: legendWidth - 2 * opts.borderwidth,
                    height: legendHeight - 2 * opts.borderwidth,
                    x: opts.borderwidth,
                    y: opts.borderwidth
                });

                Drawing.setClipUrl(scrollBox, clipId, gd);

                Drawing.setRect(scrollBar, 0, 0, 0, 0);
                delete opts._scrollY;
            } else {
                var scrollBarHeight = Math.max(constants.scrollBarMinHeight,
                    legendHeight * legendHeight / opts._height);
                var scrollBarYMax = legendHeight -
                    scrollBarHeight -
                    2 * constants.scrollBarMargin;
                var scrollBoxYMax = opts._height - legendHeight;
                var scrollRatio = scrollBarYMax / scrollBoxYMax;

                var scrollBoxY = Math.min(opts._scrollY || 0, scrollBoxYMax);

                // increase the background and clip-path width
                // by the scrollbar width and margin
                bg.attr({
                    width: legendWidth -
                        2 * opts.borderwidth +
                        constants.scrollBarWidth +
                        constants.scrollBarMargin,
                    height: legendHeight - opts.borderwidth,
                    x: opts.borderwidth / 2,
                    y: opts.borderwidth / 2
                });

                clipPath.select('rect').attr({
                    width: legendWidth -
                        2 * opts.borderwidth +
                        constants.scrollBarWidth +
                        constants.scrollBarMargin,
                    height: legendHeight - 2 * opts.borderwidth,
                    x: opts.borderwidth,
                    y: opts.borderwidth + scrollBoxY
                });

                Drawing.setClipUrl(scrollBox, clipId, gd);

                scrollHandler(scrollBoxY, scrollBarHeight, scrollRatio);

                legend.on('wheel', function() {
                    scrollBoxY = Lib.constrain(
                        opts._scrollY +
                            d3.event.deltaY / scrollBarYMax * scrollBoxYMax,
                        0, scrollBoxYMax);
                    scrollHandler(scrollBoxY, scrollBarHeight, scrollRatio);
                    if(scrollBoxY !== 0 && scrollBoxY !== scrollBoxYMax) {
                        d3.event.preventDefault();
                    }
                });

                var eventY0, scrollBoxY0;

                var drag = d3.behavior.drag()
                .on('dragstart', function() {
                    eventY0 = d3.event.sourceEvent.clientY;
                    scrollBoxY0 = scrollBoxY;
                })
                .on('drag', function() {
                    var e = d3.event.sourceEvent;
                    if(e.buttons === 2 || e.ctrlKey) return;

                    scrollBoxY = Lib.constrain(
                        (e.clientY - eventY0) / scrollRatio + scrollBoxY0,
                        0, scrollBoxYMax);
                    scrollHandler(scrollBoxY, scrollBarHeight, scrollRatio);
                });

                scrollBar.call(drag);
            }


            function scrollHandler(scrollBoxY, scrollBarHeight, scrollRatio) {
                opts._scrollY = gd._fullLayout.legend._scrollY = scrollBoxY;
                Drawing.setTranslate(scrollBox, 0, -scrollBoxY);

                Drawing.setRect(
                    scrollBar,
                    legendWidth,
                    constants.scrollBarMargin + scrollBoxY * scrollRatio,
                    constants.scrollBarWidth,
                    scrollBarHeight
                );
                clipPath.select('rect').attr('y', bw + scrollBoxY);
            }

            if(gd._context.edits.legendPosition) {
                var xf, yf, x0, y0;

                legend.classed('cursor-move', true);

                dragElement.init({
                    element: legend.node(),
                    gd: gd,
                    prepFn: function() {
                        var transform = Drawing.getTranslate(legend);

                        x0 = transform.x;
                        y0 = transform.y;
                    },
                    moveFn: function(dx, dy) {
                        var newX = x0 + dx;
                        var newY = y0 + dy;

                        Drawing.setTranslate(legend, newX, newY);

                        xf = dragElement.align(newX, 0, gs.l, gs.l + gs.w, opts.xanchor);
                        yf = dragElement.align(newY, 0, gs.t + gs.h, gs.t, opts.yanchor);
                    },
                    doneFn: function() {
                        if(xf !== undefined && yf !== undefined) {
                            Registry.call('_guiRelayout', gd, {'legend.x': xf, 'legend.y': yf});
                        }
                    },
                    clickFn: function(numClicks, e) {
                        var clickedTrace = fullLayout._infolayer.selectAll('g.traces').filter(function() {
                            var bbox = this.getBoundingClientRect();
                            return (
                                e.clientX >= bbox.left && e.clientX <= bbox.right &&
                                e.clientY >= bbox.top && e.clientY <= bbox.bottom
                            );
                        });
                        if(clickedTrace.size() > 0) {
                            clickOrDoubleClick(gd, legend, clickedTrace, numClicks, e);
                        }
                    }
                });
            }
        }], gd);
};

function clickOrDoubleClick(gd, legend, legendItem, numClicks, evt) {
    var trace = legendItem.data()[0][0].trace;
    var evtData = {
        event: evt,
        node: legendItem.node(),
        curveNumber: trace.index,
        expandedIndex: trace._expandedIndex,
        data: gd.data,
        layout: gd.layout,
        frames: gd._transitionData._frames,
        config: gd._context,
        fullData: gd._fullData,
        fullLayout: gd._fullLayout
    };

    if(trace._group) {
        evtData.group = trace._group;
    }
    if(Registry.traceIs(trace, 'pie-like')) {
        evtData.label = legendItem.datum()[0].label;
    }

    var clickVal = Events.triggerHandler(gd, 'plotly_legendclick', evtData);
    if(clickVal === false) return;

    if(numClicks === 1) {
        legend._clickTimeout = setTimeout(function() {
            handleClick(legendItem, gd, numClicks);
        }, gd._context.doubleClickDelay);
    } else if(numClicks === 2) {
        if(legend._clickTimeout) clearTimeout(legend._clickTimeout);
        gd._legendMouseDownTime = 0;

        var dblClickVal = Events.triggerHandler(gd, 'plotly_legenddoubleclick', evtData);
        if(dblClickVal !== false) handleClick(legendItem, gd, numClicks);
    }
}

function drawTexts(g, gd) {
    var legendItem = g.data()[0][0];
    var fullLayout = gd._fullLayout;
    var opts = fullLayout.legend;
    var trace = legendItem.trace;
    var isPieLike = Registry.traceIs(trace, 'pie-like');
    var traceIndex = trace.index;
    var isEditable = gd._context.edits.legendText && !isPieLike;
    var maxNameLength = opts._maxNameLength;

    var name = isPieLike ? legendItem.label : trace.name;
    if(trace._meta) {
        name = Lib.templateString(name, trace._meta);
    }

    var textEl = Lib.ensureSingle(g, 'text', 'legendtext');

    textEl.attr('text-anchor', 'start')
        .classed('user-select-none', true)
        .call(Drawing.font, fullLayout.legend.font)
        .text(isEditable ? ensureLength(name, maxNameLength) : name);

    svgTextUtils.positionText(textEl, constants.textGap, 0);

    function textLayout(s) {
        svgTextUtils.convertToTspans(s, gd, function() {
            computeTextDimensions(g, gd);
        });
    }

    if(isEditable) {
        textEl.call(svgTextUtils.makeEditable, {gd: gd, text: name})
            .call(textLayout)
            .on('edit', function(newName) {
                this.text(ensureLength(newName, maxNameLength))
                    .call(textLayout);

                var fullInput = legendItem.trace._fullInput || {};
                var update = {};

                if(Registry.hasTransform(fullInput, 'groupby')) {
                    var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby');
                    var index = groupbyIndices[groupbyIndices.length - 1];

                    var kcont = Lib.keyedContainer(fullInput, 'transforms[' + index + '].styles', 'target', 'value.name');

                    kcont.set(legendItem.trace._group, newName);

                    update = kcont.constructUpdate();
                } else {
                    update.name = newName;
                }

                return Registry.call('_guiRestyle', gd, update, traceIndex);
            });
    } else {
        textLayout(textEl);
    }
}

/*
 * Make sure we have a reasonably clickable region.
 * If this string is missing or very short, pad it with spaces out to at least
 * 4 characters, up to the max length of other labels, on the assumption that
 * most characters are wider than spaces so a string of spaces will usually be
 * no wider than the real labels.
 */
function ensureLength(str, maxLength) {
    var targetLength = Math.max(4, maxLength);
    if(str && str.trim().length >= targetLength / 2) return str;
    str = str || '';
    for(var i = targetLength - str.length; i > 0; i--) str += ' ';
    return str;
}

function setupTraceToggle(g, gd) {
    var doubleClickDelay = gd._context.doubleClickDelay;
    var newMouseDownTime;
    var numClicks = 1;

    var traceToggle = Lib.ensureSingle(g, 'rect', 'legendtoggle', function(s) {
        s.style('cursor', 'pointer')
            .attr('pointer-events', 'all')
            .call(Color.fill, 'rgba(0,0,0,0)');
    });

    traceToggle.on('mousedown', function() {
        newMouseDownTime = (new Date()).getTime();
        if(newMouseDownTime - gd._legendMouseDownTime < doubleClickDelay) {
            // in a click train
            numClicks += 1;
        } else {
            // new click train
            numClicks = 1;
            gd._legendMouseDownTime = newMouseDownTime;
        }
    });
    traceToggle.on('mouseup', function() {
        if(gd._dragged || gd._editing) return;
        var legend = gd._fullLayout.legend;

        if((new Date()).getTime() - gd._legendMouseDownTime > doubleClickDelay) {
            numClicks = Math.max(numClicks - 1, 1);
        }

        clickOrDoubleClick(gd, legend, g, numClicks, d3.event);
    });
}

function computeTextDimensions(g, gd) {
    var legendItem = g.data()[0][0];

    if(!legendItem.trace.showlegend) {
        g.remove();
        return;
    }

    var mathjaxGroup = g.select('g[class*=math-group]');
    var mathjaxNode = mathjaxGroup.node();
    var opts = gd._fullLayout.legend;
    var lineHeight = opts.font.size * LINE_SPACING;
    var height, width;

    if(mathjaxNode) {
        var mathjaxBB = Drawing.bBox(mathjaxNode);

        height = mathjaxBB.height;
        width = mathjaxBB.width;

        Drawing.setTranslate(mathjaxGroup, 0, (height / 4));
    } else {
        var text = g.select('.legendtext');
        var textLines = svgTextUtils.lineCount(text);
        var textNode = text.node();

        height = lineHeight * textLines;
        width = textNode ? Drawing.bBox(textNode).width : 0;

        // approximation to height offset to center the font
        // to avoid getBoundingClientRect
        var textY = lineHeight * (0.3 + (1 - textLines) / 2);
        svgTextUtils.positionText(text, constants.textGap, textY);
    }

    legendItem.lineHeight = lineHeight;
    legendItem.height = Math.max(height, 16) + 3;
    legendItem.width = width;
}

function computeLegendDimensions(gd, groups, traces) {
    var fullLayout = gd._fullLayout;
    var opts = fullLayout.legend;
    var isGrouped = helpers.isGrouped(opts);

    var bw = opts.borderwidth;
    var bw2 = 2 * bw;
    var textGap = constants.textGap;
    var itemGap = constants.itemGap;
    var endPad = 2 * (bw + itemGap);

    var extraWidth = 0;
    opts._width = 0;
    opts._height = 0;

    if(helpers.isVertical(opts)) {
        if(isGrouped) {
            groups.each(function(d, i) {
                Drawing.setTranslate(this, 0, i * opts.tracegroupgap);
            });
        }

        traces.each(function(d) {
            var h = d[0].height;
            Drawing.setTranslate(this, bw, itemGap + bw + opts._height + h / 2);
            opts._height += h;
            opts._width = Math.max(opts._width, d[0].width);
        });

        opts._width += itemGap + textGap + bw2;
        opts._height += endPad;

        if(isGrouped) {
            opts._height += (opts._lgroupsLength - 1) * opts.tracegroupgap;
        }

        extraWidth = textGap;
    } else if(isGrouped) {
        var maxHeight = 0;
        var maxWidth = 0;
        var maxItems = 0;
        var groupData = groups.data();
        var i;

        for(i = 0; i < groupData.length; i++) {
            var group = groupData[i];
            var groupWidths = group.map(function(d) { return d[0].width; });
            var groupHeight = group.reduce(function(a, b) { return a + b[0].height; }, 0);
            maxWidth = Math.max(maxWidth, Lib.aggNums(Math.max, null, groupWidths));
            maxHeight = Math.max(maxHeight, groupHeight);
            maxItems = Math.max(maxItems, group.length);
        }

        maxWidth += itemGap + textGap;

        var groupXOffsets = [opts._width];
        var groupYOffsets = [];
        var rowNum = 0;
        for(i = 0; i < groupData.length; i++) {
            if(fullLayout._size.w < (bw + opts._width + itemGap + maxWidth)) {
                groupXOffsets[groupXOffsets.length - 1] = groupXOffsets[0];
                opts._width = maxWidth;
                rowNum++;
            } else {
                opts._width += maxWidth + bw;
            }

            var rowYOffset = (rowNum * maxHeight);
            rowYOffset += rowNum > 0 ? opts.tracegroupgap : 0;

            groupYOffsets.push(rowYOffset);
            groupXOffsets.push(opts._width);
        }

        groups.each(function(d, i) {
            Drawing.setTranslate(this, groupXOffsets[i], groupYOffsets[i]);
        });

        groups.each(function() {
            var group = d3.select(this);
            var groupTraces = group.selectAll('g.traces');
            var groupHeight = 0;

            groupTraces.each(function(d) {
                var h = d[0].height;
                Drawing.setTranslate(this, 0, itemGap + bw + groupHeight + h / 2);
                groupHeight += h;
            });
        });

        var maxYLegend = groupYOffsets[groupYOffsets.length - 1] + maxHeight;
        opts._height = maxYLegend + endPad;

        var maxOffset = Math.max.apply(null, groupXOffsets);
        opts._width = maxOffset + maxWidth + textGap + bw2;
    } else {
        var rowHeight = 0;
        var maxTraceHeight = 0;
        var maxTraceWidth = 0;
        var offsetX = 0;
        var fullTracesWidth = 0;

        // calculate largest width for traces and use for width of all legend items
        traces.each(function(d) {
            maxTraceWidth = Math.max(maxTraceWidth, textGap + d[0].width);
            fullTracesWidth += textGap + d[0].width + itemGap;
        });

        // check if legend fits in one row
        var oneRowLegend = fullLayout._size.w > bw + fullTracesWidth - itemGap;

        traces.each(function(d) {
            var h = d[0].height;
            var traceWidth = oneRowLegend ? textGap + d[0].width : maxTraceWidth;

            if((bw + offsetX + itemGap + traceWidth) > fullLayout._size.w) {
                offsetX = 0;
                rowHeight += maxTraceHeight;
                opts._height += maxTraceHeight;
                // reset for next row
                maxTraceHeight = 0;
            }

            Drawing.setTranslate(this, bw + offsetX, itemGap + bw + h / 2 + rowHeight);
            opts._width += itemGap + traceWidth;

            // keep track of tallest trace in group
            offsetX += itemGap + traceWidth;
            maxTraceHeight = Math.max(maxTraceHeight, h);
        });

        if(oneRowLegend) {
            opts._height = maxTraceHeight;
        } else {
            opts._height += maxTraceHeight;
        }

        opts._width += bw2;
        opts._height += endPad;
    }

    // make sure we're only getting full pixels
    opts._width = Math.ceil(opts._width);
    opts._height = Math.ceil(opts._height);

    var isEditable = (
        gd._context.edits.legendText ||
        gd._context.edits.legendPosition
    );

    traces.each(function(d) {
        var h = d[0].height;
        Drawing.setRect(d3.select(this).select('.legendtoggle'),
            0,
            -h / 2,
            (isEditable ? 0 : opts._width) + extraWidth,
            h
        );
    });
}

function expandMargin(gd) {
    var fullLayout = gd._fullLayout;
    var opts = fullLayout.legend;

    var xanchor = 'left';
    if(Lib.isRightAnchor(opts)) {
        xanchor = 'right';
    } else if(Lib.isCenterAnchor(opts)) {
        xanchor = 'center';
    }

    var yanchor = 'top';
    if(Lib.isBottomAnchor(opts)) {
        yanchor = 'bottom';
    } else if(Lib.isMiddleAnchor(opts)) {
        yanchor = 'middle';
    }

    // lastly check if the margin auto-expand has changed
    Plots.autoMargin(gd, 'legend', {
        x: opts.x,
        y: opts.y,
        l: opts._width * (FROM_TL[xanchor]),
        r: opts._width * (FROM_BR[xanchor]),<commit_msg>some more linting<commit_after>/**
* Copyright 2012-2019, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var d3 = require('d3');

var Lib = require('../../lib');
var Plots = require('../../plots/plots');
var Registry = require('../../registry');
var Events = require('../../lib/events');
var dragElement = require('../dragelement');
var Drawing = require('../drawing');
var Color = require('../color');
var svgTextUtils = require('../../lib/svg_text_utils');
var handleClick = require('./handle_click');

var constants = require('./constants');
var alignmentConstants = require('../../constants/alignment');
var LINE_SPACING = alignmentConstants.LINE_SPACING;
var FROM_TL = alignmentConstants.FROM_TL;
var FROM_BR = alignmentConstants.FROM_BR;

var getLegendData = require('./get_legend_data');
var style = require('./style');
var helpers = require('./helpers');

module.exports = function draw(gd) {
    var fullLayout = gd._fullLayout;
    var clipId = 'legend' + fullLayout._uid;

    if(!fullLayout._infolayer || !gd.calcdata) return;

    if(!gd._legendMouseDownTime) gd._legendMouseDownTime = 0;

    var opts = fullLayout.legend;
    var legendData = fullLayout.showlegend && getLegendData(gd.calcdata, opts);
    var hiddenSlices = fullLayout.hiddenlabels || [];

    if(!fullLayout.showlegend || !legendData.length) {
        fullLayout._infolayer.selectAll('.legend').remove();
        fullLayout._topdefs.select('#' + clipId).remove();
        return Plots.autoMargin(gd, 'legend');
    }

    var legend = Lib.ensureSingle(fullLayout._infolayer, 'g', 'legend', function(s) {
        s.attr('pointer-events', 'all');
    });

    var clipPath = Lib.ensureSingleById(fullLayout._topdefs, 'clipPath', clipId, function(s) {
        s.append('rect');
    });

    var bg = Lib.ensureSingle(legend, 'rect', 'bg', function(s) {
        s.attr('shape-rendering', 'crispEdges');
    });
    bg.call(Color.stroke, opts.bordercolor)
        .call(Color.fill, opts.bgcolor)
        .style('stroke-width', opts.borderwidth + 'px');

    var scrollBox = Lib.ensureSingle(legend, 'g', 'scrollbox');

    var scrollBar = Lib.ensureSingle(legend, 'rect', 'scrollbar', function(s) {
        s.attr(constants.scrollBarEnterAttrs)
         .call(Color.fill, constants.scrollBarColor);
    });

    var groups = scrollBox.selectAll('g.groups').data(legendData);
    groups.enter().append('g').attr('class', 'groups');
    groups.exit().remove();

    var traces = groups.selectAll('g.traces').data(Lib.identity);
    traces.enter().append('g').attr('class', 'traces');
    traces.exit().remove();

    traces.style('opacity', function(d) {
        var trace = d[0].trace;
        if(Registry.traceIs(trace, 'pie-like')) {
            return hiddenSlices.indexOf(d[0].label) !== -1 ? 0.5 : 1;
        } else {
            return trace.visible === 'legendonly' ? 0.5 : 1;
        }
    })
    .each(function() { d3.select(this).call(drawTexts, gd); })
    .call(style, gd)
    .each(function() { d3.select(this).call(setupTraceToggle, gd); });

    Lib.syncOrAsync([
        Plots.previousPromises,
        function() { return computeLegendDimensions(gd, groups, traces); },
        function() { return expandMargin(gd); },
        function() {
            var gs = fullLayout._size;
            var bw = opts.borderwidth;

            var lx = gs.l + gs.w * opts.x;
            if(Lib.isRightAnchor(opts)) {
                lx -= opts._width;
            } else if(Lib.isCenterAnchor(opts)) {
                lx -= opts._width / 2;
            }


            // Make sure the legend left and right sides are visible
            var legendWidth = opts._width;
            var legendWidthMax = gs.w;

            if(legendWidth > legendWidthMax) {
                lx = gs.l;
                legendWidth = legendWidthMax;
            } else {
                if(lx + legendWidth > fullLayout.width) lx = fullLayout.width - legendWidth;
                if(lx < 0) lx = 0;
                legendWidth = Math.min(fullLayout.width - lx, opts._width);
            }

            var ly = gs.t + gs.h * (1 - opts.y);
            if(Lib.isBottomAnchor(opts)) {
                ly -= opts._height;
            } else if(Lib.isMiddleAnchor(opts)) {
                ly -= opts._height / 2;
            }

            // Make sure the legend top and bottom are visible
            // (legends with a scroll bar are not allowed to stretch beyond the extended
            // margins)
            var legendHeight = opts._height;
            var legendHeightMax = gs.h;

            if(legendHeight > legendHeightMax) {
                ly = gs.t;
                legendHeight = legendHeightMax;
            } else {
                if(ly + legendHeight > fullLayout.height) ly = fullLayout.height - legendHeight;
                if(ly < 0) ly = 0;
                legendHeight = Math.min(fullLayout.height - ly, opts._height);
            }

            // Set size and position of all the elements that make up a legend:
            // legend, background and border, scroll box and scroll bar
            Drawing.setTranslate(legend, lx, ly);

            // to be safe, remove previous listeners
            scrollBar.on('.drag', null);
            legend.on('wheel', null);

            if(opts._height <= legendHeight || gd._context.staticPlot) {
                // if scrollbar should not be shown.
                bg.attr({
                    width: legendWidth - bw,
                    height: legendHeight - bw,
                    x: bw / 2,
                    y: bw / 2
                });

                Drawing.setTranslate(scrollBox, 0, 0);

                clipPath.select('rect').attr({
                    width: legendWidth - 2 * bw,
                    height: legendHeight - 2 * bw,
                    x: bw,
                    y: bw
                });

                Drawing.setClipUrl(scrollBox, clipId, gd);

                Drawing.setRect(scrollBar, 0, 0, 0, 0);
                delete opts._scrollY;
            } else {
                var scrollBarHeight = Math.max(constants.scrollBarMinHeight,
                    legendHeight * legendHeight / opts._height);
                var scrollBarYMax = legendHeight -
                    scrollBarHeight -
                    2 * constants.scrollBarMargin;
                var scrollBoxYMax = opts._height - legendHeight;
                var scrollRatio = scrollBarYMax / scrollBoxYMax;

                var scrollBoxY = Math.min(opts._scrollY || 0, scrollBoxYMax);

                // increase the background and clip-path width
                // by the scrollbar width and margin
                bg.attr({
                    width: legendWidth -
                        2 * bw +
                        constants.scrollBarWidth +
                        constants.scrollBarMargin,
                    height: legendHeight - bw,
                    x: bw / 2,
                    y: bw / 2
                });

                clipPath.select('rect').attr({
                    width: legendWidth -
                        2 * bw +
                        constants.scrollBarWidth +
                        constants.scrollBarMargin,
                    height: legendHeight - 2 * bw,
                    x: bw,
                    y: bw + scrollBoxY
                });

                Drawing.setClipUrl(scrollBox, clipId, gd);

                scrollHandler(scrollBoxY, scrollBarHeight, scrollRatio);

                legend.on('wheel', function() {
                    scrollBoxY = Lib.constrain(
                        opts._scrollY +
                            d3.event.deltaY / scrollBarYMax * scrollBoxYMax,
                        0, scrollBoxYMax);
                    scrollHandler(scrollBoxY, scrollBarHeight, scrollRatio);
                    if(scrollBoxY !== 0 && scrollBoxY !== scrollBoxYMax) {
                        d3.event.preventDefault();
                    }
                });

                var eventY0, scrollBoxY0;

                var drag = d3.behavior.drag()
                .on('dragstart', function() {
                    eventY0 = d3.event.sourceEvent.clientY;
                    scrollBoxY0 = scrollBoxY;
                })
                .on('drag', function() {
                    var e = d3.event.sourceEvent;
                    if(e.buttons === 2 || e.ctrlKey) return;

                    scrollBoxY = Lib.constrain(
                        (e.clientY - eventY0) / scrollRatio + scrollBoxY0,
                        0, scrollBoxYMax);
                    scrollHandler(scrollBoxY, scrollBarHeight, scrollRatio);
                });

                scrollBar.call(drag);
            }

            function scrollHandler(scrollBoxY, scrollBarHeight, scrollRatio) {
                opts._scrollY = gd._fullLayout.legend._scrollY = scrollBoxY;
                Drawing.setTranslate(scrollBox, 0, -scrollBoxY);

                Drawing.setRect(
                    scrollBar,
                    legendWidth,
                    constants.scrollBarMargin + scrollBoxY * scrollRatio,
                    constants.scrollBarWidth,
                    scrollBarHeight
                );
                clipPath.select('rect').attr('y', bw + scrollBoxY);
            }

            if(gd._context.edits.legendPosition) {
                var xf, yf, x0, y0;

                legend.classed('cursor-move', true);

                dragElement.init({
                    element: legend.node(),
                    gd: gd,
                    prepFn: function() {
                        var transform = Drawing.getTranslate(legend);
                        x0 = transform.x;
                        y0 = transform.y;
                    },
                    moveFn: function(dx, dy) {
                        var newX = x0 + dx;
                        var newY = y0 + dy;

                        Drawing.setTranslate(legend, newX, newY);

                        xf = dragElement.align(newX, 0, gs.l, gs.l + gs.w, opts.xanchor);
                        yf = dragElement.align(newY, 0, gs.t + gs.h, gs.t, opts.yanchor);
                    },
                    doneFn: function() {
                        if(xf !== undefined && yf !== undefined) {
                            Registry.call('_guiRelayout', gd, {'legend.x': xf, 'legend.y': yf});
                        }
                    },
                    clickFn: function(numClicks, e) {
                        var clickedTrace = fullLayout._infolayer.selectAll('g.traces').filter(function() {
                            var bbox = this.getBoundingClientRect();
                            return (
                                e.clientX >= bbox.left && e.clientX <= bbox.right &&
                                e.clientY >= bbox.top && e.clientY <= bbox.bottom
                            );
                        });
                        if(clickedTrace.size() > 0) {
                            clickOrDoubleClick(gd, legend, clickedTrace, numClicks, e);
                        }
                    }
                });
            }
        }], gd);
};

function clickOrDoubleClick(gd, legend, legendItem, numClicks, evt) {
    var trace = legendItem.data()[0][0].trace;
    var evtData = {
        event: evt,
        node: legendItem.node(),
        curveNumber: trace.index,
        expandedIndex: trace._expandedIndex,
        data: gd.data,
        layout: gd.layout,
        frames: gd._transitionData._frames,
        config: gd._context,
        fullData: gd._fullData,
        fullLayout: gd._fullLayout
    };

    if(trace._group) {
        evtData.group = trace._group;
    }
    if(Registry.traceIs(trace, 'pie-like')) {
        evtData.label = legendItem.datum()[0].label;
    }

    var clickVal = Events.triggerHandler(gd, 'plotly_legendclick', evtData);
    if(clickVal === false) return;

    if(numClicks === 1) {
        legend._clickTimeout = setTimeout(function() {
            handleClick(legendItem, gd, numClicks);
        }, gd._context.doubleClickDelay);
    } else if(numClicks === 2) {
        if(legend._clickTimeout) clearTimeout(legend._clickTimeout);
        gd._legendMouseDownTime = 0;

        var dblClickVal = Events.triggerHandler(gd, 'plotly_legenddoubleclick', evtData);
        if(dblClickVal !== false) handleClick(legendItem, gd, numClicks);
    }
}

function drawTexts(g, gd) {
    var legendItem = g.data()[0][0];
    var fullLayout = gd._fullLayout;
    var opts = fullLayout.legend;
    var trace = legendItem.trace;
    var isPieLike = Registry.traceIs(trace, 'pie-like');
    var traceIndex = trace.index;
    var isEditable = gd._context.edits.legendText && !isPieLike;
    var maxNameLength = opts._maxNameLength;

    var name = isPieLike ? legendItem.label : trace.name;
    if(trace._meta) {
        name = Lib.templateString(name, trace._meta);
    }

    var textEl = Lib.ensureSingle(g, 'text', 'legendtext');

    textEl.attr('text-anchor', 'start')
        .classed('user-select-none', true)
        .call(Drawing.font, fullLayout.legend.font)
        .text(isEditable ? ensureLength(name, maxNameLength) : name);

    svgTextUtils.positionText(textEl, constants.textGap, 0);

    function textLayout(s) {
        svgTextUtils.convertToTspans(s, gd, function() {
            computeTextDimensions(g, gd);
        });
    }

    if(isEditable) {
        textEl.call(svgTextUtils.makeEditable, {gd: gd, text: name})
            .call(textLayout)
            .on('edit', function(newName) {
                this.text(ensureLength(newName, maxNameLength))
                    .call(textLayout);

                var fullInput = legendItem.trace._fullInput || {};
                var update = {};

                if(Registry.hasTransform(fullInput, 'groupby')) {
                    var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby');
                    var index = groupbyIndices[groupbyIndices.length - 1];

                    var kcont = Lib.keyedContainer(fullInput, 'transforms[' + index + '].styles', 'target', 'value.name');

                    kcont.set(legendItem.trace._group, newName);

                    update = kcont.constructUpdate();
                } else {
                    update.name = newName;
                }

                return Registry.call('_guiRestyle', gd, update, traceIndex);
            });
    } else {
        textLayout(textEl);
    }
}

/*
 * Make sure we have a reasonably clickable region.
 * If this string is missing or very short, pad it with spaces out to at least
 * 4 characters, up to the max length of other labels, on the assumption that
 * most characters are wider than spaces so a string of spaces will usually be
 * no wider than the real labels.
 */
function ensureLength(str, maxLength) {
    var targetLength = Math.max(4, maxLength);
    if(str && str.trim().length >= targetLength / 2) return str;
    str = str || '';
    for(var i = targetLength - str.length; i > 0; i--) str += ' ';
    return str;
}

function setupTraceToggle(g, gd) {
    var doubleClickDelay = gd._context.doubleClickDelay;
    var newMouseDownTime;
    var numClicks = 1;

    var traceToggle = Lib.ensureSingle(g, 'rect', 'legendtoggle', function(s) {
        s.style('cursor', 'pointer')
            .attr('pointer-events', 'all')
            .call(Color.fill, 'rgba(0,0,0,0)');
    });

    traceToggle.on('mousedown', function() {
        newMouseDownTime = (new Date()).getTime();
        if(newMouseDownTime - gd._legendMouseDownTime < doubleClickDelay) {
            // in a click train
            numClicks += 1;
        } else {
            // new click train
            numClicks = 1;
            gd._legendMouseDownTime = newMouseDownTime;
        }
    });
    traceToggle.on('mouseup', function() {
        if(gd._dragged || gd._editing) return;
        var legend = gd._fullLayout.legend;

        if((new Date()).getTime() - gd._legendMouseDownTime > doubleClickDelay) {
            numClicks = Math.max(numClicks - 1, 1);
        }

        clickOrDoubleClick(gd, legend, g, numClicks, d3.event);
    });
}

function computeTextDimensions(g, gd) {
    var legendItem = g.data()[0][0];

    if(!legendItem.trace.showlegend) {
        g.remove();
        return;
    }

    var mathjaxGroup = g.select('g[class*=math-group]');
    var mathjaxNode = mathjaxGroup.node();
    var opts = gd._fullLayout.legend;
    var lineHeight = opts.font.size * LINE_SPACING;
    var height, width;

    if(mathjaxNode) {
        var mathjaxBB = Drawing.bBox(mathjaxNode);

        height = mathjaxBB.height;
        width = mathjaxBB.width;

        Drawing.setTranslate(mathjaxGroup, 0, (height / 4));
    } else {
        var text = g.select('.legendtext');
        var textLines = svgTextUtils.lineCount(text);
        var textNode = text.node();

        height = lineHeight * textLines;
        width = textNode ? Drawing.bBox(textNode).width : 0;

        // approximation to height offset to center the font
        // to avoid getBoundingClientRect
        var textY = lineHeight * (0.3 + (1 - textLines) / 2);
        svgTextUtils.positionText(text, constants.textGap, textY);
    }

    legendItem.lineHeight = lineHeight;
    legendItem.height = Math.max(height, 16) + 3;
    legendItem.width = width;
}

function computeLegendDimensions(gd, groups, traces) {
    var fullLayout = gd._fullLayout;
    var opts = fullLayout.legend;
    var isGrouped = helpers.isGrouped(opts);

    var bw = opts.borderwidth;
    var bw2 = 2 * bw;
    var textGap = constants.textGap;
    var itemGap = constants.itemGap;
    var endPad = 2 * (bw + itemGap);

    var extraWidth = 0;
    opts._width = 0;
    opts._height = 0;

    if(helpers.isVertical(opts)) {
        if(isGrouped) {
            groups.each(function(d, i) {
                Drawing.setTranslate(this, 0, i * opts.tracegroupgap);
            });
        }

        traces.each(function(d) {
            var h = d[0].height;
            Drawing.setTranslate(this, bw, itemGap + bw + opts._height + h / 2);
            opts._height += h;
            opts._width = Math.max(opts._width, d[0].width);
        });

        opts._width += itemGap + textGap + bw2;
        opts._height += endPad;

        if(isGrouped) {
            opts._height += (opts._lgroupsLength - 1) * opts.tracegroupgap;
        }

        extraWidth = textGap;
    } else if(isGrouped) {
        var maxHeight = 0;
        var maxWidth = 0;
        var maxItems = 0;
        var groupData = groups.data();
        var i;

        for(i = 0; i < groupData.length; i++) {
            var group = groupData[i];
            var groupWidths = group.map(function(d) { return d[0].width; });
            var groupHeight = group.reduce(function(a, b) { return a + b[0].height; }, 0);
            maxWidth = Math.max(maxWidth, Lib.aggNums(Math.max, null, groupWidths));
            maxHeight = Math.max(maxHeight, groupHeight);
            maxItems = Math.max(maxItems, group.length);
        }

        maxWidth += itemGap + textGap;

        var groupXOffsets = [opts._width];
        var groupYOffsets = [];
        var rowNum = 0;
        for(i = 0; i < groupData.length; i++) {
            if(fullLayout._size.w < (bw + opts._width + itemGap + maxWidth)) {
                groupXOffsets[groupXOffsets.length - 1] = groupXOffsets[0];
                opts._width = maxWidth;
                rowNum++;
            } else {
                opts._width += maxWidth + bw;
            }

            var rowYOffset = (rowNum * maxHeight);
            rowYOffset += rowNum > 0 ? opts.tracegroupgap : 0;

            groupYOffsets.push(rowYOffset);
            groupXOffsets.push(opts._width);
        }

        groups.each(function(d, i) {
            Drawing.setTranslate(this, groupXOffsets[i], groupYOffsets[i]);
        });

        groups.each(function() {
            var group = d3.select(this);
            var groupTraces = group.selectAll('g.traces');
            var groupHeight = 0;

            groupTraces.each(function(d) {
                var h = d[0].height;
                Drawing.setTranslate(this, 0, itemGap + bw + groupHeight + h / 2);
                groupHeight += h;
            });
        });

        var maxYLegend = groupYOffsets[groupYOffsets.length - 1] + maxHeight;
        opts._height = maxYLegend + endPad;

        var maxOffset = Math.max.apply(null, groupXOffsets);
        opts._width = maxOffset + maxWidth + textGap + bw2;
    } else {
        var rowHeight = 0;
        var maxTraceHeight = 0;
        var maxTraceWidth = 0;
        var offsetX = 0;
        var fullTracesWidth = 0;

        // calculate largest width for traces and use for width of all legend items
        traces.each(function(d) {
            maxTraceWidth = Math.max(maxTraceWidth, textGap + d[0].width);
            fullTracesWidth += textGap + d[0].width + itemGap;
        });

        // check if legend fits in one row
        var oneRowLegend = fullLayout._size.w > bw + fullTracesWidth - itemGap;

        traces.each(function(d) {
            var h = d[0].height;
            var traceWidth = oneRowLegend ? textGap + d[0].width : maxTraceWidth;

            if((bw + offsetX + itemGap + traceWidth) > fullLayout._size.w) {
                offsetX = 0;
                rowHeight += maxTraceHeight;
                opts._height += maxTraceHeight;
                // reset for next row
                maxTraceHeight = 0;
            }

            Drawing.setTranslate(this, bw + offsetX, itemGap + bw + h / 2 + rowHeight);
            opts._width += itemGap + traceWidth;

            // keep track of tallest trace in group
            offsetX += itemGap + traceWidth;
            maxTraceHeight = Math.max(maxTraceHeight, h);
        });

        if(oneRowLegend) {
            opts._height = maxTraceHeight;
        } else {
            opts._height += maxTraceHeight;
        }

        opts._width += bw2;
        opts._height += endPad;
    }

    opts._width = Math.ceil(opts._width);
    opts._height = Math.ceil(opts._height);

    var isEditable = (
        gd._context.edits.legendText ||
        gd._context.edits.legendPosition
    );

    traces.each(function(d) {
        var h = d[0].height;
        Drawing.setRect(d3.select(this).select('.legendtoggle'),
            0,
            -h / 2,
            (isEditable ? 0 : opts._width) + extraWidth,
            h
        );
    });
}

function expandMargin(gd) {
    var fullLayout = gd._fullLayout;
    var opts = fullLayout.legend;

    var xanchor = 'left';
    if(Lib.isRightAnchor(opts)) {
        xanchor = 'right';
    } else if(Lib.isCenterAnchor(opts)) {
        xanchor = 'center';
    }

    var yanchor = 'top';
    if(Lib.isBottomAnchor(opts)) {
        yanchor = 'bottom';
    } else if(Lib.isMiddleAnchor(opts)) {
        yanchor = 'middle';
    }

    Plots.autoMargin(gd, 'legend', {
        x: opts.x,
        y: opts.y,
        l: opts._width * (FROM_TL[xanchor]),
        r: opts._width * (FROM_BR[xanchor]),
{'fraction_non_alphanumeric': 0.11934550406400272, 'fraction_numerical': 0.00663858036512192, 'mean_word_length': 1.7230011587485516, 'pattern_counts': {'":': 0, '<': 21, '<?xml version=': 0, '>': 29, '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': '4797647', 'n_tokens_mistral': 14054, 'n_tokens_neox': 13287, 'n_words': 2959}