UNPKG

@teipublisher/pb-components

Version:
923 lines (862 loc) 29.7 kB
import { LitElement, html, css } from 'lit-element'; import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; import { SearchResultService } from './search-result-service.js'; import { ParseDateService } from './parse-date-service.js'; import { pbMixin } from './pb-mixin.js'; import '@polymer/iron-ajax'; import { translate } from './pb-i18n.js'; import { themableMixin } from './theming.js'; /** * A timeline component to display time series data in a bar chart like view. * * Time series data can be displayed in one of 6 different scales: * * - by decade (10Y) * - by 5 years (5Y) * - by years (Y) * - by month (M) * - by week (W) * - by day (D) * * The endpoint is expected to return a JSON object. Each property should either be a date or the special * marker `?`, which indicates undated resources. * The value associated with each entry * should either correspond to a count of resources or an object with properties `count` and `info`. * `info` should be an array, containing HTML to be shown in a list within the tooltips. * Expected JSON: * ```javascript * { * "1852-01-14": { * "count": 1, * "info": [ * "<a href='/briefe/B0977' part='tooltip-link'>Alfred Escher an Joseph Wolfgang von Deschwanden, Belvoir (Enge, Zürich), Mittwoch, 14. Januar 1852</a>" * ] * }, * "1874-01-25": { * "count": 3, * "info": [ * "<a href='/briefe/B8140' part='tooltip-link'>Alfred Escher an Gustav von Mevissen, Zürich, Sonntag, 25. Januar 1874</a>", * "<a href='/briefe/B8139' part='tooltip-link'>Alfred Escher an Theodor Weishaupt, Zürich, Sonntag, 25. Januar 1874</a>", * "<a href='/briefe/B8143' part='tooltip-link'>Alfred Escher an Ludwig August Parcus, Zürich, Sonntag, 25. Januar 1874</a>" * ] * } * } * ``` * Sample Usage: * ```xml * <pb-timeline url="api/timeline" scopes="['D', 'M', 'Y', '5Y', '10Y']" * resettable="" * subscribe="docs" emit="timeline"> * <span slot="label">Angezeigter Zeitraum: </span> * </pb-timeline> * ``` * See https://www.briefedition.alfred-escher.ch/briefe/ for a running sample. The source code of the webpage is here: https://github.com/stazh/briefedition-escher. Relevant files are: * - [templates/people.html](https://github.com/stazh/briefedition-escher/blob/master/templates/people.html#L91) - usage of pb-timeline * - [modules/custom-api.json](https://github.com/stazh/briefedition-escher/blob/master/modules/custom-api.json#L1080) - `/api/timeline` endpoint delivering required JSON object * * @slot label - Inserted before the label showing the currently displayed time range * * @fires pb-timeline-date-changed - Triggered when user clicks on a single entry * @fires pb-timeline-daterange-changed - Triggered when user selects a range of entries * @fires pb-timeline-reset-selection - Requests that the timeline is reset to initial state * @fires pb-timeline-loaded - Timeline was loaded * * @cssprop --pb-timeline-height * @cssprop --pb-timeline-padding * @cssprop --pb-timeline-color-highlight * @cssprop --pb-timeline-color-light * @cssprop --pb-timeline-color-dark * @cssprop --pb-timeline-color-selected * @cssprop --pb-timeline-color-bin * @cssprop --pb-timeline-title-font-size * @cssprop --pb-timeline-tooltip-font-size * @cssprop --pb-timeline-tooltip-min-width * @cssprop --pb-timeline-tooltip-max-width * * @csspart label * @csspart tooltip * @csspart title */ export class PbTimeline extends themableMixin(pbMixin(LitElement)) { static get styles() { return css` :host { display: block; } .hidden { visibility: hidden; } .draggable { cursor: grab; user-select: none; padding-right: 30px !important; } .wrapper { margin: 0 auto; padding: var(--pb-timeline-padding); width: auto; height: var(--pb-timeline-height, 80px); display: flex; position: relative; } .wrapper.empty { display: none; } .label { display: flex; align-items: center; } .bin-container { cursor: crosshair; margin-top: 20px; min-width: var(--pb-timeline-min-width, 14px); max-width: var(--pb-timeline-max-width, 20px); flex-grow: 1; flex-basis: 0; display: flex; align-items: flex-end; // justify-content: center; position: relative; } .bin-container.border-left, .bin-container.unknown { border-left: 1px solid rgba(0, 0, 0, 0.4); } .bin-container.unknown { margin-left: 40px; } .bin-container:hover .bin { background-color: var(--pb-timeline-color-highlight, #3f52b5); } .bin-container.selected > .bin { background-color: var(--pb-timeline-color-highlight, #3f52b5); } .bin-container.selected p { font-weight: bold; } .bin-container.white { background-color: var(--pb-timeline-color-light, white); } .bin-container.grey { background-color: var(--pb-timeline-color-dark, #f1f1f1); } .bin-container.selected { background-color: var(--pb-timeline-color-selected, #e6eaff) !important; } .bin { width: 80%; background-color: var(--pb-timeline-color-bin, #ccc); border-radius: 2px; user-select: none; } p.bin-title { pointer-events: none; position: absolute; top: 5px; z-index: 10; margin: 0; font-size: var(--pb-timeline-title-font-size, 12px); user-select: none; white-space: nowrap; } p.bin-title.months { top: -1px; } p.bin-title.weeks { top: 3px; } p.bin-title.days { top: -1px; } p.bin-title.rotated { transform: rotate(-90deg); } .bins-title { cursor: auto; font-weight: normal !important; margin: 0; white-space: nowrap; z-index: 200; position: absolute; left: 0; top: -20px; font-size: var(--pb-timeline-title-font-size, 12px); background-color: var(--pb-timeline-background-color-title, #535353); color: var(--pb-timeline-color-title, #ffffff); padding: 2px 4px; border-radius: 5px; height: var(--pb-timeline-title-font-size, 12px); line-height: var(--pb-timeline-title-font-size, 12px); user-select: none; } .info { display: none; } /* TOOLTIP */ .tooltip { display: inline-block; position: absolute; min-width: var(--pb-timeline-tooltip-min-width, 200px); max-width: var(--pb-timeline-tooltip-max-width, calc(100vw - 40px)); font-size: var(--pb-timeline-tooltip-font-size, 11px); line-height: 1.25; background: var(--pb-timeline-background-color-title, #535353); color: var(--pb-timeline-color-title, #ffffff); text-align: left; border-radius: 6px; padding: 5px 10px; top: calc(var(--pb-timeline-height, 80px) + 10px); left: 0; z-index: 1000; word-wrap: break-word; overflow-wrap: break-word; } .tooltip a:link, .tooltip a:visited { color: var(--pb-timeline-color-title, #ffffff); } .tooltip ul { list-style: none; margin: 0; padding: 0; } .tooltip-close { position: absolute; top: -13px; right: -10px; } .tooltip::after { /* small triangle that points to top */ content: ''; position: absolute; bottom: 100%; left: 10px; margin-left: -5px; border-width: 5px; border-style: solid; border-color: transparent transparent var(--pb-timeline-background-color-title, #535353) transparent; } .tooltip.chevron-precise::after { left: var(--chevron-position, 50%); right: auto; margin-left: -5px; margin-right: 0; } /* pure css close button for tooltip */ .close { position: relative; display: inline-block; width: 50px; height: 50px; overflow: hidden; transform: scale(0.25); } .close.rounded.black { cursor: pointer; } .close::before, .close::after { content: ''; position: absolute; height: 2px; width: 100%; top: 50%; left: 0; margin-top: -1px; background: #fff; } .close::before { transform: rotate(45deg); } .close::after { transform: rotate(-45deg); } .close.thick::before, .close.thick::after { height: 4px; margin-top: -2px; } .close.black::before, .close.black::after { height: 8px; margin-top: -4px; } .close.rounded::before, .close.rounded::after { border-radius: 5px; } #clear { background-image: var( --pb-dialog-background-image, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E") ); background-position: center; background-size: auto 1rem; background-repeat: no-repeat; background-color: transparent; border: none; display: block; height: 1rem; width: 1rem; } `; } static get properties() { return { ...super.properties, /** * start date for timeline to display */ startDate: { type: String, reflect: true, attribute: 'start-date', }, /** * endDate for timeline to display */ endDate: { type: String, reflect: true, attribute: 'end-date', }, /** * The scope for the timeline. Must be one of the pre-defined scopes. * If not set, the component automatically tries to determine the best scope fitting the * given time series. */ scope: { type: String, }, /** * The scopes to consider for automatic scoping. * * Defaults to ["D", "W", "M", "Y", "5Y", "10Y"] */ scopes: { type: Array, }, maxInterval: { type: Number, attribute: 'max-interval', }, /** * Endpoint to load timeline data from. Expects response to be an * object with key value pairs for (date, hits). * * Will be reloaded whenever 'start-date' or 'end-date' attributes change. */ url: { type: String, }, /** * If set, data will be retrieved automatically on first load. */ auto: { type: Boolean, }, resettable: { type: Boolean, }, _language: { type: String, }, }; } constructor() { super(); this.maxHeight = 80; // in pixels, has to be identical to the max-height specified in CSS this.multiplier = 0.75; // max percentage of bin compared to the bin-conainer. Set 1 for full height (not recommended) this.mousedown = false; this.startDate = ''; this.endDate = ''; this.scope = ''; this.scopes = ['D', 'W', 'M', 'Y', '5Y', '10Y']; this.maxInterval = 60; this.url = ''; this.auto = false; this.resettable = false; this._language = 'en'; this._resetSelectionProperty(); } connectedCallback() { super.connectedCallback(); this.subscribeTo('pb-results-received', () => { const loader = this.shadowRoot.getElementById('loadData'); const url = this.toAbsoluteURL(this.url, this.getEndpoint()); loader.url = url; loader.generateRequest(); }); this.subscribeTo('pb-i18n-update', ev => { this._language = ev.detail.language; }); } firstUpdated() { this.bins = this.shadowRoot.querySelectorAll('.bin-container'); this.tooltip = this.shadowRoot.querySelector('.tooltip'); // global mouseup event document.addEventListener('mouseup', () => { this._mouseUp(); }); // pb-timeline-daterange-changed event: // changes daterange selection (marks bins on histogram) // is triggered by the componeent itself but can be also triggered // from outside by another component document.addEventListener('pb-timeline-daterange-changed', event => { const { startDateStr } = event.detail; const { endDateStr } = event.detail; if (this._fullRangeSelected(startDateStr, endDateStr)) { // do not mark the whole histogram, reset selection instead console.log('_fullRangeSelected() is true'); this.resetSelection(); return; } this.select(startDateStr, endDateStr); }); // pb-timeline-reset-selection: // resets selection (remove marking of all selected bins) // is triggered by the componeent itself but can be also triggered // from outside by another component document.addEventListener('pb-timeline-reset-selection', () => { this.resetSelection(); this._hideTooltip(); }); } /** * checks if 'scope' has changed and re-applies dataset accordingly * * @param changedProperties */ updated(changedProperties) { if (changedProperties.has('scope')) { if (this.searchResult) { if (this.scopes.includes(this.scope)) { this.setData(this.searchResult.export(this.scope)); } else { console.error('unknown scope ', this.scope); } } } } setData(dataObj) { this.dataObj = dataObj; this.maxValue = Math.max(...this.dataObj.data.map(binObj => binObj.value)); this.requestUpdate(); this.updateComplete.then(() => { this.bins = this.shadowRoot.querySelectorAll('.bin-container'); this.resetSelection(); this._resetTooltip(); }); } get label() { if (!this.dataObj || this.dataObj.data.length === 0) { return ''; } if (this.dataObj.data.length === 1) { return this.dataObj.data[0].category; } return `${this.dataObj.data[0].selectionStart}${ this.dataObj.data[this.dataObj.data.length - 1].selectionEnd }`; } getSelectedStartDateStr() { return this.shadowRoot.querySelectorAll('.bin-container.selected')[0].dataset.selectionstart; } getSelectedEndDateStr() { const selectedBins = this.shadowRoot.querySelectorAll('.bin-container.selected'); return selectedBins[selectedBins.length - 1].dataset.selectionend; } getSelectedCategories() { const selectedBins = this.shadowRoot.querySelectorAll('.bin-container.selected'); const categories = []; selectedBins.forEach(bin => categories.push(bin.dataset.category)); return categories; } getSelectedItemCount() { const selectedBins = this.shadowRoot.querySelectorAll('.bin-container.selected'); let count = 0; selectedBins.forEach(bin => { count += parseInt(bin.dataset.value); }); return count; } resetSelection() { this.bins.forEach(bin => { bin.classList.remove('selected'); }); this._resetSelectionProperty(); this._hideTooltip(); } select(startDateStr, endDateStr) { this.bins.forEach(bin => { if (bin.dataset.isodatestr >= startDateStr && bin.dataset.isodatestr <= endDateStr) { bin.classList.add('selected'); } else { bin.classList.remove('selected'); } }); this._displayTooltip(); this._showtooltipSelection(); } _fullRangeSelected(startDateStr, endDateStr) { const matchingStartDate = (startDateStr = this.bins[0].dataset.isodatestr); const matchingEndDate = endDateStr === this.bins[this.bins.length - 1].dataset.isodatestr; return matchingStartDate && matchingEndDate; } _mouseDown(event) { this.resetSelection(); this.mousedown = true; this.selection.start = this._getMousePosition(event).x; this._applySelectionToBins(); } _mouseUp() { if (this.mousedown) { this.mousedown = false; const start = this.getSelectedStartDateStr(); const end = this.getSelectedEndDateStr(); if (start) { const startDateStr = new ParseDateService().run(start); const endDateStr = new ParseDateService().run(end); const itemCount = this.getSelectedItemCount(); this._dispatchTimelineDaterangeChangedEvent( startDateStr, endDateStr, this.getSelectedCategories(), itemCount, ); } } } _mouseMove(event) { if (this.mousedown) { this._brushing(event); this._showtooltipSelection(); } else if (this.selection.start === undefined) { // no selection currently made this._showtooltip(event); } } _mouseenter() { if (this.dataObj) { // if data is loaded this._displayTooltip(); } } _getMousePosition(mouseEvent) { const rect = this.shadowRoot.querySelector('.wrapper').getBoundingClientRect(); const x = mouseEvent.clientX - rect.left + 1; // x position within the element. const y = mouseEvent.clientY - rect.top + 1; // y position within the element. return { x, y }; } _brushing(event) { this.selection.end = this._getMousePosition(event).x; this._applySelectionToBins(); } _dispatchTimelineDaterangeChangedEvent(startDateStr, endDateStr, categories, itemCount) { if (startDateStr === '????-??-??') { this.emitTo('pb-timeline-date-changed', { startDateStr: null, endDateStr: null, categories: ['?'], count: itemCount, }); } else if (startDateStr === endDateStr) { if (this.dataObj.scope !== 'D') { this.emitTo('pb-timeline-daterange-changed', { startDateStr, endDateStr: this.searchResult.getEndOfRangeDate(this.dataObj.scope, endDateStr), scope: this.dataObj.scope, categories, count: itemCount, label: this.label, }); } else { this.emitTo('pb-timeline-date-changed', { startDateStr, endDateStr: null, scope: this.dataObj.scope, categories, count: itemCount, label: this.label, }); } } else { this.emitTo('pb-timeline-daterange-changed', { startDateStr, endDateStr, categories, scope: this.dataObj.scope, count: itemCount, label: this.label, }); } } _dispatchPbTimelineResetSelectionEvent() { this.emitTo('pb-timeline-reset-selection'); } _showtooltip(event) { const interval = this._getElementInterval(event.currentTarget); const datestr = event.currentTarget.dataset.tooltip; const value = this._numberWithCommas(event.currentTarget.dataset.value); const info = event.currentTarget.querySelector('.info'); this.tooltip.querySelector( '.tooltip-text', ).innerHTML = `<div><strong>${datestr}</strong>: ${value}</div><ul>${ info ? info.innerHTML : '' }</ul>`; // Force a reflow to get accurate tooltip dimensions this.tooltip.style.visibility = 'hidden'; this.tooltip.style.display = 'block'; const tooltipWidth = this.tooltip.offsetWidth; this.tooltip.style.visibility = ''; this.tooltip.style.display = ''; const wrapper = this.shadowRoot.querySelector('.wrapper'); const wrapperRect = wrapper.getBoundingClientRect(); const wrapperWidth = wrapperRect.width; let offset; const centerPosition = Math.round((interval[0] + interval[1]) / 2); // Clear any existing chevron positioning classes this.tooltip.classList.remove('chevron-precise'); // Check if tooltip would overflow on the right if (centerPosition + tooltipWidth / 2 > wrapperWidth) { // Position tooltip to the right of the bin, aligned to the right edge offset = Math.max(0, interval[1] - tooltipWidth + 10); this.tooltip.classList.add('right'); } else if (centerPosition - tooltipWidth / 2 < 0) { // Position tooltip to the left of the bin, aligned to the left edge offset = Math.min(wrapperWidth - tooltipWidth, interval[0] - 10); this.tooltip.classList.remove('right'); } else { // Center the tooltip offset = centerPosition - tooltipWidth / 2; this.tooltip.classList.remove('right'); } this.tooltip.style.left = `${offset}px`; // Calculate precise chevron position to point to the bin const binCenter = centerPosition; const tooltipLeft = offset; const chevronPosition = Math.max(10, Math.min(tooltipWidth - 10, binCenter - tooltipLeft)); this.tooltip.style.setProperty('--chevron-position', `${chevronPosition}px`); this.tooltip.classList.add('chevron-precise'); } _showtooltipSelection() { const selectedBins = this._getSelectedBins(); const intervalStart = this._getElementInterval(selectedBins[0])[0]; // get first selected element left boundary const intervalEnd = this._getElementInterval(selectedBins[selectedBins.length - 1])[1]; // get last selected element right boundary const interval = [intervalStart, intervalEnd]; const label = `${selectedBins[0].dataset.selectionstart} - ${ selectedBins[selectedBins.length - 1].dataset.selectionend }`; const value = selectedBins.map(bin => Number(bin.dataset.value)).reduce((a, b) => a + b); const valueFormatted = this._numberWithCommas(value); this.tooltip.querySelector( '.tooltip-text', ).innerHTML = `<strong>${label}</strong>: ${valueFormatted}`; this.tooltip.querySelector('.tooltip-close').classList.remove('hidden'); this.tooltip.classList.add('draggable'); // Force a reflow to get accurate tooltip dimensions this.tooltip.style.visibility = 'hidden'; this.tooltip.style.display = 'block'; const tooltipWidth = this.tooltip.offsetWidth; this.tooltip.style.visibility = ''; this.tooltip.style.display = ''; const wrapper = this.shadowRoot.querySelector('.wrapper'); const wrapperRect = wrapper.getBoundingClientRect(); const wrapperWidth = wrapperRect.width; const centerPosition = Math.round((interval[0] + interval[1]) / 2); let offset; // Clear any existing chevron positioning classes this.tooltip.classList.remove('chevron-precise'); // Check if tooltip would overflow on the right if (centerPosition + tooltipWidth / 2 > wrapperWidth) { // Position tooltip to the right of the selection, aligned to the right edge offset = Math.max(0, interval[1] - tooltipWidth + 10); this.tooltip.classList.add('right'); } else if (centerPosition - tooltipWidth / 2 < 0) { // Position tooltip to the left of the selection, aligned to the left edge offset = Math.min(wrapperWidth - tooltipWidth, interval[0] - 10); this.tooltip.classList.remove('right'); } else { // Center the tooltip offset = centerPosition - tooltipWidth / 2; this.tooltip.classList.remove('right'); } this.tooltip.style.left = `${offset}px`; // Calculate precise chevron position to point to the center of the selection const selectionCenter = centerPosition; const tooltipLeft = offset; const chevronPosition = Math.max(10, Math.min(tooltipWidth - 10, selectionCenter - tooltipLeft)); this.tooltip.style.setProperty('--chevron-position', `${chevronPosition}px`); this.tooltip.classList.add('chevron-precise'); } _resetTooltip() { this._hideTooltip(); this.tooltip.style.left = '-1000px'; this.tooltip.querySelector('.tooltip-text').innerHTML = ''; } _hideTooltip() { if (this.selection.start === undefined) { this.tooltip.classList.add('hidden'); this.tooltip.classList.remove('draggable'); this.tooltip.querySelector('.tooltip-close').classList.add('hidden'); // Clear chevron positioning classes this.tooltip.classList.remove('chevron-precise'); } } _displayTooltip() { this.tooltip.classList.remove('hidden'); } _getElementInterval(nodeElement) { const rect = this.shadowRoot.querySelector('.wrapper').getBoundingClientRect(); const bin = nodeElement; const interval = [ bin.getBoundingClientRect().x, bin.getBoundingClientRect().x + bin.getBoundingClientRect().width, ]; const x1 = interval[0] - rect.left + 1; // x position within the element. const x2 = interval[1] - rect.left + 1; // x position within the element. return [x1, x2, rect.width / 2]; } _getSelectionInterval() { return [this.selection.start, this.selection.end].sort((a, b) => a - b); } _getSelectedBins() { return Array.prototype.slice .call(this.bins) .filter(binContainer => binContainer.classList.contains('selected')); } _resetSelectionProperty() { this.selection = { start: undefined, end: undefined, }; } _applySelectionToBins() { const selectionInterval = this._getSelectionInterval(); this.bins.forEach(bin => { const elInterval = this._getElementInterval(bin); // if (this.intervalsOverlapping(elInterval, selectionInterval)) { if (this._areOverlapping(elInterval, selectionInterval)) { bin.classList.add('selected'); } else { bin.classList.remove('selected'); } }); } _numberWithCommas(input) { return new Intl.NumberFormat(this._language, { style: 'decimal' }).format(input); } _areOverlapping(A, B) { // check if 2 intervals are overlapping return B[0] < A[0] ? B[1] > A[0] : B[0] < A[1]; } render() { return html` <div class="label" part="label"> <span class="label"><slot name="label"></slot>${this.label}</span> ${this.resettable ? html` <button id="clear" title="${translate('timeline.clear')}" @click="${this._dispatchPbTimelineResetSelectionEvent}"></button> ` : null} </div> <div class="wrapper ${!this.dataObj || this.dataObj.data.length <= 1 ? 'empty' : ''}" @mouseenter="${this._mouseenter}" @mouseleave="${this._hideTooltip}" > ${this.dataObj ? this.renderBins() : ''} ${this.renderTooltip()} <iron-ajax id="loadData" verbose handle-as="json" method="get" with-credentials @response="${this._handleResponse}" url="${this.url}?start=${this.startDate}&end=${this.endDate}" ?auto="${this.auto}" ></iron-ajax> </div> `; } renderTooltip() { return html` <div class="tooltip hidden" part="tooltip"> <div class="tooltip-text"></div> <div class="tooltip-close hidden" @click="${this._dispatchPbTimelineResetSelectionEvent}" > <span class="close rounded black"></span> </div> </div> `; } renderBins() { return html` ${this.dataObj.data.map( (binObj, indx) => html` <div class="bin-container ${binObj.seperator ? 'border-left' : ''} ${indx % 2 === 0 ? 'grey' : 'white'} ${binObj.category === '?' ? 'unknown' : ''}" data-tooltip="${binObj.tooltip}" data-category="${binObj.category}" data-selectionstart="${binObj.selectionStart}" data-selectionend="${binObj.selectionEnd}" data-isodatestr="${binObj.dateStr}" data-datestr="${binObj.dateStr}" data-value="${binObj.value}" @mousemove="${this._mouseMove}" @mousedown="${this._mouseDown}" > <div class="bin" style="height: ${(binObj.value / this.maxValue) * this.maxHeight * this.multiplier}px" ></div> <p class="bin-title ${this.dataObj.binTitleRotated ? 'rotated' : ''} ${this.scope}" > ${binObj.binTitle ? binObj.binTitle : ''} </p> ${binObj.title ? html` <p class="bins-title" part="title">${binObj.title}</p> ` : ''} ${this.renderInfo(binObj)} </div> `, )} `; } renderInfo(binObj) { if (binObj.info && binObj.info.length > 0 && binObj.info.length <= 10) { return html` <ul class="info"> ${binObj.info.map(info => html`<li>${unsafeHTML(info)}</li>`)} </ul> `; } return null; } async _handleResponse() { await this.updateComplete; const loader = this.shadowRoot.getElementById('loadData'); const data = loader.lastResponse; let newJsonData = {}; if (this.startDate && this.endDate) { Object.keys(data) .filter(key => key >= this.startDate && key < this.endDate) .forEach(key => { newJsonData[key] = data[key]; }); } else { newJsonData = data; } this.searchResult = new SearchResultService(newJsonData, this.maxInterval, this.scopes); this.setData(this.searchResult.export(this.scope)); this.emitTo('pb-timeline-loaded', { value: true, label: this.label, }); } } customElements.define('pb-timeline', PbTimeline);