I have a Plotly graph with multiple traces which includes line as well as bar plots. If 2 traces have the same value at a point, the tooltip displays only the value of a single trace. I attempted to add custom tooltip for my traces, but it only displays a tooltip for a single trace, even when multiple traces have the same value. I also tried using the Plotly hover event, but it doesn't seem to work as expected; it gets overridden by the default tooltip.
const trace: any = [
{
x: this.data[this.group],
y: this.data.Price,
type: 'scatter',
mode: 'lines+markers',
name: 'Grid Price',
line: { color: '#64323A', width: 3, shape: 'spline' },
marker: { size: 7 },
yaxis: 'y2',
hovertemplate: '%{x}<br>Grid Price: %{customdata}<extra></extra>',
customdata: this.data.Price.map((val: number) =>
Number.isInteger(val) ? val.toFixed(0) : val.toFixed(3))
},
{
x: this.data[this.group],
y: this.data.Price.map(() => this.simulatorRun?.grid_used_to_firm_h2_output ? this.simulatorRun?.load_gain_price : 0),
type: 'scatter',
mode: 'lines+markers',
name: 'Load Gain Price',
line: { color: '#182D44', width: 3, shape: 'spline' },
marker: { size: 7 },
yaxis: 'y2',
hovertemplate: '%{x}<br>Load Gain Price: %{customdata}<extra></extra>',
customdata: this.data.Price.map(() =>
this.simulatorRun?.grid_used_to_firm_h2_output ? this.simulatorRun?.load_gain_price : 0).map((val: string) => {
const numVal = parseFloat(val); /// Convert string to a number
return Number.isInteger(numVal) ? numVal.toFixed(0) : numVal.toFixed(3);
})
},
{
x: this.data[this.group],
y: this.data.Price.map(() => this.simulatorRun?.grid_used_to_firm_h2_output ? this.simulatorRun?.load_shed_price : 0),
type: 'scatter',
mode: 'lines+markers',
name: 'Load Shed Price',
line: { color: '#273D1B', width: 3, shape: 'spline' },
marker: { size: 7 },
yaxis: 'y2',
hovertemplate: '%{x}<br>Load Shed Price: %{customdata}<extra></extra>',
customdata: this.data.Price.map(() =>
this.simulatorRun?.grid_used_to_firm_h2_output ? this.simulatorRun?.load_shed_price : 0).map((val: string) => {
const numVal = parseFloat(val); /// Convert string to a number
return Number.isInteger(numVal) ? numVal.toFixed(0) : numVal.toFixed(3);
})
},
{
x: this.data[this.group],
y: this.data.Price.map(() => this.simulatorRun?.sell_electricity ? this.simulatorRun?.electricity_price : 0),
type: 'scatter',
mode: 'lines+markers',
name: 'Sell Electricity Price',
line: { color: '#B38C3A', width: 3, shape: 'spline' },
marker: { size: 7 },
yaxis: 'y2',
hovertemplate: '%{x}<br>Sell Electricity Price: %{customdata}<extra></extra>',
customdata: this.data.Price.map(() =>
this.simulatorRun?.sell_electricity ? this.simulatorRun?.electricity_price : 0).map((val: number) =>
Number.isInteger(val) ? val.toFixed(0) : val.toFixed(3))
},
{
x: this.data[this.group],
y: this.data["Grid Withdrawals (MW)"],
type: 'bar',
name: 'Grid',
marker: { color: '#046CC4' },
yaxis: 'y1',
hovertemplate: '%{x}<br>Grid: %{customdata}<extra></extra>',
customdata: this.data["Grid Withdrawals (MW)"].map((val: number) =>
Number.isInteger(val) ? val.toFixed(0) : val.toFixed(3))
},
{
x: this.data[this.group],
y: this.data["Grid Exports (MW)"],
type: 'bar',
name: 'Export',
marker: { color: '#388454' },
yaxis: 'y1',
hovertemplate: '%{x}<br>Export: %{customdata}<extra></extra>',
customdata: this.data["Grid Exports (MW)"].map((val: number) =>
Number.isInteger(val) ? val.toFixed(0) : val.toFixed(3))
},
];
/// Configure layout and responsive options
const commonTickfontSize = this.group === '48h' || this.group === 'Day' ? 12 : 14; /// Adjust font size based on the group
const tickAngle = this.group === '48h' ? -30 : this.group === 'Day' ? -45 : 0; /// Rotate for 48h and Day
const legendYPosition = this.group === '48h' ? -0.3 : this.group === 'Day' ? -0.55 : -0.2; /// Adjust the legend position based on group
const layout: Partial<Plotly.Layout> = {
hovermode: 'closest',
title: {
text: 'Grid Usage/Grid Price vs Time',
font: { size: 14 },
x: 0.5,
xanchor: 'center',
},
xaxis: {
tickvals: this.data[this.group],
ticktext: this.data[this.group].map((val: any) => this.utilsService.getDateTextForChart(val, this.group)),
tickfont: { size: commonTickfontSize, color: '#000000' },
showgrid: true,
tickangle: tickAngle,
},
yaxis: {
title: {
text: 'Operating Points (MW)',
font: { size: 16, color: '#000000' },
standoff: 30, /// Adds space between the title and the axis
},
tickfont: { size: 14, color: '#000000' },
showgrid: false,
rangemode: 'tozero'
},
yaxis2: {
tickfont: { size: 14, color: '#000000' },
overlaying: 'y',
side: 'right',
showgrid: false,
rangemode: 'tozero'
},
annotations: [ ///reverse yaxis2 title
{
xref: 'paper',
yref: 'paper',
x: 1.05,
y: 0.7,
text: 'Grid Price ($/MWh)',
showarrow: false,
font: { size: 16 },
textangle: '90' as any, /// Rotate the text
}
],
legend: {
orientation: 'h',
font: { size: 14, color: '#000000' },
x: 0.5,
y: legendYPosition,
xanchor: 'center',
yanchor: 'bottom',
},
dragmode: undefined, /// Disable dragging for zoom/pan
barmode: 'stack',
margin: { t: 50, b: 50, l: 50, r: 70 },
bargap: 0.7,
plot_bgcolor: '#F8F8F8',
paper_bgcolor: '#F8F8F8',
};
const config: Partial<Plotly.Config> = {
responsive: true,
displayModeBar: true,
modeBarButtonsToRemove: ['select2d', 'lasso2d', 'autoScale2d', 'zoomIn2d', 'zoomOut2d'] as Plotly.ModeBarDefaultButtons[], /// Disable box and lasso select, autoscale, zoom in, zoom out buttons
toImageButtonOptions: {
format: 'png',
filename: 'Grid Usage/Grid Price vs Time',
height: 500,
width: 1300,
scale: 1,
},
displaylogo: false,
};
Plotly.react(this.plot, trace, layout, config);
this.plotInited.emit({ plot: this.plot });
/// Event listeners for zoom and pan
this.plot.on('plotly_relayout', (eventData: any) => {
if (eventData['xaxis.range[0]'] || eventData['xaxis.range[1]']) {
this.plotZoomed.emit({ event: eventData });
}
});
this.plot.on('plotly_click', (eventData: any) => {
this.plotClicked.emit({ event: eventData, item: eventData.points });
});
this.plot.on('plotly_hover', (eventData: any) => {
const annotations: Partial<Plotly.Annotations>[] = [];
eventData.points.forEach((point: any) => {
const xVal = point.x;
const yVal = point.y;
const traceName = point.data.name;
// Find traces with the same x and y values
const tracesWithSameValue = eventData.points.filter(
(p: any) => p.x === xVal && p.y === yVal
);
if (tracesWithSameValue.length > 1) {
const tooltipContent = tracesWithSameValue
.map(
(trace: any) =>
`${trace.data.name}: ${trace.y.toFixed(3)}`
)
.join('<br>');
// Create a custom annotation
annotations.push({
x: xVal,
y: yVal,
text: tooltipContent,
showarrow: false,
font: { size: 12 },
align: 'left' as 'left',
bgcolor: 'rgba(255,255,255,0.8)',
bordercolor: '#ccc',
borderwidth: 1,
});
}
});
// Update the layout with custom annotations
Plotly.relayout('chart', { annotations });
});
// Remove annotations on unhover
this.plot.on('plotly_unhover', () => {
Plotly.relayout('chart', { annotations: [] });
});
}