venn.src.js 72 KB

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