How to detect click on an axis label with chart.js
In the example bellow, I can only detect click on the graph itself
https://stackblitz.com/edit/ng2-charts-bar-template-qchyz6
You will need to implement a custom plugin that can listen to all the events of the canvas:
const findLabel = (labels, evt) => {
let found = false;
let res = null;
labels.forEach(l => {
l.labels.forEach((label, index) => {
if (evt.x > label.x && evt.x < label.x2 && evt.y > label.y && evt.y < label.y2) {
res = {
label: label.label,
index
};
found = true;
}
});
});
return [found, res];
};
const getLabelHitboxes = (scales) => (Object.values(scales).map((s) => ({
scaleId: s.id,
labels: s._labelItems.map((e, i) => ({
x: e.translation[0] - s._labelSizes.widths[i],
x2: e.translation[0] + s._labelSizes.widths[i] / 2,
y: e.translation[1] - s._labelSizes.heights[i] / 2,
y2: e.translation[1] + s._labelSizes.heights[i] / 2,
label: e.label,
index: i
}))
})));
const plugin = {
id: 'customHover',
afterEvent: (chart, event, opts) => {
const evt = event.event;
if (evt.type !== 'click') {
return;
}
const [found, labelInfo] = findLabel(getLabelHitboxes(chart.scales), evt);
if (found) {
console.log(labelInfo);
}
}
}
Chart.register(plugin);
const options = {
type: 'line',
data: {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
borderColor: 'pink'
},
{
label: '# of Points',
data: [7, 11, 5, 8, 3, 7],
borderColor: 'orange'
}
]
},
options: {}
}
const ctx = document.getElementById('chartJSContainer').getContext('2d');
new Chart(ctx, options);
<body>
<canvas id="chartJSContainer" width="600" height="400"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.js"></script>
</body>
Adpatation for Angular with ng2-charts (chart-js v3.7.1)
Just use Chart.register
i.e. put the following functiion into component ngOnInit()
RegisterPlugin() {
Chart.register(
{
id: 'yAxisCustomClick',
afterEvent: (chart: Chart<'bar'>, event: {
event: ChartEvent;
replay: boolean;
changed?: boolean | undefined;
cancelable: false;
inChartArea: boolean
}) => {
const evt = event.event;
if (evt.type === 'click' && evt.x! < Object.values(chart.scales).filter(s => s.id === 'x')[0].getBasePixel()) {
const labelIndex = Object.values(chart.scales).filter(s => s.id === 'y')[0].getValueForPixel(evt.y!);
const label = Object.values(chart.scales).filter(s => s.id === 'y')[0].getTicks()[labelIndex!]?.label;
if (label) {
console.log('Do the stuff for', label)
}
}
}
}
);
}
Example in stackblitz udpated
https://stackblitz.com/edit/ng2-charts-bar-template-qchyz6
Related
I'm trying to use a useContext hook inside a react-leaflet controlComponent but I have an error when my context fires the update function.
I use a react-leaflet controlComponent because of leaflet routing machine. I think the code + the error are better than word:
MainBoard.tsx
export const CartographyContext: React.Context<CartographyContextType> = React.createContext<CartographyContextType>({ positions: [] });
...
const routeSummaryValueContext = React.useMemo(
() => ({ routeSummary, setRouteSummary }),
[routeSummary]
);
const elevationProfileValueContext = React.useMemo(
() => ({ elevationProfile, setElevationProfile }),
[elevationProfile]
);
........
<CartographyContext.Provider value={{ positions, elevationProfileValueContext, routeSummaryValueContext, positionsValueContext, addPosition, changePosition }}>
.........
<RoutingMachine
orsOptions={{
....
}} />
..........
</CartographyContext.Provider>
RoutingMachine.tsx:
const CreateRoutineMachineLayer = (props: any) => {
const geoService = new GeoLocalisationService();
const cartographyContext: CartographyContextType = React.useContext<CartographyContextType>(CartographyContext);
const [routes, setRoutes] = React.useState<any[]>();
React.useEffect(() => {
if (routes) {
//The line which cause the error
cartographyContext.elevationProfileValueContext.setElevationProfile(geoService.getElevationProfile(decodePolyline(routes[0].geometry, true)));
const summary: RouteSummary = {
ascent: routes[0].routeSummary.ascent,
descent: routes[0].routeSummary.descent,
distance: routes[0].routeSummary.distance,
estimatedDuration: routes[0].routeSummary.duration
}
cartographyContext.routeSummaryValueContext.setRouteSummary(summary);
}
}, [routes]);
const { orsOptions } = props;
const instance = L.Routing.control({
router: new OpenRouteRouter(orsOptions),
lineOptions: {
styles: [{ color: "#3933ff", weight: 4 }],
extendToWaypoints: true,
missingRouteTolerance: 0
},
routeWhileDragging: true,
autoRoute: true,
geocoder: new geocoder.Geocoder(),
}).on('routesfound', (e) => {
setRoutes(e.routes);
});
useMapEvents({
click: (e: L.LeafletMouseEvent) => {
if (instance.getWaypoints().length === 2 && instance.getWaypoints()[0].latLng == null) {
instance.spliceWaypoints(0, 1, new L.Routing.Waypoint(e.latlng, null, {}));
} else if (instance.getWaypoints().length === 2 && instance.getWaypoints()[1].latLng == null) {
instance.spliceWaypoints(1, 1, new L.Routing.Waypoint(e.latlng, null, {}));
} else {
instance.spliceWaypoints(instance.getWaypoints().length, 0, new L.Routing.Waypoint(e.latlng, null, {}));
}
}
});
return instance;
};
const RoutingMachine = createControlComponent(CreateRoutineMachineLayer);
error :
g: React has detected a change in the order of Hooks called by ForwardRef(LeafComponent). This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
Previous render Next render
------------------------------------------------------
1. useContext useContext
2. useRef useRef
3. useContext useRef
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..............
Uncaught Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
I clearly doing something wrong here but I haven't found yet.
Thank you
Kind regards
Ok I found the good implementation :
const RoutingMachine: React.FC<RoutingMachineProps> = (props) => {
//const RoutineMachine = (props: any) => {
const geoService = new GeoLocalisationService();
const cartographyContext: CartographyContextType = React.useContext<CartographyContextType>(CartographyContext);
const [instance, setInstance] = React.useState<any>();
const [alreadyDisplayed, setAlreadyDisplayed] = React.useState(false);
const { orsOptions } = props;
const map = useMap();
//const instance = L.Routing.control({
React.useEffect(() => {
const instance = L.Routing.control({
router: new OpenRouteRouter(orsOptions),
lineOptions: {
styles: [{ color: "#3933ff", weight: 4 }],
extendToWaypoints: true,
missingRouteTolerance: 0
},
routeWhileDragging: true,
autoRoute: true,
geocoder: (L.Control as any).Geocoder.google({
apiKey: GOOGLE.googleMapApiKey,
}),
}).on('routesfound', (e) => {
const routes = e.routes;
cartographyContext.setElevationProfile(geoService.getElevationProfile(decodePolyline(routes[0].geometry, true)));
const summary: RouteSummary = {
ascent: routes[0].routeSummary.ascent,
descent: routes[0].routeSummary.descent,
distance: routes[0].routeSummary.distance,
estimatedDuration: routes[0].routeSummary.duration
}
cartographyContext.setRouteSummary(summary);
})
setInstance(instance);
instance.addTo(map);
}, []);
useMapEvents({
click: (e: L.LeafletMouseEvent) => {
if (instance) {
if (instance.getWaypoints().length === 2 && instance.getWaypoints()[0].latLng == null) {
instance.spliceWaypoints(0, 1, new L.Routing.Waypoint(e.latlng, null, {}));
} else if (instance.getWaypoints().length === 2 && instance.getWaypoints()[1].latLng == null) {
instance.spliceWaypoints(1, 1, new L.Routing.Waypoint(e.latlng, null, {}));
} else {
instance.spliceWaypoints(instance.getWaypoints().length, 0, new L.Routing.Waypoint(e.latlng, null, {}));
}
}
}
});
return null;
};
export default RoutingMachine;
I am using Mapbox GL JS to capture frame by frame video of the animation of a geoJson (similar to what is described here: https://docs.mapbox.com/mapbox-gl-js/example/animate-a-line/).
The strategy for encoding mapbox animations into mp4 here are described here:
https://github.com/mapbox/mapbox-gl-js/issues/5297 and https://github.com/mapbox/mapbox-gl-js/pull/10172 .
I would like to show the distance the polyline has covered as I draw each frame. I need the distance to be in the GL itself (as opposed to, for example, an HTML element on top of the canvas), since that's where I'm capturing the video from.
Can someone help describe a performant strategy for doing this to me?
Aside from tearing apart the Mapbox GL JS, why not simply try drawing directly onto the mapbox canvas?
In this example, there are two canvases, created identically.
With the second, there is another requestAnimationFrame loop that adds an overlay.
I've also shown how it can be recorded with MediaRecorder.
const canvas1 = document.getElementById('canvas1')
const canvas2 = document.getElementById('canvas2')
const video = document.getElementById('output')
const status = document.getElementById('status')
let x = 0 // Value to be displayed
const setupCanvas = (canvas) => {
canvas.height = 300
canvas.width = 300
const canvas1Ctx = canvas.getContext('2d')
canvas1Ctx.fillStyle = 'black'
canvas1Ctx.fillRect(300, 100, 100, 100)
const animateCanvas = () => {
x += 2;
if (x > 300) x = 10
canvas1Ctx.fillStyle = 'rgba(20,20,20,1)'
canvas1Ctx.fillRect(0, 0, canvas.width, canvas.height)
canvas1Ctx.beginPath()
canvas1Ctx.arc(x, 100, 20, 0, 2 * Math.PI)
canvas1Ctx.fillStyle = 'rgba(250,0,0,1)'
canvas1Ctx.fill()
requestAnimationFrame(animateCanvas)
}
animateCanvas()
}
const addOverlay = (canvas) => {
const canvasCtx = canvas2.getContext('2d')
function animateOverlay() {
canvasCtx.font = '48px serif'
canvasCtx.fillStyle = 'white'
canvasCtx.fillText(`X: ${x}`, 10, 50)
requestAnimationFrame(animateOverlay)
}
animateOverlay()
}
const initVideoCapture = () => {
var videoStream = canvas2.captureStream(30)
var mediaRecorder = new MediaRecorder(videoStream)
var chunks = []
mediaRecorder.ondataavailable = function(e) {
chunks.push(e.data)
}
mediaRecorder.onstop = function(e) {
var blob = new Blob(chunks, {
'type': 'video/mp4'
})
chunks = []
var videoURL = URL.createObjectURL(blob)
video.src = videoURL
}
mediaRecorder.ondataavailable = function(e) {
chunks.push(e.data)
}
mediaRecorder.start()
status.textContent = 'Recording...'
setTimeout(function() {
mediaRecorder.stop()
status.textContent = 'Complete'
}, 5000)
}
setupCanvas(canvas1)
setupCanvas(canvas2)
addOverlay(canvas2)
initVideoCapture()
<canvas id="canvas1"></canvas>
<canvas id="canvas2"></canvas>
<video id="output" autoplay controls></video>
<p>Status: <span id="status">Loading</span></p>
Try this :
// show the distance a polyline has covered while animating it in mapbox gl js
var map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v9',
center: [-74.5, 40],
zoom: 9,
})
var geojson = {
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: [
[-74.5, 40],
[-74.45, 40.7],
[-74.36, 40.8],
[-74.36, 41.2],
],
},
}
map.on('load', function () {
map.addLayer({
id: 'route',
type: 'line',
source: {
type: 'geojson',
data: geojson,
},
layout: {
'line-join': 'round',
'line-cap': 'round',
},
paint: {
'line-color': '#888',
'line-width': 8,
},
})
var lineDistance = turf.lineDistance(geojson, 'kilometers')
var arc = []
var maxTravelTime = 0
for (var i = 0; i < lineDistance; i += lineDistance / 200) {
var segment = turf.along(geojson, i, 'kilometers')
arc.push(segment.geometry.coordinates)
}
map.addLayer({
id: 'point',
type: 'symbol',
source: {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {},
geometry: {
type: 'Point',
coordinates: arc[0],
},
},
],
},
},
layout: {
'icon-image': 'car',
'icon-rotate': ['get', 'bearing'],
'icon-rotation-alignment': 'map',
'icon-allow-overlap': true,
'icon-ignore-placement': true,
},
})
function animate() {
map.getSource('point').setData(geojson)
if (maxTravelTime < lineDistance) {
maxTravelTime += lineDistance / 200
} else {
maxTravelTime = 0
}
requestAnimationFrame(animate)
}
animate()
})
I was trying to adapt the spline animation for time series (https://www.highcharts.com/demo/dynamic-update) for multiple series.
I modified the example here https://jsfiddle.net/2wj3fquL/
Highcharts.chart('container', {
chart: {
type: 'spline',
animation: Highcharts.svg, // don't animate in old IE
marginRight: 10,
events: {
load: function () {
// set up the updating of the chart each second
var series = this.series;
setInterval(function () {
var x = (new Date()).getTime(), // current time
y = Math.random();
series[0].addPoint([x, y], true, true);
y = Math.random();
series[1].addPoint([x, y], true, true);
}, 1000);
}
}
},
time: {
useUTC: false
},
title: {
text: 'Live random data'
},
accessibility: {
announceNewData: {
enabled: true,
minAnnounceInterval: 15000,
announcementFormatter: function (allSeries, newSeries, newPoint) {
if (newPoint) {
return 'New point added. Value: ' + newPoint.y;
}
return false;
}
}
},
xAxis: {
type: 'datetime',
tickPixelInterval: 150
},
yAxis: {
title: {
text: 'Value'
},
plotLines: [{
value: 0,
width: 1,
color: '#808080'
}]
},
tooltip: {
headerFormat: '<b>{series.name}</b><br/>',
pointFormat: '{point.x:%Y-%m-%d %H:%M:%S}<br/>{point.y:.2f}'
},
legend: {
enabled: false
},
exporting: {
enabled: false
},
series: [{
name: 'Random data',
data: (function () {
// generate an array of random data
var data = [],
time = (new Date()).getTime(),
i;
for (i = -19; i <= 0; i += 1) {
data.push({
x: time + i * 1000,
y: Math.random()
});
}
return data;
}())
}, {
name: 'other data',
data: (function () {
// generate an array of random data
var data = [],
time = (new Date()).getTime(),
i;
for (i = -19; i <= 0; i += 1) {
data.push({
x: time + i * 1000,
y: Math.random()
});
}
return data;
}())
}]
});
Unfortunately the effect is odd since one series moves smooth while the other doesn't ...
How could I solve this problem?
Thanks
That is because the chart is redrawn twice. Disable redraw in the first addPoint method call.
events: {
load: function() {
...
setInterval(function() {
...
series[0].addPoint([x, y], false, true);
series[1].addPoint([x, y], true, true);
}, 1000);
}
}
Live demo: https://jsfiddle.net/BlackLabel/0n2yw57m/
API Reference: https://api.highcharts.com/class-reference/Highcharts.Series#addPoint
Is there a way to disable the keyboard events on Kendo UI Slider? Basically, I want to prevent changing the value of the slider when pressing left and right arrow keys. Is this possible at all?
Please note that slider is being dynamically inserted into the DOM as part of a KO custom binding handler.
ko.bindingHandlers.tone = {
init: function(element, valueAccessor) {
if (valueAccessor().settingToneEnabled) {
var $el = $(element);
var tag = '<span class="dropdown mrgn-tp-md"><ul class="dropdown-menu dropdown-menu-right text-center pddng-sm" aria-labelledby="tonedropdownMenu"><li class="pddng-lft-md pddng-rght-sm"><span id="tone-slider" title="tone"></span></li><li class="pddng-rght-sm"><i class="icon icon-delete"></i> ' + i18n['ps-deleteArticleToneLabel'] + '</li></ul></span>';
$(tag).appendTo($el);
var $slider = $('#tone-slider', $el);
var $delLink = $('a.del', $el);
var $dropdown = $('span.dropdown', $el);
$('a.dropdown-toggle', $dropdown).on('click', function() {
$('.dropdown-menu', $dropdown).toggle();
});
$slider.kendoSlider({
change: function(e) {
var va = valueAccessor();
va.value(e.value);
if ($.isFunction(va.handleUserInput)) {
va.handleUserInput();
}
},
showButtons: false,
min: -1,
max: 1,
smallStep: 1,
value: valueAccessor().value() || 0,
tickPlacement: 'none',
tooltip: {
enabled: false
}
});
$('.k-draghandle', $el).off('keydown');
$delLink.on('click', function(e) {
e.preventDefault();
if ($delLink.attr('disabled')) {
return;
}
var va = valueAccessor();
va.value(null);
if ($.isFunction(va.handleUserInput)) {
va.handleUserInput();
}
});
$el.data('slider', $slider.data("kendoSlider"));
$el.data('deleteButton', $delLink);
$el.data('dropdown', $dropdown);
} else {
$('<span href="" data-tone></span>').appendTo(element);
}
},
update: function(element, valueAccessor) {
var toneValues = {
'1': {
name: i18n['ps-tonePositive'],
val: 1,
css: 'icon-tone-positive'
},
'0': {
name: i18n['ps-toneNeutral'],
val: 0,
css: 'icon-tone-neutral'
},
'-1': {
name: i18n['ps-toneNegative'],
val: 0,
css: 'icon-tone-negative'
},
};
var $tone = $('*[data-tone]', element);
var val = valueAccessor().value() || 0;
var tone = toneValues[val.toString()] || toneValues['0'];
$tone.removeClass()
.addClass('icon').addClass(tone.css)
.attr('title', tone.name);
if (valueAccessor().settingToneEnabled) {
$('#tone-slider', element).data("kendoSlider").value(val);
}
},
};
You can try to remove the keydown handler.
See demo.
I am having trouble with an error that seems obvious but I've tried everything. I must be missing something. I'm running a rails app with Angularjs and angular-leaflet-directives. I am trying to add markers to a specific group. No matter what I do, I keep getting this error:
[AngularJS - Leaflet] A marker can only be added to a layer of type "group"
I know that this error comes up when the overlays type isn't group. The issue is that in my case, it is!
EDIT: here is a plunkr where I recreated the bug: http://plnkr.co/edit/DLCN5RYVr0BheYzTuqkQ?p=preview
Here is my code:
assets/javascript/angular/services/service.js
app.factory('Markers', ["$http", "$q", function($http, $q) {
var Markers = []
var events_markers = []
var stories_markers = []
var defaultIcon = L.icon({
iconUrl: 'assets/dot-grey.png',
iconSize: [15, 15],
iconAnchor: [15, 15],
popupAnchor: [-7, -20]
})
// Getting events + stories from rails API
var event_data = $http.get("/api/v1/events.json"),
story_data = $http.get("/api/v1/stories.json")
// Setting event markers and stories markers
$q.all([event_data, story_data]).then(function(results) {
var data_stories = results[1].data.stories
var data_events = results[0].data.event
for (i=0 ; i < data_stories.length; i++){
for (j=0; j < data_stories[i].locations.length; j++){
var lat = data_stories[i].locations[j].latitude
var lng = data_stories[i].locations[j].longitude
var title = data_stories[i].title
var layer = "stories"
Markers.push({layer: layer, lat:lat, lng:lng, message: title, icon: defaultIcon})
}
}
for (e=0 ; e < data_events.length; e++){
if (data_events[e].latitude != null){
var lat = data_events[e].latitude
var lng = data_events[e].longitude
var title = data_events[e].name
var layer = "events"
Markers.push({layer: layer, lat:lat, lng: lng, message: title, icon: defaultIcon})
}
}
return Markers
});
return {
markers: Markers
}
}]);
assets/javascript/angular/controllers/MapCtrl.js
app.controller("MapCtrl", ['$scope', "$timeout", "leafletData", "Markers", function($scope,
$timeout, leafletData, Markers ){
$scope.isVisible = true;
$scope.markers = Markers.markers;
var bounds = {
northEast:{
lat: 37.86862005954327,
lng: -122.12230682373048
},
southWest:{
lat: 37.68436373334184,
lng: -122.55901336669923
}
}
function setMap($scope, markers, bounds) {
angular.extend($scope, {
maxbounds: bounds,
defaults: {
scrollWheelZoom: false,
maxZoom: 14,
minZoom: 10
},
layers: {
baselayers: {
mapbox:{
name: 'Mapbox Litography',
url: 'http://api.tiles.mapbox.com/v4/{mapid}/{z}/{x}/{y}.png?access_token={apikey}',
type: 'xyz',
layerParams: {
apikey: 'pk.eyJ1IjoibGF1cmVuYmVuaWNob3UiLCJhIjoiQ1BlZGczRSJ9.EVMieITn7lHNi6Ato9wFwg',
mapid: 'laurenbenichou.jm96meb6'
}
}
}
},
overlays: {
stories: {
type: 'group', //Note here that the type is indeed 'group'
name: 'stories',
visible: true
},
events: {
type: 'group',
name: 'events',
visible: false
}
},
markers: markers
});
}
setMap($scope, $scope.markers, bounds)
angular.element(document).ready(function () {
function toggle(){$scope.isVisible = !$scope.isVisible;}
$timeout(toggle, 1000);
});
}])
assets/javascript/templates/map.html
<leaflet ng-hide="isVisible" defaults="defaults" markers="markers" layers="layers" height="100%" width="100%" maxbounds="maxbounds" ></leaflet>
assets/javascript/angular/app.js
var app = angular.module("litography", ['ngAnimate','ui.router','ngResource', 'templates', 'leaflet-directive', 'cn.offCanvas', 'ui.bootstrap', 'angular-flexslider'])
.config(['$stateProvider', '$urlRouterProvider', '$locationProvider', function($stateProvider, $urlRouterProvider, $locationProvider) {
/**
* Routes and States
*/
$stateProvider
.state('home', {
url: "/",
views:{
"splash": {
templateUrl: "splash.html",
controller: "SplashCtrl"
},
"map":{
templateUrl: "map.html",
controller: "MapCtrl",
resolve:{
Markers: function(Markers){
return Markers
}
}
},
"menu":{
templateUrl: "menu.html",
controller: "MenuCtrl"
}
}
})
// default fall back route
$urlRouterProvider.otherwise('/');
// enable HTML5 Mode for SEO
$locationProvider.html5Mode(true);
}]);
It can happen if you forgot to add the file: leaflet.markercluster.js
Ok, from the reduced plunker it was easier to play around and find the bug.
You need to put overlays inside (i.e. as a property of) layers:
layers: {
baselayers: {
mapbox: {
name: 'mapbox',
url: 'http://api.tiles.mapbox.com/v4/{mapid}/{z}/{x}/{y}.png?access_token={apikey}',
type: 'xyz',
layerParams: {
apikey: 'pk.eyJ1IjoibGF1cmVuYmVuaWNob3UiLCJhIjoiQ1BlZGczRSJ9.EVMieITn7lHNi6Ato9wFwg',
mapid: 'laurenbenichou.jm96meb6',
name: "stories"
}
}
},
overlays: {
stories: {
type: 'group',
name: 'stories',
visible: true,
},
events: {
type: 'group',
name: 'events',
visible: false
}
}
}