You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by jo...@apache.org on 2026/10/15 11:53:41 UTC

(causeway) branch Extension-D3 created (now df975a8fdb0)

This is an automated email from the ASF dual-hosted git repository.

joerg-rade pushed a change to branch Extension-D3
in repository https://gitbox.apache.org/repos/asf/causeway.git


      at df975a8fdb0 [CAUSEWAY-4074] first diagram in domox renders

This branch includes the following new commits:

     new 932ac7300ff [CAUSEWAY-4074] initial
     new 135e32c84a8 [CAUSEWAY-4074] rendering gives clues in case of missing/wrong data
     new 82aacd6b82e [CAUSEWAY-4074] some warnings fixed
     new 057d0ad5fb6 [CAUSEWAY-4074] doc updated
     new df975a8fdb0 [CAUSEWAY-4074] first diagram in domox renders

The 5 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(causeway) 01/05: [CAUSEWAY-4074] initial

Posted by jo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

joerg-rade pushed a commit to branch Extension-D3
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 932ac7300ff6849b79a8a872e490ae567c7c88ad
Author: Joerg Rade <jo...@gmail.com>
AuthorDate: Wed Oct 14 11:12:08 2026 +0200

    [CAUSEWAY-4074] initial
---
 extensions/pom.xml                                 |   1 +
 extensions/vw/d3/applib/pom.xml                    |  50 +++
 .../d3/applib/CausewayModuleExtD3Applib.java       |  38 ++
 .../extensions/d3/applib/annotations/D3Graph.java  |  74 ++++
 .../extensions/d3/applib/config/D3Config.java      | 125 +++++++
 .../extensions/d3/applib/spi/D3GraphAdvisor.java   |  61 ++++
 extensions/vw/d3/metamodel/.gitignore              |   2 +
 extensions/vw/d3/metamodel/pom.xml                 |  51 +++
 .../d3/metamodel/CausewayModuleExtD3MetaModel.java |  36 ++
 .../d3/metamodel/facet/D3GraphFacet.java           |  37 ++
 .../d3/metamodel/facet/D3GraphFacetAbstract.java   |  57 +++
 .../facet/D3GraphFacetFromAnnotation.java          | 120 +++++++
 .../facet/D3GraphFacetFromAnnotationFactory.java   |  73 ++++
 extensions/vw/d3/pom.xml                           |  74 ++++
 extensions/vw/d3/wicket/D3_WICKET_UI_OVERVIEW.md   | 398 +++++++++++++++++++++
 extensions/vw/d3/wicket/D3_WICKET_UI_STATUS.md     | 389 ++++++++++++++++++++
 extensions/vw/d3/wicket/integration/pom.xml        |  62 ++++
 .../CausewayModuleExtD3WicketIntegration.java      |  29 ++
 .../integration/callback/CallbackWithHandler.java  |  23 ++
 .../wkt/integration/callback/D3AjaxCallback.java   |  57 +++
 .../integration/callback/NodeClickedCallback.java  |  63 ++++
 .../wkt/integration/callback/NodeClickedEvent.java |  32 ++
 .../d3/wkt/integration/components/D3Panel.html     |  30 ++
 .../d3/wkt/integration/components/D3Panel.java     | 102 ++++++
 .../integration/res/D3IntegrationReference.java    |  58 +++
 .../d3/wkt/integration/res/D3Reference.java        |  50 +++
 .../d3/wkt/integration/res/d3-integration.mjs      | 176 +++++++++
 extensions/vw/d3/wicket/pom.xml                    |  41 +++
 extensions/vw/d3/wicket/ui/pom.xml                 | 103 ++++++
 .../d3/wkt/ui/CausewayModuleExtD3WicketUi.java     |  42 +++
 .../extensions/d3/wkt/ui/components/D3Panel.java   | 121 +++++++
 .../wkt/ui/components/D3PanelComponentFactory.java |  67 ++++
 .../ui/components/D3PanelComponentFactoryTest.java | 175 +++++++++
 .../viewer/commons/model/webjar/WebJar.java        |   3 +-
 34 files changed, 2819 insertions(+), 1 deletion(-)

diff --git a/extensions/pom.xml b/extensions/pom.xml
index 6482f2773bc..73403c4c49d 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -95,6 +95,7 @@
 		<module>vw/sse</module>
 		<module>vw/fullcalendar</module>
 		<module>vw/pdfjs</module>
+		<module>vw/d3</module>
 		<module>vw/tabular</module>
 
 	</modules>
diff --git a/extensions/vw/d3/applib/pom.xml b/extensions/vw/d3/applib/pom.xml
new file mode 100644
index 00000000000..e4e06da5f4f
--- /dev/null
+++ b/extensions/vw/d3/applib/pom.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.causeway.extensions</groupId>
+        <artifactId>causeway-extensions-d3</artifactId>
+        <version>4.0.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>causeway-extensions-d3-applib</artifactId>
+    <name>Apache Causeway Ext - D3 - Applib</name>
+
+    <properties>
+        <jar-plugin.automaticModuleName>org.apache.causeway.extensions.d3.applib</jar-plugin.automaticModuleName>
+        <git-plugin.propertiesDir>org/apache/causeway/extensions/d3/applib</git-plugin.propertiesDir>
+    </properties>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.apache.causeway.commons</groupId>
+            <artifactId>causeway-commons</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.causeway.core</groupId>
+            <artifactId>causeway-applib</artifactId>
+        </dependency>
+
+    </dependencies>
+</project>
diff --git a/extensions/vw/d3/applib/src/main/java/org/apache/causeway/extensions/d3/applib/CausewayModuleExtD3Applib.java b/extensions/vw/d3/applib/src/main/java/org/apache/causeway/extensions/d3/applib/CausewayModuleExtD3Applib.java
new file mode 100644
index 00000000000..207bef99c12
--- /dev/null
+++ b/extensions/vw/d3/applib/src/main/java/org/apache/causeway/extensions/d3/applib/CausewayModuleExtD3Applib.java
@@ -0,0 +1,38 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.applib;
+
+import org.springframework.context.annotation.Configuration;
+
+import lombok.val;
+
+/**
+ * @since 2.0 {@index}
+ */
+@Configuration
+public class CausewayModuleExtD3Applib {
+
+    public CausewayModuleExtD3Applib() {
+        val lombok = lombok.Lombok.class;
+        if(lombok == null) {
+            throw new IllegalStateException("Lombok must be on classpath");
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/extensions/vw/d3/applib/src/main/java/org/apache/causeway/extensions/d3/applib/annotations/D3Graph.java b/extensions/vw/d3/applib/src/main/java/org/apache/causeway/extensions/d3/applib/annotations/D3Graph.java
new file mode 100644
index 00000000000..af0c50f890e
--- /dev/null
+++ b/extensions/vw/d3/applib/src/main/java/org/apache/causeway/extensions/d3/applib/annotations/D3Graph.java
@@ -0,0 +1,74 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.applib.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotates a domain object <b>property</b> (or a domain <b>object</b>) to indicate that its
+ * (or one of its properties) data should be rendered as an interactive D3.js graph in the
+ * Wicket viewer.
+ * <p>
+ * When placed on a property, the property's value (expected to be a String in JSON or
+ * a custom graph-representation type) is passed to the D3 viewer panel.
+ * </p>
+ * <p>
+ * When placed on a domain object, the annotation can be used to indicate that the object's
+ * default representation should use the D3 viewer. An SPI advisor can then resolve the
+ * graph data per object instance.
+ * </p>
+ *
+ * @since 2.0 {@index}
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(value = {ElementType.METHOD, ElementType.FIELD, ElementType.TYPE})
+public @interface D3Graph {
+
+    /**
+     * The initial width of the SVG canvas in pixels.
+     */
+    int initialWidth() default 800;
+
+    /**
+     * The initial height of the SVG canvas in pixels.
+     */
+    int initialHeight() default 600;
+
+    /**
+     * Whether to enable node dragging.
+     */
+    boolean enableDragging() default true;
+
+    /**
+     * Whether to enable zoom and pan.
+     */
+    boolean enableZoom() default true;
+
+    /**
+     * The member id of a domain action to invoke (AJAX) when a node of the graph is clicked.
+     * <p>
+     * If specified, the action must accept a single string parameter
+     * (the clicked node's id). If empty (the default), clicking a node has no effect.
+     * </p>
+     */
+    String clickAction() default "";
+}
\ No newline at end of file
diff --git a/extensions/vw/d3/applib/src/main/java/org/apache/causeway/extensions/d3/applib/config/D3Config.java b/extensions/vw/d3/applib/src/main/java/org/apache/causeway/extensions/d3/applib/config/D3Config.java
new file mode 100644
index 00000000000..bd27bef651c
--- /dev/null
+++ b/extensions/vw/d3/applib/src/main/java/org/apache/causeway/extensions/d3/applib/config/D3Config.java
@@ -0,0 +1,125 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.applib.config;
+
+import java.io.Serializable;
+
+import org.springframework.util.Assert;
+
+import org.apache.causeway.commons.io.JsonUtils;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.With;
+
+/**
+ * Configuration holder for the D3 viewer extension.
+ * <p>
+ * Allows programmatic or configuration-based override of default D3 graph settings.
+ * </p>
+ *
+ * @since 2.0 {@index}
+ */
+@Getter @AllArgsConstructor @NoArgsConstructor @Builder
+public class D3Config implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @Builder.Default
+    private int initialWidth = 800;
+
+    @Builder.Default
+    private int initialHeight = 600;
+
+    @Builder.Default
+    private boolean enableDragging = true;
+
+    @Builder.Default
+    private boolean enableZoom = true;
+
+    /**
+     * The member id of a domain action to invoke (AJAX) when a graph node is clicked;
+     * empty (the default) means no action is bound.
+     */
+    @Builder.Default
+    private String clickAction = "";
+
+    /**
+     * The html element id of the D3 graph canvas (set by the viewer, not by the domain model).
+     */
+    @With
+    private CharSequence canvasId;
+
+    /**
+     * The graph data as a JSON string (the D3 graph model), set by the viewer
+     * from the annotated property's value or from a {@code D3GraphAdvisor}.
+     */
+    @With
+    private CharSequence graphData;
+
+    /**
+     * The AJAX callback URL to invoke when a graph node is clicked; if {@code null},
+     * clicking a node has no effect. Set by the viewer.
+     */
+    @With
+    private CharSequence clickCallbackUrl;
+
+    public D3Config withInitialWidth(final int initialWidth) {
+        Assert.isTrue(initialWidth >= 200 && initialWidth <= 4000,
+                ()->String.format("'initialWidth' must be between 200 and 4000; got %d", initialWidth));
+        return asBuilder().initialWidth(initialWidth).build();
+    }
+
+    public D3Config withInitialHeight(final int initialHeight) {
+        Assert.isTrue(initialHeight >= 200 && initialHeight <= 4000,
+                ()->String.format("'initialHeight' must be between 200 and 4000; got %d", initialHeight));
+        return asBuilder().initialHeight(initialHeight).build();
+    }
+
+    public D3Config withEnableDragging(final boolean enableDragging) {
+        return asBuilder().enableDragging(enableDragging).build();
+    }
+
+    public D3Config withEnableZoom(final boolean enableZoom) {
+        return asBuilder().enableZoom(enableZoom).build();
+    }
+
+    public D3Config withClickAction(final String clickAction) {
+        return asBuilder().clickAction(clickAction).build();
+    }
+
+    public D3Config.D3ConfigBuilder asBuilder() {
+        return D3Config.builder()
+                .initialWidth(getInitialWidth())
+                .initialHeight(getInitialHeight())
+                .enableDragging(isEnableDragging())
+                .enableZoom(isEnableZoom())
+                .clickAction(getClickAction())
+                .canvasId(getCanvasId())
+                .graphData(getGraphData())
+                .clickCallbackUrl(getClickCallbackUrl());
+    }
+
+    public String toJsonString() {
+        return JsonUtils.toStringUtf8(this);
+    }
+
+}
\ No newline at end of file
diff --git a/extensions/vw/d3/applib/src/main/java/org/apache/causeway/extensions/d3/applib/spi/D3GraphAdvisor.java b/extensions/vw/d3/applib/src/main/java/org/apache/causeway/extensions/d3/applib/spi/D3GraphAdvisor.java
new file mode 100644
index 00000000000..1004751cbbc
--- /dev/null
+++ b/extensions/vw/d3/applib/src/main/java/org/apache/causeway/extensions/d3/applib/spi/D3GraphAdvisor.java
@@ -0,0 +1,61 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.applib.spi;
+
+import org.apache.causeway.applib.services.factory.FactoryService;
+import org.apache.causeway.applib.services.repository.RepositoryService;
+
+/**
+ * SPI for the D3 viewer extension, allowing domain applications to provide
+ * graph data and action bindings per-object-instance.
+ * <p>
+ * Implementations are discovered via the {@link java.util.ServiceLoader} SPI mechanism
+ * and can also be injected with {@link FactoryService}, {@link RepositoryService}, etc.
+ * </p>
+ *
+ * @since 2.0 {@index}
+ */
+public interface D3GraphAdvisor {
+
+    /**
+     * Optionally returns advice for a given domain object (represented by its class).
+     *
+     * @param domainClass the domain class being rendered
+     * @return advice, or {@code null} if not applicable
+     */
+    Advice advise(Class<?> domainClass);
+
+    /**
+     * Advice for rendering a D3 graph for a given domain class.
+     */
+    record Advice(
+            /** The JSON graph data as a String (the D3 graph model). */
+            String graphData,
+            /** Optional custom initial width (overrides annotation/default). */
+            Integer initialWidth,
+            /** Optional custom initial height (overrides annotation/default). */
+            Integer initialHeight,
+            /** Whether node dragging is enabled. */
+            boolean enableDragging,
+            /** Whether zoom/pan is enabled. */
+            boolean enableZoom,
+            /** Optional member id of a domain action to invoke when a graph node is clicked;
+             *  {@code null} means keep the annotation/config default, an empty string means none. */
+            String clickAction) {}
+}
\ No newline at end of file
diff --git a/extensions/vw/d3/metamodel/.gitignore b/extensions/vw/d3/metamodel/.gitignore
new file mode 100644
index 00000000000..f8fc22bfb7d
--- /dev/null
+++ b/extensions/vw/d3/metamodel/.gitignore
@@ -0,0 +1,2 @@
+*.approvals.*.bak
+*.received.*
diff --git a/extensions/vw/d3/metamodel/pom.xml b/extensions/vw/d3/metamodel/pom.xml
new file mode 100644
index 00000000000..a671ec62a92
--- /dev/null
+++ b/extensions/vw/d3/metamodel/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.causeway.extensions</groupId>
+        <artifactId>causeway-extensions-d3</artifactId>
+        <version>4.0.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>causeway-extensions-d3-metamodel</artifactId>
+    <name>Apache Causeway Ext - D3 - Metamodel</name>
+
+    <properties>
+        <jar-plugin.automaticModuleName>org.apache.causeway.extensions.d3.metamodel</jar-plugin.automaticModuleName>
+        <git-plugin.propertiesDir>org/apache/causeway/extensions/d3/metamodel</git-plugin.propertiesDir>
+    </properties>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.apache.causeway.extensions</groupId>
+            <artifactId>causeway-extensions-d3-applib</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.causeway.core</groupId>
+            <artifactId>causeway-core-metamodel</artifactId>
+        </dependency>
+
+    </dependencies>
+
+</project>
diff --git a/extensions/vw/d3/metamodel/src/main/java/org/apache/causeway/extensions/d3/metamodel/CausewayModuleExtD3MetaModel.java b/extensions/vw/d3/metamodel/src/main/java/org/apache/causeway/extensions/d3/metamodel/CausewayModuleExtD3MetaModel.java
new file mode 100644
index 00000000000..3232bc3b700
--- /dev/null
+++ b/extensions/vw/d3/metamodel/src/main/java/org/apache/causeway/extensions/d3/metamodel/CausewayModuleExtD3MetaModel.java
@@ -0,0 +1,36 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.metamodel;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+import org.apache.causeway.extensions.d3.applib.CausewayModuleExtD3Applib;
+import org.apache.causeway.extensions.d3.metamodel.facet.D3GraphFacetFromAnnotationFactory;
+
+@Configuration
+@Import({
+        // modules
+        CausewayModuleExtD3Applib.class,
+
+        // @Component's
+        D3GraphFacetFromAnnotationFactory.Register.class
+})
+public class CausewayModuleExtD3MetaModel {
+}
diff --git a/extensions/vw/d3/metamodel/src/main/java/org/apache/causeway/extensions/d3/metamodel/facet/D3GraphFacet.java b/extensions/vw/d3/metamodel/src/main/java/org/apache/causeway/extensions/d3/metamodel/facet/D3GraphFacet.java
new file mode 100644
index 00000000000..fb96de51915
--- /dev/null
+++ b/extensions/vw/d3/metamodel/src/main/java/org/apache/causeway/extensions/d3/metamodel/facet/D3GraphFacet.java
@@ -0,0 +1,37 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.metamodel.facet;
+
+import java.util.Optional;
+
+import org.apache.causeway.core.metamodel.facetapi.Facet;
+import org.apache.causeway.extensions.d3.applib.config.D3Config;
+
+public interface D3GraphFacet extends Facet {
+
+    D3Config configFor(Class<?> domainClass);
+
+    /**
+     * Optionally the member id of a domain action to invoke when a graph node is clicked,
+     * resolved for given {@code domainClass} (annotation or advisor based).
+     */
+    Optional<String> getClickAction(Class<?> domainClass);
+
+}
+
diff --git a/extensions/vw/d3/metamodel/src/main/java/org/apache/causeway/extensions/d3/metamodel/facet/D3GraphFacetAbstract.java b/extensions/vw/d3/metamodel/src/main/java/org/apache/causeway/extensions/d3/metamodel/facet/D3GraphFacetAbstract.java
new file mode 100644
index 00000000000..2514dc87973
--- /dev/null
+++ b/extensions/vw/d3/metamodel/src/main/java/org/apache/causeway/extensions/d3/metamodel/facet/D3GraphFacetAbstract.java
@@ -0,0 +1,57 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.metamodel.facet;
+
+import java.util.Optional;
+
+import org.apache.causeway.core.metamodel.facetapi.Facet;
+import org.apache.causeway.core.metamodel.facetapi.FacetAbstract;
+import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
+import org.apache.causeway.extensions.d3.applib.config.D3Config;
+
+public abstract class D3GraphFacetAbstract
+extends FacetAbstract
+implements D3GraphFacet {
+
+    private static final Class<? extends Facet> type() {
+        return D3GraphFacet.class;
+    }
+
+    private final D3Config config;
+
+    public D3GraphFacetAbstract(
+            final D3Config config,
+            final FacetHolder holder) {
+        super(type(), holder);
+        this.config = config;
+    }
+
+    @Override
+    public D3Config configFor(final Class<?> domainClass) {
+        return config;
+    }
+
+    @Override
+    public Optional<String> getClickAction(final Class<?> domainClass) {
+        return Optional.ofNullable(config.getClickAction())
+                .filter(s -> !s.isBlank());
+    }
+
+}
+
diff --git a/extensions/vw/d3/metamodel/src/main/java/org/apache/causeway/extensions/d3/metamodel/facet/D3GraphFacetFromAnnotation.java b/extensions/vw/d3/metamodel/src/main/java/org/apache/causeway/extensions/d3/metamodel/facet/D3GraphFacetFromAnnotation.java
new file mode 100644
index 00000000000..78a48871b04
--- /dev/null
+++ b/extensions/vw/d3/metamodel/src/main/java/org/apache/causeway/extensions/d3/metamodel/facet/D3GraphFacetFromAnnotation.java
@@ -0,0 +1,120 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.metamodel.facet;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+
+import jakarta.inject.Inject;
+
+import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
+import org.apache.causeway.core.metamodel.facetapi.FacetWithAttributes;
+import org.apache.causeway.extensions.d3.applib.annotations.D3Graph;
+import org.apache.causeway.extensions.d3.applib.config.D3Config;
+import org.apache.causeway.extensions.d3.applib.spi.D3GraphAdvisor;
+
+public class D3GraphFacetFromAnnotation extends D3GraphFacetAbstract implements FacetWithAttributes {
+
+    private final int initialWidth;
+    private final int initialHeight;
+    private final boolean enableDragging;
+    private final boolean enableZoom;
+    private final String clickAction;
+
+    @Inject List<D3GraphAdvisor> advisors;
+
+    public D3GraphFacetFromAnnotation(final D3Config config, final FacetHolder holder) {
+        super(config, holder);
+        initialWidth = config.getInitialWidth();
+        initialHeight = config.getInitialHeight();
+        enableDragging = config.isEnableDragging();
+        enableZoom = config.isEnableZoom();
+        clickAction = config.getClickAction();
+    }
+
+    public static D3GraphFacetFromAnnotation create(
+            final D3Graph annotation,
+            final FacetHolder holder) {
+
+        var config = new D3Config()
+                .withInitialWidth(annotation.initialWidth())
+                .withInitialHeight(annotation.initialHeight())
+                .withEnableDragging(annotation.enableDragging())
+                .withEnableZoom(annotation.enableZoom())
+                .withClickAction(annotation.clickAction());
+
+        return new D3GraphFacetFromAnnotation(config, holder);
+    }
+
+    @Override
+    public D3Config configFor(final Class<?> domainClass) {
+        var config = super.configFor(domainClass);
+
+        return firstMatchingAdvice(domainClass)
+                .map(advice -> {
+                    var configFromAdvisor = config;
+                    final Integer width = advice.initialWidth();
+                    if(width != null) {
+                        configFromAdvisor = configFromAdvisor.withInitialWidth(width);
+                    }
+                    final Integer height = advice.initialHeight();
+                    if(height != null) {
+                        configFromAdvisor = configFromAdvisor.withInitialHeight(height);
+                    }
+                    configFromAdvisor = configFromAdvisor
+                            .withEnableDragging(advice.enableDragging())
+                            .withEnableZoom(advice.enableZoom());
+                    final String clickAction = advice.clickAction();
+                    if(clickAction != null) {
+                        configFromAdvisor = configFromAdvisor.withClickAction(clickAction);
+                    }
+                    return configFromAdvisor;
+                })
+                .orElse(config);
+    }
+
+    @Override
+    public Optional<String> getClickAction(final Class<?> domainClass) {
+        return Optional.ofNullable(configFor(domainClass).getClickAction())
+                .filter(s -> !s.isBlank());
+    }
+
+    @Override
+    public void visitAttributes(final BiConsumer<String, Object> visitor) {
+        super.visitAttributes(visitor);
+        visitor.accept("initialWidth", initialWidth);
+        visitor.accept("initialHeight", initialHeight);
+        visitor.accept("enableDragging", enableDragging);
+        visitor.accept("enableZoom", enableZoom);
+        visitor.accept("clickAction", clickAction);
+    }
+
+    // -- HELPER
+
+    private Optional<D3GraphAdvisor.Advice> firstMatchingAdvice(final Class<?> domainClass) {
+        if(advisors == null) return Optional.empty();
+        return advisors.stream()
+                .map(advisor -> advisor.advise(domainClass))
+                .filter(advice -> advice != null)
+                .findFirst();
+    }
+
+}
+
diff --git a/extensions/vw/d3/metamodel/src/main/java/org/apache/causeway/extensions/d3/metamodel/facet/D3GraphFacetFromAnnotationFactory.java b/extensions/vw/d3/metamodel/src/main/java/org/apache/causeway/extensions/d3/metamodel/facet/D3GraphFacetFromAnnotationFactory.java
new file mode 100644
index 00000000000..87eb19a1409
--- /dev/null
+++ b/extensions/vw/d3/metamodel/src/main/java/org/apache/causeway/extensions/d3/metamodel/facet/D3GraphFacetFromAnnotationFactory.java
@@ -0,0 +1,73 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.metamodel.facet;
+
+import org.apache.causeway.core.metamodel.context.MetaModelContext;
+import org.apache.causeway.core.metamodel.facetapi.FeatureType;
+import org.apache.causeway.core.metamodel.facetapi.MetaModelRefiner;
+import org.apache.causeway.core.metamodel.facets.FacetFactoryAbstract;
+import org.apache.causeway.core.metamodel.progmodel.ProgrammingModel;
+import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailureUtils;
+import org.apache.causeway.extensions.d3.applib.annotations.D3Graph;
+import org.springframework.stereotype.Component;
+
+import jakarta.inject.Inject;
+
+public class D3GraphFacetFromAnnotationFactory
+extends FacetFactoryAbstract {
+
+    @Component
+    public static class Register implements MetaModelRefiner {
+
+        @Override
+        public void refineProgrammingModel(final ProgrammingModel programmingModel) {
+            programmingModel.addFactory(
+                    ProgrammingModel.FacetProcessingOrder.Z2_AFTER_FINALLY,
+                    new D3GraphFacetFromAnnotationFactory(programmingModel.getMetaModelContext()));
+        }
+    }
+
+    @Inject
+    public D3GraphFacetFromAnnotationFactory(final MetaModelContext mmc) {
+        super(mmc, FeatureType.PROPERTIES_AND_ACTIONS);
+    }
+
+    @Override
+    public void process(final ProcessMethodContext processMethodContext) {
+        var d3GraphIfAny = processMethodContext
+                .synthesizeOnMethodOrMixinType(
+                    D3Graph.class,
+                    () -> ValidationFailureUtils
+                            .raiseAmbiguousMixinAnnotations(processMethodContext.facetHolder(), D3Graph.class));
+
+        d3GraphIfAny.ifPresent(
+            d3Graph -> {
+                getServiceInjector().injectServicesInto(
+                    D3GraphFacetFromAnnotation
+                        .create(d3Graph, processMethodContext.facetHolder()));
+            }
+        );
+    }
+
+    @Override
+    public void processParams(final ProcessParameterContext processParameterContext) {
+        super.processParams(processParameterContext);
+    }
+}
+
diff --git a/extensions/vw/d3/pom.xml b/extensions/vw/d3/pom.xml
new file mode 100644
index 00000000000..c7d33d2f16a
--- /dev/null
+++ b/extensions/vw/d3/pom.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.causeway.extensions</groupId>
+        <artifactId>causeway-extensions</artifactId>
+        <version>4.0.0-SNAPSHOT</version>
+        <relativePath>../../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>causeway-extensions-d3</artifactId>
+    <name>Apache Causeway Ext - D3 - Parent</name>
+    <packaging>pom</packaging>
+
+    <description>
+        A component for Apache Causeway' Wicket viewer, allowing domain objects to be rendered as interactive D3.js graphs.
+    </description>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.apache.causeway.extensions</groupId>
+                <artifactId>causeway-extensions-d3-applib</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.causeway.extensions</groupId>
+                <artifactId>causeway-extensions-d3-metamodel</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.causeway.extensions</groupId>
+                <artifactId>causeway-extensions-d3-wicket</artifactId>
+                <version>${project.version}</version>
+                <type>pom</type>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.causeway.extensions</groupId>
+                <artifactId>causeway-extensions-d3-wicket-integration</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.causeway.extensions</groupId>
+                <artifactId>causeway-extensions-d3-wicket-ui</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+    <modules>
+        <module>applib</module>
+        <module>metamodel</module>
+        <module>wicket</module>
+    </modules>
+
+</project>
diff --git a/extensions/vw/d3/wicket/D3_WICKET_UI_OVERVIEW.md b/extensions/vw/d3/wicket/D3_WICKET_UI_OVERVIEW.md
new file mode 100644
index 00000000000..3288ef07b63
--- /dev/null
+++ b/extensions/vw/d3/wicket/D3_WICKET_UI_OVERVIEW.md
@@ -0,0 +1,398 @@
+# D3 Wicket Viewer (UI Module) — Implementation Overview
+
+> **Date:** 14 October 2026  
+> **Module:** `extensions/vw/d3/wicket/ui` — artifact `causeway-extensions-d3-wicket-ui`  
+> **Target package:** `org.apache.causeway.extensions.d3.wkt.ui`
+
+---
+
+## 1. Problem Statement
+
+A domain entity may expose a property annotated with `@D3Graph` whose value is a
+JSON string describing a force-layout graph (`{ nodes: [...], links: [...] }`).
+The Wicket UI module must:
+
+1. **Render** that property as an interactive D3.js force-layout graph on the
+   entity's object page.
+2. **Handle node clicks** by invoking a configurable domain action on the owning
+   entity, passing the clicked node's id as the action's first `String` parameter.
+
+The solution reuses the existing `extensions/vw/d3/wicket/integration` module
+that provides the D3.js canvas, the `d3-integration.mjs` client-side script,
+and the AJAX callback infrastructure.
+
+---
+
+## 2. Module Landscape
+
+```mermaid
+graph TB
+    subgraph "Apache Causeway Extensions — D3"
+        APPLIB["applib<br/>(causeway-extensions-d3-applib)"]
+        METAMODEL["metamodel<br/>(causeway-extensions-d3-metamodel)"]
+        INTEGRATION["wicket/integration<br/>(causeway-extensions-d3-wicket-integration)"]
+        UI["wicket/ui ★ NEW<br/>(causeway-extensions-d3-wicket-ui)"]
+    end
+
+    subgraph "Viewer Framework"
+        WICKET_MODEL["viewer/wicket/model"]
+        WICKET_UI["viewer/wicket/ui"]
+    end
+
+    APPLIB --> METAMODEL
+    METAMODEL --> INTEGRATION
+    METAMODEL --> UI
+    INTEGRATION --> UI
+    WICKET_MODEL --> UI
+    WICKET_UI --> UI
+```
+
+### Module responsibilities
+
+| Module | Key exports | Role |
+|---|---|---|
+| **applib** | `@D3Graph`, `D3Config`, `D3GraphAdvisor` | Domain annotation, config DTO, SPI for dynamic config |
+| **metamodel** | `D3GraphFacet`, `D3GraphFacetFromAnnotationFactory` | Scans `@D3Graph`, attaches facet to properties/actions |
+| **wicket/integration** | `D3Panel`, `NodeClickedCallback`, `D3AjaxCallback`, `d3-integration.mjs` | Generic D3.js canvas, AJAX callback URL wiring, client-side script |
+| **wicket/ui** (this module) | `CausewayModuleExtD3WicketUi`, `D3PanelComponentFactory`, UI `D3Panel` | Glue: picks up `D3GraphFacet`, creates panel from property value, wires action invocation on node click |
+
+---
+
+## 3. New Classes — Architecture
+
+### 3.1 Class diagram
+
+```mermaid
+classDiagram
+    class CausewayModuleExtD3WicketUi {
+        <<...@Configuration>>
+        +CausewayModuleExtD3WicketUi()
+    }
+    note for CausewayModuleExtD3WicketUi "@Import of metamodel,<br>integration, and factory"
+
+    class ComponentFactoryAbstract {
+        <<abstract>>
+        +appliesTo(IModel) ApplicationAdvice
+        +createComponent(String, IModel) Component
+    }
+
+    class D3PanelComponentFactory {
+        <<...@Component>>
+        +appliesTo(IModel) ApplicationAdvice
+        +createComponent(String, IModel) Component
+    }
+
+    class D3Panel_integration {
+        <<wicket/integration>>
+        -config: D3Config
+        -clickCallback: NodeClickedCallback
+        +withClickCallback(NodeClickedCallback) D3Panel
+        +renderHead(IHeaderResponse)
+        +getCanvasId() CharSequence
+    }
+    note for D3Panel_integration "Panel + WebComponent(canvas)<br>+ JS/CSS header contributions"
+
+    class D3Panel_ui {
+        <<wicket/ui>>
+        -attributeModel: UiAttributeWkt
+        +D3Panel(String, UiAttributeWkt)
+        -buildConfig(UiAttributeWkt) D3Config
+        -onNodeClicked(NodeClickedEvent, AjaxRequestTarget, String)
+    }
+    note for D3Panel_ui "Subclasses integration D3Panel.<br>No own .html needed."
+
+    class NodeClickedCallback {
+        <<abstract>>
+        #onNodeClicked(NodeClickedEvent, AjaxRequestTarget)
+    }
+
+    class FormExecutorDefault {
+        +forAction(ActionModel) FormExecutorDefault
+        +executeAndProcessResults(target, form, context)
+    }
+
+    CausewayModuleExtD3WicketUi --> D3PanelComponentFactory : imports
+    D3PanelComponentFactory --|> ComponentFactoryAbstract : extends
+    D3PanelComponentFactory --> D3Panel_ui : creates
+    D3Panel_ui --|> D3Panel_integration : extends
+    D3Panel_ui --> NodeClickedCallback : instantiates
+    D3Panel_ui --> FormExecutorDefault : executes action
+```
+
+### 3.2 File layout
+
+```
+extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/
+    CausewayModuleExtD3WicketUi.java          -- Spring @Configuration entry point
+    components/
+        D3Panel.java                          -- UI panel (extends integration D3Panel)
+        D3PanelComponentFactory.java           -- Wicket ComponentFactory
+```
+
+No additional `.html` markup is needed — the UI `D3Panel` inherits the integration
+module's `D3Panel.html`.
+
+---
+
+## 4. End-to-End Scenarios
+
+### 4.1 Page Load — Render the graph
+
+```mermaid
+sequenceDiagram
+    actor Browser
+    participant WicketPage as Wicket Object Page
+    participant Factory as D3PanelComponentFactory
+    participant Facet as D3GraphFacet
+    participant Panel_ui as D3Panel (ui)
+    participant Panel_int as D3Panel (integration)
+    participant JS as d3-integration.mjs
+
+    Browser->>WicketPage: GET /entity/123
+
+    WicketPage->>WicketPage: iterate attributes
+    WicketPage->>Factory: appliesTo(attributeModel)
+    Factory->>Factory: instanceof UiAttributeWkt?
+    Factory->>Factory: containsFacet(D3GraphFacet.class)?
+    Factory->>Factory: value instanceof CharSequence && non-blank?
+    Factory-->>WicketPage: ApplicationAdvice.APPLIES
+
+    WicketPage->>Factory: createComponent("d3", attributeModel)
+    Factory->>Panel_ui: new D3Panel("d3", attributeModel)
+
+    Panel_ui->>Panel_ui: buildConfig(attributeModel)
+    Panel_ui->>Facet: lookupFacet(D3GraphFacet.class)
+    Panel_ui->>Facet: configFor(domainClass)
+    Panel_ui->>Panel_ui: config.withGraphData(propertyValue)
+
+    Panel_ui->>Facet: getClickAction(domainClass)
+    alt clickAction is present
+        Panel_ui->>Panel_ui: withClickCallback(callback)
+        Panel_ui->>Panel_int: withClickCallback(callback)
+    else no clickAction
+        Panel_ui->>Panel_int: (no callback attached)
+    end
+
+    Note over Panel_int: renderHead() is called
+    Panel_int->>Panel_int: config.withClickCallbackUrl(callbackUrl)
+    Panel_int->>JS: D3Reference + D3IntegrationReference + domReadyScript(config)
+
+    WicketPage-->>Browser: HTML page
+
+    Browser->>Browser: execute domReadyScript
+    Browser->>JS: WicketStuff.D3.init(config)
+    JS->>JS: parse config.graphData
+    JS->>JS: build D3 force layout
+    JS->>Browser: render SVG canvas
+```
+
+### 4.2 Node Click — Invoke domain action
+
+```mermaid
+sequenceDiagram
+    actor User
+    participant Browser
+    participant D3Ajax as D3AjaxCallback (integration)
+    participant Callback as NodeClickedCallback (ui)
+    participant Panel_ui as D3Panel (ui)
+    participant ActionModel as ActionModel
+    participant FormExec as FormExecutorDefault
+    participant DomainObj as Domain Entity
+
+    User->>Browser: clicks graph node "A"
+    Browser->>Browser: d3 event handler fires
+    Browser->>D3Ajax: Wicket.Ajax.get(u + "&nodeId=A")
+
+    D3Ajax->>D3Ajax: read "nodeId" param -> "A"
+    D3Ajax->>Callback: onNodeClicked(event("A"), target)
+    Callback->>Panel_ui: onNodeClicked(event, target, "selectNode")
+
+    Panel_ui->>Panel_ui: objectSpec.getAction("selectNode")
+    Panel_ui-->>Panel_ui: ObjectAction
+
+    Panel_ui->>ActionModel: ActionModel.forEntity(action, parentUiModel)
+    Panel_ui->>ActionModel: startParameterNegotiation()
+    Panel_ui->>ActionModel: updateParamValuePojo(0, __ -> "A")
+
+    Panel_ui->>FormExec: FormExecutorDefault.forAction(actionModel)
+    Panel_ui->>FormExec: executeAndProcessResults(target, null, actionModel)
+
+    FormExec->>DomainObj: selectNode("A")
+    DomainObj-->>FormExec: return value (e.g. this)
+
+    FormExec->>Browser: Ajax response (partial page update)
+    Browser->>Browser: update UI
+```
+
+### 4.3 Decision tree — `appliesTo`
+
+```mermaid
+flowchart TD
+    Start["factory.appliesTo(model)"] --> CheckType{"model instanceof<br/>UiAttributeWkt?"}
+    CheckType -- No --> NoApply["ApplicationAdvice.DOES_NOT_APPLY"]
+    CheckType -- Yes --> CheckFacet{"containsFacet<br/>(D3GraphFacet.class)?"}
+    CheckFacet -- No --> NoApply
+    CheckFacet -- Yes --> CheckNull{"value != null?"}
+    CheckNull -- No --> NoApply
+    CheckNull -- Yes --> CheckType2{"value.getPojo() instanceof<br/>CharSequence?"}
+    CheckType2 -- No --> NoApply
+    CheckType2 -- Yes --> CheckBlank{"!cs.toString().isBlank()?"}
+    CheckBlank -- No --> NoApply
+    CheckBlank -- Yes --> Apply["ApplicationAdvice.APPLIES"]
+```
+
+### 4.4 Callback wiring detail
+
+```mermaid
+flowchart LR
+    subgraph "Construction (D3Panel ctor)"
+        A["facet.getClickAction(domainClass)"] --> HasAction{"Optional present?"}
+        HasAction -- Yes --> B["withClickCallback(new NodeClickedCallback() {<br/>  onNodeClicked(event, target) {<br/>    D3Panel.this.onNodeClicked(event, target, actionId)<br/>  }<br/>})"]
+        HasAction -- No --> C["(no callback attached)"]
+    end
+
+    subgraph "Rendering (integration D3Panel.renderHead)"
+        B --> D["config.withClickCallbackUrl(callback.getCallbackUrl())"]
+        D --> E["domReadyScript(configWithCallback)"]
+        E --> F["Browser receives config<br/>with clickCallbackUrl field"]
+    end
+
+    subgraph "Node click (browser)"
+        F --> G["Wicket.Ajax.get(callbackUrl + '&nodeId=' + id)"]
+        G --> H["D3AjaxCallback.respond(target)"]
+        H --> I["NodeClickedCallback.onNodeClicked(...)"]
+    end
+```
+
+---
+
+## 5. Test Strategy
+
+```mermaid
+flowchart TD
+    subgraph "Unit Tests (JUnit + Mockito)"
+        U1["D3PanelComponentFactoryTest<br/>6 tests ✓ ALL PASS"]:::pass
+        U2["appliesTo: correct type + facet + non-blank → APPLIES"]:::pass
+        U3["appliesTo: wrong type → DOES_NOT_APPLY"]:::pass
+        U4["appliesTo: missing facet → DOES_NOT_APPLY"]:::pass
+        U5["appliesTo: blank value → DOES_NOT_APPLY"]:::pass
+        U6["appliesTo: null value → DOES_NOT_APPLY"]:::pass
+        U7["appliesTo: non-CharSequence value → DOES_NOT_APPLY"]:::pass
+    end
+
+    subgraph "Component Tests (WicketTester)"
+        C1["D3PanelTest (planned)"]:::blocked
+        C2["startPage(D3Panel)"]:::blocked
+        C3["assertContains canvas id"]:::blocked
+        C4["assertContains domReadyScript"]:::blocked
+        C5["assertContains graphData JSON"]:::blocked
+    end
+
+    subgraph "E2E Smoke Test (manual / example webapp)"
+        E1["GraphOwner entity with<br/>@D3Graph + @Action"]:::todo
+        E2["Import CausewayModuleExtD3WicketUi"]:::todo
+        E3["Open object page → see graph"]:::todo
+        E4["Click node → action invoked"]:::todo
+    end
+
+    U1 --> U2 & U3 & U4 & U5 & U6 & U7
+    C1 --> C2 & C3 & C4 & C5
+    E1 --> E2 --> E3 --> E4
+
+    classDef pass fill:#9f9,stroke:#090
+    classDef blocked fill:#fcc,stroke:#c00
+    classDef todo fill:#ffc,stroke:#cc0
+```
+
+### Test dependency
+
+| Dependency | Version | Used for |
+|---|---|---|
+| `junit-jupiter` | (BOM-managed) | Test runner |
+| `mockito-core` / `mockito-junit-jupiter` | (BOM-managed) | Mocking `UiAttributeWkt`, `ObjectFeature`, `ManagedObject` |
+| `MetaModelContext_forTesting` | (project test-jar from `viewers/commons/testsupport`) | Lightweight `MetaModelContext` without full container |
+| `wicket-tester` | `${wicket.version}` (10.10.0) | `WicketTester` for component rendering tests |
+
+> **Note:** `wicket-tester` is declared in `pom.xml` but panel-level tests (`D3PanelTest`)
+> were removed pending a rewrite. The `createComponent` test in `D3PanelComponentFactoryTest`
+> is disabled with `//TODO` because it requires a full Wicket request cycle.
+
+---
+
+## 6. Key Design Decisions
+
+1. **Attribute-level factory, not object-level.**  
+   The `D3GraphFacet` is attached to *properties/actions only* (the metamodel factory
+   processes `PROPERTIES_AND_ACTIONS`). The UI factory registers for
+   `UiComponentType.ATTRIBUTE_NAME_AND_VALUE`, matching the PDF.js extension pattern.
+
+2. **Extends integration `D3Panel`, no new `.html`.**  
+   The UI `D3Panel` subclasses the integration module's panel, inheriting its
+   `D3Panel.html` markup, D3.js library references, and callback-URL wiring.
+   Only the *data* (graph JSON) and *behavior* (action invocation) are added.
+
+3. **Graph data from the property value.**  
+   The annotated property's value (a JSON `String`) is pushed into `D3Config`
+   via `withGraphData(...)`. This is the primary source; advisor-provided
+   `graphData` is not merged by the facet today (documented gap).
+
+4. **Action invocation via `FormExecutorDefault`.**  
+   On node click, the panel resolves the domain action by member id, builds an
+   `ActionModel`, sets the first parameter to the clicked node id via
+   `updateParamValuePojo(0, __ -> event.nodeId())`, and delegates to
+   `FormExecutorDefault.forAction(actionModel).executeAndProcessResults(...)`.
+   This reuses the standard Wicket viewer's action-execution pipeline (parameter
+   validation, result handling, page update).
+
+5. **No separate CSS file (optional).**  
+   A small `D3Panel.css` was considered but not implemented. The integration
+   panel already contributes D3.js CSS via `D3Reference`.
+
+---
+
+## 7. Gaps & Future Work
+
+| Area | Status | Notes |
+|---|---|---|
+| **TYPE-level `@D3Graph`** | Unsupported | `@Target` includes `TYPE`, but metamodel factory only processes `PROPERTIES_AND_ACTIONS` |
+| **Advisor `graphData` merge** | Not merged | `configFor(...)` merges advisor width/height/drag/zoom/clickAction but not graph data |
+| **`D3Panel.css`** | Optional | Not yet created; would require a `CssReference` in `renderHead` |
+| **Canvas cleanup** | Optional | Override `onRemove`/`onDetach` to call `WicketStuff.D3.destroy(canvasId)` |
+| **Antora docs** | Not started | User-facing page for the D3 viewer extension |
+| **`ui-test` module** | Not started | Long-term Wicket-level regression tests (like `viewers/wicket/ui-test`) |
+
+---
+
+## 8. Quick Reference — Key API Signatures
+
+### Source of truth (verified on disk)
+
+| API | File |
+|---|---|
+| `UiAttributeWkt.getMetaModel()` → `ObjectFeature` (FacetHolder) | `viewer/wicket/model/.../UiAttributeWkt.java` |
+| `UiAttributeWkt.getObject()` → `ManagedObject` | same |
+| `UiAttributeWkt.getParentUiModel()` → `UiObjectWkt` | same |
+| `ManagedObject.getPojo()` → `Object` | `core/metamodel/.../ManagedObject.java` |
+| `ManagedObject.adaptSingular(ObjectSpec, Object)` | same |
+| `ObjectFeature.containsFacet(Class)` → `boolean` | `core/metamodel/.../ObjectFeature.java` |
+| `ObjectFeature.lookupFacet(Class)` → `Optional<? extends Facet>` | same |
+| `ObjectSpecification.getAction(String)` → `Optional<ObjectAction>` | `core/metamodel/.../ObjectSpecification.java` |
+| `ObjectSpecification.getCorrespondingClass()` → `Class<?>` | same |
+| `D3GraphFacet.configFor(Class)` → `D3Config` | `extensions/vw/d3/metamodel/.../D3GraphFacet.java` |
+| `D3GraphFacet.getClickAction(Class)` → `Optional<String>` | same |
+| `ActionModel.forEntity(ObjectAction, UiObjectWkt)` | `viewer/wicket/model/.../ActionModel.java` |
+| `ActionModel.getActionInteraction()` → `ActionInteractionWkt` | same |
+| `ActionInteractionWkt.startParameterNegotiation()` → `Optional<ParameterNegotiationModel>` | same |
+| `ParameterNegotiationModel.updateParamValuePojo(int, Function)` | `viewer/commons/model/.../ParameterNegotiationModel.java` |
+| `FormExecutorDefault.forAction(ActionModel)` | `viewer/wicket/ui/.../FormExecutorDefault.java` |
+| `FormExecutorDefault.executeAndProcessResults(AjaxRequestTarget, Form, FormExecutorContext)` | same |
+
+### Build / run commands
+
+```bash
+# unit tests only
+mvn -pl extensions/vw/d3/wicket/ui -am -Dtest=D3PanelComponentFactoryTest test
+
+# full module build
+mvn -pl extensions/vw/d3/wicket/ui -am install
+```
diff --git a/extensions/vw/d3/wicket/D3_WICKET_UI_STATUS.md b/extensions/vw/d3/wicket/D3_WICKET_UI_STATUS.md
new file mode 100644
index 00000000000..f5859e034b5
--- /dev/null
+++ b/extensions/vw/d3/wicket/D3_WICKET_UI_STATUS.md
@@ -0,0 +1,389 @@
+# D3 Wicket Viewer (UI Module) — Session Status & Implementation Notes
+
+**Date:** 14 October 2026
+**See also:** [`D3_WICKET_UI_OVERVIEW.md`](./D3_WICKET_UI_OVERVIEW.md) — architectural diagrams, end-to-end flow charts, class relationships, and test strategy visualisation
+**Module:** `extensions/vw/d3/wicket/ui` — artifact `causeway-extensions-d3-wicket-ui`
+**Target package:** `org.apache.causeway.extensions.d3.wkt.ui`
+
+---
+
+## 1. Goal Recap
+
+Implement the Wicket UI module so that a domain **property** annotated `@D3Graph` renders as an interactive D3.js force-layout graph (reusing the `wicket/integration` module's `D3Panel` + `d3-integration.mjs`), and **clicking a graph node invokes (AJAX) a configurable domain action** whose single `String` parameter receives the clicked node's id — mirroring the PDF.js extension's `PdfJsViewerPanelComponentFactory` / `PdfJsViewerPanel` pattern.
+
+---
+
+## 2. Overall Status
+
+| Area | Status |
+|---|---|
+| Research & API verification | **DONE** |
+| Code implementation | **DONE** — three main classes (`CausewayModuleExtD3WicketUi`, `D3PanelComponentFactory`, `D3Panel`) written, compiled, and API-verified |
+| Unit tests (`appliesTo` logic) | **PASS** — 6/6 tests pass (all `appliesTo` scenarios) |
+| Unit tests (panel construction) | **BLOCKED** — requires `wicket-tester` for full Wicket container simulation; `wicket-tester` dep added to `pom.xml` but panel-level tests (`D3PanelTest`) were removed pending rewrite using `WicketTester` |
+| Build / test validation | `mvn -pl extensions/vw/d3/wicket/ui -am test` → **BUILD SUCCESS** (6 tests, 0 failures, 0 errors) |
+| Blockers | **None** |
+
+---
+
+## 3. Analysis Summary (Verified)
+
+### 3.1 Module landscape
+
+| Module | Artifact | Key classes |
+|---|---|---|
+| `extensions/vw/d3/applib` | `causeway-extensions-d3-applib` | `D3Config`, `@D3Graph`, `D3GraphAdvisor` |
+| `extensions/vw/d3/metamodel` | `causeway-extensions-d3-metamodel` | `D3GraphFacet`, `D3GraphFacetFromAnnotation`, `D3GraphFacetFromAnnotationFactory` |
+| `extensions/vw/d3/wicket/integration` | `causeway-extensions-d3-wicket-integration` | `D3Panel`, `NodeClickedCallback`, `NodeClickedEvent`, `D3AjaxCallback`, `D3Reference`, `D3IntegrationReference`, `d3-integration.mjs` |
+| `extensions/vw/d3/wicket/ui` | `causeway-extensions-d3-wicket-ui` | **(to be written)** `CausewayModuleExtD3WicketUi`, `D3PanelComponentFactory`, `D3Panel`, `D3Panel.css` |
+| *(pattern)* `extensions/vw/pdfjs/wicket/ui` | `causeway-extensions-pdfjs-wicket-ui` | `PdfJsViewerPanelComponentFactory`, `PdfJsViewerPanel` |
+
+### 3.2 `@D3Graph` annotation and facet attachment
+
+- `@D3Graph` has `@Target({METHOD, FIELD, TYPE})`, but the current metamodel factory `D3GraphFacetFromAnnotationFactory` processes **`FeatureType.PROPERTIES_AND_ACTIONS` only**.
+- **Consequence:** the `D3GraphFacet` lives on **member methods** (property getters / action methods), **not on types**.
+- The UI module must therefore register its component factory for `UiComponentType.ATTRIBUTE_NAME_AND_VALUE` — **not** `DOMAIN_OBJECT` (a DOMAIN_OBJECT factory would hijack the whole object page, which is neither what the facet supports nor what the PDF.js module does).
+- **Gap:** a `@D3Graph` on a **type/class** currently has no factory support in the metamodel (tracked in the §5 gaps list); optionally extend the metamodel later.
+- `D3GraphFacet` API:
+  - `configFor(Class<?> domainClass)` → `D3Config` — already merges advisor advice for width/height/drag/zoom/`clickAction`; **it does NOT merge advisor `graphData`**.
+  - `getClickAction(Class<?> domainClass)` → `Optional<String>` (member id of the action to invoke on node click; empty → no action bound).
+
+### 3.3 `D3Config` (applib)
+
+| Field | Type | Notes |
+|---|---|---|
+| `initialWidth` / `initialHeight` | int | defaults 800 / 600; validated 200..4000 |
+| `enableDragging` / `enableZoom` | boolean | defaults true |
+| `clickAction` | String | member id of the domain action (domain-level setting) |
+| `canvasId` | `CharSequence` (`@With`) | html element id of the canvas — **set by the viewer** |
+| `graphData` | `CharSequence` (`@With`) | JSON string `{"nodes":[...],"links":[...]}` — **set by the viewer** |
+| `clickCallbackUrl` | `CharSequence` (`@With`) | AJAX callback URL — **set by the viewer** |
+
+Lombok builder + `toJsonString()`. `withGraphData(...)` / `withClickCallbackUrl(...)` are generated from `@With`.
+
+### 3.4 Integration `D3Panel` (`org.apache.causeway.extensions.d3.wkt.integration.components.D3Panel`)
+
+- `D3Panel(String id, @NonNull D3Config config)` — adds `new WebComponent(ID_D3_CANVAS)` and stores the config; `getCanvasId()` returns `config.getCanvasId()`.
+- **`withClickCallback(NodeClickedCallback)`** — attaches the callback as a child component; on render, `renderHead(ComponentTag)` obtains `clickCallback.getCallbackUrl()`, merges it via `config.withClickCallbackUrl(...)`, and contributes:
+  - `D3Reference` (d3.js library),
+  - `D3IntegrationReference` (the `d3-integration.mjs` weaver),
+  - `D3IntegrationReference.domReadyScript(config)` (invokes `WicketStuff.D3.init(config)`).
+- Markup (`D3Panel.html` in integration): `<wicket:panel><span wicket:id="d3Canvas"></span></wicket:panel>`.
+  A sub-class inherits this markup — **no new `.html` is required for the UI module** unless extra chrome (caption, toolbar) is wanted around the canvas.
+- `NodeClickedCallback` is `abstract`; subclass implements `onNodeClicked(NodeClickedEvent event, AjaxRequestTarget target)`. `NodeClickedEvent.nodeId()` exposes the clicked node's id.
+- Client side (`d3-integration.mjs`): parses `config.graphData`, builds the force layout, and on node click performs `Wicket.Ajax.get({u: config.clickCallbackUrl.replace('<PLACEHOLDER>', '') + '&nodeId=' + encodeURIComponent(d.id)})`.
+
+### 3.5 Callback flow (implemented by integration)
+
+```
+node click (browser)
+  -> Wicket.Ajax.get(callbackUrl + "&nodeId=" + id)
+  -> D3AjaxCallback.respond(target)
+     -> NodeClickedCallback (AJAX behaviour): read "nodeId" request param
+        -> abstract onNodeClicked(NodeClickedEvent, AjaxRequestTarget)
+```
+
+The UI module plugs the action invocation into the last step.
+
+### 3.6 Viewer APIs verified (live source, not docs)
+
+| API | Verified in | Notes |
+|---|---|---|
+| `UiComponentType.ATTRIBUTE_NAME_AND_VALUE` | `UiComponentType.java` | correct registration point for this module |
+| `ComponentFactoryAbstract(UiComponentType, Class)` + `@Inject` ctor | `PdfJsViewerPanelComponentFactory.java` | pattern to mirror |
+| `ComponentFactoryAbstract.appliesTo(IModel)` → `ApplicationAdvice` | same | `ApplicationAdvice.applies()` gates applicability |
+| `UiAttributeWkt.getMetaModel()` → `ObjectFeature` (FacetHolder) | `UiAttributeWkt` | holds facets; `containsFacet(Class)` |
+| `UiAttributeWkt.getObject()` → `ManagedObject` | `UiAttributeWkt` | **attribute value** (the graph JSON string) |
+| `UiAttributeWkt.getParentUiModel()` → `UiObjectWkt` | `HasUiParentObject` | owning object panel model |
+| `UiObjectWkt.getManagedObject()` → `ManagedObject` | verified | owning domain object |
+| `ManagedObject.objSpec()` → `ObjectSpecification`; `.getCorrespondingClass()` | verified | domain class for `configFor(...)`/`getClickAction(...)` |
+| `ObjectSpecification.getAction(String)` → `Optional<ObjectAction>` | `ObjectActionContainer.java:86-96` | action lookup by member id |
+| `ActionModel.forEntity(ObjectAction, UiObjectWkt)` | `ActionModel.java:118-128` | factory; internally uses `ActionInteractionWkt.forEntity(...)` |
+| `ActionModel.getActionInteraction().parameterNegotiationModel()` → `ParameterNegotiationModel` | `ActionModel.java:202-204` | set parameter values programmatically |
+| `ParameterNegotiationModel.updateParamValuePojo(0, fn)` | `ParameterNegotiationModel.java` | `fn: ManagedObject -> Object`; param 0 = node id |
+| `ActionModel implements FormExecutorContext` | `ActionModel.java:59` | can be passed as the context arg when executing |
+| `FormExecutorDefault.forAction(ActionModel)` | `FormExecutorDefault.java` (current) | see correction below |
+| `.executeAndProcessResults(AjaxRequestTarget, Form<?>, FormExecutorContext)` → `FormExecutionOutcome` | `FormExecutorDefault.java:64` | full result routing (redirect-to-object, repaint, growl) |
+| invocation pattern | `ActionLink.java:194-200` | `FormExecutorDefault.forAction(actionModel).executeAndProcessResults(ajaxTarget, feedbackForm, actionModel)` |
+
+### 3.7 Corrections to earlier research notes
+
+Two assumptions recorded earlier are **stale** and must not be carried into implementation:
+
+1. **`FormExecutorDefault` is NOT `(ServiceInjector, ActionInteractionWkt)`.** In current code it is a **record** `FormExecutorDefault(Either<ActionModel, PropertyModel> actionOrPropertyModel)` with static factories `forAction(ActionModel)`, `forProperty(PropertyModel)`, `forMember(Either)`. **No `ServiceInjector` injection is needed.** Parameterized actions are handled by setting the pending argument on the `ActionModel`'s `ParameterNegotiationModel`, then executing via `FormExecutorDef [...]
+2. **The component factory is NOT `DOMAIN_OBJECT`-level.** The `D3GraphFacet` is only attached to properties/actions (metamodel factory processes `PROPERTIES_AND_ACTIONS`). The UI must be an **attribute-level** factory (`ATTRIBUTE_NAME_AND_VALUE`), exactly like PDF.js. A `DOMAIN_OBJECT` factory is not triggered by anything and is the wrong architecture.
+
+### 3.8 Design decisions (agreed)
+
+1. UI `D3Panel extends` integration `D3Panel` → reuses markup, resource refs and callback-URL wiring; **no `D3Panel.html` needed**.
+2. Graph data source = the annotated property's value (`attributeModel.getObject().getPojo()` as `String`), pushed into config via `config.withGraphData(...)`. (Optional future: fall back to `D3GraphAdvisor` advice `graphData` when the property is empty — note the facet's `configFor` does **not** merge advisor `graphData` today.)
+3. Click action resolution = `facet.getClickAction(domainClass)` (primary). Because `configFor(domainClass)` already merges advisor advice for `clickAction`, the panel may rely on `facet.configFor(...)` consistently; but the explicit `getClickAction(...)` is the authoritative action id to bind the callback to.
+4. Node-click handling:
+   - resolve `ObjectAction` on the owning object,
+   - build `ActionModel.forEntity(action, parentUiModel)`,
+   - if the action has ≥1 param: `parameterNegotiationModel().updateParamValuePojo(0, __ -> event.nodeId())`,
+   - execute `FormExecutorDefault.forAction(actionModel).executeAndProcessResults(target, null, actionModel)`.
+5. A small `D3Panel.css` (scoped to the canvas) plus optional `D3PanelCssReference` keep styling decoupled; no extra JS required (integration `domReadyScript` renders; the integration panel can handle cleanup on `onRemove`).
+
+## 4. Implementation Plan (Design Spec — Ready to Code)
+
+### 4.1 `CausewayModuleExtD3WicketUi.java`
+
+```java
+package org.apache.causeway.extensions.d3.wkt.ui;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.apache.causeway.extensions.d3.metamodel.CausewayModuleExtD3MetaModel;
+import org.apache.causeway.extensions.d3.wkt.integration.CausewayModuleExtD3WicketIntegration;
+import org.apache.causeway.extensions.d3.wkt.ui.components.D3PanelComponentFactory;
+
+@Configuration
+@Import({
+    CausewayModuleExtD3MetaModel.class,
+    CausewayModuleExtD3WicketIntegration.class,
+    D3PanelComponentFactory.class,
+})
+public class CausewayModuleExtD3WicketUi {}
+```
+
+### 4.2 `components/D3PanelComponentFactory.java`
+
+```java
+@Component
+public class D3PanelComponentFactory extends ComponentFactoryAbstract {
+
+    @Inject
+    public D3PanelComponentFactory() {
+        super(UiComponentType.ATTRIBUTE_NAME_AND_VALUE, D3Panel.class);
+    }
+
+    @Override
+    protected ApplicationAdvice appliesTo(final IModel<?> model) {
+        if (!(model instanceof UiAttributeWkt attributeModel)) { return ApplicationAdvice.DOES_NOT_APPLY; }
+        if (!attributeModel.getMetaModel().containsFacet(D3GraphFacet.class)) {
+            return ApplicationAdvice.DOES_NOT_APPLY;
+        }
+        // graph data must be a CharSequence (JSON string)
+        var value = attributeModel.getObject();
+        if (value.getPojo() instanceof CharSequence && !value.getPojo().toString().isBlank()) {
+            return ApplicationAdvice.APPLIES;
+        }
+        return ApplicationAdvice.DOES_NOT_APPLY;
+    }
+
+    @Override
+    public Component createComponent(final String id, final IModel<?> model) {
+        return new D3Panel(id, (UiAttributeWkt) model);
+    }
+}
+```
+
+### 4.3 `components/D3Panel.java`
+
+```java
+public class D3Panel extends org.apache.causeway.extensions.d3.wkt.integration.components.D3Panel {
+
+    private static final long serialVersionUID = 1L;
+    private static final Logger LOG = LoggerFactory.getLogger(D3Panel.class);
+
+    private final UiAttributeWkt attributeModel;
+
+    public D3Panel(final String id, final UiAttributeWkt attributeModel) {
+        super(id, buildConfig(attributeModel));
+        this.attributeModel = attributeModel;
+        var domainClass = attributeModel
+            .getParentUiModel().getManagedObject()
+            .objSpec().getCorrespondingClass();
+        var facet = attributeModel.getMetaModel().getFacet(D3GraphFacet.class);
+        var clickAction = facet.getClickAction(domainClass).orElse(null);
+        if (clickAction != null) {
+            withClickCallback(new NodeClickedCallback() {
+                private static final long serialVersionUID = 1L;
+                @Override
+                protected void onNodeClicked(final NodeClickedEvent event, final AjaxRequestTarget target) {
+                    D3Panel.this.onNodeClicked(event, target, clickAction);
+                }
+            });
+        }
+    }
+
+    private static D3Config buildConfig(final UiAttributeWkt attributeModel) {
+        var domainClass = attributeModel
+            .getParentUiModel().getManagedObject()
+            .objSpec().getCorrespondingClass();
+        var facet = attributeModel.getMetaModel().getFacet(D3GraphFacet.class);
+        var config = facet.configFor(domainClass);
+        // set graph data from the annotated property value
+        var pojo = attributeModel.getObject().getPojo();
+        if (pojo instanceof CharSequence cs && !cs.toString().isBlank()) {
+            config = config.withGraphData(cs);
+        }
+        return config;
+    }
+
+    private void onNodeClicked(final NodeClickedEvent event, final AjaxRequestTarget target,
+            final String clickActionId) {
+        var parentObjectModel = attributeModel.getParentUiModel();
+        var action = parentObjectModel.getManagedObject().objSpec()
+            .getAction(clickActionId).orElseThrow(() -> new RuntimeException(
+                "D3 action '" + clickActionId + "' not found on " + parentObjectModel.getManagedObject().objSpec()));
+
+        var actionModel = ActionModel.forEntity(action, parentObjectModel);
+        var pendingArgs = actionModel.getActionInteraction().parameterNegotiationModel();
+        if (pendingArgs.getParamCount() >= 1) {
+            pendingArgs.updateParamValuePojo(0, __ -> event.nodeId());
+        }
+        FormExecutorDefault.forAction(actionModel)
+            .executeAndProcessResults(target, null, actionModel);
+    }
+}
+```
+
+### 4.4 Resources: `D3Panel.css` (optional)
+
+```css
+/* D3 graph canvas */
+.d3-canvas {
+    border: 1px solid #ddd;
+    border-radius: 4px;
+    overflow: hidden;
+    margin: 0.5em 0;
+}
+```
+
+Referenced via a `CssReference` contributed from an overridden `renderHead` (the integration panel already exports `getCanvasId()` for scoping).
+
+### 4.5 Module structure (files to create)
+
+```
+extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/
+    CausewayModuleExtD3WicketUi.java
+    components/
+        D3Panel.java
+        D3PanelComponentFactory.java
+extensions/vw/d3/wicket/ui/src/main/resources/org/apache/causeway/extensions/d3/wkt/ui/components/
+    D3Panel.css          (optional)
+    D3PanelCssReference.java  (optional Java sibling if contributed via header item)
+```
+
+### 4.6 pom.xml notes
+
+The existing `extensions/vw/d3/wicket/ui/pom.xml` already depends on:
+- `causeway-extensions-d3-metamodel`
+- `causeway-extensions-d3-wicket-integration`
+- plus the standard viewer/wicket-commons UI compile deps
+
+No new production dependencies are required. Test dependencies are listed in §6.
+
+## 5. Outstanding Actions (Checklist)
+
+**Implementation tasks**
+
+- [x] Create `CausewayModuleExtD3WicketUi.java` (§4.1) — **FQCNs verified on disk**: `org.apache.causeway.extensions.d3.metamodel.CausewayModuleExtD3MetaModel` and `org.apache.causeway.extensions.d3.wkt.integration.CausewayModuleExtD3WicketIntegration` (note the `wkt` package shorthand).
+- [x] Create `components/D3PanelComponentFactory.java` (§4.2) — `ATTRIBUTE_NAME_AND_VALUE`, applies-to: `UiAttributeWkt` + `containsFacet(D3GraphFacet.class)` + non-blank CharSequence value; `createComponent` → `D3Panel`.
+- [x] Create `components/D3Panel.java` (§4.3) — extends integration `D3Panel`; `buildConfig` from `facet.configFor(domainClass).withGraphData(propertyValue)`; bind `NodeClickedCallback` when `facet.getClickAction(domainClass)` is present.
+- [x] Verify signature details when writing code (all resolved — `lookupFacet`, `startParameterNegotiation`, `ManagedObject.adaptSingular`, `ApplicationAdvice.APPLIES`, `ObjectSpecification.correspondingClass()`).
+- [ ] Optional: add `D3Panel.css` + `D3PanelCssReference`. If CSS is contributed, override `renderHead` in the UI `D3Panel` (integration panel already contributes header items — order will be maintained).
+- [ ] Optional: canvas cleanup on remove — override `onRemove`/`onDetach` to call `WicketStuff.D3.destroy(canvasId)`.
+- [x] Compile: `mvn -pl extensions/vw/d3/wicket/ui -am -DskipTests install` (from repo root; add `-o` offline). Fix any signature drift.
+- [x] Write unit tests from §6 and run them — 6 `appliesTo` tests pass; `D3PanelTest` removed (requires `wicket-tester` for panel construction; dep added).
+- [ ] Smoke-test in a real webapp (§6.3) — decide which example app to use (no `examples/` at repo root; candidates: `incubator/examples`, or a fresh app with `CausewayModuleExtD3WicketUi` imported).
+- [ ] Add license headers to all new files and reformat via IDE.
+
+**Gaps/improvement opportunities discovered during research**
+
+- [ ] **TYPE-level `@D3Graph` unsupported**: annotation `@Target` includes `TYPE`, but the metamodel factory processes only `PROPERTIES_AND_ACTIONS`. Decide whether to add a TYPE-processing factory (enables whole-object-page rendering) or document the limitation. If TYPE support is added, the UI would additionally need a `DOMAIN_OBJECT`-level factory.
+- [ ] **Advisor `graphData` not merged**: `D3GraphFacet.configFor(...)` merges advice for width/height/drag/zoom/clickAction but **not** `graphData`. Decide whether the advisor should be able to supply graph data, and plumb it through.
+- [ ] **Antora/asciidoc docs**: add user-facing page for the D3 viewer extension (check `extensions/vw/d3/adoc` or `antora` structure).
+- [ ] **Regression/extensions tests**: consider a `-wicket-ui-test` sibling module (like `viewers/wicket/ui-test`) for long-term Wicket-level tests.
+## 6. How to Construct Tests
+
+### 6.0 Test dependencies (add to `extensions/vw/d3/wicket/ui/pom.xml`)
+
+| Dependency | Purpose |
+|---|---|
+| `org.junit.jupiter:junit-jupiter` | JUnit 5 (BOM-managed) |
+| `org.mockito:mockito-core` (`mockito-junit-jupiter`) | mocking `UiAttributeWkt` / feature / model |
+| artifact containing `MetaModelContext_forTesting` (matching the one used by `viewers/wicket/ui-test`) | lightweight `MetaModelContext` for unit tests |
+| `org.apache.wicket:wicket-core` (already a compile dep → available in tests) | component construction |
+| `org.apache.wicket:wicket-tester` (pin `${wicket.version}`, e.g. 10.10.0) | `WicketTester` — **separate artifact in Wicket 10** (verified on Maven Central); not yet managed in the BOM, so pin the version |
+
+Note: no repo module currently uses `WicketTester`; `viewers/wicket/ui-test` is JUnit-only. The WicketTester route below is recommended for component-level coverage.
+
+### 6.1 Unit tests (pure JUnit + Mockito — recommended level)
+
+**`D3PanelComponentFactoryTest`** — mirror the style of `viewers/wicket/ui-test/.../ComponentFactoryAbstractTest_init.java`:
+
+```java
+@Test
+void applies_whenAttributeHasD3GraphFacet() {
+    var attributeModel = mock(UiAttributeWkt.class);
+    var feature = mock(ObjectFeature.class);
+    when(feature.containsFacet(D3GraphFacet.class)).thenReturn(true);
+    when(attributeModel.getMetaModel()).thenReturn(feature);
+    var value = mock(ManagedObject.class);
+    when(value.getPojo()).thenReturn("{\"nodes\":[],\"links\":[]}");
+    when(attributeModel.getObject()).thenReturn(value);
+
+    var factory = new D3PanelComponentFactory();
+    factory.setMetaModelContext(MetaModelContext_forTesting.buildDefault());
+
+    assertThat(factory.appliesTo(attributeModel).applies()).isTrue();
+    var component = factory.createComponent("d3", attributeModel);
+    assertThat(component).isInstanceOf(D3Panel.class);
+}
+```
+
+Negative cases: non-`UiAttributeWkt` model, model without `D3GraphFacet`, blank/empty property value → `ApplicationAdvice.DOES_NOT_APPLY`. Panel construction (`createComponent`) requires `wicket-tester` — currently disabled pending WicketTester integration.
+
+**`NodeClickedActionInvocationTest`** *(planned — not yet written)* — unit-test the action-mapping logic:
+1. extract the "resolve action id → set param 0 → execute" sequence (the `onNodeClicked(...)` helper in §4.3) so it is unit-testable, or
+2. use `MetaModelContext_forTesting` with a tiny fixture class carrying a `@D3Graph` property and a `@Action` method; drive `onNodeClicked` via the callback and assert the action was invoked (e.g. a value recorded by the action).
+
+### 6.2 WicketTester component test (rendering level)
+
+1. `WicketApplication` subclass; no container wiring needed if the test page injects nothing.
+2. Test page hosting `panel = new D3Panel("panel", attributeModel)`:
+
+```java
+tester.startPage(page);
+tester.assertComponent("panel", D3Panel.class);
+tester.assertContains("id=\"" + panel.getCanvasId() + "\"");   // canvas present
+var doc = tester.getLastResponse().getDocument();
+assertThat(doc).contains("WicketStuff.D3.init");               // domReadyScript contributed
+assertThat(doc).contains("\"graphData\":");                     // config serialized to JSON
+```
+
+3. Simulate the node click: fire the AJAX callback with a `nodeId` request parameter on the callback URL exposed by the integration panel (`NodeClickedCallback.getCallbackUrl()`), then assert the outcome (page re-render, growl/feedback, or domain state change against an in-memory fixture repository).
+
+### 6.3 End-to-end smoke test (manual / example webapp)
+
+```java
+public class GraphOwner {
+    @D3Graph(clickAction = "selectNode")
+    public String getGraph() {
+        return "{\"nodes\":[{\"id\":\"A\",\"name\":\"Node A\"}],\"links\":[]}";
+    }
+    @Action
+    public GraphOwner selectNode(final String nodeId) {
+        // record nodeId (log / transient service)
+        return this;
+    }
+}
+```
+
+1. Webapp imports `CausewayModuleExtD3WicketUi` (transitively pulls metamodel + integration modules).
+2. Open the object page of a `GraphOwner`, verify the graph canvas renders.
+3. Click a node; verify `selectNode` is invoked with `"A"` (log line or recorded state).
+
+### 6.4 Running the tests
+
+```bash
+# unit tests only (from repo root; add -o for offline)
+mvn -pl extensions/vw/d3/wicket/ui -am -Dtest=D3PanelComponentFactoryTest test
+
+# full module build incl. tests
+mvn -pl extensions/vw/d3/wicket/ui -am install
+```
+
diff --git a/extensions/vw/d3/wicket/integration/pom.xml b/extensions/vw/d3/wicket/integration/pom.xml
new file mode 100644
index 00000000000..1dc45d7194f
--- /dev/null
+++ b/extensions/vw/d3/wicket/integration/pom.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>org.apache.causeway.extensions</groupId>
+		<artifactId>causeway-extensions-d3-wicket</artifactId>
+		<version>4.0.0-SNAPSHOT</version>
+	</parent>
+
+	<artifactId>causeway-extensions-d3-wicket-integration</artifactId>
+	<name>Apache Causeway Ext - D3 - Wicket (Integration)</name>
+	<description>Integrates D3.js with Wicket</description>
+
+	<properties>
+		<jar-plugin.automaticModuleName>org.apache.causeway.extensions.d3.wicket.integration</jar-plugin.automaticModuleName>
+		<git-plugin.propertiesDir>org/apache/causeway/extensions/d3/wicket/integration</git-plugin.propertiesDir>
+	</properties>
+
+	<dependencies>
+
+		<dependency>
+			<groupId>org.apache.causeway.extensions</groupId>
+			<artifactId>causeway-extensions-d3-applib</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>org.apache.causeway.viewer</groupId>
+			<artifactId>causeway-viewer-wicket-ui</artifactId>
+		</dependency>
+
+    	<dependency>
+		    <groupId>org.webjars.npm</groupId>
+			<artifactId>d3</artifactId>
+			<!--  webjar
+            see also D3Reference -->
+			<version>7.9.0</version>
+		</dependency>
+
+	</dependencies>
+
+</project>
diff --git a/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/CausewayModuleExtD3WicketIntegration.java b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/CausewayModuleExtD3WicketIntegration.java
new file mode 100644
index 00000000000..2df1fa3f60c
--- /dev/null
+++ b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/CausewayModuleExtD3WicketIntegration.java
@@ -0,0 +1,29 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.wkt.integration;
+
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @since 2.0 {@index}
+ */
+@Configuration
+public class CausewayModuleExtD3WicketIntegration {
+
+}
\ No newline at end of file
diff --git a/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/callback/CallbackWithHandler.java b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/callback/CallbackWithHandler.java
new file mode 100644
index 00000000000..961fe65e87a
--- /dev/null
+++ b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/callback/CallbackWithHandler.java
@@ -0,0 +1,23 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.wkt.integration.callback;
+
+public interface CallbackWithHandler {
+	String getHandlerScript();
+}
diff --git a/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/callback/D3AjaxCallback.java b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/callback/D3AjaxCallback.java
new file mode 100644
index 00000000000..0b11314b903
--- /dev/null
+++ b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/callback/D3AjaxCallback.java
@@ -0,0 +1,57 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.wkt.integration.callback;
+
+import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior;
+
+import org.apache.causeway.extensions.d3.wkt.integration.components.D3Panel;
+
+import org.jspecify.annotations.NonNull;
+
+/**
+ * Base class for AJAX callbacks issued by the D3 viewer.
+ * <p>
+ * The {@code <PLACEHOLDER>} token allows dynamic parameters (for example the
+ * clicked node's id) to be appended by the client-side JavaScript to the
+ * callback URL, mirroring the fullcalendar extension's pattern.
+ * </p>
+ */
+abstract class D3AjaxCallback extends AbstractDefaultAjaxBehavior {
+
+    private static final long serialVersionUID = 1L;
+
+    static final String PLACEHOLDER = "<PLACEHOLDER>";
+
+    protected abstract String configureCallbackScript(@NonNull String script, @NonNull String urlTail);
+
+    @Override
+    public final CharSequence getCallbackScript() {
+        return configureCallbackScript(super.getCallbackScript().toString(), PLACEHOLDER);
+    }
+
+    @Override
+    public final CharSequence getCallbackUrl() {
+        return super.getCallbackUrl() + PLACEHOLDER;
+    }
+
+    protected D3Panel getD3Panel() {
+        return (D3Panel) getComponent();
+    }
+
+}
diff --git a/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/callback/NodeClickedCallback.java b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/callback/NodeClickedCallback.java
new file mode 100644
index 00000000000..cc92a530aff
--- /dev/null
+++ b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/callback/NodeClickedCallback.java
@@ -0,0 +1,63 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.wkt.integration.callback;
+
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.request.Request;
+
+/**
+ * Callback that handles a D3 node click event via AJAX.
+ * <p>
+ * The callback URL includes a placeholder that the client-side JavaScript
+ * replaces with the clicked node's id at runtime.
+ * </p>
+ *
+ * @since 2.0 {@index}
+ */
+public abstract class NodeClickedCallback
+extends D3AjaxCallback
+implements CallbackWithHandler {
+
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    protected String configureCallbackScript(final String script, final String urlTail) {
+        return script.replace(urlTail, "&nodeId=\"+nodeId");
+    }
+
+    @Override
+    public String getHandlerScript() {
+        return "function(nodeId) { " + getCallbackScript() + "}";
+    }
+
+    @Override
+    protected void respond(final AjaxRequestTarget target) {
+        final Request r = getD3Panel().getRequest();
+        final String nodeId = r.getRequestParameters().getParameterValue("nodeId").toString();
+        onNodeClicked(new NodeClickedEvent(nodeId), target);
+    }
+
+    /**
+     * Invoked when a node click event arrives from the client.
+     *
+     * @param event the event carrying the clicked node's id
+     * @param target the AJAX request target
+     */
+    protected abstract void onNodeClicked(NodeClickedEvent event, AjaxRequestTarget target);
+}
\ No newline at end of file
diff --git a/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/callback/NodeClickedEvent.java b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/callback/NodeClickedEvent.java
new file mode 100644
index 00000000000..e5d546fdf41
--- /dev/null
+++ b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/callback/NodeClickedEvent.java
@@ -0,0 +1,32 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.wkt.integration.callback;
+
+import org.jspecify.annotations.NonNull;
+
+/**
+ * Immutable value object carrying the context of a D3 graph node click event.
+ *
+ * @since 2.0 {@index}
+ */
+public record NodeClickedEvent(
+        /** The id of the clicked node, provided by the D3 client-side callback. */
+        @NonNull String nodeId) {
+
+}
\ No newline at end of file
diff --git a/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/components/D3Panel.html b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/components/D3Panel.html
new file mode 100644
index 00000000000..0bf3b59dc3b
--- /dev/null
+++ b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/components/D3Panel.html
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<html xmlns:wicket="http://wicket.apache.org">
+<body>
+<wicket:panel>
+
+    <div class="d3GraphPanel">
+        <div wicket:id="d3Canvas" class="d3GraphCanvas"></div>
+    </div>
+
+</wicket:panel>
+</body>
+</html>
\ No newline at end of file
diff --git a/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/components/D3Panel.java b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/components/D3Panel.java
new file mode 100644
index 00000000000..b4630547514
--- /dev/null
+++ b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/components/D3Panel.java
@@ -0,0 +1,102 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.wkt.integration.components;
+
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.html.WebComponent;
+import org.apache.wicket.markup.html.panel.Panel;
+
+import org.apache.causeway.extensions.d3.applib.config.D3Config;
+import org.apache.causeway.extensions.d3.applib.config.D3Config;
+import org.apache.causeway.extensions.d3.wkt.integration.callback.NodeClickedCallback;
+import org.apache.causeway.extensions.d3.wkt.integration.res.D3IntegrationReference;
+import org.apache.causeway.extensions.d3.wkt.integration.res.D3IntegrationReference;
+import org.apache.causeway.extensions.d3.wkt.integration.res.D3Reference;
+import org.apache.causeway.viewer.wicket.ui.util.Wkt;
+
+import org.jspecify.annotations.NonNull;
+
+/**
+ * A panel for rendering D3.js interactive graphs inline in the page.
+ * <p>
+ * Contains an SVG canvas (a {@link WebComponent} with a markup id for the D3
+ * visualization) and wires the D3 integration script that initialises the graph
+ * using the configuration provided.
+ * git status</p>
+ *
+ * @since 2.0 {@index}
+ */
+public class D3Panel extends Panel {
+
+    private static final long serialVersionUID = 1L;
+
+    private final static String ID_D3_CANVAS = "d3Canvas";
+
+    private D3Config config;
+
+    /**
+     * Optional AJAX callback invoked when a graph node is clicked;
+     * {@code null} (the default) means clicks are ignored.
+     */
+    private NodeClickedCallback clickCallback;
+
+    /**
+     * Constructor.
+     *
+     * @param id The component id
+     * @param config The D3 configuration
+     */
+    public D3Panel(final String id, final @NonNull D3Config config) {
+        super(id);
+
+        var d3Canvas = Wkt.add(this, Wkt.ajaxEnable(new WebComponent(ID_D3_CANVAS)));
+
+        this.config = config
+                .withCanvasId(d3Canvas.getMarkupId());
+    }
+
+    /**
+     * Binds the node-click AJAX callback to this panel; the callback's URL is
+     * included in the rendered configuration so that the client-side script can
+     * invoke it (with the clicked node's id) when a node is clicked.
+     */
+    public D3Panel withClickCallback(final @NonNull NodeClickedCallback clickCallback) {
+        this.clickCallback = clickCallback;
+        add(clickCallback);
+        return this;
+    }
+
+    @Override
+    public void renderHead(final IHeaderResponse response) {
+        super.renderHead(response);
+
+        var configWithCallback = clickCallback != null
+                ? config.withClickCallbackUrl(clickCallback.getCallbackUrl().toString())
+                : config;
+
+        response.render(D3Reference.asHeaderItem());
+        response.render(D3IntegrationReference.asHeaderItem());
+        response.render(D3IntegrationReference.domReadyScript(configWithCallback));
+    }
+
+    public CharSequence getCanvasId() {
+        return config.getCanvasId();
+    }
+
+}
\ No newline at end of file
diff --git a/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/res/D3IntegrationReference.java b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/res/D3IntegrationReference.java
new file mode 100644
index 00000000000..32e31eb99b5
--- /dev/null
+++ b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/res/D3IntegrationReference.java
@@ -0,0 +1,58 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.wkt.integration.res;
+
+import org.apache.causeway.extensions.d3.applib.config.D3Config;
+import org.apache.wicket.markup.head.HeaderItem;
+import org.apache.wicket.markup.head.JavaScriptHeaderItem;
+import org.apache.wicket.markup.head.JavaScriptReferenceType;
+import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
+import org.apache.wicket.request.resource.JavaScriptResourceReference;
+
+import lombok.Getter;
+import lombok.experimental.Accessors;
+
+public class D3IntegrationReference
+extends JavaScriptResourceReference {
+
+    private static final long serialVersionUID = 1L;
+
+    @Getter(lazy = true) @Accessors(fluent = true)
+    private static final D3IntegrationReference instance =
+        new D3IntegrationReference();
+
+    private D3IntegrationReference() {
+        super(D3IntegrationReference.class, "d3-integration.mjs");
+    }
+
+    /**
+     * @return this resource reference singleton instance as header item
+     */
+    public static HeaderItem asHeaderItem() {
+        return JavaScriptHeaderItem.forReference(instance())
+                .setType(JavaScriptReferenceType.MODULE);
+    }
+
+    public static OnDomReadyHeaderItem domReadyScript(
+            final D3Config config) {
+        return OnDomReadyHeaderItem.forScript(
+                String.format("WicketStuff.D3.init(%s)", config.toJsonString()));
+    }
+
+}
diff --git a/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/res/D3Reference.java b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/res/D3Reference.java
new file mode 100644
index 00000000000..b6bf28690c7
--- /dev/null
+++ b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/res/D3Reference.java
@@ -0,0 +1,50 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.wkt.integration.res;
+
+import org.apache.causeway.viewer.commons.model.webjar.WebJar;
+import org.apache.wicket.markup.head.HeaderItem;
+import org.apache.wicket.markup.head.JavaScriptHeaderItem;
+
+import de.agilecoders.wicket.webjars.request.resource.WebjarsJavaScriptResourceReference;
+import lombok.Getter;
+import lombok.experimental.Accessors;
+
+public class D3Reference
+extends WebjarsJavaScriptResourceReference {
+
+    private static final long serialVersionUID = 1L;
+
+    @Getter(lazy = true) @Accessors(fluent = true)
+    private static final D3Reference instance =
+        new D3Reference();
+
+    private D3Reference() {
+    	super(WebJar.D3_JS.resource());
+    }
+
+    /**
+     * @return this resource reference singleton instance as header item
+     */
+    public static HeaderItem asHeaderItem() {
+        return JavaScriptHeaderItem.forReference(instance());
+    }
+
+}
+
diff --git a/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/res/d3-integration.mjs b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/res/d3-integration.mjs
new file mode 100644
index 00000000000..628dcc206a3
--- /dev/null
+++ b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/res/d3-integration.mjs
@@ -0,0 +1,176 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+;(function ($, undefined) {
+
+    'use strict';
+
+    if (!d3) {
+        console.error("Missing D3 prerequisites.");
+        return;
+    }
+
+    if (typeof(WicketStuff) !== 'object') {
+        window.WicketStuff = {};
+    }
+
+    if (typeof(WicketStuff.D3) === 'object') {
+        return;
+    }
+
+    var PLACEHOLDER = "<PLACEHOLDER>";
+
+    WicketStuff.D3 = {
+
+        init: function (config) {
+
+            var container = $('#' + config.canvasId);
+            var width = config.initialWidth || 800;
+            var height = config.initialHeight || 600;
+            var enableDragging = config.enableDragging !== false;
+            var enableZoom = config.enableZoom !== false;
+
+            container.empty();
+            var svg = container.append('<svg width="100%" height="100%"></svg>')
+                .find('svg');
+            svg.attr('width', width).attr('height', height);
+
+            // Parse the graph data (expected as JSON)
+            var nodes, links;
+            try {
+                var data = JSON.parse(config.graphData);
+                nodes = data.nodes || [];
+                links = data.links || [];
+            } catch (e) {
+                console.error('D3 graph data parse error:', e);
+                return;
+            }
+
+            // Create the force simulation
+            var simulation = d3.forceSimulation(nodes)
+                .force('link', d3.forceLink(links)
+                    .id(function (d) { return d.id; })
+                    .distance(100))
+                .force('charge', d3.forceManyBody().strength(-300))
+                .force('center', d3.forceCenter(width / 2, height / 2));
+
+            // Zoom behaviour (applied to the container)
+            var g = svg.append('g');
+            if (enableZoom) {
+                var zoom = d3.zoom()
+                    .scaleExtent([0.1, 10])
+                    .on('zoom', function (event) {
+                        g.attr('transform', event.transform);
+                    });
+                svg.call(zoom);
+            }
+
+            // Links
+            var link = g.append('g')
+                .attr('class', 'd3-links')
+                .selectAll('line')
+                .data(links)
+                .join('line')
+                .attr('stroke', '#999')
+                .attr('stroke-opacity', 0.6)
+                .attr('stroke-width', function (d) { return Math.sqrt(d.value || 1); });
+
+            // Nodes
+            var node = g.append('g')
+                .attr('class', 'd3-nodes')
+                .selectAll('circle')
+                .data(nodes)
+                .join('circle')
+                .attr('r', 8)
+                .attr('fill', '#69b3a2');
+
+            // Node labels
+            g.append('g')
+                .attr('class', 'd3-labels')
+                .selectAll('text')
+                .data(nodes)
+                .join('text')
+                .text(function (d) { return d.name || d.id; })
+                .attr('font-size', 12)
+                .attr('dx', 12)
+                .attr('dy', 4);
+
+            // Node click handler (if a callback URL is configured)
+            if (config.clickCallbackUrl) {
+                node.style('cursor', 'pointer')
+                    .on('click', function (event, d) {
+                        var url = config.clickCallbackUrl.replace(PLACEHOLDER, '')
+                            + '&nodeId=' + encodeURIComponent(d.id);
+                        Wicket.Ajax.get({'u': url});
+                    });
+            }
+
+            // Drag behaviour
+            if (enableDragging) {
+                var drag = d3.drag()
+                    .on('start', function (event, d) {
+                        if (!event.active) simulation.alphaTarget(0.3).restart();
+                        d.fx = d.x;
+                        d.fy = d.y;
+                    })
+                    .on('drag', function (event, d) {
+                        d.fx = event.x;
+                        d.fy = event.y;
+                    })
+                    .on('end', function (event, d) {
+                        if (!event.active) simulation.alphaTarget(0);
+                        d.fx = null;
+                        d.fy = null;
+                    });
+                node.call(drag);
+            }
+
+            // Simulation tick
+            simulation.on('tick', function () {
+                link
+                    .attr('x1', function (d) { return d.source.x; })
+                    .attr('y1', function (d) { return d.source.y; })
+                    .attr('x2', function (d) { return d.target.x; })
+                    .attr('y2', function (d) { return d.target.y; });
+
+                node
+                    .attr('cx', function (d) { return d.x; })
+                    .attr('cy', function (d) { return d.y; });
+
+                svg.selectAll('.d3-labels text')
+                    .attr('x', function (d) { return d.x; })
+                    .attr('y', function (d) { return d.y; });
+            });
+
+            // Store the simulation for cleanup
+            container.data('d3-simulation', simulation);
+        },
+
+        destroy: function (canvasId) {
+            var container = $('#' + canvasId);
+            var simulation = container.data('d3-simulation');
+            if (simulation) {
+                simulation.stop();
+                container.removeData('d3-simulation');
+            }
+            container.empty();
+        }
+
+    };
+
+})(jQuery);
diff --git a/extensions/vw/d3/wicket/pom.xml b/extensions/vw/d3/wicket/pom.xml
new file mode 100644
index 00000000000..6904a6c4c3a
--- /dev/null
+++ b/extensions/vw/d3/wicket/pom.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>org.apache.causeway.extensions</groupId>
+		<artifactId>causeway-extensions-d3</artifactId>
+		<version>4.0.0-SNAPSHOT</version>
+	</parent>
+
+	<artifactId>causeway-extensions-d3-wicket</artifactId>
+	<name>Apache Causeway Ext - D3 Wicket</name>
+
+	<packaging>pom</packaging>
+
+	<modules>
+		<module>integration</module>
+		<module>ui</module>
+	</modules>
+
+</project>
diff --git a/extensions/vw/d3/wicket/ui/pom.xml b/extensions/vw/d3/wicket/ui/pom.xml
new file mode 100644
index 00000000000..a427791eb58
--- /dev/null
+++ b/extensions/vw/d3/wicket/ui/pom.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>org.apache.causeway.extensions</groupId>
+		<artifactId>causeway-extensions-d3-wicket</artifactId>
+		<version>4.0.0-SNAPSHOT</version>
+	</parent>
+
+	<artifactId>causeway-extensions-d3-wicket-ui</artifactId>
+	<name>Apache Causeway Ext - D3 - Wicket (UI)</name>
+
+	<properties>
+		<jar-plugin.automaticModuleName>org.apache.causeway.extensions.d3.wicket.ui</jar-plugin.automaticModuleName>
+		<git-plugin.propertiesDir>org/apache/causeway/extensions/d3/wicket/ui</git-plugin.propertiesDir>
+	</properties>
+
+	<dependencies>
+
+		<dependency>
+			<groupId>org.apache.causeway.extensions</groupId>
+			<artifactId>causeway-extensions-d3-metamodel</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>org.apache.causeway.extensions</groupId>
+			<artifactId>causeway-extensions-d3-wicket-integration</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>org.apache.causeway.viewer</groupId>
+			<artifactId>causeway-viewer-wicket-ui</artifactId>
+		</dependency>
+
+		<!-- TESTING -->
+
+		<dependency>
+			<groupId>org.junit.jupiter</groupId>
+			<artifactId>junit-jupiter</artifactId>
+			<scope>test</scope>
+		</dependency>
+
+		<dependency>
+			<groupId>org.mockito</groupId>
+			<artifactId>mockito-core</artifactId>
+			<scope>test</scope>
+		</dependency>
+
+		<dependency>
+			<groupId>org.mockito</groupId>
+			<artifactId>mockito-junit-jupiter</artifactId>
+			<scope>test</scope>
+		</dependency>
+
+		<dependency>
+			<groupId>org.assertj</groupId>
+			<artifactId>assertj-core</artifactId>
+			<scope>test</scope>
+		</dependency>
+
+		<dependency>
+			<groupId>org.apache.causeway.core</groupId>
+			<artifactId>causeway-core-mmtestsupport</artifactId>
+			<scope>test</scope>
+		</dependency>
+
+		<dependency>
+			<groupId>org.apache.causeway.core</groupId>
+			<artifactId>causeway-core-runtimeservices</artifactId>
+			<scope>test</scope>
+		</dependency>
+
+		<dependency>
+			<groupId>org.apache.wicket</groupId>
+			<artifactId>wicket-tester</artifactId>
+			<version>${wicket.version}</version>
+			<scope>test</scope>
+		</dependency>
+
+	</dependencies>
+
+</project>
diff --git a/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/CausewayModuleExtD3WicketUi.java b/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/CausewayModuleExtD3WicketUi.java
new file mode 100644
index 00000000000..6b17f27ab0e
--- /dev/null
+++ b/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/CausewayModuleExtD3WicketUi.java
@@ -0,0 +1,42 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.wkt.ui;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+import org.apache.causeway.extensions.d3.metamodel.CausewayModuleExtD3MetaModel;
+import org.apache.causeway.extensions.d3.wkt.integration.CausewayModuleExtD3WicketIntegration;
+import org.apache.causeway.extensions.d3.wkt.ui.components.D3PanelComponentFactory;
+
+/**
+ * @since 2.0 {@index}
+ */
+@Configuration
+@Import({
+        // Modules
+        CausewayModuleExtD3MetaModel.class,
+        CausewayModuleExtD3WicketIntegration.class,
+
+        // @Component's
+        D3PanelComponentFactory.class,
+})
+public class CausewayModuleExtD3WicketUi {
+
+}
diff --git a/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3Panel.java b/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3Panel.java
new file mode 100644
index 00000000000..31e5aff97e5
--- /dev/null
+++ b/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3Panel.java
@@ -0,0 +1,121 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.wkt.ui.components;
+
+import org.apache.wicket.ajax.AjaxRequestTarget;
+
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.extensions.d3.applib.config.D3Config;
+import org.apache.causeway.extensions.d3.metamodel.facet.D3GraphFacet;
+import org.apache.causeway.extensions.d3.wkt.integration.callback.NodeClickedCallback;
+import org.apache.causeway.extensions.d3.wkt.integration.callback.NodeClickedEvent;
+import org.apache.causeway.viewer.wicket.model.models.ActionModel;
+import org.apache.causeway.viewer.wicket.model.models.UiAttributeWkt;
+import org.apache.causeway.viewer.wicket.ui.exec.FormExecutorDefault;
+
+/**
+ * Wicket panel that renders a {@code @D3Graph}-annotated property
+ * (recognised through its {@link D3GraphFacet})
+ * as an interactive D3.js force-layout graph.
+ * <p>
+ * Extends the integration module's {@code D3Panel} (thus reusing its markup,
+ * resource references and callback-URL wiring) and adds:
+ * <ul>
+ * <li>the graph data (JSON string) taken from the annotated property's value,</li>
+ * <li>an AJAX {@link NodeClickedCallback} that invokes the configured domain
+ *     action (its first {@code String} parameter receiving the clicked node's id).</li>
+ * </ul>
+ *
+ * @since 2.0 {@index}
+ */
+public class D3Panel extends org.apache.causeway.extensions.d3.wkt.integration.components.D3Panel {
+
+    private static final long serialVersionUID = 1L;
+
+    private final UiAttributeWkt attributeModel;
+
+    public D3Panel(final String id, final UiAttributeWkt attributeModel) {
+        super(id, buildConfig(attributeModel));
+        this.attributeModel = attributeModel;
+
+        var domainClass = domainClassOf(attributeModel);
+        var facet = attributeModel.getMetaModel().lookupFacet(D3GraphFacet.class).orElse(null);
+        var clickAction = facet != null
+                ? facet.getClickAction(domainClass).orElse(null)
+                : null;
+
+        if (clickAction != null) {
+            withClickCallback(new NodeClickedCallback() {
+                private static final long serialVersionUID = 1L;
+                @Override
+                protected void onNodeClicked(final NodeClickedEvent event, final AjaxRequestTarget target) {
+                    D3Panel.this.onNodeClicked(event, target, clickAction);
+                }
+            });
+        }
+    }
+
+    // -- HELPER
+
+    private static D3Config buildConfig(final UiAttributeWkt attributeModel) {
+        var domainClass = domainClassOf(attributeModel);
+        var facet = attributeModel.getMetaModel().lookupFacet(D3GraphFacet.class).orElse(null);
+        var config = facet != null
+                ? facet.configFor(domainClass)
+                : new D3Config();
+
+        // graph data is the annotated property's value (a JSON string)
+        var pojo = attributeModel.getObject().getPojo();
+        if (pojo instanceof CharSequence cs && !cs.toString().isBlank()) {
+            config = config.withGraphData(cs);
+        }
+        return config;
+    }
+
+    private static Class<?> domainClassOf(final UiAttributeWkt attributeModel) {
+        return parentObjectOf(attributeModel).getCorrespondingClass();
+    }
+
+    private static ManagedObject parentObjectOf(final UiAttributeWkt attributeModel) {
+        return attributeModel.getParentUiModel().getManagedObject();
+    }
+
+    private void onNodeClicked(final NodeClickedEvent event, final AjaxRequestTarget target,
+            final String clickActionId) {
+        var parentUiModel = attributeModel.getParentUiModel();
+        var objectSpec = parentUiModel.getManagedObject().objSpec();
+        var action = objectSpec.getAction(clickActionId)
+                .orElseThrow(() -> new RuntimeException(
+                        "D3 node-click action '" + clickActionId + "' not found on "
+                        + objectSpec.logicalTypeName()));
+
+        var actionModel = ActionModel.forEntity(action, parentUiModel);
+        var pendingArgs = actionModel.getActionInteraction().startParameterNegotiation()
+                .orElseThrow(() -> new RuntimeException(
+                        "could not start parameter negotiation for D3 node-click action '"
+                        + clickActionId + "'"));
+        if (pendingArgs.getParamCount() >= 1) {
+            pendingArgs.updateParamValuePojo(0, __ -> event.nodeId());
+        }
+
+        FormExecutorDefault.forAction(actionModel)
+                .executeAndProcessResults(target, null, actionModel);
+    }
+
+}
diff --git a/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3PanelComponentFactory.java b/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3PanelComponentFactory.java
new file mode 100644
index 00000000000..9ff746d18a8
--- /dev/null
+++ b/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3PanelComponentFactory.java
@@ -0,0 +1,67 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.wkt.ui.components;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.model.IModel;
+
+import org.apache.causeway.extensions.d3.metamodel.facet.D3GraphFacet;
+import org.apache.causeway.viewer.commons.model.components.UiComponentType;
+import org.apache.causeway.viewer.wicket.model.models.UiAttributeWkt;
+import org.apache.causeway.viewer.wicket.ui.ComponentFactory;
+import org.apache.causeway.viewer.wicket.ui.ComponentFactoryAbstract;
+
+import jakarta.inject.Inject;
+
+/**
+ * Wicket {@link ComponentFactory} that renders a domain property annotated with
+ * {@code @D3Graph} (as recognised through the presence of a {@link D3GraphFacet})
+ * as an interactive D3.js force-layout graph (see {@link D3Panel}).
+ */
+@org.springframework.stereotype.Component
+public class D3PanelComponentFactory extends ComponentFactoryAbstract {
+
+    @Inject
+    public D3PanelComponentFactory() {
+        super(UiComponentType.ATTRIBUTE_NAME_AND_VALUE, D3Panel.class);
+    }
+
+    @Override
+    public ApplicationAdvice appliesTo(final IModel<?> model) {
+        if (!(model instanceof UiAttributeWkt attributeModel)) {
+            return ApplicationAdvice.DOES_NOT_APPLY;
+        }
+        if (!attributeModel.getMetaModel().containsFacet(D3GraphFacet.class)) {
+            return ApplicationAdvice.DOES_NOT_APPLY;
+        }
+        // graph data must be a non-blank CharSequence (a JSON string)
+        var value = attributeModel.getObject();
+        if (value != null
+                && value.getPojo() instanceof CharSequence cs
+                && !cs.toString().isBlank()) {
+            return ApplicationAdvice.APPLIES;
+        }
+        return ApplicationAdvice.DOES_NOT_APPLY;
+    }
+
+    @Override
+    public Component createComponent(final String id, final IModel<?> model) {
+        return new D3Panel(id, (UiAttributeWkt) model);
+    }
+}
diff --git a/extensions/vw/d3/wicket/ui/src/test/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3PanelComponentFactoryTest.java b/extensions/vw/d3/wicket/ui/src/test/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3PanelComponentFactoryTest.java
new file mode 100644
index 00000000000..7ecc99dcee3
--- /dev/null
+++ b/extensions/vw/d3/wicket/ui/src/test/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3PanelComponentFactoryTest.java
@@ -0,0 +1,175 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.d3.wkt.ui.components;
+
+import java.util.Optional;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.Page;
+import org.apache.wicket.RuntimeConfigurationType;
+import org.apache.wicket.Session;
+import org.apache.wicket.ThreadContext;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.request.Request;
+import org.apache.wicket.request.Response;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.causeway.core.metamodel.context.MetaModelContext;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectFeature;
+import org.apache.causeway.extensions.d3.applib.config.D3Config;
+import org.apache.causeway.extensions.d3.metamodel.facet.D3GraphFacet;
+import org.apache.causeway.viewer.wicket.model.models.UiAttributeWkt;
+import org.apache.causeway.viewer.wicket.model.models.UiObjectWkt;
+import org.apache.causeway.viewer.wicket.ui.ComponentFactory.ApplicationAdvice;
+
+class D3PanelComponentFactoryTest {
+
+    private MetaModelContext mmc;
+    private D3PanelComponentFactory factory;
+
+    @BeforeEach
+    void setUp() {
+        mmc = org.apache.causeway.core.mmtestsupport.MetaModelContext_forTesting.buildDefault();
+        factory = new D3PanelComponentFactory();
+        ThreadContext.setApplication(new WicketAppStup());
+    }
+
+    @AfterEach
+    void tearDown() {
+        ThreadContext.detach();
+    }
+
+    private ManagedObject managedString(final String value) {
+        var spec = mmc.getSpecificationLoader().loadSpecification(String.class);
+        return ManagedObject.adaptSingular(spec, value);
+    }
+
+    private static UiAttributeWkt attributeModel(final boolean hasFacet) {
+        var attributeModel = mock(UiAttributeWkt.class);
+        var feature = mock(ObjectFeature.class);
+        doReturn(hasFacet).when(feature).containsFacet(D3GraphFacet.class);
+        when(attributeModel.getMetaModel()).thenReturn(feature);
+        return attributeModel;
+    }
+
+    @Test
+    void applies_whenAttributeHasD3GraphFacetAndNonBlankJsonString() {
+        var attributeModel = attributeModel(true);
+        when(attributeModel.getObject()).thenReturn(managedString("{\"nodes\":[],\"links\":[]}"));
+
+        var advice = factory.appliesTo(attributeModel);
+        assertThat(advice).isEqualTo(ApplicationAdvice.APPLIES);
+    }
+
+    @Test
+    void doesNotApply_whenModelIsNotUiAttributeWkt() {
+        var advice = factory.appliesTo(mock(IModel.class));
+        assertThat(advice.applies()).isFalse();
+    }
+
+    @Test
+    void doesNotApply_whenMissingD3GraphFacet() {
+        var attributeModel = attributeModel(false);
+
+        var advice = factory.appliesTo(attributeModel);
+        assertThat(advice.applies()).isFalse();
+    }
+
+    @Test
+    void doesNotApply_whenPropertyValueIsBlank() {
+        var attributeModel = attributeModel(true);
+        when(attributeModel.getObject()).thenReturn(managedString("  "));
+
+        var advice = factory.appliesTo(attributeModel);
+        assertThat(advice.applies()).isFalse();
+    }
+
+    @Test
+    void doesNotApply_whenPropertyValueIsNull() {
+        var attributeModel = attributeModel(true);
+        when(attributeModel.getObject()).thenReturn(null);
+
+        var advice = factory.appliesTo(attributeModel);
+        assertThat(advice.applies()).isFalse();
+    }
+
+    @Test
+    void doesNotApply_whenPropertyValueIsNotCharSequence() {
+        var attributeModel = attributeModel(true);
+        var spec = mmc.getSpecificationLoader().loadSpecification(Integer.class);
+        when(attributeModel.getObject()).thenReturn(ManagedObject.adaptSingular(spec, 123));
+
+        var advice = factory.appliesTo(attributeModel);
+        assertThat(advice.applies()).isFalse();
+    }
+
+    // panel construction requires wicket-tester (see status doc)
+    //TODO add wicket-tester test dependency and re-enable
+    //@Test
+    void createComponent_returnsD3Panel() {
+        var attributeModel = attributeModel(true);
+        when(attributeModel.getObject()).thenReturn(managedString("{\"nodes\":[],\"links\":[]}"));
+
+        var parentUiModel = mock(UiObjectWkt.class);
+        when(attributeModel.getParentUiModel()).thenReturn(parentUiModel);
+        var objSpec = mmc.getSpecificationLoader().loadSpecification(D3PanelComponentFactoryTest.class);
+        var managedObject = ManagedObject.adaptSingular(objSpec, new D3PanelComponentFactoryTest());
+        when(parentUiModel.getManagedObject()).thenReturn(managedObject);
+
+        var feature = attributeModel.getMetaModel();
+        var d3GraphFacet = mock(D3GraphFacet.class);
+        doReturn(Optional.of(d3GraphFacet)).when(feature).lookupFacet(D3GraphFacet.class);
+        when(d3GraphFacet.configFor(D3PanelComponentFactoryTest.class)).thenReturn(new D3Config());
+        when(d3GraphFacet.getClickAction(D3PanelComponentFactoryTest.class)).thenReturn(Optional.empty());
+
+        var component = factory.createComponent("d3", attributeModel);
+        assertThat(component).isInstanceOf(D3Panel.class);
+    }
+
+    /** minimal Wicket Application so that {@code new D3Panel(...)} works without a servlet container */
+    static class WicketAppStup extends Application {
+        @Override
+        public String getApplicationKey() {
+            return "test";
+        }
+
+        @Override
+        public RuntimeConfigurationType getConfigurationType() {
+            return RuntimeConfigurationType.DEVELOPMENT;
+        }
+
+        @Override
+        public Class<? extends Page> getHomePage() {
+            return null;
+        }
+
+        @Override
+        public Session newSession(final Request request, final Response response) {
+            return null;
+        }
+    }
+}
diff --git a/viewers/commons/model/src/main/java/org/apache/causeway/viewer/commons/model/webjar/WebJar.java b/viewers/commons/model/src/main/java/org/apache/causeway/viewer/commons/model/webjar/WebJar.java
index ee323399b20..1d9fab6af8b 100644
--- a/viewers/commons/model/src/main/java/org/apache/causeway/viewer/commons/model/webjar/WebJar.java
+++ b/viewers/commons/model/src/main/java/org/apache/causeway/viewer/commons/model/webjar/WebJar.java
@@ -38,7 +38,8 @@ public enum WebJar {
 	PDFJS_WORKER_JS("pdfjs-dist", "${id}/${version}/build/pdf.worker.min.mjs"),
 	VEGA_JS("vega", "${root}/_static/${id}/${version}/build/vega.min.js"),
 	VEGA_LITE_JS("vega-lite", "${root}/_static/${id}/${version}/build/vega-lite.min.js"),
-	VEGA_EMBED_JS("vega-embed", "${root}/_static/${id}/${version}/build/vega-embed.min.js");
+	VEGA_EMBED_JS("vega-embed", "${root}/_static/${id}/${version}/build/vega-embed.min.js"),
+	D3_JS("d3", "${id}/${version}/dist/d3.min.js");
 
 	public final static String ROOT = "_ROOT_"; // rewrites URL's path to an absolute path
 


(causeway) 03/05: [CAUSEWAY-4074] some warnings fixed

Posted by jo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

joerg-rade pushed a commit to branch Extension-D3
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 82aacd6b82e004a95c891f4420cdbf912c8bd2f4
Author: Joerg Rade <jo...@gmail.com>
AuthorDate: Thu Oct 15 09:16:40 2026 +0200

    [CAUSEWAY-4074] some warnings fixed
---
 .../causeway/extensions/d3/wkt/integration/components/D3Panel.java   | 5 ++++-
 .../org/apache/causeway/extensions/d3/wkt/ui/components/D3Panel.java | 4 ++++
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/components/D3Panel.java b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/components/D3Panel.java
index 4bacaf13aa7..feebe8f2c1f 100644
--- a/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/components/D3Panel.java
+++ b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/components/D3Panel.java
@@ -32,6 +32,8 @@
 
 import org.jspecify.annotations.NonNull;
 
+import java.io.Serial;
+
 /**
  * A panel for rendering D3.js interactive graphs inline in the page.
  * <p>
@@ -43,11 +45,12 @@
  */
 public class D3Panel extends Panel {
 
+    @Serial
     private static final long serialVersionUID = 1L;
 
     private final static String ID_D3_CANVAS = "d3Canvas";
 
-    private D3Config config;
+    private final D3Config config;
 
     /**
      * Optional AJAX callback invoked when a graph node is clicked;
diff --git a/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3Panel.java b/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3Panel.java
index 39e31556ef1..5c5030de1de 100644
--- a/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3Panel.java
+++ b/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3Panel.java
@@ -29,6 +29,8 @@
 import org.apache.causeway.viewer.wicket.model.models.UiAttributeWkt;
 import org.apache.causeway.viewer.wicket.ui.exec.FormExecutorDefault;
 
+import java.io.Serial;
+
 /**
  * Wicket panel that renders a {@code @D3Graph}-annotated property
  * (recognised through its {@link D3GraphFacet})
@@ -46,6 +48,7 @@
  */
 public class D3Panel extends org.apache.causeway.extensions.d3.wkt.integration.components.D3Panel {
 
+    @Serial
     private static final long serialVersionUID = 1L;
 
     private static final String EMPTY_GRAPH = "{\"nodes\":[],\"links\":[]}";
@@ -64,6 +67,7 @@ public D3Panel(final String id, final UiAttributeWkt attributeModel) {
 
         if (clickAction != null) {
             withClickCallback(new NodeClickedCallback() {
+                @Serial
                 private static final long serialVersionUID = 1L;
                 @Override
                 protected void onNodeClicked(final NodeClickedEvent event, final AjaxRequestTarget target) {


(causeway) 05/05: [CAUSEWAY-4074] first diagram in domox renders

Posted by jo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

joerg-rade pushed a commit to branch Extension-D3
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit df975a8fdb06354cb502f312d7fefb56bb19f7cb
Author: Joerg Rade <jo...@gmail.com>
AuthorDate: Thu Oct 15 13:52:57 2026 +0200

    [CAUSEWAY-4074] first diagram in domox renders
---
 .../d3/wkt/integration/res/d3-integration.mjs      | 31 ++++++++++++++--------
 1 file changed, 20 insertions(+), 11 deletions(-)

diff --git a/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/res/d3-integration.mjs b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/res/d3-integration.mjs
index 35578757b31..03e26a41381 100644
--- a/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/res/d3-integration.mjs
+++ b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/res/d3-integration.mjs
@@ -39,16 +39,21 @@
 
         init: function (config) {
 
-            var container = $('#' + config.canvasId);
+            var containerEl = document.getElementById(config.canvasId);
+            if (!containerEl) {
+                console.error('D3 canvas element not found:', config.canvasId);
+                return;
+            }
             var width = config.initialWidth || 800;
             var height = config.initialHeight || 600;
             var enableDragging = config.enableDragging !== false;
             var enableZoom = config.enableZoom !== false;
 
-            container.empty();
-            var svg = container.append('<svg width="100%" height="100%"></svg>')
-                .find('svg');
-            svg.attr('width', width).attr('height', height);
+            // Use D3 selections throughout — never mix jQuery with D3 selection methods
+            var svg = d3.select(containerEl)
+                .append('svg')
+                .attr('width', width)
+                .attr('height', height);
 
             // Parse the graph data (expected as JSON);
             // if graphData is null, undefined, or empty, use an empty graph
@@ -176,18 +181,22 @@
                     .attr('y', function (d) { return d.y; });
             });
 
-            // Store the simulation for cleanup
-            container.data('d3-simulation', simulation);
+            // Store the simulation for cleanup (on the DOM element)
+            containerEl._d3Simulation = simulation;
         },
 
         destroy: function (canvasId) {
-            var container = $('#' + canvasId);
-            var simulation = container.data('d3-simulation');
+            var containerEl = document.getElementById(canvasId);
+            if (!containerEl) return;
+            var simulation = containerEl._d3Simulation;
             if (simulation) {
                 simulation.stop();
-                container.removeData('d3-simulation');
+                delete containerEl._d3Simulation;
+            }
+            // Clear all children (SVG and any other content)
+            while (containerEl.firstChild) {
+                containerEl.removeChild(containerEl.firstChild);
             }
-            container.empty();
         }
 
     };


(causeway) 02/05: [CAUSEWAY-4074] rendering gives clues in case of missing/wrong data

Posted by jo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

joerg-rade pushed a commit to branch Extension-D3
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 135e32c84a86c5c199404d14a31b66aa56e2c9ed
Author: Joerg Rade <jo...@gmail.com>
AuthorDate: Thu Oct 15 09:13:27 2026 +0200

    [CAUSEWAY-4074] rendering gives clues in case of missing/wrong data
---
 extensions/vw/d3/wicket/D3_WICKET_UI_STATUS.md     |  4 +-
 .../d3/wkt/integration/components/D3Panel.css      | 53 ++++++++++++++++++
 .../d3/wkt/integration/components/D3Panel.java     | 11 ++--
 .../d3/wkt/integration/res/d3-integration.mjs      | 27 +++++++--
 .../extensions/d3/wkt/ui/components/D3Panel.java   |  7 ++-
 .../wkt/ui/components/D3PanelComponentFactory.java |  9 +--
 .../ui/components/D3PanelComponentFactoryTest.java | 64 +++++++++++++---------
 7 files changed, 130 insertions(+), 45 deletions(-)

diff --git a/extensions/vw/d3/wicket/D3_WICKET_UI_STATUS.md b/extensions/vw/d3/wicket/D3_WICKET_UI_STATUS.md
index f5859e034b5..ae6a30ed511 100644
--- a/extensions/vw/d3/wicket/D3_WICKET_UI_STATUS.md
+++ b/extensions/vw/d3/wicket/D3_WICKET_UI_STATUS.md
@@ -20,8 +20,8 @@ Implement the Wicket UI module so that a domain **property** annotated `@D3Graph
 | Research & API verification | **DONE** |
 | Code implementation | **DONE** — three main classes (`CausewayModuleExtD3WicketUi`, `D3PanelComponentFactory`, `D3Panel`) written, compiled, and API-verified |
 | Unit tests (`appliesTo` logic) | **PASS** — 6/6 tests pass (all `appliesTo` scenarios) |
-| Unit tests (panel construction) | **BLOCKED** — requires `wicket-tester` for full Wicket container simulation; `wicket-tester` dep added to `pom.xml` but panel-level tests (`D3PanelTest`) were removed pending rewrite using `WicketTester` |
-| Build / test validation | `mvn -pl extensions/vw/d3/wicket/ui -am test` → **BUILD SUCCESS** (6 tests, 0 failures, 0 errors) |
+| Unit tests (panel construction) | **PASS** — `createComponent_returnsD3Panel` rewritten with `WicketTester`; `WicketAppStup` extended to `WebApplication` with `WicketWebjars.install()` and `.mjs` resource guard; all 7 tests pass |
+| Build / test validation | `mvn -pl extensions/vw/d3/wicket/ui -am test -Dtest=D3PanelComponentFactoryTest` → **BUILD SUCCESS** (7 tests, 0 failures, 0 errors) |
 | Blockers | **None** |
 
 ---
diff --git a/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/components/D3Panel.css b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/components/D3Panel.css
new file mode 100644
index 00000000000..136a0d7710c
--- /dev/null
+++ b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/components/D3Panel.css
@@ -0,0 +1,53 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+/* Ensure the D3 panel container has a visible height */
+.d3GraphPanel {
+    min-height: 400px;
+    width: 100%;
+    overflow: hidden;
+}
+
+/* The SVG canvas should fill its container */
+.d3GraphCanvas {
+    min-height: 400px;
+    width: 100%;
+}
+
+/* D3 graph elements */
+.d3GraphCanvas svg {
+    display: block;
+}
+
+.d3-links line {
+    stroke: #999;
+    stroke-opacity: 0.6;
+}
+
+.d3-nodes circle {
+    stroke: #fff;
+    stroke-width: 1.5px;
+}
+
+.d3-labels text {
+    font-family: sans-serif;
+    font-size: 12px;
+    pointer-events: none;
+    fill: #333;
+}
diff --git a/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/components/D3Panel.java b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/components/D3Panel.java
index b4630547514..4bacaf13aa7 100644
--- a/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/components/D3Panel.java
+++ b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/components/D3Panel.java
@@ -18,15 +18,15 @@
  */
 package org.apache.causeway.extensions.d3.wkt.integration.components;
 
+import org.apache.wicket.markup.head.CssHeaderItem;
 import org.apache.wicket.markup.head.IHeaderResponse;
 import org.apache.wicket.markup.html.WebComponent;
 import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.request.resource.CssResourceReference;
 
-import org.apache.causeway.extensions.d3.applib.config.D3Config;
 import org.apache.causeway.extensions.d3.applib.config.D3Config;
 import org.apache.causeway.extensions.d3.wkt.integration.callback.NodeClickedCallback;
 import org.apache.causeway.extensions.d3.wkt.integration.res.D3IntegrationReference;
-import org.apache.causeway.extensions.d3.wkt.integration.res.D3IntegrationReference;
 import org.apache.causeway.extensions.d3.wkt.integration.res.D3Reference;
 import org.apache.causeway.viewer.wicket.ui.util.Wkt;
 
@@ -37,8 +37,7 @@
  * <p>
  * Contains an SVG canvas (a {@link WebComponent} with a markup id for the D3
  * visualization) and wires the D3 integration script that initialises the graph
- * using the configuration provided.
- * git status</p>
+ * using the configuration provided.</p>
  *
  * @since 2.0 {@index}
  */
@@ -86,6 +85,8 @@ public D3Panel withClickCallback(final @NonNull NodeClickedCallback clickCallbac
     public void renderHead(final IHeaderResponse response) {
         super.renderHead(response);
 
+        response.render(CssHeaderItem.forReference(new CssResourceReference(D3Panel.class, "D3Panel.css")));
+
         var configWithCallback = clickCallback != null
                 ? config.withClickCallbackUrl(clickCallback.getCallbackUrl().toString())
                 : config;
@@ -99,4 +100,4 @@ public CharSequence getCanvasId() {
         return config.getCanvasId();
     }
 
-}
\ No newline at end of file
+}
diff --git a/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/res/d3-integration.mjs b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/res/d3-integration.mjs
index 628dcc206a3..35578757b31 100644
--- a/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/res/d3-integration.mjs
+++ b/extensions/vw/d3/wicket/integration/src/main/java/org/apache/causeway/extensions/d3/wkt/integration/res/d3-integration.mjs
@@ -50,17 +50,36 @@
                 .find('svg');
             svg.attr('width', width).attr('height', height);
 
-            // Parse the graph data (expected as JSON)
+            // Parse the graph data (expected as JSON);
+            // if graphData is null, undefined, or empty, use an empty graph
             var nodes, links;
             try {
-                var data = JSON.parse(config.graphData);
-                nodes = data.nodes || [];
-                links = data.links || [];
+                if (config.graphData) {
+                    var data = JSON.parse(config.graphData);
+                    nodes = data.nodes || [];
+                    links = data.links || [];
+                } else {
+                    nodes = [];
+                    links = [];
+                }
             } catch (e) {
                 console.error('D3 graph data parse error:', e);
                 return;
             }
 
+            // If there's no data, render an empty graph placeholder
+            if (nodes.length === 0 && links.length === 0) {
+                svg.append('text')
+                    .attr('x', width / 2)
+                    .attr('y', height / 2)
+                    .attr('text-anchor', 'middle')
+                    .attr('font-family', 'sans-serif')
+                    .attr('font-size', '14px')
+                    .attr('fill', '#999')
+                    .text('No graph data available');
+                return;
+            }
+
             // Create the force simulation
             var simulation = d3.forceSimulation(nodes)
                 .force('link', d3.forceLink(links)
diff --git a/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3Panel.java b/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3Panel.java
index 31e5aff97e5..39e31556ef1 100644
--- a/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3Panel.java
+++ b/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3Panel.java
@@ -48,6 +48,8 @@ public class D3Panel extends org.apache.causeway.extensions.d3.wkt.integration.c
 
     private static final long serialVersionUID = 1L;
 
+    private static final String EMPTY_GRAPH = "{\"nodes\":[],\"links\":[]}";
+
     private final UiAttributeWkt attributeModel;
 
     public D3Panel(final String id, final UiAttributeWkt attributeModel) {
@@ -80,10 +82,13 @@ private static D3Config buildConfig(final UiAttributeWkt attributeModel) {
                 ? facet.configFor(domainClass)
                 : new D3Config();
 
-        // graph data is the annotated property's value (a JSON string)
+        // graph data is the annotated property's value (a JSON string);
+        // fall back to an empty graph when the value is null or blank
         var pojo = attributeModel.getObject().getPojo();
         if (pojo instanceof CharSequence cs && !cs.toString().isBlank()) {
             config = config.withGraphData(cs);
+        } else {
+            config = config.withGraphData(EMPTY_GRAPH);
         }
         return config;
     }
diff --git a/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3PanelComponentFactory.java b/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3PanelComponentFactory.java
index 9ff746d18a8..040289e7ef4 100644
--- a/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3PanelComponentFactory.java
+++ b/extensions/vw/d3/wicket/ui/src/main/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3PanelComponentFactory.java
@@ -50,14 +50,7 @@ public ApplicationAdvice appliesTo(final IModel<?> model) {
         if (!attributeModel.getMetaModel().containsFacet(D3GraphFacet.class)) {
             return ApplicationAdvice.DOES_NOT_APPLY;
         }
-        // graph data must be a non-blank CharSequence (a JSON string)
-        var value = attributeModel.getObject();
-        if (value != null
-                && value.getPojo() instanceof CharSequence cs
-                && !cs.toString().isBlank()) {
-            return ApplicationAdvice.APPLIES;
-        }
-        return ApplicationAdvice.DOES_NOT_APPLY;
+        return ApplicationAdvice.APPLIES;
     }
 
     @Override
diff --git a/extensions/vw/d3/wicket/ui/src/test/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3PanelComponentFactoryTest.java b/extensions/vw/d3/wicket/ui/src/test/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3PanelComponentFactoryTest.java
index 7ecc99dcee3..e0100e073b7 100644
--- a/extensions/vw/d3/wicket/ui/src/test/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3PanelComponentFactoryTest.java
+++ b/extensions/vw/d3/wicket/ui/src/test/java/org/apache/causeway/extensions/d3/wkt/ui/components/D3PanelComponentFactoryTest.java
@@ -20,18 +20,19 @@
 
 import java.util.Optional;
 
-import org.apache.wicket.Application;
 import org.apache.wicket.Page;
 import org.apache.wicket.RuntimeConfigurationType;
-import org.apache.wicket.Session;
 import org.apache.wicket.ThreadContext;
+import org.apache.wicket.markup.html.SecurePackageResourceGuard;
 import org.apache.wicket.model.IModel;
-import org.apache.wicket.request.Request;
-import org.apache.wicket.request.Response;
+import org.apache.wicket.protocol.http.WebApplication;
+import org.apache.wicket.util.tester.WicketTester;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import de.agilecoders.wicket.webjars.WicketWebjars;
+
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -77,7 +78,7 @@ private static UiAttributeWkt attributeModel(final boolean hasFacet) {
     }
 
     @Test
-    void applies_whenAttributeHasD3GraphFacetAndNonBlankJsonString() {
+    void applies_whenAttributeHasD3GraphFacet() {
         var attributeModel = attributeModel(true);
         when(attributeModel.getObject()).thenReturn(managedString("{\"nodes\":[],\"links\":[]}"));
 
@@ -100,36 +101,34 @@ void doesNotApply_whenMissingD3GraphFacet() {
     }
 
     @Test
-    void doesNotApply_whenPropertyValueIsBlank() {
+    void applies_whenPropertyValueIsBlank() {
         var attributeModel = attributeModel(true);
         when(attributeModel.getObject()).thenReturn(managedString("  "));
 
         var advice = factory.appliesTo(attributeModel);
-        assertThat(advice.applies()).isFalse();
+        assertThat(advice.applies()).isTrue();
     }
 
     @Test
-    void doesNotApply_whenPropertyValueIsNull() {
+    void applies_whenPropertyValueIsNull() {
         var attributeModel = attributeModel(true);
         when(attributeModel.getObject()).thenReturn(null);
 
         var advice = factory.appliesTo(attributeModel);
-        assertThat(advice.applies()).isFalse();
+        assertThat(advice.applies()).isTrue();
     }
 
     @Test
-    void doesNotApply_whenPropertyValueIsNotCharSequence() {
+    void applies_whenPropertyValueIsNotCharSequence() {
         var attributeModel = attributeModel(true);
         var spec = mmc.getSpecificationLoader().loadSpecification(Integer.class);
         when(attributeModel.getObject()).thenReturn(ManagedObject.adaptSingular(spec, 123));
 
         var advice = factory.appliesTo(attributeModel);
-        assertThat(advice.applies()).isFalse();
+        assertThat(advice.applies()).isTrue();
     }
 
-    // panel construction requires wicket-tester (see status doc)
-    //TODO add wicket-tester test dependency and re-enable
-    //@Test
+    @Test
     void createComponent_returnsD3Panel() {
         var attributeModel = attributeModel(true);
         when(attributeModel.getObject()).thenReturn(managedString("{\"nodes\":[],\"links\":[]}"));
@@ -146,15 +145,35 @@ void createComponent_returnsD3Panel() {
         when(d3GraphFacet.configFor(D3PanelComponentFactoryTest.class)).thenReturn(new D3Config());
         when(d3GraphFacet.getClickAction(D3PanelComponentFactoryTest.class)).thenReturn(Optional.empty());
 
-        var component = factory.createComponent("d3", attributeModel);
-        assertThat(component).isInstanceOf(D3Panel.class);
+        // WicketTester provides the full Wicket infrastructure (Application, Session, RequestCycle)
+        // needed to construct and render the D3Panel component
+        var tester = new WicketTester(new WicketAppStup());
+        try {
+            var component = factory.createComponent("d3", attributeModel);
+            tester.startComponentInPage(component);
+            assertThat(component).isInstanceOf(D3Panel.class);
+        } finally {
+            tester.destroy();
+        }
     }
 
-    /** minimal Wicket Application so that {@code new D3Panel(...)} works without a servlet container */
-    static class WicketAppStup extends Application {
+    /**
+     * Minimal Wicket {@link WebApplication} that enables Wicket infrastructure
+     * (Session, RequestCycle) without a servlet container, as required by
+     * {@link WicketTester} and the D3Panel component hierarchy.
+     */
+    static class WicketAppStup extends WebApplication {
         @Override
-        public String getApplicationKey() {
-            return "test";
+        protected void init() {
+            super.init();
+            // Required before any WebjarsJavaScriptResourceReference (e.g. D3Reference) can be used
+            WicketWebjars.install(this);
+            // Allows *.mjs, otherwise Wicket throws 'Access denied to (static) package resource'
+            var resourceGuard =
+                    (SecurePackageResourceGuard) getResourceSettings().getPackageResourceGuard();
+            resourceGuard.addPattern("+*.mjs");
+            // Allows *.css loaded via CssResourceReference
+            resourceGuard.addPattern("+*.css");
         }
 
         @Override
@@ -166,10 +185,5 @@ public RuntimeConfigurationType getConfigurationType() {
         public Class<? extends Page> getHomePage() {
             return null;
         }
-
-        @Override
-        public Session newSession(final Request request, final Response response) {
-            return null;
-        }
     }
 }


(causeway) 04/05: [CAUSEWAY-4074] doc updated

Posted by jo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

joerg-rade pushed a commit to branch Extension-D3
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 057d0ad5fb6acb2571e3c310ca4739260f565cc1
Author: Joerg Rade <jo...@gmail.com>
AuthorDate: Thu Oct 15 09:26:33 2026 +0200

    [CAUSEWAY-4074] doc updated
---
 extensions/vw/d3/wicket/D3_WICKET_UI_OVERVIEW.md | 82 ++++++++++++++++++++----
 1 file changed, 70 insertions(+), 12 deletions(-)

diff --git a/extensions/vw/d3/wicket/D3_WICKET_UI_OVERVIEW.md b/extensions/vw/d3/wicket/D3_WICKET_UI_OVERVIEW.md
index 3288ef07b63..28507ab5c96 100644
--- a/extensions/vw/d3/wicket/D3_WICKET_UI_OVERVIEW.md
+++ b/extensions/vw/d3/wicket/D3_WICKET_UI_OVERVIEW.md
@@ -224,7 +224,7 @@ sequenceDiagram
     Browser->>Browser: update UI
 ```
 
-### 4.3 Decision tree — `appliesTo`
+### 4.3 Decision tree — `appliesTo` (post-fix)
 
 ```mermaid
 flowchart TD
@@ -232,13 +232,8 @@ flowchart TD
     CheckType -- No --> NoApply["ApplicationAdvice.DOES_NOT_APPLY"]
     CheckType -- Yes --> CheckFacet{"containsFacet<br/>(D3GraphFacet.class)?"}
     CheckFacet -- No --> NoApply
-    CheckFacet -- Yes --> CheckNull{"value != null?"}
-    CheckNull -- No --> NoApply
-    CheckNull -- Yes --> CheckType2{"value.getPojo() instanceof<br/>CharSequence?"}
-    CheckType2 -- No --> NoApply
-    CheckType2 -- Yes --> CheckBlank{"!cs.toString().isBlank()?"}
-    CheckBlank -- No --> NoApply
-    CheckBlank -- Yes --> Apply["ApplicationAdvice.APPLIES"]
+    CheckFacet -- Yes --> Apply["ApplicationAdvice.APPLIES ✓"]
+    Apply --> Note["Panel is always rendered when facet<br/>is present; null/blank data is<br/>handled downstream in buildConfig()"]
 ```
 
 ### 4.4 Callback wiring detail
@@ -264,6 +259,67 @@ flowchart LR
     end
 ```
 
+### 4.5 Rendering chain — full flow with null-data fixes
+
+```mermaid
+flowchart TD
+    subgraph "1. Factory gating"
+        A["Entity page iterates attributes"] --> B{"D3PanelComponentFactory<br/>.appliesTo()"}
+        B --> B1{"instanceof UiAttributeWkt?"}
+        B1 -- No --> NO["DOES_NOT_APPLY"]
+        B1 -- Yes --> B2{"containsFacet<br/>(D3GraphFacet)?"}
+        B2 -- No --> NO
+        B2 -- Yes --> YES["APPLIES ★"]
+        YES --> Note1["No longer checks property<br/>value type, null, or blank"]
+    end
+
+    subgraph "2. Panel construction"
+        C["factory.createComponent()"] --> D["new D3Panel(id, attributeModel)"]
+        D --> E["buildConfig(attributeModel)"]
+        E --> E1["lookup D3GraphFacet"]
+        E1 --> E2["facet.configFor(domainClass)"]
+        E2 --> E3{"pojo instanceof CharSequence<br/>&& !blank?"}
+        E3 -- Yes --> E4["config.withGraphData(cs)"]
+        E3 -- No --> E5["config.withGraphData(EMPTY_GRAPH)<br/>{nodes:[], links:[]}"]
+        E4 --> F["Panel ready with valid graphData JSON"]
+        E5 --> F
+    end
+
+    subgraph "3. CSS rendering"
+        G["panel.renderHead()"] --> G1["CssHeaderItem.forReference<br/>(D3Panel.css) ★"]
+        G1 --> G2[".d3GraphPanel { min-height: 400px }<br/>.d3GraphCanvas { min-height: 400px }"]
+        G2 --> G3["SVG container has visible height"]
+    end
+
+    subgraph "4. JS init"
+        H["WicketStuff.D3.init(config)"] --> H1{"config.graphData truthy?"}
+        H1 -- No --> H2["nodes = [], links = [] ★"]
+        H1 -- Yes --> H3["JSON.parse(config.graphData)"]
+        H3 --> H4{"parse succeeded?"}
+        H4 -- No --> H5["console.error + return"]
+        H4 -- Yes --> H6["nodes = data.nodes || []<br/>links = data.links || []"]
+        H2 --> H7{"nodes.length === 0 &&<br/>links.length === 0?"}
+        H6 --> H7
+        H7 -- Yes --> H8["svg.append('text')<br/>'No graph data available' ★"]
+        H7 -- No --> H9["Build force simulation,<br/>render nodes + links + labels"]
+    end
+
+    NO -.->|"Panel not rendered"| END["Property shown as plain text"]
+    F --> G
+    G3 --> H
+    H8 --> END2["Visible panel with<br/>placeholder message"]
+    H9 --> END3["Interactive D3 graph"]
+
+    style YES fill:#9f9,stroke:#090
+    style E5 fill:#9f9,stroke:#090
+    style G1 fill:#9f9,stroke:#090
+    style H2 fill:#9f9,stroke:#090
+    style H8 fill:#ffc,stroke:#cc0
+    style END fill:#fcc,stroke:#c00
+    style END2 fill:#ffc,stroke:#cc0
+    style END3 fill:#9f9,stroke:#090
+```
+
 ---
 
 ## 5. Test Strategy
@@ -344,9 +400,11 @@ flowchart TD
    This reuses the standard Wicket viewer's action-execution pipeline (parameter
    validation, result handling, page update).
 
-5. **No separate CSS file (optional).**  
-   A small `D3Panel.css` was considered but not implemented. The integration
-   panel already contributes D3.js CSS via `D3Reference`.
+5. **`D3Panel.css` — dedicated styling.**  
+   A `D3Panel.css` resource provides explicit `min-height` and `width: 100%`
+   on the panel and SVG canvas containers, ensuring the graph area is visible
+   even when no data has been rendered. The integration panel's `renderHead()`
+   contributes it via `CssHeaderItem.forReference(new CssResourceReference(...))`.
 
 ---
 
@@ -356,7 +414,7 @@ flowchart TD
 |---|---|---|
 | **TYPE-level `@D3Graph`** | Unsupported | `@Target` includes `TYPE`, but metamodel factory only processes `PROPERTIES_AND_ACTIONS` |
 | **Advisor `graphData` merge** | Not merged | `configFor(...)` merges advisor width/height/drag/zoom/clickAction but not graph data |
-| **`D3Panel.css`** | Optional | Not yet created; would require a `CssReference` in `renderHead` |
+| **`D3Panel.css`** | Done | Created with `.d3GraphPanel`, `.d3GraphCanvas`, `.d3-links`, `.d3-nodes`, `.d3-labels` styles; contributed via `CssResourceReference` in `renderHead()` |
 | **Canvas cleanup** | Optional | Override `onRemove`/`onDetach` to call `WicketStuff.D3.destroy(canvasId)` |
 | **Antora docs** | Not started | User-facing page for the D3 viewer extension |
 | **`ui-test` module** | Not started | Long-term Wicket-level regression tests (like `viewers/wicket/ui-test`) |