async function make_luv_chromaticity_chart(chart_size, settings, image_url) {
chart_size = Math.min(width, chart_size);
const graph_size = chart_size - 1;
const appx_graph_range = 2/3;
const grid_spacing = 0.1;
const grid_px = Math.round(graph_size / appx_graph_range * grid_spacing);
const px_dx = grid_spacing / grid_px;
const graph_range = px_dx * graph_size;
const appx_graph_offset = 0.03;
const offset = Math.round(appx_graph_offset / px_dx) * px_dx;
var xscale = d3.scaleLinear()
.domain([-offset, graph_range - offset])
.range([0, graph_size]);
var yscale = d3.scaleLinear()
.domain([graph_range - offset, -offset])
.range([0, graph_size]);
var f = settings.bg_fill;
var bg_color = colorToCss(sRGB([f, f, f]));
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, chart_size]);
var chart = { node:svg.node() };
chart.defs = svg.append("defs");
var pat = chart.defs.append('pattern')
.attr('id', 'uv_gamut_fill')
.attr('patternUnits', 'userSpaceOnUse')
.attr("x", 0)
.attr("y", 0)
.attr("width", graph_size)
.attr("height", graph_size);
var pat_img = pat.append("image")
.attr("x", xscale(raster_extent.x))
.attr("y", yscale(raster_extent.y + raster_extent.h))
.attr("width", xscale(raster_extent.x + raster_extent.w) - xscale(raster_extent.x))
.attr("height", yscale(raster_extent.y) - yscale(raster_extent.y + raster_extent.h))
.attr("href", image_url);
var g = svg.append("g");
// gamut background color
g.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", graph_size)
.attr("height", graph_size)
.attr('fill', bg_color);
// gamut outline + fill
var line_gamut = d3.line()
.x(d => xscale(d.u))
.y(d => yscale(d.v))
.curve(d3.curveLinearClosed);
chart.contour = g.append("path")
.attr('stroke', 'black')
.attr('strokewidth', '2')
.attr('fill', 'url(#uv_gamut_fill)')
.attr("d", line_gamut(uv_data));
// sRGB gamut
var line = d3.line()
.x(d => xscale(d[0]))
.y(d => yscale(d[1]))
.curve(d3.curveLinearClosed);
const { redWL, greenWL, blueWL } = gamutVertices;
var rgb_uv = [
uv_at_wl(redWL),
uv_at_wl(greenWL),
uv_at_wl(blueWL)
].map(({u,v})=>[u,v]);
var g_gamut = g.append("g");
// gamut triangle
g_gamut.append("path")
.attr('fill', 'transparent')
.attr('stroke', 'white')
.attr('stroke-width', 2)
.attr('stroke-opacity', 1)
.attr("d", line(rgb_uv));
// dots at certain wavelengths
g.append("g")
.selectAll("circle")
.data(uv_points)
.join("circle")
.attr("fill", "black")
.attr("stroke", "white")
.attr("stroke-width", 1.5)
.attr("stroke-opacity", .5)
.attr("r", 2.5)
.attr("cx", d => xscale(d.u))
.attr("cy", d => yscale(d.v));
// text labels
chart.text_g = g.append("g")
.attr("fill", "white")
.attr("style", "text-shadow: rgba(40, 40, 40, .3) 1px 1px 1px")
.attr("font-size", 12)
.attr("text-anchor", "middle")
.attr("font-family", "sans-serif")
.selectAll("text")
.data(uv_points.filter(d => !d.notext))
.join("text")
.text(d => `${d.wl}`)
.attr("x", d => xscale(d.u) + d.nu * 9 - Math.max(0, Math.min(8, 535 - d.wl)))
.attr("y", d => yscale(d.v) - d.nv * 9 + 3);
// axes
g.append("g").attr("transform", `translate(0, ${yscale(0)})`)
.call(d3.axisBottom().scale(xscale).ticks(6)
.tickFormat(""))
.call(g => g.selectAll("line, path")
.attr("stroke", "white")
.attr("stroke-opacity", .5));
g.append("g").attr("transform", `translate(${xscale(0)}, 0)`)
.call(d3.axisLeft().scale(yscale).ticks(6)
.tickFormat(""))
.call(g => g.selectAll("line, path")
.attr("stroke", "white")
.attr("stroke-opacity", .5));
// grid
g.append("g").attr("transform", `translate(0, ${graph_size})`)
.call(d3.axisTop()
.scale(xscale).ticks(6)
.tickSize(graph_size)
.tickFormat(""))
.call(g => g.selectAll(".tick line")
.attr("stroke", "white")
.attr("stroke-opacity", 0.2));
g.append("g").attr("transform", `translate(0, 0)`)
.call(d3.axisRight()
.scale(yscale).ticks(6)
.tickSize(graph_size)
.tickFormat(""))
.call(g => g.selectAll(".tick line")
.attr("stroke", "white")
.attr("stroke-opacity", 0.2));
// await until the image is decoded (it is too bad we have to encode
// it as PNG in the first place). This avoids flicker when changing settings.
if (pat_img.node().decode)
await pat_img.node().decode();
return chart;
}