🏠 back to Observable

Getting Brush & Zoom to work with Circles

I’m trying to understand Tristan Wietsma’s Brush & Zoom example and change it to draw circles rather than areas. I have created a version below with my attempt. The circles plot correct initially, but the brush / zoom have no effect on the main (Focus) plot.

I think its because I need to replace the area object with a circle object and then refer to these in the brushed and zoomed functions, but I can’t see how to create a circle object in a similar way to the area object.

Also, if anyone can explain how the brushed and zoomed functions work, that would be really helpful.

<!doctype html>

<!-- External JS libraries -->
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
<!-- Custom JS -->
<script>
    var svg = d3.select("svg")
    // Create 2 charts:
    // - Focus: the main chart
    // - Context: a slider that allows a narrower part of the main chart to be focussed on 
    const MARGIN = { LEFT: 40, RIGHT: 20, TOP: 20, BOTTOM: 110 }
    const WIDTH = +svg.attr("width") - MARGIN.LEFT - MARGIN.RIGHT
    const HEIGHT = 500 - MARGIN.TOP - MARGIN.BOTTOM

    const MARGIN2 = { LEFT: 40, RIGHT: 20, TOP: 430, BOTTOM: 30 }
    const HEIGHT2 = 500 - MARGIN2.TOP - MARGIN2.BOTTOM

    // Scales
    const x = d3.scaleLinear()
        .range([0, WIDTH])
        .domain(["0","99"])
    const y = d3.scaleLinear()
        .range([HEIGHT, 0])
        .domain([0, 30])
        
    const x2 = d3.scaleLinear()
        .range([0, WIDTH])
        .domain(["0","99"])
    const y2 = d3.scaleLinear()
        .range([HEIGHT2, 0])
        .domain([0, 30])

    // Define Axes
    var xAxis = d3.axisBottom(x),
        xAxis2 = d3.axisBottom(x2),
        yAxis = d3.axisLeft(y);

    var brush = d3.brushX()
        .extent([[0, 0], [WIDTH, HEIGHT2]])
        .on("brush end", brushed);

    var zoom = d3.zoom()
        .scaleExtent([1, Infinity])
        .translateExtent([[0, 0], [WIDTH, HEIGHT]])
        .extent([[0, 0], [WIDTH, HEIGHT]])
        .on("zoom", zoomed);

    // Original area objects - no longer needed as using circles
    // var area = d3.area()
    //     .curve(d3.curveMonotoneX)
    //     .x(function(d) { return x(d.date); })
    //     .y0(height)
    //     .y1(function(d) { return y(d.price); });

    // var area2 = d3.area()
    //     .curve(d3.curveMonotoneX)
    //     .x(function(d) { return x2(d.date); })
    //     .y0(height2)
    //     .y1(function(d) { return y2(d.price); });

    svg.append("defs").append("clipPath")
        .attr("id", "clip")
        .append("rect")
        .attr("width", WIDTH)
        .attr("height", HEIGHT);

    var focus = svg.append("g")
        .attr("class", "focus")
        .attr("transform", "translate(" + MARGIN.LEFT + "," + MARGIN.TOP + ")");

    var context = svg.append("g")
        .attr("class", "context")
        .attr("transform", "translate(" + MARGIN2.LEFT + "," + MARGIN2.TOP + ")");

    // Toptip
    var div = d3.select("body").append("div")	
        .attr("class", "tooltip")				
        .style("opacity", 0);
        
    var data=[
        {"xValue":10,"yValue":10,"count":58},
        {"xValue":97,"yValue":25,"count":99},
        {"xValue":23,"yValue":5,"count":5},
        {"xValue":49,"yValue":17,"count":29},
        {"xValue":67,"yValue":25,"count":35}
    ];

    plotChart(data)

    function plotChart(data) {
        console.log(data)

        const circleArea = d3.scaleLinear()
            .range([10*Math.PI, 750*Math.PI])
            .domain([d3.min(data, d => d.count), d3.max(data, d => d.count)])
        const circleArea2 = d3.scaleLinear()
            .range([1*Math.PI, 25*Math.PI])
            .domain([d3.min(data, d => d.count), d3.max(data, d => d.count)])
            
        // Add circles to main (Focus) Plot
        var circles = focus.selectAll("circle")
            .data(data)

        // ENTER new elements present in new data.
        circles.enter().append("circle")
            .attr("fill-opacity","0.9")
            .attr("cy", d => y(d.yValue))
            .attr("cx", d => x(d.xValue))
            .attr("r", d => Math.sqrt(circleArea(d.count) / Math.PI))
            .attr("fill", "Red")

        // Add axes
        focus.append("g")
            .attr("class", "axis axis--x")
            .attr("transform", "translate(0," + HEIGHT + ")")
            .call(xAxis);

        focus.append("g")
            .attr("class", "axis axis--y")
            .call(yAxis);

        // Add circles to slider (Context) Plot
        var circles2 = context.selectAll("circle")
            .data(data)

        // ENTER new elements present in new data.
        circles2.enter().append("circle")
            .attr("fill-opacity","0.9")
            .attr("cy", d => y2(d.yValue))
            .attr("cx", d => x2(d.xValue))
            .attr("r", d => Math.sqrt(circleArea2(d.count) / Math.PI))
            .attr("fill", d => "Red")

        // Add axes
        context.append("g")
            .attr("class", "axis axis--x")
            .attr("transform", "translate(0," + HEIGHT2 + ")")
            .call(xAxis2);

        context.append("g")
            .attr("class", "brush")
            .call(brush)
            .call(brush.move, x.range());

        svg.append("rect")
            .attr("class", "zoom")
            .attr("width", WIDTH)
            .attr("height", HEIGHT)
            .attr("transform", "translate(" + MARGIN.LEFT + "," + MARGIN.TOP + ")")
            .call(zoom);
    }

    // 
    function brushed() {
        if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
        var s = d3.event.selection || x2.range();
        x.domain(s.map(x2.invert, x2));
        //focus.select(".area").attr("d", area); - Need to replace this, version below not working
        focus.select(".circle").attr("cx", d => x(d.xValue));
        focus.select(".axis--x").call(xAxis);
        svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
            .scale(WIDTH / (s[1] - s[0]))
            .translate(-s[0], 0));
    }
    //
    function zoomed() {
        if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
        var t = d3.event.transform;
        x.domain(t.rescaleX(x2).domain());
        //focus.select(".area").attr("d", area); - Need to replace this, version below not working
        focus.select(".circle").attr("cx", d => x(d.xValue));
        focus.select(".axis--x").call(xAxis);
        context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
    }
</script>