Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Leaflet overlaid with D3 Chart - Need chart to remain in one place

Tags:

I have a leaflet map with circle markers and a radial bar chart. I would like:

  1. the circle markers to move with the underlying map (so that they remain true to their real world position) but
  2. the radial chart to remain constant within the window / container when the map is moved

The circle markers move fine, but the radial chart is moving with the map which I don't want.

I have placed the circle markers within central_map_svg and the radial chart within chart_svg. Both are these are children of leaflet_svg which is I think is where the issue arises. However, if they don't have the same parent, then they appear separately.

I have included simplified reproducible code below.

enter image description here

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="https://d19vzq90twjlae.cloudfront.net/leaflet-0.7/leaflet.css" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
    <!-- Load d3.js -->
    <script src="https://d3js.org/d3.v5.js"></script>
    <!-- Function for radial charts -->
    <script src="https://cdn.jsdelivr.net/gh/holtzy/D3-graph-gallery@master/LIB/d3-scale-radial.js"></script>
    <!-- Leaflet -->
    <script src="https://d19vzq90twjlae.cloudfront.net/leaflet-0.7/leaflet.js"></script>

</head>
<body>
    <div id="map" style="width: 800px; height: 800px"></div>
    <script type="text/javascript">

        // set the dimensions and margins of the graph
        var size = 800;
        var margin = { top: 100, right: 0, bottom: 0, left: 0 },
            width = size - margin.left - margin.right,
            height = size - margin.top - margin.bottom,
            innerRadius = 240,
            outerRadius = Math.min(width, height) / 2;
        var mapCenter = new L.LatLng(52.482672, -1.897517);
        var places = [
            {
                "id": 1,
                "value": 15,
                "latitude": 52.481,
                "longitude": -1.899
            },
            {
                "id": 2,
                "value": 50,
                "latitude": 52.486,
                "longitude": -1.897
            },
            {
                "id": 3,
                "value": 36,
                "latitude": 52.477,
                "longitude": -1.902
            },
            {
                "id": 4,
                "value": 65,
                "latitude": 52.486,
                "longitude": -1.894
            }]

        var map = L.map('map').setView(mapCenter, 15);
        mapLink =
            '<a href="http://openstreetmap.org">OpenStreetMap</a>';
        L.tileLayer(
            'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '&copy; ' + mapLink + ' Contributors',
            maxZoom: 18
        }).addTo(map);

        // Disable mouse zoom as this causes drift
        map.scrollWheelZoom.disable();
        // Initialize the SVG layer
        map._initPathRoot();


        /* We simply pick up the SVG from the map object */
        var leaflet_svg = d3.select("#map").select("svg"),
            central_map_svg = leaflet_svg.append("g"),
            chart_svg = leaflet_svg.append("g")
                .attr("transform", "translate(" + (width / 2 + margin.left) + "," + (height / 2 + margin.top) + ")");

        function plot(data) {
            /* Add a LatLng object to each item in the dataset */
            data.forEach(function (d) {
                d.LatLng = new L.LatLng(d.latitude, d.longitude)
            })

            // X scale
            var x = d3.scaleBand()
                .range([0, 2 * Math.PI])
                .domain(data.map(function (d) { return d.id; }));

            // Y scale
            var y = d3.scaleRadial()
                .range([innerRadius, outerRadius])
                .domain([0, 68]); 

            // Add the bars
            chart_svg.append("g")
                .selectAll("path")
                .data(data)
                .enter()
                .append("path")
                .attr("d", d3.arc()
                    .innerRadius(y(0))
                    .outerRadius(function (d) { return y(d.value); })
                    .startAngle(function (d) { return x(d.id); })
                    .endAngle(function (d) { return x(d.id) + x.bandwidth(); })
                    .padAngle(0.02)
                    .padRadius(innerRadius))

            // Add the circles
            var feature = central_map_svg
                .selectAll("circle")
                .data(data)
                .enter().append("circle")
                .attr("r", 15)

            // Ensure circles correctly positoned after map zoom / update
            map.on("viewreset", update);
            update();

            function update() {
                feature.attr("transform",
                    function (d) {
                        return "translate(" +
                            map.latLngToLayerPoint(d.LatLng).x + "," +
                            map.latLngToLayerPoint(d.LatLng).y + ")";
                    }
                );
            }
        }
        plot(places)
    </script>
</body>
like image 283
Chris Avatar asked May 28 '20 13:05

Chris


1 Answers

A simple solution would be to stack a container on top of your map to accommodate your chart.

First, change your HTML to

<div id='scene'>
    <div id="map"></div>
    <div id='chart'></div>
</div>

Then add styles to display #chart on top of #map

#scene {width: 800px; height: 800px; position: relative;}
#map {width: 100%; height: 100%; z-index: 1;}
#chart {width: 100%; height: 100%; position: absolute; z-index: 2; top:0; left: 0; pointer-events: none;}
#chart svg {width: 100%; height: 100%;}

If you want to keep some parts of your chart interactive, add a rule to re-enable pointer events on those elements. For example:

#chart path {pointer-events: auto;}

Finally, point chart_svg to the correct element:

chart_svg = d3.select("#chart").append("svg").append("g")

And a demo

// set the dimensions and margins of the graph
var size = 400;
var margin = { top: 50, right: 0, bottom: 0, left: 0 },
    width = size - margin.left - margin.right,
    height = size - margin.top - margin.bottom,
    innerRadius = 120,
    outerRadius = Math.min(width, height) / 2;
var mapCenter = new L.LatLng(52.482672, -1.897517);
var places = [
    {
        "id": 1,
        "value": 15,
        "latitude": 52.481,
        "longitude": -1.899
    },
    {
        "id": 2,
        "value": 50,
        "latitude": 52.486,
        "longitude": -1.897
    },
    {
        "id": 3,
        "value": 36,
        "latitude": 52.477,
        "longitude": -1.902
    },
    {
        "id": 4,
        "value": 65,
        "latitude": 52.486,
        "longitude": -1.894
    }]

var map = L.map('map').setView(mapCenter, 15);
mapLink =
    '<a href="http://openstreetmap.org">OpenStreetMap</a>';
L.tileLayer(
    'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    attribution: '&copy; ' + mapLink + ' Contributors',
    maxZoom: 18
}).addTo(map);

// Disable mouse zoom as this causes drift
map.scrollWheelZoom.disable();
// Initialize the SVG layer
map._initPathRoot();


/* We simply pick up the SVG from the map object */
var leaflet_svg = d3.select("#map").select("svg"),
    central_map_svg = leaflet_svg.append("g"),
    chart_svg = d3.select("#chart").append("svg").append("g")
        .attr("transform", "translate(" + (width / 2 + margin.left) + "," + (height / 2 + margin.top) + ")");

function plot(data) {
    /* Add a LatLng object to each item in the dataset */
    data.forEach(function (d) {
        d.LatLng = new L.LatLng(d.latitude, d.longitude)
    })

    // X scale
    var x = d3.scaleBand()
        .range([0, 2 * Math.PI])
        .domain(data.map(function (d) { return d.id; }));

    // Y scale
    var y = d3.scaleRadial()
        .range([innerRadius, outerRadius])
        .domain([0, 68]);

    // Add the bars
    chart_svg.append("g")
        .selectAll("path")
        .data(data)
        .enter()
        .append("path")
        .attr("d", d3.arc()
            .innerRadius(y(0))
            .outerRadius(function (d) { return y(d.value); })
            .startAngle(function (d) { return x(d.id); })
            .endAngle(function (d) { return x(d.id) + x.bandwidth(); })
            .padAngle(0.02)
            .padRadius(innerRadius))
            
    chart_svg
        .selectAll("path")
        .on("mouseover", function() {
            d3.select(this).style("fill", "red");
        })
        .on("mouseout", function() {
            d3.select(this).style("fill", "black");
        })
        .on("touchend", function() {
            var el = d3.select(this);
            el.style("fill", el.style("fill") === "red" ? "black" : "red");
        })
    ;
    // Add the circles
    var feature = central_map_svg
        .selectAll("circle")
        .data(data)
        .enter().append("circle")
        .attr("r", 15)

    // Ensure circles correctly positoned after map zoom / update
    map.on("viewreset", update);
    update();

    function update() {
        feature.attr("transform",
            function (d) {
                return "translate(" +
                    map.latLngToLayerPoint(d.LatLng).x + "," +
                    map.latLngToLayerPoint(d.LatLng).y + ")";
            }
        );
    }
}
plot(places)
#scene {width: 400px; height: 400px; position: relative;}
#map {width: 100%; height: 100%; z-index: 1;}
#chart {width: 100%; height: 100%; position: absolute; z-index: 2; top:0; left: 0;  pointer-events: none;}
#chart svg {width: 100%; height: 100%;}
#chart path {pointer-events: auto;}
<link rel="stylesheet" href="https://d19vzq90twjlae.cloudfront.net/leaflet-0.7/leaflet.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v5.js"></script>
<!-- Function for radial charts -->
<script src="https://cdn.jsdelivr.net/gh/holtzy/D3-graph-gallery@master/LIB/d3-scale-radial.js"></script>
<!-- Leaflet -->
<script src="https://d19vzq90twjlae.cloudfront.net/leaflet-0.7/leaflet.js"></script>


<div id='scene'>
    <div id="map"></div>
    <div id='chart'></div>
</div>
like image 151
nikoshr Avatar answered Oct 02 '22 15:10

nikoshr