Plotly.js, show tooltips outside of chart container - d3.js

I need to implement a plotly.js chart on a page with a very restricted width. As a result, a tooltip is partially cut. Is it possible to cause tooltip not to be limited by plotly.js container size?
My code example at codepen: https://codepen.io/anatoly314/pen/gOavXzZ?editors=1111
//my single trace defined as following but it's better to see example at codepen
const yValue1 = [1000];
const trace1 = {
x: [1],
y: yValue1,
name: `Model 1`,
text: yValue1.map(value => Math.abs(value)),
type: 'bar',
textposition: 'outside'
};

It is, by design, not possible for any part of the chart to overflow its container.

I would say it is wrong to say that by design this is not possible! It is a bit hacky, but when you add the following lines, it shows the label outside of svg:
svg.main-svg,svg.main-svg *
{
overflow:visible !important;
}

The answer given by rokdd works. However the css selector should be more specific, otherwise it's natural that you will introduce subtle bugs (particularly if you need to scroll the content where the plotly chart is contained).
If we look at the DOM tree constructed by Plotly, we find that the tooltips are created inside the <g class="hoverlayer"></g> element (which is a direct child of one of the three <svg class="main-svg"></svg>). So that parent (that svg.main-svg element) is only one that needs to affected.
The ideal css selector in this case would be the :has selector. However it's still not supported (as of 2022): https://css-tricks.com/the-css-has-selector/
So the next simplest thing is to use a little bit of javascript right after we call Plotly.newPlot:
// get the correct svg element
var mainSvgEl = document.querySelector('#positive g.hoverlayer').parentElement;
mainSvgEl.style['overflow'] = 'visible';
Or in a more generic way (works for any chart):
Array.from(document.querySelectorAll('g.hoverlayer')).forEach(hoverEl => {
let mainSvgEl = hoverEl.parentElement;
mainSvgEl.style['overflow'] = 'visible';
});

Related

Using HTML Canvas from ReasonML using React Hooks

I'm looking for a quick example on how to get started using the following technologies together:
HTML5 Canvas
ReasonML
ReasonReact: "ReasonReact is a safer, simpler way to build React components, in Reason."
bs-webapi: Web API bindings for Reason
React Hooks
To get me started a snippet that does the following would be great:
Manages a reference to the HTML5 Canvas element elegantly and correctly
Is a simple react component
Clears the canvas and draws something
I already have the basic ReasonML React project setup.
Here is a sample that shows one way to put everything together:
// Helper type to pass canvas size
type dimensions = {
width: float,
height: float,
};
// Actual drawing happens here, canvas context and size as parameters.
let drawOnCanvas =
(context: Webapi.Canvas.Canvas2d.t, dimensions: dimensions): unit => {
open Webapi.Canvas.Canvas2d;
clearRect(context, ~x=0., ~y=0., ~w=dimensions.width, ~h=dimensions.height);
setFillStyle(context, String, "rgba(0,128,169,0.1)");
fillRect(context, ~x=10.0, ~y=10.0, ~w=30.0, ~h=30.0);
};
// Extract canvas dimensions from canvas element
let canvasDimensions = (canvasElement: Dom.element): dimensions =>
Webapi.Canvas.CanvasElement.{
width: float_of_int(width(canvasElement)),
height: float_of_int(height(canvasElement)),
};
// An adapter to give nicer parameters to drawOnCanvas above
let drawOnCanvasElement = (canvasElement: Dom.element): unit =>
Webapi.Canvas.CanvasElement.(
drawOnCanvas(
getContext2d(canvasElement),
canvasDimensions(canvasElement),
)
);
[#react.component]
let make = () => {
open React;
let canvasElementRef: Ref.t(option(Dom.element)) = useRef(None);
useLayoutEffect0(() => {
Ref.current(canvasElementRef)
|> Belt.Option.map(_, drawOnCanvasElement)
|> ignore;
None;
});
<canvas
width="200"
height="100"
ref={ReactDOMRe.Ref.callbackDomRef(elem =>
React.Ref.setCurrent(canvasElementRef, Js.Nullable.toOption(elem))
)}
/>;
};
Here are some random links I used when learning how to do this. (Adding them here in case they are useful for others too.):
The bs-webapi test file to quickly learn the basics
A specific answer on how to use the setFillStyle (and where I learned the link to the test file above)
An answer in reason-react project showing how to work with React Refs.
The code has a bit more type declarations than necessary and some open
statements could be added, but I like my answers a bit on the verbose
side for a bit more instructiveness.
It should be relatively easy to shorten the code.
The intermediate functions canvasDimensions and drawOnCanvasElement add
a bit of structure to the code in my opinion, but I'm not sure if they make the sample more or less clear for readers or if there would be a more elegant way to work with the canvas size.

Svelte and D3 brush

I'm struggling to understand how to use Svelte with something like D3's brush project. Svelte operates using a declarative approach. In the area chart example the SVG for lines is written out in the template HTML. To do this with D3 you would use Javascript function calls to select an element and call another function to modify the DOM. In the aforementioned chart example the D3 scaled library is only used to generate the axis array but the HTML itself is managed by Svelte. It makes sense Svelte works this way - building things up with function calls would be a lot less clean, but I can't figure out how to do this with the brush. How can I declaratively build up the brush HTML inside of my Svelte template, and how would this affect things like brush events? Would it rather be best to just use the brush functions inside of say onMount and sort-of tie change events to local Svelte variables?
The same problem exists in React, because both React and D3 want to be in charge of the DOM. In React you simply call the a function that instructs D3 to do it's work in the ComponentDidMount method (or a useEffect if using hooks.
Svelte expects to be in charge of the situation, you declare how the UI is constructed, and define the operations, leaving it to do the work. It won't be able to track what D3 does, so I suspect you need to just let D3 be in charge of that part, and not worry about it being a little bit hacky.
I managed to do this myself https://svelte.dev/repl/00f726facd434b978c737af2698e0dbc?version=3.12.1
As Mikkel said above, the way Svelte is designed doesn't play well naturally with something like D3. As I see it you have two options: try to wire D3 events into Svelte's reactive variables, or try to implement the functionality yourself.
I opted for the second version. I took the HTML and CSS that D3 Brush created, adding a mouse handler to the carets, and tied all the variables together reactively. (The last part I did very messily. Would appreciate any feedback on doing this cleaner from other Svelte users).
It also took me a bit to wrap my head around this. But in the end it's actually not that complicated. They key step is to decide which library takes care of which responsibility.
D3 simply overlaps with Svelte since it also renders things on the screen. But if you think about it, you don't really need a renderer if you already have Svelte. Once you have a renderer, the complicated part about charts is really the positioning. And this is where D3 really shines. If you "cherry pick" the best from both worlds you actually end up with a great dev experience.
But, alas, you can also leave the rendering to D3. But then you need to keep Svelte out of the picture as much as possible.
Essentially you have two options that both work well:
Svelte renders only the container DOM and then hands over to D3 to both calculate & render. You interface between both worlds only during onMount and onDestroy
Svelte renders the whole DOM, D3 provides the chart positions.
As for the brush functionality:
I found it to work best to create a ChartContainer (which is essentially just an SVG) with slots and then drop a Brush component inside.
<script>
import { createEventDispatcher } from "svelte";
export let minX;
export let maxX;
export let dX = 0;
export let height;
const dispatch = createEventDispatcher();
let startX,
endX,
mouseDown = false,
brushArea;
function onMouseDown(event) {
if (mouseDown) return;
mouseDown = true;
brushArea.removeEventListener("mousemove", onMouseMove);
brushArea.removeEventListener("mouseup", onMouseUp);
brushArea.addEventListener("mousemove", onMouseMove);
brushArea.addEventListener("mouseup", onMouseUp);
brushArea.style.cursor = "ew-resize";
startX = Math.max(event.offsetX - dX, minX);
endX = null;
}
function onMouseMove(event) {
endX = Math.min(event.offsetX - dX, maxX);
}
function onMouseUp(event) {
mouseDown = false;
if (!endX) startX = null;
brushArea.style.cursor = null;
brushArea.removeEventListener("mousemove", onMouseMove);
brushArea.removeEventListener("mouseup", onMouseUp);
const active = !!startX;
dispatch("brush", {active, startX, endX, clear});
}
function clear() {
startX = null;
endX = null;
}
</script>
<rect class="ui--chart__brush_area"
bind:this={brushArea}
x={minX}
y="0"
height={height}
width={maxX-minX}
on:mousedown={onMouseDown}
/>
{#if endX != null}
<rect class="ui--chart__brush"
x={startX < endX ? startX : endX}
y="0"
height={height}
width={startX < endX ? endX-startX : startX-endX}
/>
{/if}
The dX prop is used to consider a left margin. Might or might not be required in your use case (depending on how you setup your chart). The key thing is to be able to use offsetX from the mouse event so that you know how far off from the SVG borders your mouse events have fired.
Then, you just
listen for the brush events
extract the coordinates
convert them to values -by using yourScale.invert(coordinate)
use these values, e.g. to update your chart
Like so:
function onBrush(event) {
const {active, startX, endX, clear} = event.detail;
if (active) {
const startDate = scaleX.invert(startX);
const endDate = scaleX.invert(endX);
dispatch("brush", {active, startX, endX, startDate, endDate, clear});
}
}
Hope this helps anyone else struggling with this. Good luck!
There isn't a single answer to your question, but I think the best option is to render the "most relevant data" using svelte's html, leaving the interactive elements (like brush) to run only on client side.
You should known that svelte converts your html to js generators internally, so calling the d3 functions is actually pretty similar to what svelte does client side. The only real advantage of using the svelte's html instead of d3 functions calls is SSR, and that's the reason it's reasonable to leave the brush to be client side only (since it needs js to be interactive anyway).
Svelte is kind of a "reactive vanilla", so you can use low-level libraries almost directly. Sometimes you need to do some tricks to access DOM elements directly (like d3 usually does), for that I recommend using the bind:this directive. Example:
<script>
import { brushX, select } from 'd3';
//...
let brushElement;
$: brush = brushX()
.extent([[padding.left, padding.top], [width - padding.right, height - padding.bottom]])
.on('end', onZoom)
$: if (brushElement) {
select(brushElement)
.call(brush)
}
</script>
<svg>
...
<g bind:this={brushElement} width={...} height={...} />
...
</svg>
One thing to consider when using DOM's API is SSR (sapper), thus any call of d3's select should be done only in the browser. The if case in the code above takes care of that, because the bind:this directive will only set brushElement when running client side.

How to apply brushing on a dynamically growing dataset?

I have a dynamically growing timeseries I need to display in a zoomable/panable chart.
Try it out here (in fact: my first jsFiddle ever :) ) :
https://jsfiddle.net/Herkules001/L12k5zwx/29/
I tried to do it the same way as described here: https://dc-js.github.io/dc.js/examples/replacing-data.html
However, each time the chart updates, the zoom and filter are lost on the focus chart. (The brush is preserved on the range chart however.)
How can I add data without resetting the views and losing the zoom?
var chart = dc.lineChart("#test");
var zoom = dc.lineChart("#zoom");
//d3.csv("morley.csv", function(error, experiments) {
var experiments = d3.csvParse(d3.select('pre#data').text());
experiments.forEach(function(x) {
x.Speed = +x.Speed;
});
var ndx = crossfilter(experiments),
runDimension = ndx.dimension(function(d) {return +d.Run;}),
speedSumGroup = runDimension.group().reduceSum(function(d) {return d.Speed * d.Run / 1000;});
chart
.width(768)
.height(400)
.x(d3.scaleLinear().domain([6,20]))
.brushOn(false)
.yAxisLabel("This is the Y Axis!")
.dimension(runDimension)
.group(speedSumGroup)
.rangeChart(zoom);
zoom
.width(768)
.height(80)
.x(d3.scaleLinear().domain([6,20]))
.brushOn(true)
.yAxisLabel("")
.dimension(runDimension)
.group(speedSumGroup);
zoom.render();
chart.render();
var run = 21;
setInterval(
() => {
var chartfilter = chart.filters();
var zoomfilter = zoom.filters();
chart.filter(null);
zoom.filter(null);
ndx.add([{Expt: 6, Run: run++, Speed: 100 + 5 * run}]);
chart.x(d3.scaleLinear().domain([6,run]));
zoom.x(d3.scaleLinear().domain([6,run]));
chart.filter([chartfilter]);
zoom.filter([zoomfilter]);
chart.render();
zoom.render();
},
1000);
//});
In this case, if you are just adding data, you don't need to do the complicated clearing and restoring of filters demonstrated in the example you cited.
That part is only necessary because crossfilter.remove() originally would remove the data that matched the current filters. An awkward interface, almost never what you want.
If you're only adding data, you don't have to worry about any of that:
setInterval(
() => {
ndx.add([{Expt: 6, Run: run++, Speed: 5000 + 5 * run}]);
chart.redraw();
zoom.redraw();
},
5000);
Note that you'll get less flicker, and decent animated transitions, by using redraw instead of render. I also added evadeDomainFilter to avoid lines being clipped before the edge of the chart.
Fork of your fiddle
Removing data
If you use the predicate form of crossfilter.remove() you don't have to worry about saving and restoring filters:
ndx.remove(d => d.Run < run-20);
However, this does expose other bugs in dc.js. Seems like elasticY does not work, similar to what's described in this issue. And you get some weird animations.
Here's a demo with remove enabled.
In the end, dc.js has some pretty neat features, and there is usually a way to get it to do what you want, but it sure is quirky. It's a very complicated domain and in my experience you are going to find some of these quirks in any fully featured charting library.
Update: I fixed the replacing data example, that one is just ndx.remove(() => true) now.
zooming issues
As Joerg pointed out in the comments,
when the chart is not zoomed, it would be nice to have it also grow to show new data as it arrives
the X domain was clipped or even reversed if the focus reached outside the original domain of the chart
We can address these issues by adding a preRedraw event handler. That's the ideal place to adjust the domain; for example you can implement elasticX manually if you need to. (As you'll see in a second, we do!)
First, a naive attempt that's easy to understand:
chart.on('preRedraw', () => {
chart.elasticX(!zoom.filters().length);
});
We can turn elasticX on and off based on whether the range chart has an active filter.
This works and it's nice and simple, but why does the chart get so confused when you try to focus on a domain that wasn't in the original chart?
Welp, it records the original domain (source). So that it can restore to that domain if the focus is cleared, and also to stop you from zooming or panning past the edge of the graph.
But notice from the source link above that we have an escape hatch. It records the original domain when the X scale is set. So, instead of setting elasticX, we can calculate the extent of the data, set the domain of the scale, and tell the chart that the scale is new:
chart.on('preRedraw', () => {
if(!zoom.filters().length) {
var xExtent = d3.extent(speedSumGroup.all(), kv => kv.key);
chart.x(chart.x().domain(xExtent));
}
});
New fiddle with zooming issues fixed.
There is still one glitch which Joerg points out: if you are moving the brush while data comes in, the brush handles occasionally will occasionally stray from the ends of the brush. In my experience, these kinds of glitches are pretty common in D3 (and dynamic charting in general), because it's difficult to think about data changing during user interaction. It probably could be fixed inside the library (perhaps an interrupted transition?) but I'm not going to get into that here.

Manually selecting a feature in a map generated by d3.js (for the purposes of zooming in this case)

I'd like to implement a map that zooms in on an area similar to Mike's click-zoom-example http://bl.ocks.org/mbostock/2206590
In fact I have this working fine already. My problem is that I can't rely on the click event to implement the zoom — the zoom will be triggered by another event (a link). So when I get to this part of Mike's code:
function clicked(d) {
var x, y, k;
if (d && centered !== d) {
var centroid = path.centroid(d);
...
I'm a bit of a loss as I don't have 'd'. So, I'm assuming that I can instead, manually pass 'd' to my click function when I call it. But how do I actually select the feature (which is what 'd' represents) I want from the map?
To be a bit more concrete, I have a map of the world. The paths within the SVG group contain class information (e.g. the one for France looks like):
<path class="subunit FXX FRA" id="FXX" data-subunit="FXX" data-countryName="France" data-countryCode="FRA" d="M153.88838704622088,519........"></path>
How would I pass the 'France object' to the clicked(d) function? Or is there another approach altogether that I should be trying.
Any tips or help greatly appreciated.
You can use D3's select for this purpose:
d3.select(".FRA").each(function(d) {
// same code as inside clicked
});
Get the data associated with the France object:
d3.select('.FXX.FRA').datum()
And pass it to clicked:
clicked(d3.select('.FXX.FRA').datum())

Tickformatter not performing as expected in latest version of Flot

In our previous version of Flot, the following tickformatter function worked fine. It displayed the value and used the class correctly.
yaxis: {
tickFormatter: function(v, axis) {
return "<span class='axisLabel'>" + v + "%</span>";
}
In the latest version (v. 0.7) it renders the tags literally so next to the graph I see a something like
<span class='axisLabel'>50%</span>
where the y axis tick labels should be. I should only be seeing a list of percentages.
I've done as much debugging as I can but haven't found out what is causing this. Any ideas would be appreciated.
This is due to a recent "improvement" in how the labels are treated, I think. From README.md in the development version:
Axis labels are now drawn with canvas text with some parsing to
support newlines. This solves various issues but also means that they
no longer support HTML markup, can be accessed as DOM elements or
styled directly with CSS.
More specifically, it seems that
function insertAxisLabels() {...}
was replaced by
function drawAxisLabels(){...}
at some point. The former used to place axis labels as a bunch of <div> elements, as follows:
<div class="tickLabels" style="font-size:smaller">
<div class="xAxis x1Axis" style="color:#545454">
<div class="tickLabel" style="position:absolute;text-align:center;left:-14px;top:284px;width:75px"><em>0</em></div>
[... div elements for other labels...]
</div>
</div>
That allowed one to use html code in the tickFormatter. In the latest version, all this is gone, and the labels are added to the canvas directly via
ctx.fillText(your_label, x, y);
No html formatting tags therefore work anymore. Things that used to be simple, like styling the tick labels or turning them into links, are now less straightforward. Maybe flot developers can shed some light on what is the best way to achieve the same functionality in the new version.
I am using flot mostly for barcharts. One (ugly) workaround that seems to work for me is to replace the entire drawAxisLabels function in the newest jquery.flot.js by insertAxisLabels function from the stable version (after renaming it to drawAxisLabels). I additionally have to set manually labelWidth in xaxis options of my plots since otherwise the width of the plots is calculated incorrectly.
function formatter(val, axis) {
return "<span style='font-weight: bold'>" + val / 1000000 + "m</span>";
}
var usersData = { color: "#00FF55", data: [[1, 900000], [2, 926000], [3, 959000], [4, 1056000], [5, 1242300]] };
$(document).ready(function() {
$.plot($("#UserGraph"), [usersData], { xaxis: { ticks: [] }, yaxis: { tickFormatter: formatter } });
});
I could remove the style and the numbers go back to normal non-bold numbers.

Resources