<commit_before>// 
// Dalliance Genome Explorer
// (c) <NAME> 2006-2010
//
// feature-draw.js: new feature-tier renderer
//

var MIN_PADDING = 3;
var DEFAULT_SUBTIER_MAX = 100;

function isDasBooleanTrue(s) {
    s = ('' + s).toLowerCase();
    return s==='yes' || s==='true';
}

function SubTier() {
    this.glyphs = [];
    this.height = 0;
    this.quant = null;
}

SubTier.prototype.add = function(glyph) {
    this.glyphs.push(glyph);
    this.height = Math.max(this.height, glyph.height());
    if (glyph.quant && this.quant == null) {
        this.quant = glyph.quant;
    }
}

SubTier.prototype.hasSpaceFor = function(glyph) {
    for (var i = 0; i < this.glyphs.length; ++i) {
        var g = this.glyphs[i];
        if (g.min() <= glyph.max() && g.max() >= glyph.min()) {
            return false;
        }
    }
    return true;
}

var GLOBAL_GC;

function drawFeatureTier(tier)
{
    GLOBAL_GC = tier.viewport.getContext('2d'); // Should only be used for metrics.
    sortFeatures(tier);

    var glyphs = [];
    var specials = false;

    for (var uft in tier.ungroupedFeatures) {
        var ufl = tier.ungroupedFeatures[uft];
        // var style = styles[uft] || styles['default'];
        var style = tier.styleForFeature(ufl[0]);   // FIXME this isn't quite right...
        if (!style) continue;
        if (style.glyph == 'LINEPLOT') {
            glyphs.push(makeLineGlyph(ufl, style, tier));
            specials = true;
        } else {
            for (var pgid = 0; pgid < ufl.length; ++pgid) {
                var f = ufl[pgid];
                if (f.parts) {  // FIXME shouldn't really be needed
                    continue;
                }
                var g = glyphForFeature(f, 0, tier.styleForFeature(f), tier);
                if (g)
                    glyphs.push(g);
            }
        }
    }

    // Merge supergroups
    
    if (tier.dasSource.collapseSuperGroups && !tier.bumped) {
        for (var sg in tier.superGroups) {
            var sgg = tier.superGroups[sg];
            tier.groups[sg].type = tier.groups[sgg[0]].type;   // HACK to make styling easier in DAS1.6
            var featsByType = {};
            for (var g = 0; g < sgg.length; ++g) {
                var gf = tier.groupedFeatures[sgg[g]];
                for (var fi = 0; fi < gf.length; ++fi) {
                    var f = gf[fi];
                    pusho(featsByType, f.type, f);
                }

                if (tier.groups[sg] && !tier.groups[sg].links || tier.groups[sg].links.length == 0) {
                    tier.groups[sg].links = tier.groups[sgg[0]].links;
                }

                delete tier.groupedFeatures[sgg[g]];  // 'cos we don't want to render the unmerged version.
            }

            for (var t in featsByType) {
                var feats = featsByType[t];
                var template = feats[0];
                var loc = null;
                for (var fi = 0; fi < feats.length; ++fi) {
                    var f = feats[fi];
                    var fl = new Range(f.min, f.max);
                    if (!loc) {
                        loc = fl;
                    } else {
                        loc = union(loc, fl);
                    }
                }
                var mergedRanges = loc.ranges();
                for (var si = 0; si < mergedRanges.length; ++si) {
                    var r = mergedRanges[si];

                    // begin coverage-counting
                    var posCoverage = ((r.max()|0) - (r.min()|0) + 1) * sgg.length;
                    var actCoverage = 0;
                    for (var fi = 0; fi < feats.length; ++fi) {
                        var f = feats[fi];
                        if ((f.min|0) <= r.max() && (f.max|0) >= r.min()) {
                            var umin = Math.max(f.min|0, r.min());
                            var umax = Math.min(f.max|0, r.max());
                            actCoverage += (umax - umin + 1);
                        }
                    }
                    var visualWeight = ((1.0 * actCoverage) / posCoverage);
                    // end coverage-counting

                    var newf = new DASFeature();
                    for (k in template) {
                        newf[k] = template[k];
                    }
                    newf.min = r.min();
                    newf.max = r.max();
                    if (newf.label && sgg.length > 1) {
                        newf.label += ' (' + sgg.length + ' vars)';
                    }
                    newf.visualWeight = ((1.0 * actCoverage) / posCoverage);
                    pusho(tier.groupedFeatures, sg, newf);
                    // supergroups are already in tier.groups.
                }
            }

            delete tier.superGroups[sg]; // Do we want this?
        }       
    }

    // Glyphify groups.

    var gl = new Array();
    for (var gid in tier.groupedFeatures) {
        gl.push(gid);
    }
    gl.sort(function(g1, g2) {
        var d = tier.groupedFeatures[g1][0].score - tier.groupedFeatures[g2][0].score;
        if (d > 0) {
            return -1;
        } else if (d == 0) {
            return 0;
        } else {
            return 1;
        }
    });

    var groupGlyphs = {};
    for (var gx = 0; gx < gl.length; ++gx) {
        var gid = gl[gx];
        var g = glyphsForGroup(tier.groupedFeatures[gid], 0, tier.groups[gid], tier,
                               (tier.dasSource.collapseSuperGroups && !tier.bumped) ? 'collapsed_gene' : 'tent');
        if (g) {
            g.group = tier.groups[gid];
            groupGlyphs[gid] = g;
        }
    }

    for (var sg in tier.superGroups) {
        var sgg = tier.superGroups[sg];
        var sgGlyphs = [];
        var sgMin = 10000000000;
        var sgMax = -10000000000;
        for (var sgi = 0; sgi < sgg.length; ++sgi) {
            var gg = groupGlyphs[sgg[sgi]];
            groupGlyphs[sgg[sgi]] = null;
            if (gg) {
                sgGlyphs.push(gg);
                sgMin = Math.min(sgMin, gg.min());
                sgMax = Math.max(sgMax, gg.max());
            }
        }
        for (var sgi = 0; sgi < sgGlyphs.length; ++sgi) {
            var gg = sgGlyphs[sgi];
            glyphs.push(new PaddedGlyph(gg, sgMin, sgMax));
        }
    }
    for (var g in groupGlyphs) {
        var gg = groupGlyphs[g];
        if (gg) {
            glyphs.push(gg);
        }
    }



    // Bumping

    var unbumpedST = new SubTier();
    var bumpedSTs = [];
    var hasBumpedFeatures = false;
    var subtierMax = tier.dasSource.subtierMax || DEFAULT_SUBTIER_MAX;
    
  GLYPH_LOOP:
    for (var i = 0; i < glyphs.length; ++i) {
        var g = glyphs[i];
        if (g.bump) {
            hasBumpedFeatures = true;
        }
        if (g.bump && (tier.bumped || tier.dasSource.collapseSuperGroups)) {       // kind-of nasty.  supergroup collapsing is different from "normal" unbumping
            for (var sti = 0; sti < bumpedSTs.length;  ++sti) {
                var st = bumpedSTs[sti];
                if (st.hasSpaceFor(g)) {
                    st.add(g);
                    continue GLYPH_LOOP;
                }
            }
            if (bumpedSTs.length >= subtierMax) {
                tier.status = 'Too many overlapping features, truncating at ' + subtierMax;
            } else {
                var st = new SubTier();
                st.add(g);
                bumpedSTs.push(st);
            }
        } else {
            unbumpedST.add(g);
        }
    }

    if (unbumpedST.glyphs.length > 0) {
        bumpedSTs = [unbumpedST].concat(bumpedSTs);
    }

    for (var sti = 0; sti < bumpedSTs.length; ++sti) {
        var st = bumpedSTs[sti];
        if (st.quant) {
            st.glyphs.unshift(new GridGlyph(st.height));
        }
    }

    for (var sti = 0; sti < bumpedSTs.length; ++sti) {
        var st = bumpedSTs[sti];
        st.glyphs.sort(function (g1, g2) {
            var z1 = g1.zindex || 0;
            var z2 = g2.zindex || 0;
            return z1 - z2;
        });
    }

    tier.subtiers = bumpedSTs;
    tier.glyphCacheOrigin = tier.browser.viewStart;
}

function formatQuantLabel(v) {
    var t = '' + v;
    var dot = t.indexOf('.');
    if (dot < 0) {
        return t;
    } else {
        var dotThreshold = 2;
        if (t.substring(0, 1) == '-') {
            ++dotThreshold;
        }

        if (dot >= dotThreshold) {
            return t.substring(0, dot);
        } else {
            return t.substring(0, dot + 2);
        }
    }
}

DasTier.prototype.paint = function() {
    var retina = this.browser.retina && window.devicePixelRatio > 1;

    var subtiers = this.subtiers;
    if (!subtiers) {
	   return;
    }

    var desiredWidth = this.browser.featurePanelWidth + 2000;
    if (retina) {
        desiredWidth *= 2;
    }
    var fpw = this.viewport.width|0; // this.browser.featurePanelWidth;
    if (fpw < desiredWidth - 50) {
        this.viewport.width = fpw = desiredWidth;
    }

    var lh = MIN_PADDING;
    for (var s = 0; s < subtiers.length; ++s) {
        lh = lh + subtiers[s].height + MIN_PADDING;
    }
    lh += 6

    var canvasHeight = lh;
    if (retina) {
        canvasHeight *= 2;
    }

    if (canvasHeight != this.viewport.height) {
        this.viewport.height = canvasHeight;
    }
    
    var tierHeight = Math.max(lh, this.browser.minTierHeight);
    this.viewport.style.left = '-1000px';
    this.viewport.style.width = retina ? ('' + (fpw/2) + 'px') : ('' + fpw + 'px');
    this.viewport.style.height = '' + lh + 'px';
    this.layoutHeight =  Math.max(lh, this.browser.minTierHeight);
    this.holder.style.height = '' + this.layoutHeight + 'px';

    this.updateHeight();
    this.drawOverlay();
    this.norigin = this.browser.viewStart;

    var gc = this.viewport.getContext('2d');
    gc.fillStyle = this.background;
    gc.clearRect(0, 0, fpw, canvasHeight);
    gc.restore();

    gc.save();
    if (retina) {
        gc.scale(2, 2);
    }
    var offset = ((this.glyphCacheOrigin - this.browser.viewStart)*this.browser.scale)+1000;
    gc.translate(offset, MIN_PADDING);
   
    for (var s = 0; s < subtiers.length; ++s) {
        var quant = null;
        var glyphs = subtiers[s].glyphs;
        for (var i = 0; i < glyphs.length; ++i) {
            var glyph = glyphs[i];
            if (glyph.min() < fpw-offset && glyph.max() > -offset) { 
                var glyph = glyphs[i];
                glyph.draw(gc);
                if (glyph.quant) {
                    quant = glyph.quant;
                }
            }
        }
        gc.translate(0, subtiers[s].height + MIN_PADDING);
    }
    gc.restore();

    if (quant && this.quantLeapThreshold && this.featureSource && sourceAdapterIsCapable(this.featureSource, 'quantLeap')) {
        var ry = 3 + subtiers[0].height * (1.0 - ((this.quantLeapThreshold - quant.min) / (quant.max - quant.min)));

        gc.save();
        if (retina)
            gc.scale(2, 2);
        gc.strokeStyle = 'red';
        gc.lineWidth = 0.3;
        gc.beginPath();
        gc.moveTo(0, ry);
        gc.lineTo(5000, ry);
        gc.stroke();
        gc.restore();
    }

    this.paintQuant();
}

DasTier.prototype.paintQuant = function() {
    if (!this.quantOverlay)
        return;

    var retina = this.browser.retina && window.devicePixelRatio > 1;

    var quant;
    if (this.subtiers && this.subtiers.length > 0)
        quant = this.subtiers[0].quant;

    if (quant) {
        var h = this.subtiers[0].height;
        var w = 50;
        this.quantOverlay.height = this.viewport.height;
        this.quantOverlay.width = retina ? w*2 : w;
        this.quantOverlay.style.height = '' + (retina ? this.quantOverlay.height/2 : this.quantOverlay.height) + 'px';
        this.quantOverlay.style.width = '' + w + 'px';
        this.quantOverlay.style.display = 'block';
        var ctx = this.quantOverlay.getContext('2d');
        if (retina)
            ctx.scale(2, 2);

        ctx.fillStyle = 'white'
        ctx.globalAlpha = 0.6;

        var numTics = 2;
        if (h > 40) {
            numTics = 1 + ((h/20) | 0);
        }
        var ticSpacing = (h+MIN_PADDING*2) / (numTics - 1);
        var ticInterval = (quant.max - quant.min) / (numTics - 1);

        if (this.browser.rulerLocation == 'right') {
            ctx.fillRect(w-30, 0, 30, 20);
            ctx.fillRect(w-30, h-20 + MIN_PADDING*2, 30, 20);
            for (var t = 1; t < numTics-1; ++t) {
                ctx.fillRect(w-30, t*ticSpacing - 10, 30, 20);
            }
        } else {
            ctx.fillRect(0, 0, 30, 20);
            ctx.fillRect(0, h - 20 + MIN_PADDING*2, 30, 20);
            for (var t = 1; t < numTics-1; ++t) {
                ctx.fillRect(0, t*ticSpacing - 10, 30, 20);
            }
        }
        ctx.globalAlpha = 1.0;

        ctx.strokeStyle = 'black';
        ctx.lineWidth = 1;
        ctx.beginPath();

        if (this.browser.rulerLocation == 'right') {
            ctx.moveTo(w - 8, MIN_PADDING);
            ctx.lineTo(w, MIN_PADDING);
            ctx.lineTo(w, h + MIN_PADDING);
            ctx.lineTo(w - 8, h + MIN_PADDING);
            for (var t = 1; t < numTics-1; ++t) {
                var ty = t*ticSpacing;
                ctx.moveTo(w, ty);
                ctx.lineTo(w - 5, ty);
            }
        } else {
            ctx.moveTo(8, MIN_PADDING);
            ctx.lineTo(0, MIN_PADDING);
            ctx.lineTo(0, h+MIN_PADDING);
            ctx.lineTo(8, h+MIN_PADDING);
            for (var t = 1; t < numTics-1; ++t) {
                var ty = t*ticSpacing;
                ctx.moveTo(0, ty);
                ctx.lineTo(5, ty);
            }
        }
        ctx.stroke();

        ctx.fillStyle = 'black';

        if (this.browser.rulerLocation == 'right') {
            ctx.textAlign = 'right';
            ctx.fillText(formatQuantLabel(quant.max), w-9, 8);
            ctx.fillText(formatQuantLabel(quant.min), w-9, h + MIN_PADDING);
            for (var t = 1; t < numTics-1; ++t) {
                var ty = t*ticSpacing;
                ctx.fillText(formatQuantLabel((1.0*quant.max) - (t*ticInterval)), w - 9, ty + 3);
            }
        } else {
            ctx.textAlign = 'left';
            ctx.fillText(formatQuantLabel(quant.max), 9, 8);
            ctx.fillText(formatQuantLabel(quant.min), 9, h + MIN_PADDING);
            for (var t = 1; t < numTics-1; ++t) {
                var ty = t*ticSpacing;
                ctx.fillText(formatQuantLabel((1.0*quant.max) - (t*ticInterval)), 9, ty + 3);
            }
        }
    } else {
        this.quantOverlay.style.display = 'none';
    }
}

function glyphsForGroup(features, y, groupElement, tier, connectorType) {
    var gstyle = tier.styleForFeature(groupElement);
    var label;

    var glyphs = [];
    var strand = null;
    for (var i = 0; i < features.length; ++i) {
        var f = features[i];
        if (f.orientation && strand==null) {
            strand = f.orientation;
        }
         if (!label && f.label) {
            label = f.label;
        }

        var style = tier.styleForFeature(f);
        if (!style) {
            continue;
        }
        if (f.parts) {  // FIXME shouldn't really be needed
            continue;
        }

        var g = glyphForFeature(f, 0, style, tier, null, true);
        if (g) {
            glyphs.push(g);
        }
    }

    if (glyphs.length == 0)
        return null;
    
    var connector = 'flat';
    if (tier.dasSource.collapseSuperGroups && !tier.bumped) {
        if (strand === '+') {
            connector = 'collapsed+';
        } else if (strand === '-') {
            connector = 'collapsed-';
        }
    } else {
        if (strand === '+') {
            connector = 'hat+';
        } else if (strand === '-') {
            connector = 'hat-';
        }
    }

    var labelText = null;
    if (label || (gstyle && (gstyle.LABEL || gstyle.LABELS))) {  // HACK, LABELS should work.
        labelText = groupElement.label || label;
        var sg = tier.groupsToSupers[groupElement.id];
        if (sg && tier.superGroups[sg]) {    // workaround case where group and supergroup IDs match.
            //if (groupElement.id != tier.superGroups[sg][0]) {
            //    dg.label = null;
            // }
        }
    }

    var gg = new GroupGlyph(glyphs, connector);
    if (labelText) {
        if (strand === '+') {
            labelText = '>' + labelText;
        } else if (strand === '-') {
            labelText = '<' + labelText;
        }
        gg = new LabelledGlyph(gg, labelText, false);
    }
    gg.bump = true;
    return gg;
}

function glyphForFeature(feature, y, style, tier, forceHeight, noLabel)
{
    var scale = tier.browser.scale, origin = tier.browser.viewStart;
    var gtype = style.glyph || 'BOX';
    var glyph;

    var min = feature.min;
    var max = feature.max;
    var type = feature.type;
    var strand = feature.orientation;
    var score = feature.score;
    var label = feature.label || feature.id;

    var minPos = (min - origin) * scale;
    var rawMaxPos = ((max - origin + 1) * scale);
    var maxPos = Math.max(rawMaxPos, minPos + 1);

    var height = tier.forceHeight || style.HEIGHT || forceHeight || 12;
    var requiredHeight = height = 1.0 * height;
    var bump = style.BUMP && isDasBooleanTrue(style.BUMP);

    var gg, quant;

    if (gtype === 'CROSS' || gtype === 'EX' || gtype === 'TRIANGLE' || gtype === 'DOT' || gtype === 'SQUARE' || gtype === 'STAR' || gtype === 'PLIMSOLL') {
        var stroke = style.FGCOLOR || 'black';
        var fill = style.BGCOLOR || 'none';
        var outline = style.STROKECOLOR;

        if (isDasBooleanTrue(style.COLOR_BY_SCORE2)) {
            var grad = style.BGGRAD || style._gradient;
            if (!grad) {
                grad = makeGradient(50, style.COLOR1, style.COLOR2, style.COLOR3);
                style._gradient = grad;
            }

            var sc2 = feature.score2 || 0;
            var smin2 = style.MIN2 ? (1.0 * style.MIN2) : 0.0;
            var smax2 = style.MAX2 ? (1.0 * style.MAX2) : 1.0;
            var relScore2 = ((1.0 * sc2) - smin2) / (smax2-smin2);

            var step = (relScore2*grad.length)|0;
            if (step < 0) step = 0;
            if (step >= grad.length) step = grad.length - 1;
            stroke = grad[step];
        }

        var height = tier.forceHeight || style.HEIGHT || forceHeight || 12;
        requiredHeight = height = 1.0 * height;

        var size = style.SIZE || height;
        if (style.RSIZE) {
            size = (1.0 * style.RSIZE) * height;
        }

        if (style.STROKETHRESHOLD) {
            if (size < (1.0 * style.STROKETHRESHOLD))
                outline = null;
        }
        
        size = 1.0 * size;

        var mid = (minPos + maxPos)/2;
        var hh = size/2;

        var mark;
        var bMinPos = minPos, bMaxPos = maxPos;

        if (gtype === 'EX') {
            gg = new ExGlyph(mid, size, stroke);
        } else if (gtype === 'TRIANGLE') {
            var dir = style.DIRECTION || 'N';
            var width = style.LINEWIDTH || size;
            gg = new TriangleGlyph(mid, size, dir, width, stroke, outline);
        } else if (gtype === 'DOT') {
            gg = new DotGlyph(mid, size, stroke, outline);
        } else if (gtype === 'PLIMSOLL') {
            gg = new PlimsollGlyph(mid, size, 0.2 * size, stroke, outline);
        } else if (gtype === 'SQUARE') {
            gg = new BoxGlyph(mid - hh, 0, size, size, stroke, outline);
        } else if (gtype === 'STAR') {
            var points = 5;
            if (style.POINTS) 
                points = style.POINTS | 0;
            gg = new StarGlyph(mid, hh, points, stroke, outline);
        } else {
            gg = new CrossGlyph(mid, size, stroke);
        }

        if (fill && fill != 'none' && (maxPos - minPos) > 5) {
            var bgg = new BoxGlyph(minPos, 0, (maxPos - minPos), size, fill);
            gg = new GroupGlyph([bgg, gg]);
        }

        if (isDasBooleanTrue(style.SCATTER)) {
            var smin = tier.quantMin(style); // tier.dasSource.forceMin || style.MIN || tier.currentFeaturesMinScore;
            var smax = tier.quantMax(style); // tier.dasSource.forceMax || style.MAX || tier.currentFeaturesMaxScore;

            if (!smax) {
                if (smin < 0) {
                    smax = 0;<commit_msg>Support the case where not every feature has a score2.<commit_after>// 
// Dalliance Genome Explorer
// (c) <NAME> 2006-2010
//
// feature-draw.js: new feature-tier renderer
//

var MIN_PADDING = 3;
var DEFAULT_SUBTIER_MAX = 100;

function isDasBooleanTrue(s) {
    s = ('' + s).toLowerCase();
    return s==='yes' || s==='true';
}

function SubTier() {
    this.glyphs = [];
    this.height = 0;
    this.quant = null;
}

SubTier.prototype.add = function(glyph) {
    this.glyphs.push(glyph);
    this.height = Math.max(this.height, glyph.height());
    if (glyph.quant && this.quant == null) {
        this.quant = glyph.quant;
    }
}

SubTier.prototype.hasSpaceFor = function(glyph) {
    for (var i = 0; i < this.glyphs.length; ++i) {
        var g = this.glyphs[i];
        if (g.min() <= glyph.max() && g.max() >= glyph.min()) {
            return false;
        }
    }
    return true;
}

var GLOBAL_GC;

function drawFeatureTier(tier)
{
    GLOBAL_GC = tier.viewport.getContext('2d'); // Should only be used for metrics.
    sortFeatures(tier);

    var glyphs = [];
    var specials = false;

    for (var uft in tier.ungroupedFeatures) {
        var ufl = tier.ungroupedFeatures[uft];
        // var style = styles[uft] || styles['default'];
        var style = tier.styleForFeature(ufl[0]);   // FIXME this isn't quite right...
        if (!style) continue;
        if (style.glyph == 'LINEPLOT') {
            glyphs.push(makeLineGlyph(ufl, style, tier));
            specials = true;
        } else {
            for (var pgid = 0; pgid < ufl.length; ++pgid) {
                var f = ufl[pgid];
                if (f.parts) {  // FIXME shouldn't really be needed
                    continue;
                }
                var g = glyphForFeature(f, 0, tier.styleForFeature(f), tier);
                if (g)
                    glyphs.push(g);
            }
        }
    }

    // Merge supergroups
    
    if (tier.dasSource.collapseSuperGroups && !tier.bumped) {
        for (var sg in tier.superGroups) {
            var sgg = tier.superGroups[sg];
            tier.groups[sg].type = tier.groups[sgg[0]].type;   // HACK to make styling easier in DAS1.6
            var featsByType = {};
            for (var g = 0; g < sgg.length; ++g) {
                var gf = tier.groupedFeatures[sgg[g]];
                for (var fi = 0; fi < gf.length; ++fi) {
                    var f = gf[fi];
                    pusho(featsByType, f.type, f);
                }

                if (tier.groups[sg] && !tier.groups[sg].links || tier.groups[sg].links.length == 0) {
                    tier.groups[sg].links = tier.groups[sgg[0]].links;
                }

                delete tier.groupedFeatures[sgg[g]];  // 'cos we don't want to render the unmerged version.
            }

            for (var t in featsByType) {
                var feats = featsByType[t];
                var template = feats[0];
                var loc = null;
                for (var fi = 0; fi < feats.length; ++fi) {
                    var f = feats[fi];
                    var fl = new Range(f.min, f.max);
                    if (!loc) {
                        loc = fl;
                    } else {
                        loc = union(loc, fl);
                    }
                }
                var mergedRanges = loc.ranges();
                for (var si = 0; si < mergedRanges.length; ++si) {
                    var r = mergedRanges[si];

                    // begin coverage-counting
                    var posCoverage = ((r.max()|0) - (r.min()|0) + 1) * sgg.length;
                    var actCoverage = 0;
                    for (var fi = 0; fi < feats.length; ++fi) {
                        var f = feats[fi];
                        if ((f.min|0) <= r.max() && (f.max|0) >= r.min()) {
                            var umin = Math.max(f.min|0, r.min());
                            var umax = Math.min(f.max|0, r.max());
                            actCoverage += (umax - umin + 1);
                        }
                    }
                    var visualWeight = ((1.0 * actCoverage) / posCoverage);
                    // end coverage-counting

                    var newf = new DASFeature();
                    for (k in template) {
                        newf[k] = template[k];
                    }
                    newf.min = r.min();
                    newf.max = r.max();
                    if (newf.label && sgg.length > 1) {
                        newf.label += ' (' + sgg.length + ' vars)';
                    }
                    newf.visualWeight = ((1.0 * actCoverage) / posCoverage);
                    pusho(tier.groupedFeatures, sg, newf);
                    // supergroups are already in tier.groups.
                }
            }

            delete tier.superGroups[sg]; // Do we want this?
        }       
    }

    // Glyphify groups.

    var gl = new Array();
    for (var gid in tier.groupedFeatures) {
        gl.push(gid);
    }
    gl.sort(function(g1, g2) {
        var d = tier.groupedFeatures[g1][0].score - tier.groupedFeatures[g2][0].score;
        if (d > 0) {
            return -1;
        } else if (d == 0) {
            return 0;
        } else {
            return 1;
        }
    });

    var groupGlyphs = {};
    for (var gx = 0; gx < gl.length; ++gx) {
        var gid = gl[gx];
        var g = glyphsForGroup(tier.groupedFeatures[gid], 0, tier.groups[gid], tier,
                               (tier.dasSource.collapseSuperGroups && !tier.bumped) ? 'collapsed_gene' : 'tent');
        if (g) {
            g.group = tier.groups[gid];
            groupGlyphs[gid] = g;
        }
    }

    for (var sg in tier.superGroups) {
        var sgg = tier.superGroups[sg];
        var sgGlyphs = [];
        var sgMin = 10000000000;
        var sgMax = -10000000000;
        for (var sgi = 0; sgi < sgg.length; ++sgi) {
            var gg = groupGlyphs[sgg[sgi]];
            groupGlyphs[sgg[sgi]] = null;
            if (gg) {
                sgGlyphs.push(gg);
                sgMin = Math.min(sgMin, gg.min());
                sgMax = Math.max(sgMax, gg.max());
            }
        }
        for (var sgi = 0; sgi < sgGlyphs.length; ++sgi) {
            var gg = sgGlyphs[sgi];
            glyphs.push(new PaddedGlyph(gg, sgMin, sgMax));
        }
    }
    for (var g in groupGlyphs) {
        var gg = groupGlyphs[g];
        if (gg) {
            glyphs.push(gg);
        }
    }



    // Bumping

    var unbumpedST = new SubTier();
    var bumpedSTs = [];
    var hasBumpedFeatures = false;
    var subtierMax = tier.dasSource.subtierMax || DEFAULT_SUBTIER_MAX;
    
  GLYPH_LOOP:
    for (var i = 0; i < glyphs.length; ++i) {
        var g = glyphs[i];
        if (g.bump) {
            hasBumpedFeatures = true;
        }
        if (g.bump && (tier.bumped || tier.dasSource.collapseSuperGroups)) {       // kind-of nasty.  supergroup collapsing is different from "normal" unbumping
            for (var sti = 0; sti < bumpedSTs.length;  ++sti) {
                var st = bumpedSTs[sti];
                if (st.hasSpaceFor(g)) {
                    st.add(g);
                    continue GLYPH_LOOP;
                }
            }
            if (bumpedSTs.length >= subtierMax) {
                tier.status = 'Too many overlapping features, truncating at ' + subtierMax;
            } else {
                var st = new SubTier();
                st.add(g);
                bumpedSTs.push(st);
            }
        } else {
            unbumpedST.add(g);
        }
    }

    if (unbumpedST.glyphs.length > 0) {
        bumpedSTs = [unbumpedST].concat(bumpedSTs);
    }

    for (var sti = 0; sti < bumpedSTs.length; ++sti) {
        var st = bumpedSTs[sti];
        if (st.quant) {
            st.glyphs.unshift(new GridGlyph(st.height));
        }
    }

    for (var sti = 0; sti < bumpedSTs.length; ++sti) {
        var st = bumpedSTs[sti];
        st.glyphs.sort(function (g1, g2) {
            var z1 = g1.zindex || 0;
            var z2 = g2.zindex || 0;
            return z1 - z2;
        });
    }

    tier.subtiers = bumpedSTs;
    tier.glyphCacheOrigin = tier.browser.viewStart;
}

function formatQuantLabel(v) {
    var t = '' + v;
    var dot = t.indexOf('.');
    if (dot < 0) {
        return t;
    } else {
        var dotThreshold = 2;
        if (t.substring(0, 1) == '-') {
            ++dotThreshold;
        }

        if (dot >= dotThreshold) {
            return t.substring(0, dot);
        } else {
            return t.substring(0, dot + 2);
        }
    }
}

DasTier.prototype.paint = function() {
    var retina = this.browser.retina && window.devicePixelRatio > 1;

    var subtiers = this.subtiers;
    if (!subtiers) {
	   return;
    }

    var desiredWidth = this.browser.featurePanelWidth + 2000;
    if (retina) {
        desiredWidth *= 2;
    }
    var fpw = this.viewport.width|0;
    if (fpw < desiredWidth - 50) {
        this.viewport.width = fpw = desiredWidth;
    }

    var lh = MIN_PADDING;
    for (var s = 0; s < subtiers.length; ++s) {
        lh = lh + subtiers[s].height + MIN_PADDING;
    }
    lh += 6

    var canvasHeight = lh;
    if (retina) {
        canvasHeight *= 2;
    }

    if (canvasHeight != this.viewport.height) {
        this.viewport.height = canvasHeight;
    }
    
    var tierHeight = Math.max(lh, this.browser.minTierHeight);
    this.viewport.style.left = '-1000px';
    this.viewport.style.width = retina ? ('' + (fpw/2) + 'px') : ('' + fpw + 'px');
    this.viewport.style.height = '' + lh + 'px';
    this.layoutHeight =  Math.max(lh, this.browser.minTierHeight);
    this.holder.style.height = '' + this.layoutHeight + 'px';

    this.updateHeight();
    this.drawOverlay();
    this.norigin = this.browser.viewStart;

    var gc = this.viewport.getContext('2d');
    gc.fillStyle = this.background;
    gc.clearRect(0, 0, fpw, canvasHeight);
    gc.restore();

    gc.save();
    if (retina) {
        gc.scale(2, 2);
    }
    var offset = ((this.glyphCacheOrigin - this.browser.viewStart)*this.browser.scale)+1000;
    gc.translate(offset, MIN_PADDING);
   
    for (var s = 0; s < subtiers.length; ++s) {
        var quant = null;
        var glyphs = subtiers[s].glyphs;
        for (var i = 0; i < glyphs.length; ++i) {
            var glyph = glyphs[i];
            if (glyph.min() < fpw-offset && glyph.max() > -offset) { 
                var glyph = glyphs[i];
                glyph.draw(gc);
                if (glyph.quant) {
                    quant = glyph.quant;
                }
            }
        }
        gc.translate(0, subtiers[s].height + MIN_PADDING);
    }
    gc.restore();

    if (quant && this.quantLeapThreshold && this.featureSource && sourceAdapterIsCapable(this.featureSource, 'quantLeap')) {
        var ry = 3 + subtiers[0].height * (1.0 - ((this.quantLeapThreshold - quant.min) / (quant.max - quant.min)));

        gc.save();
        if (retina)
            gc.scale(2, 2);
        gc.strokeStyle = 'red';
        gc.lineWidth = 0.3;
        gc.beginPath();
        gc.moveTo(0, ry);
        gc.lineTo(5000, ry);
        gc.stroke();
        gc.restore();
    }

    this.paintQuant();
}

DasTier.prototype.paintQuant = function() {
    if (!this.quantOverlay)
        return;

    var retina = this.browser.retina && window.devicePixelRatio > 1;

    var quant;
    if (this.subtiers && this.subtiers.length > 0)
        quant = this.subtiers[0].quant;

    if (quant) {
        var h = this.subtiers[0].height;
        var w = 50;
        this.quantOverlay.height = this.viewport.height;
        this.quantOverlay.width = retina ? w*2 : w;
        this.quantOverlay.style.height = '' + (retina ? this.quantOverlay.height/2 : this.quantOverlay.height) + 'px';
        this.quantOverlay.style.width = '' + w + 'px';
        this.quantOverlay.style.display = 'block';
        var ctx = this.quantOverlay.getContext('2d');
        if (retina)
            ctx.scale(2, 2);

        ctx.fillStyle = 'white'
        ctx.globalAlpha = 0.6;

        var numTics = 2;
        if (h > 40) {
            numTics = 1 + ((h/20) | 0);
        }
        var ticSpacing = (h+MIN_PADDING*2) / (numTics - 1);
        var ticInterval = (quant.max - quant.min) / (numTics - 1);

        if (this.browser.rulerLocation == 'right') {
            ctx.fillRect(w-30, 0, 30, 20);
            ctx.fillRect(w-30, h-20 + MIN_PADDING*2, 30, 20);
            for (var t = 1; t < numTics-1; ++t) {
                ctx.fillRect(w-30, t*ticSpacing - 10, 30, 20);
            }
        } else {
            ctx.fillRect(0, 0, 30, 20);
            ctx.fillRect(0, h - 20 + MIN_PADDING*2, 30, 20);
            for (var t = 1; t < numTics-1; ++t) {
                ctx.fillRect(0, t*ticSpacing - 10, 30, 20);
            }
        }
        ctx.globalAlpha = 1.0;

        ctx.strokeStyle = 'black';
        ctx.lineWidth = 1;
        ctx.beginPath();

        if (this.browser.rulerLocation == 'right') {
            ctx.moveTo(w - 8, MIN_PADDING);
            ctx.lineTo(w, MIN_PADDING);
            ctx.lineTo(w, h + MIN_PADDING);
            ctx.lineTo(w - 8, h + MIN_PADDING);
            for (var t = 1; t < numTics-1; ++t) {
                var ty = t*ticSpacing;
                ctx.moveTo(w, ty);
                ctx.lineTo(w - 5, ty);
            }
        } else {
            ctx.moveTo(8, MIN_PADDING);
            ctx.lineTo(0, MIN_PADDING);
            ctx.lineTo(0, h+MIN_PADDING);
            ctx.lineTo(8, h+MIN_PADDING);
            for (var t = 1; t < numTics-1; ++t) {
                var ty = t*ticSpacing;
                ctx.moveTo(0, ty);
                ctx.lineTo(5, ty);
            }
        }
        ctx.stroke();

        ctx.fillStyle = 'black';

        if (this.browser.rulerLocation == 'right') {
            ctx.textAlign = 'right';
            ctx.fillText(formatQuantLabel(quant.max), w-9, 8);
            ctx.fillText(formatQuantLabel(quant.min), w-9, h + MIN_PADDING);
            for (var t = 1; t < numTics-1; ++t) {
                var ty = t*ticSpacing;
                ctx.fillText(formatQuantLabel((1.0*quant.max) - (t*ticInterval)), w - 9, ty + 3);
            }
        } else {
            ctx.textAlign = 'left';
            ctx.fillText(formatQuantLabel(quant.max), 9, 8);
            ctx.fillText(formatQuantLabel(quant.min), 9, h + MIN_PADDING);
            for (var t = 1; t < numTics-1; ++t) {
                var ty = t*ticSpacing;
                ctx.fillText(formatQuantLabel((1.0*quant.max) - (t*ticInterval)), 9, ty + 3);
            }
        }
    } else {
        this.quantOverlay.style.display = 'none';
    }
}

function glyphsForGroup(features, y, groupElement, tier, connectorType) {
    var gstyle = tier.styleForFeature(groupElement);
    var label;

    var glyphs = [];
    var strand = null;
    for (var i = 0; i < features.length; ++i) {
        var f = features[i];
        if (f.orientation && strand==null) {
            strand = f.orientation;
        }
         if (!label && f.label) {
            label = f.label;
        }

        var style = tier.styleForFeature(f);
        if (!style) {
            continue;
        }
        if (f.parts) {  // FIXME shouldn't really be needed
            continue;
        }

        var g = glyphForFeature(f, 0, style, tier, null, true);
        if (g) {
            glyphs.push(g);
        }
    }

    if (glyphs.length == 0)
        return null;
    
    var connector = 'flat';
    if (tier.dasSource.collapseSuperGroups && !tier.bumped) {
        if (strand === '+') {
            connector = 'collapsed+';
        } else if (strand === '-') {
            connector = 'collapsed-';
        }
    } else {
        if (strand === '+') {
            connector = 'hat+';
        } else if (strand === '-') {
            connector = 'hat-';
        }
    }

    var labelText = null;
    if (label || (gstyle && (gstyle.LABEL || gstyle.LABELS))) {  // HACK, LABELS should work.
        labelText = groupElement.label || label;
        var sg = tier.groupsToSupers[groupElement.id];
        if (sg && tier.superGroups[sg]) {    // workaround case where group and supergroup IDs match.
            //if (groupElement.id != tier.superGroups[sg][0]) {
            //    dg.label = null;
            // }
        }
    }

    var gg = new GroupGlyph(glyphs, connector);
    if (labelText) {
        if (strand === '+') {
            labelText = '>' + labelText;
        } else if (strand === '-') {
            labelText = '<' + labelText;
        }
        gg = new LabelledGlyph(gg, labelText, false);
    }
    gg.bump = true;
    return gg;
}

function glyphForFeature(feature, y, style, tier, forceHeight, noLabel)
{
    var scale = tier.browser.scale, origin = tier.browser.viewStart;
    var gtype = style.glyph || 'BOX';
    var glyph;

    var min = feature.min;
    var max = feature.max;
    var type = feature.type;
    var strand = feature.orientation;
    var score = feature.score;
    var label = feature.label || feature.id;

    var minPos = (min - origin) * scale;
    var rawMaxPos = ((max - origin + 1) * scale);
    var maxPos = Math.max(rawMaxPos, minPos + 1);

    var height = tier.forceHeight || style.HEIGHT || forceHeight || 12;
    var requiredHeight = height = 1.0 * height;
    var bump = style.BUMP && isDasBooleanTrue(style.BUMP);

    var gg, quant;

    if (gtype === 'CROSS' || gtype === 'EX' || gtype === 'TRIANGLE' || gtype === 'DOT' || gtype === 'SQUARE' || gtype === 'STAR' || gtype === 'PLIMSOLL') {
        var stroke = style.FGCOLOR || 'black';
        var fill = style.BGCOLOR || 'none';
        var outline = style.STROKECOLOR;

        if (isDasBooleanTrue(style.COLOR_BY_SCORE2)) {
            var grad = style.BGGRAD || style._gradient;
            if (!grad) {
                grad = makeGradient(50, style.COLOR1, style.COLOR2, style.COLOR3);
                style._gradient = grad;
            }

            var sc2 = feature.score2;
            if (sc2 != undefined || !stroke) {
                sc2 = sc2 || 0;

                var smin2 = style.MIN2 ? (1.0 * style.MIN2) : 0.0;
                var smax2 = style.MAX2 ? (1.0 * style.MAX2) : 1.0;
                var relScore2 = ((1.0 * sc2) - smin2) / (smax2-smin2);

                var step = (relScore2*grad.length)|0;
                if (step < 0) step = 0;
                if (step >= grad.length) step = grad.length - 1;
                stroke = grad[step];
            }
        }

        var height = tier.forceHeight || style.HEIGHT || forceHeight || 12;
        requiredHeight = height = 1.0 * height;

        var size = style.SIZE || height;
        if (style.RSIZE) {
            size = (1.0 * style.RSIZE) * height;
        }

        if (style.STROKETHRESHOLD) {
            if (size < (1.0 * style.STROKETHRESHOLD))
                outline = null;
        }
        
        size = 1.0 * size;

        var mid = (minPos + maxPos)/2;
        var hh = size/2;

        var mark;
        var bMinPos = minPos, bMaxPos = maxPos;

        if (gtype === 'EX') {
            gg = new ExGlyph(mid, size, stroke);
        } else if (gtype === 'TRIANGLE') {
            var dir = style.DIRECTION || 'N';
            var width = style.LINEWIDTH || size;
            gg = new TriangleGlyph(mid, size, dir, width, stroke, outline);
        } else if (gtype === 'DOT') {
            gg = new DotGlyph(mid, size, stroke, outline);
        } else if (gtype === 'PLIMSOLL') {
            gg = new PlimsollGlyph(mid, size, 0.2 * size, stroke, outline);
        } else if (gtype === 'SQUARE') {
            gg = new BoxGlyph(mid - hh, 0, size, size, stroke, outline);
        } else if (gtype === 'STAR') {
            var points = 5;
            if (style.POINTS) 
                points = style.POINTS | 0;
            gg = new StarGlyph(mid, hh, points, stroke, outline);
        } else {
            gg = new CrossGlyph(mid, size, stroke);
        }

        if (fill && fill != 'none' && (maxPos - minPos) > 5) {
            var bgg = new BoxGlyph(minPos, 0, (maxPos - minPos), size, fill);
            gg = new GroupGlyph([bgg, gg]);
        }

        if (isDasBooleanTrue(style.SCATTER)) {
            var smin = tier.quantMin(style);
            var smax = tier.quantMax(style);

            if (!smax) {
                if (smin < 0) {
                    smax = 0;
{'fraction_non_alphanumeric': 0.14205172202475794, 'fraction_numerical': 0.015590145851207255, 'mean_word_length': 1.530926236118866, 'pattern_counts': {'":': 0, '<': 71, '<?xml version=': 0, '>': 35, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 4, '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': '4288833', 'n_tokens_mistral': 14211, 'n_tokens_neox': 13164, 'n_words': 3027}