import {pathRound as path} from "d3"; import {inferFontVariant} from "../axes.js"; import {create, createContext} from "../context.js"; import {isNoneish, maybeColorChannel, maybeNumberChannel} from "../options.js"; import {isOrdinalScale, isThresholdScale} from "../scales.js"; import {applyInlineStyles, impliedString, maybeClassName} from "../style.js"; import {inferTickFormat} from "../marks/axis.js"; function maybeScale(scale, key) { if (key == null) return key; const s = scale(key); if (!s) throw new Error(`scale not found: ${key}`); return s; } export function legendSwatches(color, {opacity, ...options} = {}) { if (!isOrdinalScale(color) && !isThresholdScale(color)) throw new Error(`swatches legend requires ordinal or threshold color scale (not ${color.type})`); return legendItems(color, options, (selection, scale, width, height) => selection .append("svg") .attr("width", width) .attr("height", height) .attr("fill", scale.scale) .attr("fill-opacity", maybeNumberChannel(opacity)[1]) .append("rect") .attr("width", "100%") .attr("height", "100%") ); } export function legendSymbols( symbol, { fill = symbol.hint?.fill !== undefined ? symbol.hint.fill : "none", fillOpacity = 1, stroke = symbol.hint?.stroke !== undefined ? symbol.hint.stroke : isNoneish(fill) ? "currentColor" : "none", strokeOpacity = 1, strokeWidth = 1.5, r = 4.5, ...options } = {}, scale ) { const [vf, cf] = maybeColorChannel(fill); const [vs, cs] = maybeColorChannel(stroke); const sf = maybeScale(scale, vf); const ss = maybeScale(scale, vs); const size = r * r * Math.PI; fillOpacity = maybeNumberChannel(fillOpacity)[1]; strokeOpacity = maybeNumberChannel(strokeOpacity)[1]; strokeWidth = maybeNumberChannel(strokeWidth)[1]; return legendItems(symbol, options, (selection, scale, width, height) => selection .append("svg") .attr("viewBox", "-8 -8 16 16") .attr("width", width) .attr("height", height) .attr("fill", vf === "color" ? (d) => sf.scale(d) : cf) .attr("fill-opacity", fillOpacity) .attr("stroke", vs === "color" ? (d) => ss.scale(d) : cs) .attr("stroke-opacity", strokeOpacity) .attr("stroke-width", strokeWidth) .append("path") .attr("d", (d) => { const p = path(); symbol.scale(d).draw(p, size); return p; }) ); } function legendItems(scale, options = {}, swatch) { let { columns, tickFormat, fontVariant = inferFontVariant(scale), // TODO label, swatchSize = 15, swatchWidth = swatchSize, swatchHeight = swatchSize, marginLeft = 0, className, style, width } = options; const context = createContext(options); className = maybeClassName(className); tickFormat = inferTickFormat(scale.scale, scale.domain, undefined, tickFormat); const swatches = create("div", context).attr( "class", `${className}-swatches ${className}-swatches-${columns != null ? "columns" : "wrap"}` ); let extraStyle; if (columns != null) { extraStyle = `:where(.${className}-swatches-columns .${className}-swatch) { display: flex; align-items: center; break-inside: avoid; padding-bottom: 1px; } :where(.${className}-swatches-columns .${className}-swatch::before) { flex-shrink: 0; } :where(.${className}-swatches-columns .${className}-swatch-label) { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }`; swatches .style("columns", columns) .selectAll() .data(scale.domain) .enter() .append("div") .attr("class", `${className}-swatch`) .call(swatch, scale, swatchWidth, swatchHeight) .call((item) => item.append("div").attr("class", `${className}-swatch-label`).attr("title", tickFormat).text(tickFormat) ); } else { extraStyle = `:where(.${className}-swatches-wrap) { display: flex; align-items: center; min-height: 33px; flex-wrap: wrap; } :where(.${className}-swatches-wrap .${className}-swatch) { display: inline-flex; align-items: center; margin-right: 1em; }`; swatches .selectAll() .data(scale.domain) .enter() .append("span") .attr("class", `${className}-swatch`) .call(swatch, scale, swatchWidth, swatchHeight) .append(function () { return this.ownerDocument.createTextNode(tickFormat.apply(this, arguments)); }); } return swatches .call((div) => div.insert("style", "*").text( `:where(.${className}-swatches) { font-family: system-ui, sans-serif; font-size: 10px; margin-bottom: 0.5em; } :where(.${className}-swatch > svg) { margin-right: 0.5em; overflow: visible; } ${extraStyle}` ) ) .style("margin-left", marginLeft ? `${+marginLeft}px` : null) .style("width", width === undefined ? null : `${+width}px`) .style("font-variant", impliedString(fontVariant, "normal")) .call(applyInlineStyles, style) .node(); }