Customization Service
There are a lot of places where users may want to configure certain elements differently between different modes or for different deployments. A mode example might be the use of a custom overlay showing mode related DICOM header information such as radiation dose or patient age.
The use of customizationService enables these to be defined in a typed fashion by
providing an easy way to set default values for this, but to allow a
non-default value to be specified by the configuration or mode.
customizationService itself doesn't implement the actual customization,
but rather just provide mechanism to register reusable prototypes, to configure
those prototypes with actual configurations, and to use the configured objects
(components, data, whatever).
Actual implementation of the customization is totally up to the component that supports customization.
General Overview
This framework allows you to configure many features, or "slots," through customization modules. Extensions can choose to offer their own module, which outlines which values can be changed. By looking at each extension's getCustomizationModule(), you can see which objects or components are open to customization.
Below is a high-level example of how you might define a default customization and then consume and override it:
-
Defining a Customizable Default
In your extension, you might export a set of default configurations (for instance, a list that appears in a panel). Here, you provide an identifier and store the default list under that identifier. This makes the item discoverable by the customization service:
// Inside your extension’s customization module
export default function getCustomizationModule() {
return [
{
name: 'default',
value: {
defaultList: ['Item A', 'Item B'],
},
},
];
}By naming it
default, it is automatically registered.infoYou might want to have customizations ready to use in your application without actually applying them. In such cases, you can name them something other than
default. For example, in your mode, you can do this:customizationService.setCustomizations([
'@ohif/extension-cornerstone-dicom-seg.customizationModule.dicom-seg-sorts',
]);This is really useful when you want to apply a set of customizations as a pack, kind of like a bundle.
-
Retrieving the Default Customization In the panel or component (or whatever) that needs the list, you retrieve it using
getCustomization:const myList = customizationService.getCustomization('defaultList');
// If unmodified, this returns ['Item A', 'Item B']This allows your component to always fetch the most current version (original default or overridden).
-
Overriding from Outside To customize this list outside your extension, call
setCustomizationswith the identifier ('defaultList'). For example, a mode can modify the list to add or change items:// From within a mode (or globally)
customizationService.setCustomizations({
'defaultList': {
$set: ['New Item 1', 'New Item 2'],
},
});The next time any panel calls
getCustomization('defaultList'), it will get the updated list.Don't worry we will go over the
$setsyntax in more detail later.
Scope of Customization
Customizations can be declared at three different scopes, each with its own priority and lifecycle. These scopes determine how and when customizations are applied.
1. Default Scope
- Purpose: Establish baseline or "fallback" values that extensions provide.
- Options:
- Via Extensions:
- Implement a
getCustomizationModulefunction in your extension and name itdefault.
function getCustomizationModule() {
return [
{
name: 'default',
value: {
'studyBrowser.sortFunctions': {
$set: [
{
label: 'Default Sort Function',
sortFunction: (a, b) => a.SeriesDate - b.SeriesDate,
},
],
},
},
},
];
} - Implement a
- Using the
setCustomizationsMethod:- Call
setCustomizationsin your application and specifyCustomizationScope.Defaultas the second argument:
customizationService.setCustomizations(
{
'studyBrowser.sortFunctions': {
$set: [
{
label: 'Default Sort Function',
sortFunction: (a, b) => a.SeriesDate - b.SeriesDate,
},
],
},
},
CustomizationScope.Default
); - Call
- Via Extensions:
2. Mode Scope
- Purpose: Apply customizations specific to a particular mode.
- Lifecycle: These customizations are cleared or reset when switching between modes.
- Example: Use the
setCustomizationsmethod to define mode-specific behavior.customizationService.setCustomizations({
'studyBrowser.sortFunctions': {
$set: [
{
label: 'Mode-Specific Sort Function',
sortFunction: (a, b) => b.SeriesDate - a.SeriesDate,
},
],
},
});
3. Global Scope
- Purpose: Apply system-wide customizations that override both default and mode-scoped values.
- How to Configure:
-
Add global customizations directly to the application's configuration file:
window.config = {
name: 'config/default.js',
routerBasename: null,
customizationService: [
{
'studyBrowser.sortFunctions': {
$push: [
{
label: 'Global Sort Function',
sortFunction: (a, b) => b.SeriesDate - a.SeriesDate,
},
],
},
},
],
}; -
Use Namespaced Extensions:
- Instead of directly specifying customizations in the configuration, you can refer to a predefined customization module within an extension:
window.config = {
name: 'config/default.js',
routerBasename: null,
customizationService: [
'@ohif/extension-cornerstone.customizationModule.newCustomization',
],
};-
In this example, the
newCustomizationmodule within the@ohif/extension-cornerstoneextension contains the global customizations. The application will load and apply these settings globally.function getCustomizationModule() {
return [
{
name: 'newCustomization',
value: {
'studyBrowser.sortFunctions': {
$push: [
{
label: 'Global Namespace Sort Function',
sortFunction: (a, b) => b.SeriesDate - a.SeriesDate,
},
],
},
},
},
];
}
-
Priority of Scopes
When a customization is retrieved:
- Global Scope: Takes precedence if defined.
- Mode Scope: Used if no global customization is defined.
- Default Scope: Fallback when neither global nor mode-specific values are available.
As you have guessed the .setCustomizations accept a second argument which is the scope. By default it is set to mode.
Customization Syntax
The customization syntax is designed to offer flexibility when modifying configurations. Instead of simply replacing values, you can perform granular updates like appending items to arrays, inserting at specific indices, updating deeply nested fields, or applying filters. This flexibility ensures that updates are efficient, targeted, and suitable for complex data structures.
Why a Special Syntax?
Traditional value replacement might not be ideal in scenarios such as:
- Appending or prepending to an existing list instead of overwriting it.
- Selective updates for specific fields in an object without affecting other fields.
- Filtering or merging nested items in arrays or objects while preserving other parts.
To address these needs, the customization service uses a special syntax inspired by immutability-helper commands. Below are examples of each operation.
1. Replace a Value ($set)
Use $set to entirely replace a value. This is the simplest operation which would replace the entire value.
// Before: someKey = 'Old Value'
customizationService.setCustomizations({
someKey: { $set: 'New Value' },
});
// After: someKey = 'New Value'
Example with study browser:
// Before: studyBrowser.sortFunctions = []
customizationService.setCustomizations({
'studyBrowser.sortFunctions': {
$set: [
{
label: 'Sort by Patient ID',
sortFunction: (a, b) => a.PatientID.localeCompare(b.PatientID),
},
],
},
});
// After: studyBrowser.sortFunctions = [{label: 'Sort by Patient ID', sortFunction: ...}]
2. Add to an Array ($push and $unshift)
$push: Appends items to the end of an array.$unshift: Adds items to the beginning of an array.
// Before: NumbersList = [1, 2, 3]
// Push items to the end
customizationService.setCustomizations({
'NumbersList': { $push: [5, 6] },
});
// After: NumbersList = [1, 2, 3, 5, 6]
// Unshift items to the front
customizationService.setCustomizations({
'NumbersList': { $unshift: [0] },
});
// After: NumbersList = [0, 1, 2, 3, 5, 6]
3. Insert at Specific Index ($splice)
Use $splice to insert, replace, or remove items at a specific index in an array.
// Before: NumbersList = [1, 2, 3]
customizationService.setCustomizations({
'NumbersList': {
$splice: [
[2, 0, 99], // Insert 99 at index 2
],
},
});
// After: NumbersList = [1, 2, 99, 3]
4. Merge Object Properties ($merge)
Use $merge to update specific fields in an object without affecting other fields.
// Before: SeriesInfo = { label: 'Original Label', sortFunction: oldFunc }
customizationService.setCustomizations({
'SeriesInfo': {
$merge: {
label: 'Updated Label',
extraField: true,
},
},
});
// After: SeriesInfo = { label: 'Updated Label', sortFunction: oldFunc, extraField: true }
Example with nested merge:
// Before: SeriesInfo = { advanced: { subKey: 'oldValue' } }
customizationService.setCustomizations({
'SeriesInfo': {
advanced: {
$merge: {
subKey: 'updatedSubValue',
},
},
},
});
// After: SeriesInfo = { advanced: { subKey: 'updatedSubValue' } }
5. Apply a Function ($apply)
Use $apply when you need to compute the new value dynamically.
// Before: SeriesInfo = { label: 'Old Label', data: 123 }
customizationService.setCustomizations({
'SeriesInfo': {
$apply: oldValue => ({
...oldValue,
label: 'Computed Label',
}),
},
});
// After: SeriesInfo = { label: 'Computed Label', data: 123 }
6. Filter and Modify ($filter)
Use $filter to find specific items in arrays (or objects) and apply changes.
// Before: advanced = {
// functions: [
// { id: 'seriesDate', label: 'Original Label' },
// { id: 'other', label: 'Other Label' }
// ]
// }
customizationService.setCustomizations({
'advanced': {
$filter: {
match: { id: 'seriesDate' },
$merge: {
label: 'Updated via Filter',
},
},
},
});
// After: advanced = {
// functions: [
// { id: 'seriesDate', label: 'Updated via Filter' },
// { id: 'other', label: 'Other Label' }
// ]
// }
Note $filter will look recursively for
an object that matches the match criteria and then apply the $merge or $set operation to it.
Note in the example above we are not doing anything with the functions array.
Example with deeply nested filter:
// Before: advanced = {
// functions: [{
// id: 'seriesDate',
// viewFunctions: [
// { id: 'axial', label: 'Original Axial' }
// ]
// }]
// }
customizationService.setCustomizations({
'advanced': {
$filter: {
match: { id: 'axial' },
$merge: {
label: 'Axial (via Filter)',
},
},
},
});
// After: advanced = {
// functions: [{
// id: 'seriesDate',
// viewFunctions: [
// { id: 'axial', label: 'Axial (via Filter)' }
// ]
// }]
// }
Summary of Commands
| Command | Purpose | Example |
|---|---|---|
$set | Replace a value entirely | Replace a list or object |
$push | Append items to an array | Add to the end of a list |
$unshift | Prepend items to an array | Add to the start of a list |
$splice | Insert, remove, or replace at specific index | Modify specific indices in a list |
$merge | Update specific fields in an object | Change a subset of fields |
$apply | Compute the new value dynamically | Apply a function to transform values |
$filter | Find and update specific items in arrays | Target nested structures |
$transform | Apply a function to transform the customization | Apply a function to transform values |
Building Customizations Across Multiple Extensions
Sometimes it is useful to build customizations across multiple extensions. For example, you may want to build a default list of tools inside a vieweport. But then each extension may want to add their own tools to the list.
Lets say i have one default sorting function in my default extension.
function getCustomizationModule() {
return [
{
name: 'default',
value: {
'studyBrowser.sortFunctions': [
{
label: 'Series Number',
sortFunction: (a, b) => {
return a?.SeriesNumber - b?.SeriesNumber;
},
},
],
},
},
];
}
This will result in having only series number as the default sorting function.
but now in another extension let's say dicom-seg extension we can add another sorting function.
function getCustomizationModule() {
return [
{
name: "dicom-seg-sorts",
value: {
"studyBrowser.sortFunctions": {
$push: [
{
label: "Series Date",
sortFunction: (a, b) => {
return a?.SeriesDate - b?.SeriesDate;
},
},
],
},
},
},
];
}
But since the module is not default it will not get applied, but in my segmentation mode i can do
onModeEnter() {
customizationService.setCustomizations([
'@ohif/extension-cornerstone-dicom-seg.customizationModule.dicom-seg-sorts',
]);
}
needless to say if you opted to choose name: default in the getCustomizationModule it was applied globally.
Customizable Parts of OHIF
Below we are providing the example configuration for global scenario (using the configuration file), however, you can also use the setCustomizations method to set the customizations.
ohif.hotkeyBindings
| ID | ohif.hotkeyBindings |
|---|---|
| Description | Defines the hotkeys for the application. |
| Default Value | look at hotkeyBindingsCustomization.ts file |
| Example | |
measurementLabels
| ID | measurementLabels |
|---|---|
| Description | Labels for measurement tools in the viewer that are automatically asked for. |
| Default Value | [] |
| Example | |
cornerstoneViewportClickCommands
| ID | cornerstoneViewportClickCommands |
|---|---|
| Description | Defines the viewport event handlers such as button1, button2, doubleClick, etc. |
| Default Value |
{
"doubleClick": {
"commandName": "toggleOneUp",
"commandOptions": {}
},
"button1": {
"commands": [
{
"commandName": "closeContextMenu"
}
]
},
"button3": {
"commands": [
{
"commandName": "showCornerstoneContextMenu",
"commandOptions": {
"requireNearbyToolData": true,
"menuId": "measurementsContextMenu"
}
}
]
}
} |
| Example | |
cinePlayer
| ID | cinePlayer |
|---|---|
| Description | Customizes the cine player component. |
| Default Value | The CinePlayer component in the UI |
| Example |
cornerstone.windowLevelPresets
| ID | cornerstone.windowLevelPresets |
|---|---|
| Description | Window level presets for the cornerstone viewport. |
| Default Value |
{
"CT": [
{
"description": "Soft tissue",
"window": "400",
"level": "40"
},
{
"description": "Lung",
"window": "1500",
"level": "-600"
},
{
"description": "Liver",
"window": "150",
"level": "90"
},
{
"description": "Bone",
"window": "2500",
"level": "480"
},
{
"description": "Brain",
"window": "80",
"level": "40"
}
],
"PT": [
{
"description": "Default",
"window": "5",
"level": "2.5"
},
{
"description": "SUV",
"window": "0",
"level": "3"
},
{
"description": "SUV",
"window": "0",
"level": "5"
},
{
"description": "SUV",
"window": "0",
"level": "7"
},
{
"description": "SUV",
"window": "0",
"level": "8"
},
{
"description": "SUV",
"window": "0",
"level": "10"
},
{
"description": "SUV",
"window": "0",
"level": "15"
}
]
} |
| Example | |
cornerstone.colorbar
| ID | cornerstone.colorbar |
|---|---|
| Description | Customizes the appearance and behavior of the cornerstone colorbar. |
| Default Value |
{
width: '16px',
colorbarTickPosition: 'left',
colormaps,
colorbarContainerPosition: 'right',
colorbarInitialColormap: DefaultColormap,
}
|
| Example | |
cornerstone.3dVolumeRendering
| ID | cornerstone.3dVolumeRendering |
|---|---|
| Description | Customizes the settings for 3D volume rendering in the cornerstone viewport, including presets and rendering quality range. |
| Default Value |
{
volumeRenderingPresets: VIEWPORT_PRESETS,
volumeRenderingQualityRange: {
min: 1,
max: 4,
step: 1,
},
} |
| Example | |
autoCineModalities
| ID | autoCineModalities |
|---|---|
| Description | Specifies the modalities for which the cine player automatically starts. |
| Default Value | [ "OT", "US" ] |
| Example | |
cornerstone.overlayViewportTools
| ID | cornerstone.overlayViewportTools |
|---|---|
| Description | Configures the tools available in the cornerstone SEG and RT tool groups. |
| Default Value |
{
active: [
{
toolName: toolNames.WindowLevel,
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
},
{
toolName: toolNames.Pan,
bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }],
},
{
toolName: toolNames.Zoom,
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
},
{
toolName: toolNames.StackScroll,
bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
},
],
enabled: [
{
toolName: toolNames.PlanarFreehandContourSegmentation,
configuration: {
displayOnePointAsCrosshairs: true,
},
},
],
} |
| Example | |
layoutSelector.commonPresets
| ID | layoutSelector.commonPresets |
|---|---|
| Description | Defines the default layout presets available in the application. |
| Default Value |
[
{
"icon": "layout-common-1x1",
"commandOptions": {
"numRows": 1,
"numCols": 1
}
},
{
"icon": "layout-common-1x2",
"commandOptions": {
"numRows": 1,
"numCols": 2
}
},
{
"icon": "layout-common-2x2",
"commandOptions": {
"numRows": 2,
"numCols": 2
}
},
{
"icon": "layout-common-2x3",
"commandOptions": {
"numRows": 2,
"numCols": 3
}
}
] |
| Example | |
layoutSelector.advancedPresetGenerator
| ID | layoutSelector.advancedPresetGenerator |
|---|---|
| Description | Generates advanced layout presets based on hanging protocols. |
| Default Value |
({ servicesManager }) => {
// by default any hanging protocol that has isPreset set to true will be included
// a function that returns an array of presets
// of form {
// icon: 'layout-common-1x1',
// title: 'Custom Protocol',
// commandOptions: {
// protocolId: 'customProtocolId',
// },
// disabled: false,
// }
} |
| Example | |
dicomUploadComponent
| ID | dicomUploadComponent |
|---|---|
| Description | Customizes the appearance and behavior of the dicom upload component. |
| Default Value | The DicomUpload component in the UI |
| Example |
onBeforeSRAddMeasurement
| ID | onBeforeSRAddMeasurement |
|---|---|
| Description | Customizes the behavior of the SR measurement before it is added to the viewer. |
| Default Value | |
| Example | |
onBeforeDicomStore
| ID | onBeforeDicomStore |
|---|---|
| Description | A hook that modifies the DICOM dictionary before it is stored. The customization should return the modified DICOM dictionary. |
| Default Value | |
| Example | |
sortingCriteria
| ID | sortingCriteria |
|---|---|
| Description | Defines the series sorting criteria for hanging protocols. Note that this does not affect the order in which series are displayed in the study browser. |
| Default Value |
function seriesInfoSortingCriteria(firstSeries, secondSeries) {
const aLowPriority = isLowPriorityModality(firstSeries.Modality ?? firstSeries.modality);
const bLowPriority = isLowPriorityModality(secondSeries.Modality ?? secondSeries.modality);
if (aLowPriority) {
// Use the reverse sort order for low priority modalities so that the
// most recent one comes up first as usually that is the one of interest.
return bLowPriority ? defaultSeriesSort(secondSeries, firstSeries) : 1;
} else if (bLowPriority) {
return -1;
}
return defaultSeriesSort(firstSeries, secondSeries);
} |
| Example | |
customOnDropHandler
| ID | customOnDropHandler |
|---|---|
| Description | CustomOnDropHandler in the viewport grid enables users to handle additional functionalities during the onDrop event in the viewport. |
| Default Value | |
| Example | |
ui.notificationComponent
| ID | ui.notificationComponent |
|---|---|
| Description | Define the component which is used to render viewport notifications |
| Default Value | Default Notification component in viewport |
| Example | |
ui.loadingIndicatorTotalPercent
| ID | ui.loadingIndicatorTotalPercent |
|---|---|
| Description | Customizes the LoadingIndicatorTotalPercent component. |
| Default Value | null |
| Example | |
ui.loadingIndicatorProgress
| ID | ui.loadingIndicatorProgress |
|---|---|
| Description | Customizes the LoadingIndicatorProgress component. |
| Default Value | null |
| Example | |
ui.progressLoadingBar
| ID | ui.progressLoadingBar |
|---|---|
| Description | Customizes the ProgressLoadingBar component. |
| Default Value | null |
| Example | |
ui.viewportActionCorner
| ID | ui.viewportActionCorner |
|---|---|
| Description | Customizes the viewportActionCorner component. |
| Default Value | null |
| Example | |
ui.contextMenu
| ID | ui.contextMenu |
|---|---|
| Description | Customizes the Context menu component. |
| Default Value | null |
| Example | |
ui.labellingComponent
| ID | ui.labellingComponent |
|---|---|
| Description | Customizes the labelling flow component. |
| Default Value | null |
| Example | |
viewportDownload.warningMessage
| ID | viewportDownload.warningMessage |
|---|---|
| Description | Customizes the warning message for the viewport download form. |
| Default Value |
{
"enabled": true,
"value": "Not For Diagnostic Use"
} |
| Example | |
ohif.captureViewportModal
| ID | ohif.captureViewportModal |
|---|---|
| Description | The modal for capturing the viewport image. |
| Default Value | Our own default component |
| Example | |
ohif.aboutModal
| ID | ohif.aboutModal |
|---|---|
| Description | The About modal |
| Default Value | Our own default component |
| Example | |
viewportDownload.warningMessage
| ID | viewportDownload.warningMessage |
|---|---|
| Description | Customizes the warning message for the viewport download form. |
| Default Value |
{
"enabled": true,
"value": "Not For Diagnostic Use"
} |
| Example | |
ohif.captureViewportModal
| ID | ohif.captureViewportModal |
|---|---|
| Description | The modal for capturing the viewport image. |
| Default Value | Our own default component |
| Example | |
ohif.aboutModal
| ID | ohif.aboutModal |
|---|---|
| Description | The About modal |
| Default Value | Our own default component |
| Example | |
viewportNotification.beginTrackingMessage
| ID | viewportNotification.beginTrackingMessage |
|---|---|
| Description | Define the content to be displayed in begin tracking prompt |
| Default Value | Track measurements for this series? |
| Example | |
viewportNotification.trackNewSeriesMessage
| ID | viewportNotification.trackNewSeriesMessage |
|---|---|
| Description | Define the content to be displayed in track new series prompt |
| Default Value | Do you want to add this measurement to the existing report? |
| Example | |
viewportNotification.discardSeriesMessage
| ID | viewportNotification.discardSeriesMessage |
|---|---|
| Description | Define the content to be displayed in discard series prompt |
| Default Value | You have existing tracked measurements. What would you like to do with your existing tracked measurements? |
| Example | |
viewportNotification.trackNewStudyMessage
| ID | viewportNotification.trackNewStudyMessage |
|---|---|
| Description | Define the content to be displayed in track new study prompt |
| Default Value | Track measurements for this series? |
| Example | |
viewportNotification.discardStudyMessage
| ID | viewportNotification.discardStudyMessage |
|---|---|
| Description | Define the content to be displayed in discard study prompt |
| Default Value | Measurements cannot span across multiple studies. Do you want to save your tracked measurements? |
| Example | |
viewportNotification.hydrateSRMessage
| ID | viewportNotification.hydrateSRMessage |
|---|---|
| Description | Define the content to be displayed in hydrate SR prompt |
| Default Value | Do you want to continue tracking measurements for this study? |
| Example | |
viewportNotification.hydrateRTMessage
| ID | viewportNotification.hydrateRTMessage |
|---|---|
| Description | Define the content to be displayed in hydrate RT prompt |
| Default Value | Do you want to open this Segmentation? |
| Example | |
viewportNotification.hydrateSEGMessage
| ID | viewportNotification.hydrateSEGMessage |
|---|---|
| Description | Define the content to be displayed in hydrate SEG prompt |
| Default Value | Do you want to open this Segmentation? |
| Example | |
viewportNotification.discardDirtyMessage
| ID | viewportNotification.discardDirtyMessage |
|---|---|
| Description | Define the content to be displayed in hydrate SR prompt |
| Default Value | There are unsaved measurements. Do you want to save it? |
| Example | |
measurement.promptBeginTracking
| ID | measurement.promptBeginTracking |
|---|---|
| Description | Define the functionality to connect with the measurement tracking machine on begin measurement tracking |
| Default Value | promptBeginTracking |
| Example | |
measurement.promptHydrateStructuredReport
| ID | measurement.promptHydrateStructuredReport |
|---|---|
| Description | Define the functionality to connect with the measurement tracking machine on hydrate SR |
| Default Value | promptHydrateStructuredReport |
| Example | |
measurement.promptTrackNewSeries
| ID | measurement.promptTrackNewSeries |
|---|---|
| Description | Define the functionality to connect with the measurement tracking machine on tracking new series |
| Default Value | promptTrackNewSeries |
| Example | |
measurement.promptTrackNewStudy
| ID | measurement.promptTrackNewStudy |
|---|---|
| Description | Define the functionality to connect with the measurement tracking machine on tracking new study |
| Default Value | promptTrackNewStudy |
| Example | |
measurement.promptLabelAnnotation
| ID | measurement.promptLabelAnnotation |
|---|---|
| Description | Define the functionality to connect with the measurement tracking machine on begin measurement tracking |
| Default Value | promptLabelAnnotation |
| Example | |
measurement.promptSaveReport
| ID | measurement.promptSaveReport |
|---|---|
| Description | Define the functionality to connect with the measurement tracking machine on save SR report |
| Default Value | promptSaveReport |
| Example | |
measurement.promptHasDirtyAnnotations
| ID | measurement.promptHasDirtyAnnotations |
|---|---|
| Description | Define the functionality to connect with the measurement tracking machine on there are dirty annotations |
| Default Value | promptHasDirtyAnnotations |
| Example | |