legends.js 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. import {rgb} from "d3";
  2. import {createContext} from "./context.js";
  3. import {legendRamp} from "./legends/ramp.js";
  4. import {legendSwatches, legendSymbols} from "./legends/swatches.js";
  5. import {inherit, isScaleOptions} from "./options.js";
  6. import {normalizeScale} from "./scales.js";
  7. const legendRegistry = new Map([
  8. ["symbol", legendSymbols],
  9. ["color", legendColor],
  10. ["opacity", legendOpacity]
  11. ]);
  12. export function legend(options = {}) {
  13. for (const [key, value] of legendRegistry) {
  14. const scale = options[key];
  15. if (isScaleOptions(scale)) {
  16. // e.g., ignore {color: "red"}
  17. const context = createContext(options);
  18. let hint;
  19. // For symbol legends, pass a hint to the symbol scale.
  20. if (key === "symbol") {
  21. const {fill, stroke = fill === undefined && isScaleOptions(options.color) ? "color" : undefined} = options;
  22. hint = {fill, stroke};
  23. }
  24. return value(normalizeScale(key, scale, hint), legendOptions(context, scale, options), (key) =>
  25. isScaleOptions(options[key]) ? normalizeScale(key, options[key]) : null
  26. );
  27. }
  28. }
  29. throw new Error("unknown legend type; no scale found");
  30. }
  31. export function exposeLegends(scales, context, defaults = {}) {
  32. return (key, options) => {
  33. if (!legendRegistry.has(key)) throw new Error(`unknown legend type: ${key}`);
  34. if (!(key in scales)) return;
  35. return legendRegistry.get(key)(scales[key], legendOptions(context, defaults[key], options), (key) => scales[key]);
  36. };
  37. }
  38. function legendOptions({className, ...context}, {label, ticks, tickFormat} = {}, options) {
  39. return inherit(options, {className, ...context}, {label, ticks, tickFormat});
  40. }
  41. function legendColor(color, {legend = true, ...options}) {
  42. if (legend === true) legend = color.type === "ordinal" ? "swatches" : "ramp";
  43. if (color.domain === undefined) return; // no identity legend
  44. switch (`${legend}`.toLowerCase()) {
  45. case "swatches":
  46. return legendSwatches(color, options);
  47. case "ramp":
  48. return legendRamp(color, options);
  49. default:
  50. throw new Error(`unknown legend type: ${legend}`);
  51. }
  52. }
  53. function legendOpacity({type, interpolate, ...scale}, {legend = true, color = rgb(0, 0, 0), ...options}) {
  54. if (!interpolate) throw new Error(`${type} opacity scales are not supported`);
  55. if (legend === true) legend = "ramp";
  56. if (`${legend}`.toLowerCase() !== "ramp") throw new Error(`${legend} opacity legends are not supported`);
  57. return legendColor({type, ...scale, interpolate: interpolateOpacity(color)}, {legend, ...options});
  58. }
  59. function interpolateOpacity(color) {
  60. const {r, g, b} = rgb(color) || rgb(0, 0, 0); // treat invalid color as black
  61. return (t) => `rgba(${r},${g},${b},${t})`;
  62. }
  63. export function createLegends(scales, context, options) {
  64. const legends = [];
  65. for (const [key, value] of legendRegistry) {
  66. const o = options[key];
  67. if (o?.legend && key in scales) {
  68. const legend = value(scales[key], legendOptions(context, scales[key], o), (key) => scales[key]);
  69. if (legend != null) legends.push(legend);
  70. }
  71. }
  72. return legends;
  73. }