import { customElement, bindable, inject } from 'aurelia-framework'; import { EventAggregator } from 'aurelia-event-aggregator'; import * as d3 from './d3custom'; import { VisualConfig } from './config_visual'; import { UpdateVisualisation } from './messages'; @customElement('sampler-visual') @inject(VisualConfig, EventAggregator) export class SamplerVisual { @bindable colours; @bindable lang; @bindable maxSamples; @bindable sampler; @bindable soffsets; @bindable sortMethodId; constructor(configuration, ea) { // injected VisualConfig, EventAggregator this.ea = ea; this.svgWidth = 480; this.svgHeight = 340; this.axisWidth = 45; this.axisOffset = 10; this.tickLength = 10; this.labelOffset = 5; this.axisLabel = {de: 'Gewinne', en: 'Profits'}; for (let key in configuration) { this[key] = configuration[key]; } this.ea.subscribe(UpdateVisualisation, msg => { this.updateVisualisation(); }); } attached() { // (aurelia life cycle function) set up d3 view when dom ready let columns = (this.blocks.horizontal.amount * this.blocks.horizontal.size); let rows = this.blocks.vertical.amount * this.blocks.vertical.size; let vmrgn = (this.svgHeight - rows * (this.radius * (2 + this.dotspacingRatio))) / 2; let hmrgn = (this.svgWidth - columns * (this.radius * (2 + this.dotspacingRatio))) / 2 - this.axisWidth / 2; this.margin = { right: hmrgn, left: hmrgn, top: vmrgn, bottom: vmrgn }; this.width = this.svgWidth - this.margin.left - this.margin.right; this.height = this.svgHeight - this.margin.top - this.margin.bottom; this.svg = d3.select('#' + this.sampler.svgclass) .attr('width', this.width + this.margin.left + this.margin.right) .attr('height', this.height + this.margin.top + this.margin.bottom); this.lgroup = this.svg.append('g') .attr('class', 'legend-group') .attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')'); this.cgroup = this.svg.append('g') .attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')'); if (this.sampler.samples.length >= 0) { this.drawAxis(); this.updateVisualisation(); } } drawAxis() { let blockspacing = this.radius * 2 * this.blocks.horizontal.spacingRatio; let dotspacing = this.radius * this.dotspacingRatio; let xoffset = this.radius; let cgroup = this.lgroup.append('g') .attr('class', 'class-label'); cgroup.append('text') .text(this.axisLabel[this.lang]) .style('font-size', '12px') .style('font-weight', 'bold') .attr('x', xoffset + this.blocks.horizontal.size * (2 * this.radius + dotspacing) + blockspacing / 2 + blockspacing / 2) .attr('y', this.height + this.axisOffset + this.labelOffset); // horizontal line cgroup.append('line') .attr('stroke', '#000') .attr('x1', -this.axisOffset) .attr('y1', this.height) .attr('x2', this.width + this.axisOffset) .attr('y2', this.height); for (let cidx in this.sampler.classes) { cidx = parseInt(cidx); let bidx = cidx; // all labels are offset by one block + half block spacing // for every following block the full block spacing is added let boffset = xoffset + (cidx + 1) * (this.blocks.horizontal.size * (2 * this.radius + dotspacing) + blockspacing / 2) + cidx * blockspacing / 2; let lines = this.sampler.classes[bidx].name[this.lang].split('\n'); let y = this.height + this.axisOffset + this.labelOffset * 4; if (cidx === 0) { y = this.height + this.axisOffset + this.labelOffset; } let label = cgroup.append('text') .style('font-size', '12px') .style('font-weight', function() { return cidx === 0 ? 'bold' : 'normal'; }) .attr('y', y); for (let lidx = 0; lidx < lines.length; ++lidx) { label.append('tspan') .attr('x', boffset - (this.blocks.horizontal.size * (2 * this.radius + dotspacing) + blockspacing / 2)) .attr('dy', function() { return lidx > 0 ? '1.2em' : '0em'; }) .text(lines[lidx]); } if (cidx < this.sampler.classes.length - 1) { let y2 = this.height + this.axisOffset; let y2long = this.height + this.axisOffset * 4.5; if (cidx === 0) { y2 = y2long; } cgroup.append('line') .attr('stroke', '#000') .attr('stroke-dasharray', '1') .attr('opacity', '0.2') .attr('x1', boffset - this.radius - dotspacing / 2) .attr('y1', -this.axisOffset) .attr('x2', boffset - this.radius - dotspacing / 2) .attr('y2', y2long); cgroup.append('line') .attr('stroke', '#000') .attr('x1', boffset - this.radius - dotspacing / 2) .attr('y1', this.height) .attr('x2', boffset - this.radius - dotspacing / 2) .attr('y2', y2); } } } updateVisualisation() { let radius = this.radius; let diameter = 2 * radius; let dotspacing = radius * this.dotspacingRatio; let nrblocks = { h: this.blocks.horizontal.amount, v: this.blocks.vertical.amount }; let blocksize = { h: this.blocks.horizontal.size, v: this.blocks.vertical.size }; let blockspacing = { h: diameter * this.blocks.horizontal.spacingRatio, v: diameter * this.blocks.vertical.spacingRatio }; let xoffset = radius; let yoffset = radius; let iaheight = this.height; let colours = this.colours; // Invert every structure: samples, offsets, colours // FIXME: Is there still a reason to do this instead of inverting the order in the configuration? let spc = this.sampler.samplesPerClass(); let samplesPerClass = Array(...spc); samplesPerClass.reverse(); let synClassOffsets = Array(...this.soffsets); synClassOffsets.reverse(); let samplesConcat = []; // for (let i = 0; i < samplesPerClass.length; ++i) { for (let i = samplesPerClass.length - 1; i >= 0; --i) { for (let j = 0; j < samplesPerClass[i].length; ++j) { // fill array with all samples of current class samplesConcat.push(i); } for (let k = 0; k < synClassOffsets[i] * this.blocks[this.layout].amount * this.blocks.horizontal.size * this.blocks.vertical.size - samplesPerClass[i].length; ++k) { // fill up block with complementary class samplesConcat.push(undefined); } } let sclength = samplesConcat.length; for (let i = 0; i < this.maxSamples - sclength; ++i) { // fill up the array with either base class id or undefined (placeholder) samplesConcat.push(undefined); } let circles = this.cgroup.selectAll('circle') .data(samplesConcat); circles.enter() .append('circle') .attr('r', radius) .merge(circles) // merge first!!! .attr('cx', function(d, i) { // y positioning with block grouping return xoffset + (dotspacing + diameter) * Math.floor(i / (nrblocks.v * blocksize.v)) + blockspacing.h * (Math.floor(i / blocksize.h / blocksize.v / nrblocks.h)); }) .attr('cy', function(d, i) { // x positioning with block grouping return iaheight - 2 * yoffset - (dotspacing + diameter) * (i % (nrblocks.v * blocksize.v)); }) .attr('class', function(d) { if (d === undefined) { return 'placeholder'; } return 'sample'; }) .attr('fill', function(d) { let col = 'none'; // let col = '#fff'; if (d !== undefined) { col = colours[colours.length - 1 - d]; // don't forget to invert the colour indices } return col; }) .attr('stroke', function(d) { let col = 'none'; return col; }) .on('mouseover', function(e) { this.setAttribute('r', radius + dotspacing); }) .on('mouseout', function(e) { this.setAttribute('r', radius); }); circles.exit().remove(); } }