golang.org/x/build@v0.0.0-20240506185731-218518f32b70/third_party/bandchart/bandchart.js (about) 1 // Copyright 2021 Observable, Inc. 2 // Released under the ISC license. 3 // https://observablehq.com/@d3/band-chart 4 5 function BandChart(data, { 6 defined, 7 marginTop = 30, // top margin, in pixels 8 marginRight = 15, // right margin, in pixels 9 marginBottom = 30, // bottom margin, in pixels 10 marginLeft = 40, // left margin, in pixels 11 width = 480, // outer width, in pixels 12 height = 240, // outer height, in pixels 13 benchmark, 14 unit, 15 repository, 16 minViewDeltaPercent, 17 higherIsBetter, 18 } = {}) { 19 // Compute values. 20 const C = d3.map(data, d => d.CommitHash); 21 const X = d3.map(data, d => d.CommitDate); 22 const Y = d3.map(data, d => d.Center); 23 const Y1 = d3.map(data, d => d.Low); 24 const Y2 = d3.map(data, d => d.High); 25 const I = d3.range(X.length); 26 if (defined === undefined) defined = (d, i) => !isNaN(X[i]) && !isNaN(Y1[i]) && !isNaN(Y2[i]); 27 const D = d3.map(data, defined); 28 29 const xRange = [marginLeft, width - marginRight]; // [left, right] 30 const yRange = [height - marginBottom, marginTop]; // [bottom, top] 31 32 // Compute default domains. 33 let yDomain = d3.nice(...d3.extent([...Y1, ...Y2]), 10); 34 35 // Determine Y domain. 36 // 37 // Three cases: 38 // (1) All data is above Y=0 line. 39 // (2) All data is below Y=0 line. 40 // (3) Data crosses the Y=0 line. 41 // 42 // For (1) set the Y=0 line as the bottom of the domain. 43 // For (2) set it at the top. 44 // For (3) make sure the Y=0 line is in the middle. 45 // 46 // Finally, make sure we don't get closer than minViewDeltaPercent, 47 // because otherwise it just looks really noisy. 48 const minYDomain = [-minViewDeltaPercent, minViewDeltaPercent]; 49 if (yDomain[0] > 0) { 50 // (1) 51 yDomain[0] = 0; 52 if (yDomain[1] < minYDomain[1]) { 53 yDomain[1] = minYDomain[1]; 54 } 55 } else if (yDomain[1] < 0) { 56 // (2) 57 yDomain[1] = 0; 58 if (yDomain[0] > minYDomain[0]) { 59 yDomain[0] = minYDomain[0]; 60 } 61 } else { 62 // (3) 63 if (Math.abs(yDomain[1]) > Math.abs(yDomain[0])) { 64 yDomain[0] = -Math.abs(yDomain[1]); 65 } else { 66 yDomain[1] = Math.abs(yDomain[0]); 67 } 68 if (yDomain[0] > minYDomain[0]) { 69 yDomain[0] = minYDomain[0]; 70 } 71 if (yDomain[1] < minYDomain[1]) { 72 yDomain[1] = minYDomain[1]; 73 } 74 } 75 76 // Construct scales and axes. 77 const xOrdTicks = d3.range(xRange[0], xRange[1], (xRange[1]-xRange[0])/(X.length-1)); 78 xOrdTicks.push(xRange[1]); 79 const xScale = d3.scaleOrdinal(X, xOrdTicks); 80 const yScale = d3.scaleLinear(yDomain, yRange); 81 const yAxis = d3.axisLeft(yScale).ticks(height / 40, "+%"); 82 83 const svg = d3.create("svg") 84 .attr("width", width) 85 .attr("height", height) 86 .attr("viewBox", [0, 0, width, height]) 87 .attr("style", "max-width: 100%; height: auto; height: intrinsic;"); 88 89 // Chart area background color. 90 svg.append("rect") 91 .attr("fill", "white") 92 .attr("x", xRange[0]) 93 .attr("y", yRange[1]) 94 .attr("width", xRange[1] - xRange[0]) 95 .attr("height", yRange[0] - yRange[1]); 96 97 // Title (unit). 98 svg.append("g") 99 .attr("transform", `translate(${marginLeft},0)`) 100 .call(yAxis) 101 .call(g => g.select(".domain").remove()) 102 .call(g => g.selectAll(".tick line").clone() 103 .attr("x2", width - marginLeft - marginRight) 104 .attr("stroke-opacity", 0.1)) 105 .call(g => g.append("a") 106 .attr("xlink:href", "?benchmark=" + benchmark + "&unit=" + unit) 107 .append("text") 108 .attr("x", xRange[0]-40) 109 .attr("y", 24) 110 .attr("fill", "currentColor") 111 .attr("text-anchor", "start") 112 .attr("font-size", "16px") 113 .text(unit)); 114 115 const defs = svg.append("defs") 116 117 const maxHalfColorValue = 0.10; 118 const maxHalfColorOpacity = 0.5; 119 120 // Draw top half. 121 const goodColor = "#005AB5"; 122 const badColor = "#DC3220"; 123 124 // By default, lower is better. 125 var bottomColor = goodColor; 126 var topColor = badColor; 127 if (higherIsBetter) { 128 bottomColor = badColor; 129 topColor = goodColor; 130 } 131 132 // IDs, even within SVGs, are shared across the entire page. (what?) 133 // So, at least try to avoid a collision. 134 const gradientIDSuffix = Math.random()*10000000.0; 135 136 const topGradient = defs.append("linearGradient") 137 .attr("id", "topGradient"+gradientIDSuffix) 138 .attr("x1", "0%") 139 .attr("x2", "0%") 140 .attr("y1", "100%") 141 .attr("y2", "0%"); 142 topGradient.append("stop") 143 .attr("offset", "0%") 144 .style("stop-color", topColor) 145 .style("stop-opacity", 0); 146 let topGStopOpacity = maxHalfColorOpacity; 147 let topGOffsetPercent = 100.0; 148 if (yDomain[1] > maxHalfColorValue) { 149 topGOffsetPercent *= maxHalfColorValue/yDomain[1]; 150 } else { 151 topGStopOpacity *= yDomain[1]/maxHalfColorValue; 152 } 153 topGradient.append("stop") 154 .attr("offset", topGOffsetPercent+"%") 155 .style("stop-color", topColor) 156 .style("stop-opacity", topGStopOpacity); 157 158 const bottomGradient = defs.append("linearGradient") 159 .attr("id", "bottomGradient"+gradientIDSuffix) 160 .attr("x1", "0%") 161 .attr("x2", "0%") 162 .attr("y1", "0%") 163 .attr("y2", "100%"); 164 bottomGradient.append("stop") 165 .attr("offset", "0%") 166 .style("stop-color", bottomColor) 167 .style("stop-opacity", 0); 168 let bottomGStopOpacity = maxHalfColorOpacity; 169 let bottomGOffsetPercent = 100.0; 170 if (yDomain[0] < -maxHalfColorValue) { 171 bottomGOffsetPercent *= -maxHalfColorValue/yDomain[0]; 172 } else { 173 bottomGStopOpacity *= -yDomain[0]/maxHalfColorValue; 174 } 175 bottomGradient.append("stop") 176 .attr("offset", bottomGOffsetPercent+"%") 177 .style("stop-color", bottomColor) 178 .style("stop-opacity", bottomGStopOpacity); 179 180 // Top half color. 181 svg.append("rect") 182 .attr("fill", "url(#topGradient"+gradientIDSuffix+")") 183 .attr("x", xRange[0]) 184 .attr("y", yScale(yDomain[1])) 185 .attr("width", xRange[1] - xRange[0]) 186 .attr("height", (yDomain[1]/(yDomain[1]-yDomain[0]))*(height-marginTop-marginBottom)); 187 188 // Bottom half color. 189 svg.append("rect") 190 .attr("fill", "url(#bottomGradient"+gradientIDSuffix+")") 191 .attr("x", xRange[0]) 192 .attr("y", yScale(0)) 193 .attr("width", xRange[1] - xRange[0]) 194 .attr("height", (-yDomain[0]/(yDomain[1]-yDomain[0]))*(height-marginTop-marginBottom)); 195 196 // Add a harder gridline for Y=0 to make it stand out. 197 198 const line0 = d3.line() 199 .defined(i => D[i]) 200 .x(i => xScale(X[i])) 201 .y(i => yScale(0)) 202 203 svg.append("path") 204 .attr("fill", "none") 205 .attr("stroke", "#999999") 206 .attr("stroke-width", 2) 207 .attr("d", line0(I)) 208 209 // Create CI area. 210 211 const area = d3.area() 212 .defined(i => D[i]) 213 .curve(d3.curveLinear) 214 .x(i => xScale(X[i])) 215 .y0(i => yScale(Y1[i])) 216 .y1(i => yScale(Y2[i])); 217 218 svg.append("path") 219 .attr("fill", "black") 220 .attr("opacity", 0.1) 221 .attr("d", area(I)); 222 223 // Add X axis label. 224 svg.append("text") 225 .attr("x", xRange[0] + (xRange[1]-xRange[0])/2) 226 .attr("y", yRange[0] + (yRange[0]-yRange[1])*0.10) 227 .attr("fill", "currentColor") 228 .attr("text-anchor", "middle") 229 .attr("font-size", "12px") 230 .text("Commits"); 231 232 // Create center line. 233 234 const line = d3.line() 235 .defined(i => D[i]) 236 .x(i => xScale(X[i])) 237 .y(i => yScale(Y[i])) 238 239 svg.append("path") 240 .attr("fill", "none") 241 .attr("stroke", "#212121") 242 .attr("stroke-width", 2.5) 243 .attr("d", line(I)) 244 245 // Divide the chart into columns and apply links and hover actions to them. 246 svg.append("g") 247 .attr("stroke", "#2074A0") 248 .attr("stroke-opacity", 0) 249 .attr("fill", "none") 250 .selectAll("path") 251 .data(I) 252 .join("a") 253 .attr("xlink:href", (d, i) => "https://go.googlesource.com/"+repository+"/+show/"+C[i]) 254 .append("rect") 255 .attr("pointer-events", "all") 256 .attr("x", (d, i) => { 257 if (i == 0) { 258 return xOrdTicks[i]; 259 } 260 return xOrdTicks[i-1]+(xOrdTicks[i]-xOrdTicks[i-1])/2; 261 }) 262 .attr("y", marginTop) 263 .attr("width", (d, i) => { 264 if (i == 0 || i == X.length-1) { 265 return (xOrdTicks[1]-xOrdTicks[0]) / 2; 266 } 267 return xOrdTicks[1]-xOrdTicks[0]; 268 }) 269 .attr("height", height-marginTop-marginBottom) 270 .on("mouseover", (d, i) => { 271 svg.append('a') 272 .attr("class", "tooltip") 273 .call(g => g.append('line') 274 .attr("x1", xScale(X[i])) 275 .attr("y1", yRange[0]) 276 .attr("x2", xScale(X[i])) 277 .attr("y2", yRange[1]) 278 .attr("stroke", "black") 279 .attr("stroke-width", 1) 280 .attr("stroke-dasharray", 2) 281 .attr("opacity", 0.5) 282 .attr("pointer-events", "none") 283 ) 284 .call(g => g.append('text') 285 // Point metadata (commit hash and date). 286 // Above graph, top-right. 287 .attr("x", xRange[1]) 288 .attr("y", yRange[1] - 6) 289 .attr("pointer-events", "none") 290 .attr("fill", "currentColor") 291 .attr("text-anchor", "end") 292 .attr("font-family", "sans-serif") 293 .attr("font-size", 12) 294 .text(C[i].slice(0, 7) + " (" 295 + Intl.DateTimeFormat([], { 296 dateStyle: "long", 297 timeStyle: "short" 298 }).format(X[i]) 299 + ")") 300 ) 301 .call(g => g.append('text') 302 // Point center, low, high values. 303 // Bottom-right corner, next to "Commits". 304 .attr("x", xRange[1]) 305 .attr("y", yRange[0] + (yRange[0]-yRange[1])*0.10) 306 .attr("pointer-events", "none") 307 .attr("fill", "currentColor") 308 .attr("text-anchor", "end") 309 .attr("font-family", "sans-serif") 310 .attr("font-size", 12) 311 .text(Intl.NumberFormat([], { 312 style: 'percent', 313 signDisplay: 'always', 314 minimumFractionDigits: 2, 315 }).format(Y[i]) + " (" + Intl.NumberFormat([], { 316 style: 'percent', 317 signDisplay: 'always', 318 minimumFractionDigits: 2, 319 }).format(Y1[i]) + ", " + Intl.NumberFormat([], { 320 style: 'percent', 321 signDisplay: 'always', 322 minimumFractionDigits: 2, 323 }).format(Y2[i]) + ")") 324 ) 325 }) 326 .on("mouseout", () => svg.selectAll('.tooltip').remove()); 327 328 return svg.node(); 329 }