From fbc50245ff7f660ff1d1d8579d6c5156e09ad172 Mon Sep 17 00:00:00 2001 From: Martin Boulais <31805063+martinboulais@users.noreply.github.com> Date: Thu, 23 Nov 2023 12:40:29 +0100 Subject: [PATCH] [O2B-1043] Add pre-filled options for time range filter (#1242) * [O2B-1043] Add pre-filled options for time range filter * Fix normalized value * Fix titles * lint --------- Co-authored-by: xsalonx <65893715+xsalonx@users.noreply.github.com> Co-authored-by: xsalonx --- lib/public/app.css | 18 +- .../common/filters/TimeRangeFilterModel.js | 20 +- .../Filters/common/filters/timeRangeFilter.js | 126 +++++++++-- .../common/formatting/formatTimeRange.js | 47 ++++ lib/public/utilities/dateUtils.js | 133 +++++++++++ lib/public/views/LhcFills/Overview/index.js | 2 +- lib/public/views/Statistics/StatisticsPage.js | 208 +++++++++--------- .../views/Statistics/StatisticsPageModel.js | 28 +-- 8 files changed, 440 insertions(+), 142 deletions(-) create mode 100644 lib/public/components/common/formatting/formatTimeRange.js diff --git a/lib/public/app.css b/lib/public/app.css index 27364572dd..50fb531be2 100644 --- a/lib/public/app.css +++ b/lib/public/app.css @@ -2,6 +2,7 @@ html, body { scroll-behavior: smooth; --ui-component-medium: 550px; + --ui-component-large: 850px; } .cell-xs { max-width: 2rem; } @@ -122,6 +123,10 @@ html, body { gap: var(--space-xl) } +.flex-grid { + display: grid; +} + /* Float */ .float-right { float: right } @@ -475,7 +480,7 @@ label { top: 0; left: 0; z-index: 1002; - max-width: var(--ui-component-medium); + max-width: var(--ui-component-large); } .no-events { @@ -621,6 +626,17 @@ label { stroke-width: 0.5px; } +/* Custom styles */ +.time-range-selector .month-options { + display: grid; + grid-template-columns: repeat(3, 1fr); +} + +.time-range-selector .month-option:nth-last-child(-n+3), .time-range-selector .dropdown-option:last-child { + border-bottom: 1px solid var(--color-gray-dark); +} + + /** * Breakpoints : * small : x < 600 (default styles) diff --git a/lib/public/components/Filters/common/filters/TimeRangeFilterModel.js b/lib/public/components/Filters/common/filters/TimeRangeFilterModel.js index 1de749ad61..8758824521 100644 --- a/lib/public/components/Filters/common/filters/TimeRangeFilterModel.js +++ b/lib/public/components/Filters/common/filters/TimeRangeFilterModel.js @@ -35,11 +35,12 @@ export class TimeRangeFilterModel extends FilterModel { * Constructor * * @param {Partial} [value] the initial value of the filter + * @param {string|null} [periodLabel] the label of the initial value if it applies (eg. current month) * @param {object} [configuration] the inputs configuration * @param {(boolean|{from: boolean, to: boolean})} [configuration.required] defines if the from/to dates are required (true means both are * required) */ - constructor(value, configuration) { + constructor(value, periodLabel, configuration) { super(); const { required = false } = configuration || {}; @@ -56,6 +57,7 @@ export class TimeRangeFilterModel extends FilterModel { this._fromTimeInputModel.visualChange$.bubbleTo(this.visualChange$); // eslint-disable-next-line no-return-assign this._fromTimeInputModel.observe(() => { + this._periodLabel = null; this._toTimeInputModel.min = this._fromTimeInputModel.value ? this._fromTimeInputModel.value + TIME_STEP : null; }); this._fromTimeInputModel.bubbleTo(this); @@ -63,20 +65,22 @@ export class TimeRangeFilterModel extends FilterModel { this._toTimeInputModel.visualChange$.bubbleTo(this.visualChange$); // eslint-disable-next-line no-return-assign this._toTimeInputModel.observe(() => { + this._periodLabel = null; this._fromTimeInputModel.max = this._toTimeInputModel.value ? this._toTimeInputModel.value - TIME_STEP : null; }); this._toTimeInputModel.bubbleTo(this); - this.setValue(value, false); + this.setValue(value, periodLabel, false); } /** * Set the current value of the filter * @param {Partial} value the new value of the filter + * @param {string|null} periodLabel if the specified value correspond to a specific period (fox example current month), this is its label * @param {boolean} [silent] if true, the observers will not be notified * @return {void} */ - setValue(value, silent = false) { + setValue(value, periodLabel = null, silent = false) { const { from, to } = value || {}; if (from === this._fromTimeInputModel.value && to === this._toTimeInputModel.value) { @@ -92,6 +96,8 @@ export class TimeRangeFilterModel extends FilterModel { this._fromTimeInputModel.max = to - TIME_STEP; } + this._periodLabel = periodLabel; + !silent && this.notify(); } @@ -113,6 +119,14 @@ export class TimeRangeFilterModel extends FilterModel { return this._toTimeInputModel; } + /** + * If it applies, returns the label of the currently selected period (eg. current month) + * @return {string|null} the period label + */ + get periodLabel() { + return this._periodLabel; + } + // eslint-disable-next-line valid-jsdoc /** * @inheritDoc diff --git a/lib/public/components/Filters/common/filters/timeRangeFilter.js b/lib/public/components/Filters/common/filters/timeRangeFilter.js index cdf8951aec..f412617d2f 100644 --- a/lib/public/components/Filters/common/filters/timeRangeFilter.js +++ b/lib/public/components/Filters/common/filters/timeRangeFilter.js @@ -13,37 +13,78 @@ import { dropdown } from '../../../common/popover/dropdown.js'; import { h } from '/js/src/index.js'; import { dateTimeInput } from '../../../common/form/inputs/dateTimeInput.js'; -import { getLocaleDateAndTime } from '../../../../utilities/dateUtils.js'; +import { + getLocaleDateAndTime, + getStartOfMonth, + getStartOfWeek, + getStartOfYear, + getWeeksCount, + MONTH_NAMES, +} from '../../../../utilities/dateUtils.js'; +import { formatTimeRange } from '../../../common/formatting/formatTimeRange.js'; /** - * Format the current timestamp range display + * Amount of years to display as pre-defined options * - * @param {Partial} the current period - * @return {Component} the resulting component + * @type {number} */ -const formatTimeRange = ({ from, to }) => { - let parts = []; +const YEARS_OPTIONS_COUNT = 3; - // eslint-disable-next-line require-jsdoc - const formatTimestamp = (timestamp) => { - const { date, time } = getLocaleDateAndTime(timestamp); - return h('.flex-column.g1', [h('.f6', date), h('', time)]); - }; +/** + * Returns a pre-defined list of years period selectors + * + * @param {function} onChange function called with the clicked period and its label + * @return {Component} the list of years selectors + */ +const yearsOptions = ({ onChange }) => { + const currentYear = new Date().getFullYear(); + return h('', new Array(YEARS_OPTIONS_COUNT).fill(0).map((_, index) => { + const year = currentYear - YEARS_OPTIONS_COUNT + index + 1; + return h( + '.dropdown-option.ph3.pv2', + { + onclick: () => onChange( + { + from: getStartOfYear(year).getTime(), + to: getStartOfYear(year + 1).getTime(), + }, + year, + ), + }, + year, + ); + })); +}; - // eslint-disable-next-line require-jsdoc - const label = (content) => h('.gray-darker', content); +/** + * Return a pre-defined list of months period selectors + * + * @param {function} onChange function called with the clicked period and its label + * @return {Component} the list of months selectors + */ +const monthsOptions = ({ onChange }) => { + const now = new Date(); + const currentMonthIndex = now.getUTCMonth(); + const currentYear = now.getFullYear(); - if (from === undefined && to === undefined) { - parts = '-'; - } else if (from === undefined) { - parts = [label('Before'), formatTimestamp(to)]; - } else if (to === undefined) { - parts = [label('After'), formatTimestamp(from)]; - } else { - parts = [label('From'), formatTimestamp(from), label('to'), formatTimestamp(to)]; - } + return h('.month-options', new Array(MONTH_NAMES.length).fill(0).map((_, index) => { + const monthIndex = (index + currentMonthIndex + 1) % MONTH_NAMES.length; + const year = monthIndex > currentMonthIndex ? currentYear - 1 : currentYear; + let label = MONTH_NAMES[monthIndex]; + if (currentYear !== year) { + label += ` (${year})`; + } - return h('.flex-row.items-center.text-center.g3', parts); + return h('.dropdown-option.month-option.ph3.pv2', { + onclick: () => onChange( + { + from: getStartOfMonth(monthIndex + 1, year).getTime(), + to: getStartOfMonth(monthIndex + 2, year).getTime(), + }, + label, + ), + }, label); + })); }; /** @@ -56,7 +97,17 @@ export const timeRangeFilter = (timeRangeFilterModel) => dropdown( h( `.dropdown-trigger.form-control${timeRangeFilterModel.isValid ? '' : '.invalid'}`, [ - h('.flex-grow', formatTimeRange(timeRangeFilterModel.normalized)), + h('.flex-grow', formatTimeRange( + timeRangeFilterModel.normalized, + { + formatTimestamp: (timestamp) => { + const { date, time } = getLocaleDateAndTime(timestamp); + return h('.flex-column.g1', [h('.f6', date), h('', time)]); + }, + formatText: (content) => h('.gray-darker', content), + formatParts: (parts) => h('.flex-row.items-center.text-center.g3', parts), + }, + )), h('.dropdown-trigger-symbol', ''), ], ), @@ -71,5 +122,32 @@ export const timeRangeFilter = (timeRangeFilterModel) => dropdown( dateTimeInput(timeRangeFilterModel.toTimeInputModel), ]), ]), + h( + '.flex-column.dropdown-options.text-center', + { style: { 'border-left': '1px solid var(--color-gray)' } }, + [ + yearsOptions({ onChange: (period, label) => timeRangeFilterModel.setValue(period, label) }), + monthsOptions({ onChange: (period, label) => timeRangeFilterModel.setValue(period, label) }), + h( + '.dropdown-option.flex-row.g2.items-center.ph3.pv2', + [ + 'Week ', + h('input.form-control', { + type: 'number', + step: 1, + min: 1, + max: getWeeksCount(), + onchange: (e) => timeRangeFilterModel.setValue( + { + from: getStartOfWeek(e.target.value).getTime(), + to: getStartOfWeek(parseInt(e.target.value, 10) + 1).getTime(), + }, + `Week ${e.target.value}`, + ), + }), + ], + ), + ], + ), ]), ); diff --git a/lib/public/components/common/formatting/formatTimeRange.js b/lib/public/components/common/formatting/formatTimeRange.js new file mode 100644 index 0000000000..af8b483683 --- /dev/null +++ b/lib/public/components/common/formatting/formatTimeRange.js @@ -0,0 +1,47 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { formatTimestamp as defaultFormatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; + +/** + * Format the current timestamp range display + * + * @param {Partial} period the current period + * @param {object} [configuration] the formatting configuration + * @param {function} [configuration.formatTimestamp] the function to format the period's timestamps + * @param {function} [configuration.formatText] the function to format the time-range texts, such as `From`, `to`... + * @param {function} [configuration.formatParts] the function to format all the formatted timeRange parts, which are the timestamps and the + * keywords, for example [`From`, `2023-01-01`] + * @return {Component} the resulting component + */ +export const formatTimeRange = ({ from, to }, configuration) => { + const { + formatTimestamp = (timestamp) => defaultFormatTimestamp(timestamp, true), + formatText = (content) => content, + formatParts = (parts) => parts.join(' '), + } = configuration || {}; + + let parts = []; + + if (from === undefined && to === undefined) { + parts = '-'; + } else if (from === undefined) { + parts = [formatText('Before'), formatTimestamp(to)]; + } else if (to === undefined) { + parts = [formatText('After'), formatTimestamp(from)]; + } else { + parts = [formatText('From'), formatTimestamp(from), formatText('to'), formatTimestamp(to)]; + } + + return formatParts(parts); +}; diff --git a/lib/public/utilities/dateUtils.js b/lib/public/utilities/dateUtils.js index 97d63abc37..8328545fcf 100644 --- a/lib/public/utilities/dateUtils.js +++ b/lib/public/utilities/dateUtils.js @@ -16,6 +16,7 @@ const DATE_STYLE = 'short'; const TIME_STYLE = 'medium'; export const MILLISECONDS_IN_ONE_DAY = 24 * 60 * 60 * 1000; +const MILLISECONDS_IN_ONE_WEEK = 7 * MILLISECONDS_IN_ONE_DAY; /** * Returns the english-formatted date and time components of a given timestamp @@ -33,3 +34,135 @@ export const getLocaleDateAndTime = (timestamp, options) => { time: dateObject.toLocaleTimeString(LOCALE_DATE_FORMAT, { timeStyle: TIME_STYLE, ...options }), }; }; + +/** + * Returns the date corresponding to the start of the given day (if null, start of today) + * + * @param {number} [day] the day for which the start date should be returned (between 1 and 31) + * @param {number} [month] the month for which the day's start date should be returned (between 1 and 12, default to current month) + * @param {number} [year] the year for which the day's start date should be returned (default to current year) + * @return {Date} date representing the start of the given day + */ +export const getStartOfDay = (day, month, year) => { + const now = new Date(); + if (year === undefined) { + year = now.getFullYear(); + } + + if (month === undefined) { + month = now.getMonth() + 1; + } + + if (day === undefined) { + day = now.getDate(); + } + + const startOfMonth = new Date(now.getTime()); + startOfMonth.setUTCHours(0, 0, 0, 0); + startOfMonth.setUTCFullYear(year, month - 1, day); + + return startOfMonth; +}; + +/** + * Returns the date corresponding to the start of the given month (if null, start of the current month) + * + * @param {number} [month] the month for which the start date should be returned (between 1 and 12) + * @param {number} [year] the year for which the month's start date should be returned (default to current year) + * @return {Date} date representing the start of the given month + */ +export const getStartOfMonth = (month, year) => getStartOfDay(1, month, year); + +/** + * Return the date corresponding to the start of the given year (if null, start of the current year) + * + * @param {number} [year] the year for which the start date should be returned + * @return {Date} date representing the start of the given year + */ +export const getStartOfYear = (year) => getStartOfMonth(1, year); + +/** + * Returns the amount of weeks in the given year + * + * @param {number} [year] the year from which weeks count must be returned + * @return {number} the amount of weeks + */ +export const getWeeksCount = (year) => { + const now = new Date(); + if (year === undefined) { + year = now.getFullYear(); + } + + // A year can have either 52 or 53 weeks + const startOf53rdWeek = getStartOfWeek(53, year); + return startOf53rdWeek.getUTCFullYear() === year ? 53 : 52; +}; + +/** + * Return the date corresponding to the start of the given year's week (if null, uses the current week and year) + * + * @param {number} [week] the week for which the start date should be returned + * @param {number} [year] optionally the year from which week's starting date must be computed + * @return {Date} date representing the start of the given week + */ +export const getStartOfWeek = (week, year) => { + const now = new Date(); + if (year === undefined) { + year = now.getFullYear(); + } + + const startOfYear = getStartOfYear(year); + // 0 is Sunday, but we want it to be Monday, so cycle every indices by -1 (add 7 before removing 1 then modulo 7 to avoid negatives) + const weekDay = (startOfYear.getDay() + 6) % 7; + + /* + * Week 1 of the year is the first week that includes the **first Thursday of the year**. + * + * In order to find the week in question, we first try to find the first Thursday. To do so, we first look at the week that includes the + * first day of the week: + * - If the thursday is before the first day of the year, first week is the next one + * - else, it is the first week of the year + * + * We know that the Thursday is day 3. To get its offset compared to the first day of the year, we simply subtract the indices. For example: + * - If year start on Monday, Thursday is (3 - 0) => 3 days after (offset 3) + * - If year start on Thursday, Thursday is (0 - 0) => the same day (offset 0) + * - If year start on Saturday, Thursday is (3 - 5) => 2 days before (offset -2) + * + * It appears that if the result is negative, the first week will be the one that includes the next thursday. Using modulo calculation, we + * can consider that the Thursday we are looking for is `(3 - weekDay + 7) % 7` days after the first day of the year (negative value will be + * incremented by 7, so next week) + * + * In the end what we want is the monday that precedes this Thursday, so we take 3 days before + */ + const firstThursdayOfYearOffset = (10 - weekDay) % 7; + const startOfFirstWeek = new Date(startOfYear.getTime() + (firstThursdayOfYearOffset - 3) * MILLISECONDS_IN_ONE_DAY); + const startOfFirstWeekTimestamp = startOfFirstWeek.getTime(); + + if (week === undefined) { + const startOfTodayTimestamp = getStartOfDay().getTime(); + + // We are in the end of last year's last week, that overlap with the current year + if (startOfFirstWeekTimestamp > startOfTodayTimestamp) { + return new Date(startOfFirstWeekTimestamp - MILLISECONDS_IN_ONE_WEEK); + } + + week = Math.floor(startOfTodayTimestamp - startOfFirstWeekTimestamp) + 1; + } + + return new Date(startOfFirstWeekTimestamp + (week - 1) * MILLISECONDS_IN_ONE_WEEK); +}; + +export const MONTH_NAMES = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', +]; diff --git a/lib/public/views/LhcFills/Overview/index.js b/lib/public/views/LhcFills/Overview/index.js index 8ac25bb2b1..d33b7d9a61 100644 --- a/lib/public/views/LhcFills/Overview/index.js +++ b/lib/public/views/LhcFills/Overview/index.js @@ -48,7 +48,7 @@ const showLhcFillsTable = (lhcFillsOverviewModel) => { return [ h( '.flex-row.header-container.pv2', - [frontLink(h('button.btn.btn-primary', '2023 Statistics'), 'statistics')], + [frontLink(h('button.btn.btn-primary', 'Statistics'), 'statistics')], ), h('.w-100.flex-column', [ table(lhcFillsOverviewModel.lhcFills, lhcFillsActiveColumns, { classes: '.table-sm' }), diff --git a/lib/public/views/Statistics/StatisticsPage.js b/lib/public/views/Statistics/StatisticsPage.js index 328b5039b3..344df105c6 100644 --- a/lib/public/views/Statistics/StatisticsPage.js +++ b/lib/public/views/Statistics/StatisticsPage.js @@ -24,6 +24,7 @@ import { ChartDarkColors } from './chartColors.js'; import { tagOccurrencesBarChartComponent } from './charts/tagOccurrencesBarChartComponent.js'; import { timeRangeFilter } from '../../components/Filters/common/filters/timeRangeFilter.js'; import { eorReasonOccurrencesBarChartComponent } from './charts/eorReasonOccurrencesBarChartComponent.js'; +import { formatTimeRange } from '../../components/common/formatting/formatTimeRange.js'; /** * Render the statistics page @@ -32,107 +33,114 @@ import { eorReasonOccurrencesBarChartComponent } from './charts/eorReasonOccurre * @return {vnode} the resulting component * @constructor */ -export const StatisticsPage = ({ statisticsModel }) => h('.flex-grow.flex-column', [ - h('.flex-row.g3.items-center', [ - h('h1', 'Statistics'), - timeRangeFilter(statisticsModel.timeRangeFilterModel), - ]), - tabbedPanelComponent( - statisticsModel.tabbedPanelModel, - { - [STATISTICS_PANELS_KEYS.LHC_FILL_EFFICIENCY]: 'Efficiency', - [STATISTICS_PANELS_KEYS.WEEKLY_FILE_SIZE]: 'Weekly file size', - [STATISTICS_PANELS_KEYS.MEAN_RUN_DURATION]: 'Mean run duration', - [STATISTICS_PANELS_KEYS.TIME_BETWEEN_RUNS_DISTRIBUTION]: 'Time between runs', - [STATISTICS_PANELS_KEYS.EFFICIENCY_PER_DETECTOR]: 'Detector efficiency', - [STATISTICS_PANELS_KEYS.LOG_TAG_OCCURRENCES]: 'Tag occurrences in logs', - [STATISTICS_PANELS_KEYS.EOR_REASON_OCCURRENCES]: 'End of run reason occurrences', - }, - { - [STATISTICS_PANELS_KEYS.LHC_FILL_EFFICIENCY]: (remoteData) => remoteDataDisplay(remoteData, { - Success: (fillStatistics) => [ - h('h3', 'Efficiency - 2023'), - h( - '.flex-grow.chart-box', - efficiencyChartComponent(fillStatistics, () => statisticsModel.notify()), - ), - ], - }), - [STATISTICS_PANELS_KEYS.WEEKLY_FILE_SIZE]: (remoteData) => remoteDataDisplay(remoteData, { - Success: (weeklyDataSize) => [ - h('h3', 'Weekly data size - 2023'), - h( - '.flex-grow.chart-box', - weeklyDataSizeChartComponent(weeklyDataSize, () => statisticsModel.notify()), - ), - ], - }), - [STATISTICS_PANELS_KEYS.MEAN_RUN_DURATION]: (remoteData) => remoteDataDisplay(remoteData, { - Success: (fillStatistics) => [ - h('h3', 'Mean run duration - 2023'), - h( - '.flex-grow.chart-box', - meanRunDurationChartComponent(fillStatistics, () => statisticsModel.notify()), - ), - ], - }), - [STATISTICS_PANELS_KEYS.TIME_BETWEEN_RUNS_DISTRIBUTION]: (remoteData) => remoteDataDisplay(remoteData, { - Success: (histogram) => [ - h('h3', 'Time between runs - 2023'), - h( - '.flex-grow.chart-box', - timeBetweenRunsHistogramComponent(histogram), - ), - ], - }), - [STATISTICS_PANELS_KEYS.EFFICIENCY_PER_DETECTOR]: (panelModel) => { - const detectorsColors = ChartDarkColors; - return remoteDataDisplay(panelModel.data, { - Success: (efficiencyPerDetectors) => [ - h('h3', 'Efficiency per detector - 2023'), - h('.flex-row.g2', panelModel.detectors.map((detector, i) => switchInput( - panelModel.getDetectorVisibility(detector), - (visibility) => panelModel.setDetectorVisibility(detector, visibility), - { key: detector, labelAfter: detector, color: detectorsColors[i] }, - ))), - h('.flex-row.flex-grow.g5', [ - h( - '.flex-grow.chart-box', - detectorsEfficienciesComponent( - efficiencyPerDetectors, - false, - panelModel.activeDetectors, - detectorsColors.filter((_, i) => panelModel.getDetectorVisibility(panelModel.detectors[i])), - () => statisticsModel.notify(), +export const StatisticsPage = ({ statisticsModel }) => { + let { periodLabel } = statisticsModel.timeRangeFilterModel; + if (periodLabel === null) { + periodLabel = formatTimeRange(statisticsModel.timeRangeFilterModel.normalized); + } + + return h('.flex-grow.flex-column', [ + h('.flex-row.g3.items-center', [ + h('h1', 'Statistics'), + timeRangeFilter(statisticsModel.timeRangeFilterModel), + ]), + tabbedPanelComponent( + statisticsModel.tabbedPanelModel, + { + [STATISTICS_PANELS_KEYS.LHC_FILL_EFFICIENCY]: 'Efficiency', + [STATISTICS_PANELS_KEYS.WEEKLY_FILE_SIZE]: 'Weekly file size', + [STATISTICS_PANELS_KEYS.MEAN_RUN_DURATION]: 'Mean run duration', + [STATISTICS_PANELS_KEYS.TIME_BETWEEN_RUNS_DISTRIBUTION]: 'Time between runs', + [STATISTICS_PANELS_KEYS.EFFICIENCY_PER_DETECTOR]: 'Detector efficiency', + [STATISTICS_PANELS_KEYS.LOG_TAG_OCCURRENCES]: 'Tag occurrences in logs', + [STATISTICS_PANELS_KEYS.EOR_REASON_OCCURRENCES]: 'End of run reason occurrences', + }, + { + [STATISTICS_PANELS_KEYS.LHC_FILL_EFFICIENCY]: (remoteData) => remoteDataDisplay(remoteData, { + Success: (fillStatistics) => [ + h('h3', `Efficiency - ${periodLabel}`), + h( + '.flex-grow.chart-box', + efficiencyChartComponent(fillStatistics, () => statisticsModel.notify()), + ), + ], + }), + [STATISTICS_PANELS_KEYS.WEEKLY_FILE_SIZE]: (remoteData) => remoteDataDisplay(remoteData, { + Success: (weeklyDataSize) => [ + h('h3', `Weekly data size - ${periodLabel}`), + h( + '.flex-grow.chart-box', + weeklyDataSizeChartComponent(weeklyDataSize, () => statisticsModel.notify()), + ), + ], + }), + [STATISTICS_PANELS_KEYS.MEAN_RUN_DURATION]: (remoteData) => remoteDataDisplay(remoteData, { + Success: (fillStatistics) => [ + h('h3', `Mean run duration - ${periodLabel}`), + h( + '.flex-grow.chart-box', + meanRunDurationChartComponent(fillStatistics, () => statisticsModel.notify()), + ), + ], + }), + [STATISTICS_PANELS_KEYS.TIME_BETWEEN_RUNS_DISTRIBUTION]: (remoteData) => remoteDataDisplay(remoteData, { + Success: (histogram) => [ + h('h3', `Time between runs - ${periodLabel}`), + h( + '.flex-grow.chart-box', + timeBetweenRunsHistogramComponent(histogram), + ), + ], + }), + [STATISTICS_PANELS_KEYS.EFFICIENCY_PER_DETECTOR]: (panelModel) => { + const detectorsColors = ChartDarkColors; + return remoteDataDisplay(panelModel.data, { + Success: (efficiencyPerDetectors) => [ + h('h3', `Efficiency per detector - ${periodLabel}`), + h('.flex-row.g2', panelModel.detectors.map((detector, i) => switchInput( + panelModel.getDetectorVisibility(detector), + (visibility) => panelModel.setDetectorVisibility(detector, visibility), + { key: detector, labelAfter: detector, color: detectorsColors[i] }, + ))), + h('.flex-row.flex-grow.g5', [ + h( + '.flex-grow.chart-box', + detectorsEfficienciesComponent( + efficiencyPerDetectors, + false, + panelModel.activeDetectors, + detectorsColors.filter((_, i) => panelModel.getDetectorVisibility(panelModel.detectors[i])), + () => statisticsModel.notify(), + ), ), - ), - h( - '.flex-grow.chart-box', - detectorsEfficienciesComponent( - efficiencyPerDetectors, - true, - panelModel.activeDetectors, - detectorsColors.filter((_, i) => panelModel.getDetectorVisibility(panelModel.detectors[i])), - () => statisticsModel.notify(), + h( + '.flex-grow.chart-box', + detectorsEfficienciesComponent( + efficiencyPerDetectors, + true, + panelModel.activeDetectors, + detectorsColors.filter((_, i) => panelModel.getDetectorVisibility(panelModel.detectors[i])), + () => statisticsModel.notify(), + ), ), - ), - ]), + ]), + ], + }); + }, + [STATISTICS_PANELS_KEYS.LOG_TAG_OCCURRENCES]: (remoteData) => remoteDataDisplay(remoteData, { + Success: (tagOccurrences) => [ + h('h3', `Tag occurrences in logs - ${periodLabel}`), + h('.flex-grow.chart-box', tagOccurrencesBarChartComponent(tagOccurrences)), + ], + }), + [STATISTICS_PANELS_KEYS.EOR_REASON_OCCURRENCES]: (remoteData) => remoteDataDisplay(remoteData, { + Success: (eorReasonOccurrences) => [ + h('h3', `End of runs reason occurrences - ${periodLabel}`), + h('.flex-grow.chart-box', eorReasonOccurrencesBarChartComponent(eorReasonOccurrences)), ], - }); + }), }, - [STATISTICS_PANELS_KEYS.LOG_TAG_OCCURRENCES]: (remoteData) => remoteDataDisplay(remoteData, { - Success: (tagOccurrences) => [ - h('h3', 'Tag occurrences in logs - 2023'), - h('.flex-grow.chart-box', tagOccurrencesBarChartComponent(tagOccurrences)), - ], - }), - [STATISTICS_PANELS_KEYS.EOR_REASON_OCCURRENCES]: (remoteData) => remoteDataDisplay(remoteData, { - Success: (eorReasonOccurrences) => [ - h('h3', 'End of runs reason occurrences - 2023'), - h('.flex-grow.chart-box', eorReasonOccurrencesBarChartComponent(eorReasonOccurrences)), - ], - }), - }, - { panelClass: ['p2', 'g3', 'flex-column', 'flex-grow'] }, - ), -]); + { panelClass: ['p2', 'g3', 'flex-column', 'flex-grow'] }, + ), + ]); +}; diff --git a/lib/public/views/Statistics/StatisticsPageModel.js b/lib/public/views/Statistics/StatisticsPageModel.js index 07a3634501..ffe2e8f765 100644 --- a/lib/public/views/Statistics/StatisticsPageModel.js +++ b/lib/public/views/Statistics/StatisticsPageModel.js @@ -15,6 +15,7 @@ import { TabbedPanelModel } from '../../components/TabbedPanel/TabbedPanelModel. import { buildUrl } from '../../utilities/fetch/buildUrl.js'; import { RemoteDataFetcher } from '../../utilities/fetch/RemoteDataFetcher.js'; import { TimeRangeFilterModel } from '../../components/Filters/common/filters/TimeRangeFilterModel.js'; +import { getStartOfDay, getStartOfYear, MILLISECONDS_IN_ONE_DAY } from '../../utilities/dateUtils.js'; export const STATISTICS_PANELS_KEYS = { LHC_FILL_EFFICIENCY: 'lhc-fill-efficiency', @@ -35,20 +36,12 @@ export class StatisticsPageModel extends Observable { */ constructor() { super(); - const now = new Date(); - const todayMidnight = new Date(now.getTime()); - todayMidnight.setHours(0, 0, 0, 0); - const startOfYearMidnight = new Date(todayMidnight.getTime()); - startOfYearMidnight.setDate(1); - startOfYearMidnight.setMonth(0); - const tomorrowMidnight = new Date(todayMidnight.getTime() + 24 * 3600 * 1000); - const timezoneOffset = now.getTimezoneOffset(); - - this._timeRangeFilterModel = new TimeRangeFilterModel({ - from: new Date(startOfYearMidnight.getTime() - timezoneOffset * 60 * 1000).getTime(), - to: new Date(tomorrowMidnight.getTime() - timezoneOffset * 60 * 1000).getTime(), - }, { required: true }); + const period = { + from: getStartOfYear().getTime(), + to: new Date(getStartOfDay().getTime() + MILLISECONDS_IN_ONE_DAY).getTime(), + }; + this._timeRangeFilterModel = new TimeRangeFilterModel(period, `${new Date(period.from).getUTCFullYear()}`, { required: true }); this._timeRangeFilterModel.visualChange$.bubbleTo(this); this._tabbedPanelModel = new StatisticsTabbedPanelModel(); @@ -163,6 +156,15 @@ class StatisticsTabbedPanelModel extends TabbedPanelModel { } } + /** + * If the currently selected period correspond to a specific period (for ex current month), returns its label + * + * @return {string} the current period label if it applies + */ + get periodLabel() { + return null; + } + // eslint-disable-next-line valid-jsdoc /** * @inheritDoc