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  }