Closed icd2k3 closed 4 years ago
I just went through this same process, and I learned that the tooltip component doesn't work at all as I expected it to. It's essentially composed of two parts: a flyout and a label. The flyout only renders the background of the tooltip, while the label only renders the text. You can see where they render here:
This doesn't make very much sense to me, and it makes implementing a custom tooltip overly complex. I understand the desire to use primitives to create more complex components, but they don't really fit the need very well in this case.
The documentation is also not particularly useful in this regard, since they give you the impression that the tooltips are lot more customizable than they actually are. The goal here should be a tooltip that can accept a single customizable component as an argument that gets the full set of contextual props and lets users easily override the layout.
Thanks @amaschas I completely agree. In general tooltip customization feels very bare-bones within Victory.
It would also be great if tooltips were somehow decoupled from the svg viewport. Maybe as a div container that sits above the svg chart? For charts that have a lot of other elements around them like: It's very easy for the tooltip to be clipped by the chart viewport.
I think I'll try to implement a custom HTML container above the chart SVG with its own (invisible but hoverable) data points and tooltips. This would make for a good feature request I think...
@icd2k3 mind if I split this into two issues?
1) make sure datum
and index
are passed through to Flyout
2) HTML tooltips feature request
Works for me! Thanks @boygirl!
For those who may be curious I found a temporary workaround for this. It's hacky, but works fine for my use case (unfortunately not IE11 compatible):
1.) VictoryChart set style to have overflow visible (so tooltips don't get cut off):
<VictoryChart style={{ parent: { overflow: 'visible' }}} />
2.) VictoryScatter component with tooltip for points:
<VictoryScatter
labelComponent={
<VictoryTooltip
flyoutComponent={<Tooltip />}
orientation={d => <TooltipContent average={d.average} />} // hacky workaround before https://github.com/FormidableLabs/victory-core/pull/244 is released
renderInPortal
/>
}
labels=""
/>
3.) Tooltip component
const Tooltip = ({
orientation, // hacky workaround before https://github.com/FormidableLabs/victory-core/pull/244 is released
x,
y,
}) => (
<foreignObject
height="0"
width="0"
x={Math.round(x)}
y={Math.round(y)}
>
<MyCustomHTMLTooltip
active
label={
<div>
{orientation}
</div>
}
/>
</foreignObject>
);
Note that I'm using orientation
for the content as a hacky workaround before https://github.com/FormidableLabs/victory-core/pull/244 is released which will pass datum/index to the tooltip. Currently this workaround will cause PropType
warnings from React.
4.) TooltipContent component:
const TooltipContent = ({
average,
}) => (
<div>
<div>Average Rating</div>
<div>
{average}
</div>
</div>
);
There we have it! HTML tooltips in the SVG chart using foreignObject
. Unfortunately this element isn't supported in IE11, but it's good enough for now! Hope it is helpful to others looking for a workaround before an official solution.
FYI, I created a general solution inspired by VictoryPortal
:
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
const portalContextType = { portalContexts: PropTypes.object, }
class PortalContext extends React.Component {
static contextTypes = portalContextType;
static childContextTypes = portalContextType;
getChildContext() {
return {
portalContexts: {
...this.context.portalContexts,
[this.props.name]: this,
}
};
}
render() {
return this.props.contents({
portalContents: <div ref={node => { this.output_node = node; } } />,
});
}
}
class Portal extends React.Component {
static contextTypes = portalContextType;
destination() {
const portalContext = this.context.portalContexts[this.props.name];
return ReactDOM.findDOMNode(portalContext.output_node);
}
componentWillMount() {
this.output = document.createElement('div');
this.destination().appendChild(this.output);
this.componentDidUpdate();
}
componentWillUnmount() {
ReactDOM.unmountComponentAtNode(this.output);
this.destination().removeChild(this.output);
this.output = null;
}
componentDidUpdate() {
if (this.props.children) ReactDOM.render(this.props.children, this.output);
}
render() { return null; }
}
const portalContextDecorator = options => Component => props =>
<PortalContext
{ ...options }
contents={ portalProps => <Component {...portalProps} {...props} /> }
/>;
export {
Portal,
portalContextDecorator,
}
Turning this into fully-functioning tooltips is left as an exercise for the reader, by which I mean I haven't quite finished it yet.
I'm using React-Portal as a Wrapper to show the tooltip outside the svg and without using foreignObject. The advantage is that it is working in IE11.
class ChartTooltip extends React.Component {
static defaultEvents = VictoryTooltip.defaultEvents
constructor(props) {
super(props);
this.state = {position: true};
}
calcTooltipPosition = (node, x, y) => {
if (node && this.state.x !== x && this.state.y !== y) {
this.setState({
x: x,
y: y,
position: node.getBoundingClientRect()
});
}
}
render() {
const {x, y, datum, color} = this.props;
return (
<g>
<circle className="chart__line__invisible" cx={x} cy={y} r="1" ref={(node) => this.calcTooltipPosition(node, x, y)} />
<Portal closeOnEsc closeOnOutsideClick isOpened={true}>
<div className="tooltip" style={{left: this.state.position.x, top: this.state.position.y}}>
Portal Content
</div>
</Portal>
</g>
);
}
}
This is the way I use the Tooltip for Bar Charts. On Stacked Bar Charts I create an invisible BarChart with the BarTooltip
covering the Stacks so the Tooltips hover area will use this one and there is only one tooltip for each stack.
import React from 'react'
import { VictoryTooltip } from 'victory'
import { TooltipPortal } from '../charts'
export class BarTooltip extends React.Component {
static defaultEvents = VictoryTooltip.defaultEvents;
render() {
var {angle, events, realFrom, ...other} = this.props;
return (
<g>
<VictoryTooltip
{...other}
labelComponent={ <EmptySvg /> }
flyoutComponent={<TooltipPortal {...this.props} from={realFrom} showLine={false} />}
/>
</g>
);
}
}
The Portal to display html-content across browsers. The .tooltip
class must have some "position: fixed" styling.
import React from 'react'
import { VictoryTooltip } from 'victory'
import Portal from 'react-portal'
/* Tooltip */
class TooltipPortal extends React.Component {
static defaultEvents = VictoryTooltip.defaultEvents
constructor(props) {
super(props);
this.state = {position: true};
}
calcTooltipPosition = (node, x, y) => {
if (node && this.state.x !== x) {
this.setState({
x: x,
y: y,
position: node.getBoundingClientRect()
});
}
}
render() {
const {x, y, datum, offsetX, horizontal, manualPosition, padding, width} = this.props;
const transform = offsetX ? `translate(${offsetX})` : '';
var xTop = horizontal && padding ? (width + padding.left)/2 : x;
var yTop = y;
return (
<g transform={transform} >
<circle
className="chart__line__invisible"
cx={xTop}
cy={yTop}
r="1"
ref={(node) => this.calcTooltipPosition(node, x, y)}
/>
<Portal
closeOnEsc
closeOnOutsideClick
isOpened={true}>
<div
className="tooltip"
style={{left: this.state.position.left, top: this.state.position.top}}
>
Content
</div>
</Portal>
</g>
);
}
}
export {TooltipPortal};
@Utzel-Butzel The idea of overlapping charts works but I need to use node.getBoundingClientRect() + window.scrollY
in order to get the correct y position for the tooltip, otherwise its position is calculated on the basis of the viewport, not on the entire document.
@Utzel-Butzel: Thanks for this idea. Could you please share working demo jsfiddle or other for your implementation ?
@laurasilvani I've used position: fixed
to display the Tooltip. @rsoni0809 Yes, I can add an example
@Utzel-Butzel Yes, I noticed the fixed position, but it happened I scrolled while the tooltip was visible and didn't like it was fixed. So I am setting a fixed width to the chart and added some calc involving the width and height of the chart to place the tooltip correctly
I have an approach similar to @icd2k3 using foreignObjects within a custom flyout, but it only seems to work in Chrome. Is this a known issue?
Hovering a slice here should bring up a tooltip in the center of the chart: https://codesandbox.io/s/7wmy73k0jx
I'm closing this issue, as it will not be fixed within the library. The best solution is to create a custom label component that renders foreignObject
wrapped html elements.
Having to use foreignObject for custom tooltips for web could really be used in the docs!
According to the docs:
However, I am not seeing them passed to my custom flyout component (
<Tooltip />
's props):Code:
Note that my custom
<Tooltip />
component renders fine, butindex
anddatum
are not in the props object (image above). I've also tried settinglabelComponent
on my container instead, but no dice.Thanks!