Search code examples
javascriptjavajspnetbeansnetbeans-8

JSP Struggling with JavaScript Module Syntax


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}});
}

Solution

  • 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.