feat(4D): Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode (#3664)
Co-authored-by: Alireza <ar.sedghi@gmail.com> Co-authored-by: Neil <neil.a.macphee@gmail.com>
No files matched your search
@@ -110,7 +110,8 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
//Click on button and verify if icon is active on toolbar
|
||||
cy.addLengthMeasurement();
|
||||
cy.get('[data-cy="viewport-notification"]').as('notif').should('exist');
|
||||
cy.get('[data-cy="viewport-notification"]').as('notif').should('be.visible');
|
||||
// cy.get('[data-cy="viewport-notification"]').as('notif').should('be.visible');
|
||||
|
||||
cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').as('yesBtn').click();
|
||||
|
||||
//Verify the measurement exists in the table
|
||||
|
||||
@@ -122,6 +122,8 @@ describe('OHIF Study List', function () {
|
||||
'[data-cy="mode-basic-test-1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1"]'
|
||||
).click();
|
||||
cy.get('[data-cy="return-to-work-list"]').click();
|
||||
|
||||
cy.wait(1000);
|
||||
cy.get('@searchResult2').should($list => {
|
||||
expect($list.length).to.be.eq(1);
|
||||
expect($list).to.contain('PETCT');
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"dev": "cross-env NODE_ENV=development webpack serve --config .webpack/webpack.pwa.js",
|
||||
"dev:no:cache": "cross-env NODE_ENV=development webpack serve --no-cache --config .webpack/webpack.pwa.js",
|
||||
"dev:orthanc": "cross-env NODE_ENV=development PROXY_TARGET=/dicom-web PROXY_DOMAIN=http://localhost:8042 APP_CONFIG=config/docker_nginx-orthanc.js webpack serve --config .webpack/webpack.pwa.js",
|
||||
"dev:orthanc:no:cache": "cross-env NODE_ENV=development PROXY_TARGET=/dicom-web PROXY_DOMAIN=http://localhost:8042 APP_CONFIG=config/docker_nginx-orthanc.js webpack serve --no-cache --config .webpack/webpack.pwa.js",
|
||||
"dev:dcm4chee": "cross-env NODE_ENV=development APP_CONFIG=config/local_dcm4chee.js webpack serve --config .webpack/webpack.pwa.js",
|
||||
"dev:static": "cross-env NODE_ENV=development APP_CONFIG=config/local_static.js webpack serve --config .webpack/webpack.pwa.js",
|
||||
"dev:viewer": "yarn run dev",
|
||||
@@ -53,7 +54,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.68.1",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.70.0",
|
||||
"@emotion/serialize": "^1.1.3",
|
||||
"@ohif/core": "3.8.0-beta.73",
|
||||
"@ohif/extension-cornerstone": "3.8.0-beta.73",
|
||||
@@ -98,7 +99,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||
"@percy/cypress": "^3.1.1",
|
||||
"cypress": "13.6.2",
|
||||
"cypress": "13.7.2",
|
||||
"cypress-file-upload": "^3.5.3",
|
||||
"glob": "^8.0.3",
|
||||
"identity-obj-proxy": "3.0.x",
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
"default": false,
|
||||
"version": "3.0.0"
|
||||
},
|
||||
{
|
||||
"packageName": "@ohif/extension-cornerstone-dynamic-volume",
|
||||
"default": false,
|
||||
"version": "3.0.0"
|
||||
},
|
||||
{
|
||||
"packageName": "@ohif/extension-dicom-microscopy",
|
||||
"default": false,
|
||||
@@ -66,6 +71,9 @@
|
||||
{
|
||||
"packageName": "@ohif/mode-microscopy"
|
||||
},
|
||||
{
|
||||
"packageName": "@ohif/mode-preclinical-4d"
|
||||
},
|
||||
{
|
||||
"packageName": "@ohif/mode-test",
|
||||
"default": false,
|
||||
|
||||
@@ -29,6 +29,7 @@ window.config = {
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
useBulkDataURI: false,
|
||||
supportsFuzzyMatching: true,
|
||||
supportsWildcard: true,
|
||||
dicomUploadEnabled: true,
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
errorHandler,
|
||||
CustomizationService,
|
||||
PanelService,
|
||||
WorkflowStepsService,
|
||||
// utils,
|
||||
} from '@ohif/core';
|
||||
|
||||
@@ -68,6 +69,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
|
||||
CineService.REGISTRATION,
|
||||
UserAuthenticationService.REGISTRATION,
|
||||
PanelService.REGISTRATION,
|
||||
WorkflowStepsService.REGISTRATION,
|
||||
StateSyncService.REGISTRATION,
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState, useRef, useContext, createContext } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useParams, useLocation, useNavigate } from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ServicesManager, utils } from '@ohif/core';
|
||||
@@ -60,7 +60,7 @@ export default function ModeRoute({
|
||||
locationRef.current = location;
|
||||
}
|
||||
|
||||
const { displaySetService, hangingProtocolService, userAuthenticationService } = (
|
||||
const { displaySetService, panelService, hangingProtocolService, userAuthenticationService } = (
|
||||
servicesManager as ServicesManager
|
||||
).services;
|
||||
|
||||
@@ -141,7 +141,17 @@ export default function ModeRoute({
|
||||
servicesManager,
|
||||
studyInstanceUIDs,
|
||||
});
|
||||
|
||||
if (isMounted.current) {
|
||||
const { leftPanels = [], rightPanels = [], ...layoutProps } = layoutData.props;
|
||||
|
||||
panelService.reset();
|
||||
panelService.addPanels(panelService.PanelPosition.Left, leftPanels);
|
||||
panelService.addPanels(panelService.PanelPosition.Right, rightPanels);
|
||||
|
||||
// layoutProps contains all props but leftPanels and rightPanels
|
||||
layoutData.props = layoutProps;
|
||||
|
||||
layoutTemplateData.current = layoutData;
|
||||
setRefresh(!refresh);
|
||||
}
|
||||
@@ -244,8 +254,10 @@ export default function ModeRoute({
|
||||
}
|
||||
}, {}) ?? {};
|
||||
|
||||
let unsubs;
|
||||
|
||||
if (route.init) {
|
||||
return await route.init(
|
||||
unsubs = await route.init(
|
||||
{
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
@@ -273,6 +285,14 @@ export default function ModeRoute({
|
||||
let unsubscriptions;
|
||||
setupRouteInit().then(unsubs => {
|
||||
unsubscriptions = unsubs;
|
||||
|
||||
// Some code may need to run after hanging protocol initialization
|
||||
// (eg: workflowStepsService initialization on 4D mode)
|
||||
mode?.onSetupRouteComplete?.({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -15,7 +15,7 @@ const { sortingCriteria } = utils;
|
||||
* @param props.filters filters from query params to read the data from
|
||||
* @returns array of subscriptions to cancel
|
||||
*/
|
||||
export function defaultRouteInit(
|
||||
export async function defaultRouteInit(
|
||||
{ servicesManager, studyInstanceUIDs, dataSource, filters, appConfig },
|
||||
hangingProtocolId
|
||||
) {
|
||||
@@ -95,7 +95,7 @@ export function defaultRouteInit(
|
||||
});
|
||||
});
|
||||
|
||||
Promise.allSettled(allRetrieves).then(promises => {
|
||||
await Promise.allSettled(allRetrieves).then(async promises => {
|
||||
log.timeEnd(Enums.TimingEnum.STUDY_TO_DISPLAY_SETS);
|
||||
log.time(Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE);
|
||||
log.time(Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES);
|
||||
@@ -120,7 +120,7 @@ export function defaultRouteInit(
|
||||
}
|
||||
});
|
||||
|
||||
Promise.allSettled(allPromises).then(applyHangingProtocol);
|
||||
await Promise.allSettled(allPromises).then(applyHangingProtocol);
|
||||
startRemainingPromises(remainingPromises);
|
||||
applyHangingProtocol();
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.2",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.68.1",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.70.0",
|
||||
"@ohif/ui": "3.8.0-beta.73",
|
||||
"cornerstone-math": "0.1.9",
|
||||
"dicom-parser": "^1.8.21"
|
||||
|
||||
@@ -241,6 +241,12 @@ class MetadataProvider {
|
||||
sopInstanceUID: instance.SOPInstanceUID,
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.PET_IMAGE_MODULE:
|
||||
metadata = {
|
||||
frameReferenceTime: instance.FrameReferenceTime,
|
||||
actualFrameDuration: instance.ActualFrameDuration,
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.PET_ISOTOPE_MODULE:
|
||||
const { RadiopharmaceuticalInformationSequence } = instance;
|
||||
|
||||
@@ -393,6 +399,41 @@ class MetadataProvider {
|
||||
decayCorrection: instance.DecayCorrection,
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.CALIBRATION_MODULE:
|
||||
// map the DICOM tags to the cornerstone tags since cornerstone tags
|
||||
// are camelCase and instance tags are all caps
|
||||
metadata = {
|
||||
sequenceOfUltrasoundRegions: instance.SequenceOfUltrasoundRegions?.map(region => {
|
||||
return {
|
||||
regionSpatialFormat: region.RegionSpatialFormat,
|
||||
regionDataType: region.RegionDataType,
|
||||
regionFlags: region.RegionFlags,
|
||||
regionLocationMinX0: region.RegionLocationMinX0,
|
||||
regionLocationMinY0: region.RegionLocationMinY0,
|
||||
regionLocationMaxX1: region.RegionLocationMaxX1,
|
||||
regionLocationMaxY1: region.RegionLocationMaxY1,
|
||||
referencePixelX0: region.ReferencePixelX0,
|
||||
referencePixelY0: region.ReferencePixelY0,
|
||||
referencePixelPhysicalValueX: region.ReferencePixelPhysicalValueX,
|
||||
referencePixelPhysicalValueY: region.ReferencePixelPhysicalValueY,
|
||||
physicalUnitsXDirection: region.PhysicalUnitsXDirection,
|
||||
physicalUnitsYDirection: region.PhysicalUnitsYDirection,
|
||||
physicalDeltaX: region.PhysicalDeltaX,
|
||||
physicalDeltaY: region.PhysicalDeltaY,
|
||||
};
|
||||
}),
|
||||
};
|
||||
break;
|
||||
|
||||
/**
|
||||
* Below are the tags and not the modules since they are not really
|
||||
* consistent with the modules above
|
||||
*/
|
||||
case 'temporalPositionIdentifier':
|
||||
metadata = {
|
||||
temporalPositionIdentifier: instance.TemporalPositionIdentifier,
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
@@ -539,6 +580,7 @@ const WADO_IMAGE_LOADER_TAGS = {
|
||||
VOI_LUT_MODULE: 'voiLutModule',
|
||||
MODALITY_LUT_MODULE: 'modalityLutModule',
|
||||
SOP_COMMON_MODULE: 'sopCommonModule',
|
||||
PET_IMAGE_MODULE: 'petImageModule',
|
||||
PET_ISOTOPE_MODULE: 'petIsotopeModule',
|
||||
PER_SERIES_MODULE: 'petSeriesModule',
|
||||
OVERLAY_PLANE_MODULE: 'overlayPlaneModule',
|
||||
@@ -549,6 +591,7 @@ const WADO_IMAGE_LOADER_TAGS = {
|
||||
GENERAL_IMAGE_MODULE: 'generalImageModule',
|
||||
GENERAL_STUDY_MODULE: 'generalStudyModule',
|
||||
CINE_MODULE: 'cineModule',
|
||||
CALIBRATION_MODULE: 'calibrationModule',
|
||||
};
|
||||
|
||||
const INSTANCE = 'instance';
|
||||
@@ -48,6 +48,7 @@ export interface Extension {
|
||||
getCustomizationModule?: (p: ExtensionParams) => unknown;
|
||||
getSopClassHandlerModule?: (p: ExtensionParams) => unknown;
|
||||
getToolbarModule?: (p: ExtensionParams) => unknown;
|
||||
getPanelModule?: (p: ExtensionParams) => unknown;
|
||||
onModeEnter?: () => void;
|
||||
onModeExit?: () => void;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as OHIF from './index';
|
||||
|
||||
describe('Top level exports', () => {
|
||||
test('have not changed', () => {
|
||||
test.only('have not changed', () => {
|
||||
const expectedExports = [
|
||||
'MODULE_TYPES',
|
||||
//
|
||||
@@ -46,6 +46,7 @@ describe('Top level exports', () => {
|
||||
'pubSubServiceInterface',
|
||||
'PubSubService',
|
||||
'PanelService',
|
||||
'WorkflowStepsService',
|
||||
'useToolbar',
|
||||
].sort();
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
CustomizationService,
|
||||
StateSyncService,
|
||||
PanelService,
|
||||
WorkflowStepsService,
|
||||
} from './services';
|
||||
|
||||
import { DisplaySetMessage, DisplaySetMessageList } from './services/DisplaySetService';
|
||||
@@ -84,6 +85,7 @@ const OHIF = {
|
||||
PubSubService,
|
||||
PanelService,
|
||||
useToolbar,
|
||||
WorkflowStepsService,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -128,6 +130,7 @@ export {
|
||||
Enums,
|
||||
Types,
|
||||
PanelService,
|
||||
WorkflowStepsService,
|
||||
useToolbar,
|
||||
};
|
||||
|
||||
|
||||
@@ -34,29 +34,46 @@ class CineService extends PubSubService {
|
||||
// reducer state does not get updated right away and if we publish the
|
||||
// event and we use the cineService.getState() it will return the old state
|
||||
setTimeout(() => {
|
||||
this._broadcastEvent(this.EVENTS.CINE_STATE_CHANGED, isCineEnabled);
|
||||
this._broadcastEvent(this.EVENTS.CINE_STATE_CHANGED, { isCineEnabled });
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public playClip(element, playClipOptions) {
|
||||
return this.serviceImplementation._playClip(element, playClipOptions);
|
||||
const res = this.serviceImplementation._playClip(element, playClipOptions);
|
||||
|
||||
this._broadcastEvent(this.EVENTS.CINE_STATE_CHANGED, { isPlaying: true });
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public stopClip(element) {
|
||||
return this.serviceImplementation._stopClip(element);
|
||||
const res = this.serviceImplementation._stopClip(element);
|
||||
|
||||
this._broadcastEvent(this.EVENTS.CINE_STATE_CHANGED, { isPlaying: false });
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public _onModeExit() {
|
||||
this.setIsCineEnabled(false);
|
||||
}
|
||||
|
||||
public getSyncedViewports(viewportId) {
|
||||
return this.serviceImplementation._getSyncedViewports(viewportId);
|
||||
}
|
||||
|
||||
public setServiceImplementation({
|
||||
getState: getStateImplementation,
|
||||
setCine: setCineImplementation,
|
||||
setIsCineEnabled: setIsCineEnabledImplementation,
|
||||
playClip: playClipImplementation,
|
||||
stopClip: stopClipImplementation,
|
||||
getSyncedViewports: getSyncedViewportsImplementation,
|
||||
}) {
|
||||
if (getSyncedViewportsImplementation) {
|
||||
this.serviceImplementation._getSyncedViewports = getSyncedViewportsImplementation;
|
||||
}
|
||||
|
||||
if (getStateImplementation) {
|
||||
this.serviceImplementation._getState = getStateImplementation;
|
||||
}
|
||||
|
||||
@@ -1314,18 +1314,19 @@ export default class HangingProtocolService extends PubSubService {
|
||||
);
|
||||
|
||||
// Use the display set provided instead
|
||||
if (reuseDisplaySetUID) {
|
||||
if (viewportOptions.allowUnmatchedView !== true) {
|
||||
this.validateDisplaySetSelectMatch(viewportDisplaySet, id, reuseDisplaySetUID);
|
||||
}
|
||||
const displaySetInfo: HangingProtocol.DisplaySetInfo = {
|
||||
displaySetInstanceUID: reuseDisplaySetUID,
|
||||
displaySetOptions,
|
||||
};
|
||||
// Todo: find out what is wrong here
|
||||
// if (reuseDisplaySetUID) {
|
||||
// if (viewportOptions.allowUnmatchedView !== true) {
|
||||
// this.validateDisplaySetSelectMatch(viewportDisplaySet, id, reuseDisplaySetUID);
|
||||
// }
|
||||
// const displaySetInfo: HangingProtocol.DisplaySetInfo = {
|
||||
// displaySetInstanceUID: reuseDisplaySetUID,
|
||||
// displaySetOptions,
|
||||
// };
|
||||
|
||||
displaySetsInfo.push(displaySetInfo);
|
||||
return;
|
||||
}
|
||||
// displaySetsInfo.push(displaySetInfo);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Use the display set index to allow getting the "next" match, eg
|
||||
// matching all display sets, and get the matchedDisplaySetsIndex'th item
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { ActivatePanelTriggers } from '../../types';
|
||||
import { Subscription } from '../../types/IPubSub';
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
|
||||
export const EVENTS = {
|
||||
ACTIVATE_PANEL: 'event::panelService:activatePanel',
|
||||
};
|
||||
|
||||
export default class PanelService extends PubSubService {
|
||||
public static REGISTRATION = {
|
||||
name: 'panelService',
|
||||
create: (): PanelService => {
|
||||
return new PanelService();
|
||||
},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super(EVENTS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the panel with the given id. If the forceActive flag is false
|
||||
* then it is up to the component containing the panel whether to activate
|
||||
* it immediately or not. For instance, the panel might not be activated when
|
||||
* the forceActive flag is false in the case where the user might have
|
||||
* activated/displayed and then closed the panel already.
|
||||
* Note that this method simply fires a broadcast event: ActivatePanelEvent.
|
||||
* @param panelId the panel's id
|
||||
* @param forceActive optional flag indicating if the panel should be forced to be activated or not
|
||||
*/
|
||||
activatePanel(panelId: string, forceActive = false): void {
|
||||
this._broadcastEvent(EVENTS.ACTIVATE_PANEL, { panelId, forceActive });
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a mapping of events (activatePanelTriggers.sourceEvents) broadcast by
|
||||
* activatePanelTrigger.sourcePubSubService that
|
||||
* when fired/broadcasted must in turn activate the panel with the given id.
|
||||
* The subscriptions created are returned such that they can be managed and unsubscribed
|
||||
* as appropriate.
|
||||
* @param panelId the id of the panel to activate
|
||||
* @param activatePanelTriggers an array of triggers
|
||||
* @param forceActive optional flag indicating if the panel should be forced to be activated or not
|
||||
* @returns an array of the subscriptions subscribed to
|
||||
*/
|
||||
addActivatePanelTriggers(
|
||||
panelId: string,
|
||||
activatePanelTriggers: ActivatePanelTriggers[],
|
||||
forceActive = false
|
||||
): Subscription[] {
|
||||
return activatePanelTriggers
|
||||
.map(trigger =>
|
||||
trigger.sourceEvents.map(eventName =>
|
||||
trigger.sourcePubSubService.subscribe(eventName, () =>
|
||||
this.activatePanel(panelId, forceActive)
|
||||
)
|
||||
)
|
||||
)
|
||||
.flat();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import React from 'react';
|
||||
import { ActivatePanelTriggers } from '../../types';
|
||||
import { Subscription } from '../../types/IPubSub';
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
import { ExtensionManager } from '../../extensions';
|
||||
|
||||
export const EVENTS = {
|
||||
PANELS_CHANGED: 'event::panelService:panelsChanged',
|
||||
ACTIVATE_PANEL: 'event::panelService:activatePanel',
|
||||
};
|
||||
|
||||
type PanelData = {
|
||||
id: string;
|
||||
iconName: string;
|
||||
iconLabel: string;
|
||||
label: string;
|
||||
name: string;
|
||||
content: unknown;
|
||||
};
|
||||
|
||||
export enum PanelPosition {
|
||||
Left = 'left',
|
||||
Right = 'right',
|
||||
Bottom = 'bottom',
|
||||
}
|
||||
|
||||
export default class PanelService extends PubSubService {
|
||||
private _extensionManager: ExtensionManager;
|
||||
|
||||
public static REGISTRATION = {
|
||||
name: 'panelService',
|
||||
create: ({ extensionManager }): PanelService => {
|
||||
return new PanelService(extensionManager);
|
||||
},
|
||||
};
|
||||
|
||||
private _panelsGroups: Map<PanelPosition, PanelData[]> = new Map();
|
||||
|
||||
constructor(extensionManager: ExtensionManager) {
|
||||
super(EVENTS);
|
||||
this._extensionManager = extensionManager;
|
||||
}
|
||||
|
||||
public get PanelPosition(): typeof PanelPosition {
|
||||
return PanelPosition;
|
||||
}
|
||||
|
||||
private _getPanelComponent(panelId: string) {
|
||||
const entry = this._extensionManager.getModuleEntry(panelId);
|
||||
|
||||
if (!entry) {
|
||||
throw new Error(
|
||||
`${panelId} is not a valid entry for an extension module, please check your configuration or make sure the extension is registered.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!entry?.component) {
|
||||
throw new Error(
|
||||
`No component found from extension ${panelId}. Check the reference string to the extension in your Mode configuration`
|
||||
);
|
||||
}
|
||||
|
||||
const content = entry.component;
|
||||
|
||||
return { entry, content };
|
||||
}
|
||||
|
||||
public getPanelData(panelId): PanelData {
|
||||
let content, entry;
|
||||
if (Array.isArray(panelId)) {
|
||||
const panelsData = panelId.map(id => this._getPanelComponent(id));
|
||||
|
||||
// use the first panel's entry for the combined panel
|
||||
entry = panelsData[0].entry;
|
||||
|
||||
// stack the content of the panels in one react component
|
||||
content = () => (
|
||||
<>
|
||||
{panelsData.map(({ content: PanelContent }) => (
|
||||
<PanelContent key={entry.id} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
({ content, entry } = this._getPanelComponent(panelId));
|
||||
}
|
||||
|
||||
return {
|
||||
id: entry.id,
|
||||
iconName: entry.iconName,
|
||||
iconLabel: entry.iconLabel,
|
||||
label: entry.label,
|
||||
name: entry.name,
|
||||
content,
|
||||
};
|
||||
}
|
||||
|
||||
public addPanel(position: PanelPosition, panelId: string, options): void {
|
||||
let panels = this._panelsGroups.get(position);
|
||||
|
||||
if (!panels) {
|
||||
panels = [];
|
||||
this._panelsGroups.set(position, panels);
|
||||
}
|
||||
|
||||
const panelComponent = this.getPanelData(panelId);
|
||||
|
||||
panels.push(panelComponent);
|
||||
this._broadcastEvent(EVENTS.PANELS_CHANGED, { position, options });
|
||||
}
|
||||
|
||||
public addPanels(position: PanelPosition, panelsIds: string[], options): void {
|
||||
if (!Array.isArray(panelsIds)) {
|
||||
throw new Error('Invalid "panelsIds" array');
|
||||
}
|
||||
|
||||
panelsIds.forEach(panelId => this.addPanel(position, panelId, options));
|
||||
}
|
||||
|
||||
public setPanels(
|
||||
panels: { [key in PanelPosition]: string[] },
|
||||
options: {
|
||||
rightPanelClosed?: boolean;
|
||||
leftPanelClosed?: boolean;
|
||||
}
|
||||
): void {
|
||||
this.reset();
|
||||
|
||||
Object.keys(panels).forEach((position: PanelPosition) => {
|
||||
this.addPanels(position, panels[position], options);
|
||||
});
|
||||
}
|
||||
|
||||
public getPanels(position: PanelPosition): PanelData[] {
|
||||
const panels = this._panelsGroups.get(position) ?? [];
|
||||
|
||||
// Return a new array to preserve the internal state
|
||||
return [...panels];
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
const affectedPositions = Array.from(this._panelsGroups.keys());
|
||||
|
||||
this._panelsGroups.clear();
|
||||
|
||||
affectedPositions.forEach(position =>
|
||||
this._broadcastEvent(EVENTS.PANELS_CHANGED, { position })
|
||||
);
|
||||
}
|
||||
|
||||
public onModeExit(): void {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
/**5
|
||||
* Activates the panel with the given id. If the forceActive flag is false
|
||||
* then it is up to the component containing the panel whether to activate
|
||||
* it immediately or not. For instance, the panel might not be activated when
|
||||
* the forceActive flag is false in the case where the user might have
|
||||
* activated/displayed and then closed the panel already.
|
||||
* Note that this method simply fires a broadcast event: ActivatePanelEvent.
|
||||
* @param panelId the panel's id
|
||||
* @param forceActive optional flag indicating if the panel should be forced to be activated or not
|
||||
*/
|
||||
public activatePanel(panelId: string, forceActive = false): void {
|
||||
this._broadcastEvent(EVENTS.ACTIVATE_PANEL, { panelId, forceActive });
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a mapping of events (activatePanelTriggers.sourceEvents) broadcast by
|
||||
* activatePanelTrigger.sourcePubSubService that
|
||||
* when fired/broadcasted must in turn activate the panel with the given id.
|
||||
* The subscriptions created are returned such that they can be managed and unsubscribed
|
||||
* as appropriate.
|
||||
* @param panelId the id of the panel to activate
|
||||
* @param activatePanelTriggers an array of triggers
|
||||
* @param forceActive optional flag indicating if the panel should be forced to be activated or not
|
||||
* @returns an array of the subscriptions subscribed to
|
||||
*/
|
||||
public addActivatePanelTriggers(
|
||||
panelId: string,
|
||||
activatePanelTriggers: ActivatePanelTriggers[],
|
||||
forceActive = false
|
||||
): Subscription[] {
|
||||
return activatePanelTriggers
|
||||
.map(trigger =>
|
||||
trigger.sourceEvents.map(eventName =>
|
||||
trigger.sourcePubSubService.subscribe(eventName, () =>
|
||||
this.activatePanel(panelId, forceActive)
|
||||
)
|
||||
)
|
||||
)
|
||||
.flat();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export default class ServicesManager {
|
||||
|
||||
constructor(commandsManager: CommandsManager) {
|
||||
this._commandsManager = commandsManager;
|
||||
this._extensionManager = null;
|
||||
this.services = {};
|
||||
this.registeredServiceNames = [];
|
||||
}
|
||||
@@ -46,6 +47,7 @@ export default class ServicesManager {
|
||||
if (service.create) {
|
||||
this.services[service.name] = service.create({
|
||||
configuration,
|
||||
extensionManager: this._extensionManager,
|
||||
commandsManager: this._commandsManager,
|
||||
servicesManager: this,
|
||||
extensionManager: this._extensionManager,
|
||||
|
||||
@@ -68,7 +68,7 @@ export default class ToolbarService extends PubSubService {
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.unsubscriptions.forEach(unsub => unsub());
|
||||
// this.unsubscriptions.forEach(unsub => unsub());
|
||||
this.state = {
|
||||
buttons: {},
|
||||
buttonSections: {},
|
||||
@@ -120,6 +120,10 @@ export default class ToolbarService extends PubSubService {
|
||||
public addButtons(buttons: Button[]): void {
|
||||
buttons.forEach(button => {
|
||||
if (!this.state.buttons[button.id]) {
|
||||
if (!button.props) {
|
||||
button.props = {};
|
||||
}
|
||||
|
||||
this.state.buttons[button.id] = button;
|
||||
}
|
||||
});
|
||||
@@ -350,6 +354,7 @@ export default class ToolbarService extends PubSubService {
|
||||
* @param {Array} buttons - The buttons to be added to the section.
|
||||
*/
|
||||
createButtonSection(key, buttons) {
|
||||
// make sure all buttons have at least an empty props
|
||||
this.state.buttonSections[key] = buttons;
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, { ...this.state });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
import { CommandsManager } from '../../classes';
|
||||
import { ExtensionManager } from '../../extensions';
|
||||
import { ServicesManager } from '../../services';
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
|
||||
export const EVENTS = {
|
||||
ACTIVE_STEP_CHANGED: 'event::workflowStepsService:activateStepChanged',
|
||||
STEPS_CHANGED: 'event::workflowStepsService:stepsChanged',
|
||||
};
|
||||
|
||||
/*
|
||||
A mode may define a workflow and each workflow may have one or more steps.
|
||||
Each step may define a different set of tools, hanging protocol and panels
|
||||
layout that will be applied to the viewer once it gets activated making the
|
||||
viewer work in a more dynamic way.
|
||||
|
||||
Example:
|
||||
All keys inside brackets are optionals.
|
||||
|
||||
workflow: {
|
||||
[initialStepId]: 'step1',
|
||||
steps: [
|
||||
{
|
||||
id: 'firstStep',
|
||||
name: 'First Step',
|
||||
[toolbar]: {
|
||||
buttons: firstStepToolbarButtons,
|
||||
sections: [
|
||||
{
|
||||
key: 'primary',
|
||||
buttons: [ 'MeasurementTools', 'Zoom', ... ],
|
||||
},
|
||||
],
|
||||
},
|
||||
[layout]: {
|
||||
[panels]: {
|
||||
left: ['firstLeftPanelId', 'secondLeftPanelId'],
|
||||
right: ['firstRightPanelId'],
|
||||
},
|
||||
},
|
||||
[hangingProtocol]: {
|
||||
protocolId: 'default',
|
||||
[stepId]: 'firstStep',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'secondStep',
|
||||
name: 'Second Step',
|
||||
...
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
If workflow steps are defined but `initialStepId` is not set then the first
|
||||
step is set as active during mode initialization.
|
||||
*/
|
||||
|
||||
type CommandCallback = {
|
||||
commandName: string;
|
||||
options: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type WorkflowStep = {
|
||||
id: string;
|
||||
name: string;
|
||||
toolbarButtons?: {
|
||||
buttonSection: string;
|
||||
buttons: string[];
|
||||
}[];
|
||||
hangingProtocol?: {
|
||||
protocolId: string;
|
||||
stageId?: string;
|
||||
};
|
||||
layout?: {
|
||||
panels: {
|
||||
left?: string[];
|
||||
right?: string[];
|
||||
};
|
||||
};
|
||||
onEnter: () => void | CommandCallback[];
|
||||
};
|
||||
|
||||
class WorkflowStepsService extends PubSubService {
|
||||
private _extensionManager: ExtensionManager;
|
||||
private _servicesManager: ServicesManager;
|
||||
private _commandsManager: CommandsManager;
|
||||
private _workflowSteps: WorkflowStep[];
|
||||
private _activeWorkflowStep: WorkflowStep;
|
||||
|
||||
constructor(
|
||||
extensionManager: ExtensionManager,
|
||||
commandsManager: CommandsManager,
|
||||
servicesManager: ServicesManager
|
||||
) {
|
||||
super(EVENTS);
|
||||
this._workflowSteps = [];
|
||||
this._activeWorkflowStep = null;
|
||||
this._extensionManager = extensionManager;
|
||||
this._commandsManager = commandsManager;
|
||||
this._servicesManager = servicesManager;
|
||||
}
|
||||
|
||||
public get workflowSteps(): WorkflowStep[] {
|
||||
return [...this._workflowSteps];
|
||||
}
|
||||
|
||||
public get activeWorkflowStep(): WorkflowStep {
|
||||
return this._activeWorkflowStep;
|
||||
}
|
||||
|
||||
public addWorkflowSteps(workflowSteps: WorkflowStep[]): void {
|
||||
let workflowStepAdded = false;
|
||||
|
||||
workflowSteps.forEach(newWorkflowStep => {
|
||||
const workflowStepExists = this._workflowSteps.some(
|
||||
workflowStep => workflowStep.id === newWorkflowStep.id
|
||||
);
|
||||
|
||||
if (workflowStepExists) {
|
||||
throw new Error(`Duplicated workflow step id (${newWorkflowStep.id})`);
|
||||
}
|
||||
|
||||
this._workflowSteps.push(newWorkflowStep);
|
||||
workflowStepAdded = true;
|
||||
});
|
||||
|
||||
if (workflowStepAdded) {
|
||||
this._broadcastEvent(EVENTS.STEPS_CHANGED, {});
|
||||
}
|
||||
}
|
||||
|
||||
private _updateToolBar(workflowStep: WorkflowStep) {
|
||||
const { toolbarService } = this._servicesManager.services;
|
||||
const { toolbarButtons } = workflowStep;
|
||||
|
||||
const toUse = Array.isArray(toolbarButtons) ? toolbarButtons : [toolbarButtons];
|
||||
|
||||
toUse.forEach(({ buttonSection, buttons }) => {
|
||||
toolbarService.createButtonSection(buttonSection, buttons);
|
||||
});
|
||||
}
|
||||
|
||||
private _updatePanels(workflowStep: WorkflowStep) {
|
||||
const { panelService } = this._servicesManager.services;
|
||||
const panels = workflowStep?.layout?.panels;
|
||||
|
||||
if (!panels) {
|
||||
return;
|
||||
}
|
||||
|
||||
panelService.setPanels(panels, workflowStep?.layout?.options);
|
||||
}
|
||||
|
||||
private _updateHangingProtocol(workflowStep: WorkflowStep) {
|
||||
const { hangingProtocol } = workflowStep;
|
||||
|
||||
if (!hangingProtocol) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._commandsManager.runCommand('setHangingProtocol', {
|
||||
protocolId: hangingProtocol.protocolId,
|
||||
stageId: hangingProtocol.stageId,
|
||||
stageIndex: hangingProtocol.stageIndex,
|
||||
});
|
||||
}
|
||||
|
||||
private _invokeCallbacks(callbacks) {
|
||||
if (!callbacks) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commandsManager = this._commandsManager;
|
||||
|
||||
if (!Array.isArray) {
|
||||
callbacks = [callbacks];
|
||||
}
|
||||
|
||||
// Invoke all callbacks which may be a function or an object like
|
||||
// { commandName: string, options?: object }
|
||||
callbacks.forEach(callback => {
|
||||
let fn = callback;
|
||||
|
||||
if (callback?.commandName) {
|
||||
const { commandName, options } = callback;
|
||||
fn = () => commandsManager.runCommand(commandName, options);
|
||||
}
|
||||
|
||||
fn();
|
||||
});
|
||||
}
|
||||
|
||||
public setActiveWorkflowStep(workflowStepId: string): void {
|
||||
const previousWorkflowStep = this._activeWorkflowStep;
|
||||
|
||||
if (workflowStepId === previousWorkflowStep?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newWorkflowStep = this._workflowSteps.find(step => step.id === workflowStepId);
|
||||
|
||||
if (!newWorkflowStep) {
|
||||
throw new Error(`Invalid workflowStepId (${workflowStepId})`);
|
||||
}
|
||||
|
||||
// onEnter needs to be called before updating the Hanging Protocol because
|
||||
// some displaySets need to be created before moving to the next HP stage
|
||||
// (eg: convert segmentations into a chart displaySet). If needed we can
|
||||
// change it to onBeforeEnter and onAfterEnter in the future.
|
||||
this._invokeCallbacks(newWorkflowStep.onEnter);
|
||||
|
||||
this._activeWorkflowStep = newWorkflowStep;
|
||||
this._updateToolBar(newWorkflowStep);
|
||||
this._updatePanels(newWorkflowStep);
|
||||
this._updateHangingProtocol(newWorkflowStep);
|
||||
this._broadcastEvent(EVENTS.ACTIVE_STEP_CHANGED, {
|
||||
activeWorkflowStep: newWorkflowStep,
|
||||
});
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._activeWorkflowStep = null;
|
||||
this._workflowSteps = [];
|
||||
}
|
||||
|
||||
public onModeEnter(): void {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public static REGISTRATION = {
|
||||
name: 'workflowStepsService',
|
||||
create: ({ extensionManager, commandsManager, servicesManager }): WorkflowStepsService => {
|
||||
return new WorkflowStepsService(extensionManager, commandsManager, servicesManager);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export { WorkflowStepsService as default, WorkflowStepsService };
|
||||
@@ -0,0 +1,3 @@
|
||||
import WorkflowStepsService from './WorkflowStepsService';
|
||||
|
||||
export default WorkflowStepsService;
|
||||
@@ -16,6 +16,7 @@ import UserAuthenticationService from './UserAuthenticationService';
|
||||
import CustomizationService from './CustomizationService';
|
||||
import StateSyncService from './StateSyncService';
|
||||
import PanelService from './PanelService';
|
||||
import WorkflowStepsService from './WorkflowStepsService';
|
||||
|
||||
import type Services from '../types/Services';
|
||||
|
||||
@@ -40,4 +41,5 @@ export {
|
||||
PubSubService,
|
||||
UserAuthenticationService,
|
||||
PanelService,
|
||||
WorkflowStepsService,
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
StateSyncService,
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
WorkflowStepsService,
|
||||
} from '../services';
|
||||
|
||||
/**
|
||||
@@ -23,6 +24,7 @@ export default interface Services {
|
||||
uiModalService?: UIModalService;
|
||||
uiNotificationService?: UINotificationService;
|
||||
stateSyncService?: StateSyncService;
|
||||
workflowStepsService: WorkflowStepsService;
|
||||
cineService?: unknown;
|
||||
userAuthenticationService?: unknown;
|
||||
cornerstoneViewportService?: unknown;
|
||||
|
||||
|
After Width: | Height: | Size: 101 KiB |
@@ -145,7 +145,7 @@ if auth headers are used, a preflight request is required.
|
||||
props: {
|
||||
leftPanels: [tracked.thumbnailList],
|
||||
rightPanels: [dicomSeg.panel, tracked.measurements],
|
||||
rightPanelDefaultClosed: true,
|
||||
rightPanelClosed: true,
|
||||
viewports: [
|
||||
{
|
||||
namespace: tracked.viewport,
|
||||
@@ -242,6 +242,19 @@ Example usage:<br/>
|
||||
This configuration would allow the user to build a dicomweb configuration from a GCP healthcare api path e.g. http://localhost:3000/projects/your-gcp-project/locations/us-central1/datasets/your-dataset/dicomStores/your-dicom-store/study/1.3.6.1.4.1.1234.5.2.1.1234.1234.123123123123123123123123123123
|
||||
|
||||
|
||||
:::note
|
||||
You can stack multiple panel components on top of each other by providing an array of panel components in the `rightPanels` or `leftPanels` properties.
|
||||
|
||||
For instance we can use
|
||||
|
||||
```
|
||||
rightPanels: [[dicomSeg.panel, tracked.measurements], [dicomSeg.panel, tracked.measurements]]
|
||||
```
|
||||
|
||||
This will result in two panels, one with `dicomSeg.panel` and `tracked.measurements` and the other with `dicomSeg.panel` and `tracked.measurements` stacked on top of each other.
|
||||
|
||||
:::
|
||||
|
||||
### More on Accept Header Configuration
|
||||
In the previous section we showed that you can modify the `acceptHeader`
|
||||
configuration to request specific dicom transfer syntax. By default
|
||||
|
||||
@@ -107,7 +107,7 @@ function modeFactory({ modeConfiguration }) {
|
||||
rightPanels: [
|
||||
'@ohif/extension-measurement-tracking.panelModule.trackedMeasurements',
|
||||
],
|
||||
rightPanelDefaultClosed: true,
|
||||
rightPanelClosed: true,
|
||||
viewports,
|
||||
},
|
||||
};
|
||||
@@ -147,5 +147,17 @@ const mode = {
|
||||
};
|
||||
|
||||
export default mode;
|
||||
```
|
||||
|
||||
:::note
|
||||
You can stack multiple panel components on top of each other by providing an array of panel components in the `rightPanels` or `leftPanels` properties.
|
||||
|
||||
For instance we can use
|
||||
|
||||
```
|
||||
rightPanels: [[dicomSeg.panel, tracked.measurements], [dicomSeg.panel, tracked.measurements]]
|
||||
```
|
||||
|
||||
This will result in two panels, one with `dicomSeg.panel` and `tracked.measurements` and the other with `dicomSeg.panel` and `tracked.measurements` stacked on top of each other.
|
||||
|
||||
:::
|
||||
@@ -362,7 +362,6 @@ function modeFactory() {
|
||||
// exports
|
||||
```
|
||||
|
||||
### Toolbar
|
||||
|
||||
|
||||
|
||||
@@ -398,3 +397,17 @@ Use the provided `cli` to add/remove/install/uninstall modes. Read more [here](.
|
||||
:::
|
||||
|
||||
The final registration and import of the modes happen inside a non-tracked file `pluginImport.js` (this file is also for internal use only).
|
||||
|
||||
|
||||
:::note
|
||||
You can stack multiple panel components on top of each other by providing an array of panel components in the `rightPanels` or `leftPanels` properties.
|
||||
|
||||
For instance we can use
|
||||
|
||||
```
|
||||
rightPanels: [[dicomSeg.panel, tracked.measurements], [dicomSeg.panel, tracked.measurements]]
|
||||
```
|
||||
|
||||
This will result in two panels, one with `dicomSeg.panel` and `tracked.measurements` and the other with `dicomSeg.panel` and `tracked.measurements` stacked on top of each other.
|
||||
|
||||
:::
|
||||
@@ -274,6 +274,19 @@ layoutTemplate: ({ location, servicesManager }) => {
|
||||
*/
|
||||
```
|
||||
|
||||
:::note
|
||||
You can stack multiple panel components on top of each other by providing an array of panel components in the `rightPanels` or `leftPanels` properties.
|
||||
|
||||
For instance we can use
|
||||
|
||||
```
|
||||
rightPanels: [[dicomSeg.panel, tracked.measurements], [dicomSeg.panel, tracked.measurements]]
|
||||
```
|
||||
|
||||
This will result in two panels, one with `dicomSeg.panel` and `tracked.measurements` and the other with `dicomSeg.panel` and `tracked.measurements` stacked on top of each other.
|
||||
|
||||
:::
|
||||
|
||||
## FAQ
|
||||
|
||||
> What is the difference between `onModeEnter` and `route.init`
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
---
|
||||
sidebar_position: 9
|
||||
sidebar_label: WorkflowStep Service
|
||||
---
|
||||
|
||||
# Workflow Step Service
|
||||
|
||||
This service allows you to manage your workflow in smaller steps. It provides a structured way to define and navigate through different stages
|
||||
or phases of a larger process or workflow. Each step can have its own configuration, layout, toolbar buttons, and other settings tailored to the specific requirements of that stage.
|
||||
|
||||
## Anatomy of a Workflow Step
|
||||
|
||||
The anatomy of a workflow step refers to the different components or properties that define and configure each individual step within the workflow. Each step can be customized with various settings to tailor the user interface, available tools, and behavior of the application for that specific stage of the workflow. Here are the key components that make up a workflow step:
|
||||
|
||||
- `id`: A unique identifier for the step
|
||||
- `name`: A human-readable name or title for the step, which can be displayed in the user interface to help users understand the current stage of the workflow.
|
||||
- `hangingProtocol`: The hanging protocol configuration specifies the protocol and stage ID to be used for displaying the images. This ensures that the appropriate data viewports and presentation are used for the current workflow step.
|
||||
- `layout`: The layout configuration defines the arrangement and visibility of various panels or viewports within the application's user interface for the specific step. This can include specifying which panels should be visible on the left or right side of the screen, as well as any options for panel visibility or behavior.
|
||||
- `toolbarButtons`: Each step can define a set of toolbar buttons that should be available and displayed in the application's toolbar during that step. Remember the button definitions should already be registered to toolbarService beforehand, here we are just referencing the buttons id in each section.
|
||||
- `info` : An optional description or additional information about the current workflow step can be provided. which
|
||||
will be displayed as tooltip in the UI.
|
||||
|
||||
- Step Callbacks or Commands: Some workflow steps may require specific actions or commands to be executed when the step is entered or exited. These callbacks or commands can be defined within the step configuration and can be used to update the application's state, perform data processing, or trigger other relevant actions. For instance you have access to `onEnter` hook to run a command right after the step is entered.
|
||||
|
||||
For instance, a simplified example of our pre-clinical 4D workflow steps configuration might look like this:
|
||||
|
||||
```js
|
||||
const dynamicVolume = {
|
||||
sopClassHandler:
|
||||
"@ohif/extension-cornerstone-dynamic-volume.sopClassHandlerModule.dynamic-volume",
|
||||
leftPanel:
|
||||
"@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-volume",
|
||||
toolBox:
|
||||
"@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-toolbox",
|
||||
export:
|
||||
"@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-export",
|
||||
}
|
||||
|
||||
const cs3d = {
|
||||
segmentation:
|
||||
"@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation",
|
||||
}
|
||||
|
||||
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: "dataPreparation",
|
||||
name: "Data Preparation",
|
||||
layout: {
|
||||
panels: {
|
||||
left: [dynamicVolume.leftPanel],
|
||||
},
|
||||
},
|
||||
toolbarButtons: {
|
||||
buttonSection: "primary",
|
||||
buttons: ["MeasurementTools", "Zoom", "WindowLevel", "Crosshairs", "Pan"],
|
||||
},
|
||||
hangingProtocol: {
|
||||
protocolId: "default4D",
|
||||
stageId: "dataPreparation",
|
||||
},
|
||||
info: "In the Data Preparation step...",
|
||||
},
|
||||
{
|
||||
id: "roiQuantification",
|
||||
name: "ROI Quantification",
|
||||
layout: {
|
||||
panels: {
|
||||
left: [dynamicVolume.leftPanel],
|
||||
right: [
|
||||
[dynamicVolume.toolBox, cs3d.segmentation, dynamicVolume.export],
|
||||
],
|
||||
},
|
||||
options: {
|
||||
leftPanelClosed: false,
|
||||
rightPanelClosed: false,
|
||||
},
|
||||
},
|
||||
toolbarButtons: [
|
||||
{
|
||||
buttonSection: "primary",
|
||||
buttons: [
|
||||
"MeasurementTools",
|
||||
"Zoom",
|
||||
"WindowLevel",
|
||||
"Crosshairs",
|
||||
"Pan",
|
||||
],
|
||||
},
|
||||
{
|
||||
buttonSection: "dynamic-toolbox",
|
||||
buttons: ["BrushTools", "RectangleROIStartEndThreshold"],
|
||||
},
|
||||
],
|
||||
hangingProtocol: {
|
||||
protocolId: "default4D",
|
||||
stageId: "roiQuantification",
|
||||
},
|
||||
info: "The ROI quantification step ...",
|
||||
},
|
||||
{
|
||||
id: "kineticAnalysis",
|
||||
name: "Kinetic Analysis",
|
||||
layout: {
|
||||
panels: {
|
||||
left: [dynamicVolume.leftPanel],
|
||||
right: [],
|
||||
},
|
||||
},
|
||||
toolbarButtons: {
|
||||
buttonSection: "primary",
|
||||
buttons: ["MeasurementTools", "Zoom", "WindowLevel", "Crosshairs", "Pan"],
|
||||
},
|
||||
hangingProtocol: {
|
||||
protocolId: "default4D",
|
||||
stageId: "kineticAnalysis",
|
||||
},
|
||||
onEnter: [
|
||||
{
|
||||
commandName: "updateSegmentationsChartDisplaySet",
|
||||
options: { servicesManager },
|
||||
},
|
||||
],
|
||||
info: "The Kinetic Analysis step ...",
|
||||
},
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
## Integration
|
||||
|
||||
After you have defined your workflow steps, you can integrate them into your application by using the `workflowStepsService`.
|
||||
|
||||
These steps should be called on `onSetupRouteComplete` in your mode factory.
|
||||
|
||||
|
||||
Note: onModeEnter is too soon to call these steps as the mode is not yet fully initialized.
|
||||
|
||||
|
||||
```js
|
||||
onSetupRouteComplete: ({ servicesManager }) => {
|
||||
workflowStepsService.addWorkflowSteps(workflowSettings.steps);
|
||||
workflowStepsService.setActiveWorkflowStep(workflowSettings.steps[0].id);
|
||||
},
|
||||
```
|
||||
|
||||
check out the `modes/preclinical-4d/src/index.tsx` for a complete example.
|
||||
|
||||
|
||||
## User Interface
|
||||
|
||||
We have developed a simple dropdown UI element that you can use to navigate between the different steps of your workflow. This dropdown can be added to the toolbar like below:
|
||||
|
||||
```js
|
||||
toolbarService.addButtons([
|
||||
{
|
||||
id: 'ProgressDropdown',
|
||||
uiType: 'ohif.progressDropdown',
|
||||
},
|
||||
])
|
||||
toolbarService.createButtonSection('secondary', ['ProgressDropdown']);
|
||||
```
|
||||
|
||||
It will appear in the `secondary` location in the toolbar.
|
||||
|
||||

|
||||
|
||||
:::note
|
||||
if you like to place the progressbar in a different location, you can use the Toolbox component
|
||||
to create a button section and place the progress bar there.
|
||||
|
||||
Read more in the [Toolbar module](../../extensions//modules/toolbar.md)
|
||||
:::
|
||||
@@ -996,7 +996,9 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
@@ -1037,6 +1039,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
@@ -1044,9 +1047,11 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01)
|
||||
|
||||
|
||||
|
||||
* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd))
|
||||
|
||||
# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06)
|
||||
|
||||
@@ -1054,49 +1059,64 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ImageOverlayViewerTool:** add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images ([#3163](https://github.com/OHIF/Viewers/issues/3163)) ([69115da](https://github.com/OHIF/Viewers/commit/69115da06d2d437b57e66608b435bb0bc919a90f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file. See
|
||||
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30)
|
||||
# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
|
||||
|
||||
* **ImageOverlayViewerTool:** add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images ([#3163](https://github.com/OHIF/Viewers/issues/3163)) ([69115da](https://github.com/OHIF/Viewers/commit/69115da06d2d437b57e66608b435bb0bc919a90f))
|
||||
|
||||
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
## [1.4.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.3...@ohif/ui@1.4.4) (2020-05-04)
|
||||
|
||||
# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01)
|
||||
|
||||
- 🐛 Proper error handling for derived display sets
|
||||
([#1708](https://github.com/OHIF/Viewers/issues/1708))
|
||||
([5b20d8f](https://github.com/OHIF/Viewers/commit/5b20d8f323e4b3ef9988f2f2ab672d697b6da409))
|
||||
|
||||
### Features
|
||||
|
||||
* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd))
|
||||
|
||||
|
||||
|
||||
## [1.4.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.1...@ohif/ui@1.4.2) (2020-04-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file. See
|
||||
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30)
|
||||
|
||||
# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.3.3...@ohif/ui@1.4.0) (2020-03-13)
|
||||
|
||||
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
|
||||
@@ -1139,7 +1159,9 @@ All notable changes to this project will be documented in this file. See
|
||||
|
||||
## [1.4.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.1...@ohif/ui@1.4.2) (2020-04-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
- Combined Hotkeys for special characters
|
||||
([#1233](https://github.com/OHIF/Viewers/issues/1233))
|
||||
([2f30e7a](https://github.com/OHIF/Viewers/commit/2f30e7a821a238144c49c56f37d8e5565540b4bd))
|
||||
|
||||
## [1.4.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.0...@ohif/ui@1.4.1) (2020-03-17)
|
||||
|
||||
|
||||
@@ -36,6 +36,13 @@
|
||||
"@testing-library/react-hooks": "^3.2.1",
|
||||
"browser-detect": "^0.2.28",
|
||||
"classnames": "^2.3.2",
|
||||
"d3-array": "3",
|
||||
"d3-axis": "3",
|
||||
"d3-scale": "4",
|
||||
"d3-scale-chromatic": "3",
|
||||
"d3-selection": "3",
|
||||
"d3-shape": "3",
|
||||
"d3-zoom": "3",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"moment": "2.29.4",
|
||||
"mousetrap": "^1.6.5",
|
||||
@@ -57,14 +64,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.23.2",
|
||||
"@storybook/addon-actions": "^7.2.2",
|
||||
"@storybook/addon-docs": "^7.2.2",
|
||||
"@storybook/addon-essentials": "^7.2.2",
|
||||
"@storybook/addon-links": "^7.2.2",
|
||||
"@storybook/cli": "^7.2.2",
|
||||
"@storybook/react": "^7.2.2",
|
||||
"@storybook/react-webpack5": "^7.2.2",
|
||||
"@storybook/source-loader": "^7.2.2",
|
||||
"@storybook/addon-actions": "^7.6.10",
|
||||
"@storybook/addon-docs": "^7.6.10",
|
||||
"@storybook/addon-essentials": "^7.6.10",
|
||||
"@storybook/addon-links": "^7.6.10",
|
||||
"@storybook/cli": "^7.6.10",
|
||||
"@storybook/react": "^7.6.10",
|
||||
"@storybook/react-webpack5": "^7.6.10",
|
||||
"@storybook/source-loader": "^7.6.10",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"babel-loader": "^9.1.2",
|
||||
"dotenv-webpack": "^8.0.1",
|
||||
@@ -72,7 +79,7 @@
|
||||
"postcss-loader": "^7.2.4",
|
||||
"prop-types": "^15.8.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"storybook": "^7.2.2",
|
||||
"storybook": "^7.6.10",
|
||||
"tailwindcss": "3.2.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g fill-rule="evenodd">
|
||||
<path fill="currentcolor" fill-rule="nonzero" d="M17.207 10.793c.36.36.388.928.083 1.32l-.083.094-5 5c-.39.39-1.024.39-1.414 0-.36-.36-.388-.928-.083-1.32l.083-.094 4.292-4.293-4.292-4.293c-.36-.36-.388-.928-.083-1.32l.083-.094c.36-.36.928-.388 1.32-.083l.094.083 5 5z"/>
|
||||
<path fill="currentcolor" fill-rule="nonzero" d="M17.5 11.5c0 .513-.386.936-.883.993l-.117.007H6c-.552 0-1-.448-1-1 0-.513.386-.936.883-.993L6 10.5h10.5c.552 0 1 .448 1 1z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 568 B |
@@ -0,0 +1,22 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 489.785 489.785" xml:space="preserve">
|
||||
<g id="tool-brush" stroke="currentColor" fill="currentColor">
|
||||
<path id="Path" stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M479.702,10.08L479.702,10.08c-25.866-25.864-93.419-0.238-150.886,57.231l0.001,0.001l-0.002-0.001
|
||||
c-9.503,9.502-18.126,19.282-25.793,29.104c-9.596,54.899-33.63,93.829-61.77,93.831v0.001c-9.089,0-17.749-4.064-25.633-11.412
|
||||
l-0.043,0.014l-20.831-20.832c-11.778-11.777-30.87-11.777-42.648,0l-80.851,80.849l-4.268,4.268L8.835,301.279
|
||||
c-11.776,11.778-11.776,30.872,0.001,42.648L145.86,480.953c11.777,11.777,30.869,11.776,42.647,0l58.144-58.144l4.27-4.267
|
||||
l80.849-80.85c11.774-11.778,11.774-30.871-0.001-42.648l-20.831-20.831l0.015-0.043c-7.348-7.884-11.411-16.543-11.412-25.631
|
||||
v-0.001c0-0.001,0-0.001,0-0.001c0.004-28.141,38.93-52.173,93.822-61.769c9.824-7.668,19.606-16.292,29.111-25.796l-0.028-0.031
|
||||
l0.03,0.03C479.941,103.499,505.565,35.949,479.702,10.08z M248.322,390.438l0.015,0.015l-56.174,56.175l-0.003-0.003l-14.69,14.692
|
||||
c-6.001,5.998-15.723,5.998-21.721,0L28.468,334.035c-5.996-5.996-6.002-15.722-0.003-21.721l14.691-14.69v-0.001L98.5,242.281
|
||||
l0.001,0.001c13.478-13.475,31.025-17.777,39.201-9.603c8.174,8.174,4.039,25.89-9.434,39.369l-0.001,0.001
|
||||
c-13.476,13.475-17.609,31.192-9.437,39.364c8.177,8.176,25.793,3.944,40.102-10.364l0.014,0.014
|
||||
c13.355-12.825,30.352-16.79,38.355-8.786c8.007,8.006,4.203,25.162-8.614,38.526l0.047,0.047
|
||||
c-14.308,14.308-18.441,32.025-10.269,40.198c8.177,8.175,25.795,3.942,39.269-9.533l0.835-0.831l0.013,0.012
|
||||
c13.356-12.824,30.351-16.788,38.357-8.782C264.94,359.917,261.136,377.073,248.322,390.438z M303.916,313.153
|
||||
c5.998,5.999,5.998,15.721-0.001,21.718l-17.245,17.246l-149-149.002l17.246-17.245c5.998-5.998,15.722-5.996,21.719,0.001
|
||||
L303.916,313.153z M409.705,80.082c-9.404-9.404-9.405-24.651,0.001-34.058c9.405-9.405,24.651-9.403,34.057,0
|
||||
c9.404,9.406,9.404,24.653,0.001,34.058C434.357,89.487,419.111,89.488,409.705,80.082z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 480.001 480.001" xml:space="preserve">
|
||||
<g id="tool-eraser" stroke="currentColor" fill="currentColor">
|
||||
<path d="M333.142,350.846c0.115-0.115,0.215-0.239,0.323-0.357l129.681-129.706c10.878-10.878,16.864-25.368,16.855-40.8
|
||||
c-0.01-15.409-5.999-29.865-16.854-40.694l-97.844-97.874c-10.853-10.845-25.326-16.817-40.75-16.817
|
||||
c-15.426,0-29.895,5.974-40.741,16.82L16.855,308.329C5.974,319.21-0.012,333.713,0,349.168
|
||||
c0.013,15.425,6.002,29.884,16.854,40.7l62.592,62.606c0.061,0.061,0.127,0.112,0.188,0.171c0.174,0.165,0.349,0.331,0.534,0.483
|
||||
c0.082,0.067,0.171,0.126,0.255,0.19c0.175,0.135,0.349,0.271,0.532,0.395c0.07,0.047,0.145,0.085,0.215,0.13
|
||||
c0.205,0.131,0.412,0.26,0.627,0.376c0.051,0.026,0.103,0.048,0.154,0.074c0.239,0.123,0.482,0.241,0.732,0.346
|
||||
c0.033,0.014,0.067,0.024,0.101,0.037c0.269,0.108,0.54,0.208,0.819,0.293c0.034,0.011,0.07,0.017,0.104,0.027
|
||||
c0.276,0.081,0.556,0.154,0.841,0.211c0.082,0.017,0.165,0.023,0.247,0.038c0.239,0.041,0.479,0.084,0.724,0.107
|
||||
c0.33,0.033,0.663,0.051,0.998,0.051h137.91h159.308c5.522,0,10-4.478,10-10c0-5.522-4.478-10-10-10H248.566l84.22-84.236
|
||||
C332.904,351.06,333.027,350.96,333.142,350.846z M220.285,435.404H90.66l-59.675-59.689
|
||||
c-7.076-7.054-10.977-16.487-10.985-26.563c-0.008-10.106,3.897-19.582,10.996-26.681l129.825-129.803l151.091,151.091
|
||||
L220.285,435.404z M174.965,178.527L297.953,55.56c7.069-7.069,16.516-10.963,26.6-10.963c10.085,0,19.536,3.895,26.609,10.962
|
||||
l97.85,97.88c7.08,7.063,10.982,16.493,10.989,26.557c0.006,10.085-3.899,19.547-10.998,26.645l-122.95,122.974L174.965,178.527z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" xml:space="preserve">
|
||||
<g id="tool-brush" stroke="currentColor" fill="currentColor">
|
||||
<path d="M160,0l-29.1,29.8l36.9,36.9l-143,143c-24.5,24.5-24.5,64.8,0,89.3l1.3,1.3l131.4,130.8c24.5,24.5,64.8,24.5,89.3,0
|
||||
L404,273.8l14.9-14.2L217.6,58.3l-16.2-16.8l-4.5-3.9L160,0z M197.6,96.4l163.1,163.1L217.6,402c-8.7,8.7-22.4,8.7-31.1,0
|
||||
l-132-132.7c-8.7-8.7-8.7-21.8,0-30.4L197.6,96.4z M443.5,330.8l-16.8,24.6c0,0-11.2,15.6-22,34.3c-5.4,9.4-10.5,19.2-14.9,29.1
|
||||
s-8.4,19.1-8.4,31.1c0,34.1,28.1,62.1,62.1,62.1s62.1-28.1,62.1-62.1c0-12-4-21.1-8.4-31.1c-4.4-10-9.5-19.7-14.9-29.1
|
||||
c-10.8-18.7-22-34.3-22-34.3L443.5,330.8z M443.5,405.8c1.4,2.3,1.2,2.1,2.6,4.5c4.9,8.5,10.2,17.6,13.6,25.2
|
||||
c3.4,7.7,4.5,14.8,4.5,14.2c0,11.5-9.2,20.7-20.7,20.7s-20.7-9.2-20.7-20.7c0,0.6,1.1-6.6,4.5-14.2c3.4-7.7,8.7-16.7,13.6-25.2
|
||||
C442.3,407.9,442.1,408.1,443.5,405.8z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1003 B |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg version="1.1" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||
<g id="tool-scissor-circle" stroke="currentColor" fill="currentColor">
|
||||
<path
|
||||
d="m 95.186411,99.470453 a 9.7463486,9.7463486 0 0 1 6.903659,11.957487 68.30566,68.30566 0 0 0 0.006,35.2595 9.8275678,9.8275678 0 0 1 -4.504348,11.04118 8.6092746,8.6092746 0 0 1 -2.383531,0.90721 9.665129,9.665129 0 0 1 -11.907784,-6.81755 87.554699,87.554699 0 0 1 -0.07138,-45.44416 9.9087877,9.9087877 0 0 1 11.9575,-6.903667 z m 24.574799,77.814127 a 9.762664,9.762664 0 0 0 -13.76005,13.85276 88.366894,88.366894 0 0 0 39.27951,22.71357 9.9087877,9.9087877 0 0 0 7.40009,-0.99 9.7463486,9.7463486 0 0 0 -2.34626,-17.87117 67.980782,67.980782 0 0 1 -30.57329,-17.70516 z m 96.47052,0.10436 a 67.412245,67.412245 0 0 1 -30.56739,17.55431 9.7463486,9.7463486 0 0 0 -6.88789,11.94839 9.665129,9.665129 0 0 0 11.87803,6.9285 87.22982,87.22982 0 0 0 21.26761,-8.80886 85.524208,85.524208 0 0 0 18.12181,-13.93263 9.7463486,9.7463486 0 0 0 -13.81217,-13.68971 z m 24.54655,-77.851495 a 9.665129,9.665129 0 0 0 -6.81753,11.907775 68.711758,68.711758 0 0 1 -0.0644,35.3001 9.7463486,9.7463486 0 0 0 14.30377,10.96752 9.9087877,9.9087877 0 0 0 4.5574,-5.91368 87.717137,87.717137 0 0 0 -0.0308,-45.37382 9.7463486,9.7463486 0 0 0 -11.94839,-6.887895 z M 216.22515,80.888706 a 9.9087877,9.9087877 0 0 0 11.77351,1.549376 12.589034,12.589034 0 0 0 2.01805,-1.540259 9.8275678,9.8275678 0 0 0 0.009,-13.791554 88.204454,88.204454 0 0 0 -39.32011,-22.783905 9.7632558,9.7632558 0 0 0 -5.05383,18.861161 67.980782,67.980782 0 0 1 30.57329,17.705181 z m -96.4705,-0.104375 a 67.412245,67.412245 0 0 1 30.56739,-17.554309 14.213424,14.213424 0 0 0 2.41326,-1.018158 9.7463486,9.7463486 0 0 0 -7.40342,-17.858725 88.123236,88.123236 0 0 0 -39.38942,22.741485 9.7463486,9.7463486 0 1 0 13.81219,13.68971 z" />
|
||||
<path
|
||||
d="m 37.698988,37.475257 c -0.710483,-0.0944 -1.417711,0.15341 -1.907644,0.67794 l -3.029525,3.23827 c -1.186174,1.25331 -2.301137,2.75282 -3.507657,4.7097 -2.482623,4.02648 -5.107669,10.19785 -3.519051,16.70654 0.428487,1.75952 1.180885,3.27693 1.90439,4.74144 0.116786,0.23561 0.234793,0.47122 0.349951,0.7052 l 30.212235,66.147003 -12.657265,20.15681 c -0.950161,-0.35239 -1.929212,-0.64945 -2.926168,-0.88953 -1.78272,-0.43378 -3.619967,-0.69217 -5.457622,-0.76704 -10.82165,-0.44762 -22.117364,5.53615 -29.281611,15.3759 -1.323713,1.79574 -3.0873082,4.62262 -5.0474462,9.36284 -1.279765,3.39535 -2.076923,6.92091 -2.351187,10.44566 -0.477318,6.12537 0.618926,12.05747 3.167471,17.14805 2.7609572,5.52354 7.3066692,9.95613 12.7984722,12.48799 3.185783,1.4641 6.591709,2.28038 10.126223,2.42565 0,0 0,0 0.0033,0 10.545352,0.43541 21.564767,-5.11173 28.76116,-14.47742 0.01424,-0.0151 0.02604,-0.0309 0.03784,-0.0509 l 1.092573,-1.50276 c 2.69219,-3.86331 4.71011,-8.26008 5.85397,-12.74964 0.009,-0.0183 0.011,-0.0383 0.0163,-0.0574 0.35687,-1.58781 0.45902,-2.1046 0.49115,-2.30725 2.8163,-14.67559 6.73576,-24.04006 10.05177,-29.81061 l 3.50847,-5.37786 V 110.37512 L 39.351494,38.641497 c -0.329606,-0.63276 -0.94365,-1.06857 -1.652506,-1.16624 z m 0.906213,160.268183 c -3.339599,3.03929 -7.643193,4.833 -11.229385,4.68772 -1.656168,-0.0708 -3.183342,-0.53631 -4.533098,-1.3819 -0.0036,-0.002 -0.0061,-0.003 -0.0073,-0.005 l -0.0016,-0.002 c -3.15038,-1.97275 -4.933507,-5.9622 -4.768704,-10.66661 0.177417,-5.08936 2.466754,-10.26417 6.277159,-14.19055 2.733288,-2.81345 7.339224,-5.75345 12.072526,-5.55691 1.118625,0.046 2.198593,0.27019 3.211825,0.66573 4.684066,1.83968 6.263326,7.115 6.194149,11.07963 -0.09929,5.60412 -2.797174,11.34943 -7.21552,15.37021 z" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -0,0 +1,39 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="453.408px" height="453.408px" viewBox="0 0 453.408 453.408" xml:space="preserve">
|
||||
<g stroke="currentColor" fill="currentColor">
|
||||
<path d="M437.671,383.799c-8.689,0-15.735,7.047-15.735,15.738v15.736h-15.737c-8.576,0-15.736,7.162-15.736,15.736
|
||||
c0,8.576,7.16,15.738,15.736,15.738h31.474c8.691,0,15.736-7.047,15.736-15.738v-31.473
|
||||
C453.407,390.846,446.362,383.799,437.671,383.799z"/>
|
||||
<path d="M345.353,415.273h-41.96c-8.576,0-15.737,7.16-15.737,15.736s7.161,15.736,15.737,15.736h41.96
|
||||
c8.576,0,15.736-7.16,15.736-15.736S353.93,415.273,345.353,415.273z"/>
|
||||
<path d="M242.548,415.273h-15.737v-15.738c0-8.576-7.16-15.736-15.736-15.736s-15.736,7.16-15.736,15.736v31.475
|
||||
c0,8.691,7.045,15.736,15.736,15.736h31.474c8.576,0,15.736-7.16,15.736-15.736S251.124,415.273,242.548,415.273z"/>
|
||||
<path d="M211.074,207.507c8.691,0,15.736-7.045,15.736-15.736v-69.255c0-8.576-7.16-15.737-15.736-15.737
|
||||
c-8.576,0-15.736,7.161-15.736,15.737v69.255C195.338,200.461,202.383,207.507,211.074,207.507z"/>
|
||||
<path d="M242.548,6.049h-31.474c-8.691,0-15.736,7.046-15.736,15.737v31.474c0,8.576,7.16,15.737,15.736,15.737
|
||||
c8.576,0,15.736-7.161,15.736-15.737V37.523h15.737c8.576,0,15.736-7.161,15.736-15.737
|
||||
C258.284,13.21,251.124,6.049,242.548,6.049z"/>
|
||||
<path d="M345.353,6.049h-41.96c-8.576,0-15.737,7.161-15.737,15.737s7.161,15.737,15.737,15.737h41.96
|
||||
c8.576,0,15.736-7.161,15.736-15.737C361.09,13.21,353.93,6.049,345.353,6.049z"/>
|
||||
<path d="M437.671,6.049h-31.474c-8.576,0-15.736,7.161-15.736,15.737s7.16,15.737,15.736,15.737h15.737v15.737
|
||||
c0,8.576,7.159,15.737,15.735,15.737s15.736-7.161,15.736-15.737V21.787C453.407,13.096,446.362,6.049,437.671,6.049z"/>
|
||||
<path d="M437.671,245.289c-8.689,0-15.735,7.045-15.735,15.736v69.256c0,8.575,7.159,15.736,15.735,15.736
|
||||
s15.736-7.161,15.736-15.736v-69.256C453.407,252.334,446.362,245.289,437.671,245.289z"/>
|
||||
<path d="M437.671,106.779c-8.689,0-15.735,7.046-15.735,15.737v69.255c0,8.576,7.159,15.736,15.735,15.736
|
||||
s15.736-7.161,15.736-15.736v-69.255C453.407,113.825,446.362,106.779,437.671,106.779z"/>
|
||||
<path d="M211.074,346.018c8.576,0,15.736-7.161,15.736-15.736v-69.256c0-8.576-7.16-15.736-15.736-15.736
|
||||
c-8.576,0-15.736,7.16-15.736,15.736v69.256C195.338,338.856,202.498,346.018,211.074,346.018z"/>
|
||||
<path d="M91.732,2.242c-1.746-0.232-3.484,0.377-4.688,1.666l-7.445,7.958c-2.915,3.08-5.655,6.765-8.62,11.574
|
||||
c-6.101,9.895-12.552,25.061-8.648,41.056c1.053,4.324,2.902,8.053,4.68,11.652c0.287,0.579,0.577,1.158,0.86,1.733
|
||||
l74.246,162.555l-31.105,49.535c-2.335-0.866-4.741-1.596-7.191-2.186c-4.381-1.066-8.896-1.701-13.412-1.885
|
||||
c-26.594-1.1-54.353,13.605-71.959,37.786c-3.253,4.413-7.587,11.36-12.404,23.009c-3.145,8.344-5.104,17.008-5.778,25.67
|
||||
c-1.173,15.053,1.521,29.631,7.784,42.141c6.785,13.574,17.956,24.467,31.452,30.689c7.829,3.598,16.199,5.604,24.885,5.961
|
||||
c0,0,0,0,0.008,0c25.915,1.07,52.995-12.562,70.68-35.578c0.035-0.037,0.064-0.076,0.093-0.125l2.685-3.693
|
||||
c6.616-9.494,11.575-20.299,14.386-31.332c0.021-0.045,0.027-0.094,0.04-0.141c0.877-3.902,1.128-5.172,1.207-5.67
|
||||
c6.921-36.065,16.553-59.078,24.702-73.259l8.622-13.216V181.392L95.793,5.108C94.983,3.553,93.474,2.482,91.732,2.242z
|
||||
M93.959,396.098c-8.207,7.469-18.783,11.877-27.596,11.52c-4.07-0.174-7.823-1.318-11.14-3.396
|
||||
c-0.009-0.004-0.015-0.008-0.018-0.014l-0.004-0.004c-7.742-4.848-12.124-14.652-11.719-26.213
|
||||
c0.436-12.507,6.062-25.224,15.426-34.873c6.717-6.914,18.036-14.139,29.668-13.656c2.749,0.113,5.403,0.664,7.893,1.636
|
||||
c11.511,4.521,15.392,17.485,15.222,27.228C111.447,372.098,104.817,386.217,93.959,396.098z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button, ButtonEnums } from '../../components';
|
||||
|
||||
function ActionButtons({ actions, disabled, t }) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{actions.map((action, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
onClick={action.onClick}
|
||||
disabled={disabled || action.disabled}
|
||||
type={ButtonEnums.type.secondary}
|
||||
size={ButtonEnums.size.small}
|
||||
className={index > 0 ? 'ml-2' : ''}
|
||||
>
|
||||
{t ? t(action.label) : action.label}
|
||||
</Button>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
ActionButtons.propTypes = {
|
||||
actions: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
})
|
||||
).isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
ActionButtons.defaultProps = {
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export default ActionButtons;
|
||||
@@ -0,0 +1,3 @@
|
||||
import ActionButtons from './ActionButtons';
|
||||
|
||||
export default ActionButtons;
|
||||
@@ -9,6 +9,7 @@ const ButtonGroup = ({
|
||||
orientation = ButtonEnums.orientation.horizontal,
|
||||
activeIndex: defaultActiveIndex = 0,
|
||||
onActiveIndexChange,
|
||||
separated = false,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [activeIndex, setActiveIndex] = useState(defaultActiveIndex);
|
||||
@@ -28,33 +29,62 @@ const ButtonGroup = ({
|
||||
};
|
||||
|
||||
const wrapperClasses = classnames(
|
||||
'items-stretch inline-flex',
|
||||
`${separated ? '' : 'inline-flex'}`,
|
||||
orientationClasses[orientation],
|
||||
className
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classnames(wrapperClasses, 'text-[13px]')}>
|
||||
{Children.map(children, (child, index) => {
|
||||
if (React.isValidElement(child)) {
|
||||
return cloneElement(child, {
|
||||
key: index,
|
||||
className: classnames(
|
||||
'rounded-[4px] px-2 py-1 text-center',
|
||||
index === activeIndex
|
||||
? 'bg-customblue-40 text-white'
|
||||
: 'text-primary-active bg-black',
|
||||
child.props.className,
|
||||
disabled ? 'ohif-disabled' : ''
|
||||
),
|
||||
onClick: e => {
|
||||
child.props.onClick && child.props.onClick(e);
|
||||
handleButtonClick(index);
|
||||
},
|
||||
});
|
||||
}
|
||||
return child;
|
||||
<div
|
||||
className={classnames(wrapperClasses, ' text-[13px]', {
|
||||
'border-secondary-light rounded-[5px] border bg-black': !separated,
|
||||
})}
|
||||
>
|
||||
{!separated &&
|
||||
Children.map(children, (child, index) => {
|
||||
if (React.isValidElement(child)) {
|
||||
return cloneElement(child, {
|
||||
key: index,
|
||||
className: classnames(
|
||||
'rounded-[4px] px-2 py-1',
|
||||
index === activeIndex
|
||||
? 'bg-customblue-40 text-white'
|
||||
: 'text-primary-active bg-black',
|
||||
child.props.className,
|
||||
disabled ? 'ohif-disabled' : ''
|
||||
),
|
||||
onClick: e => {
|
||||
child.props.onClick && child.props.onClick(e);
|
||||
handleButtonClick(index);
|
||||
},
|
||||
});
|
||||
}
|
||||
return child;
|
||||
})}
|
||||
{separated && (
|
||||
<div className="flex space-x-2">
|
||||
{Children.map(children, (child, index) => {
|
||||
if (React.isValidElement(child)) {
|
||||
return cloneElement(child, {
|
||||
key: index,
|
||||
className: classnames(
|
||||
'rounded-[4px] px-2 py-1',
|
||||
index === activeIndex
|
||||
? 'bg-customblue-40 text-white'
|
||||
: 'text-primary-active bg-black border-secondary-light rounded-[5px] border',
|
||||
child.props.className,
|
||||
disabled ? 'ohif-disabled' : ''
|
||||
),
|
||||
onClick: e => {
|
||||
child.props.onClick && child.props.onClick(e);
|
||||
handleButtonClick(index);
|
||||
},
|
||||
});
|
||||
}
|
||||
return child;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -66,6 +96,7 @@ ButtonGroup.propTypes = {
|
||||
onActiveIndexChange: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
separated: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ButtonGroup;
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import PropTypes, { string } from 'prop-types';
|
||||
import Icon from '../Icon';
|
||||
import Typography from '../Typography';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon, Typography } from '../../';
|
||||
|
||||
/**
|
||||
* REACT CheckBox component
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
@@ -7,7 +7,6 @@ import Tooltip from '../Tooltip';
|
||||
import InputRange from '../InputRange';
|
||||
|
||||
import './CinePlayer.css';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export type CinePlayerProps = {
|
||||
className: string;
|
||||
@@ -19,6 +18,12 @@ export type CinePlayerProps = {
|
||||
onFrameRateChange: (value: number) => void;
|
||||
onPlayPauseChange: (value: boolean) => void;
|
||||
onClose: () => void;
|
||||
updateDynamicInfo?: () => void;
|
||||
dynamicInfo?: {
|
||||
timePointIndex: number;
|
||||
numTimePoints: number;
|
||||
label?: string;
|
||||
};
|
||||
};
|
||||
|
||||
const fpsButtonClassNames =
|
||||
@@ -31,12 +36,15 @@ const CinePlayer: React.FC<CinePlayerProps> = ({
|
||||
maxFrameRate,
|
||||
stepFrameRate,
|
||||
frameRate: defaultFrameRate,
|
||||
dynamicInfo = {},
|
||||
onFrameRateChange,
|
||||
onPlayPauseChange,
|
||||
onClose,
|
||||
updateDynamicInfo,
|
||||
}) => {
|
||||
const isDynamic = !!dynamicInfo?.numTimePoints;
|
||||
const [frameRate, setFrameRate] = useState(defaultFrameRate);
|
||||
const debouncedSetFrameRate = debounce(onFrameRateChange, 300);
|
||||
const debouncedSetFrameRate = useCallback(debounce(onFrameRateChange, 100), [onFrameRateChange]);
|
||||
|
||||
const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play');
|
||||
|
||||
@@ -52,45 +60,92 @@ const CinePlayer: React.FC<CinePlayerProps> = ({
|
||||
setFrameRate(defaultFrameRate);
|
||||
}, [defaultFrameRate]);
|
||||
|
||||
const handleTimePointChange = useCallback(
|
||||
(newIndex: number) => {
|
||||
if (isDynamic && dynamicInfo) {
|
||||
// Here, you would update the component's state or context that controls the current time point index
|
||||
// For demonstration, assuming a hypothetical function that updates the time point index
|
||||
updateDynamicInfo({
|
||||
...dynamicInfo,
|
||||
timePointIndex: newIndex,
|
||||
});
|
||||
}
|
||||
},
|
||||
[isDynamic, dynamicInfo]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
'border-secondary-light/60 bg-primary-dark flex select-none items-center gap-2 rounded border px-2 py-2'
|
||||
<div className={className}>
|
||||
{isDynamic && dynamicInfo && (
|
||||
<InputRange
|
||||
value={dynamicInfo.timePointIndex}
|
||||
onChange={handleTimePointChange}
|
||||
minValue={0}
|
||||
maxValue={dynamicInfo.numTimePoints - 1}
|
||||
step={1}
|
||||
containerClassName="mb-3 w-full"
|
||||
labelClassName="text-xs text-white"
|
||||
leftColor="#3a3f99"
|
||||
rightColor="#3a3f99"
|
||||
trackHeight="4px"
|
||||
thumbColor="#348cfd"
|
||||
thumbColorOuter="#000000"
|
||||
showLabel={false}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
name={getPlayPauseIconName()}
|
||||
className="active:text-primary-light hover:bg-customblue-300 cursor-pointer text-white hover:rounded"
|
||||
onClick={() => onPlayPauseChange(!isPlaying)}
|
||||
/>
|
||||
<Tooltip
|
||||
position="top"
|
||||
className="group/fps cine-fps-range-tooltip"
|
||||
tight={true}
|
||||
content={
|
||||
<InputRange
|
||||
containerClassName="h-9 px-2"
|
||||
inputClassName="w-40"
|
||||
value={frameRate}
|
||||
minValue={minFrameRate}
|
||||
maxValue={maxFrameRate}
|
||||
step={stepFrameRate}
|
||||
onChange={handleSetFrameRate}
|
||||
showLabel={false}
|
||||
/>
|
||||
<div
|
||||
className={
|
||||
'border-secondary-light/60 bg-primary-dark inline-flex select-none items-center gap-2 rounded border px-2 py-2'
|
||||
}
|
||||
>
|
||||
<div className="border-secondary-light flex h-6 items-stretch gap-1 rounded border">
|
||||
<Icon
|
||||
name={getPlayPauseIconName()}
|
||||
className="active:text-primary-light hover:bg-customblue-300 cursor-pointer text-white hover:rounded"
|
||||
onClick={() => onPlayPauseChange(!isPlaying)}
|
||||
/>
|
||||
{isDynamic && dynamicInfo && (
|
||||
<div className="min-w-16 max-w-44 flex flex-col text-white">
|
||||
{/* Add Tailwind classes for monospace font and center alignment */}
|
||||
<div className="text-[11px]">
|
||||
<span className="w-2 text-white">{dynamicInfo.timePointIndex}</span>{' '}
|
||||
<span className="text-aqua-pale">{`/${dynamicInfo.numTimePoints}`}</span>
|
||||
</div>
|
||||
<div className="text-aqua-pale text-xs">{dynamicInfo.label}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-secondary-light ml-4 flex h-6 items-stretch gap-1 rounded border">
|
||||
<div
|
||||
className={`${fpsButtonClassNames} rounded-l`}
|
||||
onClick={() => handleSetFrameRate(frameRate - 1)}
|
||||
>
|
||||
<Icon name="arrow-left-small" />
|
||||
</div>
|
||||
<div className="group-hover/fps:text-primary-light w-11 text-center text-sm leading-[22px] text-white">
|
||||
{`${frameRate} FPS`}
|
||||
</div>
|
||||
<Tooltip
|
||||
position="top"
|
||||
className="group/fps cine-fps-range-tooltip"
|
||||
tight={true}
|
||||
content={
|
||||
<InputRange
|
||||
containerClassName="h-9 px-2"
|
||||
inputClassName="w-40"
|
||||
value={frameRate}
|
||||
minValue={minFrameRate}
|
||||
maxValue={maxFrameRate}
|
||||
step={stepFrameRate}
|
||||
onChange={handleSetFrameRate}
|
||||
showLabel={false}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<div className="flex-shrink-0 text-center text-sm leading-[22px] text-white">
|
||||
<span className="inline-block text-right">{`${frameRate} `}</span>
|
||||
<span className="text-aqua-pale whitespace-nowrap text-xs">{' FPS'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<div
|
||||
className={`${fpsButtonClassNames} rounded-r`}
|
||||
onClick={() => handleSetFrameRate(frameRate + 1)}
|
||||
@@ -98,12 +153,12 @@ const CinePlayer: React.FC<CinePlayerProps> = ({
|
||||
<Icon name="arrow-right-small" />
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Icon
|
||||
name="icon-close"
|
||||
className="text-primary-active active:text-primary-light hover:bg-customblue-300 cursor-pointer hover:rounded"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<Icon
|
||||
name="icon-close"
|
||||
className="text-primary-active active:text-primary-light hover:bg-customblue-300 cursor-pointer hover:rounded"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -119,6 +174,8 @@ CinePlayer.defaultProps = {
|
||||
onPlayPauseChange: noop,
|
||||
onFrameRateChange: noop,
|
||||
onClose: noop,
|
||||
isDynamic: false,
|
||||
dynamicInfo: {},
|
||||
};
|
||||
|
||||
CinePlayer.propTypes = {
|
||||
@@ -134,6 +191,12 @@ CinePlayer.propTypes = {
|
||||
onPlayPauseChange: PropTypes.func,
|
||||
onFrameRateChange: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
isDynamic: PropTypes.bool,
|
||||
dynamicInfo: PropTypes.shape({
|
||||
timePointIndex: PropTypes.number,
|
||||
numTimePoints: PropTypes.number,
|
||||
label: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
export default CinePlayer;
|
||||
@@ -0,0 +1,57 @@
|
||||
import { CinePlayer } from '../../../components';
|
||||
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
|
||||
|
||||
export const argTypes = {
|
||||
component: CinePlayer,
|
||||
title: 'Components/CinePlayer',
|
||||
};
|
||||
|
||||
<Meta
|
||||
title="Components/CinePlayer"
|
||||
component={CinePlayer}
|
||||
/>
|
||||
|
||||
export const CinePlayerTemplate = args => (
|
||||
<div className="relative h-96 w-96 bg-black">
|
||||
<div className="absolute left-1/2 bottom-3 -translate-x-1/2">
|
||||
<CinePlayer {...args} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
<Heading
|
||||
title="CinePlayer"
|
||||
componentRelativePath="CinePlayer/CinePlayer.tsx"
|
||||
/>
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Props](#props)
|
||||
- [Contribute](#contribute)
|
||||
|
||||
## Overview
|
||||
|
||||
CinePlayer is a component that allows you to use as a boolean value
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Overview"
|
||||
args={{
|
||||
isDynamic: true,
|
||||
dynamicInfo: {
|
||||
timePointIndex: 5,
|
||||
numTimePoints: 20,
|
||||
label: 'TemporalPositionIdentifier',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{CinePlayerTemplate.bind({})}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
<ArgsTable of={CinePlayer} />
|
||||
|
||||
## Contribute
|
||||
|
||||
<Footer componentRelativePath="CinePlayer/__stories__/CinePlayer.stories.mdx" />
|
||||
@@ -20,6 +20,8 @@ function Header({
|
||||
WhiteLabeling,
|
||||
showPatientInfo = PatientInfoVisibility.VISIBLE_COLLAPSED,
|
||||
servicesManager,
|
||||
Secondary,
|
||||
appConfig,
|
||||
...props
|
||||
}): ReactNode {
|
||||
const { t } = useTranslation('Header');
|
||||
@@ -58,14 +60,17 @@ function Header({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="absolute top-1/2 left-[211px] h-8 w-20 -translate-y-1/2">{future left component}</div> */}
|
||||
<div className="absolute top-1/2 left-[250px] h-8 -translate-y-1/2">{Secondary}</div>
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform">
|
||||
<div className="flex items-center justify-center space-x-2">{children}</div>
|
||||
</div>
|
||||
<div className="absolute right-0 top-1/2 flex -translate-y-1/2 select-none items-center">
|
||||
{(showPatientInfo === PatientInfoVisibility.VISIBLE ||
|
||||
showPatientInfo === PatientInfoVisibility.VISIBLE_COLLAPSED) && (
|
||||
<HeaderPatientInfo servicesManager={servicesManager} />
|
||||
<HeaderPatientInfo
|
||||
servicesManager={servicesManager}
|
||||
appConfig={appConfig}
|
||||
/>
|
||||
)}
|
||||
<div className="border-primary-dark mx-1.5 h-[25px] border-r"></div>
|
||||
<div className="flex-shrink-0">
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon } from '@ohif/ui';
|
||||
import { utils } from '@ohif/core';
|
||||
import { useAppConfig } from '@state';
|
||||
|
||||
const { formatDate, formatPN } = utils;
|
||||
|
||||
@@ -70,8 +69,7 @@ function usePatientInfo(servicesManager) {
|
||||
return { patientInfo, isMixedPatients };
|
||||
}
|
||||
|
||||
function HeaderPatientInfo({ servicesManager }) {
|
||||
const [appConfig] = useAppConfig();
|
||||
function HeaderPatientInfo({ servicesManager, appConfig }) {
|
||||
const initialExpandedState = appConfig.showPatientInfo === 'visible';
|
||||
const [expanded, setExpanded] = useState(initialExpandedState);
|
||||
const { patientInfo, isMixedPatients } = usePatientInfo(servicesManager);
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
|
||||
import arrowDown from './../../assets/icons/arrow-down.svg';
|
||||
import arrowLeft from './../../assets/icons/arrow-left.svg';
|
||||
import arrowRight from './../../assets/icons/arrow-right.svg';
|
||||
import arrowLeftSmall from './../../assets/icons/arrow-left-small.svg';
|
||||
import arrowRightSmall from './../../assets/icons/arrow-right-small.svg';
|
||||
import calendar from './../../assets/icons/calendar.svg';
|
||||
@@ -119,6 +120,11 @@ import toolCalibration from './../../assets/icons/tool-calibrate.svg';
|
||||
import toolFreehand from './../../assets/icons/tool-freehand.svg';
|
||||
import toolFreehandPolygon from './../../assets/icons/tool-freehand-polygon.svg';
|
||||
import toolPolygon from './../../assets/icons/tool-polygon.svg';
|
||||
import toolBrush from './../../assets/icons/tool-brush.svg';
|
||||
import toolEraser from './../../assets/icons/tool-eraser.svg';
|
||||
import toolScissorRect from './../../assets/icons/tool-scissor-rect.svg';
|
||||
import toolScissorCircle from './../../assets/icons/tool-scissor-circle.svg';
|
||||
import toolPaintFill from './../../assets/icons/tool-paint-fill.svg';
|
||||
import editPatient from './../../assets/icons/edit-patient.svg';
|
||||
import panelGroupMore from './../../assets/icons/panel-group-more.svg';
|
||||
import panelGroupOpenClose from './../../assets/icons/panel-group-open-close.svg';
|
||||
@@ -216,6 +222,7 @@ import investigationalUse from './../../assets/icons/illustration-investigationa
|
||||
const ICONS = {
|
||||
'arrow-down': arrowDown,
|
||||
'arrow-left': arrowLeft,
|
||||
'arrow-right': arrowRight,
|
||||
'arrow-left-small': arrowLeftSmall,
|
||||
'arrow-right-small': arrowRightSmall,
|
||||
calendar: calendar,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { InputNumber } from '../../components'; // Import InputNumber component
|
||||
import './InputDoubleRange.css';
|
||||
@@ -19,6 +19,7 @@ type InputDoubleRangeProps = {
|
||||
trackColor?: string;
|
||||
allowNumberEdit?: boolean;
|
||||
showAdjustmentArrows?: boolean;
|
||||
allowOutOfRange?: boolean;
|
||||
};
|
||||
|
||||
const InputDoubleRange: React.FC<InputDoubleRangeProps> = ({
|
||||
@@ -36,6 +37,7 @@ const InputDoubleRange: React.FC<InputDoubleRangeProps> = ({
|
||||
labelPosition,
|
||||
trackColor,
|
||||
allowNumberEdit,
|
||||
allowOutOfRange = false,
|
||||
showAdjustmentArrows,
|
||||
}) => {
|
||||
// Set initial thumb positions as percentages
|
||||
@@ -55,7 +57,15 @@ const InputDoubleRange: React.FC<InputDoubleRangeProps> = ({
|
||||
updatedRangeValue[index] = newValues;
|
||||
}
|
||||
|
||||
const calculatePercentage = value => ((value - minValue) / (maxValue - minValue)) * 100;
|
||||
const calculatePercentage = value => {
|
||||
if (value < minValue) {
|
||||
return 0;
|
||||
}
|
||||
if (value > maxValue) {
|
||||
return 100;
|
||||
}
|
||||
return ((value - minValue) / (maxValue - minValue)) * 100;
|
||||
};
|
||||
|
||||
const newPercentageStart = calculatePercentage(updatedRangeValue[0]);
|
||||
const newPercentageEnd = calculatePercentage(updatedRangeValue[1]);
|
||||
@@ -73,17 +83,22 @@ const InputDoubleRange: React.FC<InputDoubleRangeProps> = ({
|
||||
|
||||
const LabelOrEditableNumber = (val, index) => {
|
||||
return allowNumberEdit ? (
|
||||
<InputNumber
|
||||
minValue={minValue}
|
||||
maxValue={maxValue}
|
||||
value={val}
|
||||
onChange={newValue => {
|
||||
updateRangeValues(newValue, index);
|
||||
}}
|
||||
step={step}
|
||||
labelClassName="text-white"
|
||||
showAdjustmentArrows={showAdjustmentArrows}
|
||||
/>
|
||||
// the pl-[2px] class is used to align the thumb so that it doesn't
|
||||
// go over the label when the value is full, not sure what is wrong
|
||||
// with the implementation, we need to fix it properly
|
||||
<div className={index === 1 && 'pl-[2px]'}>
|
||||
<InputNumber
|
||||
minValue={minValue}
|
||||
maxValue={maxValue}
|
||||
value={val}
|
||||
onChange={newValue => {
|
||||
updateRangeValues(newValue, index);
|
||||
}}
|
||||
step={step}
|
||||
labelClassName={classNames(labelClassName ?? 'text-white')}
|
||||
showAdjustmentArrows={showAdjustmentArrows}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span className={classNames(labelClassName ?? 'text-white')}>
|
||||
{val}
|
||||
@@ -138,31 +153,52 @@ const InputDoubleRange: React.FC<InputDoubleRangeProps> = ({
|
||||
const newValue =
|
||||
Math.round(((x / rect.width) * (maxValue - minValue) + minValue) / step) * step;
|
||||
|
||||
// Make sure newValue is within [minValue, maxValue]
|
||||
const clampedValue = Math.min(Math.max(newValue, minValue), maxValue);
|
||||
if (!allowOutOfRange) {
|
||||
const clampedValue = Math.min(Math.max(newValue, minValue), maxValue);
|
||||
|
||||
// Ensure that left and right thumbs don't switch positions
|
||||
if (selectedThumbValue === 0 && clampedValue >= rangeValue[1]) {
|
||||
return;
|
||||
}
|
||||
if (selectedThumbValue === 1 && clampedValue <= rangeValue[0]) {
|
||||
return;
|
||||
const updatedRangeValue = [...rangeValue];
|
||||
updatedRangeValue[selectedThumbValue] = clampedValue;
|
||||
setRangeValue(updatedRangeValue);
|
||||
|
||||
onChange(updatedRangeValue);
|
||||
|
||||
const percentage = Math.round(((clampedValue - minValue) / (maxValue - minValue)) * 100);
|
||||
if (selectedThumbValue === 0) {
|
||||
setPercentageStart(percentage);
|
||||
} else {
|
||||
setPercentageEnd(percentage);
|
||||
}
|
||||
} else {
|
||||
const updatedRangeValue = [...rangeValue];
|
||||
updatedRangeValue[selectedThumbValue] = newValue;
|
||||
setRangeValue(updatedRangeValue);
|
||||
|
||||
onChange(updatedRangeValue);
|
||||
|
||||
// Update the thumb position
|
||||
const percentage = Math.round(((newValue - minValue) / (maxValue - minValue)) * 100);
|
||||
if (percentage < 0) {
|
||||
if (selectedThumbValue === 0) {
|
||||
setPercentageStart(0);
|
||||
} else {
|
||||
setPercentageEnd(0);
|
||||
}
|
||||
} else if (percentage > 100) {
|
||||
if (selectedThumbValue === 0) {
|
||||
setPercentageStart(100);
|
||||
} else {
|
||||
setPercentageEnd(100);
|
||||
}
|
||||
} else {
|
||||
if (selectedThumbValue === 0) {
|
||||
setPercentageStart(percentage);
|
||||
} else {
|
||||
setPercentageEnd(percentage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the correct values in the rangeValue array
|
||||
const updatedRangeValue = [...rangeValue];
|
||||
updatedRangeValue[selectedThumbValue] = clampedValue;
|
||||
setRangeValue(updatedRangeValue);
|
||||
|
||||
onChange(updatedRangeValue);
|
||||
|
||||
// Update the thumb position
|
||||
const percentage = Math.round(((clampedValue - minValue) / (maxValue - minValue)) * 100);
|
||||
if (selectedThumbValue === 0) {
|
||||
setPercentageStart(percentage);
|
||||
} else {
|
||||
setPercentageEnd(percentage);
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate the range values percentages for gradient background
|
||||
|
||||
@@ -5,6 +5,9 @@ import './InputNumber.css';
|
||||
import Label from '../Label';
|
||||
import getMaxDigits from '../../utils/getMaxDigits';
|
||||
|
||||
const arrowHorizontalClassName =
|
||||
'cursor-pointer text-primary-active active:text-primary-light hover:opacity-70 w-4 flex items-center justify-center';
|
||||
|
||||
/**
|
||||
* React Number Input component'
|
||||
* it has two props, value and onChange
|
||||
@@ -16,6 +19,7 @@ import getMaxDigits from '../../utils/getMaxDigits';
|
||||
|
||||
const sizesClasses = {
|
||||
sm: 'w-[45px] h-[28px]',
|
||||
md: 'w-[58px] h-[28px]',
|
||||
lg: 'w-[206px] h-[35px]',
|
||||
};
|
||||
|
||||
@@ -25,11 +29,16 @@ const InputNumber: React.FC<{
|
||||
minValue?: number;
|
||||
maxValue?: number;
|
||||
step?: number;
|
||||
size?: 'sm' | 'lg';
|
||||
size?: 'sm' | 'lg' | 'md';
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
label?: string;
|
||||
showAdjustmentArrows?: boolean;
|
||||
arrowsDirection: 'vertical' | 'horizontal';
|
||||
labelPosition?: 'left' | 'bottom' | 'right' | 'top';
|
||||
inputClassName?: string;
|
||||
sizeClassName?: string;
|
||||
inputContainerClassName?: string;
|
||||
}> = ({
|
||||
value,
|
||||
onChange,
|
||||
@@ -38,19 +47,24 @@ const InputNumber: React.FC<{
|
||||
size = 'sm',
|
||||
minValue = 0,
|
||||
maxValue = 100,
|
||||
labelClassName,
|
||||
labelClassName = 'text-aqua-pale text-[11px] mx-auto',
|
||||
label,
|
||||
showAdjustmentArrows = true,
|
||||
arrowsDirection = 'vertical',
|
||||
labelPosition = 'left',
|
||||
inputClassName = 'text-white bg-primary-dark text-[14px]',
|
||||
sizeClassName,
|
||||
inputContainerClassName = 'bg-primary-dark border-secondary-light border rounded-[4px]',
|
||||
}) => {
|
||||
const [numberValue, setNumberValue] = useState(value);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const maxDigits = getMaxDigits(maxValue, step);
|
||||
const inputWidth = Math.max(maxDigits * 10, showAdjustmentArrows ? 20 : 28);
|
||||
const arrowWidth = showAdjustmentArrows ? 20 : 0;
|
||||
const containerWidth = `${inputWidth + arrowWidth}px`;
|
||||
const decimalPlaces = Number.isInteger(step) ? 0 : step.toString().split('.')[1].length;
|
||||
|
||||
const sizeToUse = sizeClassName ? sizeClassName : sizesClasses[size];
|
||||
|
||||
useEffect(() => {
|
||||
setNumberValue(value);
|
||||
}, [value]);
|
||||
@@ -95,21 +109,31 @@ const InputNumber: React.FC<{
|
||||
const increment = () => updateValue(parseFloat(numberValue) + step);
|
||||
const decrement = () => updateValue(parseFloat(numberValue) - step);
|
||||
|
||||
const labelElement = label && (
|
||||
<Label
|
||||
className={labelClassName}
|
||||
text={label}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
{label && (
|
||||
<Label
|
||||
className={labelClassName}
|
||||
text={label}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`flex overflow-hidden ${className} ${labelPosition === 'top' || labelPosition === 'bottom' ? 'flex-col' : 'flex-row'}`}
|
||||
>
|
||||
{label && labelPosition === 'left' && labelElement}
|
||||
{label && labelPosition === 'top' && labelElement}
|
||||
<div
|
||||
className={`border-secondary-light flex items-center justify-center overflow-hidden rounded-md border-2 bg-black px-1 ${
|
||||
sizesClasses[size]
|
||||
} ${className || ''}`}
|
||||
style={{ width: containerWidth }}
|
||||
className={`flex flex-grow items-center overflow-hidden ${sizeToUse} ${inputContainerClassName}`}
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="flex w-full">
|
||||
{showAdjustmentArrows && arrowsDirection === 'horizontal' && (
|
||||
<div
|
||||
className={arrowHorizontalClassName}
|
||||
onClick={() => decrement()}
|
||||
>
|
||||
<Icon name="arrow-left-small" />
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
type="number"
|
||||
value={isFocused ? numberValue : parseFloat(numberValue).toFixed(decimalPlaces)}
|
||||
@@ -117,11 +141,19 @@ const InputNumber: React.FC<{
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
className={'input-number w-full bg-black text-center text-[12px] text-white'}
|
||||
className={`input-number ${inputClassName} w-full flex-grow text-center`}
|
||||
style={{ width: inputWidth }}
|
||||
/>
|
||||
{showAdjustmentArrows && (
|
||||
<div className="up-arrowsize flex flex-col items-center justify-around">
|
||||
{showAdjustmentArrows && arrowsDirection === 'horizontal' && (
|
||||
<div
|
||||
className={arrowHorizontalClassName}
|
||||
onClick={() => increment()}
|
||||
>
|
||||
<Icon name="arrow-right-small" />
|
||||
</div>
|
||||
)}
|
||||
{showAdjustmentArrows && arrowsDirection === 'vertical' && (
|
||||
<div className="up-arrowsize ml-auto flex flex-shrink-0 flex-col items-center justify-around pr-1">
|
||||
<ArrowButton
|
||||
onClick={increment}
|
||||
rotate
|
||||
@@ -131,6 +163,8 @@ const InputNumber: React.FC<{
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{label && labelPosition === 'right' && labelElement}
|
||||
{label && labelPosition === 'bottom' && labelElement}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,17 +4,21 @@ input[type='range'] {
|
||||
|
||||
input[type='range']::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
@apply border-primary-dark bg-primary-light border-[2px] border-solid;
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--thumb-inner-color);
|
||||
border: 2px solid var(--thumb-outer-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type='range']::-moz-range-thumb {
|
||||
@apply border-primary-dark bg-primary-light border-[2px] border-solid;
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--thumb-inner-color);
|
||||
border: 2px solid var(--thumb-outer-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type='range']::-webkit-slider-runnable-track {
|
||||
@@ -31,3 +35,11 @@ input[type='range']::-moz-range-track {
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
input[type='range']::-webkit-slider-thumb {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
input[type='range']::-moz-range-thumb {
|
||||
pointer-events: all;
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import getMaxDigits from '../../utils/getMaxDigits';
|
||||
* value is a number value
|
||||
* onChange is a function that will be called when the range input is changed
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
type Label = {
|
||||
@@ -31,9 +30,13 @@ type InputRangeProps = {
|
||||
labelVariant?: string;
|
||||
showLabel?: boolean;
|
||||
labelPosition?: string;
|
||||
trackColor?: string;
|
||||
leftColor?: string;
|
||||
rightColor?: string;
|
||||
thumbColor?: string;
|
||||
thumbColorOuter?: string;
|
||||
allowNumberEdit?: boolean;
|
||||
showAdjustmentArrows?: boolean;
|
||||
trackHeight?: string;
|
||||
labels?: Label[];
|
||||
};
|
||||
|
||||
@@ -47,16 +50,18 @@ const InputRange: React.FC<InputRangeProps> = ({
|
||||
containerClassName,
|
||||
inputClassName,
|
||||
labelClassName,
|
||||
labelVariant,
|
||||
showLabel = true,
|
||||
labelPosition = 'right',
|
||||
trackColor,
|
||||
leftColor = '#5acce6',
|
||||
rightColor = '#3a3f99',
|
||||
thumbColor = '#5acce6',
|
||||
thumbColorOuter = '#090c29',
|
||||
trackHeight = '3px',
|
||||
allowNumberEdit = false,
|
||||
showAdjustmentArrows = true,
|
||||
labels = [],
|
||||
}) => {
|
||||
const [rangeValue, setRangeValue] = useState(value);
|
||||
|
||||
const maxDigits = getMaxDigits(maxValue, step);
|
||||
const labelWidth = `${maxDigits * 10}px`;
|
||||
|
||||
@@ -88,8 +93,7 @@ const InputRange: React.FC<InputRangeProps> = ({
|
||||
/>
|
||||
) : (
|
||||
<span className={classNames(labelClassName ?? 'text-white')}>
|
||||
{rangeValue}
|
||||
{unit}
|
||||
{rangeValue.toFixed(maxDigits)} {unit}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -101,19 +105,21 @@ const InputRange: React.FC<InputRangeProps> = ({
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<div className="relative flex w-full items-center space-x-2">
|
||||
<div className={'relative flex w-full items-center ' + (showLabel ? 'space-x-2' : '')}>
|
||||
{showLabel && labelPosition === 'left' && (
|
||||
<div style={{ width: labelWidth }}>{LabelOrEditableNumber}</div>
|
||||
)}
|
||||
<div className="range-track"></div>
|
||||
<input
|
||||
type="range"
|
||||
min={minValue}
|
||||
max={maxValue}
|
||||
value={rangeValue}
|
||||
className={`h-[3px] appearance-none rounded-md ${inputClassName ?? ''}`}
|
||||
className={`w-full appearance-none rounded-md ${inputClassName ?? ''}`}
|
||||
style={{
|
||||
background: `linear-gradient(to right, #5acce6 0%, #5acce6 ${rangeValuePercentage}%, #3a3f99 ${rangeValuePercentage}%, #3a3f99 100%)`,
|
||||
background: `linear-gradient(to right, ${leftColor} 0%, ${leftColor} ${rangeValuePercentage}%, ${rightColor} ${rangeValuePercentage}%, ${rightColor} 100%)`,
|
||||
'--thumb-inner-color': thumbColor,
|
||||
'--thumb-outer-color': thumbColorOuter,
|
||||
height: trackHeight,
|
||||
}}
|
||||
onChange={handleChange}
|
||||
id="myRange"
|
||||
|
||||
@@ -34,7 +34,7 @@ InputRange is a component that allows you to use as a boolean value
|
||||
<Story
|
||||
name="Overview"
|
||||
args={{
|
||||
value: 10,
|
||||
value: 20,
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
step: 10,
|
||||
|
||||
@@ -67,7 +67,7 @@ const classes = {
|
||||
Content: ({ isExpanded }) => classNames(baseClasses.Content, isExpanded ? 'block' : 'hidden'),
|
||||
};
|
||||
|
||||
const SplitButton = ({
|
||||
const LegacySplitButton = ({
|
||||
groupId,
|
||||
primary,
|
||||
secondary,
|
||||
@@ -103,7 +103,7 @@ const SplitButton = ({
|
||||
disabled={!state.isExpanded}
|
||||
>
|
||||
<div
|
||||
id="SplitButton"
|
||||
id="LegacySplitButton"
|
||||
className="relative"
|
||||
>
|
||||
<div
|
||||
@@ -148,7 +148,7 @@ const SplitButton = ({
|
||||
);
|
||||
};
|
||||
|
||||
SplitButton.propTypes = {
|
||||
LegacySplitButton.propTypes = {
|
||||
isToggle: PropTypes.bool,
|
||||
groupId: PropTypes.string.isRequired,
|
||||
primary: PropTypes.object.isRequired,
|
||||
@@ -160,11 +160,11 @@ SplitButton.propTypes = {
|
||||
Component: PropTypes.elementType,
|
||||
};
|
||||
|
||||
SplitButton.defaultProps = {
|
||||
LegacySplitButton.defaultProps = {
|
||||
isToggle: false,
|
||||
renderer: null,
|
||||
isActive: false,
|
||||
Component: null,
|
||||
};
|
||||
|
||||
export default SplitButton;
|
||||
export default LegacySplitButton;
|
||||
@@ -0,0 +1,40 @@
|
||||
.LineChart svg {
|
||||
color: #ddd;
|
||||
border: solid 1px #383f42;
|
||||
}
|
||||
|
||||
.LineChart svg .background {
|
||||
stroke: none;
|
||||
fill: #151c20;
|
||||
}
|
||||
|
||||
.LineChart svg .axis.label {
|
||||
fill: #ddd;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.LineChart svg .axis .tick line {
|
||||
stroke: #ddd;
|
||||
}
|
||||
|
||||
.LineChart svg .axis .tick text {
|
||||
fill: #ddd;
|
||||
}
|
||||
|
||||
.LineChart svg .axis.grid .domain {
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.LineChart svg .axis.grid .tick line {
|
||||
stroke: #383f42;
|
||||
}
|
||||
|
||||
.LineChart svg .line {
|
||||
stroke-width: 1px;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.LineChart svg .legend .legend-background {
|
||||
fill: none;
|
||||
stroke: #383f42;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import React, { useEffect, useLayoutEffect, useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import * as d3Selection from 'd3-selection';
|
||||
import { lineChart } from './d3LineChart';
|
||||
import './LineChart.css';
|
||||
|
||||
const LineChart = ({
|
||||
width: widthProp,
|
||||
height: heightProp,
|
||||
axis,
|
||||
series,
|
||||
showAxisLabels,
|
||||
showAxisGrid,
|
||||
showLegend,
|
||||
legendWidth,
|
||||
transparentChartBackground,
|
||||
containerClassName,
|
||||
chartContainerClassName,
|
||||
}: {
|
||||
title: string;
|
||||
width: number;
|
||||
height: number;
|
||||
showAxisGrid: boolean;
|
||||
showAxisLabels: boolean;
|
||||
showLegend: boolean;
|
||||
legendWidth: number;
|
||||
transparentChartBackground: boolean;
|
||||
containerClassName: string;
|
||||
chartContainerClassName: string;
|
||||
}): JSX.Element => {
|
||||
const chartContainerRef = useRef(null);
|
||||
const [d3SVGContainer, setD3SVGRef] = useState(null);
|
||||
const [width, setWidth] = useState(0);
|
||||
const [height, setHeight] = useState(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const chartContainer = chartContainerRef.current;
|
||||
const containerWidth = chartContainer.offsetWidth;
|
||||
const containerHeight = chartContainer.offsetHeight;
|
||||
const d3Container = d3Selection
|
||||
.create('svg')
|
||||
.attr('viewBox', [0, 0, containerWidth, containerHeight])
|
||||
.style('max-width', `${containerWidth}px`)
|
||||
.style('overflow', 'visible');
|
||||
|
||||
chartContainer.append(d3Container.node());
|
||||
|
||||
setD3SVGRef(d3Container);
|
||||
setWidth(containerWidth);
|
||||
setHeight(containerHeight);
|
||||
}, [chartContainerRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!d3SVGContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
lineChart.addLineChartNode({
|
||||
d3SVGRef: d3SVGContainer,
|
||||
axis,
|
||||
series,
|
||||
width,
|
||||
height,
|
||||
showAxisLabels,
|
||||
showAxisGrid,
|
||||
showLegend,
|
||||
legendWidth,
|
||||
transparentChartBackground,
|
||||
});
|
||||
}, [
|
||||
d3SVGContainer,
|
||||
axis,
|
||||
series,
|
||||
width,
|
||||
height,
|
||||
showAxisLabels,
|
||||
showAxisGrid,
|
||||
transparentChartBackground,
|
||||
showLegend,
|
||||
legendWidth,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
'LineChart text-white',
|
||||
{
|
||||
[`w-[${widthProp}px]`]: !!widthProp,
|
||||
[`h-[${heightProp}px]`]: !!heightProp,
|
||||
},
|
||||
{
|
||||
'w-full': !widthProp,
|
||||
'h-full': !heightProp,
|
||||
},
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
<div
|
||||
id="chartContainer"
|
||||
ref={chartContainerRef}
|
||||
className={classnames('h-full w-full', chartContainerClassName)}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LineChart.defaultProps = {
|
||||
showAxisLabels: true,
|
||||
showAxisGrid: true,
|
||||
showLegend: false,
|
||||
legendWidth: 120,
|
||||
transparentChartBackground: false,
|
||||
};
|
||||
|
||||
LineChart.propTypes = {
|
||||
title: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
showAxisLabels: PropTypes.bool,
|
||||
showAxisGrid: PropTypes.bool,
|
||||
showLegend: PropTypes.bool,
|
||||
legendWidth: PropTypes.number,
|
||||
transparentChartBackground: PropTypes.bool,
|
||||
containerClassName: PropTypes.string,
|
||||
chartContainerClassName: PropTypes.string,
|
||||
};
|
||||
|
||||
export default LineChart;
|
||||
@@ -0,0 +1,181 @@
|
||||
import LineChart from '../LineChart';
|
||||
|
||||
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
|
||||
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
|
||||
|
||||
export const argTypes = {
|
||||
component: LineChart,
|
||||
title: 'Components/LineChart',
|
||||
};
|
||||
|
||||
<Meta
|
||||
title="Components/LineChart"
|
||||
component={LineChart}
|
||||
/>
|
||||
|
||||
export const LineChartTemplate = args => (
|
||||
<div className="w-96">
|
||||
<LineChart {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
<Heading
|
||||
title="LineChart"
|
||||
componentRelativePath="LineChart/LineChart.tsx"
|
||||
/>
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Props](#props)
|
||||
- [Contribute](#contribute)
|
||||
|
||||
## Overview
|
||||
|
||||
LineChart is a component that...
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Overview"
|
||||
args={{
|
||||
width: 800,
|
||||
height: 400,
|
||||
showLegend: true,
|
||||
legendWidth: 150,
|
||||
axis: {
|
||||
x: {
|
||||
label: 'Time (s)',
|
||||
indexRef: 0,
|
||||
type: 'x',
|
||||
},
|
||||
y: {
|
||||
label: 'Signal Intensity',
|
||||
indexRef: 1,
|
||||
type: 'y',
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
label: 'First series',
|
||||
points: [
|
||||
[0, 607.54],
|
||||
[1, 648.28],
|
||||
[2, 696.7],
|
||||
[3, 693.87],
|
||||
[4, 740.02],
|
||||
[5, 707.04],
|
||||
[6, 748.73],
|
||||
[7, 792.34],
|
||||
[8, 842.03],
|
||||
[9, 808.63],
|
||||
[10, 798.81],
|
||||
[11, 836.71],
|
||||
[12, 800.22],
|
||||
[13, 761.66],
|
||||
[14, 745.5],
|
||||
[15, 774.53],
|
||||
[16, 824.04],
|
||||
[17, 837.02],
|
||||
[18, 857.5],
|
||||
[19, 818.29],
|
||||
[20, 786.71],
|
||||
[21, 797.34],
|
||||
[22, 795.29],
|
||||
[23, 771.26],
|
||||
[24, 751.51],
|
||||
[25, 800.11],
|
||||
[26, 824.1],
|
||||
[27, 851.25],
|
||||
[28, 826.65],
|
||||
[29, 845.48],
|
||||
[30, 891.25],
|
||||
[31, 899.52],
|
||||
[32, 944.36],
|
||||
[33, 947.73],
|
||||
[34, 923.11],
|
||||
[35, 889.22],
|
||||
[36, 871.86],
|
||||
[37, 871.42],
|
||||
[38, 826.14],
|
||||
[39, 866.41],
|
||||
[40, 897.81],
|
||||
[41, 858.01],
|
||||
[42, 848.76],
|
||||
[43, 818.07],
|
||||
[44, 778.81],
|
||||
[45, 737.75],
|
||||
[46, 719.47],
|
||||
[47, 730.18],
|
||||
[48, 705.91],
|
||||
[49, 689.15],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Second series',
|
||||
points: [
|
||||
[0, 307.54],
|
||||
[1, 348.28],
|
||||
[2, 396.7],
|
||||
[3, 393.87],
|
||||
[4, 440.02],
|
||||
[5, 407.04],
|
||||
[6, 448.73],
|
||||
[7, 492.34],
|
||||
[8, 542.03],
|
||||
[9, 508.63],
|
||||
[10, 498.81],
|
||||
[11, 536.71],
|
||||
[12, 500.22],
|
||||
[13, 461.66],
|
||||
[14, 445.5],
|
||||
[15, 474.53],
|
||||
[16, 524.04],
|
||||
[17, 537.02],
|
||||
[18, 557.5],
|
||||
[19, 518.29],
|
||||
[20, 486.71],
|
||||
[21, 497.34],
|
||||
[22, 495.29],
|
||||
[23, 471.26],
|
||||
[24, 451.51],
|
||||
[25, 500.11],
|
||||
[26, 524.1],
|
||||
[27, 551.25],
|
||||
[28, 526.65],
|
||||
[29, 545.48],
|
||||
[30, 591.25],
|
||||
[31, 599.52],
|
||||
[32, 644.36],
|
||||
[33, 647.73],
|
||||
[34, 623.11],
|
||||
[35, 589.22],
|
||||
[36, 571.86],
|
||||
[37, 571.42],
|
||||
[38, 526.14],
|
||||
[39, 566.41],
|
||||
[40, 597.81],
|
||||
[41, 558.01],
|
||||
[42, 548.76],
|
||||
[43, 518.07],
|
||||
[44, 478.81],
|
||||
[45, 437.75],
|
||||
[46, 419.47],
|
||||
[47, 430.18],
|
||||
[48, 405.91],
|
||||
[49, 389.15],
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
{LineChartTemplate.bind({})}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
<ArgsTable of={LineChart} />
|
||||
|
||||
## Usage
|
||||
|
||||
## Contribute
|
||||
|
||||
<Footer componentRelativePath="LineChart/__stories__/lineChart.stories.mdx" />
|
||||
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* It adds attributes to root element from passed attributes list
|
||||
*
|
||||
* @param {object} root svg element to be changed
|
||||
* @param {object} attributes list of attributes to be added on root element
|
||||
* @modifies {root}
|
||||
*
|
||||
*/
|
||||
const _addAttributes = (root, attributes = []) => {
|
||||
for (const attr of attributes) {
|
||||
const { key, value } = attr;
|
||||
|
||||
if (key && value) {
|
||||
if (key === 'class') {
|
||||
root.classed(value, true);
|
||||
} else {
|
||||
root.attr(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* It formats axis labels. It adds units if existing
|
||||
*
|
||||
* @param {Object} def
|
||||
* @param {string} def.label string representing axis label
|
||||
* @param {string} [def.unit] string representing axis unit
|
||||
* @return {string} formatted label
|
||||
*/
|
||||
const _formatAxisLabel = ({ label, unit }) => {
|
||||
let formattedLabel = label;
|
||||
|
||||
if (unit) {
|
||||
formattedLabel += `(${unit})`;
|
||||
}
|
||||
|
||||
return formattedLabel;
|
||||
};
|
||||
|
||||
/**
|
||||
* It appends chart svg element container to root element
|
||||
*
|
||||
* @param {object} root svg element to be changed
|
||||
* @param {number} width width of container
|
||||
* @param {number} height height of container
|
||||
* @param {number} translateX value to translate on axis x
|
||||
* @param {number} translateY value to translate on axis y
|
||||
* @modifies {root}
|
||||
*
|
||||
* @return {object} appended chart container (svg element)
|
||||
*/
|
||||
const _addChartContainer = (root, width, height, translateX, translateY) => {
|
||||
root.append('rect').attr('class', 'background').attr('width', width).attr('height', height);
|
||||
|
||||
return root
|
||||
.append('g')
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.append('g')
|
||||
.attr('transform', 'translate(' + translateX + ',' + translateY + ')');
|
||||
};
|
||||
|
||||
const _addLegend = (root, width, height, posX, posY) => {
|
||||
const legendContainer = root
|
||||
.append('g')
|
||||
.attr('class', 'legend')
|
||||
.attr('transform', 'translate(' + posX + ',' + posY + ')');
|
||||
|
||||
legendContainer
|
||||
.append('rect')
|
||||
.attr('class', 'legend-background')
|
||||
.attr('width', width)
|
||||
.attr('height', height);
|
||||
|
||||
return legendContainer;
|
||||
};
|
||||
|
||||
const _setLegendLabels = (root, labels, colors, itemHeight, itemWidth, textEllipses) => {
|
||||
const yOffset = itemHeight / 2;
|
||||
const textLeft = 20;
|
||||
|
||||
// Add one dot in the legend for each
|
||||
root
|
||||
.selectAll('labelDots')
|
||||
.data(labels)
|
||||
.enter()
|
||||
.append('circle')
|
||||
.attr('cx', 10)
|
||||
.attr('cy', (_d, i) => yOffset + i * itemHeight)
|
||||
.attr('r', 5)
|
||||
.style('fill', (_d, i) => colors[i]);
|
||||
|
||||
root
|
||||
.selectAll('labelText')
|
||||
.data(labels)
|
||||
.enter()
|
||||
.append('text')
|
||||
.attr('x', textLeft)
|
||||
.attr('y', (_d, i) => yOffset + i * itemHeight)
|
||||
.style('fill', (_d, i) => colors[i])
|
||||
.append('tspan')
|
||||
.style('alignment-baseline', 'middle')
|
||||
.text(d => d)
|
||||
.each(textEllipses(itemWidth - textLeft, 1));
|
||||
};
|
||||
|
||||
/**
|
||||
* It appends axis svg element to root element
|
||||
*
|
||||
* @param {object} root svg element to be changed
|
||||
* @param {TimecoursePointDef} axisDef definition of given axis
|
||||
* @param {number} posX position X to place content
|
||||
* @param {number} posY position Y to place content
|
||||
* @param {number} axisExtraAttrs list of extra attributes to be added on content
|
||||
* @param {function} axisBuilder callback function to create axis graphics
|
||||
* @param {boolean} showAxisLabels flag to display labels or not
|
||||
* @param {number} labelPosX position X to place label content
|
||||
* @param {number} labelPosY position Y to place label content
|
||||
* @param {object} labelExtraAttrs list of extra attributes to be added on label content
|
||||
* @modifies {root}
|
||||
*
|
||||
* @return {object} axis svg element
|
||||
*/
|
||||
const _addAxis = (
|
||||
root,
|
||||
axisDef,
|
||||
posX = 0,
|
||||
posY = 0,
|
||||
axisExtraAttrs = [],
|
||||
axisBuilder,
|
||||
showAxisLabels,
|
||||
labelPosX,
|
||||
labelPosY,
|
||||
labelExtraAttrs = [],
|
||||
showAxisGrid
|
||||
) => {
|
||||
const { type } = axisDef;
|
||||
const showAxisGridClass = showAxisGrid ? 'grid' : '';
|
||||
const translateValue = `${posX}, ${posY}`;
|
||||
const axisContent = root
|
||||
.append('g')
|
||||
.attr('class', `${type} ${showAxisGridClass} axis`)
|
||||
.attr('transform', `translate(${translateValue})`);
|
||||
|
||||
_addAttributes(axisContent, axisExtraAttrs);
|
||||
axisContent.call(axisBuilder());
|
||||
|
||||
if (showAxisLabels) {
|
||||
const label = _formatAxisLabel(axisDef);
|
||||
// text label for the x axis
|
||||
if (label) {
|
||||
const labelContent = root
|
||||
.append('text')
|
||||
.attr('class', 'label axis')
|
||||
.attr('x', labelPosX)
|
||||
.attr('y', labelPosY)
|
||||
.style('text-anchor', 'middle')
|
||||
.text(label);
|
||||
|
||||
_addAttributes(labelContent, labelExtraAttrs);
|
||||
}
|
||||
}
|
||||
|
||||
return axisContent;
|
||||
};
|
||||
|
||||
/**
|
||||
* It scales axis graphics
|
||||
*
|
||||
* @param {object} sourceGraphicsXAxis original d3 graphics to be proccessed (x Axis)
|
||||
* @param {object} sourceGraphicsYAxis original d3 graphics to be proccessed (y Axis)
|
||||
* @param {object} xAxisGenerator d3 X axis generator
|
||||
* @param {object} yAxisGenerator d3 Y axis generator
|
||||
* @param {object} newXAxisScale d3 continuos scale for X Axis
|
||||
* @param {object} newYAxisScale d3 continuos scale for Y Axis
|
||||
* @modifies {sourceGraphicsXAxis|sourceGraphicsYAxis}
|
||||
*/
|
||||
const _scaleAxisGraphics = (
|
||||
sourceGraphicsXAxis,
|
||||
sourceGraphicsYAxis,
|
||||
xAxisGenerator,
|
||||
yAxisGenerator,
|
||||
newXAxisScale,
|
||||
newYAxisScale
|
||||
) => {
|
||||
const newXAxis = xAxisGenerator.scale(newXAxisScale);
|
||||
const newYAxis = yAxisGenerator.scale(newYAxisScale);
|
||||
|
||||
sourceGraphicsXAxis.call(newXAxis);
|
||||
sourceGraphicsYAxis.call(newYAxis);
|
||||
};
|
||||
|
||||
/**
|
||||
* It appends background svg element to root element and also a clip path for chart boundaries
|
||||
*
|
||||
* @param {object} root svg element to be changed
|
||||
* @param {number} width content`s width
|
||||
* @param {number} height content`s height
|
||||
* @param {boolean} transparent background to be transparent or not
|
||||
* @param {number} offset content`s clip offset
|
||||
* @modifies {root}
|
||||
*
|
||||
*/
|
||||
const _addChartClipPath = (root, width, height, transparent = false, offset = 6) => {
|
||||
const translateOffset = -offset / 2;
|
||||
root
|
||||
.append('clipPath')
|
||||
.attr('id', 'clip')
|
||||
.append('rect')
|
||||
.attr('transform', `translate(${translateOffset}, ${translateOffset})`)
|
||||
.attr('width', width + offset)
|
||||
.attr('height', height + offset);
|
||||
|
||||
root
|
||||
.append('rect')
|
||||
.attr('class', 'background')
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.classed('transparent', transparent);
|
||||
};
|
||||
|
||||
const _addSeries = (root, seriesIndex, style) => {
|
||||
return root
|
||||
.append('g')
|
||||
.attr('id', `series_${seriesIndex}`)
|
||||
.attr('class', 'series')
|
||||
.attr('stroke', style.color || '#ffffff');
|
||||
};
|
||||
|
||||
/**
|
||||
* It appends line chart svg element to root element
|
||||
*
|
||||
* @param {object} root svg element to be changed
|
||||
* @param {object} dataset dataset to tie to content
|
||||
* @param {string} dAttrValue path d attribute
|
||||
* @modifies {root}
|
||||
*
|
||||
* @return {object} svg line element line
|
||||
*/
|
||||
const _addLine = (root, dataset, dAttrValue) => {
|
||||
return root
|
||||
.append('path')
|
||||
.attr('clip-path', 'url(#clip)')
|
||||
.datum(dataset)
|
||||
.attr('class', 'line')
|
||||
.attr('d', dAttrValue);
|
||||
};
|
||||
|
||||
/**
|
||||
* It updates line chart svg element with new dataset and dAttrValue value
|
||||
*
|
||||
* @param {object} root svg element to be changed
|
||||
* @param {object} dataset dataset to tie to content
|
||||
* @param {string} dAttrValue path d attribute
|
||||
* @modifies {root}
|
||||
*
|
||||
*/
|
||||
const _updateLine = (root, dataset = [], dAttrValue) => {
|
||||
const line = root.select('.line');
|
||||
|
||||
if (dataset && dataset.length > 0) {
|
||||
line.datum(dataset);
|
||||
}
|
||||
|
||||
line.attr('d', dAttrValue);
|
||||
};
|
||||
|
||||
/**
|
||||
* It appends points svg element to root element
|
||||
*
|
||||
* @param {object} root svg element to be changed
|
||||
* @param {object} dataset dataset to tie to content
|
||||
* @param {number} cxAttrValue cx attribute
|
||||
* @param {number} cyAttrValue cy attribute
|
||||
* @modifies {root}
|
||||
*
|
||||
* @return {object} points svg element
|
||||
*
|
||||
*/
|
||||
const _addPoints = (root, dataset, cxAttrValue, cyAttrValue) => {
|
||||
return root
|
||||
.selectAll('.dot')
|
||||
.data(dataset)
|
||||
.enter()
|
||||
.append('circle')
|
||||
.attr('id', (point, index) => {
|
||||
return `point-${index}`;
|
||||
})
|
||||
.attr('class', 'dot')
|
||||
.attr('clip-path', 'url(#clip)')
|
||||
.attr('cx', cxAttrValue)
|
||||
.attr('cy', cyAttrValue)
|
||||
.attr('r', 2);
|
||||
};
|
||||
|
||||
/**
|
||||
* It return points from root parent
|
||||
*
|
||||
* @param {object} root svg element to be changed
|
||||
* @return {object} points svg element
|
||||
*
|
||||
*/
|
||||
const _getPoints = root => {
|
||||
return root.selectAll('.dot');
|
||||
};
|
||||
|
||||
/**
|
||||
* It updates points svg element with new dataset, cxAttrValue and cyAttrValue
|
||||
*
|
||||
* @param {object} root svg element to be changed
|
||||
* @param {object} dataset dataset to tie to content
|
||||
* @param {number} cxAttrValue cx attribute
|
||||
* @param {number} cyAttrValue cy attribute
|
||||
* @modifies {root}
|
||||
*
|
||||
*/
|
||||
const _updatePoints = (root, dataset = [], cxAttrValue, cyAttrValue) => {
|
||||
const dot = root.selectAll('.dot');
|
||||
|
||||
dot.data(dataset).attr('cx', cxAttrValue).attr('cy', cyAttrValue);
|
||||
};
|
||||
|
||||
/**
|
||||
* It removes children nodes from root param.
|
||||
*
|
||||
* @param {object} root svg element to remove children content
|
||||
* @modifies {root}
|
||||
*
|
||||
*/
|
||||
const _removeChartContents = root => {
|
||||
root.selectAll('*').remove();
|
||||
};
|
||||
|
||||
const chart = {
|
||||
axis: {
|
||||
addNode: _addAxis,
|
||||
scaleGraphics: _scaleAxisGraphics,
|
||||
},
|
||||
series: {
|
||||
addNode: _addSeries,
|
||||
},
|
||||
lines: {
|
||||
addNode: _addLine,
|
||||
updateNode: _updateLine,
|
||||
},
|
||||
points: {
|
||||
addNode: _addPoints,
|
||||
updateNode: _updatePoints,
|
||||
getPoints: _getPoints,
|
||||
},
|
||||
background: {
|
||||
addNode: _addChartClipPath,
|
||||
},
|
||||
container: {
|
||||
addNode: _addChartContainer,
|
||||
},
|
||||
legend: {
|
||||
addNode: _addLegend,
|
||||
setLabels: _setLegendLabels,
|
||||
},
|
||||
removeContents: _removeChartContents,
|
||||
};
|
||||
|
||||
export default chart;
|
||||
@@ -0,0 +1,149 @@
|
||||
import * as d3Shape from 'd3-shape';
|
||||
import * as d3Zoom from 'd3-zoom';
|
||||
|
||||
import chart from './chart';
|
||||
|
||||
const d3ZoomNativeEvents = [
|
||||
'wheel.zoom',
|
||||
'mousedown.zoom',
|
||||
'dblclick.zoom',
|
||||
'touchstart.zoom',
|
||||
'touchmove.zoom',
|
||||
'touchend.zoom touchcancel.zoom',
|
||||
];
|
||||
/**
|
||||
* Object to override native zoom events.
|
||||
* It defines a specific callback for a event name.
|
||||
*
|
||||
* @type {Object<string, function>} zoomEvents <zoom d3 event name, callback method>
|
||||
*/
|
||||
const zoomEvents = {
|
||||
'dblclick.zoom': (root, zoom) => {
|
||||
_resetZoom(root, zoom);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* It resets given root
|
||||
*
|
||||
* @param {object} root svg element to be reset
|
||||
* @param {object} zoom d3 zoom object
|
||||
*
|
||||
* @modifies {root}
|
||||
*/
|
||||
const _resetZoom = (root, zoom) => {
|
||||
if (root) {
|
||||
root.transition().duration(750).call(zoom.transform, d3Zoom.zoomIdentity);
|
||||
}
|
||||
};
|
||||
|
||||
const DEFAULT_MAX_ZOOM_SCALE = 20;
|
||||
|
||||
const _zoom = d3Zoom.zoom().scaleExtent([1, DEFAULT_MAX_ZOOM_SCALE]);
|
||||
|
||||
const _bindZoom = (
|
||||
root,
|
||||
gX,
|
||||
gY,
|
||||
xAxisScale,
|
||||
yAxisScale,
|
||||
xAxisGenerator,
|
||||
yAxisGenerator,
|
||||
parseXPoint,
|
||||
parseYPoint,
|
||||
datasets
|
||||
) => {
|
||||
_zoom.on('zoom', _zoomListener); // .filter(zoomEventsFilter);
|
||||
|
||||
_removeListeners();
|
||||
_setListeners();
|
||||
|
||||
function _removeListeners() {
|
||||
for (const eventName of d3ZoomNativeEvents) {
|
||||
root.on(eventName, null);
|
||||
}
|
||||
}
|
||||
|
||||
function _setListeners() {
|
||||
const keys = Object.keys(zoomEvents);
|
||||
|
||||
for (const key of keys) {
|
||||
root.call(_zoom).on(key, zoomEvents[key].bind(undefined, root, _zoom));
|
||||
}
|
||||
}
|
||||
|
||||
function _zoomListener(event) {
|
||||
const transformedXAxisScale = event.transform.rescaleX(xAxisScale);
|
||||
const transformedYAxisScale = event.transform.rescaleY(yAxisScale);
|
||||
|
||||
chart.axis.scaleGraphics(
|
||||
gX,
|
||||
gY,
|
||||
xAxisGenerator,
|
||||
yAxisGenerator,
|
||||
transformedXAxisScale,
|
||||
transformedYAxisScale
|
||||
);
|
||||
|
||||
datasets.forEach((dataset, seriesIndex) => {
|
||||
const seriesContainer = root.select(`#series_${seriesIndex}`);
|
||||
// create line
|
||||
const line = d3Shape
|
||||
.line()
|
||||
.x(parseXPoint(transformedXAxisScale))
|
||||
.y(parseYPoint(transformedYAxisScale));
|
||||
|
||||
chart.lines.updateNode(seriesContainer, dataset, line);
|
||||
|
||||
chart.points.updateNode(
|
||||
seriesContainer,
|
||||
dataset,
|
||||
parseXPoint(transformedXAxisScale),
|
||||
parseYPoint(transformedYAxisScale)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return _zoom;
|
||||
};
|
||||
|
||||
const bindMouseEvents = (
|
||||
root,
|
||||
gX,
|
||||
gY,
|
||||
xAxisScale,
|
||||
yAxisScale,
|
||||
xAxisGenerator,
|
||||
yAxisGenerator,
|
||||
parseXPoint,
|
||||
parseYPoint,
|
||||
datasets
|
||||
) => {
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
_bindZoom(
|
||||
root,
|
||||
gX,
|
||||
gY,
|
||||
xAxisScale,
|
||||
yAxisScale,
|
||||
xAxisGenerator,
|
||||
yAxisGenerator,
|
||||
parseXPoint,
|
||||
parseYPoint,
|
||||
datasets
|
||||
);
|
||||
};
|
||||
|
||||
const events = {
|
||||
bindMouseEvents,
|
||||
external: {
|
||||
resetZoom: root => {
|
||||
_resetZoom(root, _zoom);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default events;
|
||||
@@ -0,0 +1,3 @@
|
||||
import * as lineChart from './lineChart';
|
||||
|
||||
export { lineChart };
|
||||
@@ -0,0 +1,272 @@
|
||||
import * as d3Array from 'd3-array';
|
||||
import * as d3Axis from 'd3-axis';
|
||||
import * as d3Scale from 'd3-scale';
|
||||
import * as d3ScaleChromatic from 'd3-scale-chromatic';
|
||||
import * as d3Selection from 'd3-selection';
|
||||
import * as d3Shape from 'd3-shape';
|
||||
|
||||
import chart from './chart';
|
||||
import events from './events';
|
||||
|
||||
const {
|
||||
external: { resetZoom },
|
||||
} = events;
|
||||
|
||||
/**
|
||||
* @typedef TimecoursePoint It defines a tuple of timecourse value (time X intensity)
|
||||
* @type {array}
|
||||
* @property {number} 0 indicates x|y value on the pair (x,y)
|
||||
* @property {number} 1 indicates x|y value on the pair (x,y)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef TimecoursePointDef It defines the shape of a given TimecoursePoint Axis
|
||||
* @type {object}
|
||||
* @property {string} label label for given TimecoursePoint Axis
|
||||
* @property {string} [unit] unit for given TimecoursePoint Axis
|
||||
* @property {string} type defines the type of given TimecoursePoint Axis
|
||||
* @property {number} indexRef refers the index into given TimecoursePoint for the current Axis Definition
|
||||
*/
|
||||
|
||||
/**
|
||||
* It gets max value of an array, considering value of param index
|
||||
*
|
||||
* @param {TimecoursePoint[]} array array of items to be evaluated
|
||||
* @param {number} index index of each array`s item to be evaluated
|
||||
* @return {any} max value
|
||||
*/
|
||||
function _getMaxValue(array, index) {
|
||||
return d3Array.max(array, arrayItem => {
|
||||
return arrayItem[index];
|
||||
});
|
||||
}
|
||||
|
||||
function _getMinValue(array, index) {
|
||||
return d3Array.min(array, arrayItem => {
|
||||
return arrayItem[index];
|
||||
});
|
||||
}
|
||||
|
||||
const LEGEND = { width: 100, margin: 10 };
|
||||
const MARGIN = { top: 20, right: 20, bottom: 50, left: 50 };
|
||||
|
||||
function _createAxisScale(domainBottom, domainUpper, rangeBottom, rangeUpper) {
|
||||
return d3Scale
|
||||
.scaleLinear()
|
||||
.domain([domainBottom, domainUpper * 1.05])
|
||||
.range([rangeBottom, rangeUpper]);
|
||||
}
|
||||
|
||||
const _getSeriesColor = series => {
|
||||
const seriesLabels = series.reduce((labels, series) => [...labels, series.label], []);
|
||||
|
||||
return d3Scale.scaleOrdinal().domain(seriesLabels).range(d3ScaleChromatic.schemeSet2);
|
||||
};
|
||||
|
||||
const _updateSeriesColors = series => {
|
||||
const seriesColor = _getSeriesColor(series);
|
||||
|
||||
return series.map(series => ({
|
||||
...series,
|
||||
color: series.color ?? seriesColor(series.label),
|
||||
}));
|
||||
};
|
||||
|
||||
const _textEllipses = (width, padding = 0) => {
|
||||
return function (...args) {
|
||||
const self = d3Selection.select(this);
|
||||
let textLength = self.node().getComputedTextLength();
|
||||
let text = self.text();
|
||||
|
||||
while (textLength > width - 2 * padding && text.length > 0) {
|
||||
text = text.slice(0, -1);
|
||||
self.text(text + '...');
|
||||
textLength = self.node().getComputedTextLength();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const _addLegend = (root, series, chartWidth, chartHeight, legendWidth) => {
|
||||
const legendItemHeight = 25;
|
||||
const legendHeight = legendItemHeight * series.length;
|
||||
|
||||
const legendContainer = chart.legend.addNode(
|
||||
root,
|
||||
legendWidth,
|
||||
legendHeight,
|
||||
chartWidth + LEGEND.margin,
|
||||
chartHeight / 2 - legendHeight / 2
|
||||
);
|
||||
|
||||
const seriesLabels = series.reduce((labels, series) => [...labels, series.label], []);
|
||||
|
||||
const seriesColors = series.reduce((colors, series) => [...colors, series.color], []);
|
||||
|
||||
chart.legend.setLabels(
|
||||
legendContainer,
|
||||
seriesLabels,
|
||||
seriesColors,
|
||||
legendItemHeight,
|
||||
legendWidth,
|
||||
_textEllipses
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* It creates a svg chart containing lines, dots, axis, labels
|
||||
*
|
||||
* @param {object} d3SVGRef svg content reference to append chart
|
||||
* @param {Object<string, TimecoursePointDef>} axis definition of axis
|
||||
* @param {object} points list of points to be created
|
||||
* @param {number} width width for whole content including lines, dots, axis, labels
|
||||
* @param {number} height height for whole content including lines, dots, axis, labels
|
||||
* @param {boolean} showAxisLabels flag to display labels or not
|
||||
*
|
||||
* @modifies {d3SVGRef}
|
||||
*/
|
||||
const addLineChartNode = ({
|
||||
d3SVGRef,
|
||||
axis,
|
||||
series,
|
||||
width,
|
||||
height,
|
||||
legendWidth = LEGEND.width,
|
||||
showAxisLabels = true,
|
||||
showAxisGrid = false,
|
||||
showLegend = false,
|
||||
transparentChartBackground = false,
|
||||
}) => {
|
||||
const marginRight = showLegend ? legendWidth + 2 * LEGEND.margin : MARGIN.right;
|
||||
const _width = width - MARGIN.left - marginRight;
|
||||
const _height = height - MARGIN.top - MARGIN.bottom;
|
||||
const { x: XAxis, y: YAxis } = axis;
|
||||
|
||||
series = _updateSeriesColors(series);
|
||||
|
||||
let maxX = -Infinity;
|
||||
let minX = Infinity;
|
||||
let maxY = -Infinity;
|
||||
let minY = Infinity;
|
||||
|
||||
series.forEach(currentSeries => {
|
||||
minX = Math.min(minX, _getMinValue(currentSeries.points, XAxis.indexRef));
|
||||
maxX = Math.max(maxX, _getMaxValue(currentSeries.points, XAxis.indexRef));
|
||||
minY = Math.min(minY, _getMinValue(currentSeries.points, YAxis.indexRef));
|
||||
maxY = Math.max(maxY, _getMaxValue(currentSeries.points, YAxis.indexRef));
|
||||
});
|
||||
|
||||
minX = axis?.x?.range?.min ?? minX;
|
||||
maxX = axis?.x?.range?.max ?? maxX;
|
||||
minY = axis?.y?.range?.min ?? minY;
|
||||
maxY = axis?.y?.range?.max ?? maxY;
|
||||
|
||||
const xAxisScale = _createAxisScale(minX, maxX, 0, _width);
|
||||
const yAxisScale = _createAxisScale(minY, maxY, _height, 0);
|
||||
|
||||
const parseXPoint = axisScale => point => {
|
||||
return (axisScale || xAxisScale)(point.x);
|
||||
};
|
||||
|
||||
const parseYPoint = axisScale => point => {
|
||||
return (axisScale || yAxisScale)(point.y);
|
||||
};
|
||||
|
||||
// Remove old D3 elements
|
||||
chart.removeContents(d3SVGRef);
|
||||
|
||||
const chartWrapper = chart.container.addNode(d3SVGRef, width, height, MARGIN.left, MARGIN.top);
|
||||
|
||||
// add background
|
||||
chart.background.addNode(chartWrapper, _width, _height, transparentChartBackground);
|
||||
|
||||
// call the x axis in a group tag
|
||||
const xAxisGenerator = d3Axis.axisBottom(xAxisScale);
|
||||
|
||||
if (showAxisGrid) {
|
||||
xAxisGenerator.tickSize(-_height).tickPadding(10);
|
||||
}
|
||||
const gXAxis = chart.axis.addNode(
|
||||
chartWrapper,
|
||||
XAxis,
|
||||
undefined,
|
||||
_height,
|
||||
undefined,
|
||||
() => xAxisGenerator,
|
||||
showAxisLabels,
|
||||
_width / 2,
|
||||
_height + MARGIN.bottom / 2 + 10,
|
||||
undefined,
|
||||
showAxisGrid
|
||||
);
|
||||
const yAxisGenerator = d3Axis.axisLeft(yAxisScale);
|
||||
|
||||
if (showAxisGrid) {
|
||||
yAxisGenerator.tickSize(-_width).tickPadding(10);
|
||||
}
|
||||
// add y axis
|
||||
const gYAxis = chart.axis.addNode(
|
||||
chartWrapper,
|
||||
YAxis,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
() => yAxisGenerator,
|
||||
showAxisLabels,
|
||||
0 - _height / 2,
|
||||
0 - MARGIN.left,
|
||||
[
|
||||
{ key: 'transform', value: 'rotate(-90)' },
|
||||
{ key: 'dy', value: '1em' },
|
||||
],
|
||||
showAxisGrid
|
||||
);
|
||||
|
||||
const datasets = [];
|
||||
|
||||
series.forEach((currentSeries, seriesIndex) => {
|
||||
const { points } = currentSeries;
|
||||
const line = d3Shape.line().x(parseXPoint(xAxisScale)).y(parseYPoint(yAxisScale));
|
||||
|
||||
const dataset = points.map((point, pointIndex) => {
|
||||
return { x: point[0], y: point[1], seriesIndex, pointIndex };
|
||||
});
|
||||
|
||||
const seriesContainer = chart.series.addNode(chartWrapper, seriesIndex, {
|
||||
color: currentSeries.color || '#00ff00',
|
||||
});
|
||||
|
||||
chart.lines.addNode(seriesContainer, dataset, line);
|
||||
|
||||
// add chart points
|
||||
chart.points.addNode(
|
||||
seriesContainer,
|
||||
dataset,
|
||||
parseXPoint(xAxisScale),
|
||||
parseYPoint(yAxisScale)
|
||||
);
|
||||
|
||||
datasets.push(dataset);
|
||||
});
|
||||
|
||||
if (showLegend) {
|
||||
_addLegend(chartWrapper, series, _width, _height, legendWidth);
|
||||
}
|
||||
|
||||
// bind events
|
||||
events.bindMouseEvents(
|
||||
chartWrapper,
|
||||
gXAxis,
|
||||
gYAxis,
|
||||
xAxisScale,
|
||||
yAxisScale,
|
||||
xAxisGenerator,
|
||||
yAxisGenerator,
|
||||
parseXPoint,
|
||||
parseYPoint,
|
||||
datasets
|
||||
);
|
||||
|
||||
return chartWrapper;
|
||||
};
|
||||
|
||||
export { addLineChartNode, resetZoom };
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './LineChart';
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
||||
import { Icon } from '@ohif/ui';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const PanelSection = ({ title, children, actionIcons = [] }) => {
|
||||
const PanelSection = ({ title, children, actionIcons = [], childrenClassName }) => {
|
||||
const [areChildrenVisible, setChildrenVisible] = useState(true);
|
||||
|
||||
const handleHeaderClick = () => {
|
||||
@@ -37,7 +37,8 @@ const PanelSection = ({ title, children, actionIcons = [] }) => {
|
||||
</div>
|
||||
{areChildrenVisible && (
|
||||
<>
|
||||
<div className="bg-primary-dark rounded-b-[4px]">{children}</div>
|
||||
<div className="h-[2px] bg-black"></div>
|
||||
<div className={`bg-primary-dark rounded-b-[4px] ${childrenClassName}`}>{children}</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@@ -49,6 +50,7 @@ PanelSection.defaultProps = {};
|
||||
PanelSection.propTypes = {
|
||||
title: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
childrenClassName: PropTypes.string,
|
||||
actionIcons: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { ProgressDropdownOption, ProgressDropdownOptionPropType } from './types';
|
||||
|
||||
const ProgressDiscreteBar = ({ options }: { options: ProgressDropdownOption[] }): ReactElement => {
|
||||
return (
|
||||
<div className="flex">
|
||||
{options.map((option, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={classnames('mr-1 h-1 grow first:rounded-l-sm last:mr-0 last:rounded-r-sm', {
|
||||
'bg-black': !option.activated && !option.completed,
|
||||
'bg-primary-main': option.activated && !option.completed,
|
||||
'bg-primary-light': option.completed,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProgressDiscreteBar.propTypes = {
|
||||
options: PropTypes.arrayOf(ProgressDropdownOptionPropType).isRequired,
|
||||
};
|
||||
|
||||
export default ProgressDiscreteBar;
|
||||
@@ -0,0 +1,168 @@
|
||||
import React, { ReactNode, useEffect, useCallback, useState, useMemo, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import ProgressDiscreteBar from './ProgressDiscreteBar';
|
||||
import ProgressItemDetail from './ProgressItemDetail';
|
||||
import ProgressItem from './ProgressItem';
|
||||
import { Icon } from '../../components';
|
||||
import { ProgressDropdownOption, ProgressDropdownOptionPropType } from './types';
|
||||
|
||||
const ProgressDropdown = ({
|
||||
options: optionsProps,
|
||||
value,
|
||||
children,
|
||||
dropDownWidth = '170',
|
||||
onChange,
|
||||
}: {
|
||||
options: ProgressDropdownOption[];
|
||||
value?: string;
|
||||
children?: ReactNode;
|
||||
onChange?: ({ selectedOption }) => void;
|
||||
}): JSX.Element => {
|
||||
const element = useRef(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const toggleOpen = () => setOpen(s => !s);
|
||||
const [options, setOptions] = useState(optionsProps);
|
||||
const [selectedOption, setSelectedOption] = useState(undefined);
|
||||
|
||||
const selectedOptionIndex = useMemo(
|
||||
() => options.findIndex(option => option.value === selectedOption?.value),
|
||||
[options, selectedOption]
|
||||
);
|
||||
|
||||
const canMoveNext = useMemo(
|
||||
() => selectedOptionIndex < options.length - 1,
|
||||
[selectedOptionIndex, options]
|
||||
);
|
||||
|
||||
const handleOptionSelected = useCallback(
|
||||
(newSelectedOption?: ProgressDropdownOption): void => {
|
||||
if (newSelectedOption?.value === selectedOption?.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
setSelectedOption(newSelectedOption);
|
||||
|
||||
if (newSelectedOption) {
|
||||
newSelectedOption.activated = true;
|
||||
newSelectedOption.onSelect?.();
|
||||
}
|
||||
|
||||
if (onChange) {
|
||||
onChange({ selectedOption: newSelectedOption });
|
||||
}
|
||||
},
|
||||
[selectedOption, onChange]
|
||||
);
|
||||
|
||||
const handleNextButtonClick = useCallback(() => {
|
||||
if (canMoveNext) {
|
||||
handleOptionSelected(options[selectedOptionIndex + 1]);
|
||||
}
|
||||
}, [options, selectedOptionIndex, canMoveNext, handleOptionSelected]);
|
||||
|
||||
// Update the options in case the options from props has changed
|
||||
useEffect(() => setOptions(optionsProps), [optionsProps]);
|
||||
|
||||
// Updates the selected item based on the value from props
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOption = value ? options.find(option => option.value === value) : undefined;
|
||||
|
||||
handleOptionSelected(newOption);
|
||||
}, [value, options, handleOptionSelected]);
|
||||
|
||||
// Listen to any click event outside of the dropdown context to hide the options
|
||||
useEffect(() => {
|
||||
const handleDocumentClick = e => {
|
||||
if (element.current && !element.current.contains(e.target)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleDocumentClick);
|
||||
|
||||
if (!open) {
|
||||
document.removeEventListener('click', handleDocumentClick);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={element}
|
||||
className="relative grow select-none text-[0px] text-white"
|
||||
>
|
||||
<div>
|
||||
<div className="mb-1.5 flex h-[26px]">
|
||||
<div
|
||||
className="bg-secondary-dark border-primary-main flex grow cursor-pointer rounded border"
|
||||
style={{ width: `${dropDownWidth}px` }}
|
||||
onClick={toggleOpen}
|
||||
>
|
||||
<div className="flex grow">
|
||||
{selectedOption && <ProgressItemDetail option={selectedOption} />}
|
||||
|
||||
{!selectedOption && <div className="ml-1 grow text-base leading-6">{children}</div>}
|
||||
</div>
|
||||
<Icon
|
||||
name="chevron-down"
|
||||
className="text-primary-active mt-0.5 ml-1"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className={classnames('ml-1.5 w-[26px] rounded text-base', {
|
||||
'bg-primary-main': canMoveNext,
|
||||
'bg-primary-dark pointer-events-none': !canMoveNext,
|
||||
})}
|
||||
>
|
||||
<Icon
|
||||
name="arrow-right"
|
||||
className={classnames('relative left-0.5 h-6 w-6 text-white', {
|
||||
'text-white': canMoveNext,
|
||||
'text-secondary-light': !canMoveNext,
|
||||
})}
|
||||
onClick={handleNextButtonClick}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className={classnames(
|
||||
'absolute top-7 left-0 right-8 z-10 mt-0.5 origin-top',
|
||||
'bg-primary-dark overflow-hidden transition-[max-height] duration-300 ',
|
||||
'border-secondary-main rounded border shadow',
|
||||
{
|
||||
hidden: !open,
|
||||
'max-h-[500px]': open,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<ProgressItem
|
||||
key={index}
|
||||
option={option}
|
||||
onSelect={() => handleOptionSelected(option)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ProgressDiscreteBar options={options} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProgressDropdown.propTypes = {
|
||||
options: PropTypes.arrayOf(ProgressDropdownOptionPropType).isRequired,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
dropDownWidth: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ProgressDropdown;
|
||||
@@ -0,0 +1,31 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ProgressItemDetail from './ProgressItemDetail';
|
||||
import { ProgressDropdownOption, ProgressDropdownOptionPropType } from './types';
|
||||
|
||||
const ProgressItem = ({
|
||||
option,
|
||||
onSelect,
|
||||
}: {
|
||||
option: ProgressDropdownOption;
|
||||
onSelect: (option: ProgressDropdownOption) => void;
|
||||
}): ReactElement => {
|
||||
const { value } = option;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={value}
|
||||
className={'hover:bg-secondary-main flex cursor-pointer py-1 transition duration-1000'}
|
||||
onClick={() => onSelect(option)}
|
||||
>
|
||||
<ProgressItemDetail option={option} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProgressItem.propTypes = {
|
||||
option: ProgressDropdownOptionPropType.isRequired,
|
||||
onSelect: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ProgressItem;
|
||||
@@ -0,0 +1,81 @@
|
||||
import React, { useState, useMemo, ReactElement } from 'react';
|
||||
import { Icon, Tooltip } from '../../components';
|
||||
import { ProgressDropdownOption, ProgressDropdownOptionPropType } from './types';
|
||||
|
||||
const MAX_TOOLTIP_LENGTH = 150;
|
||||
const iconClassNames = 'grow-0 text-primary-light h-4 w-4 mt-1 mr-2 mb-0 ml-1';
|
||||
|
||||
const ProgressItemDetail = ({ option }: { option: ProgressDropdownOption }): ReactElement => {
|
||||
const { label, info, completed } = option;
|
||||
const [truncate, setTruncate] = useState(true);
|
||||
const handleOnHideTooltip = () => setTruncate(true);
|
||||
let icon;
|
||||
|
||||
if (completed) {
|
||||
icon = 'status-tracked';
|
||||
} else if (info) {
|
||||
icon = 'launch-info';
|
||||
}
|
||||
|
||||
const tooltipText = useMemo(() => {
|
||||
if (!truncate || !info || info.length <= MAX_TOOLTIP_LENGTH) {
|
||||
return info;
|
||||
}
|
||||
|
||||
const handleReadMoreClick = e => {
|
||||
setTruncate(false);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{info.slice(0, MAX_TOOLTIP_LENGTH)}
|
||||
<button
|
||||
className="text-primary-active font-bold"
|
||||
onClick={handleReadMoreClick}
|
||||
>
|
||||
Read more...
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}, [info, truncate]);
|
||||
|
||||
const iconContent = (
|
||||
<>
|
||||
{icon && (
|
||||
<div>
|
||||
<Icon
|
||||
name={icon}
|
||||
className={iconClassNames}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!icon && <div className={iconClassNames} />}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{info && (
|
||||
<Tooltip
|
||||
content={tooltipText}
|
||||
position="bottom-left"
|
||||
tooltipBoxClassName={'max-w-xs'}
|
||||
onHide={handleOnHideTooltip}
|
||||
>
|
||||
{iconContent}
|
||||
</Tooltip>
|
||||
)}
|
||||
{!info && iconContent}
|
||||
|
||||
<div className="grow text-base leading-6">{label}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ProgressItemDetail.propTypes = {
|
||||
option: ProgressDropdownOptionPropType.isRequired,
|
||||
};
|
||||
|
||||
export default ProgressItemDetail;
|
||||
@@ -0,0 +1,106 @@
|
||||
import { ProgressDropdown } from '../../../components';
|
||||
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
|
||||
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
|
||||
|
||||
export const argTypes = {
|
||||
component: ProgressDropdown,
|
||||
title: 'Components/ProgressDropdown',
|
||||
};
|
||||
|
||||
<Meta
|
||||
title="Components/ProgressDropdown"
|
||||
component={ProgressDropdown}
|
||||
/>
|
||||
|
||||
export const ProgressDropdownTemplate = args => (
|
||||
<div className="mb-2.5 flex h-60 max-w-xs">
|
||||
<ProgressDropdown {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const loremIpsumText = new Array(7)
|
||||
.join(` Lorem ipsum, dolor sit amet consectetur adipisicing elit`)
|
||||
.substring(1);
|
||||
|
||||
<Heading
|
||||
title="ProgressDropdown"
|
||||
componentRelativePath="ProgressDropdown/ProgressDropdown.tsx"
|
||||
/>
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Props](#props)
|
||||
- [Usage](#usage)
|
||||
- [Contribute](#contribute)
|
||||
|
||||
## Overview
|
||||
|
||||
Dropdown is a modal Dropdown component which enabled to show a modal Dropdown.
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="ProgressDropdownOverview"
|
||||
args={{
|
||||
options: [],
|
||||
children: '[empty dropdown]',
|
||||
}}
|
||||
>
|
||||
{ProgressDropdownTemplate.bind({})}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
<ArgsTable of={ProgressDropdown} />
|
||||
|
||||
## Usage
|
||||
|
||||
You can list the items in the dropdown. The status of each item changes from black to dark blue in
|
||||
the status bar beneath the dropdown when the item is selected.
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="ProgressDropdownList"
|
||||
onChange={selectedItem => (selectedItem.completed = true)}
|
||||
args={{
|
||||
options: [
|
||||
{
|
||||
label: 'Item 1',
|
||||
value: 'item1',
|
||||
info: `Item 1 - ${loremIpsumText}`,
|
||||
onClick: () => {
|
||||
alert('Item 1 clicked');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Item 2',
|
||||
value: 'item2',
|
||||
info: `Item 2 - ${loremIpsumText}`,
|
||||
onClick: () => {
|
||||
alert('Item 2 clicked');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Item 3',
|
||||
value: 'item3',
|
||||
info: `Item 3 - ${loremIpsumText}`,
|
||||
onClick: () => {
|
||||
alert('Item 3 clicked');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Item 4',
|
||||
info: `Item 4 - ${loremIpsumText}`,
|
||||
onClick: () => {
|
||||
alert('Item 4 clicked');
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
{ProgressDropdownTemplate.bind({})}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Contribute
|
||||
|
||||
<Footer componentRelativePath="ProgressDropdown/__stories__/progressDropdown.stories.mdx" />
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ProgressDropdown';
|
||||
@@ -0,0 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export type ProgressDropdownOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
info?: string;
|
||||
activated?: boolean;
|
||||
completed?: boolean;
|
||||
onSelect?: () => void;
|
||||
};
|
||||
|
||||
export const ProgressDropdownOptionPropType = PropTypes.shape({
|
||||
label: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
info: PropTypes.string,
|
||||
activated: PropTypes.bool,
|
||||
completed: PropTypes.bool,
|
||||
onSelect: PropTypes.func,
|
||||
});
|
||||
@@ -53,7 +53,7 @@ const ActiveSegmentationConfig = ({
|
||||
unit="%"
|
||||
/>
|
||||
<InputRange
|
||||
minValue={4}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
value={getRoundedValue(config.fillAlpha * 100)}
|
||||
onChange={setFillAlpha}
|
||||
@@ -148,7 +148,9 @@ const SegmentationConfig = ({
|
||||
'rotate-90 transform': !isMinimized,
|
||||
})}
|
||||
/>
|
||||
<span className="text-[12px] font-[300] text-[#d8d8d8]">{t('Inactive segmentations')}</span>
|
||||
<span className="text-[12px] font-[300] text-[#d8d8d8]">
|
||||
{t('Inactive segmentations')}
|
||||
</span>
|
||||
</div>
|
||||
{!isMinimized && (
|
||||
<InactiveSegmentationConfig
|
||||
|
||||
@@ -165,6 +165,7 @@ const SidePanel = ({
|
||||
tabs,
|
||||
onOpen,
|
||||
expandedWidth = 248,
|
||||
onActiveTabIndexChange,
|
||||
}) => {
|
||||
const { t } = useTranslation('SidePanel');
|
||||
|
||||
@@ -178,14 +179,15 @@ const SidePanel = ({
|
||||
const openStatus = panelOpen ? 'open' : 'closed';
|
||||
const style = Object.assign({}, styleMap[openStatus][side], baseStyle);
|
||||
|
||||
const ActiveComponent = tabs[activeTabIndex]?.content;
|
||||
|
||||
const updatePanelOpen = useCallback((panelOpen: boolean) => {
|
||||
setPanelOpen(panelOpen);
|
||||
if (panelOpen) {
|
||||
onOpen?.();
|
||||
}
|
||||
}, []);
|
||||
const updatePanelOpen = useCallback(
|
||||
(panelOpen: boolean) => {
|
||||
setPanelOpen(panelOpen);
|
||||
if (panelOpen && onOpen) {
|
||||
onOpen();
|
||||
}
|
||||
},
|
||||
[onOpen]
|
||||
);
|
||||
|
||||
const updateActiveTabIndex = useCallback(
|
||||
(activeTabIndex: number) => {
|
||||
@@ -196,8 +198,12 @@ const SidePanel = ({
|
||||
|
||||
setActiveTabIndex(activeTabIndex);
|
||||
updatePanelOpen(true);
|
||||
|
||||
if (onActiveTabIndexChange) {
|
||||
onActiveTabIndexChange({ activeTabIndex });
|
||||
}
|
||||
},
|
||||
[updatePanelOpen]
|
||||
[onActiveTabIndexChange, updatePanelOpen]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -214,7 +220,7 @@ const SidePanel = ({
|
||||
side === 'left' ? 'justify-end pr-2' : 'justify-start pl-2'
|
||||
)}
|
||||
onClick={() => {
|
||||
updatePanelOpen(prev => !prev);
|
||||
updatePanelOpen(!panelOpen);
|
||||
}}
|
||||
data-cy={`side-panel-header-${side}`}
|
||||
>
|
||||
@@ -270,7 +276,7 @@ const SidePanel = ({
|
||||
)}
|
||||
style={{ width: `${closeIconWidth}px` }}
|
||||
onClick={() => {
|
||||
updatePanelOpen(prev => !prev);
|
||||
updatePanelOpen(!panelOpen);
|
||||
}}
|
||||
data-cy={`side-panel-header-${side}`}
|
||||
>
|
||||
@@ -356,7 +362,7 @@ const SidePanel = ({
|
||||
: { marginRight: `${closeIconWidth}px` }),
|
||||
}}
|
||||
data-cy={`${tabs[0].name}-btn`}
|
||||
onClick={() => updatePanelOpen(prev => !prev)}
|
||||
onClick={() => updatePanelOpen(!panelOpen)}
|
||||
>
|
||||
<span>{tabs[0].label}</span>
|
||||
</div>
|
||||
@@ -380,7 +386,14 @@ const SidePanel = ({
|
||||
{panelOpen ? (
|
||||
<>
|
||||
{getOpenStateComponent()}
|
||||
<ActiveComponent />
|
||||
{tabs.map((tab, tabIndex) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className={tabIndex === activeTabIndex ? '' : 'hidden'}
|
||||
>
|
||||
<tab.content />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<React.Fragment>{getCloseStateComponent()}</React.Fragment>
|
||||
@@ -410,6 +423,7 @@ SidePanel.propTypes = {
|
||||
),
|
||||
]),
|
||||
onOpen: PropTypes.func,
|
||||
onActiveTabIndexChange: PropTypes.func,
|
||||
expandedWidth: PropTypes.number,
|
||||
};
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ function ToolboxUI(props) {
|
||||
content={componentProps.label}
|
||||
secondaryContent={componentProps.disabledText}
|
||||
>
|
||||
<div className="border-secondary-light rounded border bg-black">
|
||||
<div className="bg-black">
|
||||
<Component
|
||||
{...componentProps}
|
||||
{...props}
|
||||
@@ -67,7 +67,7 @@ function ToolboxUI(props) {
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className="border-secondary-light rounded border bg-black">
|
||||
<div className="bg-black">
|
||||
<Component
|
||||
{...componentProps}
|
||||
{...props}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import './tooltip.css';
|
||||
@@ -40,27 +42,47 @@ const Tooltip = ({
|
||||
tight,
|
||||
children,
|
||||
isDisabled,
|
||||
tooltipBoxClassName,
|
||||
// time to show/hide the tooltip on mouse over and mouse out events (default: 300ms)
|
||||
showHideDelay,
|
||||
onHide,
|
||||
}) => {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const isOpen = useMemo(
|
||||
() => (isSticky || isActive) && !isDisabled,
|
||||
[isSticky, isActive, isDisabled]
|
||||
);
|
||||
const { t } = useTranslation('Buttons');
|
||||
const tooltipContainer = document.getElementById('react-portal');
|
||||
const [coords, setCoords] = useState({ x: 999999, y: 999999 });
|
||||
const parentRef = useRef(null);
|
||||
const tooltipRef = useRef(null);
|
||||
|
||||
const handleMouseOverDebounced = useMemo(
|
||||
() => debounce(() => setIsActive(true), showHideDelay),
|
||||
[showHideDelay]
|
||||
);
|
||||
|
||||
const handleMouseOutDebounced = useMemo(
|
||||
() => debounce(() => setIsActive(false), showHideDelay),
|
||||
[showHideDelay]
|
||||
);
|
||||
|
||||
const handleMouseOver = () => {
|
||||
if (!isActive) {
|
||||
setIsActive(true);
|
||||
}
|
||||
handleMouseOutDebounced.cancel();
|
||||
handleMouseOverDebounced();
|
||||
};
|
||||
|
||||
const handleMouseOut = () => {
|
||||
if (isActive) {
|
||||
setIsActive(false);
|
||||
}
|
||||
handleMouseOverDebounced.cancel();
|
||||
handleMouseOutDebounced();
|
||||
};
|
||||
|
||||
const isOpen = (isSticky || isActive) && !isDisabled;
|
||||
useEffect(() => {
|
||||
if (!isOpen && onHide) {
|
||||
onHide();
|
||||
}
|
||||
}, [isOpen, onHide]);
|
||||
|
||||
useEffect(() => {
|
||||
if (parentRef.current && tooltipRef.current) {
|
||||
@@ -121,7 +143,8 @@ const Tooltip = ({
|
||||
'tooltip-box bg-primary-dark border-secondary-light w-max-content relative inset-x-auto top-full rounded border text-base text-white',
|
||||
{
|
||||
'py-[6px] px-[8px]': !tight,
|
||||
}
|
||||
},
|
||||
tooltipBoxClassName
|
||||
)}
|
||||
>
|
||||
<div>{typeof content === 'string' ? t(content) : content}</div>
|
||||
@@ -164,16 +187,21 @@ Tooltip.defaultProps = {
|
||||
isSticky: false,
|
||||
position: 'bottom',
|
||||
isDisabled: false,
|
||||
showHideDelay: 300,
|
||||
};
|
||||
|
||||
Tooltip.propTypes = {
|
||||
isDisabled: PropTypes.bool,
|
||||
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
||||
secondaryContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
||||
position: PropTypes.oneOf(['bottom', 'bottom-left', 'bottom-right', 'left', 'right', 'top']),
|
||||
isSticky: PropTypes.bool,
|
||||
tight: PropTypes.bool,
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
tooltipBoxClassName: PropTypes.string,
|
||||
showHideDelay: PropTypes.number,
|
||||
onHide: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
@@ -1,5 +1,5 @@
|
||||
.tooltip {
|
||||
@apply absolute z-10;
|
||||
@apply absolute z-20;
|
||||
}
|
||||
|
||||
/* TOOLTIP WORKAROUND FOR ARROW UP */
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
import React, { useEffect, useCallback, useState, useMemo, ReactElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import WindowLevelHistogram from './WindowLevelHistogram';
|
||||
import InputRange from '../InputRange';
|
||||
import InputDoubleRange from '../InputDoubleRange';
|
||||
import {
|
||||
VOI,
|
||||
Colormap,
|
||||
Histogram,
|
||||
voiPropType,
|
||||
histogramPropType,
|
||||
colormapPropType,
|
||||
} from './types';
|
||||
import PanelSection from '../PanelSection';
|
||||
|
||||
const convertVOItoVOIRange = voi => {
|
||||
return {
|
||||
min: voi.windowCenter - voi.windowWidth / 2,
|
||||
max: voi.windowCenter + voi.windowWidth / 2,
|
||||
};
|
||||
};
|
||||
|
||||
const WindowLevel = ({
|
||||
title,
|
||||
step,
|
||||
histogram,
|
||||
voi: voiProp,
|
||||
opacity: opacityProp,
|
||||
showOpacitySlider,
|
||||
colormap,
|
||||
style,
|
||||
fillColor,
|
||||
lineColor,
|
||||
containerClassName,
|
||||
onVOIChange,
|
||||
onOpacityChange,
|
||||
}: {
|
||||
title: string;
|
||||
step: number;
|
||||
histogram: Histogram;
|
||||
voi: VOI;
|
||||
opacity: number;
|
||||
showOpacitySlider: boolean;
|
||||
colormap: Colormap;
|
||||
style: string;
|
||||
fillColor: string;
|
||||
lineColor: string;
|
||||
containerClassName: string;
|
||||
onVOIChange: (voi: VOI) => void;
|
||||
onOpacityChange: (opacity: number) => void;
|
||||
}): ReactElement => {
|
||||
const [opacity, setOpacity] = useState(opacityProp);
|
||||
|
||||
const range = useMemo(
|
||||
() => ({
|
||||
min: histogram.range.min,
|
||||
max: histogram.range.max,
|
||||
}),
|
||||
[histogram]
|
||||
);
|
||||
|
||||
const [voiRange, setVOIRange] = useState(
|
||||
voiProp
|
||||
? convertVOItoVOIRange(voiProp)
|
||||
: {
|
||||
min: range.min,
|
||||
max: range.max,
|
||||
}
|
||||
);
|
||||
|
||||
const handleVOIRangeChange = useCallback(
|
||||
newRange => {
|
||||
if (newRange[0] === voiRange.min && newRange[1] === voiRange.max) {
|
||||
return;
|
||||
}
|
||||
|
||||
const windowWidth = newRange[1] - newRange[0];
|
||||
const windowCenter = newRange[0] + windowWidth / 2;
|
||||
|
||||
setVOIRange({
|
||||
min: newRange[0],
|
||||
max: newRange[1],
|
||||
});
|
||||
|
||||
if (onVOIChange) {
|
||||
onVOIChange({ windowWidth, windowCenter });
|
||||
}
|
||||
},
|
||||
[onVOIChange, voiRange]
|
||||
);
|
||||
|
||||
const handleOpacityChange = useCallback(
|
||||
value => {
|
||||
if (onOpacityChange) {
|
||||
onOpacityChange(value);
|
||||
}
|
||||
},
|
||||
[onOpacityChange]
|
||||
);
|
||||
|
||||
useEffect(() => setVOIRange(convertVOItoVOIRange(voiProp)), [voiProp]);
|
||||
|
||||
useEffect(() => setOpacity(opacityProp), [opacityProp]);
|
||||
return (
|
||||
<div className={classnames('maxValue-w-sm p-0.5 text-[0px] text-white', containerClassName)}>
|
||||
<div className="px-2 pt-0 pb-[0.5]">
|
||||
{/* <legend className="pb-1 text-base text-white"> {title}</legend> */}
|
||||
<div className="flex h-4 text-xs">
|
||||
<div className="relative h-fit grow">
|
||||
<span className="absolute left-0 bottom-px leading-3">{range.min}</span>
|
||||
</div>
|
||||
<div className="relative h-fit grow text-right">
|
||||
<span className="absolute right-0 bottom-px leading-3">{range.max}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-1">
|
||||
<div className="pb-2.5">
|
||||
<div className="h-11 bg-white">
|
||||
<WindowLevelHistogram
|
||||
range={range}
|
||||
voiRange={voiRange}
|
||||
histogram={histogram}
|
||||
colormap={colormap}
|
||||
style={style}
|
||||
fillColor={fillColor}
|
||||
lineColor={lineColor}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<InputDoubleRange
|
||||
values={[voiRange.min, voiRange.max]}
|
||||
onChange={handleVOIRangeChange}
|
||||
minValue={range.min}
|
||||
maxValue={range.max}
|
||||
allowNumberEdit={true}
|
||||
showLabel={true}
|
||||
step={step}
|
||||
allowOutOfRange={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{showOpacitySlider && (
|
||||
<div className="flex items-center justify-between text-base">
|
||||
<div className="text-xs text-white">Opacity</div>
|
||||
<InputRange
|
||||
inputClassName="grow"
|
||||
maxValue={1}
|
||||
minValue={0}
|
||||
step={0.1}
|
||||
value={opacity}
|
||||
showLabel={true}
|
||||
allowNumberEdit={true}
|
||||
onChange={handleOpacityChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
WindowLevel.defaultProps = {
|
||||
step: 1,
|
||||
opacity: 1,
|
||||
showOpacitySlider: false,
|
||||
style: 'polygon',
|
||||
fillColor: '#3f3f3f',
|
||||
lineColor: '#707070',
|
||||
};
|
||||
|
||||
WindowLevel.propTypes = {
|
||||
title: PropTypes.string,
|
||||
step: PropTypes.number,
|
||||
histogram: histogramPropType.isRequired,
|
||||
voi: voiPropType,
|
||||
opacity: PropTypes.number,
|
||||
showOpacitySlider: PropTypes.bool,
|
||||
colormap: colormapPropType,
|
||||
style: PropTypes.oneOf(['polygon', 'bars']),
|
||||
fillColor: PropTypes.string,
|
||||
lineColor: PropTypes.string,
|
||||
containerClassName: PropTypes.string,
|
||||
onVOIChange: PropTypes.func,
|
||||
onOpacityChange: PropTypes.func,
|
||||
};
|
||||
|
||||
export default WindowLevel;
|
||||
@@ -0,0 +1,236 @@
|
||||
import React, { useLayoutEffect, useEffect, useRef, ReactElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Range,
|
||||
VOIRange,
|
||||
Histogram,
|
||||
Colormap,
|
||||
voiRangePropType,
|
||||
rangePropType,
|
||||
histogramPropType,
|
||||
colormapPropType,
|
||||
} from './types';
|
||||
|
||||
const DEFAULT_COLORMAP = {
|
||||
Name: 'Grayscale',
|
||||
RGBPoints: [0, 0, 0, 0, 1, 1, 1, 1],
|
||||
};
|
||||
|
||||
const clamp = (value, min, max) => Math.min(Math.max(min, value), max);
|
||||
|
||||
const interpolateVec3 = (a, b, t) => {
|
||||
return [a[0] * (1 - t) + b[0] * t, a[1] * (1 - t) + b[1] * t, a[2] * (1 - t) + b[2] * t];
|
||||
};
|
||||
|
||||
const drawBackground = (canvas, range, voiRange, colormap) => {
|
||||
const context = canvas.getContext('2d');
|
||||
const { width, height } = canvas;
|
||||
const windowWidth = voiRange.max - voiRange.min;
|
||||
const rgbPoints = colormap.RGBPoints;
|
||||
const colorsCount = rgbPoints.length / 4;
|
||||
|
||||
const getColorPoint = index => {
|
||||
const offset = 4 * index;
|
||||
|
||||
return index < colorsCount
|
||||
? {
|
||||
index,
|
||||
position: rgbPoints[offset],
|
||||
color: [rgbPoints[offset + 1], rgbPoints[offset + 2], rgbPoints[offset + 3]],
|
||||
}
|
||||
: undefined;
|
||||
};
|
||||
|
||||
let leftColorPoint = undefined;
|
||||
let rightColorPoint = getColorPoint(0);
|
||||
|
||||
const incRawPixelValue = (range.max - range.min) / (width - 1);
|
||||
let rawPixelValue = range.min;
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
const tVoiRange = (rawPixelValue - voiRange.min) / windowWidth;
|
||||
|
||||
// Find the color in a linear way (O(n) complexity)
|
||||
if (rightColorPoint) {
|
||||
for (let i = rightColorPoint.index; i < colorsCount; i++) {
|
||||
if (tVoiRange <= rightColorPoint.position) {
|
||||
break;
|
||||
}
|
||||
|
||||
leftColorPoint = rightColorPoint;
|
||||
rightColorPoint = getColorPoint(i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
let normColor;
|
||||
|
||||
if (!leftColorPoint) {
|
||||
normColor = [...rightColorPoint.color];
|
||||
} else if (!rightColorPoint) {
|
||||
normColor = [...leftColorPoint.color];
|
||||
} else {
|
||||
const tColorRange =
|
||||
(tVoiRange - leftColorPoint.position) /
|
||||
(rightColorPoint.position - leftColorPoint.position);
|
||||
|
||||
normColor = interpolateVec3(leftColorPoint.color, rightColorPoint.color, tColorRange);
|
||||
}
|
||||
|
||||
const color = normColor.map(color => clamp(Math.round(color * 255), 0, 255));
|
||||
|
||||
context.fillStyle = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
|
||||
context.fillRect(x, 0, 1, height);
|
||||
|
||||
rawPixelValue += incRawPixelValue;
|
||||
}
|
||||
};
|
||||
|
||||
/* plots the histogram bins as a polygon that traces the centers of each bin */
|
||||
const drawPolygonHistogram = (canvas, context, histogram, options) => {
|
||||
const xScale = canvas.width / histogram.numBins;
|
||||
const { scale } = options;
|
||||
const maxVal = scale(histogram.maxBinValue);
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo(0, canvas.height);
|
||||
context.lineTo(0, canvas.height - (canvas.height * scale(histogram.bins[0])) / maxVal);
|
||||
|
||||
let x = xScale / 2;
|
||||
for (let bin = 0; bin < histogram.numBins; bin++) {
|
||||
context.lineTo(x, canvas.height - (canvas.height * scale(histogram.bins[bin])) / maxVal);
|
||||
x += xScale;
|
||||
}
|
||||
context.lineTo(
|
||||
canvas.width,
|
||||
canvas.height - (canvas.height * scale(histogram.bins[histogram.numBins - 1])) / maxVal
|
||||
);
|
||||
context.lineTo(canvas.width, canvas.height);
|
||||
context.lineTo(0, canvas.height);
|
||||
|
||||
context.closePath();
|
||||
context.fill();
|
||||
context.stroke();
|
||||
};
|
||||
|
||||
/* plots the histogram bins as a series of n vertical bars (n = number of bins) */
|
||||
const drawBarHistogram = (canvas, context, histogram, options) => {
|
||||
const xScale = canvas.width / histogram.numBins;
|
||||
const { scale } = options;
|
||||
const maxVal = scale(histogram.maxBinValue);
|
||||
|
||||
context.beginPath();
|
||||
|
||||
for (let bin = 0; bin < histogram.numBins; bin++) {
|
||||
context.moveTo(xScale * bin, canvas.height);
|
||||
context.lineTo(
|
||||
xScale * bin,
|
||||
canvas.height - (canvas.height * scale(histogram.bins[bin])) / maxVal
|
||||
);
|
||||
}
|
||||
|
||||
context.closePath();
|
||||
context.strokeStyle = options.fillColor;
|
||||
context.lineWidth = xScale;
|
||||
context.stroke();
|
||||
};
|
||||
|
||||
const drawHistogram = (canvas, histogram, options) => {
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
// context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
context.fillStyle = options.fillColor;
|
||||
context.strokeStyle = options.lineColor;
|
||||
|
||||
if (options.style === 'polygon') {
|
||||
drawPolygonHistogram(canvas, context, histogram, options);
|
||||
} else if (options.style === 'bars') {
|
||||
drawBarHistogram(canvas, context, histogram, options);
|
||||
} else {
|
||||
throw new Error(`Invalid style (${options.style})`);
|
||||
}
|
||||
};
|
||||
|
||||
const WindowLevelHistogram = ({
|
||||
range,
|
||||
voiRange,
|
||||
histogram,
|
||||
colormap,
|
||||
style,
|
||||
fillColor,
|
||||
lineColor,
|
||||
}: {
|
||||
range: Range;
|
||||
voiRange: VOIRange;
|
||||
histogram: Histogram;
|
||||
colormap: Colormap;
|
||||
style: string;
|
||||
fillColor: string;
|
||||
lineColor: string;
|
||||
}): ReactElement => {
|
||||
const containerRef = useRef(null);
|
||||
const canvasRef = useRef(null);
|
||||
const minVOIPercent = ((voiRange.min - range.min) / (range.max - range.min)) * 100;
|
||||
const maxVoiPercent = (1 - (range.max - voiRange.max) / (range.max - range.min)) * 100;
|
||||
const background =
|
||||
colormap.Name !== 'Grayscale'
|
||||
? undefined
|
||||
: `linear-gradient(to right, #000 0%, #000 ${minVOIPercent}%, #fff ${maxVoiPercent}%, #fff 100%)`;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const container = containerRef.current;
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
canvas.width = container.offsetWidth;
|
||||
canvas.height = container.offsetHeight;
|
||||
}, [containerRef, canvasRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const context = canvas.getContext('2d');
|
||||
const fnIdentity = x => x;
|
||||
const options = {
|
||||
style,
|
||||
fillColor,
|
||||
lineColor,
|
||||
scale: fnIdentity,
|
||||
};
|
||||
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
drawBackground(canvas, range, voiRange, colormap);
|
||||
drawHistogram(canvas, histogram, options);
|
||||
}, [range, voiRange, histogram, colormap, style, fillColor, lineColor, canvasRef]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="h-full"
|
||||
style={{ background }}
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width="1"
|
||||
height="1"
|
||||
></canvas>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
WindowLevelHistogram.defaultProps = {
|
||||
colormap: DEFAULT_COLORMAP,
|
||||
style: 'polygon',
|
||||
fillColor: '#3f3f3f',
|
||||
lineColor: '#707070',
|
||||
};
|
||||
|
||||
WindowLevelHistogram.propTypes = {
|
||||
range: rangePropType.isRequired,
|
||||
voiRange: voiRangePropType.isRequired,
|
||||
histogram: histogramPropType.isRequired,
|
||||
colormap: colormapPropType,
|
||||
style: PropTypes.oneOf(['polygon', 'bars']),
|
||||
fillColor: PropTypes.string,
|
||||
lineColor: PropTypes.string,
|
||||
};
|
||||
|
||||
export default WindowLevelHistogram;
|
||||
@@ -0,0 +1,103 @@
|
||||
import WindowLevel from '../WindowLevel';
|
||||
|
||||
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
|
||||
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
|
||||
import '../../InputRange/InputRange.css';
|
||||
|
||||
export const argTypes = {
|
||||
component: WindowLevel,
|
||||
title: 'Components/WindowLevel',
|
||||
};
|
||||
|
||||
<Meta
|
||||
title="Components/WindowLevel"
|
||||
component={WindowLevel}
|
||||
/>
|
||||
|
||||
export const WindowLevelTemplate = args => (
|
||||
<div className="w-96">
|
||||
<WindowLevel {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
<Heading
|
||||
title="WindowLevel"
|
||||
componentRelativePath="WindowLevel/WindowLevel.tsx"
|
||||
/>
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Props](#props)
|
||||
- [Contribute](#contribute)
|
||||
|
||||
## Overview
|
||||
|
||||
WindowLevel is a component that...
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Overview"
|
||||
args={{
|
||||
title: 'Target',
|
||||
voi: {
|
||||
windowWidth: 800,
|
||||
windowCenter: 0,
|
||||
},
|
||||
showOpacitySlider: true,
|
||||
histogram: {
|
||||
numBins: 256,
|
||||
range: {
|
||||
min: -1000,
|
||||
max: 1000,
|
||||
},
|
||||
bins: [
|
||||
469817, 485077, 499920, 513954, 526106, 472824, 551087, 560798, 570524, 576182, 509119,
|
||||
585371, 585123, 586201, 581405, 504649, 569829, 560213, 550658, 535250, 455914, 504388,
|
||||
487305, 467924, 446629, 427088, 355151, 384904, 362374, 342472, 321159, 262968, 282026,
|
||||
261558, 243751, 225659, 184247, 195394, 180728, 166477, 153932, 124837, 132557, 123424,
|
||||
114390, 106514, 98492, 80936, 87261, 81444, 76932, 72578, 60694, 66376, 62465, 59951,
|
||||
57424, 48593, 53942, 52361, 50857, 49578, 42447, 47633, 46587, 45540, 45061, 38899, 44420,
|
||||
43817, 43381, 43384, 42993, 37937, 43329, 43692, 43943, 44504, 39516, 46166, 46882, 48458,
|
||||
49524, 44720, 53620, 55901, 58585, 61007, 56985, 68541, 72795, 78542, 84522, 91109, 86305,
|
||||
106424, 115156, 125756, 136606, 130742, 160773, 176254, 192943, 209036, 197566, 242622,
|
||||
261520, 282054, 301671, 280379, 340427, 361651, 381541, 400844, 366045, 431948, 448730,
|
||||
461965, 474742, 484742, 429398, 495580, 498192, 500972, 495907, 430082, 487111, 478174,
|
||||
466274, 454838, 387260, 429238, 412109, 394391, 377166, 314069, 342320, 323342, 303672,
|
||||
287065, 269839, 222018, 237587, 220910, 207113, 192675, 157585, 168534, 158188, 147129,
|
||||
137956, 112961, 122187, 114196, 108509, 103007, 85884, 94253, 90044, 87221, 84668, 71879,
|
||||
80385, 78623, 77783, 77091, 76156, 66260, 76005, 75463, 74981, 75396, 65342, 75501, 76266,
|
||||
76450, 76053, 67137, 76115, 76326, 76434, 77201, 67050, 76313, 76293, 75723, 75894, 75980,
|
||||
65911, 74524, 74224, 73147, 72685, 62974, 70932, 70591, 69998, 68042, 58874, 66580, 65624,
|
||||
64716, 63501, 54665, 61603, 60323, 59414, 57771, 50209, 56266, 55508, 54012, 53424, 52116,
|
||||
44747, 50440, 49397, 47822, 47640, 40761, 45368, 44812, 44429, 42998, 37046, 41961, 41013,
|
||||
39896, 39609, 34086, 38540, 37511, 36643, 35892, 35723, 30200, 34272, 33283, 32656, 32171,
|
||||
27407, 30907, 30045, 29634, 28707, 24846, 27648, 27349, 26831, 25707, 22627, 25339, 24986,
|
||||
24310, 23723, 23324,
|
||||
],
|
||||
maxBin: 13,
|
||||
maxBinValue: 586201,
|
||||
},
|
||||
colormap: {
|
||||
Name: '2hot',
|
||||
RGBPoints: [
|
||||
-1, 0.0416667, 0, 0, -0.873016, 0.208333, 0, 0, -0.746032, 0.375, 0, 0, -0.619048,
|
||||
0.541667, 0, 0, -0.492063, 0.708333, 0, 0, -0.365079, 0.854137, 0, 0, -0.238095, 0.937488,
|
||||
0.039062, 0, -0.111111, 1, 0.208333, 0, 0.015873, 1, 0.375, 0, 0.142857, 1, 0.541667, 0,
|
||||
0.269841, 1, 0.708333, 0, 0.396825, 1, 0.858805, 0.03125, 0.52381, 1, 0.947392, 0.15625,
|
||||
0.650794, 1, 1, 0.3125, 0.777778, 1, 1, 0.5625, 0.904762, 1, 1, 0.8125, 1, 1, 1, 1,
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
{WindowLevelTemplate.bind({})}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
<ArgsTable of={WindowLevel} />
|
||||
|
||||
## Usage
|
||||
|
||||
## Contribute
|
||||
|
||||
<Footer componentRelativePath="WindowLevel/__stories__/windowLevel.stories.mdx" />
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './WindowLevel';
|
||||
@@ -0,0 +1,64 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export type Range = {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
|
||||
export type VOI = {
|
||||
windowWidth: number;
|
||||
windowCenter: number;
|
||||
};
|
||||
|
||||
export type VOIRange = {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
|
||||
export type Histogram = {
|
||||
bins: number[];
|
||||
numBins: number;
|
||||
maxBin: number;
|
||||
maxBinValue: number;
|
||||
range: {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type Colormap = {
|
||||
Name: string;
|
||||
RGBPoints: number[];
|
||||
};
|
||||
|
||||
export const rangePropType = PropTypes.shape({
|
||||
min: PropTypes.number.isRequired,
|
||||
max: PropTypes.number.isRequired,
|
||||
});
|
||||
|
||||
export const voiRangePropType = PropTypes.shape({
|
||||
min: PropTypes.number.isRequired,
|
||||
max: PropTypes.number.isRequired,
|
||||
});
|
||||
|
||||
export const voiPropType = PropTypes.shape({
|
||||
windowCenter: PropTypes.number.isRequired,
|
||||
windowWidth: PropTypes.number.isRequired,
|
||||
});
|
||||
|
||||
export const histogramPropType = PropTypes.shape({
|
||||
bins: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.instanceOf(Int32Array)])
|
||||
.isRequired,
|
||||
numBins: PropTypes.number.isRequired,
|
||||
maxBin: PropTypes.number.isRequired,
|
||||
maxBinValue: PropTypes.number.isRequired,
|
||||
range: PropTypes.shape({
|
||||
min: PropTypes.number.isRequired,
|
||||
max: PropTypes.number.isRequired,
|
||||
}),
|
||||
});
|
||||
|
||||
export const colormapPropType = PropTypes.shape({
|
||||
Name: PropTypes.string,
|
||||
RGBPoints: PropTypes.arrayOf(PropTypes.number),
|
||||
});
|
||||
@@ -27,6 +27,7 @@ import MeasurementTable from './MeasurementTable';
|
||||
import Modal from './Modal';
|
||||
import NavBar from './NavBar';
|
||||
import Notification from './Notification';
|
||||
import ProgressDropdown from './ProgressDropdown';
|
||||
import Select from './Select';
|
||||
import SegmentationTable from './SegmentationTable';
|
||||
import { SegmentationGroupTable, SegmentationGroupTableExpanded } from './SegmentationGroupTable';
|
||||
@@ -52,6 +53,7 @@ import ThumbnailTracked from './ThumbnailTracked';
|
||||
import ThumbnailList from './ThumbnailList';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
import ExpandableToolbarButton from './ExpandableToolbarButton';
|
||||
import LineChart from './LineChart';
|
||||
import ListMenu from './ListMenu';
|
||||
import Tooltip from './Tooltip';
|
||||
import TooltipClipboard from './TooltipClipboard';
|
||||
@@ -60,6 +62,7 @@ import Viewport from './Viewport';
|
||||
import ViewportDownloadForm from './ViewportDownloadForm';
|
||||
import ViewportGrid from './ViewportGrid';
|
||||
import ViewportPane from './ViewportPane';
|
||||
import WindowLevel from './WindowLevel';
|
||||
import WindowLevelMenuItem from './WindowLevelMenuItem';
|
||||
import UserPreferences from './UserPreferences';
|
||||
import HotkeysPreferences from './HotkeysPreferences';
|
||||
@@ -91,8 +94,10 @@ import { Toolbox } from './Toolbox';
|
||||
import InvestigationalUseDialog from './InvestigationalUseDialog';
|
||||
import MeasurementItem from './MeasurementTable/MeasurementItem';
|
||||
import LayoutPreset from './LayoutPreset';
|
||||
import ActionButtons from './ActionButtons';
|
||||
|
||||
export {
|
||||
ActionButtons,
|
||||
AboutModal,
|
||||
AdvancedToolbox,
|
||||
AllInOneMenu,
|
||||
@@ -112,6 +117,7 @@ export {
|
||||
EmptyStudies,
|
||||
ErrorBoundary,
|
||||
ExpandableToolbarButton,
|
||||
LineChart,
|
||||
ListMenu,
|
||||
Icon,
|
||||
IconButton,
|
||||
@@ -120,7 +126,6 @@ export {
|
||||
InputFilterText,
|
||||
InputGroup,
|
||||
InputRange,
|
||||
InputDoubleRange,
|
||||
InputNumber,
|
||||
InputLabelWrapper,
|
||||
InputMultiSelect,
|
||||
@@ -139,6 +144,7 @@ export {
|
||||
Modal,
|
||||
NavBar,
|
||||
Notification,
|
||||
ProgressDropdown,
|
||||
ProgressLoadingBar,
|
||||
PanelSection,
|
||||
Select,
|
||||
@@ -181,7 +187,9 @@ export {
|
||||
ViewportGrid,
|
||||
ViewportPane,
|
||||
ViewportOverlay,
|
||||
WindowLevel,
|
||||
WindowLevelMenuItem,
|
||||
InputDoubleRange,
|
||||
LabellingFlow,
|
||||
HeaderPatientInfo,
|
||||
LayoutPreset,
|
||||
|
||||
@@ -21,13 +21,30 @@ export default function CineProvider({ children, service }) {
|
||||
const { id, frameRate, isPlaying = undefined } = action.payload;
|
||||
const cines = state.cines;
|
||||
|
||||
if (!cines[id]) {
|
||||
cines[id] = { id, ...DEFAULT_CINE };
|
||||
}
|
||||
cines[id].frameRate = frameRate || cines[id].frameRate;
|
||||
cines[id].isPlaying = isPlaying !== undefined ? isPlaying : cines[id].isPlaying;
|
||||
const syncedCineIds = service.getSyncedViewports(id).map(({ viewportId }) => viewportId);
|
||||
const cineIdsToUpdate = [id, ...syncedCineIds].filter(curId => {
|
||||
const currentCine = cines[curId] ?? {};
|
||||
const shouldUpdateFrameRate =
|
||||
currentCine.frameRate !== (frameRate ?? currentCine.frameRate);
|
||||
const shouldUpdateIsPlaying =
|
||||
currentCine.isPlaying !== (isPlaying ?? currentCine.isPlaying);
|
||||
|
||||
return { ...state, ...{ cines } };
|
||||
return shouldUpdateFrameRate || shouldUpdateIsPlaying;
|
||||
});
|
||||
|
||||
cineIdsToUpdate.forEach(currId => {
|
||||
let cine = cines[currId];
|
||||
|
||||
if (!cine) {
|
||||
cine = { id, ...DEFAULT_CINE };
|
||||
cines[currId] = cine;
|
||||
}
|
||||
|
||||
cine.frameRate = frameRate ?? cine.frameRate;
|
||||
cine.isPlaying = isPlaying ?? cine.isPlaying;
|
||||
});
|
||||
|
||||
return { ...state, ...cines };
|
||||
}
|
||||
case 'SET_IS_CINE_ENABLED': {
|
||||
return { ...state, ...{ isCineEnabled: action.payload } };
|
||||
|
||||
@@ -31,6 +31,7 @@ export {
|
||||
|
||||
/** COMPONENTS */
|
||||
export {
|
||||
ActionButtons,
|
||||
AboutModal,
|
||||
AdvancedToolbox,
|
||||
AllInOneMenu,
|
||||
@@ -50,6 +51,7 @@ export {
|
||||
EmptyStudies,
|
||||
ErrorBoundary,
|
||||
ExpandableToolbarButton,
|
||||
LineChart,
|
||||
ListMenu,
|
||||
Icon,
|
||||
IconButton,
|
||||
@@ -84,6 +86,7 @@ export {
|
||||
SegmentationGroupTableExpanded,
|
||||
SidePanel,
|
||||
SplitButton,
|
||||
ProgressDropdown,
|
||||
LegacySplitButton,
|
||||
StudyBrowser,
|
||||
StudyItem,
|
||||
@@ -118,6 +121,7 @@ export {
|
||||
ViewportDownloadForm,
|
||||
ViewportGrid,
|
||||
ViewportPane,
|
||||
WindowLevel,
|
||||
WindowLevelMenuItem,
|
||||
ImageScrollbar,
|
||||
ViewportOverlay,
|
||||
|
||||
@@ -20,9 +20,7 @@ const Header = () => {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
{/* <Toolbar tools={tools} activeTool={activeTool} moreTools={tools} /> */}
|
||||
</div>
|
||||
<div className="flex items-center"></div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-common-light mr-3 text-lg">FOR INVESTIGATIONAL USE ONLY</span>
|
||||
<IconButton
|
||||
|
||||