scatterplot.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. class Scatterplot extends Chart {
  2. constructor(customId,
  3. selectOnBrushFlag=true,
  4. pickOnClickFlag=true,
  5. pickOnFormFlag=true,
  6. selectOnSelectMenuFlag=false) {
  7. super(customId);
  8. this.selectOnBrushFlag = selectOnBrushFlag;
  9. this.pickOnClickFlag = pickOnClickFlag;
  10. this.pickOnFormFlag = pickOnFormFlag;
  11. this.selectOnSelectMenuFlag = selectOnSelectMenuFlag;
  12. //////////
  13. // AXES //
  14. //////////
  15. this.x = d3.scaleLinear()
  16. .range([this.margin.left, this.width - this.margin.right]).nice();
  17. this.y = d3.scaleLinear()
  18. .range([this.height - this.margin.bottom, this.margin.top]).nice();
  19. this.xAxis = d3.axisBottom(this.x).ticks(10);
  20. this.yAxis = d3.axisLeft(this.y).ticks(10);
  21. this.svg.append("g")
  22. .attr('id', "axis--x-" + this.customId)
  23. .attr("transform", "translate(0," + (this.height - this.margin.bottom) + ")")
  24. .call(this.xAxis);
  25. this.svg.append("g")
  26. .attr('id', "axis--y-" + this.customId)
  27. // offset to right so ticks are not covered
  28. .attr("transform", "translate(" + (this.margin.left) + ",0)")
  29. .call(this.yAxis)
  30. /////////////
  31. // SPECIAL //
  32. /////////////
  33. this.clip = this.svg.append("defs").append("svg:clipPath")
  34. .attr("id", "clip")
  35. .append("svg:rect")
  36. .attr("width", this.width )
  37. .attr("height", this.height )
  38. .attr("x", 0)
  39. .attr("y", 0);
  40. this.scatter = this.svg.append("g")
  41. .attr("id", "scatter")
  42. .attr("clip-path", "url(#clip)");
  43. // prevent scrolling on body when brushing on chart
  44. document.getElementById("chart-"+this.customId)
  45. .addEventListener('touchmove', function(e) {e.preventDefault(); }, false);
  46. this.brush = d3.brush()
  47. .extent([[0, 0], [this.width, this.height]])
  48. .on("end", () => {
  49. let s = d3.event.selection;
  50. if (!s) {
  51. // if double-click, reset axes
  52. if (!this.idleTimeout) {
  53. return this.idleTimeout = setTimeout(
  54. () => {this.idleTimeout = null; },
  55. this.idleDelay);
  56. }
  57. this.x.domain(d3.extent(this.data, (d) => { return d.x; })).nice();
  58. this.y.domain(d3.extent(this.data, (d) => { return d.y; })).nice();
  59. } else {
  60. if (this.selectOnBrushFlag) {
  61. // color selection, do before zoom changes range of chart
  62. this.scatter.selectAll("circle").classed("dot-selected", (d) => {
  63. return isBrushed(s, this.x(d.x), this.y(d.y))
  64. });
  65. // get list of sn of selected comics, selection logic
  66. let brushSelection = [];
  67. this.scatter.selectAll(".dot-selected")
  68. .each( (d) => { brushSelection.push(d.sn); });
  69. generalSelect(brushSelection);
  70. }
  71. // adjust axes to selected data
  72. this.x.domain([ this.x.invert(s[0][0]), this.x.invert(s[1][0]) ]);
  73. this.y.domain([ this.y.invert(s[1][1]), this.y.invert(s[0][1]) ]);
  74. this.scatter.select(".brush").call(this.brush.move, null);
  75. }
  76. // zoom
  77. let tr = this.scatter.transition().duration(750);
  78. this.svg.select("#axis--x-" + this.customId).transition(tr).call(this.xAxis);
  79. this.svg.select("#axis--y-" + this.customId).transition(tr).call(this.yAxis);
  80. this.scatter.selectAll("circle").transition(tr)
  81. .attr("cx", (d) => { return this.x(d.x); })
  82. .attr("cy", (d) => { return this.y(d.y); });
  83. });
  84. this.idleTimeout = null;
  85. this.idleDelay = 350;
  86. // on mouseover on scatter's dots, display comic title name
  87. this.tooltip = d3.select("#chart-" + this.customId)
  88. .append("div")
  89. .attr("class", "tooltip")
  90. .style("opacity", 0);
  91. if (this.pickOnFormFlag) {
  92. // attach listener
  93. this.form = d3.select("#form-"+ this.customId +"-picked")
  94. .on("change", (d, i, nodes) => {
  95. let inputData = d3.select(nodes[i]).property('value');
  96. // clear previously picked point
  97. this.scatter.select(".dot-picked").classed("dot-picked", false);
  98. // pick new point
  99. let pickedPoint = d3.selectAll("circle")
  100. .filter((d) => { return d.sn == inputData })
  101. .classed("dot-picked", true)
  102. .datum();
  103. // update dependant values
  104. generalPick(pickedPoint.title, pickedPoint.altText, pickedPoint.imageUrl, inputData)
  105. });
  106. }
  107. if (this.selectOnSelectMenuFlag) {
  108. d3.select('#select-featureDistribution')
  109. .on("change", (d, i, nodes) => {
  110. // put text on div
  111. let text = Array.from(nodes[i].querySelectorAll("option:checked"), e=>e.text);
  112. $('#selected-featureDistribution').text(text.join(", "));
  113. // send values (feature idx) to backend
  114. let values = Array.from(nodes[i].querySelectorAll("option:checked"), e=>e.value);
  115. requestFeatureDistribution(values);
  116. });
  117. }
  118. }
  119. updateAndDraw(chartData) {
  120. this.data = chartData;
  121. this.draw();
  122. }
  123. draw() {
  124. // first set domain based off of data domain
  125. this.x.domain(d3.extent(this.data, (d) => { return d.x; })).nice()
  126. this.y.domain(d3.extent(this.data, (d) => { return d.y; })).nice()
  127. // append brush before points so tooltips work
  128. this.scatter.append("g")
  129. .attr("class", "brush")
  130. .call(this.brush);
  131. // append points
  132. let dots = this.scatter.selectAll(".dot-basic")
  133. .data(this.data)
  134. .enter().append("circle")
  135. .attr("class", "dot-basic")
  136. .attr("r", 4)
  137. .attr("cx", (d) => { return this.x(d.x); })
  138. .attr("cy", (d) => { return this.y(d.y); })
  139. .attr('sn', (d) => { return d.sn })
  140. .on("mouseover", (d, i, nodes) => {
  141. d3.select(nodes[i]).classed("dot-hovered", true);
  142. this.tooltip.transition().duration(200).style("opacity", .9);
  143. this.tooltip.html(d.title)
  144. .style("left", d3.mouse(nodes[i])[0]+"px")
  145. .style("top", d3.mouse(nodes[i])[1]+"px")
  146. })
  147. .on("mouseleave", (d, i, nodes) => {
  148. d3.select(nodes[i]).classed("dot-hovered", false);
  149. this.tooltip.transition().duration(500).style("opacity", 0)
  150. });
  151. if (this.pickOnClickFlag) {
  152. dots.on("click", (d, i, nodes) => {
  153. // clear previously picked point
  154. d3.selectAll(".dot-picked").classed("dot-picked", false);
  155. // pick new point
  156. d3.select(nodes[i])
  157. .classed("dot-picked", true);
  158. // update dependant values
  159. generalPick(d.title, d.altText, d.imageUrl, d.sn)
  160. });
  161. }
  162. }
  163. }
  164. // helper function for brushing
  165. function isBrushed(brushCoords, cx, cy) {
  166. let x0 = brushCoords[0][0],
  167. x1 = brushCoords[1][0],
  168. y0 = brushCoords[0][1],
  169. y1 = brushCoords[1][1];
  170. // This return TRUE or FALSE depending on if the points is in the selected area
  171. return x0 <= cx && cx <= x1 && y0 <= cy && cy <= y1;
  172. }