@legumeinfo/web-components
Version:
Web Components for the Legume Information System and other AgBio databases
458 lines • 18.4 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 LisPhylotreeElement_1;
import { html, LitElement } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { LisResizeObserverController } from '../controllers';
import { circle } from './tnt/node-display';
import { globalSubstitution } from '../utils/decorators';
/**
* @htmlElement `<lis-phylotree-element>`
*
* A Web Component that draws a phylotree provided as either a Newick string or a
* {@link Phylotree | `Phylotree`} 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-phylotree-element>` tag requires the
* {@link https://tntvis.github.io/tnt.tree/ | TnT Tree} library, which itself requires
* <b>version 3</b> of {@link https://d3js.org/ | D3}. To allow multiple versions of D3 to
* be used on the same page, the {@link LisPhylotreeElement | `LisPhylotreeElement`} class
* uses the global `d3v3` variable if it has been set. Otherwise it uses the global `d3`
* variable by default. The following is an example of how to include these dependencies
* in the page and set the `d3v3` variable:
* ```html
* <!-- head -->
*
* <!-- D3 version 3-->
* <script src="http://d3js.org/d3.v3.min.js"></script>
*
* <!-- another version of D3 -->
* <script type="text/javascript">
* var d3v3 = d3;
* window.d3 = undefined;
* </script>
* <script src="http://d3js.org/d3.v7.min.js"></script>
*
* <!-- TnT -->
* <link rel="stylesheet" href="http://tntvis.github.io/tnt/build/tnt.css" type="text/css" />
* <script src="http://tntvis.github.io/tnt/build/tnt.min.js"></script>
*
* <!-- body -->
*
* <!-- add the Web Component to your HTML -->
* <lis-phylotree-element></lis-phylotree-element>
* ```
*
* @example
* {@link !HTMLElement | `HTMLElement`} properties can only be set via JavaScript. This means the
* {@link colorFunction | `colorFunction`}, {@link nodeClickFunction | `nodeClickFunction`}, and
* {@link labelClickFunction | `labelClickFunction`} properties must be set on a
* `<lis-phylotree-element>` tag's instance of the
* {@link LisPhylotreeElement | `LisPhylotreeElement`} class. Similarly, if the
* {@link tree | `tree`} property will be set to a {@link Phylotree | `Phylotree`} object, rather
* than a Newick string, then it too must be set via the class instance. For example:
* ```html
* <!-- add the Web Component to your HTML -->
* <lis-phylotree-element id="phylotree"></lis-phylotree-element>
*
* <!-- configure the Web Component via JavaScript -->
* <script type="text/javascript">
* // returns a color given label of a node'
* function nodeColor(label) {
* // returns a color for the given label
* }
* // label click function that gets passed the TnT Tree and the TnT Node
* // associated with the label and is called in the TnT context
* function labelClick(tree, node) {
* // `this` is the TnT context
* }
* // node click function that gets passed the TnT Tree and the clicked TnT node
* // and is called in the TnT context
* function nodeClick(tree, node) {
* // `this` is the TnT context
* }
* // get the phylotree element
* const phylotreeElement = document.getElementById('phylotree');
* // set the element's properties
* phylotreeElement.colorFunction = nodeColor;
* phylotreeElement.labelClickFunction = labelClick;
* phylotreeElement.nodeClickFunction = nodeClick;
* </script>
* ```
*
* @example
* The {@link layout | `layout`}, {@link scale | `scale`}, and {@link edgeLengths | `edgeLengths`}
* properties can be set as attributes of the `<lis-phylotree-element>` tag or as properties of the
* tag's instance of the {@link LisPhylotreeElement | `LisPhylotreeElement`} class.
* {@link layout | `layout`} sets the layout of the phylotree to `vertical` or `radial` (`vertical`
* by default). {@link scale | `scale`} determines whether or not an edge length scale will be
* shown with the tree (`false` by default). And {@link edgeLengths | `edgeLengths`} determines
* whether edges should be drawn using unit length or using length data provided by the tree (unit,
* i.e. `false`, by default). For example:
* ```html
* <!-- add the Web Component to your HTML -->
* <lis-phylotree-element
* tree="(B:6.0,(A:5.0,C:3.0,E:4.0)G:5.0,D:11.0);"
* layout="radial"
* scale
* edgeLengths
* ></lis-phylotree-element>
* <lis-phylotree-element id="phylotree"></lis-phylotree-element>
*
* <!-- configure the Web Component via JavaScript -->
* <script type="text/javascript">
* // get the gene search element
* const phylotreeElement = document.getElementById('phylotree');
* // set the element's properties
* phylotreeElement.layout = 'radial';
* phylotreeElement.scale = true;
* phylotreeElement.edgeLengths = true;
* </script>
* ```
*/
let LisPhylotreeElement = LisPhylotreeElement_1 = class LisPhylotreeElement extends LitElement {
constructor() {
super(...arguments);
// bind to the tree container div element in the template
this._treeContainerRef = createRef();
// bind to the scale container div element in the template
this._scaleContainerRef = createRef();
// a controller that allows element resize events to be observed
this.resizeObserverController = new LisResizeObserverController(this, this._resize);
// HACK: this variable is used to prevent label clicks from triggering node clicks
this._labelClicked = false;
/**
* The layout the tree should be drawn in.
*
* @attribute
*/
this.layout = 'vertical';
/**
* Determines whether or not a scale for the edge lengths will be drawn.
*
* @attribute
*/
this.scale = false;
/**
* Determines whether or not edge lengths are given by the tree. If not, each
* edge will be given unit length.
*
* @attribute
*/
this.edgeLengths = false;
/**
* Sets the pixel height of each label element.
*
* @attribute
*/
this.labelHeight = 15;
/**
* Determines if label click events are propagated to the node they are associated with.
* If so, this will trigger the node label click function. If a label click function and
* a node click function has been assigned, both click functions will be called.
*
* @attribute
*/
this.labelClickPropagation = false;
}
static newickToData(newick) {
return tnt.tree.parse_newick(newick);
}
// disable shadow DOM to inherit global styles, i.e. TnT styles
createRenderRoot() {
return this;
}
/**
* The tree data.
*
* @attribute
*/
set tree(tree) {
if (typeof tree == 'string') {
this._data = LisPhylotreeElement_1.newickToData(tree);
}
else {
this._data = tree;
}
}
_resize(entries) {
entries.forEach((entry) => {
var _a;
// @ts-expect-error Property 'layout' does not exist on type '{}'
const drawnWidth = (_a = this._tree) === null || _a === void 0 ? void 0 : _a.layout().width();
if (entry.target == this._treeContainerRef.value &&
drawnWidth !== this._treeWidth()) {
this.requestUpdate();
}
});
}
_treeContainerReady() {
if (this._treeContainerRef.value) {
this.resizeObserverController.observe(this._treeContainerRef.value);
}
}
render() {
this._drawTree();
this._drawScale();
return html ` <div
style="overflow: hidden; margin: 0 ${LisPhylotreeElement_1.TNT_LEFT_RIGHT_MARGIN}px"
${ref(this._scaleContainerRef)}
></div>
<div
style="overflow: hidden;"
${ref(this._treeContainerRef)}
${ref(this._treeContainerReady)}
></div>`;
}
_emitNodeClick(context, node) {
var _a;
(_a = this.nodeClickFunction) === null || _a === void 0 ? void 0 : _a.call(context, this._tree, node);
}
_emitLabelClick(context, node) {
var _a;
(_a = this.labelClickFunction) === null || _a === void 0 ? void 0 : _a.call(context, this._tree, node);
}
_treeWidth() {
if (this._treeContainerRef.value === undefined) {
return 0;
}
// compenstate for sub-container margins
return (this._treeContainerRef.value.offsetWidth -
LisPhylotreeElement_1.TNT_LEFT_RIGHT_MARGIN * 2);
}
_actualTreeWidth() {
if (this._treeContainerRef.value === undefined) {
return 0;
}
// compenstate for tree position and padding
return (this._treeWidth() -
LisPhylotreeElement_1.TNT_TRANSLATE -
// @ts-expect-error Object is of type 'unknown'
this._tree.layout().max_leaf_label_width());
}
_drawTree() {
if (this._data === undefined ||
this._treeContainerRef.value === undefined) {
return;
}
// reset the container
this._treeContainerRef.value.innerHTML = '';
// set the width of the tree
const width = this._treeWidth();
// styles a D3 selection if the given clickFunction is defined
const selectionClickStyleFactory = (clickFunction) => {
return (selection) => {
if (clickFunction) {
return selection.style('cursor', 'pointer');
}
return selection;
};
};
// configure the nodes
// NOTE: this uses a local copy of the tnt.tree.node_display.circle
// function because node CSS attributes can't be modified programmatically
// using only the TnT API
const nodes = circle(selectionClickStyleFactory(this.nodeClickFunction))
.size(5)
.fill((node) => {
if (node.data().color == null || node.data().color == '') {
if (this.colorFunction !== undefined && node.data().name) {
return this.colorFunction(node.data().name);
}
return 'white';
}
return node.data().color;
});
// configure the node labels
const labels = tnt.tree.label.text().height(this.labelHeight);
const defaultLabelDisplay = labels.display();
const clickable = this.labelClickFunction ||
(this.nodeClickFunction && this.labelClickPropagation);
labels.display(function (...args) {
// @ts-expect-error 'this' implicitly has type 'any'
const selection = defaultLabelDisplay.call(this, ...args);
return selectionClickStyleFactory(clickable)(selection);
});
// create the tree
this._tree = tnt
.tree()
.data(this._data)
.layout(tnt.tree.layout[this.layout]().width(width).scale(this.edgeLengths))
.node_display(nodes)
.label(labels);
// add a node click listener
if (this.nodeClickFunction !== undefined) {
const instance = this;
// @ts-expect-error Object is of type 'unknown'
this._tree.on('click', function (node) {
if (!instance._labelClicked || instance.labelClickPropagation) {
// @ts-expect-error 'this' implicitly has type 'any'
instance._emitNodeClick(this, node);
}
instance._labelClicked = false;
});
}
// add a label click listener
const instance = this;
labels.on('click', function (node) {
instance._labelClicked = true;
if (instance.labelClickFunction !== undefined ||
(instance.nodeClickFunction && instance.labelClickPropagation)) {
// @ts-expect-error 'this' implicitly has type 'any'
instance._emitLabelClick(this, node);
}
});
// draw the tree in the container
// @ts-expect-error Object is of type 'unknown'
this._tree(this._treeContainerRef.value);
}
/**
* Recursively computes the maximum distance from the root node.
*
* @param node The next node in the recursive traversal.
*/
_maxRootDist(node) {
if (node.is_leaf()) {
return node.root_dist();
}
const rootDists = node.children().map((c) => this._maxRootDist(c));
return Math.max(...rootDists);
}
// NOTE: this should be called from contexts with correct version of D3
_xAxis() {
var _a;
if (this._scaleContainerRef.value === undefined) {
return;
}
// @ts-expect-error Object is of type 'unknown'
const domain = this._maxRootDist((_a = this._tree) === null || _a === void 0 ? void 0 : _a.root());
// @ts-expect-error Object is of type 'unknown'
const distance = this._tree.scale_bar(LisPhylotreeElement_1.AXIS_SAMPLE_PIXELS, 'pixel');
const range = this._actualTreeWidth();
const scale = d3.scale.linear().domain([0, domain]).range([0, range]);
const axis = d3.svg
.axis()
.scale(scale)
.ticks(LisPhylotreeElement_1.AXIS_TICKS)
.orient('bottom');
return axis;
}
_drawScale() {
if (!this.scale ||
this._tree === undefined ||
this._scaleContainerRef.value === undefined) {
return;
}
// reset the container
this._scaleContainerRef.value.innerHTML = '';
const width = this._treeWidth();
const actualWidth = this._actualTreeWidth();
// draw the x-axis
d3.select(this._scaleContainerRef.value)
.append('svg')
.attr('width', width)
.attr('height', LisPhylotreeElement_1.SCALE_HEIGHT)
.append('g')
.attr('transform', `translate(${LisPhylotreeElement_1.TNT_TRANSLATE}, ${LisPhylotreeElement_1.TNT_TRANSLATE})`)
.attr('width', actualWidth)
.attr('class', 'x-axis');
this._updateXAxis(0);
}
_updateXAxis(duration = LisPhylotreeElement_1.TNT_TRANSITION_DURATION) {
d3.select(this._scaleContainerRef.value)
.select('.x-axis')
.transition()
.duration(duration)
.call(this._xAxis());
d3.select(this._scaleContainerRef.value)
.select('.domain')
.attr('fill', 'none')
.attr('stroke', 'black');
d3.select(this._scaleContainerRef.value)
.selectAll('g.tick > line')
.attr('stroke', 'black');
d3.selectAll(this._scaleContainerRef.value)
.selectAll('g.tick > text')
.style('font-size', '10px');
}
/**
* Calls the `.update()` method on the component's instance of the TnT Tree. This should be used
* in preference of calling the `.update()` method directly when using the scale property because
* this method will also update the scale to reflect updates in the tree.
*/
updateTree() {
// update the tree
// @ts-expect-error Object is of type 'unknown'
this._tree.update();
// update the x-axis
setTimeout(() => {
this._updateXAxis();
}, LisPhylotreeElement_1.TNT_TRANSITION_DURATION);
}
};
LisPhylotreeElement.AXIS_SAMPLE_PIXELS = 30;
LisPhylotreeElement.AXIS_TICKS = 12;
LisPhylotreeElement.SCALE_HEIGHT = 40;
LisPhylotreeElement.TNT_LEFT_RIGHT_MARGIN = 3;
LisPhylotreeElement.TNT_TRANSITION_DURATION = 500;
LisPhylotreeElement.TNT_TRANSLATE = 20;
__decorate([
state()
], LisPhylotreeElement.prototype, "_data", void 0);
__decorate([
property()
], LisPhylotreeElement.prototype, "layout", void 0);
__decorate([
property({ type: Boolean })
], LisPhylotreeElement.prototype, "scale", void 0);
__decorate([
property({ type: Boolean })
], LisPhylotreeElement.prototype, "edgeLengths", void 0);
__decorate([
property({ type: Number })
], LisPhylotreeElement.prototype, "labelHeight", void 0);
__decorate([
property()
], LisPhylotreeElement.prototype, "tree", null);
__decorate([
property({ type: Function, attribute: false })
], LisPhylotreeElement.prototype, "colorFunction", void 0);
__decorate([
property({ type: Function, attribute: false })
], LisPhylotreeElement.prototype, "nodeClickFunction", void 0);
__decorate([
property({ type: Function, attribute: false })
], LisPhylotreeElement.prototype, "labelClickFunction", void 0);
__decorate([
property({ type: Boolean })
], LisPhylotreeElement.prototype, "labelClickPropagation", void 0);
__decorate([
globalSubstitution('d3', 'd3v3')
], LisPhylotreeElement.prototype, "_emitNodeClick", null);
__decorate([
globalSubstitution('d3', 'd3v3')
], LisPhylotreeElement.prototype, "_emitLabelClick", null);
__decorate([
globalSubstitution('d3', 'd3v3')
], LisPhylotreeElement.prototype, "_drawTree", null);
__decorate([
globalSubstitution('d3', 'd3v3')
], LisPhylotreeElement.prototype, "_drawScale", null);
__decorate([
globalSubstitution('d3', 'd3v3')
], LisPhylotreeElement.prototype, "_updateXAxis", null);
__decorate([
globalSubstitution('d3', 'd3v3')
], LisPhylotreeElement.prototype, "updateTree", null);
LisPhylotreeElement = LisPhylotreeElement_1 = __decorate([
customElement('lis-phylotree-element')
], LisPhylotreeElement);
export { LisPhylotreeElement };
//# sourceMappingURL=lis-phylotree-element.js.map