<commit_before>function TimeseriesLegend(app) {
    this.app = app;
    this.container = [];
    this.cursorHeldDown = false;
    this.cursorToggleTo = true;
    this.tooltip = app.tooltip;
    
    // Key Data
    this.key_data = [
        {term: "&nbsp;", label: "Capture Term", id: 'capture', has: false},
        {term: "&#x271d;", label: "Removed Capture Term", id: 'removed', has: false},
        {term: "*", label: "Term Added Later", id: 'added', has: false},
        {term: "R", label: "Rumor", id: 'rumor', has: false},
        {term: "<svg height=10 width=10><line x1=0 y1=10 x2=10 y2=0 class='total_line' /></svg>",
            label: "Tweet Volume", id: 'total_line', has: false}
    ];
    this.key_data_byID = this.key_data.reduce(function(all, cur) {
        all[cur.id] = cur;
        return all;
    }, {});
    this.key_data_byLabel = this.key_data.reduce(function(all, cur) {
        all[cur.label] = cur;
        return all;
    }, {});
    this.color = d3.scale.category10();
    // ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
    this.codecolor = d3.scale.category10()
        .domain(['Affirm', 'Deny', 'Neutral', 'Unrelated', 'Related', 'Codable', 'Uncertainty', 'Uncodable', 'TRS', 'Affirm, !TRS']);
    
    this.init();
    
    this.features = {};
    this.features_arr = [];
    this.subsets = {};
    this.subsets_arr = [];
}
TimeseriesLegend.prototype = {
    init: function() {
        this.setTriggers();
    },
    setTriggers: function() {
        triggers.on('page_built', this.buildLegend.bind(this));
        triggers.on('subsets:updated', this.populateLegend.bind(this));
        triggers.on('legend:color scale', this.setColorScale.bind(this));
        triggers.on('legend:color', this.colorSeries.bind(this));
        triggers.on('legend:order', this.orderSeries.bind(this));
        triggers.on('legend:series', this.linkSeries.bind(this));
        
        
        triggers.on('series:highlight', this.highlightSeries.bind(this));
        triggers.on('series:unhighlight', this.unHighlightSeries.bind(this));
        
        triggers.on('series:chart click', this.chartClickGetTweets.bind(this));
        triggers.on('series:chart enter', this.chartHoverEnter.bind(this));
        triggers.on('series:chart hover', this.chartHoverMove.bind(this));
        triggers.on('series:chart exit', this.chartHoverEnd.bind(this));
        
        triggers.on('series:legend enter', this.hoverLegendEntry.bind(this));
        triggers.on('series:legend hover', this.hoverLegendEntryMove.bind(this));
        triggers.on('series:legend exit', this.hoverLegendEntryEnd.bind(this));
        
        triggers.on('legend:clean', this.showOrHideAll.bind(this));
        triggers.on('feature:toggle visibility', this.toggleFeature.bind(this));
        triggers.on('feature:legend visibility', this.showOrHideFeature.bind(this));
        triggers.on('series:legend visibility', this.showOrHideSeries.bind(this));
        
        triggers.on('series:icon down', this.startToggle.bind(this));
        triggers.on('series:icon over', this.hoverOverSeries.bind(this));
        triggers.on('series:icon up', this.endToggle.bind(this));
    },
    buildLegend: function() {
        this.container = d3.select('body').append('div')
            .attr('class', 'legend');
        
        triggers.emit('legend:color scale');
    },
    populateLegend: function() {
        // Destroy old legend
        this.container.selectAll('*').remove();
        
        // Get list of features & subsets from the collection
        this.features = {};
        this.features_arr = [];
        this.subsets = {};
        this.subsets_arr = [];
        
        // Iterate through subsets and add features
        this.app.collection.subsets_arr.forEach(function(subset) { 
            this.subsets[subset.ID] = subset;
            this.subsets_arr.push(subset);
            
            // Add (to) feature
            var feature = this.features[subset.Feature];
            if(feature) {
                feature.subsets.push(subset);
                subset.feature = feature;
            } else {
                feature = {
                    Label: subset.Feature,
                    subsets: [subset],
                }
                this.features[subset.Feature] = feature;
                this.features_arr.push(feature);
                subset.feature = feature;
            }
            // TODO make this respond to changes
            feature.color = d3.scale.category10()
                .domain(feature.subsets.map(d => d.ID));
        }, this);
        
        // Sort the features and their subsets
        this.features_arr.sort((A, B) => d3.ascending(A.Label, B.Label));
        
        // Add features to the sidemenu
        this.features_arr.forEach(function(feature) {
            feature.shown = false;
            feature.subsets.sort((A, B) =>
                                 A.Event > B.Event ?  1 :
                                 A.Event < B.Event ? -1 :
                                 A.Rumor > B.Rumor ?  1 :
                                 A.Rumor < B.Rumor ? -1 :
                                 A.DisplayMatch > B.DisplayMatch ?  1 :
                                 A.DisplayMatch < B.DisplayMatch ? -1 :
                                    0)
            
            feature.div = this.container.append('div')
                .data([feature]);
            
            feature.div.append('h4')
                .html(d => d.Label)
                .style('cursor', 'pointer')
                .on('click', triggers.emitter('feature:toggle visibility'));
            
            feature.list_div = feature.div.append('div')
                .attr('class', 'legend_series_list feature_' + util.simplify(feature.Label));
            
            this.placeNewSeries(feature);
            
            var entries = feature.list_div
                .selectAll('div.legend_entry');
            
            // Propagate data to children
            entries.each(function(d) {
                var entry = d3.select(this);
                entry.select('div.legend_icon').data([d])
                    .select('svg').select('rect');
                entry.select('div.legend_label').data([d]);
                entry.select('div.legend_only').data([d]);
            });
            
            entries
                .attr('id', d => 'legend_' + d.ID)
                .attr('class', d => 'legend_entry subset_' + d.ID);
            
            feature.list_div.selectAll('div.legend_label')
                .html(d => d.DisplayMatchWithRumor);
            
            // Hide the feature until we get data for it
            triggers.emit('feature:legend visibility', feature);
//            list_div.on('mouseout', legend.endToggle);
        }, this);
        
        triggers.emit('legend:order');
    },
    placeNewSeries: function(feature) {
        var entries = this.container.select('.feature_' + util.simplify(feature.Label))
            .selectAll('div.legend_entry');
        
        var data_entries = entries
            .data(feature.subsets);
        
        var new_entries = data_entries
            .enter().append('div')
            .attr('id', d => 'legend_' + d.ID)
            .attr('class', d => 'legend_entry subset_' + d.ID)
            .on('mouseover', triggers.emitter('series:legend enter'))
            .on('mousemove', triggers.emitter('series:legend hover'))
            .on('mouseout', triggers.emitter('series:legend exit'));

        var legend_icon_divs = new_entries.append('div')
            .attr('class', 'legend_icon');

        legend_icon_divs.append('span')
            .attr('class', 'glyphicon')
            .on('click', function() { return false; });

        legend_icon_divs.append('svg')
            .attr({
                class: "legend_icon_svg",
                width: 25, height: 25
            })
            .on('mousedown', triggers.emitter('series:icon down'))
            .on('mouseover', triggers.emitter('series:icon over'))
            .on('mouseup', triggers.emitter('series:icon up'))
            .append('rect')
            .attr({
                class: "legend_icon_rect",
                x: 2.5, y: 2.5,
                rx: 5, ry: 5,
                width: 20, height: 20
            });

        new_entries.append('div')
            .attr('class', 'legend_label');

//            new_entries.append('div')
//                .attr('class', 'legend_only')
//                .text('only')
//                .on('click', this.toggleSingle);
        
        data_entries.exit().remove();
    },
    getSeriesSorter: function() {
        var sorters = {
            feature: (A, B) => d3.ascending(A.Feature, B.Feature),
            rumor: (A, B) => d3.ascending(A.Rumor == '0' ? '' : A.rumor.Name, B.Rumor == '0' ? '' : B.rumor.Name),
            'subset id': (A, B) => d3.ascending(parseInt(A.ID), parseInt(B.ID)),
            'subset name': (A, B) => d3.ascending(A.DisplayMatch, B.DisplayMatch),
            'volume shown': (A, B) => d3.descending(A.sum ? A.sum : A.data ? A.data.sum : 0, B.sum ? B.sum : B.data ? B.data.sum : 0),
            'volume total': (A, B) => d3.descending(parseInt(A.Tweets), parseInt(B.Tweets))
        };
        
        var order  = this.app.ops['Series']['Order'].get();
        var order2 = this.app.ops['Series']['Order2'].get();
        var order3 = this.app.ops['Series']['Order3'].get();
        return (A, B) => sorters[order](A, B) || sorters[order2](A, B) || sorters[order3](A, B);
    },
    orderSeries: function(feature) {
        if(!feature) { 
            this.features_arr.forEach(triggers.emitter('legend:order').bind(this));
            // Do a grand sorting of the features?
            
            return;
        }
        
        var sort_function = this.getSeriesSorter();
        
        // Sort them in the lists, in space, and on the graph (harder)
        feature.subsets.sort(sort_function);
        feature.div.selectAll('.legend_entry').sort(sort_function);
        
        triggers.emit('legend:color scale', feature);
    },
    setColorScale: function(feature) {
        if(!feature) {
            this.features_arr.forEach(triggers.emitter('legend:color scale').bind(this));
            return;
        }
        
        // Set global scale
        var colorscale = this.app.ops['Series']['Color Scale'].get();
        var codecolorscale = this.app.ops['Series']['Code Colors'].get();
        var order = this.app.ops['Series']['Order'].get();
        
        if(feature.Label == 'Code' && codecolorscale == 'code') {
            feature.colorscale = this.codecolor;
            feature.color = subset => this.codecolor(subset.Match);
//            } else if(feature.Label == 'Code' && codecolorscale == 'rumor') {
//                feature.colorscale = this.codecolor;
//                feature.color = subset => this.codecolor(subset.Match);
        } else {
            feature.colorscale = d3.scale[colorscale]()
                .domain(feature.subsets.map(d => d.ID));
            feature.color = subset => subset.feature.colorscale(subset.ID);
        }
        
        triggers.emit('legend:color', feature);
    },
    linkSeries: function(subset) {
        // TODO handle context series
        // TODO add to subsets list
        if(subset.chart == 'focus') {
//            console.log(this.subsets, subset);
            this.subsets[subset.ID].data = subset;
        }
        
        triggers.emit('legend:color', this.subsets[subset.ID]);
    },
    colorSeries: function(subset) {
        if(!subset) { // Then color them all =D
            this.subsets_arr.forEach(this.colorSeries.bind(this));
            return;
        } else if(subset.subsets) { // It's a feature, color its children
            subset.subsets.forEach(this.colorSeries.bind(this));
            return;
        }
        if(!('data' in subset)) {
            return;
        }
            
        // Determine primary color
        if(subset.data.chart == 'focus') {
            subset.color = subset.feature.color(subset);
            subset.data.Label = subset.Label;
            subset.data.shown = this.app.ops['Series']['Shown'].get()
                .includes(parseInt(subset.ID));
            
            // Turn on/off the legend entry // TODO verify consequences
//            if(subset.data.shown) {
                subset.feature.shown = true;
                triggers.emit('feature:legend visibility', subset.feature);
//            } else {
//                triggers.emit('series:legend visibility', subset);
//            }
        } else { // Context
            subset.color = '#000'; // Black
            subset.data.shown = true;
        }
        
        // Determine fill & stroke color from that
        subset.data.fill = subset.color; //this.color(subset.ID);
        subset.data.stroke = d3.rgb(subset.data.fill).darker();
        
        // Color the legend icon
        this.container.select(".subset_" + subset.ID + " .legend_icon")
            .style('fill', subset.data.fill)
            .style('stroke', subset.data.stroke);
        
    },
    init_old: function() {
        this.container = d3.select('#legend')
            .on('mouseout', this.endToggle);
        
        data.cats_arr.forEach(this.buildLegendSection, this);
    },
    buildLegendSection: function(category) {
        var container = this.container.append('div')
            .attr('class', 'legend_section ' + category.id);
        
        // Header
        var legend_header = container.append('div')
            .data([category])
            .attr('class', 'legend_header');
        
        legend_header.append('div')
            .attr('class', 'legend_filter_div')
            .append('button')
            .attr('class', 'btn btn-xs btn-default')
            .html('<span class="glyphicon glyphicon-ban-circle"></span> Filter')
            .on('click', this.filterToggle);
        category.filter = false;
        
        legend_header.append('span')
            .attr('class', 'legend_title')
            .text(category.name)

        legend_header.append('div')
            .attr('class', 'legend_showall')
            .text('show all')
            .on('click', this.showAll);
        
        // Series
        var list = container.append('div')
            .attr('class', 'legend_series_list');
        
        if(category.name == 'Keyword') {
            legend.container_keywords = list;
            
            // Key
            this.key = container.append('div')
                .attr('id', 'legend_key');

            var legend_key_entries = this.key.selectAll('div')
                .data(legend.key_data)
                .enter().append('div')
                .attr('class', function(d) {
                    return 'legend_key_entry legend_key_' + d.id;
                });

            legend_key_entries.append('div')
                .attr('class', 'legend_key_term')
                .html(function(d) { return d.term; });

            legend_key_entries.append('div')
                .attr('class', 'legend_key_label')
                .html(function(d) { return d.label; });
        }
    },
    populate_old: function(category) {
        var section_div = legend.container.select('.' + category.id);
        var list_div = section_div.select('.legend_series_list');
        
        // Get series ids
        var ids = category.series_plotted.map(function(d) {
            return d.id;
        });
        
        // Add new entries
        var entries = list_div
            .selectAll('div.legend_entry')
            .data(ids);
        
        if(category.name == 'Keyword') {
            legend.key_data.forEach(function(item) {
                item.has = false;
            });

            list_div.selectAll('div.legend_label')
                .html(function (d) {
                    var series = data.series[d];
                    var name = series.display_name;
                    var key_data = legend.key_data_byLabel[series.type];

                    if(key_data) {
                        name += ' ' + key_data.term;
                        key_data.has = true;
                    }
                    return name;
                });

            legend.key_data.forEach(function(item) {
                this.key.select('.legend_key_' + item.id)
                    .classed('hidden', !item.has);
            }, legend);
        } else {
            list_div.selectAll('div.legend_label')
                .html(function (d) {
                    var series = data.series[d];
                    var name = series.display_name;
                    return name;
                });
        }
        
        legend.showOrHideAll(category);
    },
    cmp: function(A, B) {
//        var ordering = this.app.ops['Series']['Order'];
//        
//        if(ordering == 'alpha') {
//            var name1 = a.label || '';
//            var name2 = b.label || '';
//            name1 = name1.toLowerCase();
//            name2 = name2.toLowerCase();
//            
//            if(name1 < name2)
//                return -1;
//            else if(name1 > name2)
//                return 1;
//            return 0
//            
//        } else if(ordering == 'volume') {
//            a = a.sum;
//            b = b.sum;
//
//            if(a < b)
//                return 1;
//            else if(a > b)
//                return -1;
//            return 0
//        } else if(ordering == 'type') {
//            if((a.isKeyword && !b.isKeyword) || (a.isOldKeyword && !b.isKeyword && !b.isOldKeyword))
//                return -1;
//            else if((!a.isKeyword && b.isKeyword) || (!a.isOldKeyword && !a.isKeyword && b.isOldKeyword))
//                return 1;
//            return b.sum - a.sum;
//        } else {
//            a = a.order;
//            b = b.order;
//            
//            if(a < b)
//                return -1;
//            else if(a > b)
//                return 1;
//            return 0
//        }
        
        return A.Feature > B.Feature ?  1 :
            A.Feature < B.Feature ? -1 :
            A.Event > B.Event ?  1 :
            A.Event < B.Event ? -1 :
            A.Rumor > B.Rumor ?  1 :
            A.Rumor < B.Rumor ? -1 :
            A.DisplayMatch > B.DisplayMatch ?  1 :
            A.DisplayMatch < B.DisplayMatch ? -1 :
            0;
    },
    cmp_byID: function(a, b) {
        a = data.series[a];
        b = data.series[b];
        return legend.cmp(a, b);
    },
    startToggle: function(series) {
        this.cursorHeldDown = true;
        this.cursorToggleTo = !series.data.shown;
        this.toggleSeries(series); 
        d3.event.stopPropagation();
    },
    endToggle: function(series) {
        if(typeof(series) == "string")
            series = data.series[series];
        
        if(this.cursorHeldDown) {
            this.cursorHeldDown = false;
            triggers.emit('timeseries:stack')// TODO
//            pipeline.start('Find Which Data is Shown');
        }
    },
    highlightSeries: function(series) {
        d3.selectAll('.series, .legend_icon')
            .classed('focused', false)
            .classed('unfocused', true);
        d3.selectAll('.series.subset_' + series.ID + ', .subset_' + series.ID + ' .legend_icon')
            .classed('unfocused', false)
            .classed('focused', true);
    },
    unHighlightSeries: function() {
        d3.selectAll('.series, .legend_icon')
            .classed('focused', false)
            .classed('unfocused', false);
    },
    hoverLegendEntry: function(series) {
        // Generate tooltip
        var info = {
            ID: series.ID,
            'Total Tweets': util.formatThousands(series.Tweets),
        }
        if(series.data) {
            info['Total Shown'] = util.formatThousands(series.data.sum);
            info['Max'] = util.formatThousands(series.data.max);
        }
        
        this.tooltip.setData(info);
        this.tooltip.on();
        
        triggers.emit('series:highlight', series);
    },
    hoverLegendEntryMove: function(series) {
        this.tooltip.move(d3.event.x, d3.event.y);
    },
    hoverLegendEntryEnd: function(series) {
        this.tooltip.off();
        triggers.emit('series:unhighlight');
    },
    chartClickGetTweets: function(series) {
        // Get parent objects
        var chart = this.app[series.chart];
        var time_obj = this.app.model.time;
        
        // Get position
        var time = chart.x.invert(series.cursor_xy[0]);
        time.setSeconds(0);
        var resolution = this.app.ops['View']['Resolution'].get();
        var i_r = ['minute', 'tenminute', 'hour', 'day'].indexOf(resolution);
        var i_t = time_obj.stamp_index[util.formatDate(time)];
        var value_i = time_obj.indices[i_t][i_r];
        var time_min = time_obj[resolution + 's'][value_i];
        var time_max = time_obj[resolution + 's'][value_i + 1];
        
        triggers.emit('fetch tweets', {
            collection: series.chart == 'context' ? 'event' : 'subset',
            event_id: this.app.collection.event.ID,
            subset_id: series.ID,
            time_min: time_min,
            time_max: time_max,
            label: series.Label,
        });
    },
    chartHoverEnter: function(series) {
        this.tooltip.on();
        
        triggers.emit('series:highlight', series);
    },
    chartHoverMove: function(series) {
        this.tooltip.move(d3.event.x, d3.event.y);
//        triggers.emit('tooltip:move', [d3.event.x, d3.event.y]);
        
        // Get parent objects
        var chart = this.app[series.chart];
        var time_obj = this.app.model.time;
        
        // Get position
        var time = chart.x.invert(series.cursor_xy[0]);
        time.setSeconds(0);<commit_msg>Fixed problem fetching tweets<commit_after>function TimeseriesLegend(app) {
    this.app = app;
    this.container = [];
    this.cursorHeldDown = false;
    this.cursorToggleTo = true;
    this.tooltip = app.tooltip;
    
    // Key Data
    this.key_data = [
        {term: "&nbsp;", label: "Capture Term", id: 'capture', has: false},
        {term: "&#x271d;", label: "Removed Capture Term", id: 'removed', has: false},
        {term: "*", label: "Term Added Later", id: 'added', has: false},
        {term: "R", label: "Rumor", id: 'rumor', has: false},
        {term: "<svg height=10 width=10><line x1=0 y1=10 x2=10 y2=0 class='total_line' /></svg>",
            label: "Tweet Volume", id: 'total_line', has: false}
    ];
    this.key_data_byID = this.key_data.reduce(function(all, cur) {
        all[cur.id] = cur;
        return all;
    }, {});
    this.key_data_byLabel = this.key_data.reduce(function(all, cur) {
        all[cur.label] = cur;
        return all;
    }, {});
    this.color = d3.scale.category10();
    // ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
    this.codecolor = d3.scale.category10()
        .domain(['Affirm', 'Deny', 'Neutral', 'Unrelated', 'Related', 'Codable', 'Uncertainty', 'Uncodable', 'TRS', 'Affirm, !TRS']);
    
    this.init();
    
    this.features = {};
    this.features_arr = [];
    this.subsets = {};
    this.subsets_arr = [];
}
TimeseriesLegend.prototype = {
    init: function() {
        this.setTriggers();
    },
    setTriggers: function() {
        triggers.on('page_built', this.buildLegend.bind(this));
        triggers.on('subsets:updated', this.populateLegend.bind(this));
        triggers.on('legend:color scale', this.setColorScale.bind(this));
        triggers.on('legend:color', this.colorSeries.bind(this));
        triggers.on('legend:order', this.orderSeries.bind(this));
        triggers.on('legend:series', this.linkSeries.bind(this));
        
        
        triggers.on('series:highlight', this.highlightSeries.bind(this));
        triggers.on('series:unhighlight', this.unHighlightSeries.bind(this));
        
        triggers.on('series:chart click', this.chartClickGetTweets.bind(this));
        triggers.on('series:chart enter', this.chartHoverEnter.bind(this));
        triggers.on('series:chart hover', this.chartHoverMove.bind(this));
        triggers.on('series:chart exit', this.chartHoverEnd.bind(this));
        
        triggers.on('series:legend enter', this.hoverLegendEntry.bind(this));
        triggers.on('series:legend hover', this.hoverLegendEntryMove.bind(this));
        triggers.on('series:legend exit', this.hoverLegendEntryEnd.bind(this));
        
        triggers.on('legend:clean', this.showOrHideAll.bind(this));
        triggers.on('feature:toggle visibility', this.toggleFeature.bind(this));
        triggers.on('feature:legend visibility', this.showOrHideFeature.bind(this));
        triggers.on('series:legend visibility', this.showOrHideSeries.bind(this));
        
        triggers.on('series:icon down', this.startToggle.bind(this));
        triggers.on('series:icon over', this.hoverOverSeries.bind(this));
        triggers.on('series:icon up', this.endToggle.bind(this));
    },
    buildLegend: function() {
        this.container = d3.select('body').append('div')
            .attr('class', 'legend');
        
        triggers.emit('legend:color scale');
    },
    populateLegend: function() {
        // Destroy old legend
        this.container.selectAll('*').remove();
        
        // Get list of features & subsets from the collection
        this.features = {};
        this.features_arr = [];
        this.subsets = {};
        this.subsets_arr = [];
        
        // Iterate through subsets and add features
        this.app.collection.subsets_arr.forEach(function(subset) { 
            this.subsets[subset.ID] = subset;
            this.subsets_arr.push(subset);
            
            // Add (to) feature
            var feature = this.features[subset.Feature];
            if(feature) {
                feature.subsets.push(subset);
                subset.feature = feature;
            } else {
                feature = {
                    Label: subset.Feature,
                    subsets: [subset],
                }
                this.features[subset.Feature] = feature;
                this.features_arr.push(feature);
                subset.feature = feature;
            }
            // TODO make this respond to changes
            feature.color = d3.scale.category10()
                .domain(feature.subsets.map(d => d.ID));
        }, this);
        
        // Sort the features and their subsets
        this.features_arr.sort((A, B) => d3.ascending(A.Label, B.Label));
        
        // Add features to the sidemenu
        this.features_arr.forEach(function(feature) {
            feature.shown = false;
            feature.subsets.sort((A, B) =>
                                 A.Event > B.Event ?  1 :
                                 A.Event < B.Event ? -1 :
                                 A.Rumor > B.Rumor ?  1 :
                                 A.Rumor < B.Rumor ? -1 :
                                 A.DisplayMatch > B.DisplayMatch ?  1 :
                                 A.DisplayMatch < B.DisplayMatch ? -1 :
                                    0)
            
            feature.div = this.container.append('div')
                .data([feature]);
            
            feature.div.append('h4')
                .html(d => d.Label)
                .style('cursor', 'pointer')
                .on('click', triggers.emitter('feature:toggle visibility'));
            
            feature.list_div = feature.div.append('div')
                .attr('class', 'legend_series_list feature_' + util.simplify(feature.Label));
            
            this.placeNewSeries(feature);
            
            var entries = feature.list_div
                .selectAll('div.legend_entry');
            
            // Propagate data to children
            entries.each(function(d) {
                var entry = d3.select(this);
                entry.select('div.legend_icon').data([d])
                    .select('svg').select('rect');
                entry.select('div.legend_label').data([d]);
                entry.select('div.legend_only').data([d]);
            });
            
            entries
                .attr('id', d => 'legend_' + d.ID)
                .attr('class', d => 'legend_entry subset_' + d.ID);
            
            feature.list_div.selectAll('div.legend_label')
                .html(d => d.DisplayMatchWithRumor);
            
            // Hide the feature until we get data for it
            triggers.emit('feature:legend visibility', feature);
//            list_div.on('mouseout', legend.endToggle);
        }, this);
        
        triggers.emit('legend:order');
    },
    placeNewSeries: function(feature) {
        var entries = this.container.select('.feature_' + util.simplify(feature.Label))
            .selectAll('div.legend_entry');
        
        var data_entries = entries
            .data(feature.subsets);
        
        var new_entries = data_entries
            .enter().append('div')
            .attr('id', d => 'legend_' + d.ID)
            .attr('class', d => 'legend_entry subset_' + d.ID)
            .on('mouseover', triggers.emitter('series:legend enter'))
            .on('mousemove', triggers.emitter('series:legend hover'))
            .on('mouseout', triggers.emitter('series:legend exit'));

        var legend_icon_divs = new_entries.append('div')
            .attr('class', 'legend_icon');

        legend_icon_divs.append('span')
            .attr('class', 'glyphicon')
            .on('click', function() { return false; });

        legend_icon_divs.append('svg')
            .attr({
                class: "legend_icon_svg",
                width: 25, height: 25
            })
            .on('mousedown', triggers.emitter('series:icon down'))
            .on('mouseover', triggers.emitter('series:icon over'))
            .on('mouseup', triggers.emitter('series:icon up'))
            .append('rect')
            .attr({
                class: "legend_icon_rect",
                x: 2.5, y: 2.5,
                rx: 5, ry: 5,
                width: 20, height: 20
            });

        new_entries.append('div')
            .attr('class', 'legend_label');

//            new_entries.append('div')
//                .attr('class', 'legend_only')
//                .text('only')
//                .on('click', this.toggleSingle);
        
        data_entries.exit().remove();
    },
    getSeriesSorter: function() {
        var sorters = {
            feature: (A, B) => d3.ascending(A.Feature, B.Feature),
            rumor: (A, B) => d3.ascending(A.Rumor == '0' ? '' : A.rumor.Name, B.Rumor == '0' ? '' : B.rumor.Name),
            'subset id': (A, B) => d3.ascending(parseInt(A.ID), parseInt(B.ID)),
            'subset name': (A, B) => d3.ascending(A.DisplayMatch, B.DisplayMatch),
            'volume shown': (A, B) => d3.descending(A.sum ? A.sum : A.data ? A.data.sum : 0, B.sum ? B.sum : B.data ? B.data.sum : 0),
            'volume total': (A, B) => d3.descending(parseInt(A.Tweets), parseInt(B.Tweets))
        };
        
        var order  = this.app.ops['Series']['Order'].get();
        var order2 = this.app.ops['Series']['Order2'].get();
        var order3 = this.app.ops['Series']['Order3'].get();
        return (A, B) => sorters[order](A, B) || sorters[order2](A, B) || sorters[order3](A, B);
    },
    orderSeries: function(feature) {
        if(!feature) { 
            this.features_arr.forEach(triggers.emitter('legend:order').bind(this));
            // Do a grand sorting of the features?
            
            return;
        }
        
        var sort_function = this.getSeriesSorter();
        
        // Sort them in the lists, in space, and on the graph (harder)
        feature.subsets.sort(sort_function);
        feature.div.selectAll('.legend_entry').sort(sort_function);
        
        triggers.emit('legend:color scale', feature);
    },
    setColorScale: function(feature) {
        if(!feature) {
            this.features_arr.forEach(triggers.emitter('legend:color scale').bind(this));
            return;
        }
        
        // Set global scale
        var colorscale = this.app.ops['Series']['Color Scale'].get();
        var codecolorscale = this.app.ops['Series']['Code Colors'].get();
        var order = this.app.ops['Series']['Order'].get();
        
        if(feature.Label == 'Code' && codecolorscale == 'code') {
            feature.colorscale = this.codecolor;
            feature.color = subset => this.codecolor(subset.Match);
//            } else if(feature.Label == 'Code' && codecolorscale == 'rumor') {
//                feature.colorscale = this.codecolor;
//                feature.color = subset => this.codecolor(subset.Match);
        } else {
            feature.colorscale = d3.scale[colorscale]()
                .domain(feature.subsets.map(d => d.ID));
            feature.color = subset => subset.feature.colorscale(subset.ID);
        }
        
        triggers.emit('legend:color', feature);
    },
    linkSeries: function(subset) {
        // TODO handle context series
        // TODO add to subsets list
        if(subset.chart == 'focus') {
//            console.log(this.subsets, subset);
            this.subsets[subset.ID].data = subset;
        }
        
        triggers.emit('legend:color', this.subsets[subset.ID]);
    },
    colorSeries: function(subset) {
        if(!subset) { // Then color them all =D
            this.subsets_arr.forEach(this.colorSeries.bind(this));
            return;
        } else if(subset.subsets) { // It's a feature, color its children
            subset.subsets.forEach(this.colorSeries.bind(this));
            return;
        }
        if(!('data' in subset)) {
            return;
        }
            
        // Determine primary color
        if(subset.data.chart == 'focus') {
            subset.color = subset.feature.color(subset);
            subset.data.Label = subset.Label;
            subset.data.shown = this.app.ops['Series']['Shown'].get()
                .includes(parseInt(subset.ID));
            
            // Turn on/off the legend entry // TODO verify consequences
//            if(subset.data.shown) {
                subset.feature.shown = true;
                triggers.emit('feature:legend visibility', subset.feature);
//            } else {
//                triggers.emit('series:legend visibility', subset);
//            }
        } else { // Context
            subset.color = '#000'; // Black
            subset.data.shown = true;
        }
        
        // Determine fill & stroke color from that
        subset.data.fill = subset.color; //this.color(subset.ID);
        subset.data.stroke = d3.rgb(subset.data.fill).darker();
        
        // Color the legend icon
        this.container.select(".subset_" + subset.ID + " .legend_icon")
            .style('fill', subset.data.fill)
            .style('stroke', subset.data.stroke);
        
    },
    init_old: function() {
        this.container = d3.select('#legend')
            .on('mouseout', this.endToggle);
        
        data.cats_arr.forEach(this.buildLegendSection, this);
    },
    buildLegendSection: function(category) {
        var container = this.container.append('div')
            .attr('class', 'legend_section ' + category.id);
        
        // Header
        var legend_header = container.append('div')
            .data([category])
            .attr('class', 'legend_header');
        
        legend_header.append('div')
            .attr('class', 'legend_filter_div')
            .append('button')
            .attr('class', 'btn btn-xs btn-default')
            .html('<span class="glyphicon glyphicon-ban-circle"></span> Filter')
            .on('click', this.filterToggle);
        category.filter = false;
        
        legend_header.append('span')
            .attr('class', 'legend_title')
            .text(category.name)

        legend_header.append('div')
            .attr('class', 'legend_showall')
            .text('show all')
            .on('click', this.showAll);
        
        // Series
        var list = container.append('div')
            .attr('class', 'legend_series_list');
        
        if(category.name == 'Keyword') {
            legend.container_keywords = list;
            
            // Key
            this.key = container.append('div')
                .attr('id', 'legend_key');

            var legend_key_entries = this.key.selectAll('div')
                .data(legend.key_data)
                .enter().append('div')
                .attr('class', function(d) {
                    return 'legend_key_entry legend_key_' + d.id;
                });

            legend_key_entries.append('div')
                .attr('class', 'legend_key_term')
                .html(function(d) { return d.term; });

            legend_key_entries.append('div')
                .attr('class', 'legend_key_label')
                .html(function(d) { return d.label; });
        }
    },
    populate_old: function(category) {
        var section_div = legend.container.select('.' + category.id);
        var list_div = section_div.select('.legend_series_list');
        
        // Get series ids
        var ids = category.series_plotted.map(function(d) {
            return d.id;
        });
        
        // Add new entries
        var entries = list_div
            .selectAll('div.legend_entry')
            .data(ids);
        
        if(category.name == 'Keyword') {
            legend.key_data.forEach(function(item) {
                item.has = false;
            });

            list_div.selectAll('div.legend_label')
                .html(function (d) {
                    var series = data.series[d];
                    var name = series.display_name;
                    var key_data = legend.key_data_byLabel[series.type];

                    if(key_data) {
                        name += ' ' + key_data.term;
                        key_data.has = true;
                    }
                    return name;
                });

            legend.key_data.forEach(function(item) {
                this.key.select('.legend_key_' + item.id)
                    .classed('hidden', !item.has);
            }, legend);
        } else {
            list_div.selectAll('div.legend_label')
                .html(function (d) {
                    var series = data.series[d];
                    var name = series.display_name;
                    return name;
                });
        }
        
        legend.showOrHideAll(category);
    },
    cmp: function(A, B) {
//        var ordering = this.app.ops['Series']['Order'];
//        
//        if(ordering == 'alpha') {
//            var name1 = a.label || '';
//            var name2 = b.label || '';
//            name1 = name1.toLowerCase();
//            name2 = name2.toLowerCase();
//            
//            if(name1 < name2)
//                return -1;
//            else if(name1 > name2)
//                return 1;
//            return 0
//            
//        } else if(ordering == 'volume') {
//            a = a.sum;
//            b = b.sum;
//
//            if(a < b)
//                return 1;
//            else if(a > b)
//                return -1;
//            return 0
//        } else if(ordering == 'type') {
//            if((a.isKeyword && !b.isKeyword) || (a.isOldKeyword && !b.isKeyword && !b.isOldKeyword))
//                return -1;
//            else if((!a.isKeyword && b.isKeyword) || (!a.isOldKeyword && !a.isKeyword && b.isOldKeyword))
//                return 1;
//            return b.sum - a.sum;
//        } else {
//            a = a.order;
//            b = b.order;
//            
//            if(a < b)
//                return -1;
//            else if(a > b)
//                return 1;
//            return 0
//        }
        
        return A.Feature > B.Feature ?  1 :
            A.Feature < B.Feature ? -1 :
            A.Event > B.Event ?  1 :
            A.Event < B.Event ? -1 :
            A.Rumor > B.Rumor ?  1 :
            A.Rumor < B.Rumor ? -1 :
            A.DisplayMatch > B.DisplayMatch ?  1 :
            A.DisplayMatch < B.DisplayMatch ? -1 :
            0;
    },
    cmp_byID: function(a, b) {
        a = data.series[a];
        b = data.series[b];
        return legend.cmp(a, b);
    },
    startToggle: function(series) {
        this.cursorHeldDown = true;
        this.cursorToggleTo = !series.data.shown;
        this.toggleSeries(series); 
        d3.event.stopPropagation();
    },
    endToggle: function(series) {
        if(typeof(series) == "string")
            series = data.series[series];
        
        if(this.cursorHeldDown) {
            this.cursorHeldDown = false;
            triggers.emit('timeseries:stack')// TODO
//            pipeline.start('Find Which Data is Shown');
        }
    },
    highlightSeries: function(series) {
        d3.selectAll('.series, .legend_icon')
            .classed('focused', false)
            .classed('unfocused', true);
        d3.selectAll('.series.subset_' + series.ID + ', .subset_' + series.ID + ' .legend_icon')
            .classed('unfocused', false)
            .classed('focused', true);
    },
    unHighlightSeries: function() {
        d3.selectAll('.series, .legend_icon')
            .classed('focused', false)
            .classed('unfocused', false);
    },
    hoverLegendEntry: function(series) {
        // Generate tooltip
        var info = {
            ID: series.ID,
            'Total Tweets': util.formatThousands(series.Tweets),
        }
        if(series.data) {
            info['Total Shown'] = util.formatThousands(series.data.sum);
            info['Max'] = util.formatThousands(series.data.max);
        }
        
        this.tooltip.setData(info);
        this.tooltip.on();
        
        triggers.emit('series:highlight', series);
    },
    hoverLegendEntryMove: function(series) {
        this.tooltip.move(d3.event.x, d3.event.y);
    },
    hoverLegendEntryEnd: function(series) {
        this.tooltip.off();
        triggers.emit('series:unhighlight');
    },
    chartClickGetTweets: function(series) {
        // Get parent objects
        var chart = this.app[series.chart];
        var time_obj = this.app.model.time;
        
        // Get position
        var time = chart.x.invert(series.cursor_xy[0]);
        time.setSeconds(0);
        var resolution = this.app.ops['View']['Resolution'].get();
        var i_r = ['minute', 'tenminute', 'hour', 'day'].indexOf(resolution);
        var i_t = time_obj.stamp_index[util.formatDate(time)];
        var value_i = time_obj.indices[i_t][i_r];
        var time_min = time_obj[resolution + 's'][value_i];
        var time_max = time_obj[resolution + 's'][value_i + 1];
        
        triggers.emit('fetch tweets', {
            collection: series.chart == 'context' ? 'Event' : 'Subset',
            Event_id: this.app.collection.event.ID,
            Subset_id: series.ID,
            time_min: time_min,
            time_max: time_max,
            label: series.Label,
        });
    },
    chartHoverEnter: function(series) {
        this.tooltip.on();
        
        triggers.emit('series:highlight', series);
    },
    chartHoverMove: function(series) {
        this.tooltip.move(d3.event.x, d3.event.y);
//        triggers.emit('tooltip:move', [d3.event.x, d3.event.y]);
        
        // Get parent objects
        var chart = this.app[series.chart];
        var time_obj = this.app.model.time;
        
        // Get position
        var time = chart.x.invert(series.cursor_xy[0]);
        time.setSeconds(0);
{'fraction_non_alphanumeric': 0.14866958264989977, 'fraction_numerical': 0.006879897940586841, 'mean_word_length': 1.6678619180746324, 'pattern_counts': {'":': 0, '<': 33, '<?xml version=': 0, '>': 73, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 14, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4530768', 'n_tokens_mistral': 13170, 'n_tokens_neox': 12642, 'n_words': 2248}