| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- import {default as bisect} from "./bisect";
- import {default as d3_contour} from "./d3_geom_contour";
- export function isoline(f, value, xScale, yScale) {
- var xRange = xScale.range(), yRange = yScale.range();
- return function(x, y) {
- if ((x < xRange[0]) || (x > xRange[1]) ||
- (y < yRange[0]) || (y > yRange[1])) return false;
- return f(xScale.invert(x), yScale.invert(y)) < value;
- };
- }
- export function smoothPoints(f, points, level, xScale, yScale) {
- var xRange = xScale.range(), yRange = yScale.range();
- var ySmooth = function(y) {
- return f(xScale.invert(x), yScale.invert(y)) - level;
- };
- var xSmooth = function(x) {
- return f(xScale.invert(x), yScale.invert(y)) - level;
- };
- for (var k = 0; k < points.length; ++k) {
- var point = points[k],
- x = point[0], y = point[1];
- if ((x <= xRange[0]) || (x >= xRange[1]) ||
- (y <= yRange[0]) || (y >= yRange[1])) continue;
- var currentSmooth = ySmooth(y);
- var p = {'maxIterations' : 9};
- for (var delta = 0.5; delta <= 3; delta += 0.5) {
- if (ySmooth(y - delta) * currentSmooth < 0) {
- y = bisect(ySmooth, y, y - delta, p);
- } else if (xSmooth(x - delta) * currentSmooth < 0) {
- x = bisect(xSmooth, x, x - delta, p);
- } else if (ySmooth(y + delta) * currentSmooth < 0) {
- y = bisect(ySmooth, y, y + delta, p);
- } else if (xSmooth(x + delta) * currentSmooth < 0) {
- x = bisect(xSmooth, x, x + delta, p);
- } else {
- continue;
- }
- break;
- }
- point[0] = x;
- point[1] = y;
- }
- }
- export function getLogLevels(f, xScale, yScale, count) {
- var xRange = xScale.range(), yRange = yScale.range();
- // figure out min/max values by sampling pointson a grid
- var maxValue, minValue, value;
- maxValue = minValue = f(xScale.invert(xRange[0]), yScale.invert(yRange[0]));
- for (var y = yRange[0]; y < yRange[1]+1; ++y) {
- for (var x = xRange[0]; x < xRange[1]+1; ++x) {
- value = f(xScale.invert(x),yScale.invert(y));
- minValue = Math.min(value, minValue);
- maxValue = Math.max(value, maxValue);
- }
- }
- // lets get contour lines on a log scale, keeping
- // values on an integer scale (if possible)
- var levels = [];
- var logRange = Math.log(maxValue - Math.floor(minValue));
- var base = Math.ceil(Math.exp(logRange / (count))),
- upper = Math.pow(base, Math.ceil(logRange / Math.log(base)));
- for (var i = 0; i < count; ++i) {
- var current = Math.floor(minValue) + upper;
- if (current < minValue) {
- break;
- }
- levels.push(current);
- upper /= base;
- }
- return levels;
- }
- export function getStartingPoint(lineFunc, x, y) {
- x = Math.floor(x);
- y = Math.floor(y);
- var j = 0;
- while (true) {
- j += 1;
- if (!lineFunc(x+j, y)) {
- return [x+j, y];
- }
- if (!lineFunc(x, y+j)) {
- return [x, y+j];
- }
- }
- }
- export function getContours(f, xScale, yScale, count, minima) {
- // figure out even distribution in log space of values
- var levels = getLogLevels(f, xScale, yScale, count);
- // use marching squares algo from d3.geom.contour to build up a series of paths
- var ret = [];
- for (var i = 0; i < levels.length; ++i) {
- var level = levels[i];
- var lineFunc = isoline(f, level, xScale, yScale);
- var points= [];
- if (minima) {
- var initialPoints = [];
- for (var m = 0; m < minima.length; ++m) {
- var initial = getStartingPoint(lineFunc, xScale(minima[m].x), yScale(minima[m].y));
- var current = d3_contour(lineFunc, initial);
- // don't add points if already seen
- var duplicate = false;
- for (var j = 0 ; j < current.length; ++j) {
- var point = current[j];
- for (var k = 0; k < initialPoints.length; ++k) {
- var other = initialPoints[k];
- if ((point[0] == other[0]) &&
- (point[1] == other[1])) {
- duplicate = true;
- break;
- }
- }
- if (duplicate) break;
- }
- if (duplicate) continue;
- initialPoints.push(initial);
- smoothPoints(f, current, level, xScale, yScale);
- if (points.length) points.push(null);
- points = points.concat(current);
- }
- } else {
- points = d3_contour(lineFunc);
- smoothPoints(f, points, level, xScale, yScale);
- }
- ret.push(points);
- }
- // return the contours
- return {'paths': ret, 'levels': levels};
- }
- export function ContourPlot() {
- var drawAxis = false,
- f = function (x, y) { return (1 - x) * (1 - x) + 100 * (y - x * x) * ( y - x * x); },
- yDomain = [3, -3],
- xDomain = [-2, 2],
- minima = null,
- contourCount = 14,
- colourScale = d3.scaleLinear().domain([0, contourCount]).range(["white", d3.schemeCategory10[0]]);
- // todo: resolution independent (sample say 200x200)
- // todo: handle function with multiple local minima
- function chart(selection) {
- var width = selection.nodes()[0].offsetWidth,
- height = width * 0.75,
- padding = (drawAxis) ? 24 : 0,
- yScale = d3.scaleLinear()
- .range([padding, height - padding])
- .domain(yDomain),
- xScale = d3.scaleLinear()
- .range([padding, width - padding])
- .domain(xDomain);
- // create tooltip if doesn't exist
- d3.select("body").selectAll(".contour_tooltip").data([0]).enter()
- .append("div")
- .attr("class", "contour_tooltip")
- .style("font-size", "12px")
- .style("position", "absolute")
- .style("text-align", "center")
- .style("width", "128px")
- .style("height", "32px")
- .style("background", "#333")
- .style("color", "#ddd")
- .style("padding", "0px")
- .style("border", "0px")
- .style("border-radius", "8px")
- .style("opacity", "0");
- var tooltip = d3.selectAll(".contour_tooltip");
- // create the svg element if it doesn't already exist
- selection.selectAll("svg").data([0]).enter().append("svg");
- var svg = selection.selectAll("svg").data([0]);
- svg.attr("width", width)
- .attr("height", height)
- .on("mouseover", function() {
- tooltip.transition().duration(400).style("opacity", 0.9);
- tooltip.style("z-index", "");
- })
- .on("mousemove", function() {
- var point = d3.mouse(this),
- x = xScale.invert(point[0]),
- y = yScale.invert(point[1]),
- fx = f(x, y);
- tooltip.style("left", (d3.event.pageX) + "px")
- .style("top", (d3.event.pageY - 44) + "px");
- tooltip.html("x = " + x.toFixed(2) + " y = " + y.toFixed(2) + "<br>f(x,y) = " + fx.toFixed(2) );
- })
- .on("mouseout", function() {
- tooltip.transition().duration(400).style("opacity", 0);
- tooltip.style("z-index", -1);
- });
- var contours = getContours(f, xScale, yScale, contourCount, minima);
- var paths = contours.paths,
- levels = contours.levels;
- var line = d3.line()
- .x(function(d) { return d[0]; })
- .y(function(d) { return d[1]; })
- .curve(d3.curveLinearClosed)
- .defined(function(d) { return d; });
- var pathGroup = svg.append("g");
- pathGroup.selectAll("path").data(paths).enter()
- .append("path")
- .attr("d", line)
- .style("fill", function(d, i) { return colourScale(i); })
- .style("stroke-width", 1.5)
- .style("stroke", "white")
- .on("mouseover", function() {
- d3.select(this).style("stroke-width", "4");
- })
- .on("mouseout", function() {
- d3.select(this).style("stroke-width", "1.5");
- });
- // draw axii
- if (drawAxis) {
- var xAxis = d3.axisBottom().scale(xScale),
- yAxis = d3.axisLeft().scale(yScale);
- svg.append("g")
- .attr("class", "axis")
- .attr("transform", "translate(0," + (height - 1.0 * padding) + ")")
- .call(xAxis);
- svg.append("g")
- .attr("class", "axis")
- .attr("transform", "translate(" + (padding) + ",0)")
- .call(yAxis);
- }
- return {'xScale' : xScale, 'yScale' : yScale, 'svg' : svg};
- }
- chart.drawAxis = function(_) {
- if (!arguments.length) return drawAxis;
- drawAxis = _;
- return chart;
- };
- chart.xDomain = function(_) {
- if (!arguments.length) return xDomain;
- xDomain = _;
- return chart;
- };
- chart.yDomain = function(_) {
- if (!arguments.length) return yDomain;
- yDomain = _;
- return chart;
- };
- chart.colourScale = function(_) {
- if (!arguments.length) return colourScale;
- colourScale = _;
- return chart;
- };
- chart.contourCount = function(_) {
- if (!arguments.length) return contourCount;
- contourCount = _;
- return chart;
- };
- chart.minima = function(_) {
- if (!arguments.length) return minima;
- minima = _;
- return chart;
- };
- chart.f = function(_) {
- if (!arguments.length) return f;
- f = _;
- return chart;
- };
- return chart;
- }
|