In short, I would like to force tooltip to be shown always above the hovered point on line series, even if it goes outside chart area. Tooltip is joint for all series. Example can be seen here:
const {useRef, useState} = React;
const CHART_CONTAINER = 'campaign-budget-chart';
const CHART_COLORS = {
value1: '#05a8fa',
value2: '#ed3434',
value3: '#0ec76a',
}
function getRandomNumber(max){
return Math.floor(Math.random() * Math.floor(max))
}
const initialData = [{
timestamp: new Date(2020, 09, 25),
value1: 0,
value2: getRandomNumber(50),
value3: getRandomNumber(250),
},{
timestamp: new Date(2020, 09, 26),
value1: getRandomNumber(100),
value2: getRandomNumber(50),
value3: getRandomNumber(250),
},{
timestamp: new Date(2020, 09, 27),
value1: getRandomNumber(100),
value2: getRandomNumber(50),
value3: getRandomNumber(250),
},
{
timestamp: new Date(2020, 09, 28),
value1: getRandomNumber(100),
value2: getRandomNumber(50),
value3: getRandomNumber(250),
}];
let i = 0;
function BudgetChart() {
const chartRef = useRef(null);
const [data, setData] = useState(initialData);
const [cursor, setCursor] = React.useState({ x: 0, y: 0 });
const [cursorVisible, setCursorVisible] = React.useState(false);
function createSeries(
fieldX,
fieldY,
name,
lineColor,
) {
if (!chartRef.current) return;
console.log('Create series');
// Init series
let series = chartRef.current.series.push(new am4charts.LineSeries());
series.name = name;
series.dataFields.valueY = fieldY;
series.dataFields.dateX = fieldX;
series.strokeWidth = 3;
series.stroke = am4core.color(lineColor);
series.tooltip.pointerOrientation = 'down';
series.tooltip.background.filters.clear(); // remove shadow
series.tooltip.getFillFromObject = false;
series.tooltip.background.fill = am4core.color('#2a2b2e');
series.tooltip.background.stroke = am4core.color('#2a2b2e');
series.tooltip.label.fontSize = 12;
series.tooltip.background.pointerLength = 0;
series.tooltip.dy = -5;
series.tooltipText = '{valueY}';
series.tensionX = 0.8;
series.showOnInit = false;
// Add bullet for optimization
let circleBullet = series.bullets.push(new am4charts.CircleBullet());
circleBullet.circle.radius = 6;
circleBullet.circle.fill = lineColor;
circleBullet.circle.stroke = am4core.color('#fff');
circleBullet.circle.strokeWidth = 3;
circleBullet.propertyFields.disabled = 'optimizationTooltipDisabled';
// Set up tooltip
series.adapter.add("tooltipText", function(ev) {
var text = "[bold]{dateX}[/]\n"
chartRef.current.series.each(function(item) {
text += "[" + item.stroke.hex + "]●[/] " + item.name + ": {" + item.dataFields.valueY + "}\n";
});
return text;
});
// Bullet shadow
let shadow = circleBullet.filters.push(new am4core.DropShadowFilter());
shadow.opacity = 0.1;
}
React.useEffect(() => {
if (!chartRef.current) {
chartRef.current = am4core.create(CHART_CONTAINER, am4charts.XYChart);
chartRef.current.paddingLeft = 0;
// Add date axis
let dateAxis = chartRef.current.xAxes.push(new am4charts.DateAxis());
dateAxis.renderer.labels.template.fontSize = 12;
dateAxis.renderer.labels.template.fill = am4core.color(
'rgba(183,186,199,0.8)'
);
dateAxis.renderer.grid.template.strokeOpacity = 0;
// Add value axis
let valueAxis = chartRef.current.yAxes.push(new am4charts.ValueAxis());
valueAxis.renderer.grid.template.stroke = am4core.color(
'#f0f2fa'
);
valueAxis.renderer.grid.template.strokeOpacity = 1;
valueAxis.renderer.labels.template.fill = am4core.color(
'rgba(183,186,199,0.8)'
);
valueAxis.renderer.labels.template.fontSize = 12;
// Add cursor
chartRef.current.cursor = new am4charts.XYCursor();
chartRef.current.cursor.maxTooltipDistance = -1;
// Add legend
chartRef.current.legend = new am4charts.Legend();
chartRef.current.legend.position = 'bottom';
chartRef.current.legend.contentAlign = 'left';
chartRef.current.legend.paddingTop = 20;
// Disable axis lines
chartRef.current.cursor.lineX.disabled = true;
chartRef.current.cursor.lineY.disabled = true;
// Disable axis tooltips
dateAxis.cursorTooltipEnabled = false;
valueAxis.cursorTooltipEnabled = false;
// Disable zoom
chartRef.current.cursor.behavior = 'none';
chartRef.current.cursor.events.on('cursorpositionchanged', function(ev) {
let xAxis = ev.target.chart.xAxes.getIndex(0);
let yAxis = ev.target.chart.yAxes.getIndex(0);
setCursor({
x: xAxis.toAxisPosition(ev.target.xPosition),
y: yAxis.toAxisPosition(ev.target.yPosition),
});
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Load data into chart
React.useEffect(() => {
console.log('data ', data)
if (chartRef.current) {
chartRef.current.data = data;
Object.keys(data[0]).forEach(key => {
if(key === 'timestamp') return;
createSeries(
'timestamp',
key,
key,
CHART_COLORS[key]
);
})
}
}, [data]);
// Handle component unmounting, dispose chart
React.useEffect(() => {
return () => {
chartRef.current && chartRef.current.dispose();
};
}, []);
function handleRemoveSeries(){
setData(data.map(item => ({timestamp: item.timestamp, value1: item.value1, value2: item.value2})))
}
return (
<>
<button onClick={handleRemoveSeries}>Remove 3rd series</button>
<div
id={CHART_CONTAINER}
style={{
width: '100%',
height: '350px',
marginBottom: '50px',
}}
/>
</>
);
}
ReactDOM.render(<BudgetChart />, document.getElementById('app'));
For all values near the top of the chart, tooltip is trying to squeeze itself inside chart area. According to the docs:
IMPORTANT: in some situations, like having multiple tooltips stacked for multiple series, the "up" and "down" values might be ignored in order to make tooltip overlap algorithm work.
Is there a way to disable the tooltip overlap algorithm work?
As of version
4.10.8
(released after posting the initial question) there is a propertyignoreBounds
on series tooltip.More on ignoreBounds on amchart docs