Skip to content

Commit

Permalink
[O2B-1033] Add detector efficiency graphs (#1204)
Browse files Browse the repository at this point in the history
* [O2B-1033] Add detector efficiency graphs

* Remove show all & hide all buttons

* Use g5 instead of g4
  • Loading branch information
martinboulais committed Oct 27, 2023
1 parent 1edd5de commit 21fd67e
Show file tree
Hide file tree
Showing 8 changed files with 576 additions and 30 deletions.
4 changes: 4 additions & 0 deletions lib/public/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ html, body {
align-self: end;
}

.g5 {
gap: var(--space-xl)
}

/* Float */
.float-right { float: right }

Expand Down
54 changes: 54 additions & 0 deletions lib/public/components/common/form/switchInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* @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.
*/

/**
* @callback switchInputOnChangeCallback
* @param {boolean} value the new switch value
* @return {void}
*/

import { h } from '/js/src/index.js';

/**
* Render a switch input (styled checkbox)
*
* @param {boolean} value the current value of the switch
* @param {switchInputOnChangeCallback} onChange function called with the new value as argument when the value change
* @param {object} [options] eventual options for the input
* @param {string|number} [options.key] key to apply to the switch component
* @param {Component} [options.labelBefore] the label to display before the input
* @param {Component} [options.labelAfter] the label to display after the input
* @param {Component} [options.color] the background color of the slider when active
* @return {Component} the switch component
*/
export const switchInput = (value, onChange, options) => {
const { key, labelAfter, labelBefore, color } = options || {};
const attributes = { ...key ? { key } : {} };

return h(
'label.flex-row.g1.items-center',
attributes,
[
labelBefore,
h('.switch', [
h('input', {
onchange: (e) => onChange(e.target.checked),
type: 'checkbox',
checked: value,
}),
h('span.slider.round', { ...color && value ? { style: `background-color: ${color}` } : {} }),
]),
labelAfter,
],
);
};
68 changes: 68 additions & 0 deletions lib/public/utilities/fetch/RemoteDataFetcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* @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 { Observable, RemoteData } from '/js/src/index.js';
import { getRemoteData } from './getRemoteData.js';

/**
* Service class providing observable data fetching process
*/
export class RemoteDataFetcher extends Observable {
/**
* Constructor
*/
constructor() {
super();
this._data = RemoteData.notAsked();
this._abortController = null;
}

/**
* Fetch the given endpoint to fill current data
*
* @template T
* @param {string} endpoint the endpoint to fetch
* @return {Promise<T>} resolves once the data fetching has ended
*/
async fetch(endpoint) {
this._data = RemoteData.loading();
this.notify();

const abortController = new AbortController();
try {
if (this._abortController) {
this._abortController.abort();
}
this._abortController = abortController;
const { data } = await getRemoteData(endpoint, { signal: this._abortController.signal });
this._data = RemoteData.success(data);
this.notify();
return data;
} catch (error) {
// Use local variable because the class member (this._abortController) may already have been override in another call
if (!abortController.signal.aborted) {
this._data = RemoteData.failure(error);
}
this.notify();
throw error;
}
}

/**
* Return the current data
*
* @return {RemoteData} the current remote data fetched by the fetcher
*/
get data() {
return this._data;
}
}
127 changes: 127 additions & 0 deletions lib/public/utilities/fetch/buildUrl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* @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.
*/

/**
* @typedef {string|number|null|boolean} QueryParameterValue
*/

/**
* Build a URL from a base URL (that may already have query parameters) and a list of query parameters
*
* @param {string} baseURL the base URL to which parameters should be added
* @param {object} parameters the query parameters
* @return {string} URL the built URL
*/
export const buildUrl = (baseURL, parameters) => {
if (!parameters) {
parameters = {};
}
const [url, existingParameters] = baseURL.split('?');

/**
* Build a parameter object or array from a parameters keys path
*
* For example, a parameter `key1[key2][]=value` translates to keys path ['key1', 'key2', ''] and will lead to {key1: {key2: [value]}}
*
* @param {object|array} parentParameter the parameter's object or array up to the current key
* @param {array} nestedKeys the keys path to build from the current point
* @param {string} value the value of the parameter represented by the key path
* @param {string|null} absoluteCurrentKey the full key currently building, used to display useful error message
* @return {void}
*/
const buildParameterFromNestedKeys = (parentParameter, nestedKeys, value, absoluteCurrentKey) => {
const currentKey = nestedKeys.shift();
const absoluteParentKey = absoluteCurrentKey;
absoluteCurrentKey = absoluteCurrentKey ? `${absoluteCurrentKey}[${currentKey}]` : currentKey;

if (currentKey === '') {
// Parameter must be an array and the value is a new item in that array
if (!Array.isArray(parentParameter)) {
throw new Error(`Existing parameter at key <${absoluteParentKey}> is an array`);
}

parentParameter.push(value);
} else if (currentKey) {
// Parameter must be an object and the value is a property in that array
if (Array.isArray(parentParameter) || typeof parentParameter !== 'object' || parentParameter === null) {
throw new Error(`Existing parameter at key <${absoluteParentKey}> expects nested values`);
}

if (nestedKeys.length > 0) {
// We still have nested keys to fill
if (!(currentKey in parentParameter)) {
parentParameter[currentKey] = nestedKeys[0] === '' ? [] : {};
}
buildParameterFromNestedKeys(parentParameter[currentKey], nestedKeys, value, absoluteCurrentKey);
} else {
if (Array.isArray(parentParameter[currentKey])) {
throw new Error(`Existing parameter at key <${currentKey}> is not an array`);
} else if (typeof parentParameter[currentKey] === 'object' && parentParameter[currentKey] !== null) {
throw new Error(`Existing parameter at key <${currentKey}> is not nested`);
}
parentParameter[currentKey] = value;
}
}
};

if (existingParameters) {
for (const formattedParameter of existingParameters.split('&')) {
const [key, value] = formattedParameter.split('=');
const [firstKey, ...dirtyKeys] = key.split('[');
const nestedKeys = [firstKey, ...dirtyKeys.map((key) => key.slice(0, -1))];

buildParameterFromNestedKeys(parameters, nestedKeys, value, null);
}
}

const serializedQueryParameters = [];

if (Object.keys(parameters).length === 0) {
return url;
}

/**
* Stringify a query parameter to be used in a URL and push it in the serialized query parameters list
*
* @param {string} key the parameter's key
* @param {QueryParameterValue} value the parameter's value
* @return {void}
*/
const formatAndPushQueryParameter = (key, value) => {
if (value === undefined) {
return;
}

if (Array.isArray(value)) {
for (const subValue of value) {
formatAndPushQueryParameter(`${key}[]`, subValue);
}
return;
}

if (typeof value === 'object' && value !== null) {
for (const [subKey, subValue] of Object.entries(value)) {
formatAndPushQueryParameter(`${key}[${subKey}]`, subValue);
}
return;
}

serializedQueryParameters.push(`${key}=${value}`);
};

for (const [key, parameter] of Object.entries(parameters)) {
formatAndPushQueryParameter(key, parameter);
}

return `${url}?${serializedQueryParameters.join('&')}`;
};
41 changes: 40 additions & 1 deletion lib/public/views/Statistics/StatisticsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import { remoteDataDisplay } from '../../components/common/remoteDataDisplay.js'
import { weeklyDataSizeChartComponent } from './charts/weeklyDataSizeChartComponent.js';
import { meanRunDurationChartComponent } from './charts/meanRunDurationChartComponent.js';
import { timeBetweenRunsHistogramComponent } from './charts/timeBetweenRunsHistogramComponent.js';
import { detectorsEfficienciesComponent } from './charts/detectorsEfficienciesComponent.js';
import { switchInput } from '../../components/common/form/switchInput.js';
import { ChartDarkColors } from './chartColors.js';

/**
* Render the statistics page
Expand All @@ -35,6 +38,7 @@ export const StatisticsPage = ({ statisticsModel }) => h('.flex-grow.flex-column
[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.LHC_FILL_EFFICIENCY]: (remoteData) => remoteDataDisplay(remoteData, {
Expand Down Expand Up @@ -73,7 +77,42 @@ export const StatisticsPage = ({ statisticsModel }) => h('.flex-grow.flex-column
),
],
}),
[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(),
),
),
h(
'.flex-grow.chart-box',
detectorsEfficienciesComponent(
efficiencyPerDetectors,
true,
panelModel.activeDetectors,
detectorsColors.filter((_, i) => panelModel.getDetectorVisibility(panelModel.detectors[i])),
() => statisticsModel.notify(),
),
),
]),
],
});
},
},
{ panelClass: ['p2', 'flex-column', 'flex-grow'] },
{ panelClass: ['p2', 'g3', 'flex-column', 'flex-grow'] },
),
]);
Loading

0 comments on commit 21fd67e

Please sign in to comment.