// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ Copyright (c) 2017, the Perspective Authors. ┃ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ // ┃ This file is part of the Perspective library, distributed under the terms ┃ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import * as d3 from "d3"; import * as d3Legend from "d3-svg-legend"; import scrollableLegend from "./scrollableLegend"; import { withoutOpacity } from "../series/seriesColors"; import { getChartElement } from "../plugin/root"; import { getOrCreateElement } from "../utils/utils"; import { D3Scale } from "../types"; export type LegendComponentSettings = { hideKeys?: any[]; mainValues?: any[]; }; const scrollColorLegend = (settings) => scrollableLegend( d3Legend.legendColor().shape("circle").shapeRadius(6), settings, ); const scrollSymbolLegend = (settings) => scrollableLegend( d3Legend.legendSymbol().shapePadding(1).labelOffset(3), settings, ); export const colorLegend = () => legendComponent(scrollColorLegend); export const symbolLegend = () => legendComponent(scrollSymbolLegend, symbolScale); export const colorGroupLegend = () => legendComponent(scrollColorLegend, symbolScale); function symbolScale(fromScale) { if (!fromScale) return null; const domain = fromScale.domain(); const range = fromScale.range().map((r) => d3.symbol().type(r)()); return d3.scaleOrdinal().domain(domain).range(range); } interface LegendComponent { (container): void; settings(): LegendComponentSettings; settings(nextSettings: LegendComponentSettings): LegendComponent; scale(): D3Scale | null; scale(nextScale: D3Scale | null): LegendComponent; color(): d3.ScaleOrdinal; color(nextColor: d3.ScaleOrdinal): LegendComponent; } function legendComponent( scrollLegendComponent, scaleModifier = undefined, ): LegendComponent { let settings: LegendComponentSettings = {}; let scale = null; // type? let color: D3Scale | null = null; const legend: Partial = (container) => { if (scale && scale.range().length > 1) { const scrollLegend = scrollLegendComponent(settings); scrollLegend .scale(scale) .orient("vertical") .on("cellclick", function (_d) { // d3-svg-legend is very outdated and incompat with d3 6.x const d = this.__data__; settings.hideKeys = settings.hideKeys || []; if (settings.hideKeys.includes(d)) { settings.hideKeys = settings.hideKeys.filter( (k) => k !== d, ); } else { settings.hideKeys.push(d); } getChartElement(this)._draw(); }); scrollLegend.labels((options) => { const parts = String(options.domain[options.i]).split("|"); return settings.mainValues.length <= 1 && parts.length > 1 ? parts.slice(0, parts.length - 1).join("|") : options.domain[options.i]; }); const legendSelection = getOrCreateElement( container, "div.legend-container", () => container.append("div"), ); scrollLegend.decorate((selection) => { const isHidden = (data) => settings.hideKeys && settings.hideKeys.includes(data); const cells = selection .select("g.legendCells") .attr("transform", "translate(20,20)") .selectAll("g.cell"); cells.classed("hidden", isHidden); cells.append("title").html((d) => d); if (color) { cells .select("circle, path") .style("fill", (d) => (isHidden(d) ? null : color(d))) .style("stroke", (d) => isHidden(d) ? null : withoutOpacity(color(d)), ); } }); // render the legend legendSelection .attr("class", "legend-container") .attr("borderbox-on-hover", true) .style("z-index", "2") .call(scrollLegend); } }; legend.settings = (...args: LegendComponentSettings[]): any => { if (args.length === 0) { return settings; } settings = args[0]; return legend; }; legend.scale = (...args: (D3Scale | null)[]): any => { if (!args.length) { return scale; } scale = scaleModifier ? scaleModifier(args[0]) : args[0]; return legend; }; legend.color = (...args: D3Scale[]): any => { if (!args.length) { return color; } color = args[0]; return legend; }; return legend as LegendComponent; }