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.