diff --git a/packages/rath-client/src/dev/autoStat.ts b/packages/rath-client/src/dev/autoStat.ts new file mode 100644 index 00000000..4ce10310 --- /dev/null +++ b/packages/rath-client/src/dev/autoStat.ts @@ -0,0 +1,671 @@ +import { IFieldMeta, IRow } from "../interfaces"; +import { binMap, BIN_SIZE, entropy, generalMic, mic, pureGeneralMic, rangeNormilize } from "@kanaries/loa"; + +interface ILoaView { + fields: (IFieldMeta | '*')[]; + locked?: boolean; +} + +function getCausers (fields: IFieldMeta[], relationMatrix: number[][], baseIndex: number): [number, number][] { + let causers: [number, number][] = fields.map((f, fi) => [relationMatrix[fi][baseIndex], fi]); + causers.sort((a, b) => b[0] - a[0]) + return causers; +} + +function getEffects (fields: IFieldMeta[], relationMatrix: number[][], baseIndex: number): [number, number][] { + let effects: [number, number][] = fields.map((f, fi) => [relationMatrix[baseIndex][fi], fi]); + effects.sort((a, b) => b[0] - a[0]) + return effects; +} + +function getSubMatrix (mat: T[][], indices: number[]): T[][] { + const size = indices.length; + const subMat: T[][] = new Array(size).fill(0).map(() => new Array(size).fill(0)); + for (let i = 0; i < size; i++) { + for (let j = 0; j < size; j++) { + subMat[i][j] = mat[indices[i]][indices[j]] + } + } + return subMat; +} + +function mergeCauserEffects (causers: [number, number][], effects: [number, number][]): [number, number][] { + const ans: [number, number][] = [] + for (let i = 0; i < causers.length; i++) { + ans.push([Math.max(causers[i][0], effects[i][0]), causers[i][1]]) + } + return ans; +} + +function recInternalRelation (relationMatrix: number[][], relationMarkMatrix: boolean[][], baseIndex: number): { + from: number; + to: number; + score: number; +} | null { + const causers: [number, number][] = []; + const effects: [number, number][] = []; + + for (let i = 0; i < relationMatrix.length; i++) { + if (i !== baseIndex) { + causers.push([relationMatrix[i][baseIndex], i]) + effects.push([relationMatrix[baseIndex][i], i]) + } + } + let relations = mergeCauserEffects(causers, effects); + relations.sort((a, b) => b[0] - a[0]) + + for (let i = 0; i < relations.length; i++) { + if (relationMarkMatrix[relations[i][1]][baseIndex] || relationMarkMatrix[baseIndex][relations[i][1]]) continue; + return { + from: baseIndex, + to: relations[i][1], + score: relations[i][0] + } + } + + return null; + +} + +function uniqueFieldMeta (fields: IFieldMeta[]) { + const keySet = new Set(fields.map(f => f.fid)); + const keyMap: Map = new Map(); + for (let f of fields) { + keyMap.set(f.fid, true) + } + const ans: IFieldMeta[] = []; + for (let f of fields) { + if (keyMap.get(f.fid)) { + keyMap.set(f.fid, false) + ans.push(f) + } + } + return ans +} + +function selectRelation2Target(props: { + causers: [number, number][]; + effects: [number, number][]; + wildCardNum: number; + baseIndex: number; + markMat: boolean[][] +}): { + ansIndices: number[]; + leftWildCard: number +} { + const { causers, effects, wildCardNum, baseIndex, markMat } = props; + let wcn = wildCardNum; + const ansIndices: number[] = [] + let cIndex = 0; + let eIndex = 0; + while((cIndex < causers.length || eIndex < effects.length) && wcn > 0) { + if ((cIndex < causers.length && eIndex < effects.length && causers[cIndex][0] >= effects[eIndex][0]) || (cIndex < causers.length && eIndex >= effects.length)) { + if (!markMat[causers[cIndex][1]][baseIndex] && causers[cIndex][0] > 0.01) { + ansIndices.push(causers[cIndex][1]); + markMat[causers[cIndex][1]][baseIndex] = markMat[baseIndex][causers[cIndex][1]] = true; + wcn--; + } + cIndex++ + } else if ((cIndex < causers.length && eIndex < effects.length && causers[cIndex][0] < effects[eIndex][0]) || (cIndex >= causers.length || eIndex < effects.length)) { + if (!markMat[baseIndex][effects[eIndex][1]] && effects[eIndex][0] > 0.01) { + ansIndices.push(effects[eIndex][1]); + markMat[baseIndex][effects[eIndex][1]] = markMat[effects[eIndex][1]][baseIndex]= true; + wcn--; + } + eIndex++ + } + } + return { + ansIndices, + leftWildCard: wcn + } +} + +interface IRelation2Group { + relationMatrix: number[][]; + markMatrix: boolean[][]; + baseIndex: number; + groupIndices: number[]; + wildCardNum: number +} +function selectRelation2Group(props: IRelation2Group) { + const { relationMatrix, markMatrix, baseIndex, groupIndices, wildCardNum } = props; + let wcn = wildCardNum; + const vertices: [number, number][] = [] + const selectedVertices: [number, number][] = [] + for (let i = 0; i < groupIndices.length; i++) { + const score = Math.max(relationMatrix[baseIndex][groupIndices[i]], relationMatrix[groupIndices[i]][baseIndex]) + vertices.push([score, groupIndices[i]]) + } + vertices.sort((a, b) => b[0] - a[0]) + for (let i = 0; i < vertices.length; i++) { + if (vertices[i][0] < 0.01) break; + if ((markMatrix[vertices[i][1]][baseIndex] || markMatrix[baseIndex][vertices[i][1]]) && wcn > 0) { + selectedVertices.push(vertices[i]) + } + } + return { + selectedVertices, + wcn + } +} + +function uniqueArrValue (arr: number[]): number[] { + const set = new Set(arr); + return [...set.values()] +} + +function findStrongestEdge2GroupNotMarked(mat: number[][], markMat: boolean[][], indices: number[]): { + score: number; + from: number; + to: number; +} { + let ans = { + score: -1, + from: -1, + to: -1 + } + for (let i = 0; i < indices.length; i++) { + const baseIndex = indices[i]; + for (let j = 0; j < mat.length; j++) { + if (j === baseIndex || markMat[baseIndex][j] || markMat[j][baseIndex]) continue; + if (mat[baseIndex][j] > ans.score || mat[j][baseIndex] > ans.score) { + ans.score = Math.max(mat[baseIndex][j], mat[j][baseIndex]) + ans.from = baseIndex; + ans.to = j; + } + } + } + return ans; +} +interface IExtendSpecGroup { + relationMatrix: number[][]; + markMatrix: boolean[][]; + groupIndices: number[]; + wildCardNum: number +} +function extendSpecGroup(props: IExtendSpecGroup) { + const { relationMatrix, markMatrix, groupIndices, wildCardNum } = props; + let wcn = wildCardNum; + const vertexIndices: number[] = [] + const edges: [number, number][] = [] + let dynamicGroupIndices: number[] = [...groupIndices]; + while(wcn > 1) { + const sEdge = findStrongestEdge2GroupNotMarked(relationMatrix, markMatrix, dynamicGroupIndices) + if (sEdge.to >= 0) { + console.log(sEdge) + wcn--; + vertexIndices.push(sEdge.from, sEdge.to); + dynamicGroupIndices.push(sEdge.to) + edges.push([sEdge.from, sEdge.to]) + } else { + break; + } + } + return { + vertexIndices: uniqueArrValue(vertexIndices), + edges, + leftWildCardNum: wcn + } +} + +function markUsedRelation (markMatrix: boolean[][], edges: [number, number][]) { + for (let edge of edges) { + markMatrix[edge[0]][edge[1]] = markMatrix[edge[1]][edge[0]] = true + } +} + +// idea list +// 1. 优先locked变量直接的关系 +// 2. extend measure , extend dimension, extend filter, reduce(general) +export function autoSet(dataSource: IRow[], fields: IFieldMeta[], views: ILoaView[], linkGraph?: number[][]) { + // 1. check status of placeholder + // 2. 确认确定的信息集合,和不确定的集合。根据确定的集合,推荐不确定的集合。 + // 3. 推荐逻辑, + + // 如果有主view + console.log('props', views) + const lockedFieldSet: IFieldMeta[] = []; + const lockedFieldWeightsMap: Map = new Map(); + const fieldIndexMap: Map = new Map(); + for (let i = 0; i < fields.length; i++) { + fieldIndexMap.set(fields[i].fid, i) + } + for (let view of views) { + for (let f of view.fields) { + if (f !== '*') { + lockedFieldSet.push(f); + lockedFieldWeightsMap.set(f.fid, (lockedFieldWeightsMap.get(f.fid) || 0) + 1) + } + } + } + const lockedFieldIndices = lockedFieldSet.map(f => fieldIndexMap.get(f.fid)!); + const lockedFieldWeights = [...lockedFieldWeightsMap.entries()].sort((a, b) => b[1] - a[1]); + let fieldIndex = 0; + const relationMatrix: number[][] = getFieldRelationMatrix(dataSource, fields); + const relationMarkMatrix: boolean[][] = new Array(fields.length).fill(false).map(() => new Array(fields.length).fill(false)); + for (let view of views) { + const fieldIndices = (view.fields.filter(f => f !== '*') as IFieldMeta[]).map(f => fieldIndexMap.get(f.fid)!); + for (let i = 0; i < fieldIndices.length; i++) { + for (let j = 0; j < fieldIndices.length; j++) { + if (fieldIndices[i] !== fieldIndices[j]) { + relationMarkMatrix[fieldIndices[i]][fieldIndices[j]] = relationMarkMatrix[fieldIndices[j]][fieldIndices[i]] = true + } + } + } + } + const ansViews: ILoaView[] = []; + for (let view of views) { + const viewFields = view.fields; + let ansFields: IFieldMeta[] = view.fields.filter(f => f !== '*') as IFieldMeta[]; + if (!view.locked && viewFields.length > ansFields.length) { + const placeholders = viewFields.filter(f => f === '*'); + if (placeholders.length === viewFields.length) { + let remainPlaceholderNum = placeholders.length; + fieldIndex = 0 + // const groupIndices = lockedFieldIndices + const { + vertexIndices, + leftWildCardNum, + edges + } = extendSpecGroup({ + relationMatrix, + markMatrix: relationMarkMatrix, + groupIndices: lockedFieldIndices, + wildCardNum: remainPlaceholderNum + }) + remainPlaceholderNum = leftWildCardNum; + ansFields.push(...vertexIndices.map(ind => fields[ind])) + markUsedRelation(relationMarkMatrix, edges); + // while(remainPlaceholderNum > 0 && fieldIndex < lockedFieldWeights.length) { + // const targetFieldKey = lockedFieldWeights[fieldIndex][0]; + // const targetFieldIndexInMatrix = fieldIndexMap.get(targetFieldKey)!; + // const causers = getCausers(fields, relationMatrix, targetFieldIndexInMatrix); + // const effects = getEffects(fields, relationMatrix, targetFieldIndexInMatrix); + // // console.log(fields[targetFieldIndexInMatrix].name, causers.map(c => `${fields[c[1]].name},${c[0]}`)) + // // console.log(fields[targetFieldIndexInMatrix].name, effects.map(c => `${fields[c[1]].name},${c[0]}`)) + // let cIndex = 0; + // let eIndex = 0; + // while((cIndex < causers.length || eIndex < effects.length) && remainPlaceholderNum > 0) { + // if ((cIndex < causers.length && eIndex < effects.length && causers[cIndex][0] >= effects[eIndex][0]) || (cIndex < causers.length && eIndex >= effects.length)) { + // if (!relationMarkMatrix[causers[cIndex][1]][targetFieldIndexInMatrix] && causers[cIndex][0] > 0.01) { + // ansFields.push(fields[causers[cIndex][1]]); + // relationMarkMatrix[causers[cIndex][1]][targetFieldIndexInMatrix] = relationMarkMatrix[targetFieldIndexInMatrix][causers[cIndex][1]] = true; + // remainPlaceholderNum--; + // } + // cIndex++ + // } else if ((cIndex < causers.length && eIndex < effects.length && causers[cIndex][0] < effects[eIndex][0]) || (cIndex >= causers.length || eIndex < effects.length)) { + // if (!relationMarkMatrix[targetFieldIndexInMatrix][effects[eIndex][1]] && effects[eIndex][0] > 0.01) { + // ansFields.push(fields[effects[eIndex][1]]); + // relationMarkMatrix[targetFieldIndexInMatrix][effects[eIndex][1]] = relationMarkMatrix[effects[eIndex][1]][targetFieldIndexInMatrix]= true; + // remainPlaceholderNum--; + // } + // eIndex++ + // } + // } + + // if (causers.filter(ca => ca[0] > 0).every(ca => relationMarkMatrix[ca[1]][targetFieldIndexInMatrix])) { + // if (effects.filter(ef => ef[0] > 0).every(ef => relationMarkMatrix[targetFieldIndexInMatrix][ef[1]])) { + // fieldIndex++; + // } + // } + // } + for (let i = 0; i < relationMarkMatrix.length; i++) { + relationMarkMatrix[i][i] = false; + } + } else { + // cases when placeholders.length !== viewFields.length + // 动态调整,以视图内的非placeholder的点为最高优先级。 + let localLockedFieldWeights = lockedFieldWeights.filter(loc => viewFields.find(f => f !== '*' && f.fid === loc[0])); + // TODO:优先看当前的wildcard内部的匹配关系,如果没有已有的关系,则添加。 + let remainPlaceholderNum = placeholders.length; + fieldIndex = 0 + for (let i = 0; i < relationMarkMatrix.length; i++) { + relationMarkMatrix[i][i] = true; + } + const specifiedFields = viewFields.filter(f => f !== '*') as IFieldMeta[]; + const sfIndices: number[] = lockedFieldSet.map(f => fieldIndexMap.get(f.fid)!); + const localRelationMatrix = getSubMatrix(relationMatrix, sfIndices); + // console.log('in local', localRelationMatrix, lockedFieldSet.map(l => l.name || l.fid)) + for (let i = 0; i < specifiedFields.length; i++) { + // if (specifiedFields[i].fid === 'Year') debugger; + const localRelationMarkMatrix = getSubMatrix(relationMarkMatrix, sfIndices); + + const targetIndex = lockedFieldSet.findIndex(f => f.fid === specifiedFields[i].fid) + const rec = recInternalRelation(localRelationMatrix, localRelationMarkMatrix, targetIndex) + if (rec !== null) { + if (ansFields.find(f => f.fid === fields[sfIndices[rec.to]].fid)) { + continue; + } + localRelationMarkMatrix[rec.from][rec.to] = localRelationMarkMatrix[rec.to][rec.from] = true; + relationMarkMatrix[sfIndices[rec.from]][sfIndices[rec.to]] = relationMarkMatrix[sfIndices[rec.to]][sfIndices[rec.from]] = true + remainPlaceholderNum--; + ansFields.push(fields[sfIndices[rec.to]]) + } + } + // console.log('end local', ansFields.length, ansFields.map(f => f.fid || f.name)) + // debugger + const { + vertexIndices, + leftWildCardNum, + edges + } = extendSpecGroup({ + relationMatrix, + markMatrix: relationMarkMatrix, + groupIndices: lockedFieldIndices, + wildCardNum: remainPlaceholderNum + 1 + }) + remainPlaceholderNum = leftWildCardNum; + ansFields.push(...vertexIndices.map(ind => fields[ind])) + markUsedRelation(relationMarkMatrix, edges); + // while(remainPlaceholderNum > 0 && fieldIndex < localLockedFieldWeights.length) { + // const targetFieldKey = localLockedFieldWeights[fieldIndex][0]; + // const targetFieldIndexInMatrix = fieldIndexMap.get(targetFieldKey)!; + // const causers = getCausers(fields, relationMatrix, targetFieldIndexInMatrix); + // const effects = getEffects(fields, relationMatrix, targetFieldIndexInMatrix); + // if (!ansFields.find(f => f.analyticType === 'measure')) { + // for (let i = 0; i < fields.length; i++) { + // if (fields[causers[i][1]].analyticType === 'measure') { + // causers[i][0]++; + // } + // } + // for (let i = 0; i < fields.length; i++) { + // if (fields[effects[i][1]].analyticType === 'measure') { + // effects[i][0]++; + // } + // } + // console.log({ + // causers, + // effects + // }) + // causers.sort((a, b) => b[0] - a[0]) + // effects.sort((a, b) => b[0] - a[0]) + // } + // // console.log(fields[targetFieldIndexInMatrix].name, causers.map(c => `${fields[c[1]].name},${c[0]}`)) + // // console.log(fields[targetFieldIndexInMatrix].name, effects.map(c => `${fields[c[1]].name},${c[0]}`)) + // let cIndex = 0; + // let eIndex = 0; + // while((cIndex < causers.length || eIndex < effects.length) && remainPlaceholderNum > 0) { + // if ((cIndex < causers.length && eIndex < effects.length && causers[cIndex][0] >= effects[eIndex][0]) || (cIndex < causers.length && eIndex >= effects.length)) { + // if (!relationMarkMatrix[causers[cIndex][1]][targetFieldIndexInMatrix] && causers[cIndex][0] > 0.01) { + // ansFields.push(fields[causers[cIndex][1]]); + // relationMarkMatrix[causers[cIndex][1]][targetFieldIndexInMatrix] = relationMarkMatrix[targetFieldIndexInMatrix][causers[cIndex][1]]= true; + // remainPlaceholderNum--; + // } + // cIndex++ + // } else if ((cIndex < causers.length && eIndex < effects.length && causers[cIndex][0] < effects[eIndex][0]) || (cIndex >= causers.length || eIndex < effects.length)) { + // if (!relationMarkMatrix[targetFieldIndexInMatrix][effects[eIndex][1]] && effects[eIndex][0] > 0.01) { + // ansFields.push(fields[effects[eIndex][1]]); + // relationMarkMatrix[targetFieldIndexInMatrix][effects[eIndex][1]] = relationMarkMatrix[effects[eIndex][1]][targetFieldIndexInMatrix] = true; + // remainPlaceholderNum--; + // } + // eIndex++ + // } + // } + + // if (causers.filter(ca => ca[0] > 0).every(ca => relationMarkMatrix[ca[1]][targetFieldIndexInMatrix])) { + // if (effects.filter(ef => ef[0] > 0).every(ef => relationMarkMatrix[targetFieldIndexInMatrix][ef[1]])) { + // fieldIndex++; + // } + // } + // } + for (let i = 0; i < relationMarkMatrix.length; i++) { + relationMarkMatrix[i][i] = false; + } + } + } else { + ansFields = view.fields.filter(f => f !== '*') as IFieldMeta[]; + } + console.log('ans fields', ansFields, view.fields, ansFields.map(af => ansFields.map(af2 => relationMatrix[fieldIndexMap.get(af.fid)!][fieldIndexMap.get(af2.fid)!]))) + ansViews.push({ + ...view, + fields: uniqueFieldMeta(ansFields) + }) + } + return ansViews +} + +function getFieldRelationMatrix (dataSource: IRow[], fields: IFieldMeta[]) { + const size = fields.length; + let relationMatrix: number[][] = new Array(size).fill(0).map(() => new Array(size).fill(0)); + for (let i = 0; i < fields.length; i++) { + for (let j = 0; j < fields.length; j++) { + if (i === j) { + relationMatrix[i][j] = 1; + continue; + } + const X = dataSource.map(r => r[fields[i].fid]); + const Y = dataSource.map(r => r[fields[j].fid]); + if (fields[i].semanticType === 'quantitative' && fields[j].semanticType === 'quantitative') { + relationMatrix[i][j] = mic(X, Y); + } + if (fields[i].semanticType !== 'quantitative' && fields[j].semanticType === 'quantitative') { + if (fields[i].semanticType === 'temporal')relationMatrix[i][j] = pureGeneralMic(X, Y) + else relationMatrix[i][j] = generalMic(X, Y) + } + if (fields[i].semanticType === 'quantitative' && fields[j].semanticType !== 'quantitative') { + if (fields[j].semanticType === 'temporal') relationMatrix[i][j] = inverseGeneralMic(X, Y, getTemporalFreqRange) + else relationMatrix[i][j] = inverseGeneralMic(X, Y) + } + if (fields[i].semanticType !== 'quantitative' && fields[j].semanticType !== 'quantitative') { + if (fields[j].semanticType === 'temporal') relationMatrix[i][j] = nnMic(X, Y, getTemporalFreqRange) + // 这里如果要用hack的temporal解法的话,需要用purennmic来做T-T类型。但是我们目前并不想提升T-T类型。不如等到之后时间系统改造完用正规的方法搞。 + else relationMatrix[i][j] = nnMic(X, Y) + } + } + } + return relationMatrix +} + +const SPLITOR = '_l_' + +function getFreqMap (values: any[]): Map { + const counter: Map = new Map(); + for (let val of values) { + if (!counter.has(val)) { + counter.set(val, 0) + } + counter.set(val, counter.get(val)! + 1) + } + return counter +} + +export function liteGroupBy(values: any[], by: any[]): Map { + const groups: Map = new Map(); + for (let i = 0; i < values.length; i++) { + let g = groups.get(by[i]) || []; + g.push(values[i]) + groups.set(by[i], g) + } + return groups; +} + +/** + * 返回一个BIN_SIZE大小的元素数组,包含了freq前16 + */ +export function getFreqRange (values: any[]): any[] { + const FM = getFreqMap(values); + const sortedFM = [...FM.entries()].sort((a, b) => b[1] - a[1]) + return sortedFM.slice(0, BIN_SIZE); +} + +/** + * 返回一个BIN_SIZE大小的元素数组,包含了freq前16 + */ + export function getTemporalFreqRange (values: any[]): any[] { + const FM = getFreqMap(values); + const sortedFM = [...FM.entries()].sort((a, b) => b[1] - a[1]) + return sortedFM +} + +export function binGroupByShareFreqRange (Y: any[], range: any[]): number[] { + const fl: number[] = new Array(range.length).fill(0); + const rangeIndices: Map = new Map(); + // for (let val of range) { + for (let i = 0; i < range.length; i++) { + rangeIndices.set(range[i], i); + } + for (let val of Y) { + if (rangeIndices.has(val)) { + fl[rangeIndices.get(val)!]++; + } else { + fl[fl.length - 1]++; + } + } + return fl; +} + +// function addVec (vec1: number[], vec2: number[]) { +// let size = Math.min(vec1.length, vec2.length); +// let ans = new Array(size).fill(0); +// for (let i = 0; i < size; i++) { +// ans[i] = vec1[i] + vec2[i] +// } +// return ans; +// } + +function vecAdd (mutVec: number[], inc: number[]) { + const size = Math.min(mutVec.length, inc.length); + for (let i = 0; i < size; i++) { + mutVec[i] += inc[i]; + } +} + +function inverseGeneralMic (X: number[], Y: any[], FR: (values: any[]) => any[] | undefined = getFreqRange) { + const binTags = binMap(X); + // const FM = getFreqMap(Y); + const globalRange = getFreqRange(Y) + // console.log('g range', globalRange) + const groups = liteGroupBy(Y, binTags) + // 这里groups.size = BIN_SZIE + let condH = 0; + let globalFl = new Array(globalRange.length).fill(0); + for (let [sgKey, subGroup] of groups) { + const p = subGroup.length / Y.length; + const subFl = binGroupByShareFreqRange(subGroup, globalRange); + const subEnt = entropy(rangeNormilize(subFl.filter(v => v > 0))) + condH += p * subEnt; + vecAdd(globalFl, subFl); + } + const H = entropy(rangeNormilize(globalFl.filter(v => v > 0))) + return (H - condH) / Math.log2(Math.min(BIN_SIZE, globalRange.length)) +} + +function nnMic (X: any[], Y: any[], FR: (values: any[]) => any[] | undefined = getFreqRange) { + // const FM = getFreqMap(Y); + // const globalRange = [...FM.keys()]; + const globalRange = getFreqRange(Y) + + const groups = liteGroupBy(Y, X) + + const sortedGroup = [...groups.entries()].sort((a, b) => b[1].length - a[1].length) + // for (let group of sortedGroup) + let usedGroupNum = Math.min(sortedGroup.length, BIN_SIZE - 1) + let i = 0; + let condH = 0; + let globalFl = new Array(globalRange.length).fill(0); + for (i = 0; i < usedGroupNum; i++) { + const p = sortedGroup[i][1].length / Y.length; + const subFl = binGroupByShareFreqRange(sortedGroup[i][1], globalRange) + const subEnt = entropy(rangeNormilize(subFl.filter(v => v > 0))) + condH += subEnt * p; + vecAdd(globalFl, subFl); + } + + if (sortedGroup.length > usedGroupNum) { + let noiseFl = new Array(BIN_SIZE).fill(0); + let noiseP = 0; + for (; i < sortedGroup.length; i++) { + const subFl = binGroupByShareFreqRange(sortedGroup[i][1], globalRange) + noiseP += sortedGroup[i][1].length; + vecAdd(noiseFl, subFl) + } + noiseP /= Y.length; + const noiseEnt = entropy(rangeNormilize(noiseFl.filter(v => v > 0))) + condH += noiseP * noiseEnt + vecAdd(globalFl, noiseFl) + } + const H = entropy(rangeNormilize(globalFl.filter(v => v > 0))) + return (H - condH) / Math.log2(Math.min(BIN_SIZE, globalRange.length)) + +} +/** + * no noise + * @param X + * @param Y + * @param FR + * @returns + */ +function purennMic (X: any[], Y: any[], FR: (values: any[]) => any[] | undefined = getFreqRange) { + // const FM = getFreqMap(Y); + // const globalRange = [...FM.keys()]; + const globalRange = getFreqRange(Y) + + const groups = liteGroupBy(Y, X) + + const sortedGroup = [...groups.entries()].sort((a, b) => b[1].length - a[1].length) + // for (let group of sortedGroup) + let usedGroupNum = sortedGroup.length + let i = 0; + let condH = 0; + let globalFl = new Array(globalRange.length).fill(0); + for (i = 0; i < usedGroupNum; i++) { + const p = sortedGroup[i][1].length / Y.length; + const subFl = binGroupByShareFreqRange(sortedGroup[i][1], globalRange) + const subEnt = entropy(rangeNormilize(subFl.filter(v => v > 0))) + condH += subEnt * p; + vecAdd(globalFl, subFl); + } + + // if (sortedGroup.length > usedGroupNum) { + // let noiseFl = new Array(BIN_SIZE).fill(0); + // let noiseP = 0; + // for (; i < sortedGroup.length; i++) { + // const subFl = binGroupByShareFreqRange(sortedGroup[i][1], globalRange) + // noiseP += sortedGroup[i][1].length; + // vecAdd(noiseFl, subFl) + // } + // noiseP /= Y.length; + // const noiseEnt = entropy(rangeNormilize(noiseFl.filter(v => v > 0))) + // condH += noiseP * noiseEnt + // vecAdd(globalFl, noiseFl) + // } + const H = entropy(rangeNormilize(globalFl.filter(v => v > 0))) + return (H - condH) / Math.log2(Math.min(BIN_SIZE, globalRange.length)) + +} + +// function recommendBasedOnTargets (originWildCardNum: number) { +// let wildCardNum = originWildCardNum; +// const ansFields: IFieldMeta[] = []; +// while(wildCardNum > 0 && fieldIndex < lockedFieldWeights.length) { +// const targetFieldKey = lockedFieldWeights[fieldIndex][0]; +// const targetFieldIndexInMatrix = fieldIndexMap.get(targetFieldKey)!; +// // ansFields.push(fields[targetFieldIndexInMatrix]) +// // wildCardNum--; +// let causers: [number, number][] = fields.map((f, fi) => [relationMatrix[fi][targetFieldIndexInMatrix], fi]); +// let effects: [number, number][] = fields.map((f, fi) => [relationMatrix[targetFieldIndexInMatrix][fi], fi]); +// causers.sort((a, b) => b[0] - a[0]) +// effects.sort((a, b) => b[0] - a[0]) +// console.log(fields[targetFieldIndexInMatrix].name, causers.map(c => `${fields[c[1]].name},${c[0]}`)) +// console.log(fields[targetFieldIndexInMatrix].name, effects.map(c => `${fields[c[1]].name},${c[0]}`)) +// let cIndex = 0; +// let eIndex = 0; +// while((cIndex < causers.length || eIndex < effects.length) && wildCardNum > 0) { +// if (causers[cIndex][0] >= effects[eIndex][0]) { +// if (!relationMarkMatrix[causers[cIndex][1]][targetFieldIndexInMatrix] && causers[cIndex][0] > 0) { +// ansFields.push(fields[causers[cIndex][1]]); +// relationMarkMatrix[causers[cIndex][1]][targetFieldIndexInMatrix] = true; +// wildCardNum--; +// } +// cIndex++ +// } else { +// if (!relationMarkMatrix[targetFieldIndexInMatrix][effects[eIndex][1]] && effects[eIndex][0] > 0.6) { +// ansFields.push(fields[effects[eIndex][1]]); +// relationMarkMatrix[targetFieldIndexInMatrix][effects[eIndex][1]] = true; +// wildCardNum--; +// } +// eIndex++ +// } +// } +// } \ No newline at end of file diff --git a/packages/rath-client/src/pages/progressiveDashboard/index.tsx b/packages/rath-client/src/pages/progressiveDashboard/index.tsx new file mode 100644 index 00000000..3664956b --- /dev/null +++ b/packages/rath-client/src/pages/progressiveDashboard/index.tsx @@ -0,0 +1,345 @@ +// import { autoSet } from '@kanaries/loa'; +import { autoSet } from '../../dev/autoStat'; +import produce from 'immer'; +import { toJS } from 'mobx'; +import { observer } from 'mobx-react-lite'; +import { ActionButton, DefaultButton, Dropdown, IconButton, IDropdownOption, Stack, Toggle } from 'office-ui-fabric-react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import ReactVega from '../../components/react-vega'; +import { IFieldMeta } from '../../interfaces'; +import { distVis } from '../../queries/distVis'; +import { useGlobalStore } from '../../store'; +import styled from 'styled-components'; + +const Segment = styled.div` + display: flex; + flex-wrap: wrap; +`; +const Cont = styled.div` + .vis-container { + max-height: 360px; + overflow-y: auto; + } + .operation-container { + border-top: 1px solid #eee; + padding-top: 1em; + max-height: 350px; + overflow-y: auto; + /* :hover { + height: auto; + } */ + } + padding: 0.5em; + margin: 0.25em; + background-color: #fff; + flex-grow: 1; +`; + +// const init_list: { locked: boolean; fields: (IFieldMeta | '*')[] }[] = [ +// { +// locked: true, +// fields: [toJS(fieldMetas.filter((f) => f.analyticType === 'measure')[0])], +// }, +// // { +// // locked: , +// // fields: [] +// // // fields: [fieldMetas.filter(f => f.semanticType === 'temporal')[0], fieldMetas.filter(f => f.analyticType === 'measure')[1]] +// // }, +// { +// locked: false, +// fields: ['*', '*'], +// }, +// { +// locked: false, +// fields: ['*', '*'], +// }, +// { +// locked: false, +// fields: ['*', '*'], +// }, +// { +// locked: false, +// fields: ['*', '*'], +// }, +// { +// locked: false, +// fields: ['*', '*'], +// }, +// { +// locked: false, +// fields: ['*', '*'], +// }, +// { +// locked: false, +// fields: ['*', '*'], +// }, +// ] + +const ProgressiveDashboard: React.FC = (props) => { + // const ; + const { dataSourceStore } = useGlobalStore(); + const { cleanedData, fieldMetas } = dataSourceStore; + const [originSpecList, setOriginSpecList] = useState<{ locked: boolean; fields: (IFieldMeta | '*')[] }[]>([ + { + locked: true, + fields: [toJS(fieldMetas.filter((f) => f.analyticType === 'measure')[0])], + }, + // { + // locked: , + // fields: [] + // // fields: [fieldMetas.filter(f => f.semanticType === 'temporal')[0], fieldMetas.filter(f => f.analyticType === 'measure')[1]] + // }, + { + locked: false, + fields: ['*', '*'], + }, + { + locked: false, + fields: ['*', '*'], + }, + { + locked: false, + fields: ['*', '*'], + }, + { + locked: false, + fields: ['*', '*'], + }, + { + locked: false, + fields: ['*', '*'], + }, + { + locked: false, + fields: ['*', '*'], + }, + { + locked: false, + fields: ['*', '*'], + }, + ]); + + const [visSettingList, setVisSettingList] = useState([]); + + const updateFieldsInView = useCallback( + (viewIndex: number, fieldIndex: number, fieldKey: string) => { + const val: IFieldMeta | '*' = (fieldKey === '*' ? '*' : fieldMetas.find((f) => f.fid === fieldKey))!; + setOriginSpecList((sl) => { + const nextSl = produce(sl, (draft) => { + draft[viewIndex].fields[fieldIndex] = val; + }); + return nextSl; + }); + }, + [fieldMetas] + ); + + const addFields2View = useCallback( + (viewIndex: number, fieldKey: string) => { + const targetView = originSpecList[viewIndex]; + const val: IFieldMeta | '*' = (fieldKey === '*' ? '*' : fieldMetas.find((f) => f.fid === fieldKey))!; + if (val !== '*' && targetView.fields.find((f) => (f as IFieldMeta).fid === (val as IFieldMeta).fid)) { + return; + } + setOriginSpecList((sl) => { + const nextSl = produce(sl, (draft) => { + draft[viewIndex].fields.push(val); + }); + return nextSl; + }); + }, + [originSpecList, fieldMetas] + ); + + const removeFieldsFromView = useCallback( + (viewIndex: number, fieldKey: string) => { + const targetView = originSpecList[viewIndex]; + const val: IFieldMeta | '*' = (fieldKey === '*' ? '*' : fieldMetas.find((f) => f.fid === fieldKey))!; + const targetFieldIndex = targetView.fields.findIndex( + (f) => f === val || (f as IFieldMeta).fid === (val as IFieldMeta).fid + ); + if (targetFieldIndex > -1) { + setOriginSpecList((sl) => { + const nextSl = produce(sl, (draft) => { + draft[viewIndex].fields.splice(targetFieldIndex, 1); + }); + return nextSl; + }); + } + }, + [originSpecList, fieldMetas] + ); + // recommendVisList里是确定性的推荐,不含wildcard + const recommendVisList = useMemo(() => { + if (cleanedData.length > 0 && fieldMetas.length > 0) { + console.log(cleanedData, toJS(fieldMetas)); + return autoSet(cleanedData, toJS(fieldMetas), originSpecList); + } + return []; + }, [cleanedData, fieldMetas, originSpecList]); + + const addView = useCallback(() => { + setOriginSpecList((sl) => [ + ...sl, + { + locked: false, + fields: ['*', '*'], + }, + ]); + }, []); + + const deleteView = useCallback((viewIndex: number) => { + setOriginSpecList((sl) => { + const nextSl = [...sl] + nextSl.splice(viewIndex, 1); + return nextSl + }); + }, []); + + const selectableFields = useMemo(() => { + return [ + { + key: '*', + text: '*', + }, + ].concat( + fieldMetas.map((fm) => { + return { + key: fm.fid, + text: fm.name || fm.fid, + }; + }) + ); + }, [fieldMetas]); + + const toggleViewLocked = useCallback((viewIndex) => { + setOriginSpecList((sl) => { + const nextSl = produce(sl, (draft) => { + draft[viewIndex].locked = !draft[viewIndex].locked; + draft[viewIndex].fields = recommendVisList[viewIndex].fields + const unLockIndex = draft.findIndex(v => v.locked === false) + if (unLockIndex > -1) { + const t = draft[unLockIndex]; + draft[unLockIndex] = draft[viewIndex] + draft[viewIndex] = t; + } + }); + return nextSl; + }); + }, [recommendVisList]); + + const toggleVisSetting = useCallback((viewIndex) => { + setVisSettingList((l) => { + const nl = [...l]; + nl[viewIndex] = !nl[viewIndex]; + return nl; + }); + }, []); + + useEffect(() => { + setVisSettingList(originSpecList.map(() => false)); + }, [originSpecList]); + + console.log({ + recommendVisList, + originSpecList + }) + + return ( + + {recommendVisList.map((vis, visIndex) => ( + + {/* { + + } */} + + { + toggleViewLocked(visIndex); + }} + /> + { + toggleVisSetting(visIndex); + }} + /> + { + deleteView(visIndex); + }} + /> + +
+ f !== '*') as IFieldMeta[], + imp: 0, + }, + })} + /> +
+ {visSettingList[visIndex] && ( +
+ + {originSpecList[visIndex].fields.map((f, fIndex) => { + const selectedKey = + originSpecList[visIndex].fields[fIndex] === '*' + ? '*' + : (originSpecList[visIndex].fields[fIndex] as IFieldMeta).fid; + const fieldSizeMatch = originSpecList[visIndex].fields.length === recommendVisList[visIndex].fields.length; + let selectedName: string | null = null;; + if (fieldSizeMatch) { + selectedName = ((recommendVisList[visIndex].fields[fIndex] as IFieldMeta).name || (recommendVisList[visIndex].fields[fIndex] as IFieldMeta).fid) + } + return ( + + + { + removeFieldsFromView(visIndex, selectedKey); + }} + /> + { + newItem && + updateFieldsInView(visIndex, fIndex, newItem.key + ''); + }} + /> + { + updateFieldsInView(visIndex, fIndex, (recommendVisList[visIndex].fields[fIndex] as IFieldMeta).fid) + }} /> + + + ); + })} + + { + addFields2View(visIndex, '*'); + }} + /> + + +
+ )} +
+ ))} + + + +
+ ); +}; + +export default observer(ProgressiveDashboard); diff --git a/packages/rath-client/src/queries/distVis.ts b/packages/rath-client/src/queries/distVis.ts index 669dd4da..916d63a3 100644 --- a/packages/rath-client/src/queries/distVis.ts +++ b/packages/rath-client/src/queries/distVis.ts @@ -2,8 +2,9 @@ * distVis 是分布式可视化的推荐,是比较新的模块,目前暂时用于dev模块,即voyager模式下的测试。 */ import { IPattern } from '@kanaries/loa'; -import { IFieldMeta, IResizeMode, IVegaSubset } from "../interfaces"; +import { IResizeMode, IVegaSubset } from "../interfaces"; import { encodingDecorate } from "./base/utils"; +import { autoMark, autoStat, encode, humanHabbit, VizEncoder } from './distribution/bot'; import { applyDefaultSort, applyInteractiveParams2DistViz, applySizeConfig2DistViz } from "./distribution/utils"; export const geomTypeMap: { [key: string]: any } = { interval: "boxplot", @@ -13,24 +14,6 @@ export const geomTypeMap: { [key: string]: any } = { density: "point" }; -const channels = { - quantitative: ['y' , 'x', 'size', 'color', 'opacity'], - ordinal: ['y', 'x', 'color', 'opacity', 'size', 'shape'], - nominal: ['y', 'x', 'color', 'row', 'column', 'opacity', 'size', 'shape'], - temporal: ['y', 'x', 'opacity', 'color', 'shape'] -} as const; -// const channels = { -// quantitative: ['y' , 'x', 'size', 'color', 'opacity'], -// ordinal: ['y', 'x', 'color', 'size', 'shape'], -// nominal: ['y', 'x', 'color', 'row', 'column', 'size', 'shape'], -// temporal: ['y', 'x', 'opacity', 'color', 'shape'] -// } as const; - -const highOrderChannels = { - dimension: ['row', 'column'], - measure: ['repeat'] -} as const; - interface BaseVisProps { // dataSource: DataSource; pattern: IPattern; @@ -40,340 +23,20 @@ interface BaseVisProps { height?: number; stepSize?: number; } -function humanHabbit (encoding: any) { - if (encoding.x && encoding.x.type !== 'temporal') { - if (encoding.y && encoding.y.type === 'temporal') { - const t = encoding.x; - encoding.x = encoding.y; - encoding.y = t; - } - } -} - -interface EncodeProps{ - fields: IFieldMeta[]; - usedChannels?: Set; - statFields?: IFieldMeta[]; - statEncodes?: IFieldEncode[] -} -function encode (props: EncodeProps) { - const { - fields, - usedChannels = new Set(), - statFields = [], - statEncodes = [] - } = props; - const orderFields = [...fields]; - let encoding: any = {} - let inHighOrderStatus: keyof typeof highOrderChannels | null = null; - let highOrderIndex: number = 0; - orderFields.sort((a, b) => b.features.entropy - a.features.entropy); - statFields.sort((a, b) => b.features.entropy - a.features.entropy); - const totalFields = [...statFields, ...orderFields].sort((a, b) => b.features.entropy - a.features.entropy); - // orderFields.unshift(...statFields); - for (let i = 0; i < totalFields.length; i++) { - const chs = channels[totalFields[i].semanticType]; - let encoded: boolean = false; - const statIndex = statFields.findIndex(f => f.fid === totalFields[i].fid) - const orderIndex = orderFields.findIndex(f => f.fid === totalFields[i].fid) - const isStatField = statIndex > -1; - if (isStatField) { - for (let j = 0; j < chs.length; j++) { - if (!usedChannels.has(chs[j])) { - encoding[chs[j]] = statEncodes[statIndex] - usedChannels.add(chs[j]) - // if (statFields[statIndex].semanticType === 'quantitative') { - // if (statFields[statIndex].features.entropy / Math.log2(16) < 0.2) { - // encoding[chs[j]].scale = { type: 'sqrt' } - // } - // } - encoded = true; - break; - } - } - // 发生可能很低 - // FIXME 多度量repeat设计 - if (!encoded) { - inHighOrderStatus = statFields[statIndex].analyticType; - if (inHighOrderStatus === 'dimension' && highOrderIndex < highOrderChannels[inHighOrderStatus].length) { - encoding[highOrderChannels[inHighOrderStatus][highOrderIndex]] = statEncodes[statIndex] - highOrderIndex++ - } - } - } else { - for (let j = 0; j < chs.length; j++) { - if (!usedChannels.has(chs[j])) { - encoding[chs[j]] = { - field: orderFields[orderIndex].fid, - type: orderFields[orderIndex].semanticType, - title: orderFields[orderIndex].name || orderFields[orderIndex].fid - } - usedChannels.add(chs[j]) - encoded = true; - break; - } - } - if (!encoded) { - inHighOrderStatus = orderFields[orderIndex].analyticType; - if (inHighOrderStatus === 'dimension' && highOrderIndex < highOrderChannels[inHighOrderStatus].length) { - encoding[highOrderChannels[inHighOrderStatus][highOrderIndex]] = { - field: orderFields[orderIndex].fid, - type: orderFields[orderIndex].semanticType - } - highOrderIndex++ - } - } - } - } - - // for (let i = 0; i < statFields.length; i++) { - // const chs = channels[statFields[i].semanticType]; - // let encoded: boolean = false; - // for (let j = 0; j < chs.length; j++) { - // if (!usedChannels.has(chs[j])) { - // encoding[chs[j]] = statEncodes[i] - // usedChannels.add(chs[j]) - // encoded = true; - // break; - // } - // } - // // 发生可能很低 - // if (!encoded) { - // inHighOrderStatus = statFields[i].analyticType; - // if (inHighOrderStatus === 'dimension' && highOrderIndex < highOrderChannels[inHighOrderStatus].length) { - // encoding[highOrderChannels[inHighOrderStatus][highOrderIndex]] = statEncodes[i] - // highOrderIndex++ - // } - // } - // } - // for (let i = 0; i < orderFields.length; i++) { - // const chs = channels[orderFields[i].semanticType]; - // let encoded: boolean = false; - // for (let j = 0; j < chs.length; j++) { - // if (!usedChannels.has(chs[j])) { - // encoding[chs[j]] = { - // field: orderFields[i].fid, - // type: orderFields[i].semanticType, - // title: orderFields[i].name || orderFields[i].fid - // } - // usedChannels.add(chs[j]) - // encoded = true; - // break; - // } - // } - // if (!encoded) { - // inHighOrderStatus = orderFields[i].analyticType; - // if (inHighOrderStatus === 'dimension' && highOrderIndex < highOrderChannels[inHighOrderStatus].length) { - // encoding[highOrderChannels[inHighOrderStatus][highOrderIndex]] = { - // field: orderFields[i].fid, - // type: orderFields[i].semanticType - // } - // highOrderIndex++ - // } - // } - // } - return encoding -} - -function isSetEqual (a1: any[], a2: any[]) { - const s1 = new Set(a1); - const s2 = new Set(a2); - if (s1.size !== s2.size) return false; - for (let ele of s1) { - if (!s2.has(ele)) return false; - } - return true; -} - -function autoMark (fields: IFieldMeta[], statFields: IFieldMeta[]= [], originFields: IFieldMeta[] = [], statEncodes: IFieldEncode[] = []) { - // const orderFields = [...fields]; - // const orderStatFields = [...statFields]; - // orderFields.sort((a, b) => b.features.entropy - a.features.entropy); - // orderStatFields.sort((a, b) => b.features.entropy - a.features.entropy); - const semanticFields = [...statFields, ...originFields].sort((a, b) => b.features.entropy - a.features.entropy).slice(0, 2); - const semantics = semanticFields.map(f => f.semanticType) - // if (fields.length === 1) { - // return 'bar' - // } - // FIXME: 时间序列多目标 - // if (statFields.length > 0) { - // // 仅对count生效。 - // return 'bar' - // } - if (statEncodes.find(f => f.aggregate === 'count')) { - return 'bar' - } - // if (fields.length === 1) { - // return 'bar' - // } - const cond_sinleTargets = fields.filter(f => f.analyticType === 'measure').length === 1; - - if (cond_sinleTargets) { - if (isSetEqual(semantics, ['nominal', 'nominal'])) { - return 'text' - } else if (isSetEqual(semantics, ['nominal', 'quantitative'])) { - const quanField = semanticFields.find(s => s.semanticType === 'quantitative')!; - const onlyNominalDimension = fields.filter(f => f.analyticType === 'dimension') - .filter(f => f.semanticType !== 'nominal').length === 0; - if (onlyNominalDimension) { - if (quanField.features.unique > 400) return 'boxplot'; - if (quanField.features.unique > 16) return 'tick'; - } - return 'bar' - } else if (isSetEqual(semantics, ['ordinal', 'quantitative'])) { - return 'line' - } else if (isSetEqual(semantics, ['nominal', 'ordinal'])) { - return 'point' - } else if (isSetEqual(semantics, ['nominal', 'temporal'])) { - return 'point' - } else if (isSetEqual(semantics, ['quantitative', 'quantitative'])) { - return 'area' - } else if (isSetEqual(semantics, ['temporal', 'quantitative'])) { - return 'line' - } - } else { - if (isSetEqual(semantics, ['nominal', 'nominal'])) { - return 'text' - } else if (isSetEqual(semantics, ['nominal', 'quantitative'])) { - return 'tick' - } else if (isSetEqual(semantics, ['ordinal', 'quantitative'])) { - return 'line' - } else if (isSetEqual(semantics, ['nominal', 'ordinal'])) { - return 'point' - } else if (isSetEqual(semantics, ['quantitative', 'quantitative'])) { - return 'circle' - } - } - return 'point' -} - -function markFixEncoding (markType: string, usedChannels: Set) { - if (markType === 'bar') { - usedChannels.add('size'); - usedChannels.add('shape'); - } -} - -// function autoAgg (props: {encoding: any; fields: IFieldMeta[]; markType: string; op?: string; statFields?: IFieldMeta[]}) { -// const { -// encoding, -// fields, -// markType, -// op = 'mean', -// statFields = [] -// } = props -// if (fields.length > 1) { -// if (markType === 'bar' || markType === 'line') { -// if (encoding.x && encoding.x.type === 'quantitative') { -// encoding.x.aggregate = op; -// if (encoding.x.title) { -// encoding.x.title = `${op}(${encoding.x.title})` -// } -// } -// if (encoding.y && encoding.y.type === 'quantitative') { -// encoding.y.aggregate = op; -// if (encoding.y.title) { -// encoding.y.title = `${op}[${encoding.y.title}]` -// } -// } -// } -// } -// } - -interface IFieldEncode { - field?: string; - title?: string; - type?: string; - aggregate?: string; - bin?: boolean; -} - -// FIXME: 统一aggregate逻辑。 -function autoStat (fields: IFieldMeta[]): { - statFields: IFieldMeta[]; - distFields: IFieldMeta[]; - statEncodes: IFieldEncode[]; -} { - const statFields: IFieldMeta[] = []; - const statEncodes: IFieldEncode[] = []; - const cond_singlefield = fields.length === 1; - const cond_nonquanmeasure = fields.filter(f => f.analyticType === 'measure').filter(f => f.semanticType === 'nominal' || f.semanticType === 'ordinal').length > 0; - if (cond_singlefield || cond_nonquanmeasure) { - statFields.push({ - fid: '__tmp_stat_id_unique', - semanticType: 'quantitative', - analyticType: 'measure', - geoRole: 'none', - features: { - entropy: Infinity, - maxEntropy: Infinity, - unique: 1000 - }, - distribution: [] - }) - statEncodes.push({ - aggregate: 'count' - }) - fields.filter(f => f.semanticType === 'quantitative').forEach(f => { - statFields.push({ ...f }) - statEncodes.push({ - field: f.fid, - title: f.name || f.fid, - type: f.semanticType, - bin: true - }) - }) - } else { - const targets = fields.filter(f => f.analyticType === 'measure'); - const onlyNominalDimension = fields.filter(f => f.analyticType === 'dimension') - .filter(f => f.semanticType !== 'nominal').length === 0; - // 单目标的场景 - if (targets.length === 1) { - // 连续型 度量做聚合,非连续型度量做分箱; - targets.forEach(f => { - if (onlyNominalDimension && f.features.unique > 16) { - statEncodes.push({ - field: f.fid, - type: f.semanticType, - title: `${f.name || f.fid}`, - }) - } else { - statEncodes.push({ - field: f.fid, - type: f.semanticType, - title: `mean(${f.name || f.fid})`, - aggregate: 'mean' - }) - } - statFields.push({ ...f }) - }) - fields.filter(f => f.analyticType === 'dimension' && f.semanticType === 'quantitative').forEach(f => { - statFields.push({ ...f }) - statEncodes.push({ - field: f.fid, - title: f.name || f.fid, - type: f.semanticType, - bin: true - }) - }) - } - } - const distFields = fields.filter(f => !statFields.find(sf => sf.fid === f.fid)); - return { statFields, distFields, statEncodes } -} export function distVis(props: BaseVisProps): IVegaSubset { const { pattern, resizeMode = IResizeMode.auto, width, height, interactive, stepSize } = props; const { fields } = pattern; - const usedChannels: Set = new Set(); const { statFields, distFields, statEncodes } = autoStat(fields); let markType = autoMark(fields, statFields, distFields, statEncodes) - markFixEncoding(markType, usedChannels) + const channelEncoder = new VizEncoder(markType); // if (filters && filters.length > 0) { // usedChannels.add('color') // } const enc = encode({ - fields: distFields, usedChannels, statFields, + fields: distFields, + channelEncoder, + statFields, statEncodes }) // if (filters && filters.length > 0) { diff --git a/packages/rath-client/src/queries/distribution/bot.ts b/packages/rath-client/src/queries/distribution/bot.ts new file mode 100644 index 00000000..20cec90f --- /dev/null +++ b/packages/rath-client/src/queries/distribution/bot.ts @@ -0,0 +1,302 @@ +import { ISemanticType } from "@kanaries/loa"; +import { IFieldMeta, IVegaSubset } from "../../interfaces"; +interface IFieldEncode { + field?: string; + title?: string; + type?: string; + aggregate?: string; + bin?: boolean; +} +function isSetEqual (a1: any[], a2: any[]) { + const s1 = new Set(a1); + const s2 = new Set(a2); + if (s1.size !== s2.size) return false; + for (let ele of s1) { + if (!s2.has(ele)) return false; + } + return true; +} +export function autoMark (fields: IFieldMeta[], statFields: IFieldMeta[]= [], originFields: IFieldMeta[] = [], statEncodes: IFieldEncode[] = []) { + // const orderFields = [...fields]; + // const orderStatFields = [...statFields]; + // orderFields.sort((a, b) => b.features.entropy - a.features.entropy); + // orderStatFields.sort((a, b) => b.features.entropy - a.features.entropy); + const semanticFields = [...statFields, ...originFields].sort((a, b) => b.features.entropy - a.features.entropy).slice(0, 2); + const semantics = semanticFields.map(f => f.semanticType) + // if (fields.length === 1) { + // return 'bar' + // } + // FIXME: 时间序列多目标 + // if (statFields.length > 0) { + // // 仅对count生效。 + // return 'bar' + // } + if (statEncodes.find(f => f.aggregate === 'count')) { + return 'bar' + } + // if (fields.length === 1) { + // return 'bar' + // } + const cond_sinleTargets = fields.filter(f => f.analyticType === 'measure').length === 1; + + if (cond_sinleTargets) { + if (isSetEqual(semantics, ['nominal', 'nominal'])) { + return 'text' + } else if (isSetEqual(semantics, ['nominal', 'quantitative'])) { + const quanField = semanticFields.find(s => s.semanticType === 'quantitative')!; + const onlyNominalDimension = fields.filter(f => f.analyticType === 'dimension') + .filter(f => f.semanticType !== 'nominal').length === 0; + if (onlyNominalDimension) { + if (quanField.features.unique > 400) return 'boxplot'; + // if (quanField.features.unique > 16) return 'tick'; + } + return 'bar' + } else if (isSetEqual(semantics, ['ordinal', 'quantitative'])) { + return 'bar' + } else if (isSetEqual(semantics, ['nominal', 'ordinal'])) { + return 'point' + } else if (isSetEqual(semantics, ['nominal', 'temporal'])) { + return 'point' + } else if (isSetEqual(semantics, ['quantitative', 'quantitative'])) { + return 'area' + } else if (isSetEqual(semantics, ['temporal', 'quantitative'])) { + const temporalField = semanticFields.find(s => s.semanticType === 'temporal')!; + if (temporalField.features.unique > 16) return 'line' + return 'bar' + } + } else { + if (isSetEqual(semantics, ['nominal', 'nominal'])) { + return 'square' + } else if (isSetEqual(semantics, ['nominal', 'quantitative'])) { + return 'tick' + } else if (isSetEqual(semantics, ['ordinal', 'quantitative'])) { + return 'point' + } else if (isSetEqual(semantics, ['nominal', 'ordinal'])) { + return 'tick' + } else if (isSetEqual(semantics, ['quantitative', 'quantitative'])) { + return 'circle' + } else if (isSetEqual(semantics, ['nominal', 'temporal'])) { + return 'point' + } + } + return 'point' +} + +// FIXME: 统一aggregate逻辑。 +export function autoStat(fields: IFieldMeta[]): { + statFields: IFieldMeta[]; + distFields: IFieldMeta[]; + statEncodes: IFieldEncode[]; +} { + const statFields: IFieldMeta[] = []; + const statEncodes: IFieldEncode[] = []; + const cond_singlefield = fields.length === 1; + const cond_nonquanmeasure = fields.filter(f => f.analyticType === 'measure').filter(f => f.semanticType === 'nominal' || f.semanticType === 'ordinal').length > 0; + if (cond_singlefield || cond_nonquanmeasure) { + statFields.push({ + fid: '__tmp_stat_id_unique', + semanticType: 'quantitative', + analyticType: 'measure', + geoRole: 'none', + features: { + entropy: Infinity, + maxEntropy: Infinity, + unique: 1000 + }, + distribution: [] + }) + statEncodes.push({ + aggregate: 'count' + }) + fields.filter(f => f.semanticType === 'quantitative').forEach(f => { + statFields.push({ ...f }) + statEncodes.push({ + field: f.fid, + title: f.name || f.fid, + type: f.semanticType, + bin: true + }) + }) + } else { + const targets = fields.filter(f => f.analyticType === 'measure'); + // 单目标的场景 + if (targets.length === 1) { + // 连续型 度量做聚合,非连续型度量做分箱; + targets.forEach(f => { + statFields.push({ ...f }) + statEncodes.push({ + field: f.fid, + type: f.semanticType, + title: `mean(${f.name || f.fid})`, + aggregate: 'mean' + }) + }) + fields.filter(f => f.analyticType === 'dimension' && f.semanticType === 'quantitative').forEach(f => { + statFields.push({ ...f }) + statEncodes.push({ + field: f.fid, + title: f.name || f.fid, + type: f.semanticType, + bin: true + }) + }) + } + } + const distFields = fields.filter(f => !statFields.find(sf => sf.fid === f.fid)); + return { statFields, distFields, statEncodes } +} + +export type IChannel = 'y' | 'x' | 'size' | 'color' | 'opacity' | 'row' | 'column' | 'shape'; +export const channels: { [key in ISemanticType]: IChannel[]} = { + quantitative: ['y', 'x', 'size', 'color', 'opacity'], + ordinal: ['y', 'x', 'color', 'opacity', 'size', 'shape'], + nominal: ['y', 'x', 'color', 'row', 'column', 'size', 'shape', 'opacity'], + temporal: ['y', 'x', 'size', 'color', 'opacity', 'shape'] +}; + +export const highOrderChannels = { + dimension: ['row', 'column'], + measure: ['repeat'] +} as const; + +interface EncodeProps { + fields: IFieldMeta[]; + channelEncoder: VizEncoder; + statFields?: IFieldMeta[]; + statEncodes?: IFieldEncode[] +} +export function encode(props: EncodeProps) { + const { + fields, + channelEncoder = new VizEncoder(), + statFields = [], + statEncodes = [] + } = props; + const orderFields = [...fields]; + let encoding: any = {} + let inHighOrderStatus: keyof typeof highOrderChannels | null = null; + let highOrderIndex: number = 0; + orderFields.sort((a, b) => b.features.entropy - a.features.entropy); + statFields.sort((a, b) => b.features.entropy - a.features.entropy); + const totalFields = [...statFields, ...orderFields].sort((a, b) => b.features.entropy - a.features.entropy); + // orderFields.unshift(...statFields); + for (let i = 0; i < totalFields.length; i++) { + const chs = channels[totalFields[i].semanticType]; + let encoded: boolean = false; + const statIndex = statFields.findIndex(f => f.fid === totalFields[i].fid) + const orderIndex = orderFields.findIndex(f => f.fid === totalFields[i].fid) + const isStatField = statIndex > -1; + if (isStatField) { + for (let j = 0; j < chs.length; j++) { + if (channelEncoder.avaiable(chs[j])) { + encoding[chs[j]] = statEncodes[statIndex] + channelEncoder.encode(chs[j], null) + encoded = true; + break; + } + } + // 发生可能很低 + // FIXME 多度量repeat设计 + if (!encoded) { + inHighOrderStatus = statFields[statIndex].analyticType; + if (inHighOrderStatus === 'dimension' && highOrderIndex < highOrderChannels[inHighOrderStatus].length) { + encoding[highOrderChannels[inHighOrderStatus][highOrderIndex]] = statEncodes[statIndex] + highOrderIndex++ + } + } + } else { + for (let j = 0; j < chs.length; j++) { + if (channelEncoder.avaiable(chs[j])) { + encoding[chs[j]] = { + field: orderFields[orderIndex].fid, + type: orderFields[orderIndex].semanticType, + title: orderFields[orderIndex].name || orderFields[orderIndex].fid + } + if (orderFields[orderIndex].semanticType === 'temporal' && chs[j] === 'color') { + encoding[chs[j]].scale = { + scheme: 'viridis' + } + } + channelEncoder.encode(chs[j], orderFields[orderIndex]) + encoded = true; + break; + } + } + if (!encoded) { + inHighOrderStatus = orderFields[orderIndex].analyticType; + if (inHighOrderStatus === 'dimension' && highOrderIndex < highOrderChannels[inHighOrderStatus].length) { + encoding[highOrderChannels[inHighOrderStatus][highOrderIndex]] = { + field: orderFields[orderIndex].fid, + type: orderFields[orderIndex].semanticType + } + highOrderIndex++ + } + } + } + } + return encoding +} + +export function humanHabbit (encoding: IVegaSubset['encoding']) { + if (encoding.x && encoding.x.type !== 'temporal') { + if (encoding.y && encoding.y.type === 'temporal') { + const t = encoding.x; + encoding.x = encoding.y; + encoding.y = t; + } + } +} + +function markFixEncoding (markType: string, usedChannels: Map) { + if (markType === 'bar') { + usedChannels.set('size', null); + usedChannels.set('shape', null); + } + if (markType === 'tick') { + usedChannels.set('size', null) + } +} + +// function defineChannelSeparability (channels: IChannel[]): number[][] { +// const mat = new Array(channels.length).fill(1).map(() => new Array(channels.length).fill(1)) +// const channelIndices: Map = new Map(channels.map((c, i) => [c, i])); +// const nonSeparate = (c1: IChannel, c2: IChannel) => { +// if (channelIndices.has(c1) && channelIndices.has(c2)) { +// mat[channelIndices.get(c1)!][channelIndices.get(c2)!] = mat[channelIndices.get(c1)!][channelIndices.get(c2)!] +// } +// } +// nonSeparate('color', 'shape'); +// return mat +// } + +export class VizEncoder { + public useChannels: Map; + public markType: string | null = null; + constructor (markType?: string) { + this.useChannels = new Map(); + if (markType) { + this.markType = markType || null; + markFixEncoding(markType, this.useChannels) + } + } + /** + * + * @param channel + * @param fieldId + * @returns success or not + */ + public encode (channel: IChannel, field?: IFieldMeta | null): boolean { + if (this.useChannels.has(channel)) return false; + if (channel === 'color' && field?.semanticType !== 'nominal') { + this.useChannels.set('opacity', null); + } + if (channel === 'opacity' && field?.semanticType !== 'nominal') { + this.useChannels.set('opacity', null); + } + this.useChannels.set(channel, field ? field.fid : null); + return true + } + public avaiable (channel: IChannel) { + return !this.useChannels.has(channel); + } +} \ No newline at end of file diff --git a/packages/rath-client/src/queries/labdistVis.ts b/packages/rath-client/src/queries/labdistVis.ts index 89985e73..37718633 100644 --- a/packages/rath-client/src/queries/labdistVis.ts +++ b/packages/rath-client/src/queries/labdistVis.ts @@ -7,7 +7,8 @@ import { bin, binMap, mic, pureGeneralMic, rangeNormilize } from '@kanaries/loa' import { IFieldMeta, IResizeMode, IRow, IVegaSubset } from "../interfaces"; import { deepcopy } from "../utils"; import { encodingDecorate } from "./base/utils"; -import { applyInteractiveParams2DistViz, applySizeConfig2DistViz } from "./distribution/utils"; +import { applyDefaultSort, applyInteractiveParams2DistViz, applySizeConfig2DistViz } from "./distribution/utils"; +import { autoMark, autoStat, encode, humanHabbit, VizEncoder } from './distribution/bot'; export const geomTypeMap: { [key: string]: any } = { interval: "boxplot", line: "line", @@ -16,24 +17,6 @@ export const geomTypeMap: { [key: string]: any } = { density: "point" }; -const channels = { - quantitative: ['y', 'x', 'size', 'color', 'opacity'], - ordinal: ['y', 'x', 'opacity', 'color', 'size', 'shape'], - nominal: ['y', 'x', 'color', 'row', 'column', 'opacity', 'size', 'shape'], - temporal: ['y', 'x', 'size', 'color', 'opacity', 'shape'] -} as const; -// const channels = { -// quantitative: ['y' , 'x', 'size', 'color', 'opacity'], -// ordinal: ['y', 'x', 'color', 'size', 'shape'], -// nominal: ['y', 'x', 'color', 'row', 'column', 'size', 'shape'], -// temporal: ['y', 'x', 'color', 'shape'] -// } as const; - -const highOrderChannels = { - dimension: ['row', 'column'], - measure: ['repeat'] -} as const; - interface BaseVisProps { dataSource: IRow[]; pattern: IPattern; @@ -42,152 +25,6 @@ interface BaseVisProps { width?: number; height?: number; } -function humanHabbit(encoding: any) { - if (encoding.x && encoding.x.type !== 'temporal') { - if (encoding.y && encoding.y.type === 'temporal') { - const t = encoding.x; - encoding.x = encoding.y; - encoding.y = t; - } - } -} - -interface EncodeProps { - fields: IFieldMeta[]; - usedChannels?: Set; - statFields?: IFieldMeta[]; - statEncodes?: IFieldEncode[] -} -function encode(props: EncodeProps) { - const { - fields, - usedChannels = new Set(), - statFields = [], - statEncodes = [] - } = props; - const orderFields = [...fields]; - let encoding: any = {} - let inHighOrderStatus: keyof typeof highOrderChannels | null = null; - let highOrderIndex: number = 0; - orderFields.sort((a, b) => b.features.entropy - a.features.entropy); - statFields.sort((a, b) => b.features.entropy - a.features.entropy); - const totalFields = [...statFields, ...orderFields].sort((a, b) => b.features.entropy - a.features.entropy); - // const totalFields = [...statFields, ...orderFields].sort((a, b) => a.features.entropy - b.features.entropy); - - // orderFields.unshift(...statFields); - for (let i = 0; i < totalFields.length; i++) { - const chs = channels[totalFields[i].semanticType]; - let encoded: boolean = false; - const statIndex = statFields.findIndex(f => f.fid === totalFields[i].fid) - const orderIndex = orderFields.findIndex(f => f.fid === totalFields[i].fid) - const isStatField = statIndex > -1; - if (isStatField) { - for (let j = 0; j < chs.length; j++) { - if (!usedChannels.has(chs[j])) { - encoding[chs[j]] = statEncodes[statIndex] - usedChannels.add(chs[j]) - encoded = true; - // if (statFields[statIndex].semanticType === 'quantitative') { - // if (statFields[statIndex].features.entropy / Math.log2(16) > 0.8) { - // encoding[chs[j]].scale = { type: 'sqrt' } - // } - // } - break; - } - } - // 发生可能很低 - // FIXME 多度量repeat设计 - if (!encoded) { - inHighOrderStatus = statFields[statIndex].analyticType; - if (inHighOrderStatus === 'dimension' && highOrderIndex < highOrderChannels[inHighOrderStatus].length) { - encoding[highOrderChannels[inHighOrderStatus][highOrderIndex]] = statEncodes[statIndex] - highOrderIndex++ - } - } - } else { - for (let j = 0; j < chs.length; j++) { - if (!usedChannels.has(chs[j])) { - encoding[chs[j]] = { - field: orderFields[orderIndex].fid, - type: orderFields[orderIndex].semanticType, - title: orderFields[orderIndex].name || orderFields[orderIndex].fid - } - if (orderFields[orderIndex].semanticType === 'temporal' && chs[j] === 'color') { - encoding[chs[j]].scale = { - scheme: 'viridis' - } - } - // if (orderFields[orderIndex].semanticType === 'quantitative') { - // if (orderFields[orderIndex].features.entropy / Math.log2(16) > 0.8) { - // encoding[chs[j]].scale = { type: 'sqrt' } - // } - // } - usedChannels.add(chs[j]) - encoded = true; - break; - } - } - if (!encoded) { - inHighOrderStatus = orderFields[orderIndex].analyticType; - if (inHighOrderStatus === 'dimension' && highOrderIndex < highOrderChannels[inHighOrderStatus].length) { - encoding[highOrderChannels[inHighOrderStatus][highOrderIndex]] = { - field: orderFields[orderIndex].fid, - type: orderFields[orderIndex].semanticType - } - highOrderIndex++ - } - } - } - } - - // for (let i = 0; i < statFields.length; i++) { - // const chs = channels[statFields[i].semanticType]; - // let encoded: boolean = false; - // for (let j = 0; j < chs.length; j++) { - // if (!usedChannels.has(chs[j])) { - // encoding[chs[j]] = statEncodes[i] - // usedChannels.add(chs[j]) - // encoded = true; - // break; - // } - // } - // // 发生可能很低 - // if (!encoded) { - // inHighOrderStatus = statFields[i].analyticType; - // if (inHighOrderStatus === 'dimension' && highOrderIndex < highOrderChannels[inHighOrderStatus].length) { - // encoding[highOrderChannels[inHighOrderStatus][highOrderIndex]] = statEncodes[i] - // highOrderIndex++ - // } - // } - // } - // for (let i = 0; i < orderFields.length; i++) { - // const chs = channels[orderFields[i].semanticType]; - // let encoded: boolean = false; - // for (let j = 0; j < chs.length; j++) { - // if (!usedChannels.has(chs[j])) { - // encoding[chs[j]] = { - // field: orderFields[i].fid, - // type: orderFields[i].semanticType, - // title: orderFields[i].name || orderFields[i].fid - // } - // usedChannels.add(chs[j]) - // encoded = true; - // break; - // } - // } - // if (!encoded) { - // inHighOrderStatus = orderFields[i].analyticType; - // if (inHighOrderStatus === 'dimension' && highOrderIndex < highOrderChannels[inHighOrderStatus].length) { - // encoding[highOrderChannels[inHighOrderStatus][highOrderIndex]] = { - // field: orderFields[i].fid, - // type: orderFields[i].semanticType - // } - // highOrderIndex++ - // } - // } - // } - return encoding -} function isSetEqual(a1: any[], a2: any[]) { const s1 = new Set(a1); @@ -248,141 +85,6 @@ function autoCoord(fields: IFieldMeta[], spec: {[key: string]: any}, dataSource: } } -function autoMark(fields: IFieldMeta[], statFields: IFieldMeta[] = [], originFields: IFieldMeta[] = [], statEncodes: IFieldEncode[] = []) { - // const orderFields = [...fields]; - // const orderStatFields = [...statFields]; - // orderFields.sort((a, b) => b.features.entropy - a.features.entropy); - // orderStatFields.sort((a, b) => b.features.entropy - a.features.entropy); - const semantics = [...statFields, ...originFields].sort((a, b) => b.features.entropy - a.features.entropy).slice(0, 2).map(f => f.semanticType) - // if (fields.length === 1) { - // return 'bar' - // } - // FIXME: 时间序列多目标 - // if (statFields.length > 0) { - // // 仅对count生效。 - // return 'bar' - // } - if (statEncodes.find(f => f.aggregate === 'count')) { - return 'bar' - } - // if (fields.length === 1) { - // return 'bar' - // } - const cond_sinleTargets = fields.filter(f => f.analyticType === 'measure').length === 1; - - if (cond_sinleTargets) { - if (isSetEqual(semantics, ['nominal', 'nominal'])) { - return 'text' - } else if (isSetEqual(semantics, ['nominal', 'quantitative'])) { - return 'bar' - } else if (isSetEqual(semantics, ['ordinal', 'quantitative'])) { - return 'line' - } else if (isSetEqual(semantics, ['nominal', 'ordinal'])) { - return 'point' - } else if (isSetEqual(semantics, ['nominal', 'temporal'])) { - return 'point' - } else if (isSetEqual(semantics, ['quantitative', 'quantitative'])) { - return 'circle' - } else if (isSetEqual(semantics, ['temporal', 'quantitative'])) { - return 'line' - } - } else { - if (isSetEqual(semantics, ['nominal', 'nominal'])) { - return 'square' - } else if (isSetEqual(semantics, ['nominal', 'quantitative'])) { - return 'tick' - } else if (isSetEqual(semantics, ['ordinal', 'quantitative'])) { - return 'point' - } else if (isSetEqual(semantics, ['nominal', 'ordinal'])) { - return 'tick' - } else if (isSetEqual(semantics, ['quantitative', 'quantitative'])) { - return 'circle' - } else if (isSetEqual(semantics, ['nominal', 'temporal'])) { - return 'point' - } - } - return 'point' -} - -function markFixEncoding(markType: string, usedChannels: Set) { - if (markType === 'bar') { - usedChannels.add('size'); - usedChannels.add('shape'); - } -} - -interface IFieldEncode { - field?: string; - title?: string; - type?: string; - aggregate?: string; - bin?: boolean; -} - -// FIXME: 统一aggregate逻辑。 -function autoStat(fields: IFieldMeta[]): { - statFields: IFieldMeta[]; - distFields: IFieldMeta[]; - statEncodes: IFieldEncode[]; -} { - const statFields: IFieldMeta[] = []; - const statEncodes: IFieldEncode[] = []; - const cond_singlefield = fields.length === 1; - const cond_nonquanmeasure = fields.filter(f => f.analyticType === 'measure').filter(f => f.semanticType === 'nominal' || f.semanticType === 'ordinal').length > 0; - if (cond_singlefield || cond_nonquanmeasure) { - statFields.push({ - fid: '__tmp_stat_id_unique', - semanticType: 'quantitative', - analyticType: 'measure', - geoRole: 'none', - features: { - entropy: Infinity, - maxEntropy: Infinity, - unique: 1000 - }, - distribution: [] - }) - statEncodes.push({ - aggregate: 'count' - }) - fields.filter(f => f.semanticType === 'quantitative').forEach(f => { - statFields.push({ ...f }) - statEncodes.push({ - field: f.fid, - title: f.name || f.fid, - type: f.semanticType, - bin: true - }) - }) - } else { - const targets = fields.filter(f => f.analyticType === 'measure'); - // 单目标的场景 - if (targets.length === 1) { - // 连续型 度量做聚合,非连续型度量做分箱; - targets.forEach(f => { - statFields.push({ ...f }) - statEncodes.push({ - field: f.fid, - type: f.semanticType, - title: `mean(${f.name || f.fid})`, - aggregate: 'mean' - }) - }) - fields.filter(f => f.analyticType === 'dimension' && f.semanticType === 'quantitative').forEach(f => { - statFields.push({ ...f }) - statEncodes.push({ - field: f.fid, - title: f.name || f.fid, - type: f.semanticType, - bin: true - }) - }) - } - } - const distFields = fields.filter(f => !statFields.find(sf => sf.fid === f.fid)); - return { statFields, distFields, statEncodes } -} - export function labDistVis(props: BaseVisProps): IVegaSubset { const { pattern, dataSource, width, height, interactive, resizeMode = IResizeMode.auto } = props; const fields = deepcopy(pattern.fields) as IFieldMeta[]; @@ -441,30 +143,18 @@ export function labDistVis(props: BaseVisProps): IVegaSubset { dimensions[i].features.originEntropy = dimensions[i].features.entropy dimensions[i].features.entropy = totalEntLoss; } - const usedChannels: Set = new Set(); const { statFields, distFields, statEncodes } = autoStat(fields); let markType = autoMark(fields, statFields, distFields, statEncodes) - markFixEncoding(markType, usedChannels) + const channelEncoder = new VizEncoder(markType); // if (filters && filters.length > 0) { // usedChannels.add('color') // } const enc = encode({ - fields: distFields, usedChannels, statFields, + fields: distFields, + channelEncoder, + statFields, statEncodes }) - if (markType === 'bar' && statEncodes.length > 0) { - if (enc && enc.x && enc.y) { - if (enc.x.field && enc.y.field) { - const sortEncodeField = (enc.y.type === 'quantitative' ? enc.x : enc.y); - const sortBasedEncodeField = (enc.y.type === 'quantitative' ? enc.y : enc.x); - sortEncodeField.sort = { - field: sortBasedEncodeField.field, - op: sortBasedEncodeField.aggregate || 'count', - order: 'descending' - } - } - } - } // if (filters && filters.length > 0) { // const field = filters[0].field; // enc.color = { @@ -503,11 +193,12 @@ export function labDistVis(props: BaseVisProps): IVegaSubset { // }], mark: { type: markType as any, - opacity: markType === 'circle' ? 0.56 : 0.88 + opacity: markType === 'circle' ? 0.66 : 0.88 }, encoding: enc }; autoCoord(fields, basicSpec, dataSource) + applyDefaultSort(basicSpec, fields) applySizeConfig2DistViz(basicSpec, { mode: resizeMode, width,