Spaces:
Runtime error
Runtime error
| import * as d3 from "d3"; | |
| import 'd3-selection-multi' | |
| import { D3Sel } from "../etc/Util"; | |
| import { Edge, EdgeData } from "./EdgeConnector" | |
| import { VComponent } from "./VisComponent"; | |
| import { SimpleEventHandler } from "../etc/SimpleEventHandler"; | |
| import * as tp from "../etc/types" | |
| export type AttentionData = number[][] | |
| export const scaleLinearWidth = opacity => 5 * opacity^0.33; | |
| export class AttentionGraph extends VComponent<AttentionData>{ | |
| css_name = ''; | |
| _current: {}; | |
| _data: AttentionData; // The passed data | |
| edgeData: EdgeData; // A wrapper around _data. User should not mind | |
| plotData: Edge[]; // Needed for plotting | |
| /** COMPONENTS | |
| * Expose the components belonging to the class as properties of the class. | |
| * This is useful to create methods that specifically modify a single part or component without having to reselect it. | |
| * Makes for more responsive applications | |
| * */ | |
| svg: D3Sel; | |
| graph: D3Sel; | |
| // The below components require data | |
| paths: D3Sel; | |
| opacityScales: d3.ScaleLinear<any, any>[]; | |
| linkGen: d3.Link<any, any, any> | |
| // OPTIONS WITH DEFAULTS | |
| _threshold = 0.7; // Accumulation threshold. Between 0-1 | |
| normBy: tp.NormBy | |
| static events = {} // No events needed for this one | |
| options = { | |
| boxheight: 26, // The height of the div boxes around the SVG element | |
| height: 500, | |
| width: 200, | |
| offset: 0, // Should I offset the left side by 1 or not? | |
| } | |
| constructor(d3Parent: D3Sel, eventHandler?: SimpleEventHandler, options: {} = {}) { | |
| super(d3Parent, eventHandler) | |
| this.superInitSVG(options) | |
| this._init() | |
| } | |
| _init() { | |
| this.svg = this.parent; | |
| this.graph = this.svg.selectAll(`.atn-curve`); | |
| this.linkGen = d3.linkHorizontal() | |
| .x(d => d[0]) | |
| .y(d => d[1]); | |
| } | |
| // Define whether to use the 'j' or 'i' attribute to calculate opacities | |
| private scaleIdx(): "i" | "j" { | |
| switch (this.normBy) { | |
| case tp.NormBy.COL: | |
| return 'j' | |
| case tp.NormBy.ROW: | |
| return 'i' | |
| case tp.NormBy.ALL: | |
| return 'i' | |
| } | |
| } | |
| /** | |
| * Create connections between locations of the SVG using D3's linkGen | |
| */ | |
| private createConnections() { | |
| const self = this; | |
| const op = this.options; | |
| if (this.paths) { | |
| this.paths.attrs({ | |
| 'd': (d, i) => { | |
| const data: { source: [number, number], target: [number, number] } = | |
| { | |
| source: [0, op.boxheight * (d.i + 0.5 + op.offset)], | |
| target: [op.width, op.boxheight * (d.j + 0.5)] // + 2 allows small offset | |
| }; | |
| return this.linkGen(data); | |
| }, | |
| 'class': 'atn-curve' | |
| }) | |
| .attr("src-idx", (d, i) => d.i) | |
| .attr("target-idx", (d, i) => d.j); | |
| } | |
| } | |
| /** | |
| * Change the height of the SVG | |
| */ | |
| private updateHeight() { | |
| const op = this.options; | |
| if (this.svg != null) { | |
| this.svg.attr("height", this.options.height + (op.offset * this.options.boxheight)) | |
| } | |
| return this; | |
| } | |
| /** | |
| * Change the width of the SVG | |
| */ | |
| private updateWidth() { | |
| if (this.svg != null) { | |
| this.svg.attr("width", this.options.width) | |
| } | |
| return this; | |
| } | |
| /** | |
| * Change the Opacity of the lines according to the value of the data | |
| */ | |
| private updateOpacity() { | |
| const self = this; | |
| if (this.paths != null) { | |
| // paths.transition().duration(500).attr('opacity', (d) => { | |
| this.paths.attr('opacity', (d) => { | |
| const val = this.opacityScales[d[self.scaleIdx()]](d.v); | |
| return val; | |
| }) | |
| this.paths.attr('stroke-width', (d) => { | |
| const val = this.opacityScales[d[self.scaleIdx()]](d.v); | |
| return scaleLinearWidth(val) //5 * val^0.33; | |
| }) | |
| } | |
| return this; | |
| } | |
| /** | |
| * Rerender the graph in the event that the data changes | |
| */ | |
| private updateData() { | |
| if (this.graph != null) { | |
| d3.selectAll(".atn-curve").remove(); | |
| const data = this.plotData | |
| this.paths = this.graph | |
| .data(data) | |
| .join('path'); | |
| this.createConnections(); | |
| this.updateOpacity(); | |
| return this; | |
| } | |
| } | |
| /** | |
| * Scale the opacity according to the values of the data, from 0 to max of contained data | |
| * Normalize by each source target, or across the whole | |
| */ | |
| private createScales = () => { | |
| this.opacityScales = []; | |
| let arr = [] | |
| // Group normalization | |
| switch (this.normBy){ | |
| case tp.NormBy.ROW: | |
| arr = this.edgeData.extent(1); | |
| this.opacityScales = []; | |
| arr.forEach((v, i) => { | |
| (this.opacityScales as d3.ScaleLinear<any, any>[]).push( | |
| d3.scaleLinear() | |
| .domain([0, v[1]]) | |
| .range([0, 0.9]) | |
| ) | |
| }) | |
| break; | |
| case tp.NormBy.COL: | |
| arr = this.edgeData.extent(0); | |
| this.opacityScales = []; | |
| arr.forEach((v, i) => { | |
| (this.opacityScales as d3.ScaleLinear<any, any>[]).push( | |
| d3.scaleLinear() | |
| .domain([0, v[1]]) | |
| .range([0, 0.9]) | |
| ) | |
| }) | |
| break; | |
| case tp.NormBy.ALL: | |
| const maxIn = d3.max(this.plotData.map((d) => d.v)) | |
| for (let i = 0; i < this._data.length; i++) { | |
| this.opacityScales.push(d3.scaleLinear() | |
| .domain([0, maxIn]) | |
| .range([0, 1])); | |
| } | |
| break; | |
| default: | |
| console.log("Nor norming specified"); | |
| break; | |
| } | |
| } | |
| /** | |
| * Access / modify the data in a D3 style way. If modified, the component will update just the part that is needed to be updated | |
| */ | |
| data(): AttentionData | |
| data(value: AttentionData): this | |
| data(value?) { | |
| if (value == null) { | |
| return this._data; | |
| } | |
| this._data = value; | |
| this.edgeData = new EdgeData(value); | |
| this.plotData = this.edgeData.format(this._threshold); | |
| this.createScales(); | |
| this.updateData(); | |
| return this; | |
| } | |
| /** | |
| * Access / modify the height in a D3 style way. If modified, the component will update just the part that is needed to be updated | |
| */ | |
| height(): number | |
| height(value: number): this | |
| height(value?) { | |
| if (value == null) { | |
| return this.options.height | |
| } | |
| this.options.height = value | |
| this.updateHeight() | |
| return this; | |
| } | |
| /** | |
| * Access / modify the width in a D3 style way. If modified, the component will update just the part that is needed to be updated | |
| */ | |
| width(): number | |
| width(value: number): this | |
| width(value?: number): this | number { | |
| if (value == null) { | |
| return this.options.width; | |
| } | |
| this.options.width = value; | |
| this.updateWidth(); | |
| return this; | |
| } | |
| /** | |
| * Access / modify the threshold in a D3 style way. If modified, the component will update just the part that is needed to be updated | |
| */ | |
| threshold(): number | |
| threshold(value: number): this | |
| threshold(value?) { | |
| if (value == null) { | |
| return this._threshold; | |
| } | |
| this._threshold = value; | |
| this.plotData = this.edgeData.format(this._threshold); | |
| this.createScales(); | |
| this.updateData(); | |
| return this; | |
| } | |
| _wrangle(data: AttentionData) { | |
| return data; | |
| } | |
| _render(data: AttentionData) { | |
| this.svg.html('') | |
| this.updateHeight(); | |
| this.updateWidth(); | |
| this.updateData(); | |
| return this; | |
| } | |
| } |