venn.src.js 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623
  1. /**
  2. * @license Highcharts JS v7.0.2 (2019-01-17)
  3. *
  4. * (c) 2017-2019 Highsoft AS
  5. * Authors: Jon Arild Nygard
  6. *
  7. * License: www.highcharts.com/license
  8. */
  9. 'use strict';
  10. (function (factory) {
  11. if (typeof module === 'object' && module.exports) {
  12. factory['default'] = factory;
  13. module.exports = factory;
  14. } else if (typeof define === 'function' && define.amd) {
  15. define(function () {
  16. return factory;
  17. });
  18. } else {
  19. factory(typeof Highcharts !== 'undefined' ? Highcharts : undefined);
  20. }
  21. }(function (Highcharts) {
  22. var draw = (function () {
  23. var isFn = function (x) {
  24. return typeof x === 'function';
  25. };
  26. /**
  27. * Handles the drawing of a point.
  28. *
  29. * @private
  30. * @function draw
  31. *
  32. * @param {object} params
  33. * Parameters.
  34. *
  35. * @todo
  36. * - add type checking.
  37. */
  38. var draw = function draw(params) {
  39. var point = this,
  40. graphic = point.graphic,
  41. animatableAttribs = params.animatableAttribs,
  42. onComplete = params.onComplete,
  43. css = params.css,
  44. renderer = params.renderer;
  45. if (point.shouldDraw()) {
  46. if (!graphic) {
  47. point.graphic = graphic =
  48. renderer[params.shapeType](params.shapeArgs).add(params.group);
  49. }
  50. graphic
  51. .css(css)
  52. .attr(params.attribs)
  53. .animate(
  54. animatableAttribs,
  55. params.isNew ? false : undefined,
  56. onComplete
  57. );
  58. } else if (graphic) {
  59. graphic.animate(animatableAttribs, undefined, function () {
  60. point.graphic = graphic = graphic.destroy();
  61. if (isFn(onComplete)) {
  62. onComplete();
  63. }
  64. });
  65. }
  66. if (graphic) {
  67. graphic.addClass(point.getClassName(), true);
  68. }
  69. };
  70. return draw;
  71. }());
  72. var geometry = (function () {
  73. /**
  74. * Calculates the center between a list of points.
  75. *
  76. * @param {array} points A list of points to calculate the center of.
  77. */
  78. var getCenterOfPoints = function getCenterOfPoints(points) {
  79. var sum = points.reduce(function (sum, point) {
  80. sum.x += point.x;
  81. sum.y += point.y;
  82. return sum;
  83. }, { x: 0, y: 0 });
  84. return {
  85. x: sum.x / points.length,
  86. y: sum.y / points.length
  87. };
  88. };
  89. /**
  90. * Calculates the distance between two points based on their x and y
  91. * coordinates.
  92. *
  93. * @param {object} p1 The x and y coordinates of the first point.
  94. * @param {object} p2 The x and y coordinates of the second point.
  95. * @returns {number} Returns the distance between the points.
  96. */
  97. var getDistanceBetweenPoints = function getDistanceBetweenPoints(p1, p2) {
  98. return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
  99. };
  100. /**
  101. * Calculates the angle between two points.
  102. *
  103. * TODO: add unit tests.
  104. *
  105. * @param {object} p1 The first point.
  106. * @param {object} p2 The second point.
  107. * @returns {number} Returns the angle in radians.
  108. */
  109. var getAngleBetweenPoints = function getAngleBetweenPoints(p1, p2) {
  110. return Math.atan2(p2.x - p1.x, p2.y - p1.y);
  111. };
  112. var geometry = {
  113. getAngleBetweenPoints: getAngleBetweenPoints,
  114. getCenterOfPoints: getCenterOfPoints,
  115. getDistanceBetweenPoints: getDistanceBetweenPoints
  116. };
  117. return geometry;
  118. }());
  119. var geometryCircles = (function (geometry) {
  120. var getAngleBetweenPoints = geometry.getAngleBetweenPoints,
  121. getCenterOfPoints = geometry.getCenterOfPoints,
  122. getDistanceBetweenPoints = geometry.getDistanceBetweenPoints;
  123. var round = function round(x, decimals) {
  124. var a = Math.pow(10, decimals);
  125. return Math.round(x * a) / a;
  126. };
  127. /**
  128. * Calculates the area of a circular segment based on the radius of the circle
  129. * and the height of the segment.
  130. * See http://mathworld.wolfram.com/CircularSegment.html
  131. *
  132. * @param {number} r The radius of the circle.
  133. * @param {number} h The height of the circular segment.
  134. * @returns {number} Returns the area of the circular segment.
  135. */
  136. var getCircularSegmentArea = function getCircularSegmentArea(r, h) {
  137. return r * r * Math.acos(1 - h / r) - (r - h) * Math.sqrt(h * (2 * r - h));
  138. };
  139. /**
  140. * Calculates the area of overlap between two circles based on their radiuses
  141. * and the distance between them.
  142. * See http://mathworld.wolfram.com/Circle-CircleIntersection.html
  143. *
  144. * @param {number} r1 Radius of the first circle.
  145. * @param {number} r2 Radius of the second circle.
  146. * @param {number} d The distance between the two circles.
  147. * @returns {number} Returns the area of overlap between the two circles.
  148. */
  149. var getOverlapBetweenCircles =
  150. function getOverlapBetweenCircles(r1, r2, d) {
  151. var overlap = 0;
  152. // If the distance is larger than the sum of the radiuses then the circles
  153. // does not overlap.
  154. if (d < r1 + r2) {
  155. var r1Square = r1 * r1,
  156. r2Square = r2 * r2;
  157. if (d <= Math.abs(r2 - r1)) {
  158. // If the circles are completely overlapping, then the overlap
  159. // equals the area of the smallest circle.
  160. overlap = Math.PI * Math.min(r1Square, r2Square);
  161. } else {
  162. // Height of first triangle segment.
  163. var d1 = (r1Square - r2Square + d * d) / (2 * d),
  164. // Height of second triangle segment.
  165. d2 = d - d1;
  166. overlap = (
  167. getCircularSegmentArea(r1, r1 - d1) +
  168. getCircularSegmentArea(r2, r2 - d2)
  169. );
  170. }
  171. // Round the result to two decimals.
  172. overlap = round(overlap, 14);
  173. }
  174. return overlap;
  175. };
  176. /**
  177. * Calculates the intersection points of two circles.
  178. *
  179. * NOTE: does not handle floating errors well.
  180. *
  181. * @param {object} c1 The first circle.s
  182. * @param {object} c2 The second sircle.
  183. * @returns {array} Returns the resulting intersection points.
  184. */
  185. var getCircleCircleIntersection =
  186. function getCircleCircleIntersection(c1, c2) {
  187. var d = getDistanceBetweenPoints(c1, c2),
  188. r1 = c1.r,
  189. r2 = c2.r,
  190. points = [];
  191. if (d < r1 + r2 && d > Math.abs(r1 - r2)) {
  192. // If the circles are overlapping, but not completely overlapping, then
  193. // it exists intersecting points.
  194. var r1Square = r1 * r1,
  195. r2Square = r2 * r2,
  196. // d^2 - r^2 + R^2 / 2d
  197. x = (r1Square - r2Square + d * d) / (2 * d),
  198. // y^2 = R^2 - x^2
  199. y = Math.sqrt(r1Square - x * x),
  200. x1 = c1.x,
  201. x2 = c2.x,
  202. y1 = c1.y,
  203. y2 = c2.y,
  204. x0 = x1 + x * (x2 - x1) / d,
  205. y0 = y1 + x * (y2 - y1) / d,
  206. rx = -(y2 - y1) * (y / d),
  207. ry = -(x2 - x1) * (y / d);
  208. points = [
  209. { x: round(x0 + rx, 14), y: round(y0 - ry, 14) },
  210. { x: round(x0 - rx, 14), y: round(y0 + ry, 14) }
  211. ];
  212. }
  213. return points;
  214. };
  215. /**
  216. * Calculates all the intersection points for between a list of circles.
  217. *
  218. * @param {array} circles The circles to calculate the points from.
  219. * @returns {array} Returns a list of intersection points.
  220. */
  221. var getCirclesIntersectionPoints = function getIntersectionPoints(circles) {
  222. return circles.reduce(function (points, c1, i, arr) {
  223. var additional = arr.slice(i + 1)
  224. .reduce(function (points, c2, j) {
  225. var indexes = [i, j + i + 1];
  226. return points.concat(
  227. getCircleCircleIntersection(c1, c2)
  228. .map(function (p) {
  229. p.indexes = indexes;
  230. return p;
  231. })
  232. );
  233. }, []);
  234. return points.concat(additional);
  235. }, []);
  236. };
  237. /**
  238. * Tests wether a point lies within a given circle.
  239. *
  240. * @param {object} point The point to test for.
  241. * @param {object} circle The circle to test if the point is within.
  242. * @returns {boolean} Returns true if the point is inside, false if outside.
  243. */
  244. var isPointInsideCircle = function isPointInsideCircle(point, circle) {
  245. return getDistanceBetweenPoints(point, circle) <= circle.r + 1e-10;
  246. };
  247. /**
  248. * Tests wether a point lies within a set of circles.
  249. *
  250. * @param {object} point The point to test.
  251. * @param {array} circles The list of circles to test against.
  252. * @returns {boolean} Returns true if the point is inside all the circles, false
  253. * if not.
  254. */
  255. var isPointInsideAllCircles = function isPointInsideAllCircles(point, circles) {
  256. return !circles.some(function (circle) {
  257. return !isPointInsideCircle(point, circle);
  258. });
  259. };
  260. /**
  261. * Tests wether a point lies outside a set of circles.
  262. *
  263. * TODO: add unit tests.
  264. *
  265. * @param {object} point The point to test.
  266. * @param {array} circles The list of circles to test against.
  267. * @returns {boolean} Returns true if the point is outside all the circles,
  268. * false if not.
  269. */
  270. var isPointOutsideAllCircles =
  271. function isPointOutsideAllCircles(point, circles) {
  272. return !circles.some(function (circle) {
  273. return isPointInsideCircle(point, circle);
  274. });
  275. };
  276. /**
  277. * Calculate the path for the area of overlap between a set of circles.
  278. *
  279. * TODO: handle cases with only 1 or 0 arcs.
  280. *
  281. * @param {array} circles List of circles to calculate area of.
  282. * @returns {string} Returns the path for the area of overlap. Returns an empty
  283. * string if there are no intersection between all the circles.
  284. */
  285. var getAreaOfIntersectionBetweenCircles =
  286. function getAreaOfIntersectionBetweenCircles(circles) {
  287. var intersectionPoints = getCirclesIntersectionPoints(circles)
  288. .filter(function (p) {
  289. return isPointInsideAllCircles(p, circles);
  290. }),
  291. result;
  292. if (intersectionPoints.length > 1) {
  293. // Calculate the center of the intersection points.
  294. var center = getCenterOfPoints(intersectionPoints);
  295. intersectionPoints = intersectionPoints
  296. // Calculate the angle between the center and the points.
  297. .map(function (p) {
  298. p.angle = getAngleBetweenPoints(center, p);
  299. return p;
  300. })
  301. // Sort the points by the angle to the center.
  302. .sort(function (a, b) {
  303. return b.angle - a.angle;
  304. });
  305. var startPoint = intersectionPoints[intersectionPoints.length - 1];
  306. var arcs = intersectionPoints
  307. .reduce(function (data, p1) {
  308. var startPoint = data.startPoint,
  309. midPoint = getCenterOfPoints([startPoint, p1]);
  310. // Calculate the arc from the intersection points and their
  311. // circles.
  312. var arc = p1.indexes
  313. // Filter out circles that are not included in both
  314. // intersection points.
  315. .filter(function (index) {
  316. return startPoint.indexes.indexOf(index) > -1;
  317. })
  318. // Iterate the circles of the intersection points and
  319. // calculate arcs.
  320. .reduce(function (arc, index) {
  321. var circle = circles[index],
  322. angle1 = getAngleBetweenPoints(circle, p1),
  323. angle2 = getAngleBetweenPoints(circle, startPoint),
  324. angleDiff = angle2 - angle1 +
  325. (angle2 < angle1 ? 2 * Math.PI : 0),
  326. angle = angle2 - angleDiff / 2,
  327. width = getDistanceBetweenPoints(
  328. midPoint,
  329. {
  330. x: circle.x + circle.r * Math.sin(angle),
  331. y: circle.y + circle.r * Math.cos(angle)
  332. }
  333. ),
  334. r = circle.r;
  335. // Width can sometimes become to large due to floating
  336. // point errors
  337. if (width > r * 2) {
  338. width = r * 2;
  339. }
  340. // Get the arc with the smallest width.
  341. if (!arc || arc.width > width) {
  342. arc = {
  343. r: r,
  344. largeArc: width > r ? 1 : 0,
  345. width: width,
  346. x: p1.x,
  347. y: p1.y
  348. };
  349. }
  350. // Return the chosen arc.
  351. return arc;
  352. }, null);
  353. // If we find an arc then add it to the list and update p2.
  354. if (arc) {
  355. var r = arc.r;
  356. data.arcs.push(
  357. ['A', r, r, 0, arc.largeArc, 1, arc.x, arc.y]
  358. );
  359. data.startPoint = p1;
  360. }
  361. return data;
  362. }, {
  363. startPoint: startPoint,
  364. arcs: []
  365. }).arcs;
  366. if (arcs.length === 0) {
  367. } else if (arcs.length === 1) {
  368. } else {
  369. arcs.unshift(['M', startPoint.x, startPoint.y]);
  370. result = {
  371. center: center,
  372. d: arcs
  373. };
  374. }
  375. }
  376. return result;
  377. };
  378. var geometryCircles = {
  379. getAreaOfIntersectionBetweenCircles: getAreaOfIntersectionBetweenCircles,
  380. getCircleCircleIntersection: getCircleCircleIntersection,
  381. getCirclesIntersectionPoints: getCirclesIntersectionPoints,
  382. getCircularSegmentArea: getCircularSegmentArea,
  383. getOverlapBetweenCircles: getOverlapBetweenCircles,
  384. isPointInsideCircle: isPointInsideCircle,
  385. isPointInsideAllCircles: isPointInsideAllCircles,
  386. isPointOutsideAllCircles: isPointOutsideAllCircles,
  387. round: round
  388. };
  389. return geometryCircles;
  390. }(geometry));
  391. (function (draw, geometry, geometryCircles, H) {
  392. /* *
  393. * Experimental Highcharts module which enables visualization of a Venn Diagram.
  394. *
  395. * (c) 2016-2019 Highsoft AS
  396. *
  397. * Authors: Jon Arild Nygard
  398. *
  399. * Layout algorithm by Ben Frederickson:
  400. * https://www.benfrederickson.com/better-venn-diagrams/
  401. *
  402. * License: www.highcharts.com/license
  403. */
  404. var color = H.Color,
  405. extend = H.extend,
  406. getAreaOfIntersectionBetweenCircles =
  407. geometryCircles.getAreaOfIntersectionBetweenCircles,
  408. getCircleCircleIntersection = geometryCircles.getCircleCircleIntersection,
  409. getCenterOfPoints = geometry.getCenterOfPoints,
  410. getDistanceBetweenPoints = geometry.getDistanceBetweenPoints,
  411. getOverlapBetweenCirclesByDistance =
  412. geometryCircles.getOverlapBetweenCircles,
  413. isArray = H.isArray,
  414. isNumber = H.isNumber,
  415. isObject = H.isObject,
  416. isPointInsideAllCircles = geometryCircles.isPointInsideAllCircles,
  417. isPointOutsideAllCircles = geometryCircles.isPointOutsideAllCircles,
  418. isString = H.isString,
  419. merge = H.merge,
  420. seriesType = H.seriesType;
  421. var objectValues = function objectValues(obj) {
  422. return Object.keys(obj).map(function (x) {
  423. return obj[x];
  424. });
  425. };
  426. /**
  427. * Calculates the area of overlap between a list of circles.
  428. * @private
  429. * @todo add support for calculating overlap between more than 2 circles.
  430. * @param {Array<object>} circles List of circles with their given positions.
  431. * @return {number} Returns the area of overlap between all the circles.
  432. */
  433. var getOverlapBetweenCircles = function getOverlapBetweenCircles(circles) {
  434. var overlap = 0;
  435. // When there is only two circles we can find the overlap by using their
  436. // radiuses and the distance between them.
  437. if (circles.length === 2) {
  438. var circle1 = circles[0];
  439. var circle2 = circles[1];
  440. overlap = getOverlapBetweenCirclesByDistance(
  441. circle1.r,
  442. circle2.r,
  443. getDistanceBetweenPoints(circle1, circle2)
  444. );
  445. }
  446. return overlap;
  447. };
  448. /**
  449. * Calculates the difference between the desired overlap and the actual overlap
  450. * between two circles.
  451. * @private
  452. * @param {object} mapOfIdToCircle Map from id to circle.
  453. * @param {Array<object>} relations List of relations to calculate the loss of.
  454. * @return {number} Returns the loss between positions of the circles for the
  455. * given relations.
  456. */
  457. var loss = function loss(mapOfIdToCircle, relations) {
  458. var precision = 10e10;
  459. // Iterate all the relations and calculate their individual loss.
  460. return relations.reduce(function (totalLoss, relation) {
  461. var loss = 0;
  462. if (relation.sets.length > 1) {
  463. var wantedOverlap = relation.value;
  464. // Calculate the actual overlap between the sets.
  465. var actualOverlap = getOverlapBetweenCircles(
  466. // Get the circles for the given sets.
  467. relation.sets.map(function (set) {
  468. return mapOfIdToCircle[set];
  469. })
  470. );
  471. var diff = wantedOverlap - actualOverlap;
  472. loss = Math.round((diff * diff) * precision) / precision;
  473. }
  474. // Add calculated loss to the sum.
  475. return totalLoss + loss;
  476. }, 0);
  477. };
  478. /**
  479. * Finds the root of a given function. The root is the input value needed for
  480. * a function to return 0.
  481. *
  482. * See https://en.wikipedia.org/wiki/Bisection_method#Algorithm
  483. *
  484. * TODO: Add unit tests.
  485. *
  486. * @param {function} f The function to find the root of.
  487. * @param {number} a The lowest number in the search range.
  488. * @param {number} b The highest number in the search range.
  489. * @param {number} [tolerance=1e-10] The allowed difference between the returned
  490. * value and root.
  491. * @param {number} [maxIterations=100] The maximum iterations allowed.
  492. */
  493. var bisect = function bisect(f, a, b, tolerance, maxIterations) {
  494. var fA = f(a),
  495. fB = f(b),
  496. nMax = maxIterations || 100,
  497. tol = tolerance || 1e-10,
  498. delta = b - a,
  499. n = 1,
  500. x, fX;
  501. if (a >= b) {
  502. throw new Error('a must be smaller than b.');
  503. } else if (fA * fB > 0) {
  504. throw new Error('f(a) and f(b) must have opposite signs.');
  505. }
  506. if (fA === 0) {
  507. x = a;
  508. } else if (fB === 0) {
  509. x = b;
  510. } else {
  511. while (n++ <= nMax && fX !== 0 && delta > tol) {
  512. delta = (b - a) / 2;
  513. x = a + delta;
  514. fX = f(x);
  515. // Update low and high for next search interval.
  516. if (fA * fX > 0) {
  517. a = x;
  518. } else {
  519. b = x;
  520. }
  521. }
  522. }
  523. return x;
  524. };
  525. /**
  526. * Uses the bisection method to make a best guess of the ideal distance between
  527. * two circles too get the desired overlap.
  528. * Currently there is no known formula to calculate the distance from the area
  529. * of overlap, which makes the bisection method preferred.
  530. * @private
  531. * @param {number} r1 Radius of the first circle.
  532. * @param {number} r2 Radiues of the second circle.
  533. * @param {number} overlap The wanted overlap between the two circles.
  534. * @return {number} Returns the distance needed to get the wanted overlap
  535. * between the two circles.
  536. */
  537. var getDistanceBetweenCirclesByOverlap =
  538. function getDistanceBetweenCirclesByOverlap(r1, r2, overlap) {
  539. var maxDistance = r1 + r2,
  540. distance = maxDistance;
  541. if (overlap > 0) {
  542. distance = bisect(function (x) {
  543. var actualOverlap = getOverlapBetweenCirclesByDistance(r1, r2, x);
  544. // Return the differance between wanted and actual overlap.
  545. return overlap - actualOverlap;
  546. }, 0, maxDistance);
  547. }
  548. return distance;
  549. };
  550. var isSet = function (x) {
  551. return isArray(x.sets) && x.sets.length === 1;
  552. };
  553. /**
  554. * Finds an optimal position for a given point.
  555. * @private
  556. * @todo add unit tests.
  557. * @todo add constraints to optimize the algorithm.
  558. * @param {Function} fn The function to test a point.
  559. * @param {Array<*>} initial The initial point to optimize.
  560. * @return {Array<*>} Returns the opimized position of a point.
  561. */
  562. var nelderMead = function nelderMead(fn, initial) {
  563. var maxIterations = 100,
  564. sortByFx = function (a, b) {
  565. return a.fx - b.fx;
  566. },
  567. pRef = 1, // Reflection parameter
  568. pExp = 2, // Expansion parameter
  569. pCon = -0.5, // Contraction parameter
  570. pOCon = pCon * pRef, // Outwards contraction parameter
  571. pShrink = 0.5; // Shrink parameter
  572. var weightedSum = function weightedSum(weight1, v1, weight2, v2) {
  573. return v1.map(function (x, i) {
  574. return weight1 * x + weight2 * v2[i];
  575. });
  576. };
  577. var getSimplex = function getSimplex(initial) {
  578. var n = initial.length,
  579. simplex = new Array(n + 1);
  580. // Initial point to the simplex.
  581. simplex[0] = initial;
  582. simplex[0].fx = fn(initial);
  583. // Create a set of extra points based on the initial.
  584. for (var i = 0; i < n; ++i) {
  585. var point = initial.slice();
  586. point[i] = point[i] ? point[i] * 1.05 : 0.001;
  587. point.fx = fn(point);
  588. simplex[i + 1] = point;
  589. }
  590. return simplex;
  591. };
  592. var updateSimplex = function (simplex, point) {
  593. point.fx = fn(point);
  594. simplex[simplex.length - 1] = point;
  595. return simplex;
  596. };
  597. var shrinkSimplex = function (simplex) {
  598. var best = simplex[0];
  599. return simplex.map(function (point) {
  600. var p = weightedSum(1 - pShrink, best, pShrink, point);
  601. p.fx = fn(p);
  602. return p;
  603. });
  604. };
  605. var getCentroid = function (simplex) {
  606. var arr = simplex.slice(0, -1),
  607. length = arr.length,
  608. result = [],
  609. sum = function (data, point) {
  610. data.sum += point[data.i];
  611. return data;
  612. };
  613. for (var i = 0; i < length; i++) {
  614. result[i] = simplex.reduce(sum, { sum: 0, i: i }).sum / length;
  615. }
  616. return result;
  617. };
  618. var getPoint = function (centroid, worst, a, b) {
  619. var point = weightedSum(a, centroid, b, worst);
  620. point.fx = fn(point);
  621. return point;
  622. };
  623. // Create a simplex
  624. var simplex = getSimplex(initial);
  625. // Iterate from 0 to max iterations
  626. for (var i = 0; i < maxIterations; i++) {
  627. // Sort the simplex
  628. simplex.sort(sortByFx);
  629. // Create a centroid from the simplex
  630. var worst = simplex[simplex.length - 1];
  631. var centroid = getCentroid(simplex);
  632. // Calculate the reflected point.
  633. var reflected = getPoint(centroid, worst, 1 + pRef, -pRef);
  634. if (reflected.fx < simplex[0].fx) {
  635. // If reflected point is the best, then possibly expand.
  636. var expanded = getPoint(centroid, worst, 1 + pExp, -pExp);
  637. simplex = updateSimplex(
  638. simplex,
  639. (expanded.fx < reflected.fx) ? expanded : reflected
  640. );
  641. } else if (reflected.fx >= simplex[simplex.length - 2].fx) {
  642. // If the reflected point is worse than the second worse, then
  643. // contract.
  644. var contracted;
  645. if (reflected.fx > worst.fx) {
  646. // If the reflected is worse than the worst point, do a
  647. // contraction
  648. contracted = getPoint(centroid, worst, 1 + pCon, -pCon);
  649. if (contracted.fx < worst.fx) {
  650. simplex = updateSimplex(simplex, contracted);
  651. } else {
  652. simplex = shrinkSimplex(simplex);
  653. }
  654. } else {
  655. // Otherwise do an outwards contraction
  656. contracted = getPoint(centroid, worst, 1 - pOCon, pOCon);
  657. if (contracted.fx < reflected.fx) {
  658. simplex = updateSimplex(simplex, contracted);
  659. } else {
  660. simplex = shrinkSimplex(simplex);
  661. }
  662. }
  663. } else {
  664. simplex = updateSimplex(simplex, reflected);
  665. }
  666. }
  667. return simplex[0];
  668. };
  669. /**
  670. * Calculates a margin for a point based on the iternal and external circles.
  671. * The margin describes if the point is well placed within the internal circles,
  672. * and away from the external
  673. * @private
  674. * @todo add unit tests.
  675. * @param {object} point The point to evaluate.
  676. * @param {Array<object>} internal The internal circles.
  677. * @param {Array<object>} external The external circles.
  678. * @return {number} Returns the margin.
  679. */
  680. var getMarginFromCircles =
  681. function getMarginFromCircles(point, internal, external) {
  682. var margin = internal.reduce(function (margin, circle) {
  683. var m = circle.r - getDistanceBetweenPoints(point, circle);
  684. return (m <= margin) ? m : margin;
  685. }, Number.MAX_VALUE);
  686. margin = external.reduce(function (margin, circle) {
  687. var m = getDistanceBetweenPoints(point, circle) - circle.r;
  688. return (m <= margin) ? m : margin;
  689. }, margin);
  690. return margin;
  691. };
  692. /**
  693. * Finds the optimal label position by looking for a position that has a low
  694. * distance from the internal circles, and as large possible distane to the
  695. * external circles.
  696. * @private
  697. * @todo Optimize the intial position.
  698. * @todo Add unit tests.
  699. * @param {Array<object>} internal Internal circles.
  700. * @param {Array<object>} external External circles.
  701. * @return {object} Returns the found position.
  702. */
  703. var getLabelPosition = function getLabelPosition(internal, external) {
  704. // Get the best label position within the internal circles.
  705. var best = internal.reduce(function (best, circle) {
  706. var d = circle.r / 2;
  707. // Give a set of points with the circle to evaluate as the best label
  708. // position.
  709. return [
  710. { x: circle.x, y: circle.y },
  711. { x: circle.x + d, y: circle.y },
  712. { x: circle.x - d, y: circle.y },
  713. { x: circle.x, y: circle.y + d },
  714. { x: circle.x, y: circle.y - d }
  715. ]
  716. // Iterate the given points and return the one with the largest margin.
  717. .reduce(function (best, point) {
  718. var margin = getMarginFromCircles(point, internal, external);
  719. // If the margin better than the current best, then update best.
  720. if (best.margin < margin) {
  721. best.point = point;
  722. best.margin = margin;
  723. }
  724. return best;
  725. }, best);
  726. }, {
  727. point: undefined,
  728. margin: -Number.MAX_VALUE
  729. }).point;
  730. // Use nelder mead to optimize the initial label position.
  731. var optimal = nelderMead(
  732. function (p) {
  733. return -(
  734. getMarginFromCircles({ x: p[0], y: p[1] }, internal, external)
  735. );
  736. },
  737. [best.x, best.y]
  738. );
  739. // Update best to be the point which was found to have the best margin.
  740. best = {
  741. x: optimal[0],
  742. y: optimal[1]
  743. };
  744. if (!(
  745. isPointInsideAllCircles(best, internal) &&
  746. isPointOutsideAllCircles(best, external)
  747. )) {
  748. // If point was either outside one of the internal, or inside one of the
  749. // external, then it was invalid and should use a fallback.
  750. best = getCenterOfPoints(internal);
  751. }
  752. // Return the best point.
  753. return best;
  754. };
  755. /**
  756. * Calulates data label positions for a list of relations.
  757. * @private
  758. * @todo add unit tests
  759. * @todo NOTE: may be better suited as a part of the layout function.
  760. * @param {Array<object>} relations The list of relations.
  761. * @return {object} Returns a map from id to the data label position.
  762. */
  763. var getLabelPositions = function getLabelPositions(relations) {
  764. var singleSets = relations.filter(isSet);
  765. return relations.reduce(function (map, relation) {
  766. if (relation.value) {
  767. var sets = relation.sets,
  768. id = sets.join(),
  769. // Create a list of internal and external circles.
  770. data = singleSets.reduce(function (data, set) {
  771. // If the set exists in this relation, then it is internal,
  772. // otherwise it will be external.
  773. var isInternal = sets.indexOf(set.sets[0]) > -1,
  774. property = isInternal ? 'internal' : 'external';
  775. // Add the circle to the list.
  776. data[property].push(set.circle);
  777. return data;
  778. }, {
  779. internal: [],
  780. external: []
  781. });
  782. // Calulate the label position.
  783. map[id] = getLabelPosition(
  784. data.internal,
  785. data.external
  786. );
  787. }
  788. return map;
  789. }, {});
  790. };
  791. /**
  792. * Takes an array of relations and adds the properties `totalOverlap` and
  793. * `overlapping` to each set. The property `totalOverlap` is the sum of value
  794. * for each relation where this set is included. The property `overlapping` is
  795. * a map of how much this set is overlapping another set.
  796. * NOTE: This algorithm ignores relations consisting of more than 2 sets.
  797. * @private
  798. * @param {Array<object>} relations The list of relations that should be sorted.
  799. * @return {Array<object>} Returns the modified input relations with added
  800. * properties `totalOverlap` and `overlapping`.
  801. */
  802. var addOverlapToSets = function addOverlapToSets(relations) {
  803. // Calculate the amount of overlap per set.
  804. var mapOfIdToProps = relations
  805. // Filter out relations consisting of 2 sets.
  806. .filter(function (relation) {
  807. return relation.sets.length === 2;
  808. })
  809. // Sum up the amount of overlap for each set.
  810. .reduce(function (map, relation) {
  811. var sets = relation.sets;
  812. sets.forEach(function (set, i, arr) {
  813. if (!isObject(map[set])) {
  814. map[set] = {
  815. overlapping: {},
  816. totalOverlap: 0
  817. };
  818. }
  819. map[set].totalOverlap += relation.value;
  820. map[set].overlapping[arr[1 - i]] = relation.value;
  821. });
  822. return map;
  823. }, {});
  824. relations
  825. // Filter out single sets
  826. .filter(isSet)
  827. // Extend the set with the calculated properties.
  828. .forEach(function (set) {
  829. var properties = mapOfIdToProps[set.sets[0]];
  830. extend(set, properties);
  831. });
  832. // Returns the modified relations.
  833. return relations;
  834. };
  835. /**
  836. * Takes two sets and finds the one with the largest total overlap.
  837. * @private
  838. * @param {object} a The first set to compare.
  839. * @param {object} b The second set to compare.
  840. * @return {number} Returns 0 if a and b are equal, <0 if a is greater, >0 if b
  841. * is greater.
  842. */
  843. var sortByTotalOverlap = function sortByTotalOverlap(a, b) {
  844. return b.totalOverlap - a.totalOverlap;
  845. };
  846. /**
  847. * Uses a greedy approach to position all the sets. Works well with a small
  848. * number of sets, and are in these cases a good choice aesthetically.
  849. * @private
  850. * @param {Array<object>} relations List of the overlap between two or more
  851. * sets, or the size of a single set.
  852. * @return {Array<object>} List of circles and their calculated positions.
  853. */
  854. var layoutGreedyVenn = function layoutGreedyVenn(relations) {
  855. var positionedSets = [],
  856. mapOfIdToCircles = {};
  857. // Define a circle for each set.
  858. relations
  859. .filter(function (relation) {
  860. return relation.sets.length === 1;
  861. }).forEach(function (relation) {
  862. mapOfIdToCircles[relation.sets[0]] = relation.circle = {
  863. x: Number.MAX_VALUE,
  864. y: Number.MAX_VALUE,
  865. r: Math.sqrt(relation.value / Math.PI)
  866. };
  867. });
  868. /**
  869. * Takes a set and updates the position, and add the set to the list of
  870. * positioned sets.
  871. * @private
  872. * @param {object} set The set to add to its final position.
  873. * @param {object} coordinates The coordinates to position the set at.
  874. */
  875. var positionSet = function positionSet(set, coordinates) {
  876. var circle = set.circle;
  877. circle.x = coordinates.x;
  878. circle.y = coordinates.y;
  879. positionedSets.push(set);
  880. };
  881. // Find overlap between sets. Ignore relations with more then 2 sets.
  882. addOverlapToSets(relations);
  883. // Sort sets by the sum of their size from large to small.
  884. var sortedByOverlap = relations
  885. .filter(isSet)
  886. .sort(sortByTotalOverlap);
  887. // Position the most overlapped set at 0,0.
  888. positionSet(sortedByOverlap.pop(), { x: 0, y: 0 });
  889. var relationsWithTwoSets = relations.filter(function (x) {
  890. return x.sets.length === 2;
  891. });
  892. // Iterate and position the remaining sets.
  893. sortedByOverlap.forEach(function (set) {
  894. var circle = set.circle,
  895. radius = circle.r,
  896. overlapping = set.overlapping;
  897. var bestPosition = positionedSets
  898. .reduce(function (best, positionedSet, i) {
  899. var positionedCircle = positionedSet.circle,
  900. overlap = overlapping[positionedSet.sets[0]];
  901. // Calculate the distance between the sets to get the correct
  902. // overlap
  903. var distance = getDistanceBetweenCirclesByOverlap(
  904. radius,
  905. positionedCircle.r,
  906. overlap
  907. );
  908. // Create a list of possible coordinates calculated from
  909. // distance.
  910. var possibleCoordinates = [
  911. { x: positionedCircle.x + distance, y: positionedCircle.y },
  912. { x: positionedCircle.x - distance, y: positionedCircle.y },
  913. { x: positionedCircle.x, y: positionedCircle.y + distance },
  914. { x: positionedCircle.x, y: positionedCircle.y - distance }
  915. ];
  916. // If there are more circles overlapping, then add the
  917. // intersection points as possible positions.
  918. positionedSets.slice(i + 1).forEach(function (positionedSet2) {
  919. var positionedCircle2 = positionedSet2.circle,
  920. overlap2 = overlapping[positionedSet2.sets[0]],
  921. distance2 = getDistanceBetweenCirclesByOverlap(
  922. radius,
  923. positionedCircle2.r,
  924. overlap2
  925. );
  926. // Add intersections to list of coordinates.
  927. possibleCoordinates = possibleCoordinates.concat(
  928. getCircleCircleIntersection({
  929. x: positionedCircle.x,
  930. y: positionedCircle.y,
  931. r: distance2
  932. }, {
  933. x: positionedCircle2.x,
  934. y: positionedCircle2.y,
  935. r: distance2
  936. })
  937. );
  938. });
  939. // Iterate all suggested coordinates and find the best one.
  940. possibleCoordinates.forEach(function (coordinates) {
  941. circle.x = coordinates.x;
  942. circle.y = coordinates.y;
  943. // Calculate loss for the suggested coordinates.
  944. var currentLoss = loss(
  945. mapOfIdToCircles, relationsWithTwoSets
  946. );
  947. // If the loss is better, then use these new coordinates.
  948. if (currentLoss < best.loss) {
  949. best.loss = currentLoss;
  950. best.coordinates = coordinates;
  951. }
  952. });
  953. // Return resulting coordinates.
  954. return best;
  955. }, {
  956. loss: Number.MAX_VALUE,
  957. coordinates: undefined
  958. });
  959. // Add the set to its final position.
  960. positionSet(set, bestPosition.coordinates);
  961. });
  962. // Return the positions of each set.
  963. return mapOfIdToCircles;
  964. };
  965. /**
  966. * Calculates the positions of all the sets in the venn diagram.
  967. * @private
  968. * @todo Add support for constrained MDS.
  969. * @param {Array<object>} relations List of the overlap between two or more sets, or the
  970. * size of a single set.
  971. * @return {Arrat<object>} List of circles and their calculated positions.
  972. */
  973. var layout = function (relations) {
  974. var mapOfIdToShape = {};
  975. // Calculate best initial positions by using greedy layout.
  976. if (relations.length > 0) {
  977. mapOfIdToShape = layoutGreedyVenn(relations);
  978. relations
  979. .filter(function (x) {
  980. return !isSet(x);
  981. })
  982. .forEach(function (relation) {
  983. var sets = relation.sets,
  984. id = sets.join(),
  985. circles = sets.map(function (set) {
  986. return mapOfIdToShape[set];
  987. });
  988. // Add intersection shape to map
  989. mapOfIdToShape[id] =
  990. getAreaOfIntersectionBetweenCircles(circles);
  991. });
  992. }
  993. return mapOfIdToShape;
  994. };
  995. var isValidRelation = function (x) {
  996. var map = {};
  997. return (
  998. isObject(x) &&
  999. (isNumber(x.value) && x.value > -1) &&
  1000. (isArray(x.sets) && x.sets.length > 0) &&
  1001. !x.sets.some(function (set) {
  1002. var invalid = false;
  1003. if (!map[set] && isString(set)) {
  1004. map[set] = true;
  1005. } else {
  1006. invalid = true;
  1007. }
  1008. return invalid;
  1009. })
  1010. );
  1011. };
  1012. var isValidSet = function (x) {
  1013. return (isValidRelation(x) && isSet(x) && x.value > 0);
  1014. };
  1015. /**
  1016. * Prepares the venn data so that it is usable for the layout function. Filter
  1017. * out sets, or intersections that includes sets, that are missing in the data
  1018. * or has (value < 1). Adds missing relations between sets in the data as
  1019. * value = 0.
  1020. * @private
  1021. * @param {Array<object>} data The raw input data.
  1022. * @return {Array<object>} Returns an array of valid venn data.
  1023. */
  1024. var processVennData = function processVennData(data) {
  1025. var d = isArray(data) ? data : [];
  1026. var validSets = d
  1027. .reduce(function (arr, x) {
  1028. // Check if x is a valid set, and that it is not an duplicate.
  1029. if (isValidSet(x) && arr.indexOf(x.sets[0]) === -1) {
  1030. arr.push(x.sets[0]);
  1031. }
  1032. return arr;
  1033. }, [])
  1034. .sort();
  1035. var mapOfIdToRelation = d.reduce(function (mapOfIdToRelation, relation) {
  1036. if (isValidRelation(relation) && !relation.sets.some(function (set) {
  1037. return validSets.indexOf(set) === -1;
  1038. })) {
  1039. mapOfIdToRelation[relation.sets.sort().join()] = relation;
  1040. }
  1041. return mapOfIdToRelation;
  1042. }, {});
  1043. validSets.reduce(function (combinations, set, i, arr) {
  1044. var remaining = arr.slice(i + 1);
  1045. remaining.forEach(function (set2) {
  1046. combinations.push(set + ',' + set2);
  1047. });
  1048. return combinations;
  1049. }, []).forEach(function (combination) {
  1050. if (!mapOfIdToRelation[combination]) {
  1051. var obj = {
  1052. sets: combination.split(','),
  1053. value: 0
  1054. };
  1055. mapOfIdToRelation[combination] = obj;
  1056. }
  1057. });
  1058. // Transform map into array.
  1059. return objectValues(mapOfIdToRelation);
  1060. };
  1061. /**
  1062. * Calculates the proper scale to fit the cloud inside the plotting area.
  1063. * @private
  1064. * @todo add unit test
  1065. * @param {number} targetWidth Width of target area.
  1066. * @param {number} targetHeight Height of target area.
  1067. * @param {object} field The playing field.
  1068. * @param {Highcharts.Series} series Series object.
  1069. * @return {object} Returns the value to scale the playing field up to the size
  1070. * of the target area, and center of x and y.
  1071. */
  1072. var getScale = function getScale(targetWidth, targetHeight, field) {
  1073. var height = field.bottom - field.top, // top is smaller than bottom
  1074. width = field.right - field.left,
  1075. scaleX = width > 0 ? 1 / width * targetWidth : 1,
  1076. scaleY = height > 0 ? 1 / height * targetHeight : 1,
  1077. adjustX = (field.right + field.left) / 2,
  1078. adjustY = (field.top + field.bottom) / 2,
  1079. scale = Math.min(scaleX, scaleY);
  1080. return {
  1081. scale: scale,
  1082. centerX: targetWidth / 2 - adjustX * scale,
  1083. centerY: targetHeight / 2 - adjustY * scale
  1084. };
  1085. };
  1086. /**
  1087. * If a circle is outside a give field, then the boundaries of the field is
  1088. * adjusted accordingly. Modifies the field object which is passed as the first
  1089. * parameter.
  1090. * @private
  1091. * @todo NOTE: Copied from wordcloud, can probably be unified.
  1092. * @param {object} field The bounding box of a playing field.
  1093. * @param {object} placement The bounding box for a placed point.
  1094. * @return {object} Returns a modified field object.
  1095. */
  1096. var updateFieldBoundaries = function updateFieldBoundaries(field, circle) {
  1097. var left = circle.x - circle.r,
  1098. right = circle.x + circle.r,
  1099. bottom = circle.y + circle.r,
  1100. top = circle.y - circle.r;
  1101. // TODO improve type checking.
  1102. if (!isNumber(field.left) || field.left > left) {
  1103. field.left = left;
  1104. }
  1105. if (!isNumber(field.right) || field.right < right) {
  1106. field.right = right;
  1107. }
  1108. if (!isNumber(field.top) || field.top > top) {
  1109. field.top = top;
  1110. }
  1111. if (!isNumber(field.bottom) || field.bottom < bottom) {
  1112. field.bottom = bottom;
  1113. }
  1114. return field;
  1115. };
  1116. /**
  1117. * A Venn diagram displays all possible logical relations between a collection
  1118. * of different sets. The sets are represented by circles, and the relation
  1119. * between the sets are displayed by the overlap or lack of overlap between
  1120. * them. The venn diagram is a special case of Euler diagrams, which can also
  1121. * be displayed by this series type.
  1122. *
  1123. * @sample {highcharts} highcharts/demo/venn-diagram/
  1124. * Venn diagram
  1125. * @sample {highcharts} highcharts/demo/euler-diagram/
  1126. * Euler diagram
  1127. *
  1128. * @extends plotOptions.scatter
  1129. * @excluding connectEnds, connectNulls, cropThreshold, findNearestPointBy,
  1130. * getExtremesFromAll, jitter, label, linecap, lineWidth,
  1131. * linkedTo, marker, negativeColor, pointInterval,
  1132. * pointIntervalUnit, pointPlacement, pointStart, softThreshold,
  1133. * stacking, steps, threshold, xAxis, yAxis, zoneAxis, zones
  1134. * @product highcharts
  1135. * @optionparent plotOptions.venn
  1136. */
  1137. var vennOptions = {
  1138. borderColor: '#cccccc',
  1139. borderDashStyle: 'solid',
  1140. borderWidth: 1,
  1141. brighten: 0,
  1142. clip: false,
  1143. colorByPoint: true,
  1144. dataLabels: {
  1145. enabled: true,
  1146. formatter: function () {
  1147. return this.point.name;
  1148. }
  1149. },
  1150. marker: false,
  1151. opacity: 0.75,
  1152. showInLegend: false,
  1153. states: {
  1154. hover: {
  1155. opacity: 1,
  1156. halo: false,
  1157. borderColor: '#333333'
  1158. },
  1159. select: {
  1160. color: '#cccccc',
  1161. borderColor: '#000000',
  1162. animation: false
  1163. }
  1164. },
  1165. tooltip: {
  1166. pointFormat: '{point.name}: {point.value}'
  1167. }
  1168. };
  1169. var vennSeries = {
  1170. isCartesian: false,
  1171. axisTypes: [],
  1172. directTouch: true,
  1173. pointArrayMap: ['value'],
  1174. translate: function () {
  1175. var chart = this.chart;
  1176. this.processedXData = this.xData;
  1177. this.generatePoints();
  1178. // Process the data before passing it into the layout function.
  1179. var relations = processVennData(this.options.data);
  1180. // Calculate the positions of each circle.
  1181. var mapOfIdToShape = layout(relations);
  1182. // Calculate positions of each data label
  1183. var mapOfIdToLabelPosition = getLabelPositions(relations);
  1184. // Calculate the scale, and center of the plot area.
  1185. var field = Object.keys(mapOfIdToShape)
  1186. .filter(function (key) {
  1187. var shape = mapOfIdToShape[key];
  1188. return shape && isNumber(shape.r);
  1189. })
  1190. .reduce(function (field, key) {
  1191. return updateFieldBoundaries(field, mapOfIdToShape[key]);
  1192. }, { top: 0, bottom: 0, left: 0, right: 0 }),
  1193. scaling = getScale(chart.plotWidth, chart.plotHeight, field),
  1194. scale = scaling.scale,
  1195. centerX = scaling.centerX,
  1196. centerY = scaling.centerY;
  1197. // Iterate all points and calculate and draw their graphics.
  1198. this.points.forEach(function (point) {
  1199. var sets = isArray(point.sets) ? point.sets : [],
  1200. id = sets.join(),
  1201. shape = mapOfIdToShape[id],
  1202. shapeArgs,
  1203. dataLabelPosition = mapOfIdToLabelPosition[id];
  1204. if (shape) {
  1205. if (shape.r) {
  1206. shapeArgs = {
  1207. x: centerX + shape.x * scale,
  1208. y: centerY + shape.y * scale,
  1209. r: shape.r * scale
  1210. };
  1211. } else if (shape.d) {
  1212. // TODO: find a better way to handle scaling of a path.
  1213. var d = shape.d.reduce(function (path, arr) {
  1214. if (arr[0] === 'M') {
  1215. arr[1] = centerX + arr[1] * scale;
  1216. arr[2] = centerY + arr[2] * scale;
  1217. } else if (arr[0] === 'A') {
  1218. arr[1] = arr[1] * scale;
  1219. arr[2] = arr[2] * scale;
  1220. arr[6] = centerX + arr[6] * scale;
  1221. arr[7] = centerY + arr[7] * scale;
  1222. }
  1223. return path.concat(arr);
  1224. }, [])
  1225. .join(' ');
  1226. shapeArgs = {
  1227. d: d
  1228. };
  1229. }
  1230. // Scale the position for the data label.
  1231. if (dataLabelPosition) {
  1232. dataLabelPosition.x = centerX + dataLabelPosition.x * scale;
  1233. dataLabelPosition.y = centerY + dataLabelPosition.y * scale;
  1234. } else {
  1235. dataLabelPosition = {};
  1236. }
  1237. }
  1238. point.shapeArgs = shapeArgs;
  1239. // Placement for the data labels
  1240. if (dataLabelPosition && shapeArgs) {
  1241. point.plotX = dataLabelPosition.x;
  1242. point.plotY = dataLabelPosition.y;
  1243. }
  1244. // Set name for usage in tooltip and in data label.
  1245. point.name = point.options.name || sets.join('∩');
  1246. });
  1247. },
  1248. /**
  1249. * Draw the graphics for each point.
  1250. * @private
  1251. */
  1252. drawPoints: function () {
  1253. var series = this,
  1254. // Series properties
  1255. chart = series.chart,
  1256. group = series.group,
  1257. points = series.points || [],
  1258. // Chart properties
  1259. renderer = chart.renderer;
  1260. // Iterate all points and calculate and draw their graphics.
  1261. points.forEach(function (point) {
  1262. var attribs,
  1263. shapeArgs = point.shapeArgs;
  1264. // Add point attribs
  1265. if (!chart.styledMode) {
  1266. attribs = series.pointAttribs(point, point.state);
  1267. }
  1268. // Draw the point graphic.
  1269. point.draw({
  1270. isNew: !point.graphic,
  1271. animatableAttribs: shapeArgs,
  1272. attribs: attribs,
  1273. group: group,
  1274. renderer: renderer,
  1275. shapeType: shapeArgs && shapeArgs.d ? 'path' : 'circle'
  1276. });
  1277. });
  1278. },
  1279. /**
  1280. * Calculates the style attributes for a point. The attributes can vary
  1281. * depending on the state of the point.
  1282. * @private
  1283. * @param {object} point The point which will get the resulting attributes.
  1284. * @param {string} state The state of the point.
  1285. * @return {object} Returns the calculated attributes.
  1286. */
  1287. pointAttribs: function (point, state) {
  1288. var series = this,
  1289. seriesOptions = series.options || {},
  1290. pointOptions = point && point.options || {},
  1291. stateOptions = (state && seriesOptions.states[state]) || {},
  1292. options = merge(
  1293. seriesOptions,
  1294. { color: point && point.color },
  1295. pointOptions,
  1296. stateOptions
  1297. );
  1298. // Return resulting values for the attributes.
  1299. return {
  1300. 'fill': color(options.color)
  1301. .setOpacity(options.opacity)
  1302. .brighten(options.brightness)
  1303. .get(),
  1304. 'stroke': options.borderColor,
  1305. 'stroke-width': options.borderWidth,
  1306. 'dashstyle': options.borderDashStyle
  1307. };
  1308. },
  1309. animate: function (init) {
  1310. if (!init) {
  1311. var series = this,
  1312. animOptions = H.animObject(series.options.animation);
  1313. series.points.forEach(function (point) {
  1314. var args = point.shapeArgs;
  1315. if (point.graphic && args) {
  1316. var attr = {},
  1317. animate = {};
  1318. if (args.d) {
  1319. // If shape is a path, then animate opacity.
  1320. attr.opacity = 0.001;
  1321. } else {
  1322. // If shape is a circle, then animate radius.
  1323. attr.r = 0;
  1324. animate.r = args.r;
  1325. }
  1326. point.graphic
  1327. .attr(attr)
  1328. .animate(animate, animOptions);
  1329. // If shape is path, then fade it in after the circles
  1330. // animation
  1331. if (args.d) {
  1332. setTimeout(function () {
  1333. if (point && point.graphic) {
  1334. point.graphic.animate({
  1335. opacity: 1
  1336. });
  1337. }
  1338. }, animOptions.duration);
  1339. }
  1340. }
  1341. }, series);
  1342. series.animate = null;
  1343. }
  1344. },
  1345. utils: {
  1346. addOverlapToSets: addOverlapToSets,
  1347. geometry: geometry,
  1348. geometryCircles: geometryCircles,
  1349. getDistanceBetweenCirclesByOverlap: getDistanceBetweenCirclesByOverlap,
  1350. loss: loss,
  1351. processVennData: processVennData,
  1352. sortByTotalOverlap: sortByTotalOverlap
  1353. }
  1354. };
  1355. var vennPoint = {
  1356. draw: draw,
  1357. shouldDraw: function () {
  1358. var point = this;
  1359. // Only draw points with single sets.
  1360. return !!point.shapeArgs;
  1361. },
  1362. isValid: function () {
  1363. return isNumber(this.value);
  1364. }
  1365. };
  1366. /**
  1367. * A `venn` series. If the [type](#series.venn.type) option is
  1368. * not specified, it is inherited from [chart.type](#chart.type).
  1369. *
  1370. * @extends series,plotOptions.venn
  1371. * @excluding connectEnds, connectNulls, cropThreshold, dataParser, dataURL,
  1372. * findNearestPointBy, getExtremesFromAll, label, linecap, lineWidth,
  1373. * linkedTo, marker, negativeColor, pointInterval, pointIntervalUnit,
  1374. * pointPlacement, pointStart, softThreshold, stack, stacking, steps,
  1375. * threshold, xAxis, yAxis, zoneAxis, zones
  1376. * @product highcharts
  1377. * @apioption series.venn
  1378. */
  1379. /**
  1380. * @type {Array<*>}
  1381. * @extends series.scatter.data
  1382. * @excluding marker, x, y
  1383. * @product highcharts
  1384. * @apioption series.venn.data
  1385. */
  1386. /**
  1387. * The name of the point. Used in data labels and tooltip. If name is not
  1388. * defined then it will default to the joined values in
  1389. * [sets](#series.venn.sets).
  1390. *
  1391. * @sample {highcharts} highcharts/demo/venn-diagram/
  1392. * Venn diagram
  1393. * @sample {highcharts} highcharts/demo/euler-diagram/
  1394. * Euler diagram
  1395. *
  1396. * @type {number}
  1397. * @since 7.0.0
  1398. * @product highcharts
  1399. * @apioption series.venn.data.name
  1400. */
  1401. /**
  1402. * The value of the point, resulting in a relative area of the circle, or area
  1403. * of overlap between two sets in the venn or euler diagram.
  1404. *
  1405. * @sample {highcharts} highcharts/demo/venn-diagram/
  1406. * Venn diagram
  1407. * @sample {highcharts} highcharts/demo/euler-diagram/
  1408. * Euler diagram
  1409. *
  1410. * @type {number}
  1411. * @since 7.0.0
  1412. * @product highcharts
  1413. * @apioption series.venn.data.value
  1414. */
  1415. /**
  1416. * The set or sets the options will be applied to. If a single entry is defined,
  1417. * then it will create a new set. If more than one entry is defined, then it
  1418. * will define the overlap between the sets in the array.
  1419. *
  1420. * @sample {highcharts} highcharts/demo/venn-diagram/
  1421. * Venn diagram
  1422. * @sample {highcharts} highcharts/demo/euler-diagram/
  1423. * Euler diagram
  1424. *
  1425. * @type {Array<string>}
  1426. * @since 7.0.0
  1427. * @product highcharts
  1428. * @apioption series.venn.data.sets
  1429. */
  1430. /**
  1431. * @private
  1432. * @class
  1433. * @name Highcharts.seriesTypes.venn
  1434. *
  1435. * @augments Highcharts.Series
  1436. */
  1437. seriesType('venn', 'scatter', vennOptions, vennSeries, vennPoint);
  1438. }(draw, geometry, geometryCircles, Highcharts));
  1439. return (function () {
  1440. }());
  1441. }));