exbert / client /src /ts /vis /CorpusMatManager.ts
bhoov's picture
First commit
63858e7
raw
history blame
9.67 kB
import * as d3 from 'd3'
import * as R from 'ramda'
import * as tp from '../etc/types'
import { D3Sel } from '../etc/Util'
import { VComponent } from '../vis/VisComponent'
import { SimpleEventHandler } from "../etc/SimpleEventHandler";
import { SVG } from "../etc/SVGplus"
import { spacyColors } from "../etc/SpacyInfo"
import "../etc/xd3"
// Need additoinal height information to render boxes
interface BaseDataInterface extends tp.FaissSearchResults {
height: number
}
type DataInterface = BaseDataInterface[]
interface ColorMetaBaseData {
pos: string
dep: string
is_ent: boolean
token: string
}
type DisplayOptions = "pos" | "dep" | "ent"
function managerData2MatData(dataIn: DataInterface, indexOffset = 0, toPick = ['pos']) {
const outOfRangeObj: ColorMetaBaseData = {
pos: null,
dep: null,
is_ent: null,
token: null,
}
const chooseProps = R.pick(toPick)
const dataOut = dataIn.map(d => {
const wordIdx = d.index + indexOffset;
if ((wordIdx < 0) || (wordIdx >= d.tokens.length)) {
return R.assoc('height', d.height, outOfRangeObj)
}
const newObj = chooseProps(d.tokens[wordIdx])
return R.assoc('height', d.height, newObj)
})
return dataOut
}
export class CorpusMatManager extends VComponent<DataInterface>{
css_name = 'corpus-mat-container'
options = {
cellWidth: 10,
toPick: ['pos'],
idxs: [-1, 0, 1],
divHover: {
width: 60,
height: 40
}
}
static events = {
mouseOver: "CorpusMatManager_MouseOver",
mouseOut: "CorpusMatManager_MouseOut",
click: "CorpusMatManager_Click",
dblClick: "CorpusMatManager_DblClick",
rectMouseOver: "CorpusMatManager_RectMouseOver",
rectMouseOut: "CorpusMatManager_RectMouseOut",
rectClick: "CorpusMatManager_RectClick",
rectDblClick: "CorpusMatManager_RectDblClick",
}
// The d3 components that are saved to make rendering faster
corpusMats: D3Sel
rowGroups: D3Sel
divHover: D3Sel
_current = {}
rowCssName = 'index-match-results'
cellCssName = 'index-cell-result'
_data: DataInterface
static colorScale: tp.ColorMetaScale = spacyColors.colorScale;
// Selections
constructor(d3parent: D3Sel, eventHandler?: SimpleEventHandler, options = {}) {
super(d3parent, eventHandler)
this.idxs = [-1, 0, 1];
this.superInitHTML(options)
this._init()
}
get idxs() {
return this.options.idxs;
}
set idxs(val: number[]) {
this.options.idxs = val
}
// Create static dom elements
_init() {
const self = this;
this.corpusMats = this.base.selectAll('.corpus-mat')
this.rowGroups = this.corpusMats.selectAll(`.${this.rowCssName}`)
this.divHover = this.base.append('div')
.classed('mat-hover-display', true)
.classed('text-center', true)
.style('width', String(this.options.divHover.width) + 'px')
.style('height', String(this.options.divHover.height) + 'px')
this.divHover.append('p')
}
pick(val: DisplayOptions) {
this.options.toPick = [val]
this.redraw()
}
addRight() {
const addedIdx = R.last(this.idxs) + 1;
this.idxs.push(addedIdx)
this.addCorpusMat(addedIdx, "right")
}
addLeft() {
const addedIdx = this.idxs[0] - 1;
const addDecrementedHead: (x: number[]) => number[] = x => R.insert(0, R.head(x) - 1)(x)
this.idxs = addDecrementedHead(this.idxs)
this.addCorpusMat(addedIdx, "left")
}
killRight() {
this.kill(Math.max(...this.idxs))
}
killLeft() {
this.kill(Math.min(...this.idxs))
}
/**
* Remove edge value from contained indexes
*
* @param d Index to remove
*/
kill(d: number) {
if (d != 0) {
if (d == Math.min(...this.idxs) || d == Math.max(...this.idxs)) {
this.idxs = R.without([d], this.idxs)
this.base.selectAll(`.offset-${d}`).remove()
}
}
}
_wrangle(data: DataInterface) {
return data
}
data(val?: DataInterface) {
if (val == null) {
return this._data;
}
this._data = val;
this._updateData();
return this;
}
/**
* The main rendering code, called whenever the data changes.
*/
private _updateData() {
const self = this;
const op = this.options;
this.base.selectAll('.corpus-mat').remove()
this.idxs.forEach((idxOffset, i) => {
self.addCorpusMat(idxOffset)
})
}
/**
* Add another word's meta information matrix column to either side of the index
*
* @param idxOffset Distance of word from matched word in the sentence
* @param toThe Indicates adding to the "left" or to the "right" of the index
*/
addCorpusMat(idxOffset: number, toThe: "right" | "left" = "right") {
const self = this;
const op = this.options;
const boxWidth = op.cellWidth * op.toPick.length;
const boxHeight = R.sum(R.map(R.prop('height'), this._data))
let corpusMat;
if (toThe == "right") {
corpusMat = this.base.append('div')
}
else if (toThe == "left") {
corpusMat = this.base.insert('div', ":first-child")
}
else {
throw Error("toThe must have argument of 'left' or 'right'")
}
corpusMat = corpusMat
.data([idxOffset])
.attr('class', `corpus-mat offset-${idxOffset}`)
.attr('offset', idxOffset)
.append('svg')
.attrs({
width: boxWidth,
height: boxHeight,
})
.on('mouseover', function (d, i) {
self.eventHandler.trigger(CorpusMatManager.events.mouseOver, { idx: i, offset: d, val: self.options.toPick[0] })
})
.on('mouseout', (d, i) => {
this.eventHandler.trigger(CorpusMatManager.events.mouseOut, { idx: i, offset: d })
})
this.addRowGroup(corpusMat)
}
/**
*
* @param mat The base div on which to add matrices and rows
*/
addRowGroup(mat: D3Sel) {
const self = this;
const op = this.options;
const heights = R.map(R.prop('height'), this._data)
const [heightSum, rawHeightList] = R.mapAccum((x, y) => [R.add(x, y), R.add(x, y)], 0, heights)
const fixList: (x: number[]) => number[] = R.compose(R.dropLast(1),
// @ts-ignore
R.prepend(0)
)
const heightList = fixList(rawHeightList)
const rowGroup = mat.selectAll(`.${self.rowCssName}`)
.data(d => managerData2MatData(self._data, d, op.toPick))
.join("g")
.attr("class", (d, i) => {
return `${self.rowCssName} ${self.rowCssName}-${i}`
})
.attr("row-num", (d,i) => i)
.attr("height", d => d.height)
.attr("transform", (d, i) => {
const out = SVG.translate({
x: 0,
y: heightList[i],
})
return out
})
op.toPick.forEach(prop => {
self.addRect(rowGroup, 0, prop)
})
}
addRect(g: D3Sel, xShift: number, prop: string) {
const self = this
const op = this.options
const rects = g.append('rect')
.attrs({
width: op.cellWidth,
height: d => d.height - 3,
transform: (d, i) => {
return SVG.translate({
x: xShift,
y: 1.5,
})
},
})
.style('fill', d => CorpusMatManager.colorScale[prop](d[prop]))
const getBaseX = () => (<HTMLElement>self.base.node()).getBoundingClientRect().left
const getBaseY = () => (<HTMLElement>self.base.node()).getBoundingClientRect().top
g.on('mouseover', function (d, i) {
self.divHover.style('visibility', 'visible')
// Get offset
const col = d3.select(this.parentNode.parentNode) // Column
const offset = +col.attr('offset')
self.eventHandler.trigger(CorpusMatManager.events.rectMouseOver, {idx: i, offset: offset})
})
.on('mouseout', function (d, i) {
self.divHover.style('visibility', 'hidden')
const col = d3.select(this.parentNode.parentNode) // Column
const offset = +col.attr('offset')
self.eventHandler.trigger(CorpusMatManager.events.rectMouseOut, {idx: i, offset: offset})
})
.on('mousemove', function(d, i) {
const mouse = d3.mouse(self.base.node())
const divOffset = [3, 3]
const left = mouse[0] + getBaseX() - (op.divHover.width + divOffset[0])
const top = mouse[1] + getBaseY() - (op.divHover.height + divOffset[1])
self.divHover
.style('left', String(left) + 'px')
.style('top', String(top) + 'px')
.selectAll('p')
.text(d[prop])
})
}
/**
* @param data Data to display
*/
_render(data: DataInterface) {
this._updateData();
}
}