Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

denewey's avatar

D3 Bubble Chart code gives Uncaught TypeError for .attr()

I have a d3 bubble chart script which works fine embedded in a php view file. Unfortunately, the code in the site loads the php view files via jQuery files. So I am passing the data to the d3 script in the view file from a jQuery file. I can pass the data but I keep getting the "Uncaught TypeError: svg.append(...).data(...).selectAll(...).join(...).attr is not a function". This is a Laravel site.

After doing some research, I am suspecting the error may be a jQuery ".attr() is not a function" is an html or js error rather than a d3 error.

I took a look at the following solution recommended: https://stackoverflow.com/questions/22291761/d3-when-setattribute-works-but-attr-doesnt. This explains that the code in question was not addressing a d3 element but rather an html element. I suspect that is the issue here, but I didn't understand the solution proposed

Below is the jQuery code that pulls the data via Ajax and then updates the 'data' in the d3 script:

this.Load = function (data) {
                if (_xhr) {
                    _xhr.abort();
                    _xhr = null;
                }

                _this.SetLoading(true);
                _xhr = $.ajax({
                    url: $("meta[name='root']").attr("content") + '/app/heatmap/bubble',
                    type: 'POST',
                    headers: {
                        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
                    },
                    data: {
                        date_id: data.dateRange.date,
                        venue_id: data.venue,
                        floor_id: data.floor,
                        zone_id: data.zone
                    },
                    dataType: 'JSON',
                    async: true,
                    cache: false,
                    error: function (jqXHR, textStatus, errorThrown) {
                        _this.SetLoading(false);
                    },
                    success: function (response) {
                        _this.SetLoading(false);
                        _this.SetKey(data.dateRange.key);
                        _this.Update(response);
                    }
                });
            };

            this.Update = function (data) {
                if (_.isUndefined(data) || _.isNull(data)) {
                    data = {};
                }

                if (this.BubbleChart && data) {
                    this.BubbleChart.Update(displayChart(data));
                }
            };

And the below code is from the view blade file that contains the d3 script:

<div id="heatmap-bubble" class="block block-condensed no-margin {{ getDateRangeKey(getMainControlsSelections()->dateRange) }}"
             data-venue="{{ $mainControlsData->venue ? $mainControlsData->venue->id : '' }}"
        >
            <div class="app-heading">
                <div class="title">
                    <h2>{{ trans('app/heatmap.map.title') }}</h2>
                    <p>{{ trans('app/heatmap.map.subtitle') }}</p>
                </div>
                <div class="heading-elements">
                    <div class="{{ $mainControlsData->venue && $mainControlsData->venue->spotData && count($mainControlsData->venue->spotData->floors) > 1 && !$mainControlsData->floor ? '' : 'hidden' }}">
                        <label>
                            <span class="fa fa-object-ungroup"></span>
                        </label>
                        <select id="mapControls-floor"
                                data-noneSelectedText="{{ trans('app/main.mainControls.floor.noneSelectedText') }}"
                                {{ count($mainControlsData->venue->spotData->floors) > 1 ? '' : 'disabled' }}
                        >
                            @if($mainControlsData->venue && isset($mainControlsData->venue->spotData->floors))
                                @foreach($mainControlsData->venue->spotData->floors as $floor)
                                    <option value="{{ $floor->id }}"{{ ($mainControlsData->floor && $mainControlsData->floor->id === $floor->id) ? ' selected' : '' }}>{{ $floor->name }}</option>
                                @endforeach
                            @endif
                        </select>
                    </div>
                </div>
            </div>

            <div class="block-content">

                <div class="row">

                    <div id="bubble-chart">
                        <span id="bubbleData"
                              data-type="text"
                              data-visitors
                        ></span>
                       
                        <script src="https://d3js.org/d3.v6.js"></script>
                        <script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
                        <div id="bubble_viz"></div>
                        <script>

                            var displayChart = function(data) {

                            // set the dimensions and margins of the graph
                            const width = 700
                            const height = 460

                            // append the svg object to the body of the page
                            const svg = d3.select("#bubble_viz")
                                .append("svg")
                                .attr("width", width)
                                .attr("height", height)

                            var zones = [];
                            for (var i = 0, l = data.length; i < l; i++) {
                                var obj = data[i];
                                zones[i] = obj.zone;
                            }

                            // Color palette for continents?
                            const color = d3.scale.ordinal()
                                .domain([1, 2, 3, 4, 5, 6,7])
                                .range(["#98ccd9", "#79b9c9", "#549eb0", "#6f939b", "#217185", "#cfe3e8", "#044e61"]);

                            // Size scale for venues
                            const size = d3.scale.linear()
                                .domain([0, 100])
                                .range([15,100])  // circle will be between 15 and 100 px wide

                            // create a tooltip
                            const Tooltip = d3.select("#bubble_viz")
                                .append("div")
                                .style("opacity", 0)
                                .attr("class", "tooltip")
                                .style("background-color", "#ebeef2")
                                .style("border", "solid")
                                .style("border-width", "2px")
                                .style("border-radius", "5px")
                                .style("padding", "5px")

                            // Three function that change the tooltip when user hover / move / leave a cell
                            const mouseover = function(event, d) {
                                Tooltip
                                    .style("opacity", 1)
                            }
                            const mousemove = function(event, d) {
                                Tooltip
                                    .html('<b>' + d.zone + '</b>' + "<br>" + d.value + " visitors")
                                    .style("left", (event.x/2-300) + "px")
                                    .style("top", (event.y/2-200) + "px")
                            }
                            var mouseleave = function(event, d) {
                                Tooltip
                                    .style("opacity", 0)
                            }

                            // Initialize the circle: all located at the center of the svg area
                            var node = svg.append("g")
                                .selectAll("circle")
                                .data(data)
                                .join("circle")
                                .attr("class", "node")
                                .attr("r", d => size(d.value))
                                .attr("cx", width)
                                .attr("cy", height)
                                .style("fill", d => color(d.zone))
                                .style("fill-opacity", 0.8)
                                .attr("stroke", "black")
                                .style("stroke-width", 1)
                                .on("mouseover", mouseover) // What to do when hovered
                                .on("mousemove", mousemove)
                                .on("mouseleave", mouseleave)
                                .call(d3.drag() // call specific function when circle is dragged
                                    .on("start", dragstarted)
                                    .on("drag", dragged)
                                    .on("end", dragended));

                            let texts = svg.selectAll(null)
                                .data(data)
                                .enter()
                                .append('text')
                                .attr("text-anchor", "middle")
                                .text(d => d.zone)
                                .attr('color', 'black')
                                .attr('font-size', 15)

                            // Features of the forces applied to the nodes:
                            const simulation = d3.forceSimulation()
                                .force("center", d3.forceCenter().x(width / 2).y(height / 2)) // Attraction to the center of the svg area
                                .force("charge", d3.forceManyBody().strength(.1)) // Nodes are attracted one each other of value is > 0
                                .force("collide", d3.forceCollide().strength(.2).radius(function(d){ return (size(d.value)+3) }).iterations(1)) // Force that avoids circle overlapping

                            // Apply these forces to the nodes and update their positions.
                            // Once the force algorithm is happy with positions ('alpha' value is low enough), simulations will stop.
                            simulation
                                .nodes(data)
                                .on("tick", function(d){
                                    node
                                        .attr("cx", d => d.x)
                                        .attr("cy", d => d.y)
                                    texts
                                        .attr("cx", d => d.x)
                                        .attr("cy", d => d.y)
                                });

                            // What happens when a circle is dragged?
                            function dragstarted(event, d) {
                                if (!event.active) simulation.alphaTarget(.03).restart();
                                d.fx = d.x;
                                d.fy = d.y;
                            }
                            function dragged(event, d) {
                                d.fx = event.x;
                                d.fy = event.y;
                            }
                            function dragended(event, d) {
                                if (!event.active) simulation.alphaTarget(.03);
                                d.fx = null;
                                d.fy = null;
                            }
                            }
                        </script>

                    </div><!-- /#mapContainer -->

                    <div id="mapSlider">
                        <!--<div class="left"></div>-->
                        <div class="right">
                            <div class="slider-container"></div><!-- /.slider-container -->
                        </div>
                    </div>

                </div><!-- /.row -->

            </div><!-- /.block-content -->

        </div><!-- /#heatmap-bubble -->

The code fails at the assignment of the var node, with the error:

Uncaught TypeError: d3.select(...).append(...).selectAll(...).data(...).join(...).attr is not a function

There are actually 3 places above that where .attr() is set without an error, so I'm not understanding what is generating this error and how to fix it.

As an added note this d3 script worked perfectly embedded in the view code when the data for the chart was sent directly from the Controller to the view. However, that doesn't allow for the dynamic update that is afforded by the jQuery.

0 likes
3 replies
denewey's avatar

I found another rendition of the same code to define the 'node' variable, when substituted into my d3 code it bypassed the error on .attr(), but now errors on "Uncaught TypeError: d3.drag is not a function". This is the new piece of code:

var node = svg.append("g")
                                .selectAll("circle")
                                .data(data)
                                .enter()
                                .append("circle")
                                .attr("class", "node")
                                .attr("r", function(d){ return size(d.value)})
                                .attr("cx", width / 2)
                                .attr("cy", height / 2)
                                .style("fill", function(d){ return color(d.region)})
                                .style("fill-opacity", 0.8)
                                .attr("stroke", "black")
                                .style("stroke-width", 1)
                                .on("mouseover", mouseover) // What to do when hovered
                                .on("mousemove", mousemove)
                                .on("mouseleave", mouseleave)
                                .call(d3.drag() // call specific function when circle is dragged
                                    .on("start", dragstarted)
                                    .on("drag", dragged)
                                    .on("end", dragended));

I guess I'm not understanding why these Uncaught type errors are occurring. Why did the first one clear up with this new code snippet and why did the new one come up?

denewey's avatar

I have discovered that the site has the package rickshaw.js installed which includes d3 v3.5.17 in the package. I have been able to fix a lot of the code but the d3.forceSimulation piece comes from a newer version of d3 and I have been unable to find how to handle this for the current version. It would be best to upgrade to a more current version of d3, but I am not that well versed in NPM and Node and do not know how to update this package. Reading over the pull requests for the package in Git Hub it is apparent the support for it was dropped a few years ago. There is one fork that updated to d3 v4. How would I safely upgrade my package to this fork?

denewey's avatar
denewey
OP
Best Answer
Level 2

After further research, I found that there is a method to allow for more than one version of d3 to be active in a web page. That solution can be found here: https://chewett.co.uk/blog/2021/how-to-load-multiple-d3-versions-at-once/. This has solved my dilemma of not wanting to mess with the current version of d3 which is powering the legacy graphing on the site, but also needing the features of a more current version of d3 for my new renderings. using this solution I have gotten the bubble chart working and now can concerntrate on refining the display.

Please or to participate in this conversation.