exbert-project/exbert
183
1import * as d3 from "d3";2import 'd3-selection-multi'3import { D3Sel } from "../etc/Util";4import { Edge, EdgeData } from "./EdgeConnector"5import { VComponent } from "./VisComponent";6import { SimpleEventHandler } from "../etc/SimpleEventHandler";7import * as tp from "../etc/types"8 9export type AttentionData = number[][]10 11export const scaleLinearWidth = opacity => 5 * opacity^0.33;12 13export class AttentionGraph extends VComponent<AttentionData>{14 css_name = '';15 _current: {};16 17 _data: AttentionData; // The passed data18 edgeData: EdgeData; // A wrapper around _data. User should not mind19 plotData: Edge[]; // Needed for plotting20 21 /** COMPONENTS22 * Expose the components belonging to the class as properties of the class. 23 * This is useful to create methods that specifically modify a single part or component without having to reselect it. 24 * Makes for more responsive applications25 * */26 svg: D3Sel;27 graph: D3Sel;28 29 // The below components require data30 paths: D3Sel;31 opacityScales: d3.ScaleLinear<any, any>[];32 linkGen: d3.Link<any, any, any>33 34 // OPTIONS WITH DEFAULTS35 _threshold = 0.7; // Accumulation threshold. Between 0-136 normBy: tp.NormBy37 38 static events = {} // No events needed for this one39 40 options = {41 boxheight: 26, // The height of the div boxes around the SVG element42 height: 500,43 width: 200,44 offset: 0, // Should I offset the left side by 1 or not?45 }46 47 constructor(d3Parent: D3Sel, eventHandler?: SimpleEventHandler, options: {} = {}) {48 super(d3Parent, eventHandler)49 this.superInitSVG(options)50 this._init()51 }52 53 _init() {54 this.svg = this.parent;55 this.graph = this.svg.selectAll(`.atn-curve`);56 this.linkGen = d3.linkHorizontal()57 .x(d => d[0])58 .y(d => d[1]);59 }60 61 // Define whether to use the 'j' or 'i' attribute to calculate opacities62 private scaleIdx(): "i" | "j" {63 switch (this.normBy) {64 case tp.NormBy.COL:65 return 'j'66 case tp.NormBy.ROW:67 return 'i'68 case tp.NormBy.ALL:69 return 'i'70 71 }72 73 }74 75 /**76 * Create connections between locations of the SVG using D3's linkGen77 */78 private createConnections() {79 const self = this;80 const op = this.options;81 if (this.paths) {82 this.paths.attrs({83 'd': (d, i) => {84 const data: { source: [number, number], target: [number, number] } =85 {86 source: [0, op.boxheight * (d.i + 0.5 + op.offset)],87 target: [op.width, op.boxheight * (d.j + 0.5)] // + 2 allows small offset88 };89 return this.linkGen(data);90 },91 'class': 'atn-curve'92 })93 .attr("src-idx", (d, i) => d.i)94 .attr("target-idx", (d, i) => d.j);95 }96 }97 98 /**99 * Change the height of the SVG100 */101 private updateHeight() {102 const op = this.options;103 if (this.svg != null) {104 this.svg.attr("height", this.options.height + (op.offset * this.options.boxheight))105 }106 return this;107 }108 109 /**110 * Change the width of the SVG111 */112 private updateWidth() {113 if (this.svg != null) {114 this.svg.attr("width", this.options.width)115 }116 return this;117 }118 119 /**120 * Change the Opacity of the lines according to the value of the data121 */122 private updateOpacity() {123 const self = this;124 if (this.paths != null) {125 // paths.transition().duration(500).attr('opacity', (d) => {126 this.paths.attr('opacity', (d) => {127 const val = this.opacityScales[d[self.scaleIdx()]](d.v);128 return val;129 })130 this.paths.attr('stroke-width', (d) => {131 const val = this.opacityScales[d[self.scaleIdx()]](d.v);132 return scaleLinearWidth(val) //5 * val^0.33;133 })134 }135 return this;136 }137 138 /**139 * Rerender the graph in the event that the data changes140 */141 private updateData() {142 if (this.graph != null) {143 d3.selectAll(".atn-curve").remove();144 145 const data = this.plotData146 147 this.paths = this.graph148 .data(data)149 .join('path');150 151 this.createConnections();152 this.updateOpacity();153 154 return this;155 }156 }157 158 /**159 * Scale the opacity according to the values of the data, from 0 to max of contained data160 * Normalize by each source target, or across the whole161 */162 private createScales = () => {163 this.opacityScales = [];164 let arr = []165 166 // Group normalization167 switch (this.normBy){168 case tp.NormBy.ROW:169 arr = this.edgeData.extent(1);170 this.opacityScales = [];171 arr.forEach((v, i) => {172 (this.opacityScales as d3.ScaleLinear<any, any>[]).push(173 d3.scaleLinear()174 .domain([0, v[1]])175 .range([0, 0.9])176 )177 })178 break;179 case tp.NormBy.COL:180 arr = this.edgeData.extent(0);181 this.opacityScales = [];182 arr.forEach((v, i) => {183 (this.opacityScales as d3.ScaleLinear<any, any>[]).push(184 d3.scaleLinear()185 .domain([0, v[1]])186 .range([0, 0.9])187 )188 })189 break;190 case tp.NormBy.ALL:191 const maxIn = d3.max(this.plotData.map((d) => d.v))192 for (let i = 0; i < this._data.length; i++) {193 this.opacityScales.push(d3.scaleLinear()194 .domain([0, maxIn])195 .range([0, 1]));196 }197 break;198 default:199 console.log("Nor norming specified");200 break;201 }202 }203 204 /**205 * Access / modify the data in a D3 style way. If modified, the component will update just the part that is needed to be updated206 */207 data(): AttentionData208 data(value: AttentionData): this209 data(value?) {210 if (value == null) {211 return this._data;212 }213 214 this._data = value;215 this.edgeData = new EdgeData(value);216 this.plotData = this.edgeData.format(this._threshold);217 this.createScales();218 this.updateData();219 return this;220 }221 222 /**223 * Access / modify the height in a D3 style way. If modified, the component will update just the part that is needed to be updated224 */225 height(): number226 height(value: number): this227 height(value?) {228 if (value == null) {229 return this.options.height230 }231 232 this.options.height = value233 this.updateHeight()234 return this;235 }236 237 /**238 * Access / modify the width in a D3 style way. If modified, the component will update just the part that is needed to be updated239 */240 width(): number241 width(value: number): this242 width(value?: number): this | number {243 if (value == null) {244 return this.options.width;245 }246 this.options.width = value;247 this.updateWidth();248 return this;249 }250 251 /**252 * Access / modify the threshold in a D3 style way. If modified, the component will update just the part that is needed to be updated253 */254 threshold(): number255 threshold(value: number): this256 threshold(value?) {257 if (value == null) {258 return this._threshold;259 }260 261 this._threshold = value;262 this.plotData = this.edgeData.format(this._threshold);263 this.createScales();264 this.updateData();265 return this;266 }267 268 _wrangle(data: AttentionData) {269 return data;270 }271 272 _render(data: AttentionData) {273 this.svg.html('')274 this.updateHeight();275 this.updateWidth();276 277 this.updateData();278 return this;279 }280}