|
| 1 | +<!DOCTYPE html> |
| 2 | +<html> |
| 3 | +<head> |
| 4 | + <meta charset="utf-8"> |
| 5 | + <title>Custom Marker Functions - All Demos</title> |
| 6 | + <script src="../../dist/plotly.js"></script> |
| 7 | + <style> |
| 8 | + body { font-family: Arial, sans-serif; margin: 20px; max-width: 900px; margin: 0 auto; padding: 20px; } |
| 9 | + h1 { border-bottom: 2px solid #3b82f6; padding-bottom: 10px; } |
| 10 | + h2 { color: #333; margin-top: 40px; } |
| 11 | + .plot { width: 100%; height: 400px; margin: 20px 0; } |
| 12 | + .info { background: #e7f3ff; padding: 10px; border-left: 4px solid #3b82f6; margin: 10px 0; } |
| 13 | + pre { background: #f5f5f5; padding: 15px; overflow-x: auto; font-size: 13px; } |
| 14 | + code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; } |
| 15 | + </style> |
| 16 | +</head> |
| 17 | +<body> |
| 18 | + <h1>Custom Marker Functions</h1> |
| 19 | + <div class="info"> |
| 20 | + Pass functions as <code>marker.symbol</code> to create custom shapes.<br> |
| 21 | + <strong>Simple:</strong> <code>function(r)</code> — r is marker radius<br> |
| 22 | + <strong>Data-aware:</strong> <code>function(r, customdata)</code> — access per-point data |
| 23 | + </div> |
| 24 | + |
| 25 | + <!-- Demo 1: Basic custom markers --> |
| 26 | + <h2>1. Basic Custom Markers</h2> |
| 27 | + <p>Define functions returning SVG path strings. Mix with built-in symbols.</p> |
| 28 | + <div id="plot1" class="plot"></div> |
| 29 | + <pre> |
| 30 | +// Heart shape |
| 31 | +function heart(r) { |
| 32 | + var x = r * 0.6, y = r * 0.8; |
| 33 | + return 'M0,' + (-y/2) + |
| 34 | + 'C' + (-x) + ',' + (-y) + ' ' + (-x*2) + ',' + (-y/3) + ' ' + (-x*2) + ',0' + |
| 35 | + 'C' + (-x*2) + ',' + (y/2) + ' 0,' + y + ' 0,' + (y*1.5) + |
| 36 | + 'C0,' + y + ' ' + (x*2) + ',' + (y/2) + ' ' + (x*2) + ',0' + |
| 37 | + 'C' + (x*2) + ',' + (-y/3) + ' ' + x + ',' + (-y) + ' 0,' + (-y/2) + 'Z'; |
| 38 | +} |
| 39 | + |
| 40 | +// 5-point star |
| 41 | +function star(r) { |
| 42 | + var path = 'M'; |
| 43 | + for (var i = 0; i < 10; i++) { |
| 44 | + var rad = i % 2 === 0 ? r : r * 0.4; |
| 45 | + var ang = i * Math.PI / 5 - Math.PI / 2; |
| 46 | + path += (i ? 'L' : '') + (rad * Math.cos(ang)).toFixed(2) + ',' + (rad * Math.sin(ang)).toFixed(2); |
| 47 | + } |
| 48 | + return path + 'Z'; |
| 49 | +} |
| 50 | + |
| 51 | +Plotly.newPlot('plot1', [{ |
| 52 | + x: [1, 2, 3, 4, 5], |
| 53 | + y: [2, 3, 4, 3, 2], |
| 54 | + mode: 'markers+lines', |
| 55 | + marker: { |
| 56 | + symbol: [heart, star, 'circle', star, heart], // mix functions and strings |
| 57 | + size: 25, |
| 58 | + color: ['red', 'gold', 'blue', 'gold', 'red'] |
| 59 | + } |
| 60 | +}]);</pre> |
| 61 | + |
| 62 | + <!-- Demo 2: Data-aware markers --> |
| 63 | + <h2>2. Data-Aware Markers</h2> |
| 64 | + <p>Access <code>customdata[i]</code> to vary shape per point.</p> |
| 65 | + <div id="plot2" class="plot"></div> |
| 66 | + <pre> |
| 67 | +function shapeByData(r, customdata) { |
| 68 | + if (customdata === 'star') { |
| 69 | + // Star shape |
| 70 | + var path = 'M'; |
| 71 | + for (var i = 0; i < 10; i++) { |
| 72 | + var rad = i % 2 === 0 ? r : r * 0.4; |
| 73 | + var ang = i * Math.PI / 5 - Math.PI / 2; |
| 74 | + path += (i ? 'L' : '') + (rad * Math.cos(ang)).toFixed(2) + ',' + (rad * Math.sin(ang)).toFixed(2); |
| 75 | + } |
| 76 | + return path + 'Z'; |
| 77 | + } |
| 78 | + if (customdata === 'big') { |
| 79 | + r *= 1.4; // Larger diamond |
| 80 | + } |
| 81 | + // Default: diamond |
| 82 | + return 'M' + r + ',0L0,' + r + 'L-' + r + ',0L0,-' + r + 'Z'; |
| 83 | +} |
| 84 | + |
| 85 | +Plotly.newPlot('plot2', [{ |
| 86 | + x: [1, 2, 3, 4], |
| 87 | + y: [1, 1, 1, 1], |
| 88 | + customdata: ['normal', 'big', 'star', 'normal'], |
| 89 | + mode: 'markers', |
| 90 | + marker: { symbol: shapeByData, size: 25, color: '#10b981' } |
| 91 | +}]);</pre> |
| 92 | + |
| 93 | + <!-- Demo 3: Weather map --> |
| 94 | + <h2>3. Weather Map</h2> |
| 95 | + <p>Complex example: sun, cloud, and wind barbs based on weather data.</p> |
| 96 | + <div id="plot3" class="plot"></div> |
| 97 | + <pre> |
| 98 | +function weatherMarker(r, data) { |
| 99 | + if (data.type === 'sunny') { |
| 100 | + // Sun: circle with rays |
| 101 | + var cr = r * 0.5, path = 'M' + cr + ',0A' + cr + ',' + cr + ' 0 1,1 -' + cr + ',0A' + cr + ',' + cr + ' 0 1,1 ' + cr + ',0'; |
| 102 | + for (var i = 0; i < 8; i++) { |
| 103 | + var ang = i * Math.PI / 4; |
| 104 | + path += 'M' + ((cr+2) * Math.cos(ang)).toFixed(1) + ',' + ((cr+2) * Math.sin(ang)).toFixed(1) + |
| 105 | + 'L' + ((cr+r*0.4) * Math.cos(ang)).toFixed(1) + ',' + ((cr+r*0.4) * Math.sin(ang)).toFixed(1); |
| 106 | + } |
| 107 | + return path; |
| 108 | + } |
| 109 | + if (data.type === 'cloudy') { |
| 110 | + return 'M-8,3 A6,6 0 1,1 -2,-4 A7,7 0 1,1 8,-2 A5,5 0 1,1 10,3 Z'; |
| 111 | + } |
| 112 | + if (data.type === 'wind') { |
| 113 | + // Wind barb: staff + barbs based on speed |
| 114 | + var path = 'M0,' + r + 'L0,-' + r, y = -r; |
| 115 | + for (var i = 0; i < Math.min(data.speed, 3); i++) { |
| 116 | + path += 'M0,' + y + 'L' + (r*0.6) + ',' + (y + r*0.3); |
| 117 | + y += r * 0.3; |
| 118 | + } |
| 119 | + return path; |
| 120 | + } |
| 121 | + return 'M' + r + ',0A' + r + ',' + r + ' 0 1,0 -' + r + ',0A' + r + ',' + r + ' 0 1,0 ' + r + ',0'; |
| 122 | +} |
| 123 | + |
| 124 | +var locations = [ |
| 125 | + { name: 'Seattle', lon: -122, lat: 47, weather: { type: 'cloudy' } }, |
| 126 | + { name: 'SF', lon: -122, lat: 38, weather: { type: 'sunny' } }, |
| 127 | + { name: 'Denver', lon: -105, lat: 40, weather: { type: 'sunny' } }, |
| 128 | + { name: 'Chicago', lon: -88, lat: 42, weather: { type: 'cloudy' } }, |
| 129 | + { name: 'NYC', lon: -74, lat: 41, weather: { type: 'cloudy' } }, |
| 130 | + { name: 'Miami', lon: -80, lat: 26, weather: { type: 'sunny' } }, |
| 131 | + // Wind arrows (jet stream) |
| 132 | + { lon: -115, lat: 46, weather: { type: 'wind', direction: 100, speed: 3 } }, |
| 133 | + { lon: -100, lat: 42, weather: { type: 'wind', direction: 120, speed: 2 } }, |
| 134 | + { lon: -85, lat: 36, weather: { type: 'wind', direction: 150, speed: 1 } } |
| 135 | +]; |
| 136 | + |
| 137 | +Plotly.newPlot('plot3', [{ |
| 138 | + x: locations.map(l => l.lon), |
| 139 | + y: locations.map(l => l.lat), |
| 140 | + customdata: locations.map(l => l.weather), |
| 141 | + text: locations.map(l => l.name || ''), |
| 142 | + mode: 'markers+text', |
| 143 | + textposition: 'bottom center', |
| 144 | + marker: { |
| 145 | + symbol: weatherMarker, |
| 146 | + size: 30, |
| 147 | + color: locations.map(l => ({ sunny: '#FFD700', cloudy: '#708090', wind: '#4169E1' }[l.weather.type])), |
| 148 | + angle: locations.map(l => l.weather.direction || 0) |
| 149 | + } |
| 150 | +}], { xaxis: { range: [-130, -70] }, yaxis: { range: [20, 52], scaleanchor: 'x' } });</pre> |
| 151 | + |
| 152 | + <script> |
| 153 | + // === Demo 1: Basic markers === |
| 154 | + function heart(r) { |
| 155 | + var x = r * 0.6, y = r * 0.8; |
| 156 | + return 'M0,' + (-y/2) + 'C' + (-x) + ',' + (-y) + ' ' + (-x*2) + ',' + (-y/3) + ' ' + (-x*2) + ',0C' + (-x*2) + ',' + (y/2) + ' 0,' + y + ' 0,' + (y*1.5) + 'C0,' + y + ' ' + (x*2) + ',' + (y/2) + ' ' + (x*2) + ',0C' + (x*2) + ',' + (-y/3) + ' ' + x + ',' + (-y) + ' 0,' + (-y/2) + 'Z'; |
| 157 | + } |
| 158 | + function star(r) { |
| 159 | + var path = 'M'; |
| 160 | + for (var i = 0; i < 10; i++) { |
| 161 | + var rad = i % 2 === 0 ? r : r * 0.4, ang = i * Math.PI / 5 - Math.PI / 2; |
| 162 | + path += (i ? 'L' : '') + (rad * Math.cos(ang)).toFixed(2) + ',' + (rad * Math.sin(ang)).toFixed(2); |
| 163 | + } |
| 164 | + return path + 'Z'; |
| 165 | + } |
| 166 | + Plotly.newPlot('plot1', [{ |
| 167 | + x: [1, 2, 3, 4, 5], y: [2, 3, 4, 3, 2], mode: 'markers+lines', |
| 168 | + marker: { symbol: [heart, star, 'circle', star, heart], size: 25, color: ['red', 'gold', 'blue', 'gold', 'red'] } |
| 169 | + }], { title: 'Mix custom functions with built-in symbols' }); |
| 170 | + |
| 171 | + // === Demo 2: Data-aware === |
| 172 | + function shapeByData(r, customdata) { |
| 173 | + if (customdata === 'star') { |
| 174 | + var path = 'M'; |
| 175 | + for (var i = 0; i < 10; i++) { |
| 176 | + var rad = i % 2 === 0 ? r : r * 0.4, ang = i * Math.PI / 5 - Math.PI / 2; |
| 177 | + path += (i ? 'L' : '') + (rad * Math.cos(ang)).toFixed(2) + ',' + (rad * Math.sin(ang)).toFixed(2); |
| 178 | + } |
| 179 | + return path + 'Z'; |
| 180 | + } |
| 181 | + if (customdata === 'big') r *= 1.4; |
| 182 | + return 'M' + r + ',0L0,' + r + 'L-' + r + ',0L0,-' + r + 'Z'; |
| 183 | + } |
| 184 | + Plotly.newPlot('plot2', [{ |
| 185 | + x: [1, 2, 3, 4], y: [1, 1, 1, 1], customdata: ['normal', 'big', 'star', 'normal'], |
| 186 | + mode: 'markers', marker: { symbol: shapeByData, size: 25, color: '#10b981' } |
| 187 | + }], { title: 'Shape varies by customdata value', xaxis: { range: [0, 5] } }); |
| 188 | + |
| 189 | + // === Demo 3: Weather map === |
| 190 | + function weatherMarker(r, data) { |
| 191 | + if (data.type === 'sunny') { |
| 192 | + var cr = r * 0.5, path = 'M' + cr + ',0A' + cr + ',' + cr + ' 0 1,1 -' + cr + ',0A' + cr + ',' + cr + ' 0 1,1 ' + cr + ',0'; |
| 193 | + for (var i = 0; i < 8; i++) { |
| 194 | + var ang = i * Math.PI / 4; |
| 195 | + path += 'M' + ((cr+2) * Math.cos(ang)).toFixed(1) + ',' + ((cr+2) * Math.sin(ang)).toFixed(1) + 'L' + ((cr+r*0.4) * Math.cos(ang)).toFixed(1) + ',' + ((cr+r*0.4) * Math.sin(ang)).toFixed(1); |
| 196 | + } |
| 197 | + return path; |
| 198 | + } |
| 199 | + if (data.type === 'cloudy') return 'M-8,3 A6,6 0 1,1 -2,-4 A7,7 0 1,1 8,-2 A5,5 0 1,1 10,3 Z'; |
| 200 | + if (data.type === 'wind') { |
| 201 | + var path = 'M0,' + r + 'L0,-' + r, y = -r; |
| 202 | + for (var i = 0; i < Math.min(data.speed, 3); i++) { path += 'M0,' + y + 'L' + (r*0.6) + ',' + (y + r*0.3); y += r * 0.3; } |
| 203 | + return path; |
| 204 | + } |
| 205 | + return 'M' + r + ',0A' + r + ',' + r + ' 0 1,0 -' + r + ',0A' + r + ',' + r + ' 0 1,0 ' + r + ',0'; |
| 206 | + } |
| 207 | + var locations = [ |
| 208 | + { name: 'Seattle', lon: -122, lat: 47, weather: { type: 'cloudy' } }, |
| 209 | + { name: 'SF', lon: -122, lat: 38, weather: { type: 'sunny' } }, |
| 210 | + { name: 'Denver', lon: -105, lat: 40, weather: { type: 'sunny' } }, |
| 211 | + { name: 'Chicago', lon: -88, lat: 42, weather: { type: 'cloudy' } }, |
| 212 | + { name: 'NYC', lon: -74, lat: 41, weather: { type: 'cloudy' } }, |
| 213 | + { name: 'Miami', lon: -80, lat: 26, weather: { type: 'sunny' } }, |
| 214 | + { lon: -115, lat: 46, weather: { type: 'wind', direction: 100, speed: 3 } }, |
| 215 | + { lon: -100, lat: 42, weather: { type: 'wind', direction: 120, speed: 2 } }, |
| 216 | + { lon: -85, lat: 36, weather: { type: 'wind', direction: 150, speed: 1 } } |
| 217 | + ]; |
| 218 | + Plotly.newPlot('plot3', [{ |
| 219 | + x: locations.map(l => l.lon), y: locations.map(l => l.lat), |
| 220 | + customdata: locations.map(l => l.weather), text: locations.map(l => l.name || ''), |
| 221 | + mode: 'markers+text', textposition: 'bottom center', |
| 222 | + marker: { symbol: weatherMarker, size: 30, color: locations.map(l => ({ sunny: '#FFD700', cloudy: '#708090', wind: '#4169E1' }[l.weather.type])), angle: locations.map(l => l.weather.direction || 0) } |
| 223 | + }], { title: 'Weather icons with wind direction via marker.angle', xaxis: { range: [-130, -70] }, yaxis: { range: [20, 52], scaleanchor: 'x' } }); |
| 224 | + </script> |
| 225 | +</body> |
| 226 | +</html> |
0 commit comments