diff --git a/src/core/index.ts b/src/core/index.ts index 1490e7f..f2b08f7 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -14,3 +14,4 @@ export * from './lis-simple-table-element'; export * from './lis-loading-element'; export * from './lis-modal-element'; export * from './lis-histogram-element'; +export * from './lis-scatter-plot-element'; diff --git a/src/core/lis-scatter-plot-element.ts b/src/core/lis-scatter-plot-element.ts new file mode 100644 index 0000000..b92b856 --- /dev/null +++ b/src/core/lis-scatter-plot-element.ts @@ -0,0 +1,224 @@ +import {LitElement, html} from 'lit'; +import {customElement, property, state} from 'lit/decorators.js'; +import {Ref, createRef, ref} from 'lit/directives/ref.js'; +import {LisResizeObserverController} from '../controllers'; +import * as d3 from 'd3'; + +export type ScatterPlotPointModel = [number, number]; + +/** + * @htmlElement `` is a custom web component for creating scatter plots using D3.js. + * + * @example + * Attributes: + * - {@link data | `data`}: An array of objects where each object represents a bar in the histogram. Each object should have a `name` and `count` property. + * - {@link xlabel | `xlabel`}: The label for the x-axis. + * - {@link ylabel | `ylabel`}: The label for the y-axis. + * - {@link width | `width`}: The width of the histogram in pixels. + * - {@link height | `height`}: The height of the histogram in pixels. + * - {@link orientation | `orientation`}: The orientation of the histogram. Can be either 'horizontal' or 'vertical'. Default is 'horizontal'. + * + * Example using JavaScript and HTML driven using ``: + * + * ```html + * + * + * + * + * ``` + * + * Example using only html: + * ```html + * + * + * ``` + */ +@customElement('lis-scatter-plot-element') +export class LisScatterPlotElement extends LitElement { + // bind to the container div element in the template + private _scatterPlotContainerRef: Ref = createRef(); + + // a controller that allows element resize events to be observed + protected resizeObserverController = new LisResizeObserverController( + this, + this.resize, + ); + + @state() + private _data: ScatterPlotPointModel[] = []; + + @state() + private _xlabel: string = 'X-axis'; + + @state() + private _ylabel: string = 'Y-axis'; + + @state() + private _width: number = 500; + + @state() + private _height: number = 500; + + /** + * The data to display in the scatter plot. + * + * @attribute + */ + @property() + set data(data: ScatterPlotPointModel[]) { + this._data = data; // parse data if needed here before setting it + } + + /** + * The label for the x-axis. + * + * @attribute + */ + @property() + set xlabel(xlabel: string) { + this._xlabel = xlabel; // format axis label if needed here before setting it + } + + /** + * The label for the y-axis. + * + * @attribute + */ + @property() + set ylabel(ylabel: string) { + this._ylabel = ylabel; // format axis label if needed here before setting it + } + + /** + * The width of the scatter plot in pixels. + * + * @attribute + */ + @property() + set width(width: number) { + this._width = +width; // format number width + } + + /** + * The height of the scatter plot in pixels. + * + * @attribute + */ + @property() + set height(height: number) { + this._height = +height; // format number height + } + + private resize(entries: ResizeObserverEntry[]) { + entries.forEach((entry: ResizeObserverEntry) => { + if ( + entry.target == this._scatterPlotContainerRef.value && + entry.contentBoxSize + ) { + this.requestUpdate(); + } + }); + } + + private scatterPlotContainerReady() { + if (this._scatterPlotContainerRef.value) { + this.resizeObserverController.observe( + this._scatterPlotContainerRef.value, + ); + } + } + + override render() { + this.renderScatterPlot(this._data); + return html`
`; + } + override createRenderRoot() { + return this; + } + + renderScatterPlot(data: ScatterPlotPointModel[]) { + if (!this._scatterPlotContainerRef.value) return; + this._scatterPlotContainerRef.value.innerHTML = ''; + const padding = 60; // padding around the SVG + const width = this._width - 2 * padding; // adjust width + const height = this._height - 2 * padding; // adjust height + + const svgContainer = d3 + .select(this._scatterPlotContainerRef.value) + .append('svg') + .attr('width', width + 2 * padding) + .attr('height', height + 2 * padding) + .append('g') + .attr('transform', `translate(${padding}, ${padding})`); + + const x = d3 + .scaleLinear() + .domain([0, d3.max(data, (d) => d[0]) as number]) + .range([0, width]); + + const y = d3 + .scaleLinear() + .domain([0, d3.max(data, (d) => d[1]) as number]) + .range([height, 0]); + + // Add the x-axis + svgContainer + .append('g') + .attr('transform', `translate(0,${height})`) + .call(d3.axisBottom(x)); + + // Add the y-axis + svgContainer.append('g').call(d3.axisLeft(y)); + + // Add the x-axis label + svgContainer + .append('text') + .attr('x', width / 2) + .attr('y', height + padding / 2) + .style('text-anchor', 'middle') + .text(this._xlabel); + + // Add the y-axis label + svgContainer + .append('text') + .attr('transform', 'rotate(-90)') + .attr('y', -padding / 2) + .attr('x', -height / 2) + .style('text-anchor', 'middle') + .text(this._ylabel); + } +}