contour_plot.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import {default as bisect} from "./bisect";
  2. import {default as d3_contour} from "./d3_geom_contour";
  3. export function isoline(f, value, xScale, yScale) {
  4. var xRange = xScale.range(), yRange = yScale.range();
  5. return function(x, y) {
  6. if ((x < xRange[0]) || (x > xRange[1]) ||
  7. (y < yRange[0]) || (y > yRange[1])) return false;
  8. return f(xScale.invert(x), yScale.invert(y)) < value;
  9. };
  10. }
  11. export function smoothPoints(f, points, level, xScale, yScale) {
  12. var xRange = xScale.range(), yRange = yScale.range();
  13. var ySmooth = function(y) {
  14. return f(xScale.invert(x), yScale.invert(y)) - level;
  15. };
  16. var xSmooth = function(x) {
  17. return f(xScale.invert(x), yScale.invert(y)) - level;
  18. };
  19. for (var k = 0; k < points.length; ++k) {
  20. var point = points[k],
  21. x = point[0], y = point[1];
  22. if ((x <= xRange[0]) || (x >= xRange[1]) ||
  23. (y <= yRange[0]) || (y >= yRange[1])) continue;
  24. var currentSmooth = ySmooth(y);
  25. var p = {'maxIterations' : 9};
  26. for (var delta = 0.5; delta <= 3; delta += 0.5) {
  27. if (ySmooth(y - delta) * currentSmooth < 0) {
  28. y = bisect(ySmooth, y, y - delta, p);
  29. } else if (xSmooth(x - delta) * currentSmooth < 0) {
  30. x = bisect(xSmooth, x, x - delta, p);
  31. } else if (ySmooth(y + delta) * currentSmooth < 0) {
  32. y = bisect(ySmooth, y, y + delta, p);
  33. } else if (xSmooth(x + delta) * currentSmooth < 0) {
  34. x = bisect(xSmooth, x, x + delta, p);
  35. } else {
  36. continue;
  37. }
  38. break;
  39. }
  40. point[0] = x;
  41. point[1] = y;
  42. }
  43. }
  44. export function getLogLevels(f, xScale, yScale, count) {
  45. var xRange = xScale.range(), yRange = yScale.range();
  46. // figure out min/max values by sampling pointson a grid
  47. var maxValue, minValue, value;
  48. maxValue = minValue = f(xScale.invert(xRange[0]), yScale.invert(yRange[0]));
  49. for (var y = yRange[0]; y < yRange[1]+1; ++y) {
  50. for (var x = xRange[0]; x < xRange[1]+1; ++x) {
  51. value = f(xScale.invert(x),yScale.invert(y));
  52. minValue = Math.min(value, minValue);
  53. maxValue = Math.max(value, maxValue);
  54. }
  55. }
  56. // lets get contour lines on a log scale, keeping
  57. // values on an integer scale (if possible)
  58. var levels = [];
  59. var logRange = Math.log(maxValue - Math.floor(minValue));
  60. var base = Math.ceil(Math.exp(logRange / (count))),
  61. upper = Math.pow(base, Math.ceil(logRange / Math.log(base)));
  62. for (var i = 0; i < count; ++i) {
  63. var current = Math.floor(minValue) + upper;
  64. if (current < minValue) {
  65. break;
  66. }
  67. levels.push(current);
  68. upper /= base;
  69. }
  70. return levels;
  71. }
  72. export function getStartingPoint(lineFunc, x, y) {
  73. x = Math.floor(x);
  74. y = Math.floor(y);
  75. var j = 0;
  76. while (true) {
  77. j += 1;
  78. if (!lineFunc(x+j, y)) {
  79. return [x+j, y];
  80. }
  81. if (!lineFunc(x, y+j)) {
  82. return [x, y+j];
  83. }
  84. }
  85. }
  86. export function getContours(f, xScale, yScale, count, minima) {
  87. // figure out even distribution in log space of values
  88. var levels = getLogLevels(f, xScale, yScale, count);
  89. // use marching squares algo from d3.geom.contour to build up a series of paths
  90. var ret = [];
  91. for (var i = 0; i < levels.length; ++i) {
  92. var level = levels[i];
  93. var lineFunc = isoline(f, level, xScale, yScale);
  94. var points= [];
  95. if (minima) {
  96. var initialPoints = [];
  97. for (var m = 0; m < minima.length; ++m) {
  98. var initial = getStartingPoint(lineFunc, xScale(minima[m].x), yScale(minima[m].y));
  99. var current = d3_contour(lineFunc, initial);
  100. // don't add points if already seen
  101. var duplicate = false;
  102. for (var j = 0 ; j < current.length; ++j) {
  103. var point = current[j];
  104. for (var k = 0; k < initialPoints.length; ++k) {
  105. var other = initialPoints[k];
  106. if ((point[0] == other[0]) &&
  107. (point[1] == other[1])) {
  108. duplicate = true;
  109. break;
  110. }
  111. }
  112. if (duplicate) break;
  113. }
  114. if (duplicate) continue;
  115. initialPoints.push(initial);
  116. smoothPoints(f, current, level, xScale, yScale);
  117. if (points.length) points.push(null);
  118. points = points.concat(current);
  119. }
  120. } else {
  121. points = d3_contour(lineFunc);
  122. smoothPoints(f, points, level, xScale, yScale);
  123. }
  124. ret.push(points);
  125. }
  126. // return the contours
  127. return {'paths': ret, 'levels': levels};
  128. }
  129. export function ContourPlot() {
  130. var drawAxis = false,
  131. f = function (x, y) { return (1 - x) * (1 - x) + 100 * (y - x * x) * ( y - x * x); },
  132. yDomain = [3, -3],
  133. xDomain = [-2, 2],
  134. minima = null,
  135. contourCount = 14,
  136. colourScale = d3.scaleLinear().domain([0, contourCount]).range(["white", d3.schemeCategory10[0]]);
  137. // todo: resolution independent (sample say 200x200)
  138. // todo: handle function with multiple local minima
  139. function chart(selection) {
  140. var width = selection.nodes()[0].offsetWidth,
  141. height = width * 0.75,
  142. padding = (drawAxis) ? 24 : 0,
  143. yScale = d3.scaleLinear()
  144. .range([padding, height - padding])
  145. .domain(yDomain),
  146. xScale = d3.scaleLinear()
  147. .range([padding, width - padding])
  148. .domain(xDomain);
  149. // create tooltip if doesn't exist
  150. d3.select("body").selectAll(".contour_tooltip").data([0]).enter()
  151. .append("div")
  152. .attr("class", "contour_tooltip")
  153. .style("font-size", "12px")
  154. .style("position", "absolute")
  155. .style("text-align", "center")
  156. .style("width", "128px")
  157. .style("height", "32px")
  158. .style("background", "#333")
  159. .style("color", "#ddd")
  160. .style("padding", "0px")
  161. .style("border", "0px")
  162. .style("border-radius", "8px")
  163. .style("opacity", "0");
  164. var tooltip = d3.selectAll(".contour_tooltip");
  165. // create the svg element if it doesn't already exist
  166. selection.selectAll("svg").data([0]).enter().append("svg");
  167. var svg = selection.selectAll("svg").data([0]);
  168. svg.attr("width", width)
  169. .attr("height", height)
  170. .on("mouseover", function() {
  171. tooltip.transition().duration(400).style("opacity", 0.9);
  172. tooltip.style("z-index", "");
  173. })
  174. .on("mousemove", function() {
  175. var point = d3.mouse(this),
  176. x = xScale.invert(point[0]),
  177. y = yScale.invert(point[1]),
  178. fx = f(x, y);
  179. tooltip.style("left", (d3.event.pageX) + "px")
  180. .style("top", (d3.event.pageY - 44) + "px");
  181. tooltip.html("x = " + x.toFixed(2) + " y = " + y.toFixed(2) + "<br>f(x,y) = " + fx.toFixed(2) );
  182. })
  183. .on("mouseout", function() {
  184. tooltip.transition().duration(400).style("opacity", 0);
  185. tooltip.style("z-index", -1);
  186. });
  187. var contours = getContours(f, xScale, yScale, contourCount, minima);
  188. var paths = contours.paths,
  189. levels = contours.levels;
  190. var line = d3.line()
  191. .x(function(d) { return d[0]; })
  192. .y(function(d) { return d[1]; })
  193. .curve(d3.curveLinearClosed)
  194. .defined(function(d) { return d; });
  195. var pathGroup = svg.append("g");
  196. pathGroup.selectAll("path").data(paths).enter()
  197. .append("path")
  198. .attr("d", line)
  199. .style("fill", function(d, i) { return colourScale(i); })
  200. .style("stroke-width", 1.5)
  201. .style("stroke", "white")
  202. .on("mouseover", function() {
  203. d3.select(this).style("stroke-width", "4");
  204. })
  205. .on("mouseout", function() {
  206. d3.select(this).style("stroke-width", "1.5");
  207. });
  208. // draw axii
  209. if (drawAxis) {
  210. var xAxis = d3.axisBottom().scale(xScale),
  211. yAxis = d3.axisLeft().scale(yScale);
  212. svg.append("g")
  213. .attr("class", "axis")
  214. .attr("transform", "translate(0," + (height - 1.0 * padding) + ")")
  215. .call(xAxis);
  216. svg.append("g")
  217. .attr("class", "axis")
  218. .attr("transform", "translate(" + (padding) + ",0)")
  219. .call(yAxis);
  220. }
  221. return {'xScale' : xScale, 'yScale' : yScale, 'svg' : svg};
  222. }
  223. chart.drawAxis = function(_) {
  224. if (!arguments.length) return drawAxis;
  225. drawAxis = _;
  226. return chart;
  227. };
  228. chart.xDomain = function(_) {
  229. if (!arguments.length) return xDomain;
  230. xDomain = _;
  231. return chart;
  232. };
  233. chart.yDomain = function(_) {
  234. if (!arguments.length) return yDomain;
  235. yDomain = _;
  236. return chart;
  237. };
  238. chart.colourScale = function(_) {
  239. if (!arguments.length) return colourScale;
  240. colourScale = _;
  241. return chart;
  242. };
  243. chart.contourCount = function(_) {
  244. if (!arguments.length) return contourCount;
  245. contourCount = _;
  246. return chart;
  247. };
  248. chart.minima = function(_) {
  249. if (!arguments.length) return minima;
  250. minima = _;
  251. return chart;
  252. };
  253. chart.f = function(_) {
  254. if (!arguments.length) return f;
  255. f = _;
  256. return chart;
  257. };
  258. return chart;
  259. }