function make_luv_chromaticity_chart(chart_size, settings) {
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;
const xscale = d3.scaleLinear()
.domain([-offset, graph_range - offset])
.range([0, graph_size]);
const yscale = d3.scaleLinear()
.domain([graph_range - offset, -offset])
.range([0, graph_size]);
const white_text = settings.bg_fill < 0.15;
var f = settings.bg_fill;
var bg_color = colorToCss(sRGB([f, f, f]));
const svg = d3.create("svg")
.attr("viewBox", [0, 0, chart_size, chart_size]);
var chart = { node: svg.node(), xscale: xscale, yscale: yscale, white_text: white_text };
chart.defs = svg.append("defs");
if (settings.image_url) {
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", settings.image_url);
}
// svg path for our gamut ("d" attribute)
var line_gamut = d3.line()
.x(d => xscale(d.u))
.y(d => yscale(d.v))
.curve(d3.curveLinearClosed);
const uv_d = line_gamut(uv_data);
// gamut background color with hole
svg.append("path")
.attr('fill', bg_color)
.attr('stroke-width', '0')
.attr("d", `M0,0 L0,${chart_size} L${chart_size},${chart_size} L${chart_size},0 Z` +
(settings.image_url ? "" : ' ' + uv_d));
var g = svg.append("g");
// gamut outline + fill
chart.contour = g.append("path")
.attr('stroke', 'black')
.attr('strokewidth', '2')
.attr('fill', settings.image_url ? 'url(#uv_gamut_fill)' : 'transparent')
.attr("d", uv_d);
// sRGB gamut
var line = d3.line()
.x(d => xscale(d[0]))
.y(d => yscale(d[1]))
.curve(d3.curveLinearClosed);
const rgbM = RgbColorM.rgbToXyz;
var rgb_uv = [
XYZ_to_Luv_uv(apply_m(rgbM, [1, 0, 0])),
XYZ_to_Luv_uv(apply_m(rgbM, [0, 1, 0])),
XYZ_to_Luv_uv(apply_m(rgbM, [0, 0, 1]))];
var rgb_uv_dots = [
XYZ_to_Luv_uv(apply_m(rgbM, [1, 0, 0])),
XYZ_to_Luv_uv(apply_m(rgbM, [1, 1, 0])),
XYZ_to_Luv_uv(apply_m(rgbM, [0, 1, 0])),
XYZ_to_Luv_uv(apply_m(rgbM, [0, 1, 1])),
XYZ_to_Luv_uv(apply_m(rgbM, [0, 0, 1])),
XYZ_to_Luv_uv(apply_m(rgbM, [1, 0, 1])),
XYZ_to_Luv_uv(apply_m(rgbM, [1, 1, 1]))];
var g_gamut = g.append("g");
// gamut triangle
g_gamut.append("path")
.attr('fill', 'transparent')
.attr('stroke', 'white')
.attr('strokewidth', 1)
.attr('stroke-opacity', .5)
.attr("d", line(rgb_uv));
// group to update marks
chart.g_mark = g_gamut.append("g");
// gamut corner dots and white point
g_gamut.append("g")
.selectAll("circle")
.data(rgb_uv_dots)
.join("circle")
.attr('fill', 'transparent')
.attr('stroke', 'white')
.attr('strokewidth', 1)
.attr('stroke-opacity', .5)
.attr("r", 2.5)
.attr("cx", d => xscale(d[0]))
.attr("cy", d => yscale(d[1]));
// 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("style", white_text ? "fill: #aaa"
: "text-shadow: rgba(255, 255, 255, .3) 1px 1px 1px; fill: black;")
.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().offset(.5).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().offset(.5).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().offset(.5).scale(xscale).ticks(6)
.tickSize(graph_size)
.tickFormat(""))
.call(g => g.selectAll(".tick line")
.attr("stroke", "white")
.attr("stroke-opacity", 0.2))
.call(g => g.selectAll(".domain")
.attr("stroke", "none"));
g.append("g").attr("transform", `translate(0, 0)`)
.call(d3.axisRight().offset(.5).scale(yscale).ticks(6)
.tickSize(graph_size)
.tickFormat(""))
.call(g => g.selectAll(".tick line")
.attr("stroke", "white")
.attr("stroke-opacity", 0.2))
.call(g => g.selectAll(".domain")
.attr("stroke", "none"));
return chart;
}