Skip to content
This repository has been archived by the owner on Sep 7, 2020. It is now read-only.

Support for Brush #4

Closed
juanpr2 opened this issue Sep 16, 2015 · 24 comments
Closed

Support for Brush #4

juanpr2 opened this issue Sep 16, 2015 · 24 comments
Assignees

Comments

@juanpr2
Copy link

juanpr2 commented Sep 16, 2015

I'm trying to implement a Brush in a chart, but I get an undefined is not a valid argument for 'in' (evaluating 'name in object') error from the ReactEventListener.
I believe the cause is the brush event from d3.
Could this be implemented in the react-faux-dom?

@Olical
Copy link
Owner

Olical commented Sep 17, 2015

Can you post any code examples? It'd be great to see how this works. I haven't explored the full D3 API yet so some things are still new to me.

One thing I found was that you can't use d3.mouse because that assumes it was passed a native event object. You have to use d3.event and then extract the coordinates out of the React synthetic event object. So your issue may be something similar.

I'll see what I can do with some more context, but there will be things that just don't work because of the nature of React. Well, we can get anything working but it'd probably require changes to the actual D3 source, some things can't be faked I think.

As a guess, I'd say brush uses d3.mouse which doesn't know that it needs to call e.nativeEvent. There may be a nice way around this, I may just have to improve {add,remove}EventListener so it behaves exactly like the real thing.

@Olical Olical self-assigned this Sep 17, 2015
@juanpr2
Copy link
Author

juanpr2 commented Sep 17, 2015

Sure,
The code is adapted from this example:
[https://gist.githubusercontent.com/jisaacks/5677681/raw/96759f97a0589336373d97eb287de3ea4b6659bf/main.js]

import d3 from 'd3';
import React, { Component } from 'react';
import ReactFauxDOM from 'react-faux-dom';

export default class Graph extends Component {
  render() {

    const { data } = this.props

      let width = 750,
        height = 100,
        margin = {
          top: 20,
          right: 20,
          bottom: 20,
          left: 50
        };

      const node = ReactFauxDOM.createElement('svg')
      const svg = d3.select(node)
      .attr('width', width + margin.left + margin.right)
      .attr('height', height + margin.top + margin.bottom)

          var x = d3.time.scale()
              .range( [0, width] )
              .domain( [data[0].date, data[data.length-1].date] );

          var y = d3.scale.linear()
              .range( [height, 0] )
              .domain( [0, 20] );

          var line = d3.svg.line()
              .interpolate("basis")
              .x(function(d){ return x(d.date); })
              .y(function(d){ return y(d.data); });

          var area = d3.svg.area()
              .interpolate("basis")
              .x(function(d){ return x(d.date); })
              .y1(function(d){ return y(d.data); })
              .y0(function(d){ return y(0); });

          var brush = d3.svg.brush().x(x);

          var focus = svg.append("g");

          focus.append("path")
              .attr("class", "area")
              .style({"fill": "#ccc"})
              .datum(data)
              .attr("d", area);

          focus.append("path")
              .attr("class", "line")
              .style({
                  "fill": "none",
                  "stroke": "#000",
                  "stroke-width": "2"
              })
              .datum(data)
              .attr("d", line);

          focus.append("g")
              .attr("class","x brush")
              .call(brush.extent([data[7].date,data[11].date]))
              .selectAll("rect")
              .attr("height", height)
              .style({
                  "fill": "#69f",
                  "fill-opacity": "0.3"
              });

      console.log(node);
      return node.toReact()
    }
  }

var data =
        [
            {date: new Date(2015, 2, 5), data: 1},
            {date: new Date(2015, 2, 6), data: 2},
            {date: new Date(2015, 2, 7), data: 0},
            {date: new Date(2015, 2, 8), data: 3},
            {date: new Date(2015, 2, 9), data: 2},
            {date: new Date(2015, 2, 10), data: 2},
            {date: new Date(2015, 2, 11), data: 3},
            {date: new Date(2015, 2, 12), data: 3},
            {date: new Date(2015, 2, 13), data: 3},
            {date: new Date(2015, 2, 14), data: 2},
            {date: new Date(2015, 2, 15), data: 3},
            {date: new Date(2015, 2, 16), data: 3},
            {date: new Date(2015, 2, 17), data: 6}
]

React.render(
  React.createElement(Graph, {data: data}),
  document.getElementById('mount-chart')
)

What I noticed is that the brushstart() function from d3 receives Window as the this parameter instead of the SVG element g.

@wuxianliang
Copy link

Brush is definitely important for dynamic charts. It is the last piece to prevent me from coding dc.js like charts.

@wuxianliang
Copy link

How to use a bush to pick up specific data is the next problem. I did not understand brush internally, so can not figure out.

@wuxianliang
Copy link

It seems this project is a good example. https://github.com/react-d3/react-d3-brush

@martpie
Copy link

martpie commented Sep 30, 2015

I'm trying to figure out how to do this too. I guess I could do a custom brush using react, but this would force me to recode a brush while d3 has a native one.

@Olical
Copy link
Owner

Olical commented Sep 30, 2015

It should be something along the lines of this. (although I haven't checked yet!)

  • Work out what events brush sets (mouse move?)
  • Make sure it is being given the correct event object, it can't handle React synthetic events, we probably need to translate them to an object brush can use.

The core of this issue, as far as I can tell, reaches further than brush. You can't use d3.mouse because the d3.event object is a React event, not a normal DOM one. So maybe if we fix that issue the rest will just work.

A lot of this project has been about just fixing one small thing and then suddenly a whole host of things in D3 start working. Small changes can have a huge impact.

I think events are the first place to start.

@wuxianliang
Copy link

@KeitIG What I found two days ago is not a solution in @Olical 's sense. It uses d3.brush() directly.
Let's see events in brush.js

https://github.com/mbostock/d3/blob/78e0a4bb81a6565bf61e3ef1b898ef8377478766/src/svg/brush.js

@Olical
Copy link
Owner

Olical commented Sep 30, 2015

As you can see from here https://github.com/mbostock/d3/blob/78e0a4bb81a6565bf61e3ef1b898ef8377478766/src/svg/brush.js#L30-L31 there's some odd events being added. These are the kind of things we need to shim in order for this to work.

And things like this, because I'm pretty sure d3.mouse doesn't work right now. https://github.com/mbostock/d3/blob/78e0a4bb81a6565bf61e3ef1b898ef8377478766/src/svg/brush.js#L156

If we can handle those weird looking events and get d3.mouse working with React synthetic events we're most of the way there.

@wuxianliang
Copy link

Dear @sxywu
Would you like to help us further this approach? Thank you.

@martpie
Copy link

martpie commented Oct 2, 2015

Hey @Olical !

sorry to bother you, but do you think you will be able to fix this soon ? To know if I have to switch back to a classic componentDidMount/componentWillUnmount.

@Olical
Copy link
Owner

Olical commented Oct 2, 2015

I'm on holiday next week but will bring my laptop, so I may have a play with it while I'm away. I can't promise I'll be able to support it just yet though so going back to the original solution is probably a good idea for now. Keep and eye on this for when I do fix it though! I'm hoping it won't take long.

@RassaLibre
Copy link

Brushing would be super cool. I am following this example, my code is following:

render(){

    const el = d3.select(ReactFauxDOM.createElement("svg"))
                 .attr("width", this.props.width)
                 .attr("height", this.props.height + this.props.brushMargin.top)
                 .attr("id", this.props.id)
                 .attr("data", null);

    let actualGraphWidth = this.props.width - this.props.margin.left - this.props.margin.right;
    let actualGraphHeight = this.props.height - this.props.margin.top - this.props.margin.bottom;

    let rangeX = d3.scale.linear().range([0, actualGraphWidth]);
    let rangeY = d3.scale.linear().range([actualGraphHeight, 0]);

    let xAxis = d3.svg.axis().scale(rangeX).orient("bottom").ticks(10);
    let yAxis = d3.svg.axis().scale(rangeY).orient("left").ticks(10);

    let valueLine = d3.svg.line()
                          .x((d)=>{return rangeX(d.x)})
                          .y((d)=>{return rangeY(d.y)})
                          .interpolate("monotone");

    let focus = el.append("g")
                   .attr("class","focus")
                   .attr("transform",
                        "translate(" + this.props.margin.left + "," + this.props.margin.top + ")");

    //TODO:consider total maximum in all datasets
    rangeX.domain([0, d3.max(this.props.data[0].values, (d)=>{return d.x;})]);
    rangeY.domain([0, d3.max(this.props.data[0].values, (d)=>{return d.y;})]);

    //draw the paths and their points
    for(let path of this.props.data){
        let lineWrapper = focus.append("g").attr("class","line-wrapper").attr("data-series", path.name);
        let svgPath = lineWrapper.append("path").attr("class","line");
        let pathOptionsKeys = Object.keys(path.pathOptions);
        for(let pathOptionKey of pathOptionsKeys){
            svgPath.attr(pathOptionKey, path.pathOptions[pathOptionKey]);
        }
        svgPath.attr("d", valueLine(path.values));
        //and points
        let points = lineWrapper.selectAll(".point").data(path.values).enter()
                        .append("svg:circle")
                        .attr("class","point")
                        .attr("cx",(d,i)=>{return rangeX(d.x)})
                        .attr("cy",(d,i)=>{return rangeY(d.y)})
                        .attr("stroke", path.pathOptions.stroke)
                        .attr("stroke-width", path.pathOptions["stroke-width"])
                        .attr("fill",(d,i)=>{ return "white" })
                        .attr("r",(d,i)=>{return 4});
    }

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

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

    if(this.props.brush){
        //range
        let brushRangeX = d3.scale.linear().range([0, actualGraphWidth]);
        let brushRangeY = d3.scale.linear().range([this.props.brushHeight, 0]);

        brushRangeX.domain(rangeX.domain());
        brushRangeY.domain(rangeY.domain());

        //axis
        let brushXAxis = d3.svg.axis().scale(brushRangeX).orient("bottom");

        //brush reference
        let brush = d3.svg.brush().x(brushRangeX).on("brush", (param)=>{
            rangeX.domain(brush.empty() ? brushRangeX.domain() : brush.extent());

            focus.selectAll(".line-wrapper").select(".line").attr("d", (d, i)=>{
                return valueLine(this.props.data[i].values);
            });
            focus.selectAll(".line-wrapper").selectAll(".point").attr("cx",(d, i)=>{
                return rangeX(d.x);
            }).attr("cy",(d,i)=>{
                return rangeY(d.y);
            });
            focus.select(".x.axis").call(xAxis);
        });

        let context = el.append("g")
                         .attr("class","context")
                         .attr("transform",
                            "translate(" + this.props.brushMargin.left +
                            ", " + this.props.brushMargin.top + ")");

        let contextValueLine = d3.svg.line().x((d)=>{return brushRangeX(d.x)})
                                            .y((d)=>{return brushRangeY(d.y)})
                                            .interpolate("monotone");

        //draw the paths into the context
        for(let path of this.props.data){
            let contextPath = context.append("path").attr("class","line");
            let pathOptionsKeys = Object.keys(path.pathOptions);
            for(let pathOptionKey of pathOptionsKeys){
                contextPath.attr(pathOptionKey, path.pathOptions[pathOptionKey]);
            }
            contextPath.attr("d", contextValueLine(path.values));
        }

        context.append("g").attr("class","x axis")
                           .attr("transform", "translate(0," + this.props.brushHeight + ")")
                           .call(brushXAxis);

        context.append("g").attr("class","x brush").call(brush).selectAll("rect")
                           .classed("do-not-body-scroll", true)
                           .attr("y", -6).attr("height", this.props.brushHeight + 7);           
    }

    return el.node().toReact()
}

And I first get a warning in the console:

Warning: Unsupported vendor-prefixed style property webkitTapHighlightColor. Did you mean WebkitTapHighlightColor?

And when I actually try to brush it throws an error:

Uncaught TypeError: Cannot use 'in' operator to search for 'userSelect' in undefined in d3.js at line 471

@Olical
Copy link
Owner

Olical commented Oct 6, 2015

That's an interesting example, thanks for sharing. The webkit error is just to do with the way I'm camel casing the style properties, fairly easy to fix. Not sure what userSelect is but I can investigate. This should prove useful though.

Olical added a commit that referenced this issue Oct 11, 2015
This is something that someone noticed in #4 which could also contribute
to it sometimes. Gradually fixing all of these little unrelated issues
to make stuff like brush finally work.

I should help with future animation and force directed graph work too.
@Olical
Copy link
Owner

Olical commented Oct 11, 2015

@juanpr2 I actually think this may work now, I can't see why not now I've fixed events and few other bits. I think your main problem is that var brush = d3.svg.brush().x(x); is stateful, so you should instantiate that outside of your render function and reuse the same reference to brush each time. If you were using the flux pattern or something you'd put it in one of your stores.

Just make sure any stateful things are outside of the render function and they should work. Apart from animation, I'm not sure if that's possible at all, but you can use React animation libraries, so it's not so bad. Still requires investigation.

@RassaLibre
Copy link

I ended up writing my own React components and I am using D3 just to do the calculations everything else is React's responsibility. It was not that hard, I had to write my own brushing component but at the end I am super happy to have it all the reactive way.

@Olical
Copy link
Owner

Olical commented Oct 20, 2015

@RassaLibre I'm glad you found a solution! :) I'm still going to try and get D3's brushing working though, I'm sure it's possible. I need some more long train journeys to work on this...

@Olical
Copy link
Owner

Olical commented Nov 7, 2015

I'm looking into this at the moment, I have what should be a working example but it looks like d3_window is called in the brush stack which does some weird stuff. If I can fix that we should be okay. I'll post the example to http://lab.oli.me.uk/

@Olical
Copy link
Owner

Olical commented Nov 7, 2015

Oh, I see, because it's trying to add ontouchmove to the window! Maybe those things should be deferred to the root node of the faux DOM? That's a tough one to handle, and there's not going to be a one size fits all solution.

@Olical
Copy link
Owner

Olical commented Nov 7, 2015

I think setting the ownerDocument to itself may actually do the trick. I'll try it when I get back from dinner :)

@Olical Olical added the state label Nov 11, 2015
@Olical Olical mentioned this issue Nov 11, 2015
@vikrammirla
Copy link

I also got an error when I tried to use d3 brush-
Warning: Unsupported vendor-prefixed style property webkitTapHighlightColor. Did you mean WebkitTapHighlightColor?

Any luck folks with the fix?

@Olical
Copy link
Owner

Olical commented Nov 13, 2015

@vikrammirla That should have been fixed by this PR a while ago? #16

@Olical
Copy link
Owner

Olical commented Nov 17, 2015

I'm afraid I'm going to close this (among other issues), the reason is stated in the new limitations section in the readme (PR #30). This is essentially a question of state management which is illustrated in my lab with a simple example. The principal is essentially the same though!

@Olical Olical closed this as completed Nov 17, 2015
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

6 participants