line-graph.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. const d3 = require('../d3.js');
  2. const margin = { top: 10, right: 10, bottom: 10, left: 10 }
  3. const xAxisHeight = 20;
  4. const yAxisWidth = 20;
  5. const legendItemRectSize = 24;
  6. const legendItemTextMargin = 4;
  7. function iseq(array) {
  8. const seq = [];
  9. for (let i = 0; i < array.length; i++) seq.push(i);
  10. return seq;
  11. }
  12. class LineGraph {
  13. constructor({ container, height, colors=d3.schemePaired }) {
  14. this._container = d3.select(container);
  15. this._height = height;
  16. this._colors = colors;
  17. this._innerHeight = height - margin.top - margin.bottom - xAxisHeight;
  18. this._data = null;
  19. this._svg = this._container.append('svg')
  20. .attr('height', this._height)
  21. this._svg.node().setAttribute('xmlns', 'http://www.w3.org/2000/svg')
  22. this._svg.node().setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink')
  23. this._legend = this._container.append('div')
  24. .classed('legend', true)
  25. .style('margin-left', `${margin.left + yAxisWidth}px`)
  26. .style('margin-right', `${margin.right}px`);
  27. this._clipRect = this._svg
  28. .append('defs').append('clipPath')
  29. .attr('id', 'ar-line-graph-clip')
  30. .append('rect')
  31. .attr('height', this._innerHeight);
  32. this._graph = this._svg.append('g')
  33. .attr('transform',
  34. 'translate(' + (margin.left + yAxisWidth) + ',' + margin.top + ')');
  35. // Create background
  36. this._background = this._graph.append("rect")
  37. .attr("class", "background")
  38. .attr("height", this._innerHeight);
  39. // define scales
  40. this._colorScale = d3.scaleOrdinal();
  41. this._xScale = d3.scaleLinear()
  42. .domain([1, 0]);
  43. this._yScale = d3.scaleLinear()
  44. .domain([1, 0])
  45. .range([this._innerHeight, 0]);
  46. // create grid
  47. this._xGrid = d3.axisBottom(this._xScale)
  48. .ticks(8)
  49. .tickSize(-this._height);
  50. this._xGridElement = this._graph.append("g")
  51. .attr("class", "grid")
  52. .attr("transform", "translate(0," + this._innerHeight + ")");
  53. // create grid
  54. this._yGrid = d3.axisLeft(this._yScale)
  55. .ticks(8);
  56. this._yGridElement = this._graph.append("g")
  57. .attr("class", "grid");
  58. // define axis
  59. this._xAxis = d3.axisBottom(this._xScale)
  60. .ticks(4);
  61. this._xAxisElement = this._graph.append('g')
  62. .attr("class", "axis")
  63. .attr('transform', 'translate(0,' + this._innerHeight + ')');
  64. this._yAxis = d3.axisLeft(this._yScale)
  65. .ticks(4);
  66. this._yAxisElement = this._graph.append('g')
  67. .attr("class", "axis");
  68. this._yAxisTitle = this._graph.append('g')
  69. .attr("class", "axis-title");
  70. this._lines = this._graph.append("g")
  71. .classed('lines', true);
  72. this._lineDrawer = d3.line()
  73. .curve(d3.curveBasis)
  74. .x((d) => this._xScale(d[0]))
  75. .y((d) => this._yScale(d[1]));
  76. }
  77. setData(data) {
  78. this._data = data;
  79. }
  80. draw() {
  81. this._colorScale
  82. .domain(iseq(this._data.lines.length))
  83. .range(this._colors.slice(0, this._data.lines.length));
  84. this._yScale.domain(this._data.yDomain);
  85. this._xScale.domain(this._data.xDomain);
  86. this._drawLegend();
  87. this.resize();
  88. }
  89. _drawLegend() {
  90. const legendSelectPhase1 = this._legend.selectAll('svg.legend-item')
  91. .data(this._data.lines);
  92. legendSelectPhase1.select('.legend-color')
  93. .style('fill', (d, i) => this._colorScale(i));
  94. legendSelectPhase1.select('.legend-text')
  95. .text((d) => d.description);
  96. const lengedGroupEnter = legendSelectPhase1
  97. .enter().append('svg')
  98. .attr('height', legendItemRectSize)
  99. .classed('legend-item', true);
  100. lengedGroupEnter.append('rect')
  101. .classed('legend-background', true)
  102. .attr('width', legendItemRectSize)
  103. .attr('height', legendItemRectSize);
  104. lengedGroupEnter.append('text')
  105. .classed('legend-color', true)
  106. .style('fill', (d, i) => this._colorScale(i))
  107. .attr('x', legendItemRectSize / 2)
  108. .attr('y', legendItemRectSize / 2)
  109. .text('–');
  110. lengedGroupEnter.append('text')
  111. .classed('legend-text', true)
  112. .attr('x', legendItemRectSize + legendItemTextMargin)
  113. .attr('y', legendItemRectSize / 2)
  114. .text((d) => d.description);
  115. legendSelectPhase1.exit().remove();
  116. const legendSelectPhase2 = this._legend.selectAll('svg.legend-item')
  117. .data(this._colorScale.domain());
  118. legendSelectPhase2
  119. .attr('width', function () {
  120. return (
  121. legendItemRectSize +
  122. legendItemTextMargin +
  123. this.querySelector('.legend-text').getComputedTextLength()
  124. );
  125. })
  126. this._legend
  127. .style('grid-template-columns', function () {
  128. const maxWidth = Math.max(
  129. ...Array.from(this.querySelectorAll('svg.legend-item'))
  130. .map((elem) => Math.ceil(parseInt(elem.getAttribute('width'))))
  131. );
  132. return `repeat(auto-fit, minmax(${Math.ceil(maxWidth)}px, 1fr)`;
  133. });
  134. }
  135. resize() {
  136. const width = this._container.node().clientWidth;
  137. const innerWidth = width - (margin.left + margin.right + yAxisWidth);
  138. this._svg
  139. .attr('width', width);
  140. this._clipRect
  141. .attr('width', innerWidth);
  142. // set background
  143. this._background
  144. .attr("width", innerWidth);
  145. // set the ranges
  146. this._xScale.range([0, innerWidth]);
  147. // update grid
  148. this._yGrid.tickSize(-innerWidth);
  149. const yTicksMajors = this._yScale.ticks(4);
  150. this._yGridElement
  151. .call(this._yGrid);
  152. this._yGridElement
  153. .selectAll('.tick')
  154. .classed('minor', (d) => !yTicksMajors.includes(d));
  155. const xTicksMajors = [0, 6, 12, 18, 24];
  156. this._xGridElement.call(this._xGrid);
  157. this._xGridElement
  158. .selectAll('.tick')
  159. .classed('minor', (d) => !xTicksMajors.includes(d));
  160. // update axis
  161. this._xAxisElement.call(this._xAxis);
  162. this._yAxisElement
  163. .call(this._yAxis);
  164. // draw lines
  165. const curveSelect = this._lines.selectAll('path.mathline')
  166. .data(this._data.lines);
  167. curveSelect
  168. .attr('d', (d) => this._lineDrawer(d.line))
  169. .style("stroke", (d, i) => this._colorScale(i));
  170. curveSelect.enter().append("path")
  171. .attr('class', 'mathline')
  172. .attr('clip-path', 'url(#ar-line-graph-clip)')
  173. .attr('d', (d) => this._lineDrawer(d.line))
  174. .style("stroke", (d, i) => this._colorScale(i));
  175. curveSelect.exit().remove();
  176. }
  177. }
  178. module.exports = LineGraph;