d3/d3-brush

Custom handle causing confusing error

Cowlephant opened this issue · 6 comments

I have created a custom handle for my brush, following some of the V4 examples, but when I click on the handle to manipulate it when the selection has the same values, it gives an error.

EXCEPTION: Cannot read property '0' of null

If I go to the source, seems to be caused in this area of the d3 code, at the w0 variable area.

    if (type === "overlay") {
      state.selection = selection$$1 = [
        [w0 = dim === Y ? W : point0[0], n0 = dim === X ? N : point0[1]],
        [e0 = dim === Y ? E : w0, s0 = dim === X ? S : n0]
      ];
    } else {
      w0 = selection$$1[0][0];
      n0 = selection$$1[0][1];
      e0 = selection$$1[1][0];
      s0 = selection$$1[1][1];
    }

If I disable pointerevents for my custom handle, everything works fine with no error.

Maybe it's just an issue with the way I am trying to accomodate single date values for my selection, and to not have the selection area disappear.

Creating the brush items

this.brush = d3.brushX()
            .extent([[0, 0], [this.rootWidth, this.rootHeight / 2]])
            .on("start brush", this.brushmoved);

        this.brushGroup = this.root.append("g")
            .attr("transform", "translate(0," + this.rootHeight / 2 + ")")
            .attr("class", "brush")
            .call(this.brush);

        this.handle = this.brushGroup.selectAll(".handle-custom")
            .data([{ type: "w" }, { type: "e" }])
            .enter().append("path")
            .attr("class", function (d: any) {
                return "handle-custom " + d.type;
            })
            .attr("display", null)
            .attr("d", this.TREFOIL_HALF);

Updating my brush, on resizes or otherwise.

updateBrush = (ctx: DateSelectorComponent) => {
        this.brushGroup.call(this.brush.extent([[0, 0], [this.rootWidth, this.rootHeight / 2]]));
        this.brushGroup.call(this.brush.move,
            [this.selectedFromDate,
            this.selectedThroughDate].map(this.xScale));
    };

Updating my handles

    updateHandle = (ctx: DateSelectorComponent) => {
        this.handle
            .attr("transform", function (d, i) {
                let from = ctx.xScale(ctx.selectedFromDate);
                let through = ctx.xScale(ctx.selectedThroughDate);
                return "translate(" + (d.type === "w" ? from : through) + "," + ctx.rootHeight / 4 + ")" +
                    (d.type === "w" ? "scale(1.25, 1.25)" : "scale(-1.25, 1.25)");
            });
    };

brushmoved function

    brushmoved = () => {
        // make sure date range is updated and handles are moved to match the selection extent
        if (!d3.event.sourceEvent || d3.event.sourceEvent.type === "brush") return;
        this.updateDates(this);
        this.updateHandle(this);
    };

Everything works fine except clicking on my custom handle area again.
rangeselector

Did you look at this example?

https://bl.ocks.org/mbostock/4349545

I have yes. I'll pour through it again and see if the answer is in there. Probably an oversight on my part.

I think the major issue is of a zero-width selection, and my forcing the handles to be visible during that selection.

What is the best way to allow for a single width selection? That gif above represents 18 months. If I click on a value, I want to use that day that I clicked on as my only value. I don't want to set the extent to the start of the day and the end of the day, ideally... because of the visual representation I'm trying to go for, keeping the handles tight together when on a single date. All of the brush examples I've seen either snap to a wider date range, or just disappear the brush with that single selection.

Same thing happens in the example you provided, if I comment out the lines that hide the brush when there is a null selection. I account for the null selection and make sure I have some sort of selection, but not sure how to prevent this error from happening.

handle.attr("display", "none");
circle.classed("active", false);

Is it just complete bad practice to attempt this? I like the style of the handles directly together, especially for my example which completes the trefoil.

I have the same issue. Is it possible to have an empty selection? Since resize controls are required in my case and I cannot apply the same behavior as @mbostock in his great example .

Sorry for the delay. brush.move was too aggressive in converting a degenerate (trivial) selection to the null selection. This should only happen on interaction. Here is an example of preventing the brush from being cleared:

https://observablehq.com/@d3/empty-brush-selection