Skip to main content

Custom Result Display

Authors of CIRCUS CS plug-ins can provide a fully custom display (view) to display plug-in results.

A custom display is essentially a React component (JavaScript file) that follows certain rules and is bundled using Webpack. The bundled files are then included in the plug-in Docker image file.

We use Webpack's Module Federation mechanism to dynamically load your custom display from the main CIRCUS CS app.

note

Your custom display will be used only from a results page of the plug-in that provides the display. You cannot share custom displays across multiple plug-ins.

Creating a Sample Plug-in Using Our Starter Template​

The easiest way to get started is to use our starter kit hosted on GitHub.

mkdir my-circus-plugin
cd my-circus-plugin
npx @utrad-ical/create-circus-cad-plugin -i
note

npx is a script runner that downloads and executes a package directly from the NPM registry.

This will generate the following files along with dependencies:

πŸ“‚my-circus-plugin
β”œβ”€β”€ README.md
β”œβ”€β”€ πŸ“‚node_modules
β”œβ”€β”€ package.json
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ postbuild.sh
β”œβ”€β”€ server.js
β”œβ”€β”€ tsconfig.json
β”œβ”€β”€ webpack.config.js
β”œβ”€β”€ πŸ“‚data
β”‚ β”œβ”€β”€ results.json
β”‚ └── sample.png
β”œβ”€β”€ πŸ“‚docker
β”‚ β”œβ”€β”€ Dockerfile
β”‚ β”œβ”€β”€ plugin.json
β”‚ └── πŸ“‚apps
β”‚ β”œβ”€β”€ cad.js
β”‚ └── sample.png
β”œβ”€β”€ πŸ“‚public
β”‚ └── index.html
└── πŸ“‚src
β”œβ”€β”€ App.tsx
β”œβ”€β”€ bootstrap.tsx
β”œβ”€β”€ index.ts
β”œβ”€β”€ sampleJob.json
└── πŸ“‚components
└── SampleViewer.tsx
  • data contains mock CIRCUS CS plug-in result files (used to check your custom display in the local environment)
  • docker contains files to build Docker image from your CIRCUS CS plug-in and your custom display
  • public contains index.html to check your custom display in the local environment
  • src contains source code for building a custom display.

Place Your Main Executable at docker/apps​

Under docker/apps/, you will see cad.js, which is a sample program that only outputs dummy results. Replace it with your main executable (written in any language you like). Read this page for the details.

Edit Dockerfile and Manifest File at docker​

  • Edit the plug-in manifest file: ./docker/plugin.json
  • Modify Dockerfile to specify your executable file: ./docker/Dockerfile
  • Modify veiwer component: ./webpack.config.js, src/components/SampleViewer.tsx
  • Add sample result files to check your viewer in the local environment: ./data, src/sampleJob.json

Edit ./webpack.config.js and ./docker/plugin.json​

Your custom display will be stored in a Docker image and has to be consumed by a running CIRCUS web app. To achieve this, you need to specify which module (React component) to expose to CIRCUS CS.

For example, if your module name is named as "MyXyzVisualizer" and stored in src/components/MyXyzVisualizer.tsx, declare it webpack.config.js:

webpack.config.js
exposes: {
"./MyXyzVisualizer": "[the path to the module]",
},

Note that the module name must be prefixed with ./. You can specify as many modules as you want here. Your React component must be exported as an default export using the export default ES5 syntax.

To use the exposed module in your result screen, use the following syntax in the plugin.json manifest file:

docker/plugin.json
{
...,
"displayStrategy": [
{
"type": "@MyXyzVisualizer",
...,
}
]
}

Your custom display name must be prefixed with @. This prefix tells CIRCUS that you want to use a custom component stored in a plug-in image file instead of one of our built-in displays.

info

Do not touch these ModuleFederationPlugin options: name, library, and filename.

webpack.config.js
name: "CircusCsModule",
library: {
name: "CircusCsModule",
type: "window",
},
filename: "remoteEntry.js",

Check your custom display in the local environment​

Save result files to send to your custom display. The following files need to be changed.
data: Simulated directory to save output files of your CIRCUS CS plug-in.
src/sampleJob.json: Simulation of the most of the important data provided via useCsResults() custom hook. Read here for the details.

Run mock REST API server. This server can send files in data. The default port number is 3000.

terminal 1
npm run json-server

Run webpack-dev-server to check your custom display. Rename a module name and a file name of import statement to your module name in src/App.tsx. The default port number is 3002.

terminal 2
npm start

Check your custom display via http://localhost:3002.

Shared Modules​

The following modules are shared among the webpack containers; you can import these directly without worrying bundle size bloating or duplicate instances.

  • react
  • react-dom
  • styled-components
  • @utrad-ical/circus-rs
  • @utrad-ical/circus-ui-kit

Other modules are not shared. You can still pack them into your bundle.

Accessing the CAD data​

Most of the important data are provided via useCsResults() custom hook, which returns an object like this:

interface CsResultsContextType {
job: Job;
plugin: Plugin;
consensual: boolean;
editable: boolean;
loadAttachment: PluginAttachmentLoader;
eventLogger: EventLogger;
rsHttpClient: RsHttpClient;
getVolumeLoader: (series: SeriesDefinition) => DicomVolumeLoader;
loadDisplay: <O extends object, F>(name: string) => Promise<Display<O, F>>;
}

export interface Job {
jobId: string;
userEmail: string;
pluginId: string;
series: SeriesDefinition[];
feedbacks: FeedbackEntry<any>[];
createdAt: string;
finishedAt: string;
results: any; // The data in results.json
}
  • The data the plug-in wrote to results.json is located at job.results. This is always available when this display is invoked.
  • Other arbitrary files output by the plug-in (e.g., PDF) can be asynchronously fetched via authorized HTTP requests. You can use either the loadAttachment function or the useAttachment custom hook (which is a wrapper around loadAttachment).
info

Use only function components. The useCsResults() hook does not work in React class components.

Declaratively Load Data Using usePluginAttachment​

usePluginAttachment (obtained via '@utrad-ical/circus-ui-kit') is a React custom hook that allows you to load an arbitrary file from the CAD results directory in a declarative manner. This is the easiest approach when you only need to load a few files with known names.

import { usePluginAttachment } from '@utrad-ical/circus-ui-kit';

// Function component
const Results = () => {
const csvDataStr = usePluginAttachment('my-data.csv', 'text');

if (!csvDataStr) return <div>Loading...</div>;
if (csvDataStr instanceof Error) return <div>An error happened</div>;

return <div>{csvDataStr}</div>;
};

export default Results;

Note that the data you requested will be fetched from the network and arrives asynchronously. The returned data from usePluginAttachment can be one of the following types:

  • undefined: When the data has not been loaded yet.
  • Error: When an error has happened while the loading process (e.g., a wrong file name was passed).
  • string | object | ArrayBuffer: When the data has been successfully loaded.

If you want to load more than one file, simply use usePluginAttachment multiple times.

const DisplayLoadingTwoFiles = () => {
const data1 = usePluginAttachment('data1.txt', 'text');
const data2 = usePluginAttachment('data2.txt', 'text');

if (!data1 || !data2) return <div>Loading...</div>;
if (data1 instanceof Error || data2 instanceof Error) return <div>Error</div>;

return (
<ul>
<li>{data1}</li>
<li>{data2}</li>
</ul>
);
};

export default DisplayLoadingTwoFiles;

Flexibly Load Data Using loadAttachment​

loadAttachment (can be obtained via useCsResults) is an async function with which you can load arbitrary files from the plug-in result directory. It asynchronously returns a JavaScript Response object. usePluginAttachment uses this under the hood, too. This requires a deeper knowledge of React and the DOM, but enables more flexible loading approaches.

The following example does the same thing as the previous example.

import { useState, useEffect } from 'react';
import { useCsResults } from '@utrad-ical/circus-ui-kit';

const Results = () => {
const { loadAttachment } = useCsResults();
const [csvDataStr, setCsvDataStr] = useState(undefined);
const [error, setError] = useState(undefined);

useEffect(() => {
const load = async () => {
try {
const response = await loadAttachment('my-data.csv');
setCsvDataStr(await response.text());
} catch (err) {
setError(err);
}
};
load();
}, []);

if (!csvDataStr) return <div>Loading...</div>;
if (error) return <div>An error happened</div>;
return <div>{csvDataStr}</div>;
};

Collecting User Feedback​

When you want to make your display support feedback collection, use initialFeedbackValue, onFeedbackChange and personalOpinions coming from the props.

interface DisplayProps<O extends object, F extends unknown> {
initialFeedbackValue: F | undefined;
personalOpinions: readonly FeedbackEntry<F>[];
options: O;
onFeedbackChange: (status: FeedbackReport<F>) => void;
children?: React.ReactNode;
}

interface ValidFeedbackReport<T> {
valid: true;
value: T;
}

interface InvalidFeedbackReport {
valid: false;
error?: any;
}

type FeedbackReport<T> = ValidFeedbackReport<T> | InvalidFeedbackReport;

The mechanism is different from the plain controlled component pattern in that initialValue will not be updated between re-renders. That is, you have to manage the current feedback data the user is editing inside your display's state, as an uncontrolled component.

To tell the CIRCUS system that your display wants to collect users' feedback, call onFeedbackChange on initial render. Then, whenever the user makes an edit to your display, report its validation status using onFeedbackChange, usually in combination with useEffect(..., [currentFeedback]). For example, if you want your display to collect a score from 1 to 5 from a user, first call onFeedbackChange({ value: false, error: 'Input your evaluation' }) on initial render. Manage the selection state typically using the useState hook, and when the user selects their score, call something like onFeedbackChange({ valid: true, valie: 4 }).

If your display doesn't need to collect feedback at all, simply avoid calling onFeedbackChange. CIRCUS CS allows the user to register the feedback when no display is blocking it by the valid: false status.

You can report any data as feedback as long as they are JSON-serializable. Do not return non-serializable data such as a Date, Map or symbol.

This is a sample display which collects numerical feedback (greater than 3) from the user.

const NumberInputDisplay = props => {
const { initialFeedbackValue, onFeedbackChange, personalOpinions } = props;
// editable will be false when a feedback entry has been already registered.
const { editable, consensual } = useCsResults();

// Use function-style initializer
const [currendFeedback, setCurrentFeedback] = useState(() => {
// If initialFeedback is set, use it
if (typeof initialFeedbackValue === 'number') return initialFeedbackValue;
// Perform personal feedback integration when the user enters consensual mode.
// Here, we calculate the mean of the values entered in personal mode.
if (consensual && editable) {
const sum = personalOpinions.map(o => o.data).reduce((a, b) => a + b, 0);
return sum / personalOpinions.length;
}
// Otherwise, just return the default value used when a user
// first enters this display.
return 0;
});

// Report whether the current status is valid every time it changes.
useEffect(() => {
if (currentFeedback > 3) {
onFeedbackChange({ valid: true, value: currentFeedback });
} else {
onFeedbackChange({ valid: false, error: 'Must be greater than 3' });
}
}, [currentFeedback]);

return (
<label>
Number larger than 3:{' '}
<input
type="number"
disabled={!editable}
value={currentFeedback}
onChange={ev => setCurrentFeedback(Number(ev.target.value))}
/>
</label>
);
};
info

When you integrate personal feedback entries, use props.personalOpinions rathern than job.feedbacks from useCsResults(). These seem similar, but the latter contains all the feed data including data from other displays, and you cannot determine which part of the data is relevant to your display.