1

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;

  • HelloBubble.jsp
  • /JavaScript/HelloBubble.js

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}});
}
13
  • what errors are you getting? Commented Apr 21, 2023 at 20:57
  • On those two lines, I'll update question, they are IDE errors that are failing to compile the .war because it's treating those lines as Java rather than JavaScript (I think) Commented Apr 21, 2023 at 20:58
  • 1
    you can try to put the script in a separate .js file instead of the jsp directly and link it using: <script type="module" src="script.js"></script> Commented Apr 21, 2023 at 20:59
  • 1
    JavaScript interprets ${...} as a placeholder inside a JS template literal. JSP interprets ${...} as a JSP EL expression. I think that is why your template literal works in a plain HTML page (no JSP), but why it gets hijacked in a JSP page - and throws a Java error. Try replacing your JavaScript template literals with strings - for example, try this: const uid2 = "O-" + Math.random().toString(16).slice(2);. See what happens... Commented Apr 22, 2023 at 0:59
  • 1
    According to stackoverflow.com/questions/8271033/… you can probably leave the JavaScript code in the JSP template if in the JavaScript code you replace ${ with \${ which escapes it for the JSP processor. Commented Apr 22, 2023 at 6:48

1 Answer 1

3

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.

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks @Thomas Kläger much appreciated for you taking the time to get this working and showing a full working example for this. You've provided significantly more help than I have from the official support places from D3. I've compared what you have done (and I've run it and got it working now in a JSP).
Looking through the comparison with what I did before VS this working example, I think I missed some of the escapes. I couldn't quite figure out which ones I needed to escape and which I didn't. With all different " ` ' in the JavaScript it wasn't clear which ones were getting in the way and which ones weren't with the error messages I was getting in the IDE Console (when loading the JSP in the browser) and others just weren't being highlighted as an issue (likely because NetBeans 8 is probably before JavaScript Literals were 'invented' if you will.
Summary seems to be though: Just replace ${ with \${ absolutely everywhere in the JavaScript if you have the JavaScript inside a JSP page. Seems so simple in the end.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.