From a7fb76bd15d81b2247da3faab1025431f053b178 Mon Sep 17 00:00:00 2001 From: Alexandre Capt Date: Tue, 1 Oct 2024 10:28:50 +0200 Subject: [PATCH] feat(rum-oversight): date range picker --- tools/oversight/charts/skyline.js | 75 +++- tools/oversight/elements/daterange-picker.js | 392 +++++++++++++++++++ tools/oversight/explorer.html | 20 +- tools/oversight/loader.js | 70 +++- tools/oversight/rum-slicer.css | 72 ++-- tools/oversight/slicer.js | 50 ++- 6 files changed, 601 insertions(+), 78 deletions(-) create mode 100644 tools/oversight/elements/daterange-picker.js diff --git a/tools/oversight/charts/skyline.js b/tools/oversight/charts/skyline.js index 713ed657..d8263610 100644 --- a/tools/oversight/charts/skyline.js +++ b/tools/oversight/charts/skyline.js @@ -42,16 +42,20 @@ export default class SkylineChart extends AbstractChart { groupFn.fillerFn = (existing) => { const endDate = this.chartConfig.endDate ? new Date(this.chartConfig.endDate) : new Date(); - // set start date depending on the unit - let startDate = new Date(endDate); - // roll back to beginning of time - if (this.chartConfig.unit === 'day') startDate.setDate(endDate.getDate() - 30); - if (this.chartConfig.unit === 'hour') startDate.setDate(endDate.getDate() - 7); - if (this.chartConfig.unit === 'week') startDate.setMonth(endDate.getMonth() - 12); - if (this.chartConfig.startDate) { - // nevermind, we have a start date in the config, let's use that + + let startDate; + if (!this.chartConfig.startDate) { + // set start date depending on the unit + startDate = new Date(endDate); + // roll back to beginning of time + if (this.chartConfig.unit === 'day') startDate.setDate(endDate.getDate() - 30); + if (this.chartConfig.unit === 'hour') startDate.setDate(endDate.getDate() - 7); + if (this.chartConfig.unit === 'week') startDate.setMonth(endDate.getMonth() - 12); + if (this.chartConfig.unit === 'month') startDate.setMonth(endDate.getMonth() - 1); + } else { startDate = new Date(this.chartConfig.startDate); } + const slots = new Set(existing); const slotTime = new Date(startDate); // return Array.from(slots); @@ -62,6 +66,7 @@ export default class SkylineChart extends AbstractChart { if (this.chartConfig.unit === 'day') slotTime.setDate(slotTime.getDate() + 1); if (this.chartConfig.unit === 'hour') slotTime.setHours(slotTime.getHours() + 1); if (this.chartConfig.unit === 'week') slotTime.setDate(slotTime.getDate() + 7); + if (this.chartConfig.unit === 'month') slotTime.setMonth(slotTime.getMonth() + 1); maxSlots -= 1; if (maxSlots < 0) { // eslint-disable-next-line no-console @@ -452,12 +457,41 @@ export default class SkylineChart extends AbstractChart { async draw() { const params = new URL(window.location).searchParams; - const view = ['week', 'month', 'year'].indexOf(params.get('view')) !== -1 - ? params.get('view') - : 'week'; - // TODO re-add. I think this should be a filter + const view = params.get('view'); + // eslint-disable-next-line no-unused-vars - const endDate = params.get('endDate') ? `${params.get('endDate')}T00:00:00` : null; + const startDate = params.get('startDate'); + const endDate = params.get('endDate'); + + let customView = 'year'; + let unit = 'month'; + let units = 12; + if (view === 'custom') { + const diff = endDate ? new Date(endDate).getTime() - new Date(startDate).getTime() : 0; + if (diff < (1000 * 60 * 60 * 24)) { + // less than a day + customView = 'hour'; + unit = 'hour'; + units = 24; + } else if (diff <= (1000 * 60 * 60 * 24 * 7)) { + // less than a week + customView = 'week'; + unit = 'hour'; + units = Math.round(diff / (1000 * 60 * 60)); + } else if (diff <= (1000 * 60 * 60 * 24 * 31)) { + // less than a month + customView = 'month'; + unit = 'day'; + units = 30; + } else if (diff <= (1000 * 60 * 60 * 24 * 365 * 3)) { + // less than 3 years + customView = 'week'; + unit = 'week'; + units = Math.round(diff / (1000 * 60 * 60 * 24 * 7)); + } + } + + const focus = params.get('focus'); if (this.dataChunks.filtered.length < 1000) { this.elems.lowDataWarning.ariaHidden = 'false'; @@ -470,18 +504,32 @@ export default class SkylineChart extends AbstractChart { view, unit: 'day', units: 30, + focus, + startDate, endDate, }, week: { view, unit: 'hour', units: 24 * 7, + focus, + startDate, endDate, }, year: { view, unit: 'week', units: 52, + focus, + startDate, + endDate, + }, + custom: { + view: customView, + unit, + units, + focus, + startDate, endDate, }, }; @@ -559,6 +607,7 @@ export default class SkylineChart extends AbstractChart { this.stepSize = undefined; this.clsAlreadyLabeled = false; this.lcpAlreadyLabeled = false; + this.chart.update(); // add trend indicators diff --git a/tools/oversight/elements/daterange-picker.js b/tools/oversight/elements/daterange-picker.js new file mode 100644 index 00000000..80c50667 --- /dev/null +++ b/tools/oversight/elements/daterange-picker.js @@ -0,0 +1,392 @@ +function debounce(func, wait) { + let timeout; + // eslint-disable-next-line func-names + return function (...args) { + const context = this; + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(context, args), wait); + }; +} + +// date management +function pad(number) { + return number.toString().padStart(2, '0'); +} + +function toDateString(date) { + // convert date + const year = date.getFullYear(); + const month = pad(date.getMonth() + 1); + const day = pad(date.getDate()); + + return `${year}-${month}-${day}`; +} + +const STYLES = ` + .daterange-wrapper { + display: grid; + align-items: end; + gap: var(--spacing-l); + font-size: var(--type-body-s-size); + } + + .input-wrapper, .daterange-wrapper { + position: relative; + display: block; + } + + input { + width: 100%; + font: inherit; + border-color: var(--gray-100); + border: 2px solid var(--gray-300); + padding: 0.4em 0.85em 0.1em; + background-color: var(--gray-100); + cursor: pointer; + transition: border-color 0.2s, background-color 0.2s; + border-radius: 4px; + } + + input ~ ul li { + padding: 0.4em 0; + padding-left: 2rem; + cursor: pointer; + } + + ul.menu li:last-child { + position: relative; + margin-top: 16px; + } + + ul.menu li:last-child::before { + content: ''; + position: absolute; + top: calc((-0.5 * 16px) - (2px / 2)); + left: 0; + right: 0; + height: 2px; + background-color: var(--gray-200); + } + + .input-wrapper { + display: none; + background-color: white; + } + + input[data-custom='true'] ~ .input-wrapper { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 16px 12px; + right: 0; + margin-top: 4px; + border-radius: 4px; + padding: calc(0.4em + 2px); + background-color: white; + border: 1px solid var(--gray-100); + box-shadow: 5px 5px 5px var(--gray-700); + z-index: 10; + } + + ul { + position: relative; + list-style: none; + left: 0; + right: 0; + margin: 0; + border-radius: 8px; + padding: calc(0.4em + 2px); + background-color: white; + border: 1px solid var(--gray-100); + box-shadow: 5px 5px 5px var(--gray-700); + z-index: 20; + } + + ul.menu:not([hidden]) + .input-wrapper { + display: none; + } + + .date-field { + display: block; + margin-top: 0; + } + + .date-field label { + display: block; + } + + @media (width >= 740px) { + .input-wrapper { + grid-template-columns: repeat(2, 1fr); + } + + input[data-custom='true'] ~ .input-wrapper { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + + @media (width >= 900px) { + .daterange-wrapper { + font-size: var(--type-body-l-size); + } + + ul { + position: absolute; + } + + input { + min-width: 200px; + } + + .input-wrapper { + min-width: 600px; + } + + input[data-custom='true'] ~ .input-wrapper { + grid-template-columns: minmax(0, 1fr); + } + + .date-field { + display: flex; + align-items: center; + gap: 16px; + } + + .date-field label { + margin-top: 4px; + } + } + + @media (width >= 1200px) { + input[data-custom='true'] ~ .input-wrapper { + grid-template-columns: repeat(2, minmax(0, 1fr)); + position: absolute; + } + } +`; + +const TEMPLATE = ` +
+ + + +
+`; + +export default class TimeRangePicker extends HTMLElement { + constructor() { + super(); + + this.inputElement = null; + this.dropdownElement = null; + this.fromElement = null; + this.toElement = null; + this.datetimeWrapperElement = null; + + this.id = this.getAttribute('id'); + } + + connectedCallback() { + this.initDOM(); + this.initValue(); + } + + initDOM() { + this.attachShadow({ mode: 'open' }); + + const style = document.createElement('style'); + style.textContent = STYLES; + + const section = document.createRange().createContextualFragment(TEMPLATE); + + const sul = section.querySelector('ul'); + const ul = this.querySelector('ul'); + const options = this.querySelectorAll('ul li'); + + options.forEach((option) => { + if (option.getAttribute('aria-selected') !== 'true') { + option.setAttribute('aria-selected', false); + } + option.dataset.role = 'option'; + sul.appendChild(option); + }); + + ul.remove(); + + this.shadowRoot.innerHTML = ''; + + this.shadowRoot.appendChild(style); + this.shadowRoot.appendChild(section); + + this.inputElement = this.shadowRoot.querySelector('input'); + this.dropdownElement = this.shadowRoot.querySelector('ul'); + this.fromElement = this.shadowRoot.querySelector('[name="date-from"]'); + this.toElement = this.shadowRoot.querySelector('[name="date-to"]'); + this.datetimeWrapperElement = this.shadowRoot.querySelector('.input-wrapper'); + + this.registerListeners(); + } + + initValue() { + const { toElement } = this; + toElement.value = toDateString(new Date()); + } + + registerListeners() { + const { inputElement, dropdownElement } = this; + const options = dropdownElement.querySelectorAll('li'); + + inputElement.addEventListener('click', () => { + const expanded = inputElement.getAttribute('aria-expanded') === 'true'; + inputElement.setAttribute('aria-expanded', !expanded); + dropdownElement.hidden = expanded; + }); + + const $this = this; + options.forEach((option) => { + option.addEventListener('click', () => { + $this.value = { + value: option.dataset.value, + from: $this.fromElement.value || null, + to: $this.toElement.value || null, + }; + }); + }); + + const setValue = () => { + $this.value = { + value: 'custom', + from: this.fromElement.value, + to: this.toElement.value, + }; + }; + + this.fromElement.addEventListener('change', debounce(setValue, 500)); + this.toElement.addEventListener('change', debounce(setValue, 500)); + } + + get value() { + return { + value: this.inputElement.dataset.value, + from: this.fromElement.value, + to: this.toElement.value, + }; + } + + set value(config) { + const { value, from, to } = config; + const { + inputElement, dropdownElement, fromElement, toElement, + } = this; + + const option = dropdownElement.querySelector(`li[data-value="${value}"]`); + if (!option) { + return; + } + + inputElement.value = option.textContent; + inputElement.dataset.value = option.dataset.value; + inputElement.setAttribute('aria-expanded', false); + + const options = dropdownElement.querySelectorAll('li'); + options.forEach((o) => o.setAttribute('aria-selected', o === option)); + + dropdownElement.hidden = true; + + let dateFrom = new Date(from); + let dateTo = new Date(to); + if (dateFrom > dateTo) { + // swap the 2 dates + dateFrom = new Date(to); + dateTo = new Date(from); + } + + if (from) { + fromElement.value = toDateString(dateFrom); + } + + if (to) { + toElement.value = toDateString(dateTo); + } + + this.updateTimeframe({ + value, + from: this.fromElement.value, + to: this.toElement.value, + }); + + this.dispatchEvent(new Event('change', { + detail: { + value, + from: this.fromElement.value, + to: this.toElement.value, + }, + })); + } + + updateTimeframe({ value }) { + // maintain the readonly state of the date fields and default value + const { fromElement, toElement } = this; + + const now = new Date(); + + [fromElement, toElement].forEach((field) => { + field.readOnly = true; + }); + this.toggleCustomTimeframe(value === 'custom'); + + if (value === 'week') { + if (!fromElement.value) { + const lastWeek = now; + lastWeek.setHours(-7 * 24, 0, 0, 0); + fromElement.value = toDateString(lastWeek); + } + if (!toElement.value) { + toElement.value = toDateString(now); + } + } else if (value === 'month') { + if (!fromElement.value) { + const lastMonth = now; + lastMonth.setMonth(now.getMonth() - 1); + fromElement.value = toDateString(lastMonth); + } + if (!toElement.value) { + toElement.value = toDateString(now); + } + } else if (value === 'year') { + if (!fromElement.value) { + const lastYear = now; + lastYear.setFullYear(now.getFullYear() - 1); + fromElement.value = toDateString(lastYear); + } + if (!toElement.value) { + toElement.value = toDateString(now); + } + } else if (value === 'custom') { + [fromElement, toElement].forEach((field) => { + field.removeAttribute('readonly'); + }); + } + } + + toggleCustomTimeframe(enabled) { + const { inputElement, datetimeWrapperElement } = this; + + inputElement.dataset.custom = enabled; + datetimeWrapperElement.hidden = !enabled; + [...datetimeWrapperElement.children].forEach((child) => { + child.setAttribute('aria-hidden', !enabled); + }); + } +} diff --git a/tools/oversight/explorer.html b/tools/oversight/explorer.html index 8414b952..5db9e7db 100644 --- a/tools/oversight/explorer.html +++ b/tools/oversight/explorer.html @@ -22,6 +22,7 @@ import VitalsFacet from './elements/vitals-facet.js'; import URLSelector from './elements/url-selector.js'; import NumberFormat from './elements/number-format.js'; + import DateRangePicker from './elements/daterange-picker.js'; window.slicer = { Chart: SkylineChart, }; @@ -35,6 +36,7 @@ customElements.define('url-selector', URLSelector); customElements.define('conversion-tracker', ConversionTracker); customElements.define('number-format', NumberFormat); + customElements.define('daterange-picker', DateRangePicker); @@ -48,14 +50,15 @@
www.aem.live -
- - -
+ + + +
    @@ -327,6 +330,7 @@

    TTFB

    + \ No newline at end of file diff --git a/tools/oversight/loader.js b/tools/oversight/loader.js index dfcb2371..78bfc098 100644 --- a/tools/oversight/loader.js +++ b/tools/oversight/loader.js @@ -55,7 +55,19 @@ export default class DataLoader { return u.toString(); } - async fetchUTCMonth(utcISOString) { + // eslint-disable-next-line class-methods-use-this + filterByDateRange(data, start, end) { + if (start || end) { + const filtered = data.filter((bundle) => { + const time = new Date(bundle.timeSlot); + return ((start ? time >= start : true) && (end ? time <= end : true)); + }); + return filtered; + } + return data; + } + + async fetchUTCMonth(utcISOString, start, end) { this.granularity = 'month'; const [date] = utcISOString.split('T'); const dateSplits = date.split('-'); @@ -66,10 +78,10 @@ export default class DataLoader { const json = await resp.json(); const { rumBundles } = json; rumBundles.forEach((bundle) => addCalculatedProps(bundle)); - return { date, rumBundles }; + return { date, rumBundles: this.filterByDateRange(rumBundles, start, end) }; } - async fetchUTCDay(utcISOString) { + async fetchUTCDay(utcISOString, start, end) { this.granularity = 'day'; const [date] = utcISOString.split('T'); const datePath = date.split('-').join('/'); @@ -78,10 +90,10 @@ export default class DataLoader { const json = await resp.json(); const { rumBundles } = json; rumBundles.forEach((bundle) => addCalculatedProps(bundle)); - return { date, rumBundles }; + return { date, rumBundles: this.filterByDateRange(rumBundles, start, end) }; } - async fetchUTCHour(utcISOString) { + async fetchUTCHour(utcISOString, start, end) { this.granularity = 'hour'; const [date, time] = utcISOString.split('T'); const datePath = date.split('-').join('/'); @@ -91,11 +103,11 @@ export default class DataLoader { const json = await resp.json(); const { rumBundles } = json; rumBundles.forEach((bundle) => addCalculatedProps(bundle)); - return { date, hour, rumBundles }; + return { date, hour, rumBundles: this.filterByDateRange(rumBundles, start, end) }; } - async fetchLastWeek() { - const date = new Date(); + async fetchLastWeek(endDate) { + const date = endDate ? new Date(endDate) : new Date(); const hoursInWeek = 7 * 24; const promises = []; for (let i = 0; i < hoursInWeek; i += 1) { @@ -120,7 +132,7 @@ export default class DataLoader { async fetchPrevious12Months(endDate) { const date = endDate ? new Date(endDate) : new Date(); - const months = 12; + const months = 13; // 13 to include 2 partial months (first and last) const promises = []; for (let i = 0; i < months; i += 1) { promises.push(this.fetchUTCMonth(date.toISOString())); @@ -163,4 +175,44 @@ export default class DataLoader { } return Promise.all(promises); } + + async fetchPeriod(startDate, endDate) { + const start = new Date(startDate); + const originalStart = new Date(start); + const end = endDate ? new Date(endDate) : new Date(); + + const diff = end.getTime() - start.getTime(); + if (diff < 0) { + throw new Error('Start date must be before end date'); + } + + const promises = []; + + if (diff <= (1000 * 60 * 60 * 24 * 7)) { + // less than a week + const days = Math.round((diff / (1000 * 60 * 60 * 24))) + 1; + + for (let i = 0; i < days; i += 1) { + promises.push(this.fetchUTCDay(start.toISOString(), originalStart, end)); + start.setDate(start.getDate() + 1); + } + } else if (diff <= (1000 * 60 * 60 * 24 * 31)) { + // less than a month + const days = Math.round((diff / (1000 * 60 * 60 * 24))) + 1; + + for (let i = 0; i < days; i += 1) { + promises.push(this.fetchUTCDay(start.toISOString(), originalStart, end)); + start.setDate(start.getDate() + 1); + } + } else { + const months = Math.round(diff / (1000 * 60 * 60 * 24 * 31)) + 1; + + for (let i = 0; i < months; i += 1) { + promises.push(this.fetchUTCMonth(start.toISOString(), originalStart, end)); + start.setMonth(start.getMonth() + 1); + } + } + + return Promise.all(promises); + } } diff --git a/tools/oversight/rum-slicer.css b/tools/oversight/rum-slicer.css index 9de2b0f4..448ece96 100644 --- a/tools/oversight/rum-slicer.css +++ b/tools/oversight/rum-slicer.css @@ -25,6 +25,14 @@ --light-indigo: #d3d5ff; --medium-indigo: #acafff; --dark-indigo: #4046ca; + --gray-100: #e9e9e9; + --gray-200: #e1e1e1; + --gray-300: #dadada; + --gray-400: #c6c6c6; + --gray-500: #8f8f8f; + --gray-600: #717171; + --gray-700: #505050; + } body { @@ -45,26 +53,26 @@ body main > .section { /* general performance */ main li.score-good, vitals-facet div[data-value^="good"] label { - opacity: 1; color: var(--dark-green); background-color: var(--light-green); } main li.score-ni, vitals-facet div[data-value^="ni"] label { - opacity: 1; color: var(--dark-orange); background-color: var(--light-orange); } main li.score-poor, vitals-facet div[data-value^="poor"] label { - opacity: 1; color: var(--dark-red); background-color: var(--light-red); } -/* heading */ -main .title div { +main .title { + padding-top: 8px; display: flex; + justify-content: space-between; + align-items: center; + flex-flow: row wrap; gap: 1ch; } @@ -78,6 +86,15 @@ main .title select { font: inherit; } +main .title > * { + flex: 2 0px; +} + +main .title daterange-picker { + flex: 1 100%; + order: 3; +} + main .title incognito-checkbox { display: none; margin-bottom: 10px; @@ -88,13 +105,6 @@ main .title incognito-checkbox[mode="incognito"] { display: inline-block; } -main .title { - padding-top: 8px; - display: flex; - justify-content: space-between; - align-items: center; -} - main .title url-selector { margin-top: 8px; margin-bottom: 8px; @@ -103,6 +113,7 @@ main .title url-selector { display: flex; align-items: center; width: 100%; + flex-grow: 50; } main .title url-selector img { @@ -161,7 +172,6 @@ main .key-metrics ul > * p { main .key-metrics ul > * p .extra { font-weight: normal; font-size: 80%; - opacity: 0.6; } main .key-metrics ul > * p .extra::before { @@ -403,7 +413,6 @@ figcaption > span { background-color: var(--light-indigo); padding: 0 1em; text-align: end; - opacity: 0.6; margin-right: 0.5em; margin-bottom: 0.5em; white-space: nowrap; @@ -421,15 +430,6 @@ figcaption > span { background-color: var(--light-purple); } -.filter-tags > span.filter-tag-lcp, -.filter-tags > span.filter-tag-cls, -.filter-tags > span.filter-tag-inp, -.filter-tags > span.filter-tag-conversions, -.filter-tags > span.filter-tag-visits { - opacity: 1; -} - - /* filters */ .quick-filter { @@ -501,14 +501,9 @@ vitals-facet label { background-color: #eee; padding: 2px 6px; text-align: end; - opacity: 0.2; font-size: 12px; } -fieldset li[class$="-"] { - opacity: 0.2; -} - fieldset li.interesting { border-width: 2px; padding: 2px 5px; @@ -573,18 +568,17 @@ vitals-facet label::before { opacity: 1; } -#facets fieldset label .extra { +#facets fieldset label .extra, +#facets fieldset label span.extra { display: none; } #facets fieldset label .extra.interesting { display: inline; - opacity: 0.8; } #facets fieldset label .extra.significant { display: inline; - opacity: 0.9; } #facets fieldset label .count::before, @@ -666,7 +660,6 @@ vitals-facet fieldset label { } #facets vitals-facet fieldset input[type=checkbox] { - opacity: 0.1; width: 0; height: 0; } @@ -874,6 +867,7 @@ vitals-facet fieldset label { border: 2px solid; top: 4px; left: 4px; + z-index: 1; } .clipboard::after { @@ -959,7 +953,6 @@ a.drillup::after { max-width: 80%; background-color: #444; color: white; - opacity: 1; top: 32px; left: 0; right: 0; @@ -997,6 +990,19 @@ facet-sidebar[aria-disabled="true"] div.quick-filter { } @media (min-width: 900px) { + main .title { + flex-wrap: nowrap; + } + + main .title > * { + flex: 3 0; + } + + main .title daterange-picker { + flex: unset; + order: unset; + } + main .title url-selector input { font-size: var(--type-heading-xl-size); } diff --git a/tools/oversight/slicer.js b/tools/oversight/slicer.js index 29d9c645..ca98fde1 100644 --- a/tools/oversight/slicer.js +++ b/tools/oversight/slicer.js @@ -27,7 +27,7 @@ loader.apiEndpoint = API_ENDPOINT; const herochart = new window.slicer.Chart(dataChunks, elems); -window.addEventListener('pageshow', () => elems.canvas && herochart.render()); +window.addEventListener('pageshow', () => !elems.canvas && herochart.render()); // set up metrics for dataChunks dataChunks.addSeries('pageViews', (bundle) => bundle.weight); @@ -331,15 +331,14 @@ export async function draw() { console.log(`full ui updated in ${new Date() - startTime}ms`); } -async function loadData(scope, chart) { +async function loadData(config) { + const scope = config.value; const params = new URL(window.location.href).searchParams; - const endDate = params.get('endDate') ? `${params.get('endDate')}T00:00:00` : null; - const startDate = params.get('startDate') ? `${params.get('startDate')}T00:00:00` : null; + const startDate = params.get('startDate') ? `${params.get('startDate')}` : null; + const endDate = params.get('endDate') ? `${params.get('endDate')}` : null; if (startDate && endDate) { - dataChunks.load(await loader.fetchDateRange(startDate, endDate)); - chart.config.startDate = startDate; - chart.config.endDate = endDate; + dataChunks.load(await loader.fetchPeriod(startDate, endDate)); } else if (scope === 'week') { dataChunks.load(await loader.fetchLastWeek(endDate)); } else if (scope === 'month') { @@ -354,8 +353,14 @@ export function updateState() { const { searchParams } = new URL(window.location.href); url.searchParams.set('domain', DOMAIN); url.searchParams.set('filter', elems.filterInput.value); - url.searchParams.set('view', elems.viewSelect.value); - if (searchParams.get('endDate')) url.searchParams.set('endDate', searchParams.get('endDate')); + + const viewConfig = elems.viewSelect.value; + url.searchParams.set('view', viewConfig.value); + if (viewConfig.value === 'custom') { + url.searchParams.set('startDate', viewConfig.from); + url.searchParams.set('endDate', viewConfig.to); + } + // if (searchParams.get('endDate')) url.searchParams.set('endDate', searchParams.get('endDate')); if (searchParams.get('metrics')) url.searchParams.set('metrics', searchParams.get('metrics')); const drilldown = new URL(window.location).searchParams.get('drilldown'); if (drilldown) url.searchParams.set('drilldown', drilldown); @@ -417,26 +422,41 @@ const io = new IntersectionObserver((entries) => { elems.filterInput = sidebar.elems.filterInput; const params = new URL(window.location).searchParams; - const view = params.get('view') || 'week'; + let view = params.get('view'); + if (!view) { + view = 'week'; + params.set('view', view); + const url = new URL(window.location.href); + url.search = params.toString(); + window.history.replaceState({}, '', url); + } + + const startDate = params.get('startDate') ? `${params.get('startDate')}` : null; + const endDate = params.get('endDate') ? `${params.get('endDate')}` : null; elems.incognito.addEventListener('change', async () => { loader.domainKey = elems.incognito.getAttribute('domainkey'); - await loadData(view, herochart); + + await loadData(elems.viewSelect.value); draw(); }); herochart.render(); - // sidebar.updateFacets(); elems.filterInput.value = params.get('filter'); - elems.viewSelect.value = view; + elems.viewSelect.value = { + value: view, + from: startDate, + to: endDate, + }; + setDomain(params.get('domain') || 'www.thinktanked.org', params.get('domainkey') || ''); const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; elems.timezoneElement.textContent = timezone; if (elems.incognito.getAttribute('domainkey')) { - loadData(view, herochart).then(draw); + loadData(elems.viewSelect.value).then(draw); } elems.filterInput.addEventListener('input', () => { @@ -444,7 +464,7 @@ const io = new IntersectionObserver((entries) => { draw(); }); - elems.viewSelect.addEventListener('input', () => { + elems.viewSelect.addEventListener('change', () => { updateState(); window.location.reload(); });