I have an x-range chart, and I need to add custom components (SVG icons) at the start of each data point ("x" prop value). Something like this (red circles are where custom components should be):
My idea is to place a custom component with position absolute, but I can't figure out how to transform timestamp values of the X axis to pixels. Or maybe there is a better solution?
Codesandbox demo
There are multiple ways to achieve what you want:
A. You can get a chart reference and use Axis.toPixels method to calculate the required coordinates:
export default function Chart() {
const chartComponentRef = useRef(null);
const [x1, setX1] = useState(0);
useEffect(() => {
if (chartComponentRef) {
const chart = chartComponentRef.current.chart;
const xAxis = chart?.xAxis[0];
const x1 = xAxis?.toPixels(Date.UTC(2022, 7, 10));
setX1(x1);
}
}, [chartComponentRef]);
return (
<div style={{ position: "relative" }}>
<HighchartsReact
ref={chartComponentRef}
highcharts={Highcharts}
options={staticOptions}
/>
<div style={{ position: "absolute", bottom: 70, left: x1 }}>ICON</div>
</div>
);
}
Live demo: https://codesandbox.io/s/highcharts-react-custom-componentt-ysjfh8?file=/src/Chart.js
API Reference: https://api.highcharts.com/class-reference/Highcharts.Axis#toPixels
B. You can also get coordinates from a point:
useEffect(() => {
if (chartComponentRef) {
const chart = chartComponentRef.current.chart;
const point = chart.series[0].points[2];
setX1(chart.plotLeft + point.plotX);
}
}, [chartComponentRef]);
Live demo: https://codesandbox.io/s/highcharts-react-custom-componentt-f7nveg?file=/src/Chart.js
C. Use Highcharts API to render and update the custom icons:
const staticOptions = {
chart: {
type: "xrange",
height: 200,
events: {
render: function () {
const chart = this;
const r = 10;
const distance = 25;
chart.series[0].points.forEach((point, index) => {
if (!point.customIcon) {
point.customIcon = chart.renderer
.circle()
.attr({
fill: "transparent",
stroke: "red",
"stroke-width": 2
})
.add();
}
point.customIcon.attr({
x: point.plotX + chart.plotLeft + r,
y:
point.plotY +
chart.plotTop +
(index % 2 ? distance : -distance),
r
});
});
}
}
},
...
};
Live demo: https://codesandbox.io/s/highcharts-react-custom-componentt-f-87eof0?file=/src/Chart.js
API Reference: https://api.highcharts.com/class-reference/Highcharts.SVGRenderer#circle
Related
I am using a Konva stage to hover over floor areas on browser (see running Next app here https://www.planpoint.io/themes/modern-1) and it works great on any device, except on iPhones. After some touches on the floors areas, they suddenly dissapear as if the canvas element would no longer be there. Has tested it with Safari and Chrome on iOS16.2 . The only difference for mobile devices is the use of touchstart, touchend events, instead of mouseover, mouseout events.
Here is the code for rendering the canvas component and background image
import { useEffect, useState, useRef, useLayoutEffect } from 'react'
import Image from 'next/image'
import Konva from 'konva';
import i18nService from '../../../helpers/i18nService'
import useWindowSize from '../../../hooks/useWindowSize'
export default function InteractiveCanvas(props) {
const [showCanvas, setShowCanvas] = useState(false);
const [placeholderWidth, setPlaceholderWidth] = useState(1);
const [placeholderHeight, setPlaceholderHeight] = useState(1);
const [width, height] = useWindowSize();
const canvasAreaRef = useRef(null)
const canvasPlaceholderRef = useRef()
let stage, shapesLayer, tooltipLayer;
useEffect(() => {
if (showCanvas) {
props.loaded() // Canvas has loaded
}
},[showCanvas])
useLayoutEffect(() => { initializeCanvas() });
useEffect(() => { initializeCanvas() }, [width, height]);
function initializeCanvas() {
if (!canvasPlaceholderRef.current || !canvasPlaceholderRef.current.clientWidth || !canvasPlaceholderRef.current.clientHeight || !canvasPlaceholderRef.current.clientWidth) {
// wait until the placeholder is available
setTimeout(function() { initializeCanvas() }, 1000)
} else if (canvasPlaceholderRef.current) {
// initialize placeholder
setPlaceholderWidth(canvasAreaRef.current.clientWidth)
setPlaceholderHeight(canvasPlaceholderRef.current.clientHeight)
// initialize konva
let width = canvasPlaceholderRef.current.clientWidth;
let height = canvasPlaceholderRef.current.clientHeight;
stage = new Konva.Stage({
container: props.konvaContainer,
width: width,
height: height,
name: 'stage'
});
shapesLayer = new Konva.Layer({ name: 'shapes' });
tooltipLayer = new Konva.Layer({ name: 'tooltips' });
let tooltip = new Konva.Label({
opacity: 1,
visible: false,
listening: false,
name: 'label'
});
tooltip.add(
new Konva.Tag({
fill: '#313131',
pointerDirection: 'down',
pointerWidth: 20,
pointerHeight: 10,
cornerRadius: 4,
lineJoin: 'round',
shadowColor: 'black',
shadowBlur: 10,
shadowOffsetX: 10,
shadowOffsetY: 10,
shadowOpacity: 0.25,
name: 'tag'
})
);
tooltip.add(
new Konva.Text({
text: '',
align: 'center',
lineHeight: 2,
fontFamily: 'Inter',
fontSize: 13,
padding: 5,
fill: 'white',
name: 'tag',
width: props.wideTooltip ? 160 : 80
})
);
tooltipLayer.add(tooltip);
let areas = getData();
// draw areas
for (let key in areas) {
let area = areas[key];
let shape = new Konva.Line({
stroke: 'white',
strokeWidth: 2,
points: area.points,
fill: area.color,
opacity: area.disabled || area.visible ? 1 : key === props.selected?.name ? 1 : 0, // Keep the selected floor/unit highlight shape with opacity 1 after render
disabled: area.disabled,
closed: true,
target: area.target,
key: key,
subline: area.subline,
perfectDrawEnabled: false,
name: 'line',
cursor: 'pointer'
});
let group = new Konva.Group({name: 'group'})
shapesLayer.add(group);
group.add(shape);
}
stage.add(shapesLayer);
stage.add(tooltipLayer);
stage.on('mouseover touchstart', function (evt) {
if (evt && evt.target) {
let shape = evt.target;
if (shape && !shape.attrs.editable) {
shape.opacity(1);
shapesLayer.draw();
if (evt.type === 'touchstart' && shape.attrs.name === 'line') props.onClick(shape.attrs.target)
}
}
});
stage.on('mouseout touchend', function (evt) {
if (evt && evt.target) {
let shape = evt.target;
if(shape.attrs.name === 'stage' || shape.attrs.target === props.selected?._id) return // Do nothing unless it is a different line shape
if (shape && !shape.attrs.editable) {
shape.opacity(shape.attrs.disabled || shape.attrs.visible ? 1 : 0);
shapesLayer.draw();
tooltip.hide();
tooltipLayer.draw();
}
}
});
stage.on('mousemove', function(evt) {
let shape = evt.target;
if (shape) {
let mousePos = stage.getPointerPosition();
let x = mousePos.x;
let y = mousePos.y - 5;
updateTooltip(tooltip, x, y, shape.attrs.key, shape.attrs.subline);
tooltipLayer.draw();
}
});
stage.on('click tap', function(evt) {
let shape = evt.target;
if(shape.attrs.name === 'stage' || shape.attrs.target === props.selected?._id) return // Do nothing unless it is a different line shape
if (shape && shape.attrs.target) props.onClick(shape.attrs.target)
})
setTimeout(() => {
setShowCanvas(true)
shapesLayer.draw();
tooltipLayer.draw();
}, 100)
}
}
function getData() {
let areas = {}
function transformPoints(points) {
const c = JSON.parse(points || '[]')
return c.map((e, i) => (i % 2) ? (placeholderWidth * e) : placeholderHeight * e)
}
// add inactive paths
for (let p of props.inactivePaths) {
let newData = {
target: p.target,
color: p.disabled ? (props.disabledColor || '#E1171799') : props.accentColor || 'rgba(15, 33, 49, 0.65)',
points: transformPoints(p.path),
visible: p.visible,
disabled: p.disabled
}
if (props.project && props.project.showFloorOverview) {
let availableUnits = p.units.filter(u => u.availability.toLowerCase() === 'available').length
newData.subline = props.project.showFloorOverview ? `${availableUnits} ${i18nService.i18n(props.locale, "canvas.unitsavailable")}` : ''
}
areas[p.title] = newData
}
return areas;
}
function updateTooltip(tooltip, x, y, text, subline) {
const conditionalSubline = subline ? `\n${subline}` : ''
tooltip.children[1].text(text+conditionalSubline);
// tooltip.getText().text(text);
tooltip.position({ x: x, y: y });
if (text) tooltip.show();
};
return (
<div className={props.styles.canvasContainer}>
<img
className={props.styles.canvasImgPlaceholder}
ref={canvasPlaceholderRef}
alt='Canvas Placeholder'
src={props.background}
/>
<div className={props.styles.canvasPlaceholderBox} data-hide={showCanvas}>
<Image src="/images/planpoint_icon.svg" width={200} height={200} alt="" />
</div>
<div
className={props.styles.canvasKonvaContainer}
id={props.konvaContainer}
ref={canvasAreaRef}
data-hide={!showCanvas}
style={{backgroundImage: `url('${props.background}')`}}
>
</div>
</div>
)
}
I want to highlight the selected Y-axis label in Gantt charts. Also is there any way through which we can give some background color to the y-axis title?
yAxis: {
className: "highcharts-color-0",
uniqueNames: true,
title: {
text: "Data"
},
labels: {
events: {
click: function () {
alert("hellowww");
var chart = this,
series = chart.series,
plotLeft = chart.plotLeft,
plotTop = chart.plotTop,
plotWidth = chart.plotWidth;
if (chart.myBackground) {
chart.myBackground.destroy();
}
chart.myBackground = chart.renderer
.rect(10, plotTop, 500, 30, 1)
.attr({
"stroke-width": 2,
stroke: "red",
fill: "yellow",
opacity: 0.5,
zIndex: -1
})
.add();
}
}
}
}
enter image description here
link to code pen https://codepen.io/mehrotrarohit07/pen/PoKxvQp?editors=1010
Try to use this approach:
yAxis: {
labels: {
events: {
click: function() {
const chart = this.chart;
const axis = this.axis;
const labelPos = this.pos;
const tick = axis.ticks[labelPos]
const x = chart.marginRight;
const width = tick.slotWidth;
const height = axis.height / (axis.tickPositions.length);
const y = axis.top + labelPos * height;
chart.renderer
.rect(x, y, tick.slotWidth, height)
.attr({
fill: 'yellow',
zIndex: 0
})
.add();
}
}
}
},
I hope that everything is clear from the code - in case of any doubts feel free to ask.
Demo: https://jsfiddle.net/BlackLabel/qyj327Lx/
API: https://api.highcharts.com/class-reference/Highcharts.SVGRenderer#rect
Can we shoe the text inside the Highchart timeline bar?
for reference please look years into the attached snap.[timeline_spac][1]
[1]: https://i.stack.imgur.com/n1yvQ.jpg
You can use the SVGRenderer tool to render those labels.
Demo: https://jsfiddle.net/BlackLabel/d60o35sL/
events: {
render() {
const chart = this;
chart.series[0].points.forEach(point => {
const x = point.plotX;
const y = point.plotY + chart.plotTop + point.graphic.height / 3;
if (point.customLabel) {
point.customLabel.destroy()
}
point.customLabel = chart.renderer
.text(
'test',
x,
y
)
.css({
color: 'white',
})
.add();
point.customLabel.toFront()
})
}
}
},
API: https://api.highcharts.com/class-reference/Highcharts.SVGRenderer#text
API: https://api.highcharts.com/highcharts/chart.events.render
I have an indicator which is a function of x, y.
How can i draw this indicator on top of chart?
Lets say here https://jsfiddle.net/yv3pehj8/
plotOptions: {
series: {
point: {
events: {
mouseOver: function () {
var chart = this.series.chart;
if (!chart.lbl) {
chart.lbl = chart.renderer.label('')
.attr({
padding: 10,
r: 10,
fill: Highcharts.getOptions().colors[1]
})
.css({
color: '#FFFFFF'
})
.add();
}
chart.lbl
.show()
.attr({
text: 'x: ' + this.x + ', y: ' + this.y
});
}
}
}
Instead of printing x,y value on the corner, I wanted to draw a line at an angle of 45 degrees to the hover point ranging from [x-10, x+10]. How could I do it in the most performant way?
TIA
EDIT
Many thanks for the solution. To make it a bit more complicated. i want to reach the following effect as seen in this image []
i want to draw the blue/red curves as you see in the image on mouse hover.. Would you happen to have a solution for this #ppotaczek??
Instead of rendering the label, you can render a path:
plotOptions: {
series: {
point: {
events: {
mouseOver: function() {
var chart = this.series.chart,
path = [
'M',
chart.plotLeft + this.plotX - 10,
chart.plotTop + this.plotY - 10,
'L',
chart.plotLeft + this.plotX + 10,
chart.plotTop + this.plotY + 10
];
if (!chart.lbl) {
chart.lbl = chart.renderer.path(path)
.attr({
'stroke-width': 2,
stroke: 'red'
})
.add();
}
chart.lbl
.show()
.attr({
d: path
});
}
}
},
...
}
}
Live demo: https://jsfiddle.net/BlackLabel/zo46fuxb/
API Reference: https://api.highcharts.com/class-reference/Highcharts.SVGRenderer#path
Now I'm moving my project from openlayers 2 to openlayers 3. Unfortunately I can't find how to show title (tooltip) for feature. In OL2 there was a style named graphicTitle.
Could you give me advice how to implement tooltip on OL3?
This is example from ol3 developers.
jsfiddle.net/uarf1888/
var tooltip = document.getElementById('tooltip');
var overlay = new ol.Overlay({
element: tooltip,
offset: [10, 0],
positioning: 'bottom-left'
});
map.addOverlay(overlay);
function displayTooltip(evt) {
var pixel = evt.pixel;
var feature = map.forEachFeatureAtPixel(pixel, function(feature) {
return feature;
});
tooltip.style.display = feature ? '' : 'none';
if (feature) {
overlay.setPosition(evt.coordinate);
tooltip.innerHTML = feature.get('name');
}
};
map.on('pointermove', displayTooltip);
Here's the Icon Symobolizer example from the openlayers website. It shows how to have a popup when you click on an icon feature. The same principle applies to any kind of feature. This is what I used as an example when I did mine.
This is a basic example using the ol library. The most important is to define the overlay object. We will need an element to append the text we want to display in the tooltip, a position to show the tooltip and the offset (x and y) where the tooltip will start.
const tooltip = document.getElementById('tooltip');
const overlay = new ol.Overlay({
element: tooltip,
offset: [10, 0],
positioning: 'bottom-left'
});
map.addOverlay(overlay);
Now, we need to dynamically update the innerHTML of the tooltip.
function displayTooltip(evt) {
const pixel = evt.pixel;
const feature = map.forEachFeatureAtPixel(pixel, function(feature) {
return feature;
});
tooltip.style.display = feature ? '' : 'none';
if (feature) {
overlay.setPosition(evt.coordinate);
tooltip.innerHTML = feature.get('name');
}
};
map.on('pointermove', displayTooltip);
let styleCache = {};
const styleFunction = function(feature, resolution) {
// 2012_Earthquakes_Mag5.kml stores the magnitude of each earthquake in a
// standards-violating <magnitude> tag in each Placemark. We extract it from
// the Placemark's name instead.
const name = feature.get('name');
const magnitude = parseFloat(name.substr(2));
const radius = 5 + 20 * (magnitude - 5);
let style = styleCache[radius];
if (!style) {
style = [new ol.style.Style({
image: new ol.style.Circle({
radius: radius,
fill: new ol.style.Fill({
color: 'rgba(255, 153, 0, 0.4)'
}),
stroke: new ol.style.Stroke({
color: 'rgba(255, 204, 0, 0.2)',
width: 1
})
})
})];
styleCache[radius] = style;
}
return style;
};
const vector = new ol.layer.Vector({
source: new ol.source.Vector({
url: 'https://gist.githubusercontent.com/anonymous/5f4202f2d49d8574fd3c/raw/2c7ee40e3f4ad9dd4c8d9fb31ec53aa07e3865a9/earthquakes.kml',
format: new ol.format.KML({
extractStyles: false
})
}),
style: styleFunction
});
const raster = new ol.layer.Tile({
source: new ol.source.Stamen({
layer: 'toner'
})
});
const map = new ol.Map({
layers: [raster, vector],
target: 'map',
view: new ol.View({
center: [0, 0],
zoom: 2
})
});
const tooltip = document.getElementById('tooltip');
const overlay = new ol.Overlay({
element: tooltip,
offset: [10, 0],
positioning: 'bottom-left'
});
map.addOverlay(overlay);
function displayTooltip(evt) {
const pixel = evt.pixel;
const feature = map.forEachFeatureAtPixel(pixel, function(feature) {
return feature;
});
tooltip.style.display = feature ? '' : 'none';
if (feature) {
overlay.setPosition(evt.coordinate);
tooltip.innerHTML = feature.get('name');
}
};
map.on('pointermove', displayTooltip);
#map {
position: relative;
height: 100vh;
width: 100vw;
}
.tooltip {
position: relative;
padding: 3px;
background: rgba(0, 0, 0, .7);
color: white;
opacity: 1;
white-space: nowrap;
font: 10pt sans-serif;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="map" class="map">
<div id="tooltip" class="tooltip"></div>
</div>
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io#master/en/v6.8.1/build/ol.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io#master/en/v6.8.1/css/ol.css">