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.
 
 
 
 
StackGenVis/frontend/node_modules/vue-slider-component/lib/utils/control.ts

572 lines
14 KiB

import Decimal from './decimal'
import {
Value,
Mark,
MarkOption,
Marks,
MarksProp,
ProcessProp,
ProcessOption,
MarksFunction,
DotOption,
} from '../typings'
// The distance each slider changes
type DotsPosChangeArray = number[]
export const enum ERROR_TYPE {
VALUE = 1,
INTERVAL,
MIN,
MAX,
ORDER,
}
type ERROR_MESSAGE = { [key in ERROR_TYPE]: string }
export const ERROR_MSG: ERROR_MESSAGE = {
[ERROR_TYPE.VALUE]: 'The type of the "value" is illegal',
[ERROR_TYPE.INTERVAL]:
'The prop "interval" is invalid, "(max - min)" cannot be divisible by "interval"',
[ERROR_TYPE.MIN]: 'The "value" cannot be less than the minimum.',
[ERROR_TYPE.MAX]: 'The "value" cannot be greater than the maximum.',
[ERROR_TYPE.ORDER]:
'When "order" is false, the parameters "minRange", "maxRange", "fixed", "enabled" are invalid.',
}
/**
* Slider logic control center
*
* @export
* @class Control
*/
export default class Control {
dotsPos: number[] = [] // The position of each slider
dotsValue: Value[] = [] // The value of each slider
data?: Value[]
enableCross: boolean
fixed: boolean
max: number
min: number
interval: number
minRange: number
maxRange: number
order: boolean
marks?: MarksProp
included?: boolean
process?: ProcessProp
adsorb?: boolean
dotOptions?: DotOption | DotOption[]
onError?: (type: ERROR_TYPE, message: string) => void
constructor(options: {
value: Value | Value[]
data?: Value[]
enableCross: boolean
fixed: boolean
max: number
min: number
interval: number
order: boolean
minRange?: number
maxRange?: number
marks?: MarksProp
included?: boolean
process?: ProcessProp
adsorb?: boolean
dotOptions?: DotOption | DotOption[]
onError?: (type: ERROR_TYPE, message: string) => void
}) {
this.data = options.data
this.max = options.max
this.min = options.min
this.interval = options.interval
this.order = options.order
this.marks = options.marks
this.included = options.included
this.process = options.process
this.adsorb = options.adsorb
this.dotOptions = options.dotOptions
this.onError = options.onError
if (this.order) {
this.minRange = options.minRange || 0
this.maxRange = options.maxRange || 0
this.enableCross = options.enableCross
this.fixed = options.fixed
} else {
if (options.minRange || options.maxRange || !options.enableCross || options.fixed) {
this.emitError(ERROR_TYPE.ORDER)
}
this.minRange = 0
this.maxRange = 0
this.enableCross = true
this.fixed = false
}
this.setValue(options.value)
}
setValue(value: Value | Value[]) {
this.setDotsValue(Array.isArray(value) ? [...value] : [value], true)
}
setDotsValue(value: Value[], syncPos?: boolean) {
this.dotsValue = value
if (syncPos) {
this.syncDotsPos()
}
}
// Set the slider position
setDotsPos(dotsPos: number[]) {
const list = this.order ? [...dotsPos].sort((a, b) => a - b) : dotsPos
this.dotsPos = list
this.setDotsValue(list.map(dotPos => this.getValueByPos(dotPos)), this.adsorb)
}
// Get value by position
getValueByPos(pos: number): Value {
let value = this.parsePos(pos)
// When included is true, the return value is the value of the nearest mark
if (this.included) {
let dir = 100
this.markList.forEach(mark => {
const curDir = Math.abs(mark.pos - pos)
if (curDir < dir) {
dir = curDir
value = mark.value
}
})
}
return value
}
// Sync slider position
syncDotsPos() {
this.dotsPos = this.dotsValue.map(v => this.parseValue(v))
}
/**
* Get all the marks
*
* @readonly
* @type {Mark[]}
* @memberof Control
*/
get markList(): Mark[] {
if (!this.marks) {
return []
}
const getMarkByValue = (value: Value, mark?: MarkOption): Mark => {
const pos = this.parseValue(value)
return {
pos,
value,
label: value,
active: this.isActiveByPos(pos),
...mark,
}
}
if (this.marks === true) {
return this.getValues().map(value => getMarkByValue(value))
} else if (Object.prototype.toString.call(this.marks) === '[object Object]') {
return Object.keys(this.marks)
.sort((a, b) => +a - +b)
.map(value => {
const item = (this.marks as Marks)[value]
return getMarkByValue(value, typeof item !== 'string' ? item : { label: item })
})
} else if (Array.isArray(this.marks)) {
return this.marks.map(value => getMarkByValue(value))
} else if (typeof this.marks === 'function') {
return this.getValues()
.map(value => ({ value, result: (this.marks as MarksFunction)(value) }))
.filter(({ result }) => !!result)
.map(({ value, result }) => getMarkByValue(value, result as Mark))
} else {
return []
}
}
/**
* Get the most recent slider index by position
*
* @param {number} pos
* @returns {number}
* @memberof Control
*/
getRecentDot(pos: number): number {
const arr = this.dotsPos.map(dotPos => Math.abs(dotPos - pos))
return arr.indexOf(Math.min(...arr))
}
/**
* Get index by value
*
* @param {Value} value
* @returns {number}
* @memberof Control
*/
getIndexByValue(value: Value): number {
if (this.data) {
return this.data.indexOf(value)
}
return new Decimal(+value)
.minus(this.min)
.divide(this.interval)
.toNumber()
}
/**
* Get value by index
*
* @param {index} number
* @returns {Value}
* @memberof Control
*/
getValueByIndex(index: number): Value {
if (index < 0) {
index = 0
} else if (index > this.total) {
index = this.total
}
return this.data
? this.data[index]
: new Decimal(index)
.multiply(this.interval)
.plus(this.min)
.toNumber()
}
/**
* Set the position of a single slider
*
* @param {number} pos
* @param {number} index
*/
setDotPos(pos: number, index: number) {
pos = this.getValidPos(pos, index).pos
const changePos = pos - this.dotsPos[index]
if (!changePos) {
return
}
let changePosArr: DotsPosChangeArray = new Array(this.dotsPos.length)
if (this.fixed) {
changePosArr = this.getFixedChangePosArr(changePos, index)
} else if (this.minRange || this.maxRange) {
changePosArr = this.getLimitRangeChangePosArr(pos, changePos, index)
} else {
changePosArr[index] = changePos
}
this.setDotsPos(this.dotsPos.map((curPos, i) => curPos + (changePosArr[i] || 0)))
}
/**
* In fixed mode, get the position of all slider changes
*
* @param {number} changePos Change distance of a single slider
* @param {number} index slider index
* @returns {DotsPosChangeArray}
* @memberof Control
*/
private getFixedChangePosArr(changePos: number, index: number): DotsPosChangeArray {
this.dotsPos.forEach((originPos, i) => {
if (i !== index) {
const { pos: lastPos, inRange } = this.getValidPos(originPos + changePos, i)
if (!inRange) {
changePos =
Math.min(Math.abs(lastPos - originPos), Math.abs(changePos)) * (changePos < 0 ? -1 : 1)
}
}
})
return this.dotsPos.map(_ => changePos)
}
/**
* In minRange/maxRange mode, get the position of all slider changes
*
* @param {number} pos position of a single slider
* @param {number} changePos Change distance of a single slider
* @param {number} index slider index
* @returns {DotsPosChangeArray}
* @memberof Control
*/
private getLimitRangeChangePosArr(
pos: number,
changePos: number,
index: number,
): DotsPosChangeArray {
const changeDots = [{ index, changePos }]
const newChangePos = changePos
;[this.minRange, this.maxRange].forEach((isLimitRange?: number, rangeIndex?: number) => {
if (!isLimitRange) {
return false
}
const isMinRange = rangeIndex === 0
const isForward = changePos > 0
let next = 0
if (isMinRange) {
next = isForward ? 1 : -1
} else {
next = isForward ? -1 : 1
}
// Determine if the two positions are within the legal interval
const inLimitRange = (pos2: number, pos1: number): boolean => {
const diff = Math.abs(pos2 - pos1)
return isMinRange ? diff < this.minRangeDir : diff > this.maxRangeDir
}
let i = index + next
let nextPos = this.dotsPos[i]
let curPos = pos
while (this.isPos(nextPos) && inLimitRange(nextPos, curPos)) {
const { pos: lastPos } = this.getValidPos(nextPos + newChangePos, i)
changeDots.push({
index: i,
changePos: lastPos - nextPos,
})
i = i + next
curPos = lastPos
nextPos = this.dotsPos[i]
}
})
return this.dotsPos.map((_, i) => {
const changeDot = changeDots.filter(dot => dot.index === i)
return changeDot.length ? changeDot[0].changePos : 0
})
}
private isPos(pos: any): boolean {
return typeof pos === 'number'
}
/**
* Get a valid position by pos
*
* @param {number} pos
* @param {number} index
* @returns {{ pos: number, inRange: boolean }}
*/
getValidPos(pos: number, index: number): { pos: number; inRange: boolean } {
const range = this.valuePosRange[index]
let inRange = true
if (pos < range[0]) {
pos = range[0]
inRange = false
} else if (pos > range[1]) {
pos = range[1]
inRange = false
}
return {
pos,
inRange,
}
}
/**
* Calculate the position of the slider by value
*
* @param {Value} val
* @returns {number}
*/
parseValue(val: Value): number {
if (this.data) {
val = this.data.indexOf(val)
} else if (typeof val === 'number' || typeof val === 'string') {
val = +val
if (val < this.min) {
this.emitError(ERROR_TYPE.MIN)
return 0
}
if (val > this.max) {
this.emitError(ERROR_TYPE.MAX)
return 0
}
if (typeof val !== 'number' || val !== val) {
this.emitError(ERROR_TYPE.VALUE)
return 0
}
val = new Decimal(val)
.minus(this.min)
.divide(this.interval)
.toNumber()
}
const pos = new Decimal(val).multiply(this.gap).toNumber()
return pos < 0 ? 0 : pos > 100 ? 100 : pos
}
/**
* Calculate the value by position
*
* @param {number} pos
* @returns {Value}
* @memberof Control
*/
parsePos(pos: number): Value {
const index = Math.round(pos / this.gap)
return this.getValueByIndex(index)
}
/**
* Determine if the location is active
*
* @param {number} pos
* @returns {boolean}
* @memberof Control
*/
isActiveByPos(pos: number): boolean {
return this.processArray.some(([start, end]) => pos >= start && pos <= end)
}
/**
* Get each value
*
* @returns {Value[]}
* @memberof Control
*/
getValues(): Value[] {
if (this.data) {
return this.data
} else {
const values: Value[] = []
for (let i = 0; i <= this.total; i++) {
values.push(
new Decimal(i)
.multiply(this.interval)
.plus(this.min)
.toNumber(),
)
}
return values
}
}
/**
* Calculate the distance of the range
* @param range number
*/
getRangeDir(range: number): number {
return range
? new Decimal(range)
.divide(
new Decimal(this.data ? this.data.length - 1 : this.max)
.minus(this.data ? 0 : this.min)
.toNumber(),
)
.multiply(100)
.toNumber()
: 100
}
private emitError(type: ERROR_TYPE) {
if (this.onError) {
this.onError(type, ERROR_MSG[type])
}
}
get processArray(): ProcessOption {
if (this.process) {
if (typeof this.process === 'function') {
return this.process(this.dotsPos)
} else if (this.dotsPos.length === 1) {
return [[0, this.dotsPos[0]]]
} else if (this.dotsPos.length > 1) {
return [[Math.min(...this.dotsPos), Math.max(...this.dotsPos)]]
}
}
return []
}
/**
* The total number of values
*
* @type {number}
* @memberof Control
*/
get total(): number {
let total = 0
if (this.data) {
total = this.data.length - 1
} else {
total = new Decimal(this.max)
.minus(this.min)
.divide(this.interval)
.toNumber()
}
if (total - Math.floor(total) !== 0) {
this.emitError(ERROR_TYPE.INTERVAL)
return 0
}
return total
}
// Distance between each value
get gap(): number {
return 100 / this.total
}
private cacheRangeDir: { [key: string]: number } = {}
// The minimum distance between the two sliders
get minRangeDir(): number {
if (this.cacheRangeDir[this.minRange]) {
return this.cacheRangeDir[this.minRange]
}
return (this.cacheRangeDir[this.minRange] = this.getRangeDir(this.minRange))
}
// Maximum distance between the two sliders
get maxRangeDir(): number {
if (this.cacheRangeDir[this.maxRange]) return this.cacheRangeDir[this.maxRange]
return (this.cacheRangeDir[this.maxRange] = this.getRangeDir(this.maxRange))
}
private getDotRange(index: number, key: 'min' | 'max', defaultValue: number): number {
if (!this.dotOptions) {
return defaultValue
}
const option = Array.isArray(this.dotOptions) ? this.dotOptions[index] : this.dotOptions
return option && option[key] !== void 0 ? this.parseValue(option[key] as Value) : defaultValue
}
/**
* Sliding range of each slider
*
* @type {Array<[number, number]>}
* @memberof Control
*/
get valuePosRange(): Array<[number, number]> {
const dotsPos = this.dotsPos
const valuePosRange: Array<[number, number]> = []
dotsPos.forEach((pos, i) => {
valuePosRange.push([
Math.max(
this.minRange ? this.minRangeDir * i : 0,
!this.enableCross ? dotsPos[i - 1] || 0 : 0,
this.getDotRange(i, 'min', 0),
),
Math.min(
this.minRange ? 100 - this.minRangeDir * (dotsPos.length - 1 - i) : 100,
!this.enableCross ? dotsPos[i + 1] || 100 : 100,
this.getDotRange(i, 'max', 100),
),
])
})
return valuePosRange
}
get dotsIndex(): number[] {
return this.dotsValue.map(val => this.getIndexByValue(val))
}
}