Skip to content

[DTOSS-12327] - Render image markers on breast features diagram#1100

Merged
colinrotherham merged 16 commits intomainfrom
DTOSS-12327-render-markers
Mar 19, 2026
Merged

[DTOSS-12327] - Render image markers on breast features diagram#1100
colinrotherham merged 16 commits intomainfrom
DTOSS-12327-render-markers

Conversation

@colinrotherham
Copy link
Contributor

Description

This PR uses the persisted region X/Y data to render image markers

Breast.features.diagram.mp4

Jira link

Review notes

In draft whilst I sort out some failing tests

Review checklist

  • Check database queries are correctly scoped to current_provider

@github-actions
Copy link

github-actions bot commented Mar 3, 2026

The review app at this URL has been deleted:
https://pr-1100.manage-breast-screening.non-live.screening.nhs.uk

@colinrotherham colinrotherham marked this pull request as ready for review March 3, 2026 12:52
@colinrotherham colinrotherham requested a review from MatMoore March 3, 2026 12:52
this.$image.addEventListener('mousemove', this.onMouseMove.bind(this))
this.$image.addEventListener('mouseleave', this.onMouseLeave.bind(this))
this.$image.addEventListener('click', this.onClick.bind(this))
if (!readOnly) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have a unit test for this case?

const point = this.$imageMap.createPoint(x, y, id)
const region = this.$imageMap.createRegion($path, point)

this.setMarker(region, index)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is features still the right name for this, now that we have markers as well? They seem like different representations of the same thing.

Also, having all these side effects inside a getter feels like a code smell to me. Perhaps we could turn features into an attribute and then onUpdate calls a method to recompute it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree entirely

I've pushed up a change to use event listeners

  1. Breast diagram listens via addEventListener()
  2. Breast diagram handles this.onClick() when image map dispatches click
  3. Breast diagram calls this.add() to modify values

I've temporarily added 4124fa2 so you can see this.remove() in action too

this.$imageMap.setState('active', feature.region, null)
const { $imageMap, features, markers } = this

// Remove excess markers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm finding this code quite difficult to follow.

From the name, I expected render() to be called any time something changes, but actually it's just called one time as part of initialising the diagram.

It looks almost as if markers should be empty at this point, but it's populated as a side effect of accessing this.features. Then we are removing some markers from $root but it's not clear why we would have extra markers at this point.

I think this complexity comes from having to keep in sync multiple different things, and having to do that throughout the whole class. To reason about the correctness of the code you have to consider:

  • the values array
  • the features array
  • the image map's state (a region is active or not active)
  • the markers array
  • whether the marker is added to $root or not

I don't know if there's a way we could either reduce the amount of state, or encapsulate the state changes in add/update/remove methods so that these things all change at the same time?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this feedback, you're right

My latest push makes sure:

  • Image map only needs to worry about regions (SVG paths)
  • Feature diagram only needs to worry about markers

Regarding this bit:

// Remove excess markers
for (const marker of markers.splice(values.length)) {
  marker.$root.remove()
}

I've used a similar technique to particle systems in games, to keep memory usage down

Rather than create, append and position every marker on every render, I've reused them

Similarly when features are removed from the diagram values.length is reduced, so we .remove() unused markers over that length rather than clearing and creating them all again

* @param {Partial<ImageMarkerConfig>} [config] - Image marker config
*/
constructor($root, config = {}) {
$root = $root ?? document.createElement(config.href ? 'a' : 'div')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe worth adding a comment to explain that div is for the readonly version. Sidenote: is <div> ok to use here or should it be <g> or something specific to svg?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good point

Are we still happy with the proposal to use <div> for readonly and <a> <button> otherwise?

* @param {DOMPoint} point - SVG point at pointer coordinates
*/
set point(point) {
const offsetX = Math.min(Math.max(point.x, 20), this.config.width - 20)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the significance of 20 here? We're bounding the point to within 20px of the border?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

20px is the width of the gutter

I've split out the code and added some comments in 5fb6b76

i.e. All clicks within the gutter are rounded 20px away from the image map edges

It makes sure we don't lose the makers near the edges:

Marker in gutter, top left Marker in gutter, bottom right

cursor: crosshair;
}

input[readonly] ~ .app-breast-diagram__image-map .app-breast-diagram__svg {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly worth creating a specific class like app-breast-diagram__image-map--readonly rather than depending strictly on the html structure for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in ecc909f and f3a0be0

Copy link
Contributor

@MatMoore MatMoore left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the slow reply. I think it looks a lot clearer now post-refactor. Lets merge (once the temporary commit is removed) 🚢

I guess next part is the selection of feature type and the legend on the right?

In the meantime I'll finish up #1196

"method": "post",
"data-module": "app-breast-diagram",
"data-debug": {
"value": "true" if "debug" in request.GET else false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you've got a mix of string and bool here. If the intention is for value to be boolean you can simplify the expression to "debug" in request.GET

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's intentional

We need an actual attribute value for the component config to pick up

  • When true it appends data-debug
  • When "true" it appends data-debug="true"
  • When false it skips the attribute

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah got it, all good then 👍🏻

@colinrotherham
Copy link
Contributor Author

Thanks @MatMoore

You'll see these two variables in mammograms/medical_information/breast_features/form.jinja

{% set diagram_debug = "debug" in request.GET %}
{% set diagram_read_only = false %}

They become data-debug="true" and/or data-read-only="true" on app-breast-diagram

Ready for you to wire up in #1109

@colinrotherham colinrotherham force-pushed the DTOSS-12327-render-markers branch from dd6dbef to be7ebb1 Compare March 19, 2026 18:35
@sonarqubecloud
Copy link


if (!markers[index]) {
markers[index] = new ImageMarker(null, {
href: $input.readOnly ? undefined : `#marker-${index + 1}`,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can address where the marker links to (or if we use a button) when adding the form fields

@colinrotherham colinrotherham merged commit 6c62d1c into main Mar 19, 2026
13 checks passed
@colinrotherham colinrotherham deleted the DTOSS-12327-render-markers branch March 19, 2026 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants