RMS
RMS

Reputation: 123

How do I synchronize the zoom level of multiple charts with Plotly.js?

I created 3 time-series plots using Plotly.js and I would like to keep the zoom level/panning synchronized so if the user zooms/pans on any of the 3 plots, the other 2 get updated.

Right now I have an event handler set on the first chart for the plotly_relayout event. The event handler uses the event data passed in for calling Plotly.relayout on the other two charts.

Does anyone know how to identify which plot first emitted the plotly_relayout event so it doesn't get stuck in a loop handling its own event? Or perhaps there's a better way to handle keeping the zoom/pan synchronized between multiple charts?

See the Pen Linked Zoom Events in Plotly.js Charts by Restless Minds Studio (@restlessmindsstudio) on CodePen.

Upvotes: 8

Views: 11452

Answers (4)

Filipe
Filipe

Reputation: 659

Working from both @RMS (@ChrisG's) and @Alex's answers, I still had problems when I wanted to zoom both x and y (the browser would freeze completely either while zooming in one of the directions or when zooming out). I have just started using js for this function, but I made it work for any kind of zoom this way:

var myDiv = document.getElementById("myDiv");
var myDiv2 = document.getElementById("myDiv2");
var myDiv3 = document.getElementById("myDiv3");
var startTime = new Date().getTime();
var timeStep = 60000;
var myDivMax = 70;
var myDiv2Max = 9000;
var myDiv3Max = 500;

var d3 = Plotly.d3,
  N = 40,
  x = d3.range(N).map(() => {
    return new Date((startTime += timeStep));
  }),
  y = d3.range(N).map(() => Math.random() * myDivMax),
  data = [{ x: x, y: y }];
var layout = {
  height: 200,
  margin: { l: 45, t: 5, r: 45, b: 45 },
  xaxis: {
    tickfont: {
      size: 10,
      color: '#7f7f7f'
    }
  },
  yaxis: {
    tickfont: {
      size: 10,
      color: '#7f7f7f'
    }
  }
};
var layout2 = {
  height: 200,
  margin: { l: 45, t: 5, r: 45, b: 45 },
  xaxis: {
    tickfont: {
      size: 10,
      color: '#7f7f7f'
    }
  },
  yaxis: {
    tickfont: {
      size: 10,
      color: '#7f7f7f'
    }
  }
};
var layout3 = {
  height: 200,
  margin: { l: 45, t: 5, r: 45, b: 45 },
  xaxis: {
    tickfont: {
      size: 10,
      color: '#7f7f7f'
    }
  },
  yaxis: {
    tickfont: {
      size: 10,
      color: '#7f7f7f'
    }
  }
};

Plotly.plot(myDiv, data, layout);
var data2 = [{ x: x, y: d3.range(N).map(() => Math.random() * myDiv2Max) }];
Plotly.plot(myDiv2, data2, layout2);
var data3 = [{ x: x, y: d3.range(N).map(() => Math.random() * myDiv3Max) }];
Plotly.plot(myDiv3, data3, layout3);

var divs = [myDiv, myDiv2, myDiv3];

function relayout(ed, divs) {
      if (Object.entries(ed).length == 0) {return;}
      divs.forEach((div, i) => {
        let x = div.layout.xaxis;
        let y = div.layout.yaxis;
        var update = {};
        if ("xaxis.autorange" in ed && ed["xaxis.autorange"] != x.autorange) {
          update['xaxis.autorange']= ed["xaxis.autorange"];
        }
        if ("yaxis.autorange" in ed && ed["yaxis.autorange"] != y.autorange) {
          update['yaxis.autorange'] = ed["yaxis.autorange"];
        }
        if ("xaxis.range[0]" in ed && ed["xaxis.range[0]"] != x.range[0]) {
          update['xaxis.range[0]'] = ed["xaxis.range[0]"];
        }
        if ("xaxis.range[1]" in ed && ed["xaxis.range[1]"] != x.range[1]) {
          update['xaxis.range[1]'] = ed["xaxis.range[1]"];
        }
        if ("yaxis.range[0]" in ed && ed["yaxis.range[0]"] != y.range[0]) {
          update['yaxis.range[0]'] = ed["yaxis.range[0]"];
        }
        if ("yaxis.range[1]" in ed && ed["yaxis.range[1]"] != y.range[1]) {
          update['yaxis.range[1]'] = ed["yaxis.range[1]"];
        }
        Plotly.relayout(div, update);
      });
    }


divs.forEach(div => {
  div.on("plotly_relayout", function(ed) {relayout(ed, divs);});
});
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<h2>Resize any chart, the others will get the same zoom level applied</h2>
<div id="myDiv"></div>
<div id="myDiv2"></div>
<div id="myDiv3"></div>

Here is also a codepen link.

Upvotes: 1

Sachin Dev Sharma
Sachin Dev Sharma

Reputation: 221

Similar thing can be done in plotly dash. Code snippet is below.

app=dash.Dash()

app.layout = html.Div([
            dcc.Graph(id='graph',figure=fig),
            html.Pre(id='relayout-data', style=styles['pre']),
            dcc.Graph(id='graph2', figure=fig)])

@app.callback(Output('relayout-data', 'children'),
              [Input('graph', 'relayoutData')])
def display_relayout_data(relayoutData):
    return json.dumps(relayoutData, indent=2)


@app.callback(Output('graph2', 'figure'),
             [Input('graph', 'relayoutData')], 
             [State('graph2', 'figure')])
def graph_event(select_data,  fig):
    try:
       fig['layout'] = {'xaxis':{'range':[select_data['xaxis.range[0]'],select_data['xaxis.range[1]']]}}
    except KeyError:
       fig['layout'] = {'xaxis':{'range':[zoomed out value]}}
return fig

app.run_server()

Upvotes: -1

Alex
Alex

Reputation: 331

As Ashish Rathi noticed, the above solution has a bug - code execution enters infinite loop when you try to push certain buttons. Solution for that would be to check 'ed' variable in that own relayout handler and if it has no attributes - just to exit. Looks like in some cases Plotly sends that plotly_relayout event with empty object containing updates just for fun - probably some kind of bug.

And in my case I didn't want to have Y zoom to be propagated from one plot to another - thus I prepared a separate structure with only those attributes I want to have propagated.

var myDiv = document.getElementById("myDiv");
var myDiv2 = document.getElementById("myDiv2");
var myDiv3 = document.getElementById("myDiv3");
var startTime = new Date().getTime();
var timeStep = 60000;
var myDivMax = 70;
var myDiv2Max = 9000;
var myDiv3Max = 500;

var d3 = Plotly.d3,
  N = 40,
  x = d3.range(N).map(() => {
    return new Date((startTime += timeStep));
  }),
  y = d3.range(N).map(() => Math.random() * myDivMax),
  data = [{ x: x, y: y }];
var layout = {
  height: 200,
  margin: { l: 45, t: 5, r: 45, b: 45 },
  xaxis: {
    tickfont: {
      size: 10,
      color: "#7f7f7f"
    }
  },
  yaxis: {
    fixedrange: true,
    tickfont: {
      size: 10,
      color: "#7f7f7f"
    }
  }
};
var layout2 = {
  height: 200,
  margin: { l: 45, t: 5, r: 45, b: 45 },
  xaxis: {
    tickfont: {
      size: 10,
      color: "#7f7f7f"
    }
  },
  yaxis: {
    fixedrange: true,
    tickfont: {
      size: 10,
      color: "#7f7f7f"
    }
  }
};
var layout3 = {
  height: 200,
  margin: { l: 45, t: 5, r: 45, b: 45 },
  xaxis: {
    tickfont: {
      size: 10,
      color: "#7f7f7f"
    }
  },
  yaxis: {
    fixedrange: true,
    tickfont: {
      size: 10,
      color: "#7f7f7f"
    }
  }
};

Plotly.plot(myDiv, data, layout);
var data2 = [{ x: x, y: d3.range(N).map(() => Math.random() * myDiv2Max) }];
Plotly.plot(myDiv2, data2, layout2);
var data3 = [{ x: x, y: d3.range(N).map(() => Math.random() * myDiv3Max) }];
Plotly.plot(myDiv3, data3, layout3);

var divs = [myDiv, myDiv2, myDiv3];

function relayout(ed, divs) {
  if (Object.entries(ed).length === 0) {return;}
  divs.forEach((div, i) => {
    let x = div.layout.xaxis;
    if (ed["xaxis.autorange"] && x.autorange) return;
    if (x.range[0] != ed["xaxis.range[0]"] ||x.range[1] != ed["xaxis.range[1]"])
    {
      var update = {
      'xaxis.range[0]': ed["xaxis.range[0]"],
      'xaxis.range[1]': ed["xaxis.range[1]"],
      'xaxis.autorange': ed["xaxis.autorange"],
     };
     Plotly.relayout(div, update);
    }
  });
}

var plots = [myDiv, myDiv2, myDiv3];
plots.forEach(div => {
  div.on("plotly_relayout", function(ed) {
    relayout(ed, divs);
  });
});
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<h2>Resize any chart, the others will get the same zoom level applied</h2>
<div id="myDiv"></div>
<div id="myDiv2"></div>
<div id="myDiv3"></div>

Upvotes: 5

RMS
RMS

Reputation: 123

I'm duplicating @ChrisG's comment here to give him credit and show my question as answered:

Found a much better solution: https://codepen.io/anon/pen/NXRpzW?editors=1010

var myDiv = document.getElementById("myDiv");
var myDiv2 = document.getElementById("myDiv2");
var myDiv3 = document.getElementById("myDiv3");
var startTime = new Date().getTime();
var timeStep = 60000;
var myDivMax = 70;
var myDiv2Max = 9000;
var myDiv3Max = 500;

var d3 = Plotly.d3,
  N = 40,
  x = d3.range(N).map(() => {
    return new Date((startTime += timeStep));
  }),
  y = d3.range(N).map(() => Math.random() * myDivMax),
  data = [{ x: x, y: y }];
var layout = {
  height: 200,
  margin: { l: 45, t: 5, r: 45, b: 45 },
  xaxis: {
    tickfont: {
      size: 10,
      color: "#7f7f7f"
    }
  },
  yaxis: {
    fixedrange: true,
    tickfont: {
      size: 10,
      color: "#7f7f7f"
    }
  }
};
var layout2 = {
  height: 200,
  margin: { l: 45, t: 5, r: 45, b: 45 },
  xaxis: {
    tickfont: {
      size: 10,
      color: "#7f7f7f"
    }
  },
  yaxis: {
    fixedrange: true,
    tickfont: {
      size: 10,
      color: "#7f7f7f"
    }
  }
};
var layout3 = {
  height: 200,
  margin: { l: 45, t: 5, r: 45, b: 45 },
  xaxis: {
    tickfont: {
      size: 10,
      color: "#7f7f7f"
    }
  },
  yaxis: {
    fixedrange: true,
    tickfont: {
      size: 10,
      color: "#7f7f7f"
    }
  }
};

Plotly.plot(myDiv, data, layout);
var data2 = [{ x: x, y: d3.range(N).map(() => Math.random() * myDiv2Max) }];
Plotly.plot(myDiv2, data2, layout2);
var data3 = [{ x: x, y: d3.range(N).map(() => Math.random() * myDiv3Max) }];
Plotly.plot(myDiv3, data3, layout3);

var divs = [myDiv, myDiv2, myDiv3];

function relayout(ed, divs) {
  divs.forEach((div, i) => {
    let x = div.layout.xaxis;
    if (ed["xaxis.autorange"] && x.autorange) return;
    if (
      x.range[0] != ed["xaxis.range[0]"] ||
      x.range[1] != ed["xaxis.range[1]"]
    )
      Plotly.relayout(div, ed);
  });
}

var plots = [myDiv, myDiv2, myDiv3];
plots.forEach(div => {
  div.on("plotly_relayout", function(ed) {
    console.log(ed);
    relayout(ed, divs);
  });
});
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<h2>Resize any chart, the others will get the same zoom level applied</h2>
<div id="myDiv"></div>
<div id="myDiv2"></div>
<div id="myDiv3"></div>

Upvotes: 3

Related Questions