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}});
}
Michael Cropper
  • 872
  • 1
  • 10
  • 28
  • what errors are you getting? – premek.v Apr 21 '23 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) – Michael Cropper Apr 21 '23 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: `` – premek.v Apr 21 '23 at 20:59
  • That is a very good simplistic idea @premek.v that strangely never entered my mind. Let me give that a go now, that is highly likely to solve the problem. That being said... It actually isn't going to solve the full issue since I need to dynamically insert the data into the JavaScript that is required for the chart to load (i.e. from the DB). But I'll give the external stuff a go now out of interest to see if this static example works with that. – Michael Cropper Apr 21 '23 at 21:10
  • @premek.v I've just added Update 2 in the original question. Think you might be on to something there. I'm just not familiar with JavaScript Modules (first I'd actually heard of them when playing with this!). So I've got a bit of learning to do on these things. Got a different error, but certainly feels as though you may be along the right lines in your thinking. What do you think the next thing to try is? – Michael Cropper Apr 21 '23 at 21:23
  • To add, the above Update 2 has resulted in the IDE Console stopping throwing the errors I mentioned. Hence why it feels like this is in the right direction of travel. – Michael Cropper Apr 21 '23 at 21:26
  • Reading around, feels like this may be something ES6 related that I don't quite understand yet. – Michael Cropper Apr 21 '23 at 21:42
  • @kingdaro Based on your answer in https://stackoverflow.com/questions/49338193/how-to-use-code-from-script-with-type-module - I feel like you probably know the answer to this challenge? – Michael Cropper Apr 21 '23 at 22:42
  • 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... – andrewJames Apr 22 '23 at 00:59
  • 1
    According to https://stackoverflow.com/questions/8271033/how-to-escape-el-dollar-signs 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. – Thomas Kläger Apr 22 '23 at 06:48
  • Good suggestion @ThomasKläger with the \$ I've tried that, but no luck unfortunately. It seems to try to render but doesn't work properly. At least from the testing I've tried. – Michael Cropper Apr 24 '23 at 21:31
  • @andrewJames You are most likely correct here. I'm struggling to actually implement this though. The JavaScript code is from the D3 JavaScript library which has awful documentation, extremely cryptic naming conventions and no comments in their code snippets so even with a lot of playing trying to convert their half baked code snippets from JavaScript Template Literals to Strings, I can't get the thing to work when I'm trying to convert this. It's all a best guest. – Michael Cropper Apr 24 '23 at 21:49
  • Probably too late, but a suggestion: If you try any suggested fixes, then maybe you can edit your question to show what you tried and what happened when you tried it. Maybe you got a new, different error. That could be a sign of progress. Maybe next time... But I guess you have a different way forward now. Good luck! – andrewJames Apr 24 '23 at 22:39

1 Answers1

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.

Thomas Kläger
  • 17,754
  • 3
  • 23
  • 34
  • 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). – Michael Cropper Apr 27 '23 at 19:47
  • 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. – Michael Cropper Apr 27 '23 at 19:48
  • 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. – Michael Cropper Apr 27 '23 at 19:49