pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/plotly/plotly.js/pull/7653.diff

I / 2; + var x = radius * Math.cos(ang); + var y = radius * Math.sin(ang); + path += (i === 0 ? '' : 'L') + x.toFixed(2) + ',' + y.toFixed(2); + } + path += 'Z'; + return path; + } + + // Create the plot + var trace = { + x: [1, 2, 3, 4, 5], + y: [2, 3, 4, 3, 2], + mode: 'markers+lines', + name: 'Custom Markers', + marker: { + symbol: [heartMarker, star5Marker, 'circle', star5Marker, heartMarker], + size: 20, + color: ['#e74c3c', '#f39c12', '#3498db', '#ff9800', '#c0392b'], + line: { + color: '#34495e', + width: 2 + } + }, + line: { + color: '#95a5a6', + width: 2, + dash: 'dot' + } + }; + + var layout = { + title: 'Custom Marker Functions Demo', + xaxis: { + title: 'X Axis', + gridcolor: '#ecf0f1' + }, + yaxis: { + title: 'Y Axis', + gridcolor: '#ecf0f1' + }, + plot_bgcolor: '#fafafa', + showlegend: true + }; + + Plotly.newPlot('plot1', [trace], layout); + console.log('Plot created successfully!'); + + + diff --git a/devtools/demos/custom_marker_demos.html b/devtools/demos/custom_marker_demos.html new file mode 100644 index 00000000000..199efc872cc --- /dev/null +++ b/devtools/demos/custom_marker_demos.html @@ -0,0 +1,185 @@ + + + + + Custom Markers - SVG path strings (New API) + + + + +

Custom Markers — SVG path strings

+
+ Pass SVG path strings as marker.symbol to create custom shapes.
+ Paths are precomputed at r=20; Plotly scales them by size/20.
+ Use an array for per-point shapes. Rotation uses marker.angle + (applied as transform="rotate()" via SVG <use>). +
+ + +

1. Basic Custom Markers

+

Precomputed SVG paths at r=20. Mix with built-in symbol names.

+
+
+// Heart shape at r=20
+var HEART = 'M0,-8C-12,-16 -24,-5.33 -24,0C-24,8 0,16 0,24C0,16 24,8 24,0C24,-5.33 12,-16 0,-8Z';
+
+// 5-point star at r=20
+var STAR = 'M0,-20L4.70,-6.47L19.02,-6.18L7.61,2.47L11.76,16.18' +
+           'L0,8L-11.76,16.18L-7.61,2.47L-19.02,-6.18L-4.70,-6.47Z';
+
+Plotly.newPlot('plot1', [{
+    x: [1, 2, 3, 4, 5],
+    y: [2, 3, 4, 3, 2],
+    mode: 'markers+lines',
+    marker: {
+        // mix path strings and built-in symbol names
+        symbol: [HEART, STAR, 'circle', STAR, HEART],
+        size: 25,
+        color: ['red', 'gold', 'blue', 'gold', 'red']
+    }
+}]);
+ + +

2. Per-Point Shapes Driven by Data

+

Build a per-point symbol array from your data instead of using a function.

+
+
+var DIAMOND     = 'M20,0L0,20L-20,0L0,-20Z';
+var BIG_DIAMOND = 'M28,0L0,28L-28,0L0,-28Z';  // 1.4× size
+var STAR = '...';  // (same as Demo 1)
+
+var types = ['normal', 'big', 'star', 'normal'];
+var symbols = types.map(function(t) {
+    if (t === 'big')  return BIG_DIAMOND;
+    if (t === 'star') return STAR;
+    return DIAMOND;
+});
+
+Plotly.newPlot('plot2', [{
+    x: [1, 2, 3, 4],
+    y: [1, 1, 1, 1],
+    mode: 'markers',
+    marker: { symbol: symbols, size: 25, color: '#10b981' }
+}]);
+ + +

3. Weather Map with Rotation

+

Per-point symbols + marker.angle for wind direction rotation via SVG.

+
+
+// Paths at r=20 for each weather type
+var SUN_PATH   = 'M10,0A10,10 0 1,1 0,-10A10,10 0 0,1 10,0Z' + /* rays... */;
+var CLOUD_PATH = 'M-12,4A7,7 0 1,1 -2,-4A8,8 0 1,1 10,-2A6,6 0 1,1 14,4L-12,4Z';
+var WIND_PATHS = {
+    1: 'M0,24L0,-24M0,-24L12,-18',
+    2: 'M0,24L0,-24M0,-24L12,-18M0,-18L12,-12',
+    3: 'M0,24L0,-24M0,-24L12,-18M0,-18L12,-12M0,-12L12,-6'
+};
+
+var symbols = locations.map(function(l) {
+    if (l.weather.type === 'sunny')  return SUN_PATH;
+    if (l.weather.type === 'cloudy') return CLOUD_PATH;
+    return WIND_PATHS[l.weather.speed];
+});
+
+Plotly.newPlot('plot3', [{
+    mode: 'markers+text',
+    marker: {
+        symbol: symbols,
+        angle: locations.map(l => l.weather.direction || 0),  // SVG rotate()
+        size: 30   // scale = 30/20 = 1.5×
+    }
+}]);
+ + + + diff --git a/devtools/demos/weather_map_demo.html b/devtools/demos/weather_map_demo.html new file mode 100644 index 00000000000..024b11aab03 --- /dev/null +++ b/devtools/demos/weather_map_demo.html @@ -0,0 +1,114 @@ + + + + + Weather Map Demo - SVG Symbol/Use Rendering + + + + +

Weather Map Demo

+

Custom markers using static SVG path strings at r=20, scaled by size/20.

+
+ New API: marker.symbol accepts an array of SVG path strings + precomputed at r=20. Rotation is applied via marker.angle using + transform="rotate()" on each <use> element. +
+
+
+ Legend: ☀️ Sunny | ☁️ Cloudy | 🌬️ Wind (more barbs = stronger) +
+ + + + diff --git a/draftlogs/7653_add.md b/draftlogs/7653_add.md new file mode 100644 index 00000000000..d5bf2455325 --- /dev/null +++ b/draftlogs/7653_add.md @@ -0,0 +1 @@ +- Add custom marker symbol support [#7653](https://github.com/plotly/plotly.js/pull/7653) diff --git a/package-lock.json b/package-lock.json index 5867cccd72f..86427b191a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,6 @@ "mouse-event-offset": "^3.0.2", "mouse-wheel": "^1.2.0", "native-promise-only": "^0.8.1", - "parse-svg-path": "^0.1.2", "point-in-polygon": "^1.1.0", "polybooljs": "^1.2.2", "probe-image-size": "^7.2.3", @@ -104,6 +103,7 @@ "minify-stream": "^2.1.0", "npm-link-check": "^5.0.1", "open": "^8.4.2", + "parse-svg-path": "^0.1.2", "pixelmatch": "^5.3.0", "prepend-file": "^2.0.1", "prettysize": "^2.0.0", @@ -454,7 +454,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -478,7 +477,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -968,6 +966,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, + "peer": true, "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -982,6 +981,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", "dev": true, + "peer": true, "engines": { "node": ">=6.0.0" } @@ -991,6 +991,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, + "peer": true, "engines": { "node": ">=6.0.0" } @@ -1000,6 +1001,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", "dev": true, + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -1016,6 +1018,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, + "peer": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1440,7 +1443,8 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/geojson": { "version": "7946.0.14", @@ -1670,6 +1674,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", "dev": true, + "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6" @@ -1679,25 +1684,29 @@ "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", "dev": true, + "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.11.6", "@webassemblyjs/helper-api-error": "1.11.6", @@ -1708,13 +1717,15 @@ "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", "dev": true, + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", @@ -1727,6 +1738,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", "dev": true, + "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -1736,6 +1748,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", "dev": true, + "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -1744,13 +1757,15 @@ "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", "dev": true, + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", @@ -1767,6 +1782,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", "dev": true, + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", @@ -1780,6 +1796,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", "dev": true, + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", @@ -1792,6 +1809,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", "dev": true, + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", @@ -1806,6 +1824,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", "dev": true, + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" @@ -1815,13 +1834,15 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/abs-svg-path": { "version": "0.1.1", @@ -1847,7 +1868,6 @@ "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1888,7 +1908,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2450,7 +2469,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "peer": true }, "node_modules/canvas": { "version": "3.1.0", @@ -2459,7 +2479,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "node-addon-api": "^7.0.0", "prebuild-install": "^7.1.1" @@ -2613,6 +2632,7 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", "dev": true, + "peer": true, "engines": { "node": ">=6.0" } @@ -3848,7 +3868,8 @@ "version": "1.5.6", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz", "integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/element-size": { "version": "1.1.1", @@ -4005,7 +4026,8 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -4071,7 +4093,6 @@ "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", "dev": true, "hasInstallScript": true, - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -4249,6 +4270,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "peer": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -4261,6 +4283,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", "dev": true, + "peer": true, "engines": { "node": ">=4.0" } @@ -4949,7 +4972,8 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/glob/node_modules/brace-expansion": { "version": "2.0.2", @@ -6224,6 +6248,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, + "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -6238,6 +6263,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "peer": true, "engines": { "node": ">=8" } @@ -6247,6 +6273,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -6382,7 +6409,6 @@ "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.2.tgz", "integrity": "sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ==", "dev": true, - "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -6480,7 +6506,6 @@ "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-3.3.1.tgz", "integrity": "sha512-Nxh7eX9mOQMyK0VSsMxdod+bcqrR/ikrmEiWj5M6fwuQ7oI+YEF1FckaDsWfs6TIpULm9f0fTKMjF7XcrvWyqQ==", "dev": true, - "peer": true, "dependencies": { "jasmine-core": "^3.5.0" }, @@ -6610,6 +6635,7 @@ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "dev": true, + "peer": true, "engines": { "node": ">=6.11.5" } @@ -7052,7 +7078,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "dev": true, + "peer": true }, "node_modules/merge2": { "version": "1.4.1", @@ -7414,7 +7441,8 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/next-tick": { "version": "1.1.0", @@ -7458,7 +7486,8 @@ "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "dev": true + "dev": true, + "peer": true }, "node_modules/node-source-walk": { "version": "7.0.0", @@ -8117,7 +8146,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.1", @@ -8431,6 +8459,7 @@ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, + "peer": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -9055,6 +9084,7 @@ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "peer": true, "dependencies": { "randombytes": "^2.1.0" } @@ -9943,6 +9973,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dev": true, + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", @@ -9978,6 +10009,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -9990,6 +10022,7 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", "dev": true, + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -10331,7 +10364,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10468,6 +10500,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "escalade": "^3.1.2", "picocolors": "^1.0.1" @@ -10606,6 +10639,7 @@ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, + "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -10697,6 +10731,7 @@ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", "dev": true, + "peer": true, "engines": { "node": ">=10.13.0" } @@ -10720,6 +10755,7 @@ "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dev": true, + "peer": true, "peerDependencies": { "acorn": "^8" } @@ -10729,6 +10765,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" diff --git a/package.json b/package.json index 61bb926e518..43637b008f2 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,6 @@ "mouse-event-offset": "^3.0.2", "mouse-wheel": "^1.2.0", "native-promise-only": "^0.8.1", - "parse-svg-path": "^0.1.2", "point-in-polygon": "^1.1.0", "polybooljs": "^1.2.2", "probe-image-size": "^7.2.3", @@ -162,6 +161,7 @@ "minify-stream": "^2.1.0", "npm-link-check": "^5.0.1", "open": "^8.4.2", + "parse-svg-path": "^0.1.2", "pixelmatch": "^5.3.0", "prepend-file": "^2.0.1", "prettysize": "^2.0.0", diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index 861df3131a5..f555970ce04 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -111,9 +111,18 @@ drawing.translatePoint = function (d, sel, xa, ya) { var y = ya.c2p(d.y); if (isNumeric(x) && isNumeric(y) && sel.node()) { - // for multiline text this works better if (sel.node().nodeName === 'text') { sel.attr('x', x).attr('y', y); + } else if (sel.node().nodeName === 'use') { + // For markers: preserve the non-translate suffix (scale/rotate) set by singlePointStyle + // Read directly from DOM node since sel may be a d3 transition + var node = sel.node(); + var scale = node.getAttribute('data-scale'); + var rot = node.getAttribute('data-rot'); + var t = strTranslate(x, y); + if (rot) t += ' ' + rot; + if (scale) t += ' scale(' + scale + ')'; + sel.attr('transform', t); } else { sel.attr('transform', strTranslate(x, y)); } @@ -322,7 +331,7 @@ drawing.fillGroupStyle = function (s, gd, forLegend) { var SYMBOLDEFS = require('./symbol_defs'); drawing.symbolNames = []; -drawing.symbolFuncs = []; +drawing.symbolPaths = []; drawing.symbolBackOffs = []; drawing.symbolNeedLines = {}; drawing.symbolNoDot = {}; @@ -342,7 +351,7 @@ Object.keys(SYMBOLDEFS).forEach(function (k) { k + '-open' ); drawing.symbolNames[n] = k; - drawing.symbolFuncs[n] = symDef.f; + drawing.symbolPaths[n] = symDef.p; drawing.symbolBackOffs[n] = symDef.backoff || 0; if (symDef.needLine) { @@ -367,35 +376,108 @@ Object.keys(SYMBOLDEFS).forEach(function (k) { }); var MAXSYMBOL = drawing.symbolNames.length; -// add a dot in the middle of the symbol var DOTPATH = 'M0,0.5L0.5,0L0,-0.5L-0.5,0Z'; +drawing.symbolDotPath = DOTPATH; + +// Pre-build all four variant paths for every symbol, indexed by the legacy +// numeric code (same encoding the user types as `symbol: N`): +// [0, 100) closed (base path) +// [100, 200) open (same base path — open/closed is CSS-only) +// [200, 300) closed-dot (base + dot sub-path) +// [300, 400) open-dot (same as closed-dot — open is CSS-only) +// +// Symbols with noDot leave the dot-variant slots undefined (those variants are invalid). +for(var _i = 0; _i < MAXSYMBOL; _i++) { + drawing.symbolPaths[_i + 100] = drawing.symbolPaths[_i]; // open = same path + if(!drawing.symbolNoDot[_i]) { + drawing.symbolPaths[_i + 200] = drawing.symbolPaths[_i] + DOTPATH; + drawing.symbolPaths[_i + 300] = drawing.symbolPaths[_i] + DOTPATH; + } +} -drawing.symbolNumber = function (v) { +/** + * Unified symbol lookup. + * Accepts a built-in name ('circle', 'circle-open', 'circle-dot', …), + * a legacy numeric code (0, 100, 200, 300, …), or a raw SVG path string + * (any string starting with 'M'/'m'). + * + * Returns {n, path, open, dot, backoff, noDot, noFill}. + * n matches the legacy numeric encoding unambiguously: + * n = idx + (open ? 100 : 0) + (dot ? 200 : 0) + * so lookupSymbol(100).n === 100, lookupSymbol('circle-open').n === 100, etc. + * n is null for custom SVG paths (id assigned per-SVG by ensureSymbolDef). + * Throws an Error for unrecognised input. + */ +drawing.lookupSymbol = function (v) { + // Raw SVG path — no deterministic n; ensureSymbolDef will assign a per-SVG id. + if (typeof v === 'string' && /^[Mm]/.test(v)) { + return { n: null, path: v, open: false, dot: false, backoff: 0, noDot: false, noFill: false }; + } + + var name, open = false, dot = false, idx; if (isNumeric(v)) { - v = +v; + var n = Math.floor(Math.max(+v, 0)); + if (n >= 400) throw new Error('Unknown marker symbol: ' + v); + open = n % 200 >= 100; + dot = n >= 200; + idx = n % 100; + if (idx >= MAXSYMBOL) throw new Error('Unknown marker symbol: ' + v); + name = drawing.symbolNames[idx]; } else if (typeof v === 'string') { - var vbase = 0; - if (v.indexOf('-open') > 0) { - vbase = 100; - v = v.replace('-open', ''); - } - if (v.indexOf('-dot') > 0) { - vbase += 200; - v = v.replace('-dot', ''); - } - v = drawing.symbolNames.indexOf(v); - if (v >= 0) { - v += vbase; - } + if (v.indexOf('-open') > 0) { open = true; v = v.replace('-open', ''); } + if (v.indexOf('-dot') > 0) { dot = true; v = v.replace('-dot', ''); } + idx = drawing.symbolNames.indexOf(v); + if (idx < 0) throw new Error('Unknown marker symbol: ' + v); + name = v; + } else { + throw new Error('Unknown marker symbol: ' + v); } - return v % 100 >= MAXSYMBOL || v >= 400 ? 0 : Math.floor(Math.max(v, 0)); + var symN = idx + (open ? 100 : 0) + (dot ? 200 : 0); + return { + n: symN, + name: name, + path: drawing.symbolPaths[symN], + open: open, + dot: dot, + backoff: drawing.symbolBackOffs[idx] || 0, + noDot: !!drawing.symbolNoDot[idx], + noFill: !!drawing.symbolNoFill[idx] + }; }; -function makePointPath(symbolNumber, r, t, s) { - var base = symbolNumber % 100; - return drawing.symbolFuncs[base](r, t, s) + (symbolNumber >= 200 ? DOTPATH : ''); -} +// sym.n already equals the legacy numeric encoding, so symbolNumber is a simple wrapper. +drawing.symbolNumber = function (v) { + return drawing.lookupSymbol(v).n; +}; + +drawing.ensureSymbolDef = function (gd, sym) { + var defs = gd._fullLayout._defs; + var node = defs.node(); + // Per-SVG map: built-ins keyed by sym.n (each variant gets its own ); + // custom SVG paths keyed by path string → 'c0', 'c1', … + // Stored on the DOM node so it is freed when the SVG is removed. + var symMap = node._symMap || (node._symMap = {}); + + // Use sym.n as key for built-ins; sym.path for custom (sym.n === null). + var key = sym.n !== null ? sym.n : sym.path; + if(key in symMap) return symMap[key]; + + var id; + if(sym.n !== null) { + id = sym.name + (sym.open ? '-open' : '') + (sym.dot ? '-dot' : ''); + } else { + if(!node._customSymCount) node._customSymCount = 0; + id = 'c' + node._customSymCount++; + } + symMap[key] = id; + + defs.append('symbol') + .attr('id', id) + .attr('overflow', 'visible') + .append('path').attr('d', sym.path); + return id; +}; var stopFormatter = numberFormat('~f'); var gradientInfo = { @@ -914,17 +996,36 @@ drawing.singlePointStyle = function (d, sel, trace, fns, gd, pt) { r = d.mrc = fns.selectedSizeFn(d); } - // turn the symbol into a sanitized number - var x = drawing.symbolNumber(d.mx || marker.symbol) || 0; + var symbolValue = d.mx || marker.symbol; + var sym = drawing.lookupSymbol(symbolValue); - // save if this marker is open - // because that impacts how to handle colors - d.om = x % 200 >= 100; + // save if this marker is open (impacts color handling) + d.om = sym.open; var angle = getMarkerAngle(d, trace); var standoff = getMarkerStandoff(d, trace); - - sel.attr('d', makePointPath(x, r, angle, standoff)); + var scale = r / 20; + + // Build rotation/standoff suffix for transforms + var rot = ''; + if (angle) rot += 'rotate(' + angle + ')'; + if (standoff) rot += (rot ? ' ' : '') + 'translate(0,' + standoff + ')'; + + sel.attr('href', '#' + drawing.ensureSymbolDef(gd, sym)) + .attr('data-scale', scale) + .attr('data-rot', rot || null); + + // Update full transform: keep existing translate (from translatePoint), append rot+scale + // Use node.getAttribute() since sel may be a d3 transition (no getter support) + var node = sel.node(); + var curT = node ? (node.getAttribute('transform') || '') : ''; + var tPart = curT.match(/^translate\([^)]*\)/); + var newT = (tPart ? tPart[0] : ''); + if (rot) newT += ' ' + rot; + newT += ' scale(' + scale + ')'; + sel.attr('transform', newT.trim()); + + sel.style('vector-effect', gd._context.staticPlot ? 'none' : 'non-scaling-stroke'); } var perPointGradient = false; @@ -1201,13 +1302,16 @@ drawing.selectedPointStyle = function (s, trace) { if (fns.selectedSizeFn) { seq.push(function (pt, d) { - var mx = d.mx || marker.symbol || 0; var mrc2 = fns.selectedSizeFn(d); - - pt.attr( - 'd', - makePointPath(drawing.symbolNumber(mx), mrc2, getMarkerAngle(d, trace), getMarkerStandoff(d, trace)) - ); + var scale = mrc2 / 20; + var node = pt.node(); + var rot = node ? (node.getAttribute('data-rot') || '') : ''; + var curT = node ? (node.getAttribute('transform') || '') : ''; + var tPart = curT.match(/^translate\([^)]*\)/); + var newT = (tPart ? tPart[0] : ''); + if (rot) newT += ' ' + rot; + newT += ' scale(' + scale + ')'; + pt.attr('data-scale', scale).attr('transform', newT.trim()); // save for Drawing.selectedTextStyle d.mrc2 = mrc2; @@ -1498,7 +1602,12 @@ function applyBackoff(pt, start) { var endMarkerSize = endMarker.size; if (Lib.isArrayOrTypedArray(endMarkerSize)) endMarkerSize = endMarkerSize[endI]; - b = endMarker ? drawing.symbolBackOffs[drawing.symbolNumber(endMarkerSymbol)] * endMarkerSize : 0; + if(endMarker) { + var endMarkerSym = drawing.lookupSymbol(endMarkerSymbol); + b = (endMarkerSym.backoff || 0) * endMarkerSize; + } else { + b = 0; + } b += drawing.getMarkerStandoff(d[endI], trace) || 0; } diff --git a/src/components/drawing/symbol_defs.js b/src/components/drawing/symbol_defs.js index 3aab9be891c..0a807de8f12 100644 --- a/src/components/drawing/symbol_defs.js +++ b/src/components/drawing/symbol_defs.js @@ -1,813 +1,302 @@ 'use strict'; -var parseSvgPath = require('parse-svg-path'); -var round = // require('@plotly/d3').round; - function(x, n) { - return n ? Math.round(x * (n = Math.pow(10, n))) / n : Math.round(x); - }; - /** Marker symbol definitions * users can specify markers either by number or name * add 100 (or '-open') and you get an open marker * open markers have no fill and use line color as the stroke color * add 200 (or '-dot') and you get a dot in the middle * add both and you get both + * + * Each symbol has a `p` property: the SVG path string for r=20, centered at origen. + * All coordinates are integers. */ - -var emptyPath = 'M0,0Z'; -var sqrt2 = Math.sqrt(2); -var sqrt3 = Math.sqrt(3); -var PI = Math.PI; -var cos = Math.cos; -var sin = Math.sin; - module.exports = { circle: { n: 0, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rs = round(r, 2); - var circle = 'M' + rs + ',0A' + rs + ',' + rs + ' 0 1,1 0,-' + rs + 'A' + rs + ',' + rs + ' 0 0,1 ' + rs + ',0Z'; - return standoff ? align(angle, standoff, circle) : circle; - } + p: 'M20,0A20,20 0 1,1 0,-20A20,20 0 0,1 20,0Z' }, square: { n: 1, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rs = round(r, 2); - return align(angle, standoff, 'M' + rs + ',' + rs + 'H-' + rs + 'V-' + rs + 'H' + rs + 'Z'); - } + p: 'M20,20H-20V-20H20Z' }, diamond: { n: 2, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rd = round(r * 1.3, 2); - return align(angle, standoff, 'M' + rd + ',0L0,' + rd + 'L-' + rd + ',0L0,-' + rd + 'Z'); - } + p: 'M26,0L0,26L-26,0L0,-26Z' }, cross: { n: 3, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rc = round(r * 0.4, 2); - var rc2 = round(r * 1.2, 2); - return align(angle, standoff, 'M' + rc2 + ',' + rc + 'H' + rc + 'V' + rc2 + 'H-' + rc + - 'V' + rc + 'H-' + rc2 + 'V-' + rc + 'H-' + rc + 'V-' + rc2 + - 'H' + rc + 'V-' + rc + 'H' + rc2 + 'Z'); - } + p: 'M24,8H8V24H-8V8H-24V-8H-8V-24H8V-8H24Z' }, x: { n: 4, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rx = round(r * 0.8 / sqrt2, 2); - var ne = 'l' + rx + ',' + rx; - var se = 'l' + rx + ',-' + rx; - var sw = 'l-' + rx + ',-' + rx; - var nw = 'l-' + rx + ',' + rx; - return align(angle, standoff, 'M0,' + rx + ne + se + sw + se + sw + nw + sw + nw + ne + nw + ne + 'Z'); - } + p: 'M0,11l11,11l11,-11l-11,-11l11,-11l-11,-11l-11,11l-11,-11l-11,11l11,11l-11,11l11,11Z' }, 'triangle-up': { n: 5, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rt = round(r * 2 / sqrt3, 2); - var r2 = round(r / 2, 2); - var rs = round(r, 2); - return align(angle, standoff, 'M-' + rt + ',' + r2 + 'H' + rt + 'L0,-' + rs + 'Z'); - } + p: 'M-23,10H23L0,-20Z' }, 'triangle-down': { n: 6, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rt = round(r * 2 / sqrt3, 2); - var r2 = round(r / 2, 2); - var rs = round(r, 2); - return align(angle, standoff, 'M-' + rt + ',-' + r2 + 'H' + rt + 'L0,' + rs + 'Z'); - } + p: 'M-23,-10H23L0,20Z' }, 'triangle-left': { n: 7, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rt = round(r * 2 / sqrt3, 2); - var r2 = round(r / 2, 2); - var rs = round(r, 2); - return align(angle, standoff, 'M' + r2 + ',-' + rt + 'V' + rt + 'L-' + rs + ',0Z'); - } + p: 'M10,-23V23L-20,0Z' }, 'triangle-right': { n: 8, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rt = round(r * 2 / sqrt3, 2); - var r2 = round(r / 2, 2); - var rs = round(r, 2); - return align(angle, standoff, 'M-' + r2 + ',-' + rt + 'V' + rt + 'L' + rs + ',0Z'); - } + p: 'M-10,-23V23L20,0Z' }, 'triangle-ne': { n: 9, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var r1 = round(r * 0.6, 2); - var r2 = round(r * 1.2, 2); - return align(angle, standoff, 'M-' + r2 + ',-' + r1 + 'H' + r1 + 'V' + r2 + 'Z'); - } + p: 'M-24,-12H12V24Z' }, 'triangle-se': { n: 10, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var r1 = round(r * 0.6, 2); - var r2 = round(r * 1.2, 2); - return align(angle, standoff, 'M' + r1 + ',-' + r2 + 'V' + r1 + 'H-' + r2 + 'Z'); - } + p: 'M12,-24V12H-24Z' }, 'triangle-sw': { n: 11, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var r1 = round(r * 0.6, 2); - var r2 = round(r * 1.2, 2); - return align(angle, standoff, 'M' + r2 + ',' + r1 + 'H-' + r1 + 'V-' + r2 + 'Z'); - } + p: 'M24,12H-12V-24Z' }, 'triangle-nw': { n: 12, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var r1 = round(r * 0.6, 2); - var r2 = round(r * 1.2, 2); - return align(angle, standoff, 'M-' + r1 + ',' + r2 + 'V-' + r1 + 'H' + r2 + 'Z'); - } + p: 'M-12,24V-12H24Z' }, pentagon: { n: 13, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var x1 = round(r * 0.951, 2); - var x2 = round(r * 0.588, 2); - var y0 = round(-r, 2); - var y1 = round(r * -0.309, 2); - var y2 = round(r * 0.809, 2); - return align(angle, standoff, 'M' + x1 + ',' + y1 + 'L' + x2 + ',' + y2 + 'H-' + x2 + - 'L-' + x1 + ',' + y1 + 'L0,' + y0 + 'Z'); - } + p: 'M19,-6L12,16H-12L-19,-6L0,-20Z' }, hexagon: { n: 14, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var y0 = round(r, 2); - var y1 = round(r / 2, 2); - var x = round(r * sqrt3 / 2, 2); - return align(angle, standoff, 'M' + x + ',-' + y1 + 'V' + y1 + 'L0,' + y0 + - 'L-' + x + ',' + y1 + 'V-' + y1 + 'L0,-' + y0 + 'Z'); - } + p: 'M17,-10V10L0,20L-17,10V-10L0,-20Z' }, hexagon2: { n: 15, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var x0 = round(r, 2); - var x1 = round(r / 2, 2); - var y = round(r * sqrt3 / 2, 2); - return align(angle, standoff, 'M-' + x1 + ',' + y + 'H' + x1 + 'L' + x0 + - ',0L' + x1 + ',-' + y + 'H-' + x1 + 'L-' + x0 + ',0Z'); - } + p: 'M-10,17H10L20,0L10,-17H-10L-20,0Z' }, octagon: { n: 16, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var a = round(r * 0.924, 2); - var b = round(r * 0.383, 2); - return align(angle, standoff, 'M-' + b + ',-' + a + 'H' + b + 'L' + a + ',-' + b + 'V' + b + - 'L' + b + ',' + a + 'H-' + b + 'L-' + a + ',' + b + 'V-' + b + 'Z'); - } + p: 'M-8,-18H8L18,-8V8L8,18H-8L-18,8V-8Z' }, star: { n: 17, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rs = r * 1.4; - var x1 = round(rs * 0.225, 2); - var x2 = round(rs * 0.951, 2); - var x3 = round(rs * 0.363, 2); - var x4 = round(rs * 0.588, 2); - var y0 = round(-rs, 2); - var y1 = round(rs * -0.309, 2); - var y3 = round(rs * 0.118, 2); - var y4 = round(rs * 0.809, 2); - var y5 = round(rs * 0.382, 2); - return align(angle, standoff, 'M' + x1 + ',' + y1 + 'H' + x2 + 'L' + x3 + ',' + y3 + - 'L' + x4 + ',' + y4 + 'L0,' + y5 + 'L-' + x4 + ',' + y4 + - 'L-' + x3 + ',' + y3 + 'L-' + x2 + ',' + y1 + 'H-' + x1 + - 'L0,' + y0 + 'Z'); - } + p: 'M6,-9H27L10,3L16,23L0,11L-16,23L-10,3L-27,-9H-6L0,-28Z' }, hexagram: { n: 18, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var y = round(r * 0.66, 2); - var x1 = round(r * 0.38, 2); - var x2 = round(r * 0.76, 2); - return align(angle, standoff, 'M-' + x2 + ',0l-' + x1 + ',-' + y + 'h' + x2 + - 'l' + x1 + ',-' + y + 'l' + x1 + ',' + y + 'h' + x2 + - 'l-' + x1 + ',' + y + 'l' + x1 + ',' + y + 'h-' + x2 + - 'l-' + x1 + ',' + y + 'l-' + x1 + ',-' + y + 'h-' + x2 + 'Z'); - } + p: 'M-15,0l-8,-13h15l8,-13l8,13h15l-8,13l8,13h-15l-8,13l-8,-13h-15Z' }, 'star-triangle-up': { n: 19, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var x = round(r * sqrt3 * 0.8, 2); - var y1 = round(r * 0.8, 2); - var y2 = round(r * 1.6, 2); - var rc = round(r * 4, 2); - var aPart = 'A ' + rc + ',' + rc + ' 0 0 1 '; - return align(angle, standoff, 'M-' + x + ',' + y1 + aPart + x + ',' + y1 + - aPart + '0,-' + y2 + aPart + '-' + x + ',' + y1 + 'Z'); - } + p: 'M-28,16A80,80 0 0 1 28,16A80,80 0 0 1 0,-32A80,80 0 0 1 -28,16Z' }, 'star-triangle-down': { n: 20, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var x = round(r * sqrt3 * 0.8, 2); - var y1 = round(r * 0.8, 2); - var y2 = round(r * 1.6, 2); - var rc = round(r * 4, 2); - var aPart = 'A ' + rc + ',' + rc + ' 0 0 1 '; - return align(angle, standoff, 'M' + x + ',-' + y1 + aPart + '-' + x + ',-' + y1 + - aPart + '0,' + y2 + aPart + x + ',-' + y1 + 'Z'); - } + p: 'M28,-16A80,80 0 0 1 -28,-16A80,80 0 0 1 0,32A80,80 0 0 1 28,-16Z' }, 'star-square': { n: 21, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rp = round(r * 1.1, 2); - var rc = round(r * 2, 2); - var aPart = 'A ' + rc + ',' + rc + ' 0 0 1 '; - return align(angle, standoff, 'M-' + rp + ',-' + rp + aPart + '-' + rp + ',' + rp + - aPart + rp + ',' + rp + aPart + rp + ',-' + rp + - aPart + '-' + rp + ',-' + rp + 'Z'); - } + p: 'M-22,-22A40,40 0 0 1 -22,22A40,40 0 0 1 22,22A40,40 0 0 1 22,-22A40,40 0 0 1 -22,-22Z' }, 'star-diamond': { n: 22, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rp = round(r * 1.4, 2); - var rc = round(r * 1.9, 2); - var aPart = 'A ' + rc + ',' + rc + ' 0 0 1 '; - return align(angle, standoff, 'M-' + rp + ',0' + aPart + '0,' + rp + - aPart + rp + ',0' + aPart + '0,-' + rp + - aPart + '-' + rp + ',0' + 'Z'); - } + p: 'M-28,0A38,38 0 0 1 0,28A38,38 0 0 1 28,0A38,38 0 0 1 0,-28A38,38 0 0 1 -28,0Z' }, 'diamond-tall': { n: 23, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var x = round(r * 0.7, 2); - var y = round(r * 1.4, 2); - return align(angle, standoff, 'M0,' + y + 'L' + x + ',0L0,-' + y + 'L-' + x + ',0Z'); - } + p: 'M0,28L14,0L0,-28L-14,0Z' }, 'diamond-wide': { n: 24, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var x = round(r * 1.4, 2); - var y = round(r * 0.7, 2); - return align(angle, standoff, 'M0,' + y + 'L' + x + ',0L0,-' + y + 'L-' + x + ',0Z'); - } + p: 'M0,14L28,0L0,-14L-28,0Z' }, hourglass: { n: 25, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rs = round(r, 2); - return align(angle, standoff, 'M' + rs + ',' + rs + 'H-' + rs + 'L' + rs + ',-' + rs + 'H-' + rs + 'Z'); - }, + p: 'M20,20H-20L20,-20H-20Z', noDot: true }, bowtie: { n: 26, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rs = round(r, 2); - return align(angle, standoff, 'M' + rs + ',' + rs + 'V-' + rs + 'L-' + rs + ',' + rs + 'V-' + rs + 'Z'); - }, + p: 'M20,20V-20L-20,20V-20Z', noDot: true }, 'circle-cross': { n: 27, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rs = round(r, 2); - return align(angle, standoff, 'M0,' + rs + 'V-' + rs + 'M' + rs + ',0H-' + rs + - 'M' + rs + ',0A' + rs + ',' + rs + ' 0 1,1 0,-' + rs + - 'A' + rs + ',' + rs + ' 0 0,1 ' + rs + ',0Z'); - }, + p: 'M0,20V-20M20,0H-20M20,0A20,20 0 1,1 0,-20A20,20 0 0,1 20,0Z', needLine: true, noDot: true }, 'circle-x': { n: 28, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rs = round(r, 2); - var rc = round(r / sqrt2, 2); - return align(angle, standoff, 'M' + rc + ',' + rc + 'L-' + rc + ',-' + rc + - 'M' + rc + ',-' + rc + 'L-' + rc + ',' + rc + - 'M' + rs + ',0A' + rs + ',' + rs + ' 0 1,1 0,-' + rs + - 'A' + rs + ',' + rs + ' 0 0,1 ' + rs + ',0Z'); - }, + p: 'M14,14L-14,-14M14,-14L-14,14M20,0A20,20 0 1,1 0,-20A20,20 0 0,1 20,0Z', needLine: true, noDot: true }, 'square-cross': { n: 29, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rs = round(r, 2); - return align(angle, standoff, 'M0,' + rs + 'V-' + rs + 'M' + rs + ',0H-' + rs + - 'M' + rs + ',' + rs + 'H-' + rs + 'V-' + rs + 'H' + rs + 'Z'); - }, + p: 'M0,20V-20M20,0H-20M20,20H-20V-20H20Z', needLine: true, noDot: true }, 'square-x': { n: 30, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rs = round(r, 2); - return align(angle, standoff, 'M' + rs + ',' + rs + 'L-' + rs + ',-' + rs + - 'M' + rs + ',-' + rs + 'L-' + rs + ',' + rs + - 'M' + rs + ',' + rs + 'H-' + rs + 'V-' + rs + 'H' + rs + 'Z'); - }, + p: 'M20,20L-20,-20M20,-20L-20,20M20,20H-20V-20H20Z', needLine: true, noDot: true }, 'diamond-cross': { n: 31, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rd = round(r * 1.3, 2); - return align(angle, standoff, 'M' + rd + ',0L0,' + rd + 'L-' + rd + ',0L0,-' + rd + 'Z' + - 'M0,-' + rd + 'V' + rd + 'M-' + rd + ',0H' + rd); - }, + p: 'M26,0L0,26L-26,0L0,-26ZM0,-26V26M-26,0H26', needLine: true, noDot: true }, 'diamond-x': { n: 32, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rd = round(r * 1.3, 2); - var r2 = round(r * 0.65, 2); - return align(angle, standoff, 'M' + rd + ',0L0,' + rd + 'L-' + rd + ',0L0,-' + rd + 'Z' + - 'M-' + r2 + ',-' + r2 + 'L' + r2 + ',' + r2 + - 'M-' + r2 + ',' + r2 + 'L' + r2 + ',-' + r2); - }, + p: 'M26,0L0,26L-26,0L0,-26ZM-13,-13L13,13M-13,13L13,-13', needLine: true, noDot: true }, 'cross-thin': { n: 33, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rc = round(r * 1.4, 2); - return align(angle, standoff, 'M0,' + rc + 'V-' + rc + 'M' + rc + ',0H-' + rc); - }, + p: 'M0,28V-28M28,0H-28', needLine: true, noDot: true, noFill: true }, 'x-thin': { n: 34, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rx = round(r, 2); - return align(angle, standoff, 'M' + rx + ',' + rx + 'L-' + rx + ',-' + rx + - 'M' + rx + ',-' + rx + 'L-' + rx + ',' + rx); - }, + p: 'M20,20L-20,-20M20,-20L-20,20', needLine: true, noDot: true, noFill: true }, asterisk: { n: 35, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rc = round(r * 1.2, 2); - var rs = round(r * 0.85, 2); - return align(angle, standoff, 'M0,' + rc + 'V-' + rc + 'M' + rc + ',0H-' + rc + - 'M' + rs + ',' + rs + 'L-' + rs + ',-' + rs + - 'M' + rs + ',-' + rs + 'L-' + rs + ',' + rs); - }, + p: 'M0,24V-24M24,0H-24M17,17L-17,-17M17,-17L-17,17', needLine: true, noDot: true, noFill: true }, hash: { n: 36, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var r1 = round(r / 2, 2); - var r2 = round(r, 2); - - return align(angle, standoff, 'M' + r1 + ',' + r2 + 'V-' + r2 + - 'M' + (r1 - r2) + ',-' + r2 + 'V' + r2 + - 'M' + r2 + ',' + r1 + 'H-' + r2 + - 'M-' + r2 + ',' + (r1 - r2) + 'H' + r2); - }, + p: 'M10,20V-20M-10,-20V20M20,10H-20M-20,-10H20', needLine: true, noFill: true }, 'y-up': { n: 37, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var x = round(r * 1.2, 2); - var y0 = round(r * 1.6, 2); - var y1 = round(r * 0.8, 2); - return align(angle, standoff, 'M-' + x + ',' + y1 + 'L0,0M' + x + ',' + y1 + 'L0,0M0,-' + y0 + 'L0,0'); - }, + p: 'M-24,16L0,0M24,16L0,0M0,-32L0,0', needLine: true, noDot: true, noFill: true }, 'y-down': { n: 38, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var x = round(r * 1.2, 2); - var y0 = round(r * 1.6, 2); - var y1 = round(r * 0.8, 2); - return align(angle, standoff, 'M-' + x + ',-' + y1 + 'L0,0M' + x + ',-' + y1 + 'L0,0M0,' + y0 + 'L0,0'); - }, + p: 'M-24,-16L0,0M24,-16L0,0M0,32L0,0', needLine: true, noDot: true, noFill: true }, 'y-left': { n: 39, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var y = round(r * 1.2, 2); - var x0 = round(r * 1.6, 2); - var x1 = round(r * 0.8, 2); - return align(angle, standoff, 'M' + x1 + ',' + y + 'L0,0M' + x1 + ',-' + y + 'L0,0M-' + x0 + ',0L0,0'); - }, + p: 'M16,24L0,0M16,-24L0,0M-32,0L0,0', needLine: true, noDot: true, noFill: true }, 'y-right': { n: 40, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var y = round(r * 1.2, 2); - var x0 = round(r * 1.6, 2); - var x1 = round(r * 0.8, 2); - return align(angle, standoff, 'M-' + x1 + ',' + y + 'L0,0M-' + x1 + ',-' + y + 'L0,0M' + x0 + ',0L0,0'); - }, + p: 'M-16,24L0,0M-16,-24L0,0M32,0L0,0', needLine: true, noDot: true, noFill: true }, 'line-ew': { n: 41, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rc = round(r * 1.4, 2); - return align(angle, standoff, 'M' + rc + ',0H-' + rc); - }, + p: 'M28,0H-28', needLine: true, noDot: true, noFill: true }, 'line-ns': { n: 42, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rc = round(r * 1.4, 2); - return align(angle, standoff, 'M0,' + rc + 'V-' + rc); - }, + p: 'M0,28V-28', needLine: true, noDot: true, noFill: true }, 'line-ne': { n: 43, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rx = round(r, 2); - return align(angle, standoff, 'M' + rx + ',-' + rx + 'L-' + rx + ',' + rx); - }, + p: 'M20,-20L-20,20', needLine: true, noDot: true, noFill: true }, 'line-nw': { n: 44, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rx = round(r, 2); - return align(angle, standoff, 'M' + rx + ',' + rx + 'L-' + rx + ',-' + rx); - }, + p: 'M20,20L-20,-20', needLine: true, noDot: true, noFill: true }, 'arrow-up': { n: 45, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rx = round(r, 2); - var ry = round(r * 2, 2); - return align(angle, standoff, 'M0,0L-' + rx + ',' + ry + 'H' + rx + 'Z'); - }, + p: 'M0,0L-20,40H20Z', backoff: 1, noDot: true }, 'arrow-down': { n: 46, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rx = round(r, 2); - var ry = round(r * 2, 2); - return align(angle, standoff, 'M0,0L-' + rx + ',-' + ry + 'H' + rx + 'Z'); - }, + p: 'M0,0L-20,-40H20Z', noDot: true }, 'arrow-left': { n: 47, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rx = round(r * 2, 2); - var ry = round(r, 2); - return align(angle, standoff, 'M0,0L' + rx + ',-' + ry + 'V' + ry + 'Z'); - }, + p: 'M0,0L40,-20V20Z', noDot: true }, 'arrow-right': { n: 48, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rx = round(r * 2, 2); - var ry = round(r, 2); - return align(angle, standoff, 'M0,0L-' + rx + ',-' + ry + 'V' + ry + 'Z'); - }, + p: 'M0,0L-40,-20V20Z', noDot: true }, 'arrow-bar-up': { n: 49, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rx = round(r, 2); - var ry = round(r * 2, 2); - return align(angle, standoff, 'M-' + rx + ',0H' + rx + 'M0,0L-' + rx + ',' + ry + 'H' + rx + 'Z'); - }, + p: 'M-20,0H20M0,0L-20,40H20Z', backoff: 1, needLine: true, noDot: true }, 'arrow-bar-down': { n: 50, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rx = round(r, 2); - var ry = round(r * 2, 2); - return align(angle, standoff, 'M-' + rx + ',0H' + rx + 'M0,0L-' + rx + ',-' + ry + 'H' + rx + 'Z'); - }, + p: 'M-20,0H20M0,0L-20,-40H20Z', needLine: true, noDot: true }, 'arrow-bar-left': { n: 51, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rx = round(r * 2, 2); - var ry = round(r, 2); - return align(angle, standoff, 'M0,-' + ry + 'V' + ry + 'M0,0L' + rx + ',-' + ry + 'V' + ry + 'Z'); - }, + p: 'M0,-20V20M0,0L40,-20V20Z', needLine: true, noDot: true }, 'arrow-bar-right': { n: 52, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var rx = round(r * 2, 2); - var ry = round(r, 2); - return align(angle, standoff, 'M0,-' + ry + 'V' + ry + 'M0,0L-' + rx + ',-' + ry + 'V' + ry + 'Z'); - }, + p: 'M0,-20V20M0,0L-40,-20V20Z', needLine: true, noDot: true }, arrow: { n: 53, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var headAngle = PI / 2.5; // 36 degrees - golden ratio - var x = 2 * r * cos(headAngle); - var y = 2 * r * sin(headAngle); - - return align(angle, standoff, - 'M0,0' + - 'L' + -x + ',' + y + - 'L' + x + ',' + y + - 'Z' - ); - }, + p: 'M0,0L-12,38L12,38Z', backoff: 0.9, noDot: true }, 'arrow-wide': { n: 54, - f: function(r, angle, standoff) { - if(skipAngle(angle)) return emptyPath; - - var headAngle = PI / 4; // 90 degrees - var x = 2 * r * cos(headAngle); - var y = 2 * r * sin(headAngle); - - return align(angle, standoff, - 'M0,0' + - 'L' + -x + ',' + y + - 'A ' + 2 * r + ',' + 2 * r + ' 0 0 1 ' + x + ',' + y + - 'Z' - ); - }, + p: 'M0,0L-28,28A40,40 0 0 1 28,28Z', backoff: 0.4, noDot: true } }; - -function skipAngle(angle) { - return angle === null; -} - -var lastPathIn, lastPathOut; -var lastAngle, lastStandoff; - -function align(angle, standoff, path) { - if((!angle || angle % 360 === 0) && !standoff) return path; - - if( - lastAngle === angle && - lastStandoff === standoff && - lastPathIn === path - ) return lastPathOut; - - lastAngle = angle; - lastStandoff = standoff; - lastPathIn = path; - - function rotate(t, xy) { - var cosT = cos(t); - var sinT = sin(t); - - var x = xy[0]; - var y = xy[1] + (standoff || 0); - return [ - x * cosT - y * sinT, - x * sinT + y * cosT - ]; - } - - var t = angle / 180 * PI; - - var x = 0; - var y = 0; - var cmd = parseSvgPath(path); - var str = ''; - - for(var i = 0; i < cmd.length; i++) { - var cmdI = cmd[i]; - var op = cmdI[0]; - - var x0 = x; - var y0 = y; - - if(op === 'M' || op === 'L') { - x = +cmdI[1]; - y = +cmdI[2]; - } else if(op === 'm' || op === 'l') { - x += +cmdI[1]; - y += +cmdI[2]; - } else if(op === 'H') { - x = +cmdI[1]; - } else if(op === 'h') { - x += +cmdI[1]; - } else if(op === 'V') { - y = +cmdI[1]; - } else if(op === 'v') { - y += +cmdI[1]; - } else if(op === 'A') { - x = +cmdI[1]; - y = +cmdI[2]; - - var E = rotate(t, [+cmdI[6], +cmdI[7]]); - cmdI[6] = E[0]; - cmdI[7] = E[1]; - cmdI[3] = +cmdI[3] + angle; - } - - // change from H, V, h, v to L or l - if(op === 'H' || op === 'V') op = 'L'; - if(op === 'h' || op === 'v') op = 'l'; - - if(op === 'm' || op === 'l') { - x -= x0; - y -= y0; - } - - var B = rotate(t, [x, y]); - - if(op === 'H' || op === 'V') op = 'L'; - - - if( - op === 'M' || op === 'L' || - op === 'm' || op === 'l' - ) { - cmdI[1] = B[0]; - cmdI[2] = B[1]; - } - cmdI[0] = op; - - str += cmdI[0] + cmdI.slice(1).join(','); - } - - lastPathOut = str; - - return str; -} diff --git a/src/components/legend/style.js b/src/components/legend/style.js index d2b3350c0c8..887d00a31aa 100644 --- a/src/components/legend/style.js +++ b/src/components/legend/style.js @@ -257,9 +257,9 @@ module.exports = function style(s, gd, legend) { var ptgroup = d3.select(this).select('g.legendpoints'); - var pts = ptgroup.selectAll('path.scatterpts').data(showMarker ? dMod : []); + var pts = ptgroup.selectAll('use.scatterpts').data(showMarker ? dMod : []); // make sure marker is on the bottom, in case it enters after text - pts.enter().insert('path', ':first-child').classed('scatterpts', true).attr('transform', centerTransform); + pts.enter().insert('use', ':first-child').classed('scatterpts', true).attr('transform', centerTransform); pts.exit().remove(); pts.call(Drawing.pointStyle, tMod, gd); diff --git a/src/traces/box/plot.js b/src/traces/box/plot.js index 2a8a654cad6..a071a23595f 100644 --- a/src/traces/box/plot.js +++ b/src/traces/box/plot.js @@ -198,7 +198,7 @@ function plotPoints(sel, axes, trace, t) { gPoints.exit().remove(); - var paths = gPoints.selectAll('path') + var paths = gPoints.selectAll('use') .data(function(d) { var i; var pts = d.pts2; @@ -270,7 +270,7 @@ function plotPoints(sel, axes, trace, t) { return pts; }); - paths.enter().append('path') + paths.enter().append('use') .classed('point', true); paths.exit().remove(); diff --git a/src/traces/box/style.js b/src/traces/box/style.js index 5e28eeefa27..ed03ce7b981 100644 --- a/src/traces/box/style.js +++ b/src/traces/box/style.js @@ -41,7 +41,7 @@ function style(gd, cd, sel) { }) .call(Color.stroke, trace.line.color); - var pts = el.selectAll('path.point'); + var pts = el.selectAll('use.point'); Drawing.pointStyle(pts, trace, gd); } }); @@ -49,7 +49,7 @@ function style(gd, cd, sel) { function styleOnSelect(gd, cd, sel) { var trace = cd[0].trace; - var pts = sel.selectAll('path.point'); + var pts = sel.selectAll('use.point'); if(trace.selectedpoints) { Drawing.selectedPointStyle(pts, trace); diff --git a/src/traces/scatter/plot.js b/src/traces/scatter/plot.js index 3c9e19df936..aa22cd10920 100644 --- a/src/traces/scatter/plot.js +++ b/src/traces/scatter/plot.js @@ -517,11 +517,11 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition // marker points - selection = points.selectAll('path.point'); + selection = points.selectAll('use.point'); join = selection.data(markerFilter, keyFunc); - var enter = join.enter().append('path') + var enter = join.enter().append('use') .classed('point', true); if(hasTransition) { diff --git a/src/traces/scatter/style.js b/src/traces/scatter/style.js index 734ecf9f9d0..23216de88ba 100644 --- a/src/traces/scatter/style.js +++ b/src/traces/scatter/style.js @@ -33,7 +33,7 @@ function style(gd) { } function stylePoints(sel, trace, gd) { - Drawing.pointStyle(sel.selectAll('path.point'), trace, gd); + Drawing.pointStyle(sel.selectAll('use.point'), trace, gd); } function styleText(sel, trace, gd) { @@ -44,7 +44,7 @@ function styleOnSelect(gd, cd, sel) { var trace = cd[0].trace; if(trace.selectedpoints) { - Drawing.selectedPointStyle(sel.selectAll('path.point'), trace); + Drawing.selectedPointStyle(sel.selectAll('use.point'), trace); Drawing.selectedTextStyle(sel.selectAll('text'), trace); } else { stylePoints(sel, trace, gd); diff --git a/src/traces/scattergeo/plot.js b/src/traces/scattergeo/plot.js index 9124900e8b3..4e9cae57d58 100644 --- a/src/traces/scattergeo/plot.js +++ b/src/traces/scattergeo/plot.js @@ -45,9 +45,9 @@ function plot(gd, geo, calcData) { } if(subTypes.hasMarkers(trace)) { - s.selectAll('path.point') + s.selectAll('use.point') .data(Lib.identity) - .enter().append('path') + .enter().append('use') .classed('point', true) .each(function(calcPt) { removeBADNUM(calcPt, this); }); } diff --git a/src/traces/scattergl/convert.js b/src/traces/scattergl/convert.js index 22e3f48db31..485d8a0b057 100644 --- a/src/traces/scattergl/convert.js +++ b/src/traces/scattergl/convert.js @@ -458,41 +458,77 @@ var SYMBOL_SDF_SIZE = constants.SYMBOL_SDF_SIZE; var SYMBOL_SIZE = constants.SYMBOL_SIZE; var SYMBOL_STROKE = constants.SYMBOL_STROKE; var SYMBOL_SDF = {}; -var SYMBOL_SVG_CIRCLE = Drawing.symbolFuncs[0](SYMBOL_SIZE * 0.05); +// Small circle path (r=1) used as center dot in SDF symbol rendering +var SYMBOL_SVG_CIRCLE = 'M1,0A1,1 0 1,1 0,-1A1,1 0 0,1 1,0Z'; + +// Rotate an SVG path string by angleDeg degrees around the origen, for use +// in the SDF (signed-distance-field) pipeline where svg-path-sdf only accepts +// a flat path string and has no SVG-transform support. +// SVG markers (scatter/box) rotate via transform="rotate(...)" on instead. +// Only handles M/L/H/V/A commands (sufficient for all built-in symbol paths). +function rotatePath(path, angleDeg) { + if (!angleDeg || angleDeg % 360 === 0) return path; + var t = angleDeg * Math.PI / 180; + var cosT = Math.cos(t); + var sinT = Math.sin(t); + function rot(x, y) { return [x * cosT - y * sinT, x * sinT + y * cosT]; } + // Parse path commands with a simple regex + return path.replace(/([MLHVAZmlhva])([^MLHVAZmlhva]*)/g, function(_, op, args) { + var nums = args.trim() ? args.trim().split(/[\s,]+/).map(Number) : []; + var u = op.toUpperCase(); + if (u === 'Z') return op; + if (u === 'M' || u === 'L') { + var p = rot(nums[0], nums[1]); + return op + p[0] + ',' + p[1]; + } + if (u === 'H') { + var ph = rot(nums[0], 0); + return 'L' + ph[0] + ',' + ph[1]; + } + if (u === 'V') { + var pv = rot(0, nums[0]); + return 'L' + pv[0] + ',' + pv[1]; + } + if (u === 'A') { + // args: rx ry x-rotation large-arc-flag sweep-flag x y + var pa = rot(nums[5], nums[6]); + return op + nums[0] + ',' + nums[1] + ' ' + (nums[2] + angleDeg) + ' ' + nums[3] + ' ' + nums[4] + ' ' + pa[0] + ',' + pa[1]; + } + return op + args; + }); +} function getSymbolSdf(d, trace) { var symbol = d.mx; if (symbol === 'circle') return null; var symbolPath, symbolSdf; - var symbolNumber = Drawing.symbolNumber(symbol); - var symbolFunc = Drawing.symbolFuncs[symbolNumber % 100]; - var symbolNoDot = !!Drawing.symbolNoDot[symbolNumber % 100]; - var symbolNoFill = !!Drawing.symbolNoFill[symbolNumber % 100]; - + var sym = Drawing.lookupSymbol(symbol); var isDot = helpers.isDotSymbol(symbol); // until we may handle angles in shader? - if (d.ma) symbol += '_' + d.ma; + var cacheKey = symbol; + if (d.ma) cacheKey += '_' + d.ma; // get symbol sdf from cache or generate it - if (SYMBOL_SDF[symbol]) return SYMBOL_SDF[symbol]; + if (SYMBOL_SDF[cacheKey]) return SYMBOL_SDF[cacheKey]; var angle = Drawing.getMarkerAngle(d, trace); - if (isDot && !symbolNoDot) { - symbolPath = symbolFunc(SYMBOL_SIZE * 1.1, angle) + SYMBOL_SVG_CIRCLE; + var basePath = rotatePath(sym.path, angle); + if (isDot && !sym.noDot) { + symbolPath = basePath + SYMBOL_SVG_CIRCLE; } else { - symbolPath = symbolFunc(SYMBOL_SIZE, angle); + symbolPath = basePath; } symbolSdf = svgSdf(symbolPath, { w: SYMBOL_SDF_SIZE, h: SYMBOL_SDF_SIZE, viewBox: [-SYMBOL_SIZE, -SYMBOL_SIZE, SYMBOL_SIZE, SYMBOL_SIZE], - stroke: symbolNoFill ? SYMBOL_STROKE : -SYMBOL_STROKE + stroke: sym.noFill ? SYMBOL_STROKE : -SYMBOL_STROKE }); - SYMBOL_SDF[symbol] = symbolSdf; + SYMBOL_SDF[cacheKey] = symbolSdf; return symbolSdf || null; } diff --git a/test/jasmine/tests/box_test.js b/test/jasmine/tests/box_test.js index b350804d63c..9afad5d7ba5 100644 --- a/test/jasmine/tests/box_test.js +++ b/test/jasmine/tests/box_test.js @@ -1122,7 +1122,7 @@ describe('Test box restyle:', function() { var trace3 = d3Select(gd).select('.boxlayer > .trace'); _assertOne(msg, exp, trace3, 'boxCnt', 'path.box'); _assertOne(msg, exp, trace3, 'meanlineCnt', 'path.mean'); - _assertOne(msg, exp, trace3, 'ptsCnt', 'path.point'); + _assertOne(msg, exp, trace3, 'ptsCnt', 'use.point'); } Plotly.newPlot(gd, fig) diff --git a/test/jasmine/tests/scatter_symbol_perf_test.js b/test/jasmine/tests/scatter_symbol_perf_test.js new file mode 100644 index 00000000000..6e305aa530f --- /dev/null +++ b/test/jasmine/tests/scatter_symbol_perf_test.js @@ -0,0 +1,221 @@ +'use strict'; + +var Plotly = require('../../../lib/index'); +var Drawing = require('../../../src/components/drawing'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var d3Select = require('../../strict-d3').select; +var d3SelectAll = require('../../strict-d3').selectAll; + +describe('lookupSymbol .n property', function() { + it('matches the legacy numeric input exactly', function() { + // The .n property must equal what the user types as symbol:N + // so there are zero doubts about a consistent design. + var cases = [ + // numeric inputs + {v: 0, n: 0}, + {v: 1, n: 1}, + {v: 100, n: 100}, + {v: 101, n: 101}, + {v: 200, n: 200}, + {v: 201, n: 201}, + {v: 300, n: 300}, + {v: 301, n: 301}, + // string inputs resolve to the same n as their numeric equivalent + {v: 'circle', n: 0}, + {v: 'circle-open', n: 100}, + {v: 'circle-dot', n: 200}, + {v: 'circle-open-dot', n: 300}, + {v: 'square', n: 1}, + {v: 'square-open', n: 101}, + {v: 'square-dot', n: 201}, + {v: 'square-open-dot', n: 301}, + ]; + cases.forEach(function(c) { + var sym = Drawing.lookupSymbol(c.v); + expect(sym).toBeTruthy('lookupSymbol(' + c.v + ') should return a symbol object'); + expect(sym.n).toBe(c.n, 'lookupSymbol(' + c.v + ').n'); + }); + }); + + it('open variants share the same SVG path as their closed counterpart', function() { + expect(Drawing.lookupSymbol(0).path).toBe(Drawing.lookupSymbol(100).path, + 'circle and circle-open share path'); + expect(Drawing.lookupSymbol(200).path).toBe(Drawing.lookupSymbol(300).path, + 'circle-dot and circle-open-dot share path'); + expect(Drawing.lookupSymbol(1).path).toBe(Drawing.lookupSymbol(101).path, + 'square and square-open share path'); + }); + + it('all four variant .n values are distinct for the same base symbol', function() { + var ns = [0, 100, 200, 300].map(function(v) { return Drawing.lookupSymbol(v).n; }); + expect(ns).toEqual([0, 100, 200, 300], 'all four circle variant n values are distinct'); + }); +}); + +describe('Marker symbol performance', function() { + var gd; + + beforeEach(function() { gd = createGraphDiv(); }); + afterEach(destroyGraphDiv); + + function getUseHref(useEl) { + return useEl.getAttribute('href') || + useEl.getAttributeNS('http://www.w3.org/1999/xlink', 'href'); + } + + it('should use + with 1 symbol def for 1000 identical markers', function(done) { + var N = 1000; + var x = [], y = []; + for(var i = 0; i < N; i++) { x.push(i); y.push(Math.sin(i / 50)); } + + Plotly.newPlot(gd, [{ + mode: 'markers', + x: x, y: y, + marker: { symbol: 'circle', size: 8 } + }]).then(function() { + var defs = d3Select(gd).select('defs'); + var symbolDefs = defs.selectAll('symbol'); + expect(symbolDefs.size()).toBe(1, 'only 1 definition'); + + var useEls = d3Select(gd).selectAll('use.point'); + expect(useEls.size()).toBe(N, N + ' elements'); + + // No should exist + var pathPts = d3Select(gd).selectAll('path.point'); + expect(pathPts.size()).toBe(0, 'no point elements'); + }).then(done, done.fail); + }); + + it('should produce small SVG with 10 distinct symbols over 1000 points', function(done) { + var N = 1000; + var symbols = ['circle', 'square', 'diamond', 'cross', 'x', + 'triangle-up', 'triangle-down', 'pentagon', 'hexagon', 'star']; + var x = [], y = [], sym = []; + for(var i = 0; i < N; i++) { + x.push(i); y.push(Math.sin(i / 50)); + sym.push(symbols[i % symbols.length]); + } + + Plotly.newPlot(gd, [{ + mode: 'markers', + x: x, y: y, + marker: { symbol: sym, size: 10 } + }]).then(function() { + var svgEl = gd.querySelector('.main-svg'); + var svgStr = new XMLSerializer().serializeToString(svgEl); + var byteSize = new Blob([svgStr]).size; + + // With , 10 symbol defs + 1000 refs should be much smaller + // than 1000 full elements + expect(byteSize).toBeLessThan(400000, 'SVG byte size under 400KB'); + + var symbolDefs = d3Select(gd).select('defs').selectAll('symbol'); + expect(symbolDefs.size()).toBe(10, '10 definitions'); + }).then(done, done.fail); + }); + + it('should re-render on marker size change without new symbol def', function(done) { + var N = 1000; + var x = [], y = []; + for(var i = 0; i < N; i++) { x.push(i); y.push(Math.sin(i / 50)); } + + Plotly.newPlot(gd, [{ + mode: 'markers', + x: x, y: y, + marker: { symbol: 'square', size: 8 } + }]).then(function() { + // Capture current href — it shouldn't change on resize + var firstUse = gd.querySelector('use.point'); + var hrefBefore = getUseHref(firstUse); + var scaleBefore = parseFloat(firstUse.getAttribute('data-scale')); + + return Plotly.restyle(gd, { 'marker.size': 16 }).then(function() { + var firstUseAfter = gd.querySelector('use.point'); + var hrefAfter = getUseHref(firstUseAfter); + var scaleAfter = parseFloat(firstUseAfter.getAttribute('data-scale')); + + expect(hrefAfter).toBe(hrefBefore, 'href unchanged — no new symbol def needed'); + // Scale should have increased (size 16 → scale 0.8 vs size 8 → scale 0.4) + expect(scaleAfter).toBeGreaterThan(scaleBefore + 0.001, 'scale increased after size restyle'); + }); + }).then(done, done.fail); + }); + + it('should apply vector-effect: non-scaling-stroke to marker elements', function(done) { + Plotly.newPlot(gd, [{ + mode: 'markers', + x: [1, 2, 3], y: [1, 2, 3], + marker: { symbol: 'circle', size: 20, line: { width: 2, color: 'red' } } + }]).then(function() { + var useEls = d3SelectAll(gd.querySelectorAll('use.point')); + useEls.each(function() { + var ve = this.style.vectorEffect || d3Select(this).style('vector-effect'); + expect(ve).toBe('non-scaling-stroke', 'non-scaling-stroke applied'); + }); + }).then(done, done.fail); + }); + + it('symbol:0 and symbol:100 each get their own with descriptive ids', function(done) { + // Each variant (closed / open / dot / open-dot) gets its own element, + // even though open variants share the same SVG path as the closed counterpart. + // The open/closed distinction is still CSS-only (fill:none vs filled). + Plotly.newPlot(gd, [{ + mode: 'markers', + x: [1, 2, 3, 4], + y: [1, 2, 3, 4], + marker: { symbol: [0, 100, 0, 100], size: 10 } + }]).then(function() { + var defs = d3Select(gd).select('defs'); + var symbolDefs = defs.selectAll('symbol'); + expect(symbolDefs.size()).toBe(2, '2 defs: one for circle, one for circle-open'); + + var ids = []; + symbolDefs.each(function() { ids.push(this.getAttribute('id')); }); + expect(ids.sort()).toEqual(['circle', 'circle-open'], 'ids are "circle" and "circle-open"'); + + var useEls = gd.querySelectorAll('use.point'); + expect(useEls.length).toBe(4, '4 elements'); + // Even-index points use symbol:0 → #circle; odd-index → symbol:100 → #circle-open + for(var i = 0; i < useEls.length; i++) { + var href = getUseHref(useEls[i]); + expect(href).toBe(i % 2 === 0 ? '#circle' : '#circle-open', 'href matches variant'); + } + }).then(done, done.fail); + }); + + it('symbol:200/300 each get their own with descriptive ids', function(done) { + Plotly.newPlot(gd, [{ + mode: 'markers', + x: [1, 2], + y: [1, 2], + marker: { symbol: [200, 300], size: 10 } + }]).then(function() { + var defs = d3Select(gd).select('defs'); + var symbolDefs = defs.selectAll('symbol'); + expect(symbolDefs.size()).toBe(2, '2 defs: circle-dot and circle-open-dot'); + + var ids = []; + symbolDefs.each(function() { ids.push(this.getAttribute('id')); }); + expect(ids.sort()).toEqual(['circle-dot', 'circle-open-dot'], 'ids are "circle-dot" and "circle-open-dot"'); + }).then(done, done.fail); + }); + + it('all four variants of a symbol get their own with correct ids', function(done) { + Plotly.newPlot(gd, [{ + mode: 'markers', + x: [1, 2, 3, 4], + y: [1, 2, 3, 4], + marker: { symbol: ['square', 'square-open', 'square-dot', 'square-open-dot'], size: 10 } + }]).then(function() { + var defs = d3Select(gd).select('defs'); + var symbolDefs = defs.selectAll('symbol'); + expect(symbolDefs.size()).toBe(4, '4 defs for all square variants'); + + var ids = []; + symbolDefs.each(function() { ids.push(this.getAttribute('id')); }); + expect(ids.sort()).toEqual(['square', 'square-dot', 'square-open', 'square-open-dot'], + 'ids cover all four variants'); + }).then(done, done.fail); + }); +}); diff --git a/test/jasmine/tests/scatter_test.js b/test/jasmine/tests/scatter_test.js index 4c27d0a9bb8..61c3b621e8a 100644 --- a/test/jasmine/tests/scatter_test.js +++ b/test/jasmine/tests/scatter_test.js @@ -27,9 +27,8 @@ var getOpacity = function(node) { return Number(node.style.opacity); }; var getFillOpacity = function(node) { return Number(node.style['fill-opacity']); }; var getColor = function(node) { return node.style.fill; }; var getMarkerSize = function(node) { - // find path arc multiply by 2 to get the corresponding marker.size value - // (works for circles only) - return d3Select(node).attr('d').split('A')[1].split(',')[0] * 2; + // data-scale = r/20, marker.size = r*2, so size = data-scale * 40 + return Number(node.getAttribute('data-scale')) * 40; }; describe('Test scatter', function() { pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy