Skip to main content
Version: 3.10.0-beta.80 (Latest)

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.

note

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:

  1. 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.

    info

    You 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.

  2. 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).

  3. Overriding from Outside To customize this list outside your extension, call setCustomizations with 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 $set syntax 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:
    1. Via Extensions:
      • Implement a getCustomizationModule function in your extension and name it default.
      function getCustomizationModule() {
      return [
      {
      name: 'default',
      value: {
      'studyBrowser.sortFunctions': {
      $set: [
      {
      label: 'Default Sort Function',
      sortFunction: (a, b) => a.SeriesDate - b.SeriesDate,
      },
      ],
      },
      },
      },
      ];
      }
    2. Using the setCustomizations Method:
      • Call setCustomizations in your application and specify CustomizationScope.Default as the second argument:
      customizationService.setCustomizations(
      {
      'studyBrowser.sortFunctions': {
      $set: [
      {
      label: 'Default Sort Function',
      sortFunction: (a, b) => a.SeriesDate - b.SeriesDate,
      },
      ],
      },
      },
      CustomizationScope.Default
      );

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 setCustomizations method 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:
    1. Add global customizations directly to the application's configuration file:

      window.config = {
      name: 'config/default.js',
      routerBasename: '/',
      customizationService: [
      {
      'studyBrowser.sortFunctions': {
      $push: [
      {
      label: 'Global Sort Function',
      sortFunction: (a, b) => b.SeriesDate - a.SeriesDate,
      },
      ],
      },
      },
      ],
      };
    2. 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: '/',
      customizationService: [
      '@ohif/extension-cornerstone.customizationModule.newCustomization',
      ],
      };
      • In this example, the newCustomization module within the @ohif/extension-cornerstone extension 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:

  1. Global Scope: Takes precedence if defined.
  2. Mode Scope: Used if no global customization is defined.
  3. 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

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โ€‹

CommandPurposeExample
$setReplace a value entirelyReplace a list or object
$pushAppend items to an arrayAdd to the end of a list
$unshiftPrepend items to an arrayAdd to the start of a list
$spliceInsert, remove, or replace at specific indexModify specific indices in a list
$mergeUpdate specific fields in an objectChange a subset of fields
$applyCompute the new value dynamicallyApply a function to transform values
$filterFind and update specific items in arraysTarget nested structures
$transformApply a function to transform the customizationApply 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.

measurementLabels

IDmeasurementLabels
Description
Labels for measurement tools in the viewer that are automatically asked for.
Default Value
[]
Example

window.config = {
  // rest of window config
  customizationService: [
    {
      measurementLabels: {
        $set: {
          labelOnMeasure: true,
          exclusive: true,
          items: [
            { value: 'Head', label: 'Head' },
            { value: 'Shoulder', label: 'Shoulder' },
            { value: 'Knee', label: 'Knee' },
            { value: 'Toe', label: 'Toe' },
          ],
        },
      },
    },
  ],
};
    

cornerstoneViewportClickCommands

IDcornerstoneViewportClickCommands
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

window.config = {
  // rest of window config
  customizationService: [
    {
      cornerstoneViewportClickCommands: {
        doubleClick: {
          $push: [
            () => {
              console.debug('double click');
            },
          ],
        },
      },
    },
  ],
};
    

cinePlayer

IDcinePlayer
Description
Customizes the cine player component.
Default Value
The CinePlayer component in the UI
Example

cornerstone.windowLevelActionMenu

IDcornerstone.windowLevelActionMenu
Description
Window level action menu for the cornerstone viewport.
Default Value
null
Example

      window.config = {
        // rest of window config
        customizationService: [
          {
            'cornerstone.windowLevelActionMenu': {
                $set: CustomizedComponent,
            },
          },
        ],
      };
    

cornerstone.windowLevelPresets

IDcornerstone.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

window.config = {
  // rest of window config
  customizationService: [
    {
      'cornerstone.windowLevelPresets': {
        $filter: {
          match: { id: 'ct-soft-tissue' },
          $merge: {
            window: '500',
            level: '50',
          },
        },
      },
    },
  ],
};
    

cornerstone.colorbar

IDcornerstone.colorbar
Description
Customizes the appearance and behavior of the cornerstone colorbar.
Default Value

     {
      width: '16px',
      colorbarTickPosition: 'left',
      colormaps,
      colorbarContainerPosition: 'right',
      colorbarInitialColormap: DefaultColormap,
    }
    
Example

window.config = {
  // rest of window config
  customizationService: [
    {
      'cornerstone.colorbar': {
        $merge: {
          width: '20px',
          colorbarContainerPosition: 'left',
        },
      },
    },
  ],
};
    

cornerstone.3dVolumeRendering

IDcornerstone.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

window.config = {
  // rest of window config
  customizationService: [
    {
      'cornerstone.3dVolumeRendering': {
        $merge: {
          volumeRenderingQualityRange: {
            min: 2,
            max: 6,
            step: 0.5,
          },
        },
      },
    },
  ],
};
    

autoCineModalities

IDautoCineModalities
Description
Specifies the modalities for which the cine player automatically starts.
Default Value
[
  "OT",
  "US"
]
Example

window.config = {
  // rest of window config
  customizationService: [
    {
      'autoCineModalities': {
        $set: ['OT', 'US', 'MR'], // Adds 'MR' as an additional modality for auto cine playback
      },
    },
  ],
};
  

cornerstone.overlayViewportTools

IDcornerstone.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

IDlayoutSelector.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

window.config = {
  // rest of window config
  customizationService: [
    {
      'layoutSelector.commonPresets': {
        $set: [
          {
            icon: 'layout-common-1x1',
            commandOptions: {
              numRows: 1,
              numCols: 1,
            },
          },
          {
            icon: 'layout-common-1x2',
            commandOptions: {
              numRows: 1,
              numCols: 2,
            },
          },
        ],
      },
    },
  ],
};
  

layoutSelector.advancedPresetGenerator

IDlayoutSelector.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

window.config = {
  // rest of window config
  customizationService: [
    {
      'layoutSelector.advancedPresetGenerator': {
        $apply: (defaultGenerator) => {
          return ({ servicesManager }) => {
            const presets = defaultGenerator({ servicesManager });

            // Add a custom preset for a specific hanging protocol
            presets.push({
              icon: 'custom-icon',
              title: 'Custom Protocol',
              commandOptions: {
                protocolId: 'customProtocolId',
              },
              disabled: false,
            });

            return presets;
          };
        },
      },
    },
  ],
};
  

dicomUploadComponent

IDdicomUploadComponent
Description
Customizes the appearance and behavior of the dicom upload component.
Default Value
The DicomUpload component in the UI
Example

onBeforeSRAddMeasurement

IDonBeforeSRAddMeasurement
Description
Customizes the behavior of the SR measurement before it is added to the viewer.
Default Value
Example

window.config = {
  // rest of window config
  customizationService: [
    {
      onBeforeSRAddMeasurement: {
        $set: ({ measurement, StudyInstanceUID, SeriesInstanceUID }) => {
          // Note: it should return measurement
          console.debug('onBeforeSRAddMeasurement');
          return measurement;
        },
      },
    },
  ],
};
    

onBeforeDicomStore

IDonBeforeDicomStore
Description
A hook that modifies the DICOM dictionary before it is stored. The customization should return the modified DICOM dictionary.
Default Value
Example

window.config = {
  // rest of window config
  customizationService: [
    {
      'onBeforeDicomStore': {
        $set: ({ dicomDict, measurementData, naturalizedReport }) => {
          // Example customization: Add a custom tag to the DICOM dictionary
          dicomDict['0010,0010'] = 'CustomizedPatientName'; // Patient's Name (example)
          dicomDict['0008,103E'] = 'CustomStudyDescription'; // Study Description (example)

          // Return the modified DICOM dictionary
          return dicomDict;
        },
      },
    },
  ],
};
  

sortingCriteria

IDsortingCriteria
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

window.config = {
  // rest of window config
  customizationService: [
    {
      'sortingCriteria': {
        $set: function customSortingCriteria(firstSeries, secondSeries) {

          return someSort(firstSeries, secondSeries);
        },
      },
    },
  ],
};
  

customOnDropHandler

IDcustomOnDropHandler
Description
CustomOnDropHandler in the viewport grid enables users to handle additional functionalities during the onDrop event in the viewport.
Default Value
Example

window.config = {
  // rest of window config
  customizationService: [
    {
      customOnDropHandler: {
        $set: customOnDropHandler
      },
    },
  ],
};