Bit confused by this one.
Sample: HelloBubble.html
<!DOCTYPE html>
<html>
<head></head>
<body>
<script type="module">
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
const svg = BubbleChart([["Hello", 10], ["World", 20]]);
document.body.appendChild(svg);
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/bubble-chart
function BubbleChart(data, {
name = ([x]) => x, // alias for label
label = name, // given d in data, returns text to display on the bubble
value = ([, y]) => y, // given d in data, returns a quantitative size
group, // given d in data, returns a categorical value for color
title, // given d in data, returns text to show on hover
link, // given a node d, its link (if any)
linkTarget = "_blank", // the target attribute for links, if any
width = 640, // outer width, in pixels
height = width, // outer height, in pixels
padding = 3, // padding between circles
margin = 1, // default margins
marginTop = margin, // top margin, in pixels
marginRight = margin, // right margin, in pixels
marginBottom = margin, // bottom margin, in pixels
marginLeft = margin, // left margin, in pixels
groups, // array of group names (the domain of the color scale)
colors = d3.schemeTableau10, // an array of colors (for groups)
fill = "#ccc", // a static fill color, if no group channel is specified
fillOpacity = 0.7, // the fill opacity of the bubbles
stroke, // a static stroke around the bubbles
strokeWidth, // the stroke width around the bubbles, if any
strokeOpacity, // the stroke opacity around the bubbles, if any
} = {}) {
// Compute the values.
const D = d3.map(data, d => d);
const V = d3.map(data, value);
const G = group == null ? null : d3.map(data, group);
const I = d3.range(V.length).filter(i => V[i] > 0);
// Unique the groups.
if (G && groups === undefined) groups = I.map(i => G[i]);
groups = G && new d3.InternSet(groups);
// Construct scales.
const color = G && d3.scaleOrdinal(groups, colors);
// Compute labels and titles.
const L = label == null ? null : d3.map(data, label);
const T = title === undefined ? L : title == null ? null : d3.map(data, title);
// Compute layout: create a 1-deep hierarchy, and pack it.
const root = d3.pack()
.size([width - marginLeft - marginRight, height - marginTop - marginBottom])
.padding(padding)
(d3.hierarchy({children: I})
.sum(i => V[i]));
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-marginLeft, -marginTop, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.attr("fill", "currentColor")
.attr("font-size", 10)
.attr("font-family", "sans-serif")
.attr("text-anchor", "middle");
const leaf = svg.selectAll("a")
.data(root.leaves())
.join("a")
.attr("xlink:href", link == null ? null : (d, i) => link(D[d.data], i, data))
.attr("target", link == null ? null : linkTarget)
.attr("transform", d => `translate(${d.x},${d.y})`);
leaf.append("circle")
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.attr("stroke-opacity", strokeOpacity)
.attr("fill", G ? d => color(G[d.data]) : fill == null ? "none" : fill)
.attr("fill-opacity", fillOpacity)
.attr("r", d => d.r);
if (T) leaf.append("title")
.text(d => T[d.data]);
if (L) {
// A unique identifier for clip paths (to avoid conflicts).
const uid = `O-${Math.random().toString(16).slice(2)}`;
leaf.append("clipPath")
.attr("id", d => `${uid}-clip-${d.data}`)
.append("circle")
.attr("r", d => d.r);
leaf.append("text")
.attr("clip-path", d => `url(${new URL(`#${uid}-clip-${d.data}`, location)})`)
.selectAll("tspan")
.data(d => `${L[d.data]}`.split(/\n/g))
.join("tspan")
.attr("x", 0)
.attr("y", (d, i, D) => `${i - D.length / 2 + 0.85}em`)
.attr("fill-opacity", (d, i, D) => i === D.length - 1 ? 0.7 : null)
.text(d => d);
}
return Object.assign(svg.node(), {scales: {color}});
}
</script>
</body>
</html>
Now I put the exact same content inside a JSP, and I have problems, specifically with these couple of lines;
.attr("clip-path", d => `url(${new URL(`#${uid}-clip-${d.data}`, location)})`)
and
const uid = `O-${Math.random().toString(16).slice(2)}`;
Should say that this is a copy and paste JavaScript example from the docs of D3 (give or take...)
I'm just confused why this works fine in a basic .html file, but I get errors failing to compile the .war file when I'm using NetBeans IDE 8.2 (yes, I know, it's old... I like it) with a Java Maven Web App Project.
I'm not a JavaScript guru. But what is very apparent in those two lines I'm getting errors on is that this looks extremely similar to Java syntax. Feels as though JavaScript is perhaps getting a bit too big for it's boots here and is confusing the IDE.
In addition, I also have no idea what this code actually does at the minute either. Took me a few weeks to get a working example from D3 that I could actually get working with the docs being so poor. Hence I'm assuming the root cause is highly likely more modern JavaScript syntax (and modules) and an older NetBeans getting a bit confused.
I'd just prefer not to upgrade my local development environment because of one library (although I'm probably going to have to test this while working through this question on StackOverflow to rule that out...)
Update 1 - Errors in IDE when Running JSP in Web Browser
Error being caused by the two lines above.
In IDE on compile is - Technically not on compile, but has a big red line saying "Encoutered "URL" at line 1, column 7. Was expecting one of; {loads of different types of opening/closing brackets, about 20 of them}";
Then when I run the JSP I get this error appearing in the IDE console (the page fails to load in the web browser);
***lots of other stuff***
javax.el.MethodNotFoundException: Method not found: class java.lang.String.slice(java.lang.Long)
415:
416: if (L) {
417: // A unique identifier for clip paths (to avoid conflicts).
418: const uid = `O-${Math.random().toString(16).slice(2)}`;
419:
420: leaf.append("clipPath")
421: .attr("id", d => `${uid}-clip-${d.data}`)
***lots of other stuff***
Stacktrace:] with root cause
javax.el.MethodNotFoundException: Method not found: class java.lang.String.slice(java.lang.Long)
Then I delete that line causing the error and get a different error;
21-Apr-2023 22:05:24.783 SEVERE [http-nio-8084-exec-295] org.apache.catalina.core.ApplicationDispatcher.invoke Servlet.service() for servlet jsp threw exception
javax.el.ELException: The identifier [new] is not a valid Java identifier as required by section 1.19 of the EL specification (Identifier ::= Java language identifier). This check can be disabled by setting the system property org.apache.el.parser.SKIP_IDENTIFIER_CHECK to true.
***lots of other stuff***
423: .attr("r", d => d.r);
424:
425: leaf.append("text")
426: .attr("clip-path", d => `url(${new URL(`#${uid}-clip-${d.data}`, location)})`)
427: .selectAll("tspan")
428: .data(d => `${L[d.data]}`.split(/\n/g))
429: .join("tspan")
Stacktrace:] with root cause
javax.el.ELException: The identifier [new] is not a valid Java identifier as required by section 1.19 of the EL specification (Identifier ::= Java language identifier). This check can be disabled by setting the system property org.apache.el.parser.SKIP_IDENTIFIER_CHECK to true.
Update 2 - Trying with JavaScript in External .js File We may be onto something here. So what I have just tried... is splitting this out as suggested.
So we now have;
JSP;
<script src="/JavaScript/HelloBubble.js"></script>
<script type="module">
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
const svg = BubbleChart([["Hello", 10], ["World", 20]]);
document.body.appendChild(svg);
</script>
JavaScript;
Rest of code from earlier, excluding for ease
When I do this, this is the error I'm getting in the Web Browser Console when loading the page - Seems like it is a step in the right direction though;
caught TypeError: svg.selectAll(...).data(...).join is not a function
at BubbleChart (HelloBubble.js:73:14)
at HelloBubble**[.jsp]**:524:25
Added the [.jsp] bit above for ease of understanding.
This is feeling to me that the two bits of JavaScript (the data in the JSP, and the core function in the .js file) aren't quite talking to each other.
For completeness, this is now the complete contents of the HelloBubble.js file;
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/bubble-chart
function BubbleChart(data, {
name = ([x]) => x, // alias for label
label = name, // given d in data, returns text to display on the bubble
value = ([, y]) => y, // given d in data, returns a quantitative size
group, // given d in data, returns a categorical value for color
title, // given d in data, returns text to show on hover
link, // given a node d, its link (if any)
linkTarget = "_blank", // the target attribute for links, if any
width = 640, // outer width, in pixels
height = width, // outer height, in pixels
padding = 3, // padding between circles
margin = 1, // default margins
marginTop = margin, // top margin, in pixels
marginRight = margin, // right margin, in pixels
marginBottom = margin, // bottom margin, in pixels
marginLeft = margin, // left margin, in pixels
groups, // array of group names (the domain of the color scale)
colors = d3.schemeTableau10, // an array of colors (for groups)
fill = "#ccc", // a static fill color, if no group channel is specified
fillOpacity = 0.7, // the fill opacity of the bubbles
stroke, // a static stroke around the bubbles
strokeWidth, // the stroke width around the bubbles, if any
strokeOpacity, // the stroke opacity around the bubbles, if any
} = {}) {
// Compute the values.
const D = d3.map(data, d => d);
const V = d3.map(data, value);
const G = group == null ? null : d3.map(data, group);
const I = d3.range(V.length).filter(i => V[i] > 0);
// Unique the groups.
if (G && groups === undefined)
groups = I.map(i => G[i]);
groups = G && new d3.InternSet(groups);
// Construct scales.
const color = G && d3.scaleOrdinal(groups, colors);
// Compute labels and titles.
const L = label == null ? null : d3.map(data, label);
const T = title === undefined ? L : title == null ? null : d3.map(data, title);
// Compute layout: create a 1-deep hierarchy, and pack it.
const root = d3.pack()
.size([width - marginLeft - marginRight, height - marginTop - marginBottom])
.padding(padding)
(d3.hierarchy({children: I})
.sum(i => V[i]));
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-marginLeft, -marginTop, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.attr("fill", "currentColor")
.attr("font-size", 10)
.attr("font-family", "sans-serif")
.attr("text-anchor", "middle");
const leaf = svg.selectAll("a")
.data(root.leaves())
.join("a")
.attr("xlink:href", link == null ? null : (d, i) => link(D[d.data], i, data))
.attr("target", link == null ? null : linkTarget)
.attr("transform", d => `translate(${d.x},${d.y})`);
leaf.append("circle")
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.attr("stroke-opacity", strokeOpacity)
.attr("fill", G ? d => color(G[d.data]) : fill == null ? "none" : fill)
.attr("fill-opacity", fillOpacity)
.attr("r", d => d.r);
if (T)
leaf.append("title")
.text(d => T[d.data]);
if (L) {
// A unique identifier for clip paths (to avoid conflicts).
leaf.append("clipPath")
.attr("id", d => `${uid}-clip-${d.data}`)
.append("circle")
.attr("r", d => d.r);
leaf.append("text")
.attr("clip-path", d => `url(${new URL(`#${uid}-clip-${d.data}`, location)})`)
.selectAll("tspan")
.data(d => `${L[d.data]}`.split(/\n/g))
.join("tspan")
.attr("x", 0)
.attr("y", (d, i, D) => `${i - D.length / 2 + 0.85}em`)
.attr("fill-opacity", (d, i, D) => i === D.length - 1 ? 0.7 : null)
.text(d => d);
}
return Object.assign(svg.node(), {scales: {color}});
}
I was able to make your HTML page work as JSP. For this it was necessary to replace each and every instance of ${
in your page with \${
(10 times in total). Maybe in your test you forgot one of them.
The converted template is
<!DOCTYPE html>
<html>
<head></head>
<body>
<script type="module">
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
const svg = BubbleChart([["Hello", 10], ["World", 20]]);
document.body.appendChild(svg);
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/bubble-chart
function BubbleChart(data, {
name = ([x]) => x, // alias for label
label = name, // given d in data, returns text to display on the bubble
value = ([, y]) => y, // given d in data, returns a quantitative size
group, // given d in data, returns a categorical value for color
title, // given d in data, returns text to show on hover
link, // given a node d, its link (if any)
linkTarget = "_blank", // the target attribute for links, if any
width = 640, // outer width, in pixels
height = width, // outer height, in pixels
padding = 3, // padding between circles
margin = 1, // default margins
marginTop = margin, // top margin, in pixels
marginRight = margin, // right margin, in pixels
marginBottom = margin, // bottom margin, in pixels
marginLeft = margin, // left margin, in pixels
groups, // array of group names (the domain of the color scale)
colors = d3.schemeTableau10, // an array of colors (for groups)
fill = "#ccc", // a static fill color, if no group channel is specified
fillOpacity = 0.7, // the fill opacity of the bubbles
stroke, // a static stroke around the bubbles
strokeWidth, // the stroke width around the bubbles, if any
strokeOpacity, // the stroke opacity around the bubbles, if any
} = {}) {
// Compute the values.
const D = d3.map(data, d => d);
const V = d3.map(data, value);
const G = group == null ? null : d3.map(data, group);
const I = d3.range(V.length).filter(i => V[i] > 0);
// Unique the groups.
if (G && groups === undefined) groups = I.map(i => G[i]);
groups = G && new d3.InternSet(groups);
// Construct scales.
const color = G && d3.scaleOrdinal(groups, colors);
// Compute labels and titles.
const L = label == null ? null : d3.map(data, label);
const T = title === undefined ? L : title == null ? null : d3.map(data, title);
// Compute layout: create a 1-deep hierarchy, and pack it.
const root = d3.pack()
.size([width - marginLeft - marginRight, height - marginTop - marginBottom])
.padding(padding)
(d3.hierarchy({children: I})
.sum(i => V[i]));
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-marginLeft, -marginTop, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.attr("fill", "currentColor")
.attr("font-size", 10)
.attr("font-family", "sans-serif")
.attr("text-anchor", "middle");
const leaf = svg.selectAll("a")
.data(root.leaves())
.join("a")
.attr("xlink:href", link == null ? null : (d, i) => link(D[d.data], i, data))
.attr("target", link == null ? null : linkTarget)
.attr("transform", d => `translate(\${d.x},\${d.y})`);
leaf.append("circle")
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.attr("stroke-opacity", strokeOpacity)
.attr("fill", G ? d => color(G[d.data]) : fill == null ? "none" : fill)
.attr("fill-opacity", fillOpacity)
.attr("r", d => d.r);
if (T) leaf.append("title")
.text(d => T[d.data]);
if (L) {
// A unique identifier for clip paths (to avoid conflicts).
const uid = `O-\${Math.random().toString(16).slice(2)}`;
leaf.append("clipPath")
.attr("id", d => `\${uid}-clip-\${d.data}`)
.append("circle")
.attr("r", d => d.r);
leaf.append("text")
.attr("clip-path", d => `url(\${new URL(`#\${uid}-clip-\${d.data}`, location)})`)
.selectAll("tspan")
.data(d => `\${L[d.data]}`.split(/\n/g))
.join("tspan")
.attr("x", 0)
.attr("y", (d, i, D) => `\${i - D.length / 2 + 0.85}em`)
.attr("fill-opacity", (d, i, D) => i === D.length - 1 ? 0.7 : null)
.text(d => d);
}
return Object.assign(svg.node(), {scales: {color}});
}
</script>
</body>
</html>
I tested this locally with Tomcat 10.1.8 by adding this page to the example webapp and then entering the URL in a webbrowser.