@legumeinfo/web-components
Version:
Web Components for the Legume Information System and other AgBio databases
285 lines • 11.3 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var LisLegendElement_1;
import { html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { LisResizeObserverController } from '../controllers';
import { globalSubstitution } from '../utils/decorators';
/**
* @htmlElement `<lis-legend-element>`
*
* A Web Component that draws a legend for data provided as a {@link LegendData | `LegendData`}
* object. Note that the component fills all of the available horizontal space and will
* automatically redraw if the width of its parent element changes.
*
* @example
* The `<lis-legend-element>` tag requires <b>version 7</b> of {@link https://d3js.org/ | D3}.
* To allow multiple versions of D3 to be used on the same page, the
* {@link LisLegendElement | `LisLegendElement`} class uses the global `d3v7` variable if it has
* been set. Otherwise it uses the global `d3` variable by default. The following is an example of
* how to include D3 in the page and set the `d3v7` variable:
* ```html
* <!-- head -->
*
* <!-- D3 version 7 -->
* <script src='http://d3js.org/d3.v7.min.js'></script>
*
* <!-- another version of D3 -->
* <script type='text/javascript'>
* var d3v7 = d3;
* window.d7 = undefined;
* </script>
* <script src='http://d3js.org/d3.v3.min.js'></script>
*
* <!-- body -->
*
* <!-- add the Web Component to your HTML -->
* <lis-legend-element></lis-legend-element>
* ```
*
* @example
* {@link !HTMLElement | `HTMLElement`} properties can only be set via JavaScript. This means the
* {@link data | `data`}, {@link colorFunction | `colorFunction`}, and
* {@link clickFunction | `nodeClickFunction`} properties must be set on a `<lis-legend-element>`
* tag's instance of the {@link LisLegendElement | `LisLegendElement`} class. For example:
* ```html
* <!-- add the Web Component to your HTML -->
* <lis-legend-element id="legend"></lis-legend-element>
*
* <!-- configure the Web Component via JavaScript -->
* <script type="text/javascript">
* // legend data
* const data = [
* {label: 'Acacia crassicarpa'},
* {label: 'Aeschynomene evenia'},
* {label: 'Apios americana'},
* ];
* // returns a color given an entry
* function color(entry) {
* // returns a color for the given label
* }
* // click callback that gets passed the clicked entry
* function click(entry) {
* // do something
* }
* // get the legend element
* const legendElement = document.getElementById('legend');
* // set the element's properties
* phylotreeElement.data = data;
* phylotreeElement.colorFunction = color;
* phylotreeElement.clickFunction = click;
* </script>
* ```
*
* @example
* The {@link layout | `layout`}, {@link glyph | `glyph`}, and {@link position | `position`}
* properties can be set as attributes of the `<lis-legend-element>` tag or as properties of the
* tag's instance of the {@link LisLegendElement | `LisLegendElement`} class.
* {@link layout | `layout`} sets the layout of the legend to `vertical` or `horizontal` (`vertical`
* by default). {@link glyph | `glyph`} determines whether the glyph of each entry will be drawn as
* a circle or a square (`circle` by default). And {@link position | `position`} determines the
* position of the glyph relative to the label (`left` by default). For example:
* ```html
* <!-- add the Web Component to your HTML -->
* <lis-legend-element
* layout="horizontal"
* glyph="square"
* position="contain"
* ></lis-legend-element>
* <lis-legend-element id="legend"></lis-legend-element>
*
* <!-- configure the Web Component via JavaScript -->
* <script type="text/javascript">
* // get the legend element
* const legendElement = document.getElementById('legend');
* // set the element's properties
* legendElement.layout = 'horizontal';
* legendElement.glyph = 'square';
* legendElement.position = 'contain';
* </script>
* ```
*/
let LisLegendElement = LisLegendElement_1 = class LisLegendElement extends LitElement {
constructor() {
super(...arguments);
// bind to the tree container div element in the template
this._legendContainerRef = createRef();
// a controller that allows element resize events to be observed
this.resizeObserverController = new LisResizeObserverController(this, this._resize);
/**
* The layout the legend should be drawn in.
*
* @attribute
*/
this.layout = 'vertical';
/**
* The glyph to use for each element in the legend.
*
* @attribute
*/
this.glyph = 'circle';
/**
* Determines the position of the glyph relative to the label.
*
* @attribute
*/
this.position = 'left';
/**
* The legend data.
*
* @attribute
*/
this.data = [];
}
// disable shadow DOM to inherit global styles
createRenderRoot() {
return this;
}
_resize(entries) {
entries.forEach((entry) => {
// TODO: compare legend width to container width
//const drawnWidth = this._tree?.layout().width();
if (entry.target == this._legendContainerRef.value) {
this.requestUpdate();
}
});
}
_legendContainerReady() {
if (this._legendContainerRef.value) {
this.resizeObserverController.observe(this._legendContainerRef.value);
}
}
render() {
this._drawLegend();
return html ` <div
style="overflow: hidden;"
${ref(this._legendContainerRef)}
${ref(this._legendContainerReady)}
></div>`;
}
_legendWidth() {
if (this._legendContainerRef.value === undefined) {
return 0;
}
// compenstate for sub-container margins
return this._legendContainerRef.value.offsetWidth;
}
_drawLegend() {
if (this._legendContainerRef.value === undefined) {
return;
}
// reset the container
this._legendContainerRef.value.innerHTML = '';
// create the SVG element
const width = this._legendWidth();
const svg = d3.create('svg').attr('width', width);
this._legendContainerRef.value.append(svg.node());
// variables
const radius = this.glyph == 'circle' ? LisLegendElement_1.GLYPH_SIZE / 2 : 0;
const padding = Math.max(radius, LisLegendElement_1.GLYPH_MARGIN);
const color = this.position == 'contain' ? '#FFFFFF' : 'inherit';
// add a group for each entry
let x = 0;
let row = 0;
this.data.forEach((e, i) => {
// create the entry
const entry = svg.append('g');
if (this.clickFunction !== undefined) {
entry
.style('cursor', 'pointer')
.on('click', () => { var _a; return (_a = this.clickFunction) === null || _a === void 0 ? void 0 : _a.call(this, e); });
}
// add label
let offset = LisLegendElement_1.GLYPH_MARGIN; // position == 'right'
if (this.position == 'contain') {
offset = padding;
}
else if (this.position == 'left') {
offset += LisLegendElement_1.GLYPH_SIZE;
}
const text = entry
.append('text')
.attr('line-height', LisLegendElement_1.GLYPH_SIZE)
.attr('font-size', LisLegendElement_1.GLYPH_SIZE / 1.2) // 1.2 is roughly what web browsers use
.style('dominant-baseline', 'middle')
.attr('x', offset)
.attr('y', LisLegendElement_1.GLYPH_SIZE / 2)
.text(e.label)
.style('fill', color);
// add glyph
const textWidth = text.node().getComputedTextLength();
let w = LisLegendElement_1.GLYPH_SIZE;
if (this.position == 'contain') {
w = textWidth + padding * 2;
}
if (this.position == 'right') {
offset += textWidth + LisLegendElement_1.GLYPH_MARGIN;
}
else {
offset = 0;
}
entry
.append('rect')
.attr('width', w)
.attr('height', LisLegendElement_1.GLYPH_SIZE)
.attr('x', offset)
.attr('rx', radius)
.style('fill', () => { var _a; return e.color || ((_a = this.colorFunction) === null || _a === void 0 ? void 0 : _a.call(this, e)); });
text.raise();
// set the group position now that we know its size
if (this.layout == 'horizontal') {
const paddedWidth = entry.node().getBBox().width + LisLegendElement_1.GLYPH_MARGIN;
if (x + paddedWidth > width) {
x = 0;
row += 1;
}
const y = row * LisLegendElement_1.GLYPH_SIZE +
(row + 1) * LisLegendElement_1.GLYPH_MARGIN;
entry.attr('transform', `translate(${x}, ${y})`);
x += paddedWidth;
}
else {
const y = i * LisLegendElement_1.GLYPH_SIZE +
(i + 1) * LisLegendElement_1.GLYPH_MARGIN;
entry.attr('transform', `translate(0, ${y})`);
}
});
// set the SVG height now that all element have been added
const n = this.layout == 'horizontal' ? row + 1 : this.data.length;
const height = n * LisLegendElement_1.GLYPH_SIZE + (n + 1) * LisLegendElement_1.GLYPH_MARGIN;
svg.attr('height', height);
}
};
LisLegendElement.GLYPH_MARGIN = 5;
LisLegendElement.GLYPH_SIZE = 15;
__decorate([
property()
], LisLegendElement.prototype, "layout", void 0);
__decorate([
property()
], LisLegendElement.prototype, "glyph", void 0);
__decorate([
property()
], LisLegendElement.prototype, "position", void 0);
__decorate([
property({ attribute: false })
], LisLegendElement.prototype, "data", void 0);
__decorate([
property({ type: Function, attribute: false })
], LisLegendElement.prototype, "colorFunction", void 0);
__decorate([
property({ type: Function, attribute: false })
], LisLegendElement.prototype, "clickFunction", void 0);
__decorate([
globalSubstitution('d3', 'd3v7')
], LisLegendElement.prototype, "_drawLegend", null);
LisLegendElement = LisLegendElement_1 = __decorate([
customElement('lis-legend-element')
], LisLegendElement);
export { LisLegendElement };
//# sourceMappingURL=lis-legend-element.js.map