diagram.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import { nelderMead } from 'fmin';
  2. import { distance, getCenter, intersectionArea } from './circleintersection';
  3. function circleMargin(current, interior, exterior) {
  4. var margin = interior[0].radius - distance(interior[0], current), i, m;
  5. for (i = 1; i < interior.length; ++i) {
  6. m = interior[i].radius - distance(interior[i], current);
  7. if (m <= margin) {
  8. margin = m;
  9. }
  10. }
  11. for (i = 0; i < exterior.length; ++i) {
  12. m = distance(exterior[i], current) - exterior[i].radius;
  13. if (m <= margin) {
  14. margin = m;
  15. }
  16. }
  17. return margin;
  18. }
  19. // compute the center of some circles by maximizing the margin of
  20. // the center point relative to the circles (interior) after subtracting
  21. // nearby circles (exterior)
  22. export function computeTextCentre(interior, exterior) {
  23. // get an initial estimate by sampling around the interior circles
  24. // and taking the point with the biggest margin
  25. var points = [];
  26. var i;
  27. for (i = 0; i < interior.length; ++i) {
  28. var c = interior[i];
  29. points.push({ x: c.x, y: c.y });
  30. points.push({ x: c.x + c.radius / 2, y: c.y });
  31. points.push({ x: c.x - c.radius / 2, y: c.y });
  32. points.push({ x: c.x, y: c.y + c.radius / 2 });
  33. points.push({ x: c.x, y: c.y - c.radius / 2 });
  34. }
  35. var initial = points[0], margin = circleMargin(points[0], interior, exterior);
  36. for (i = 1; i < points.length; ++i) {
  37. var m = circleMargin(points[i], interior, exterior);
  38. if (m >= margin) {
  39. initial = points[i];
  40. margin = m;
  41. }
  42. }
  43. // maximize the margin numerically
  44. var solution = nelderMead(function (p) {
  45. return -1 * circleMargin({ x: p[0], y: p[1] }, interior, exterior);
  46. }, [initial.x, initial.y], { maxIterations: 500, minErrorDelta: 1e-10 }).x;
  47. var ret = { x: solution[0], y: solution[1] };
  48. // check solution, fallback as needed (happens if fully overlapped
  49. // etc)
  50. var valid = true;
  51. for (i = 0; i < interior.length; ++i) {
  52. if (distance(ret, interior[i]) > interior[i].radius) {
  53. valid = false;
  54. break;
  55. }
  56. }
  57. for (i = 0; i < exterior.length; ++i) {
  58. if (distance(ret, exterior[i]) < exterior[i].radius) {
  59. valid = false;
  60. break;
  61. }
  62. }
  63. if (!valid) {
  64. if (interior.length == 1) {
  65. ret = { x: interior[0].x, y: interior[0].y };
  66. }
  67. else {
  68. var areaStats = {};
  69. intersectionArea(interior, areaStats);
  70. if (areaStats.arcs.length === 0) {
  71. ret = { x: 0, y: -1000, disjoint: true };
  72. }
  73. else if (areaStats.arcs.length == 1) {
  74. ret = { x: areaStats.arcs[0].circle.x, y: areaStats.arcs[0].circle.y };
  75. }
  76. else if (exterior.length) {
  77. // try again without other circles
  78. ret = computeTextCentre(interior, []);
  79. }
  80. else {
  81. // take average of all the points in the intersection
  82. // polygon. this should basically never happen
  83. // and has some issues:
  84. // https://github.com/benfred/venn.js/issues/48#issuecomment-146069777
  85. ret = getCenter(areaStats.arcs.map(function (a) {
  86. return a.p1;
  87. }));
  88. }
  89. }
  90. }
  91. return ret;
  92. }
  93. // given a dictionary of {setid : circle}, returns
  94. // a dictionary of setid to list of circles that completely overlap it
  95. function getOverlappingCircles(circles) {
  96. var ret = {}, circleids = [];
  97. for (var circleid in circles) {
  98. circleids.push(circleid);
  99. ret[circleid] = [];
  100. }
  101. for (var i = 0; i < circleids.length; i++) {
  102. var a = circles[circleids[i]];
  103. for (var j = i + 1; j < circleids.length; ++j) {
  104. var b = circles[circleids[j]], d = distance(a, b);
  105. if (d + b.radius <= a.radius + 1e-10) {
  106. ret[circleids[j]].push(circleids[i]);
  107. }
  108. else if (d + a.radius <= b.radius + 1e-10) {
  109. ret[circleids[i]].push(circleids[j]);
  110. }
  111. }
  112. }
  113. return ret;
  114. }
  115. export function computeTextCentres(circles, areas) {
  116. var ret = {}, overlapped = getOverlappingCircles(circles);
  117. for (var i = 0; i < areas.length; ++i) {
  118. var area = areas[i].sets, areaids = {}, exclude = {};
  119. for (var j = 0; j < area.length; ++j) {
  120. areaids[area[j]] = true;
  121. var overlaps = overlapped[area[j]];
  122. // keep track of any circles that overlap this area,
  123. // and don't consider for purposes of computing the text
  124. // centre
  125. for (var k = 0; k < overlaps.length; ++k) {
  126. exclude[overlaps[k]] = true;
  127. }
  128. }
  129. var interior = [], exterior = [];
  130. for (var setid in circles) {
  131. if (setid in areaids) {
  132. interior.push(circles[setid]);
  133. }
  134. else if (!(setid in exclude)) {
  135. exterior.push(circles[setid]);
  136. }
  137. }
  138. var centre = computeTextCentre(interior, exterior);
  139. ret[area] = centre;
  140. if (centre.disjoint && areas[i].size > 0) {
  141. console.log('WARNING: area ' + area + ' not represented on screen');
  142. }
  143. }
  144. return ret;
  145. }
  146. /**
  147. * 根据圆心(x, y) 半径 r 返回圆的绘制 path
  148. * @param x 圆心点 x
  149. * @param y 圆心点 y
  150. * @param r 圆的半径
  151. * @returns 圆的 path
  152. */
  153. export function circlePath(x, y, r) {
  154. var ret = [];
  155. // ret.push('\nM', x, y);
  156. // ret.push('\nm', -r, 0);
  157. // ret.push('\na', r, r, 0, 1, 0, r * 2, 0);
  158. // ret.push('\na', r, r, 0, 1, 0, -r * 2, 0);
  159. var x0 = x - r;
  160. var y0 = y;
  161. ret.push('M', x0, y0);
  162. ret.push('A', r, r, 0, 1, 0, x0 + 2 * r, y0);
  163. ret.push('A', r, r, 0, 1, 0, x0, y0);
  164. return ret.join(' ');
  165. }
  166. // inverse of the circlePath function, returns a circle object from an svg path
  167. export function circleFromPath(path) {
  168. var tokens = path.split(' ');
  169. return { x: parseFloat(tokens[1]), y: parseFloat(tokens[2]), radius: -parseFloat(tokens[4]) };
  170. }
  171. /** returns a svg path of the intersection area of a bunch of circles */
  172. export function intersectionAreaPath(circles) {
  173. var stats = {};
  174. intersectionArea(circles, stats);
  175. var arcs = stats.arcs;
  176. if (arcs.length === 0) {
  177. return 'M 0 0';
  178. }
  179. else if (arcs.length == 1) {
  180. var circle = arcs[0].circle;
  181. return circlePath(circle.x, circle.y, circle.radius);
  182. }
  183. else {
  184. // draw path around arcs
  185. var ret = ['\nM', arcs[0].p2.x, arcs[0].p2.y];
  186. for (var i = 0; i < arcs.length; ++i) {
  187. var arc = arcs[i], r = arc.circle.radius, wide = arc.width > r;
  188. ret.push('\nA', r, r, 0, wide ? 1 : 0, 1, arc.p1.x, arc.p1.y);
  189. }
  190. return ret.join(' ');
  191. }
  192. }
  193. //# sourceMappingURL=diagram.js.map