123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- class Scatterplot extends Chart {
- constructor(customId,
- selectOnBrushFlag=true,
- pickOnClickFlag=true,
- pickOnFormFlag=true,
- selectOnSelectMenuFlag=false) {
- super(customId);
- this.selectOnBrushFlag = selectOnBrushFlag;
- this.pickOnClickFlag = pickOnClickFlag;
- this.pickOnFormFlag = pickOnFormFlag;
- this.selectOnSelectMenuFlag = selectOnSelectMenuFlag;
- //////////
- // AXES //
- //////////
- this.x = d3.scaleLinear()
- .range([this.margin.left, this.width - this.margin.right]).nice();
- this.y = d3.scaleLinear()
- .range([this.height - this.margin.bottom, this.margin.top]).nice();
- this.xAxis = d3.axisBottom(this.x).ticks(10);
- this.yAxis = d3.axisLeft(this.y).ticks(10);
- this.svg.append("g")
- .attr('id', "axis--x-" + this.customId)
- .attr("transform", "translate(0," + (this.height - this.margin.bottom) + ")")
- .call(this.xAxis);
- this.svg.append("g")
- .attr('id', "axis--y-" + this.customId)
- // offset to right so ticks are not covered
- .attr("transform", "translate(" + (this.margin.left) + ",0)")
- .call(this.yAxis)
- /////////////
- // SPECIAL //
- /////////////
- this.clip = this.svg.append("defs").append("svg:clipPath")
- .attr("id", "clip")
- .append("svg:rect")
- .attr("width", this.width )
- .attr("height", this.height )
- .attr("x", 0)
- .attr("y", 0);
- this.scatter = this.svg.append("g")
- .attr("id", "scatter")
- .attr("clip-path", "url(#clip)");
- // prevent scrolling on body when brushing on chart
- document.getElementById("chart-"+this.customId)
- .addEventListener('touchmove', function(e) {e.preventDefault(); }, false);
- this.brush = d3.brush()
- .extent([[0, 0], [this.width, this.height]])
- .on("end", () => {
- let s = d3.event.selection;
- if (!s) {
- // if double-click, reset axes
- if (!this.idleTimeout) {
- return this.idleTimeout = setTimeout(
- () => {this.idleTimeout = null; },
- this.idleDelay);
- }
- this.x.domain(d3.extent(this.data, (d) => { return d.x; })).nice();
- this.y.domain(d3.extent(this.data, (d) => { return d.y; })).nice();
- } else {
- if (this.selectOnBrushFlag) {
- // color selection, do before zoom changes range of chart
- this.scatter.selectAll("circle").classed("dot-selected", (d) => {
- return isBrushed(s, this.x(d.x), this.y(d.y))
- });
- // get list of sn of selected comics, selection logic
- let brushSelection = [];
- this.scatter.selectAll(".dot-selected")
- .each( (d) => { brushSelection.push(d.sn); });
- generalSelect(brushSelection);
- }
- // adjust axes to selected data
- this.x.domain([ this.x.invert(s[0][0]), this.x.invert(s[1][0]) ]);
- this.y.domain([ this.y.invert(s[1][1]), this.y.invert(s[0][1]) ]);
- this.scatter.select(".brush").call(this.brush.move, null);
- }
- // zoom
- let tr = this.scatter.transition().duration(750);
- this.svg.select("#axis--x-" + this.customId).transition(tr).call(this.xAxis);
- this.svg.select("#axis--y-" + this.customId).transition(tr).call(this.yAxis);
- this.scatter.selectAll("circle").transition(tr)
- .attr("cx", (d) => { return this.x(d.x); })
- .attr("cy", (d) => { return this.y(d.y); });
- });
- this.idleTimeout = null;
- this.idleDelay = 350;
- // on mouseover on scatter's dots, display comic title name
- this.tooltip = d3.select("#chart-" + this.customId)
- .append("div")
- .attr("class", "tooltip")
- .style("opacity", 0);
- if (this.pickOnFormFlag) {
- // attach listener
- this.form = d3.select("#form-"+ this.customId +"-picked")
- .on("change", (d, i, nodes) => {
- let inputData = d3.select(nodes[i]).property('value');
- // clear previously picked point
- this.scatter.select(".dot-picked").classed("dot-picked", false);
- // pick new point
- let pickedPoint = d3.selectAll("circle")
- .filter((d) => { return d.sn == inputData })
- .classed("dot-picked", true)
- .datum();
- // update dependant values
- generalPick(pickedPoint.title, pickedPoint.altText, pickedPoint.imageUrl, inputData)
- });
- }
- if (this.selectOnSelectMenuFlag) {
- d3.select('#select-featureDistribution')
- .on("change", (d, i, nodes) => {
- // put text on div
- let text = Array.from(nodes[i].querySelectorAll("option:checked"), e=>e.text);
- $('#selected-featureDistribution').text(text.join(", "));
- // send values (feature idx) to backend
- let values = Array.from(nodes[i].querySelectorAll("option:checked"), e=>e.value);
- requestFeatureDistribution(values);
- });
- }
- }
- updateAndDraw(chartData) {
- this.data = chartData;
- this.draw();
- }
- draw() {
- // first set domain based off of data domain
- this.x.domain(d3.extent(this.data, (d) => { return d.x; })).nice()
- this.y.domain(d3.extent(this.data, (d) => { return d.y; })).nice()
- // append brush before points so tooltips work
- this.scatter.append("g")
- .attr("class", "brush")
- .call(this.brush);
- // append points
- let dots = this.scatter.selectAll(".dot-basic")
- .data(this.data)
- .enter().append("circle")
- .attr("class", "dot-basic")
- .attr("r", 4)
- .attr("cx", (d) => { return this.x(d.x); })
- .attr("cy", (d) => { return this.y(d.y); })
- .attr('sn', (d) => { return d.sn })
- .on("mouseover", (d, i, nodes) => {
- d3.select(nodes[i]).classed("dot-hovered", true);
- this.tooltip.transition().duration(200).style("opacity", .9);
- this.tooltip.html(d.title)
- .style("left", d3.mouse(nodes[i])[0]+"px")
- .style("top", d3.mouse(nodes[i])[1]+"px")
- })
- .on("mouseleave", (d, i, nodes) => {
- d3.select(nodes[i]).classed("dot-hovered", false);
- this.tooltip.transition().duration(500).style("opacity", 0)
- });
- if (this.pickOnClickFlag) {
- dots.on("click", (d, i, nodes) => {
- // clear previously picked point
- d3.selectAll(".dot-picked").classed("dot-picked", false);
- // pick new point
- d3.select(nodes[i])
- .classed("dot-picked", true);
- // update dependant values
- generalPick(d.title, d.altText, d.imageUrl, d.sn)
- });
- }
- }
- }
- // helper function for brushing
- function isBrushed(brushCoords, cx, cy) {
- let x0 = brushCoords[0][0],
- x1 = brushCoords[1][0],
- y0 = brushCoords[0][1],
- y1 = brushCoords[1][1];
- // This return TRUE or FALSE depending on if the points is in the selected area
- return x0 <= cx && cx <= x1 && y0 <= cy && cy <= y1;
- }
|