/** @jsxImportSource react */ import { Widget, VDOM, WidgetConfig } from "../ui/Widget"; import { HtmlElement, HtmlElementConfig, HtmlElementInstance, } from "../widgets/HtmlElement"; import { PureContainer } from "../ui/PureContainer"; import { getShape } from "./shapes"; import { isUndefined } from "../util/isUndefined"; import { isNonEmptyArray } from "../util/isNonEmptyArray"; import { parseStyle } from "../util/parseStyle"; import { withHoverSync } from "../ui/HoverSync"; import { Format } from "../util/Format"; import { Instance } from "../ui/Instance"; import { RenderingContext } from "../ui/RenderingContext"; import { StringProp, StyleProp, BooleanProp } from "../ui/Prop"; export interface LegendEntryData { name: string; displayText?: string; active?: boolean; colorIndex?: number; disabled?: boolean; selected?: boolean; style?: any; shape?: string; shapeSize?: number; hoverSync?: any; hoverChannel?: string; hoverId?: any; onClick?: (e: MouseEvent) => void; value?: number; } export interface LegendConfig extends HtmlElementConfig { /** Name of the legend. Default is `legend`. */ name?: string; /** Default shape to use for all entries. */ shape?: StringProp; /** Style applied to each entry. */ entryStyle?: StyleProp; /** CSS class applied to each entry. */ entryClass?: StringProp; /** Style applied to the value display. */ valueStyle?: StyleProp; /** CSS class applied to the value display. */ valueClass?: StringProp; /** Set to `true` to show values next to legend entries. */ showValues?: BooleanProp; /** Format used for displaying values. Default is `s`. */ valueFormat?: string; /** Set to `true` for vertical layout. */ vertical?: boolean; /** Size of the shape in pixels. Default is `18`. */ shapeSize?: number; /** Size of the SVG container in pixels. Default is `20`. */ svgSize?: number; } export interface LegendInstance extends HtmlElementInstance { legends: Record< string, { entries: LegendEntryData[]; names: Record } >; } export class Legend extends HtmlElement { declare baseClass: string; declare name: string; declare vertical: boolean; declare shapeSize: number; declare shape: string | null; declare svgSize: number; declare showValues: boolean; declare valueFormat: string; declare entryStyle: any; declare valueStyle: any; static Scope: typeof PureContainer; constructor(config: LegendConfig) { super(config); } declareData(...args: any[]): void { super.declareData(...args, { shape: undefined, entryStyle: { structured: true }, entryClass: { structured: true }, valueStyle: { structured: true }, valueClass: { structured: true }, showValues: undefined, }); } init(): void { this.entryStyle = parseStyle(this.entryStyle); this.valueStyle = parseStyle(this.valueStyle); super.init(); } prepareData(context: RenderingContext, instance: LegendInstance): void { let { data } = instance; data.stateMods = Object.assign(data.stateMods || {}, { vertical: this.vertical, }); super.prepareData(context, instance); } isValidHtmlAttribute(attrName: string): string | false { switch (attrName) { case "shapeSize": case "svgSize": case "shape": case "entryStyle": case "entryClass": case "valueStyle": case "valueClass": case "showValues": case "valueFormat": return false; default: return super.isValidHtmlAttribute(attrName); } } explore(context: RenderingContext, instance: LegendInstance): void { if (!context.legends) context.legends = {}; instance.legends = context.legends; context.addLegendEntry = ( legendName: string | false, entry: LegendEntryData, ) => { if (!legendName) return; //case when all legends are scoped and new entry is added outside the scope if (!context.legends) return; let legend = context.legends[legendName]; if (!legend) legend = context.legends[legendName] = { entries: [], names: {}, }; if (!legend.names[entry.name]) { legend.entries.push(entry); legend.names[entry.name] = entry; } }; super.explore(context, instance); } renderChildren( context: RenderingContext, instance: LegendInstance, ): React.ReactNode[] { const CSS = this.CSS; let entries = instance.legends[this.name] && instance.legends[this.name].entries, list: React.ReactNode; let { entryClass, entryStyle, shape, valueClass, valueStyle } = instance.data; let valueFormatter = Format.parse(this.valueFormat); let valueClasses = this.showValues ? CSS.expand(CSS.element(this.baseClass, "value"), valueClass) : undefined; let entryTextClass = CSS.element(this.baseClass, "entry-text"); if (isNonEmptyArray(entries)) { list = entries.map((e: LegendEntryData, i: number) => withHoverSync( i, e.hoverSync, e.hoverChannel, e.hoverId, ({ onMouseMove, onMouseLeave, hover }) => (
{this.renderShape(e, shape)}
{e.displayText || e.name}
{this.showValues && (
{valueFormatter(e.value)}
)}
), ), ); } return [list, super.renderChildren(context, instance)]; } renderShape( entry: LegendEntryData, legendEntriesShape: string | null | undefined, ): React.ReactNode { const className = this.CSS.element(this.baseClass, "shape", { [`color-${entry.colorIndex}`]: entry.colorIndex != null && (isUndefined(entry.active) || entry.active), }); const shape = getShape(legendEntriesShape || entry.shape || "square"); // if the entry has a custom fill or stroke set, use it for both values let style = { ...entry.style }; if (hasVisibleValue(style.stroke) && !style.fill) style.fill = style.stroke; if (hasVisibleValue(style.fill) && !style.stroke) style.stroke = style.fill; return ( {shape( this.svgSize / 2, this.svgSize / 2, entry.shapeSize || this.shapeSize, { style, className, }, )} ); } } Legend.prototype.name = "legend"; Legend.prototype.baseClass = "legend"; Legend.prototype.vertical = false; Legend.prototype.memoize = false; Legend.prototype.shapeSize = 18; Legend.prototype.shape = null; Legend.prototype.svgSize = 20; Legend.prototype.showValues = false; Legend.prototype.valueFormat = "s"; Widget.alias("legend", Legend); interface LegendScopeInstance extends Instance { legends: Record< string, { entries: LegendEntryData[]; names: Record } >; } Legend.Scope = class extends PureContainer { explore(context: RenderingContext, instance: LegendScopeInstance): void { context.push("legends", (instance.legends = {})); super.explore(context, instance); } exploreCleanup( context: RenderingContext, instance: LegendScopeInstance, ): void { context.pop("legends"); } prepare(context: RenderingContext, instance: LegendScopeInstance): void { context.push("legends", instance.legends); } prepareCleanup( context: RenderingContext, instance: LegendScopeInstance, ): void { context.pop("legends"); } }; export const LegendScope = Legend.Scope; function hasVisibleValue(value: string) { return value && value != "transparent" && value != "none"; }