StackGenVis: Alignment of Data, Algorithms, and Models for Stacking Ensemble Learning Using Performance Metrics
https://doi.org/10.1109/TVCG.2020.3030352
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
712 lines
16 KiB
712 lines
16 KiB
'use strict'
|
|
|
|
|
|
const rgba = require('color-normalize')
|
|
const getBounds = require('array-bounds')
|
|
const extend = require('object-assign')
|
|
const glslify = require('glslify')
|
|
const pick = require('pick-by-alias')
|
|
const flatten = require('flatten-vertex-data')
|
|
const triangulate = require('earcut')
|
|
const normalize = require('array-normalize')
|
|
const { float32, fract32 } = require('to-float32')
|
|
const WeakMap = require('es6-weak-map')
|
|
const parseRect = require('parse-rect')
|
|
|
|
|
|
module.exports = Line2D
|
|
|
|
|
|
/** @constructor */
|
|
function Line2D (regl, options) {
|
|
if (!(this instanceof Line2D)) return new Line2D(regl, options)
|
|
|
|
if (typeof regl === 'function') {
|
|
if (!options) options = {}
|
|
options.regl = regl
|
|
}
|
|
else {
|
|
options = regl
|
|
}
|
|
if (options.length) options.positions = options
|
|
regl = options.regl
|
|
|
|
if (!regl.hasExtension('ANGLE_instanced_arrays')) {
|
|
throw Error('regl-error2d: `ANGLE_instanced_arrays` extension should be enabled');
|
|
}
|
|
|
|
// persistent variables
|
|
this.gl = regl._gl
|
|
this.regl = regl
|
|
|
|
// list of options for lines
|
|
this.passes = []
|
|
|
|
// cached shaders instance
|
|
this.shaders = Line2D.shaders.has(regl) ? Line2D.shaders.get(regl) : Line2D.shaders.set(regl, Line2D.createShaders(regl)).get(regl)
|
|
|
|
|
|
// init defaults
|
|
this.update(options)
|
|
}
|
|
|
|
|
|
Line2D.dashMult = 2
|
|
Line2D.maxPatternLength = 256
|
|
Line2D.precisionThreshold = 3e6
|
|
Line2D.maxPoints = 1e4
|
|
Line2D.maxLines = 2048
|
|
|
|
|
|
// cache of created draw calls per-regl instance
|
|
Line2D.shaders = new WeakMap()
|
|
|
|
|
|
// create static shaders once
|
|
Line2D.createShaders = function (regl) {
|
|
let offsetBuffer = regl.buffer({
|
|
usage: 'static',
|
|
type: 'float',
|
|
data: [0,1, 0,0, 1,1, 1,0]
|
|
})
|
|
|
|
let shaderOptions = {
|
|
primitive: 'triangle strip',
|
|
instances: regl.prop('count'),
|
|
count: 4,
|
|
offset: 0,
|
|
|
|
uniforms: {
|
|
miterMode: (ctx, prop) => prop.join === 'round' ? 2 : 1,
|
|
miterLimit: regl.prop('miterLimit'),
|
|
scale: regl.prop('scale'),
|
|
scaleFract: regl.prop('scaleFract'),
|
|
translateFract: regl.prop('translateFract'),
|
|
translate: regl.prop('translate'),
|
|
thickness: regl.prop('thickness'),
|
|
dashPattern: regl.prop('dashTexture'),
|
|
opacity: regl.prop('opacity'),
|
|
pixelRatio: regl.context('pixelRatio'),
|
|
id: regl.prop('id'),
|
|
dashSize: regl.prop('dashLength'),
|
|
viewport: (c, p) => [p.viewport.x, p.viewport.y, c.viewportWidth, c.viewportHeight],
|
|
depth: regl.prop('depth')
|
|
},
|
|
|
|
blend: {
|
|
enable: true,
|
|
color: [0,0,0,0],
|
|
equation: {
|
|
rgb: 'add',
|
|
alpha: 'add'
|
|
},
|
|
func: {
|
|
srcRGB: 'src alpha',
|
|
dstRGB: 'one minus src alpha',
|
|
srcAlpha: 'one minus dst alpha',
|
|
dstAlpha: 'one'
|
|
}
|
|
},
|
|
depth: {
|
|
enable: (c, p) => {
|
|
return !p.overlay
|
|
}
|
|
},
|
|
stencil: {enable: false},
|
|
scissor: {
|
|
enable: true,
|
|
box: regl.prop('viewport')
|
|
},
|
|
viewport: regl.prop('viewport')
|
|
}
|
|
|
|
|
|
// simplified rectangular line shader
|
|
let drawRectLine = regl(extend({
|
|
vert: glslify('./rect-vert.glsl'),
|
|
frag: glslify('./rect-frag.glsl'),
|
|
|
|
attributes: {
|
|
// if point is at the end of segment
|
|
lineEnd: {
|
|
buffer: offsetBuffer,
|
|
divisor: 0,
|
|
stride: 8,
|
|
offset: 0
|
|
},
|
|
// if point is at the top of segment
|
|
lineTop: {
|
|
buffer: offsetBuffer,
|
|
divisor: 0,
|
|
stride: 8,
|
|
offset: 4
|
|
},
|
|
// beginning of line coordinate
|
|
aCoord: {
|
|
buffer: regl.prop('positionBuffer'),
|
|
stride: 8,
|
|
offset: 8,
|
|
divisor: 1
|
|
},
|
|
// end of line coordinate
|
|
bCoord: {
|
|
buffer: regl.prop('positionBuffer'),
|
|
stride: 8,
|
|
offset: 16,
|
|
divisor: 1
|
|
},
|
|
aCoordFract: {
|
|
buffer: regl.prop('positionFractBuffer'),
|
|
stride: 8,
|
|
offset: 8,
|
|
divisor: 1
|
|
},
|
|
bCoordFract: {
|
|
buffer: regl.prop('positionFractBuffer'),
|
|
stride: 8,
|
|
offset: 16,
|
|
divisor: 1
|
|
},
|
|
color: {
|
|
buffer: regl.prop('colorBuffer'),
|
|
stride: 4,
|
|
offset: 0,
|
|
divisor: 1
|
|
}
|
|
}
|
|
}, shaderOptions))
|
|
|
|
// create regl draw
|
|
let drawMiterLine
|
|
|
|
try {
|
|
drawMiterLine = regl(extend({
|
|
// culling removes polygon creasing
|
|
cull: {
|
|
enable: true,
|
|
face: 'back'
|
|
},
|
|
|
|
vert: glslify('./miter-vert.glsl'),
|
|
frag: glslify('./miter-frag.glsl'),
|
|
|
|
attributes: {
|
|
// is line end
|
|
lineEnd: {
|
|
buffer: offsetBuffer,
|
|
divisor: 0,
|
|
stride: 8,
|
|
offset: 0
|
|
},
|
|
// is line top
|
|
lineTop: {
|
|
buffer: offsetBuffer,
|
|
divisor: 0,
|
|
stride: 8,
|
|
offset: 4
|
|
},
|
|
// left color
|
|
aColor: {
|
|
buffer: regl.prop('colorBuffer'),
|
|
stride: 4,
|
|
offset: 0,
|
|
divisor: 1
|
|
},
|
|
// right color
|
|
bColor: {
|
|
buffer: regl.prop('colorBuffer'),
|
|
stride: 4,
|
|
offset: 4,
|
|
divisor: 1
|
|
},
|
|
prevCoord: {
|
|
buffer: regl.prop('positionBuffer'),
|
|
stride: 8,
|
|
offset: 0,
|
|
divisor: 1
|
|
},
|
|
aCoord: {
|
|
buffer: regl.prop('positionBuffer'),
|
|
stride: 8,
|
|
offset: 8,
|
|
divisor: 1
|
|
},
|
|
bCoord: {
|
|
buffer: regl.prop('positionBuffer'),
|
|
stride: 8,
|
|
offset: 16,
|
|
divisor: 1
|
|
},
|
|
nextCoord: {
|
|
buffer: regl.prop('positionBuffer'),
|
|
stride: 8,
|
|
offset: 24,
|
|
divisor: 1
|
|
}
|
|
}
|
|
}, shaderOptions))
|
|
} catch (e) {
|
|
// IE/bad Webkit fallback
|
|
drawMiterLine = drawRectLine
|
|
}
|
|
|
|
// fill shader
|
|
let drawFill = regl({
|
|
primitive: 'triangle',
|
|
elements: (ctx, prop) => prop.triangles,
|
|
offset: 0,
|
|
|
|
vert: glslify('./fill-vert.glsl'),
|
|
frag: glslify('./fill-frag.glsl'),
|
|
|
|
uniforms: {
|
|
scale: regl.prop('scale'),
|
|
color: regl.prop('fill'),
|
|
scaleFract: regl.prop('scaleFract'),
|
|
translateFract: regl.prop('translateFract'),
|
|
translate: regl.prop('translate'),
|
|
opacity: regl.prop('opacity'),
|
|
pixelRatio: regl.context('pixelRatio'),
|
|
id: regl.prop('id'),
|
|
viewport: (ctx, prop) => [prop.viewport.x, prop.viewport.y, ctx.viewportWidth, ctx.viewportHeight]
|
|
},
|
|
|
|
attributes: {
|
|
position: {
|
|
buffer: regl.prop('positionBuffer'),
|
|
stride: 8,
|
|
offset: 8
|
|
},
|
|
positionFract: {
|
|
buffer: regl.prop('positionFractBuffer'),
|
|
stride: 8,
|
|
offset: 8
|
|
}
|
|
},
|
|
|
|
blend: shaderOptions.blend,
|
|
|
|
depth: { enable: false },
|
|
scissor: shaderOptions.scissor,
|
|
stencil: shaderOptions.stencil,
|
|
viewport: shaderOptions.viewport
|
|
})
|
|
|
|
return {
|
|
fill: drawFill, rect: drawRectLine, miter: drawMiterLine
|
|
}
|
|
}
|
|
|
|
|
|
// used to for new lines instances
|
|
Line2D.defaults = {
|
|
dashes: null,
|
|
join: 'miter',
|
|
miterLimit: 1,
|
|
thickness: 10,
|
|
cap: 'square',
|
|
color: 'black',
|
|
opacity: 1,
|
|
overlay: false,
|
|
viewport: null,
|
|
range: null,
|
|
close: false,
|
|
fill: null
|
|
}
|
|
|
|
|
|
Line2D.prototype.render = function (...args) {
|
|
if (args.length) {
|
|
this.update(...args)
|
|
}
|
|
|
|
this.draw()
|
|
}
|
|
|
|
|
|
Line2D.prototype.draw = function (...args) {
|
|
// render multiple polylines via regl batch
|
|
(args.length ? args : this.passes).forEach((s, i) => {
|
|
// render array pass as a list of passes
|
|
if (s && Array.isArray(s)) return this.draw(...s)
|
|
|
|
if (typeof s === 'number') s = this.passes[s]
|
|
|
|
if (!(s && s.count > 1 && s.opacity)) return
|
|
|
|
this.regl._refresh()
|
|
|
|
if (s.fill && s.triangles && s.triangles.length > 2) {
|
|
this.shaders.fill(s)
|
|
}
|
|
|
|
if (!s.thickness) return
|
|
|
|
// high scale is only available for rect mode with precision
|
|
if (s.scale[0] * s.viewport.width > Line2D.precisionThreshold || s.scale[1] * s.viewport.height > Line2D.precisionThreshold) {
|
|
this.shaders.rect(s)
|
|
}
|
|
|
|
// thin this.passes or too many points are rendered as simplified rect shader
|
|
else if (s.join === 'rect' || (!s.join && (s.thickness <= 2 || s.count >= Line2D.maxPoints))) {
|
|
this.shaders.rect(s)
|
|
}
|
|
else {
|
|
this.shaders.miter(s)
|
|
}
|
|
})
|
|
|
|
return this
|
|
}
|
|
|
|
Line2D.prototype.update = function (options) {
|
|
if (!options) return
|
|
|
|
if (options.length != null) {
|
|
if (typeof options[0] === 'number') options = [{positions: options}]
|
|
}
|
|
|
|
// make options a batch
|
|
else if (!Array.isArray(options)) options = [options]
|
|
|
|
let { regl, gl } = this
|
|
|
|
// process per-line settings
|
|
options.forEach((o, i) => {
|
|
let state = this.passes[i]
|
|
|
|
if (o === undefined) return
|
|
|
|
// null-argument removes pass
|
|
if (o === null) {
|
|
this.passes[i] = null
|
|
return
|
|
}
|
|
|
|
if (typeof o[0] === 'number') o = {positions: o}
|
|
|
|
// handle aliases
|
|
o = pick(o, {
|
|
positions: 'positions points data coords',
|
|
thickness: 'thickness lineWidth lineWidths line-width linewidth width stroke-width strokewidth strokeWidth',
|
|
join: 'lineJoin linejoin join type mode',
|
|
miterLimit: 'miterlimit miterLimit',
|
|
dashes: 'dash dashes dasharray dash-array dashArray',
|
|
color: 'color colour stroke colors colours stroke-color strokeColor',
|
|
fill: 'fill fill-color fillColor',
|
|
opacity: 'alpha opacity',
|
|
overlay: 'overlay crease overlap intersect',
|
|
close: 'closed close closed-path closePath',
|
|
range: 'range dataBox',
|
|
viewport: 'viewport viewBox',
|
|
hole: 'holes hole hollow'
|
|
})
|
|
|
|
// init state
|
|
if (!state) {
|
|
this.passes[i] = state = {
|
|
id: i,
|
|
scale: null,
|
|
scaleFract: null,
|
|
translate: null,
|
|
translateFract: null,
|
|
count: 0,
|
|
hole: [],
|
|
depth: 0,
|
|
|
|
dashLength: 1,
|
|
dashTexture: regl.texture({
|
|
channels: 1,
|
|
data: new Uint8Array([255]),
|
|
width: 1,
|
|
height: 1,
|
|
mag: 'linear',
|
|
min: 'linear'
|
|
}),
|
|
|
|
colorBuffer: regl.buffer({
|
|
usage: 'dynamic',
|
|
type: 'uint8',
|
|
data: new Uint8Array()
|
|
}),
|
|
positionBuffer: regl.buffer({
|
|
usage: 'dynamic',
|
|
type: 'float',
|
|
data: new Uint8Array()
|
|
}),
|
|
positionFractBuffer: regl.buffer({
|
|
usage: 'dynamic',
|
|
type: 'float',
|
|
data: new Uint8Array()
|
|
})
|
|
}
|
|
|
|
o = extend({}, Line2D.defaults, o)
|
|
}
|
|
if (o.thickness != null) state.thickness = parseFloat(o.thickness)
|
|
if (o.opacity != null) state.opacity = parseFloat(o.opacity)
|
|
if (o.miterLimit != null) state.miterLimit = parseFloat(o.miterLimit)
|
|
if (o.overlay != null) {
|
|
state.overlay = !!o.overlay
|
|
if (i < Line2D.maxLines) {
|
|
state.depth = 2 * (Line2D.maxLines - 1 - i % Line2D.maxLines) / Line2D.maxLines - 1.;
|
|
}
|
|
}
|
|
if (o.join != null) state.join = o.join
|
|
if (o.hole != null) state.hole = o.hole
|
|
if (o.fill != null) state.fill = !o.fill ? null : rgba(o.fill, 'uint8')
|
|
if (o.viewport != null) state.viewport = parseRect(o.viewport)
|
|
|
|
if (!state.viewport) {
|
|
state.viewport = parseRect([
|
|
gl.drawingBufferWidth,
|
|
gl.drawingBufferHeight
|
|
])
|
|
}
|
|
|
|
if (o.close != null) state.close = o.close
|
|
|
|
// reset positions
|
|
if (o.positions === null) o.positions = []
|
|
if (o.positions) {
|
|
let positions, count
|
|
|
|
// if positions are an object with x/y
|
|
if (o.positions.x && o.positions.y) {
|
|
let xPos = o.positions.x
|
|
let yPos = o.positions.y
|
|
count = state.count = Math.max(
|
|
xPos.length,
|
|
yPos.length
|
|
)
|
|
positions = new Float64Array(count * 2)
|
|
for (let i = 0; i < count; i++) {
|
|
positions[i * 2] = xPos[i]
|
|
positions[i * 2 + 1] = yPos[i]
|
|
}
|
|
}
|
|
else {
|
|
positions = flatten(o.positions, 'float64')
|
|
count = state.count = Math.floor(positions.length / 2)
|
|
}
|
|
|
|
let bounds = state.bounds = getBounds(positions, 2)
|
|
|
|
// create fill positions
|
|
// FIXME: fill positions can be set only along with positions
|
|
if (state.fill) {
|
|
let pos = []
|
|
|
|
// filter bad vertices and remap triangles to ensure shape
|
|
let ids = {}
|
|
let lastId = 0
|
|
|
|
for (let i = 0, ptr = 0, l = state.count; i < l; i++) {
|
|
let x = positions[i*2]
|
|
let y = positions[i*2 + 1]
|
|
if (isNaN(x) || isNaN(y) || x == null || y == null) {
|
|
x = positions[lastId*2]
|
|
y = positions[lastId*2 + 1]
|
|
ids[i] = lastId
|
|
}
|
|
else {
|
|
lastId = i
|
|
}
|
|
pos[ptr++] = x
|
|
pos[ptr++] = y
|
|
}
|
|
|
|
let triangles = triangulate(pos, state.hole || [])
|
|
|
|
for (let i = 0, l = triangles.length; i < l; i++) {
|
|
if (ids[triangles[i]] != null) triangles[i] = ids[triangles[i]]
|
|
}
|
|
|
|
state.triangles = triangles
|
|
}
|
|
|
|
// update position buffers
|
|
let npos = new Float64Array(positions)
|
|
normalize(npos, 2, bounds)
|
|
|
|
let positionData = new Float64Array(count * 2 + 6)
|
|
|
|
// rotate first segment join
|
|
if (state.close) {
|
|
if (positions[0] === positions[count*2 - 2] &&
|
|
positions[1] === positions[count*2 - 1]) {
|
|
positionData[0] = npos[count*2 - 4]
|
|
positionData[1] = npos[count*2 - 3]
|
|
}
|
|
else {
|
|
positionData[0] = npos[count*2 - 2]
|
|
positionData[1] = npos[count*2 - 1]
|
|
}
|
|
}
|
|
else {
|
|
positionData[0] = npos[0]
|
|
positionData[1] = npos[1]
|
|
}
|
|
|
|
positionData.set(npos, 2)
|
|
|
|
// add last segment
|
|
if (state.close) {
|
|
// ignore coinciding start/end
|
|
if (positions[0] === positions[count*2 - 2] &&
|
|
positions[1] === positions[count*2 - 1]) {
|
|
positionData[count*2 + 2] = npos[2]
|
|
positionData[count*2 + 3] = npos[3]
|
|
state.count -= 1
|
|
}
|
|
else {
|
|
positionData[count*2 + 2] = npos[0]
|
|
positionData[count*2 + 3] = npos[1]
|
|
positionData[count*2 + 4] = npos[2]
|
|
positionData[count*2 + 5] = npos[3]
|
|
}
|
|
}
|
|
// add stub
|
|
else {
|
|
positionData[count*2 + 2] = npos[count*2 - 2]
|
|
positionData[count*2 + 3] = npos[count*2 - 1]
|
|
positionData[count*2 + 4] = npos[count*2 - 2]
|
|
positionData[count*2 + 5] = npos[count*2 - 1]
|
|
}
|
|
|
|
state.positionBuffer(float32(positionData))
|
|
state.positionFractBuffer(fract32(positionData))
|
|
}
|
|
|
|
if (o.range) {
|
|
state.range = o.range
|
|
} else if (!state.range) {
|
|
state.range = state.bounds
|
|
}
|
|
|
|
if ((o.range || o.positions) && state.count) {
|
|
let bounds = state.bounds
|
|
|
|
let boundsW = bounds[2] - bounds[0],
|
|
boundsH = bounds[3] - bounds[1]
|
|
|
|
let rangeW = state.range[2] - state.range[0],
|
|
rangeH = state.range[3] - state.range[1]
|
|
|
|
state.scale = [
|
|
boundsW / rangeW,
|
|
boundsH / rangeH
|
|
]
|
|
state.translate = [
|
|
-state.range[0] / rangeW + bounds[0] / rangeW || 0,
|
|
-state.range[1] / rangeH + bounds[1] / rangeH || 0
|
|
]
|
|
|
|
state.scaleFract = fract32(state.scale)
|
|
state.translateFract = fract32(state.translate)
|
|
}
|
|
|
|
if (o.dashes) {
|
|
let dashLength = 0., dashData
|
|
|
|
if (!o.dashes || o.dashes.length < 2) {
|
|
dashLength = 1.
|
|
dashData = new Uint8Array([255, 255, 255, 255, 255, 255, 255, 255])
|
|
}
|
|
|
|
else {
|
|
dashLength = 0.;
|
|
for(let i = 0; i < o.dashes.length; ++i) {
|
|
dashLength += o.dashes[i]
|
|
}
|
|
dashData = new Uint8Array(dashLength * Line2D.dashMult)
|
|
let ptr = 0
|
|
let fillColor = 255
|
|
|
|
// repeat texture two times to provide smooth 0-step
|
|
for (let k = 0; k < 2; k++) {
|
|
for(let i = 0; i < o.dashes.length; ++i) {
|
|
for(let j = 0, l = o.dashes[i] * Line2D.dashMult * .5; j < l; ++j) {
|
|
dashData[ptr++] = fillColor
|
|
}
|
|
fillColor ^= 255
|
|
}
|
|
}
|
|
}
|
|
|
|
state.dashLength = dashLength
|
|
state.dashTexture({
|
|
channels: 1,
|
|
data: dashData,
|
|
width: dashData.length,
|
|
height: 1,
|
|
mag: 'linear',
|
|
min: 'linear'
|
|
}, 0, 0)
|
|
}
|
|
|
|
if (o.color) {
|
|
let count = state.count
|
|
let colors = o.color
|
|
|
|
if (!colors) colors = 'transparent'
|
|
|
|
let colorData = new Uint8Array(count * 4 + 4)
|
|
|
|
// convert colors to typed arrays
|
|
if (!Array.isArray(colors) || typeof colors[0] === 'number') {
|
|
let c = rgba(colors, 'uint8')
|
|
|
|
for (let i = 0; i < count + 1; i++) {
|
|
colorData.set(c, i * 4)
|
|
}
|
|
} else {
|
|
for (let i = 0; i < count; i++) {
|
|
let c = rgba(colors[i], 'uint8')
|
|
colorData.set(c, i * 4)
|
|
}
|
|
colorData.set(rgba(colors[0], 'uint8'), count * 4)
|
|
}
|
|
|
|
state.colorBuffer({
|
|
usage: 'dynamic',
|
|
type: 'uint8',
|
|
data: colorData
|
|
})
|
|
}
|
|
})
|
|
|
|
// remove unmentioned passes
|
|
if (options.length < this.passes.length) {
|
|
for (let i = options.length; i < this.passes.length; i++) {
|
|
let pass = this.passes[i]
|
|
if (!pass) continue
|
|
pass.colorBuffer.destroy()
|
|
pass.positionBuffer.destroy()
|
|
pass.dashTexture.destroy()
|
|
}
|
|
this.passes.length = options.length
|
|
}
|
|
|
|
// remove null items
|
|
let passes = []
|
|
for (let i = 0; i < this.passes.length; i++) {
|
|
if (this.passes[i] !== null) passes.push(this.passes[i])
|
|
}
|
|
this.passes = passes
|
|
|
|
return this
|
|
}
|
|
|
|
Line2D.prototype.destroy = function () {
|
|
this.passes.forEach(pass => {
|
|
pass.colorBuffer.destroy()
|
|
pass.positionBuffer.destroy()
|
|
pass.dashTexture.destroy()
|
|
})
|
|
|
|
this.passes.length = 0
|
|
|
|
return this
|
|
}
|
|
|
|
|