I would like to add toggle visibility on click to a node in a Highcharts Organisation chart (Highcharts).
Is there a built in support for this?
This feature is not available from regular Highcharts API, however I created an example/guideline how to implement it by yourself.
First, you need to hide points after initial load in the events.load callback:
Code:
events: {
load() {
//hide nodes after initial load
let chart = this,
nodes = chart.series[0].nodeLookup;
for (let i in nodes) {
if (nodes[i].column > 2) {
nodes[i].graphic.hide();
nodes[i].dataLabel.hide();
nodes[i].linksTo[0].graphic.hide();
nodes[i].visible = false;
}
}
}
}
API to load event: https://api.highcharts.com/highcharts/chart.events.load
Next, you need to implement logic to show wanted nodes after click event on point. You can improve this logic and code as an independent function which will be trigger inside the point.events.click callback.
Code:
events: {
click() {
//show nodes
let series = this.series,
nodes = series.nodeLookup;
for (let i in nodes) {
if (nodes[i].linksTo.length && nodes[i].linksTo[0].from === "CEO") {
if (nodes[i].visible) {
nodes[i].graphic.hide()
nodes[i].dataLabel.hide();
nodes[i].visible = false;
} else {
nodes[i].graphic.show()
nodes[i].dataLabel.show();
nodes[i].visible = true;
}
}
}
this.linksFrom.forEach(link => {
if (link.graphic.visibility == "visible") {
link.graphic.hide()
} else {
link.graphic.show()
}
})
}
}
API: https://api.highcharts.com/highcharts/series.organization.data.events.click
Demo: https://jsfiddle.net/BlackLabel/b5nxv6k9/ - click on the CEO point to see how it work.
Related
I have an application with a NavigationBar that includes three destinations. It is configured so that two of the destinations point to a seperate nested NavGraph, namely IssuesGraph and InstructionsGraph. While navigating within the nested graphs and then switching between the two destinations, the respective stack for each nested graph is saved as I wish.
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
bottomNavigationItems.forEachIndexed { index, item ->
NavigationBarItem(
icon = { Icon(item.imageResource, stringResource(id = item.titleResource)) },
label = { Text(stringResource(id = item.titleResource)) },
selected = currentDestination?.hierarchy?.any { it.route == item.route } == true,
onClick = {
navController.navigate(item.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
However, I now need to reset the complete navigation graph, so I want the App to display the first destination within the IssuesGraph, and if I navigate to the InstructionsGraph, it should show its first destination, too.
My NavHost looks like this:
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination,
) {
issueGraph(navController, issuesVM)
instructionsGraph(navController, instructionsVM)
composable(Screen.SettingsScreen.route) {
SettingsScreen(
onSave = {
// Here, I want to trigger a reset of
// the backstacks of issuesGraph and instructionsGraph
// and navigate to the first screen of the issuesGraph
navController.navigate(Screen.IssuesGraph.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = false
}
}
}
)
}
}
The seperate NavGraphs are built using the NavGraphBuilder extension function:
fun NavGraphBuilder.issueGraph(navController: NavController, issuesVM: IssuesVM) {
navigation(startDestination = Screen.IssuesHome.route, Screen.IssuesGraph.route) {
composable(Screen.IssuesHome.route) {
//...
}
}
}
The problem is, that calls like this
navController.navigate(Screen.IssuesGraph.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = false
}
}
only clear the back stack for the nested NavGraph the the goal route Screen.IssuesGraph.route resides in. So in my case, the IssuesGraph is reset, but the InstructionsGraph isn't. When I navigate there, it still shows its previous backstack.
I already stumbled across the setGraph documentation, but couldn't understand how I could use it in my case.
Thanks for any efforts!
I need to make an API call when clicking on a bar element. For example, look at this jsfiddle.
https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/plotoptions/series-point-events-click-column/
See how the x axis has the 12 months? Let's say that when clicking on the bar for "April," I want to make an API call in order to get sales data for the month of April and display that on the graph. I can't find a way to do this, because this click function...
plotOptions: {
series: {
events: {
click: function (event) {
//code goes here
}
}
}
}
... can only access items inside of the chart. I need to make an outside call to the database when clicking on a bar. Anything I can do? Thanks.
I tried this
document.querySelector('.rect.highcharts-point').addEventListener('click', e => {
//code goes here
});
It didn't work at all which is confusing, because this method clearly works when referencing the chart as whole, but doesn't work for just a bar element as you can see it working for the entire chart in this jsfiddle.
https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/chart/events-container/
Three ways how you can achieve that:
From the callback to the built-in point click() event:
plotOptions: {
series: {
point: {
events: {
click() {
let point = this;
getPoint(point)
}
}
}
}
},
.
function getPoint(p) {
console.log('From the callback: ', p)
}
From the div container:
document.querySelector('.chart-container').addEventListener('click', e => {
if (e.point != undefined) {
console.log('From the container: ', e.point)
}
});
Looping through the rects:
function getPointLoop(p) {
p.addEventListener('click', e => {
console.log('From the loop:', e.target.point)
})
}
let points = Array.from(document.querySelectorAll('rect.highcharts-point'))
for (i = 0; i < points.length; i++) {
getPointLoop(points[i])
}
Demo:
https://jsfiddle.net/BlackLabel/9bos6eu8/
I'm using Highcharts to create organization charts that where each node can be collapsed when clicked as in provided example : http://jsfiddle.net/vegaelce/83uktasc/
It works well but it would be better if it is possible to "redraw" the chart once a node is fold/unfold (to optimise the space left and realign the nodes).
I tried without success :
chart.redraw();
Have you any idea how to make this ?
Thanks in advance
You need to set new data to implement the required feature. Nodes are hidden on svg level and they are not ignored in the redraw.
function getData(to) {
const data = [
...
];
if (to) {
const filters = [to];
return data.filter(el => {
const matched = filters.find(filter => filter === el[0]);
if (matched) {
filters.push(el[1]);
}
return !matched;
});
}
return data;
}
Highcharts.chart('container', {
...,
plotOptions: {
series: {
point: {
events: {
click: function() {
if (this.linksFrom.length) {
this.series.setData(getData(this.title || this.name));
} else {
this.series.setData(getData());
}
}
}
}
}
}
});
Live demo: http://jsfiddle.net/BlackLabel/5njf0cwq/
API Reference: https://api.highcharts.com/class-reference/Highcharts.Series#setData
Is there currently a way to have a legend item save its state on refresh? For example, I have a graph that has 3 lines: a, b, and c. I toggle off line c so it doesn't show on the graph. Currently when refreshing the page, a, b, and c will show on the graph again. Is there a way to have it remember to turn off c?
Use localStorage feature to store current state of the chart series visibility. You need to change the state of your local storage position when clicking on legend item, so just use the plotOptions.series.events.legendItemClick handler, and then on every chart load event, set your series visibility basing on the stored information.
chart: {
events: {
load: function() {
var series = this.series;
// If there is no legend items visibility flags saved in local storage, save them.
if (!localStorage.legendItems) {
let legend = []
series.forEach(function(series) {
legend.push(series.visible)
})
localStorage.legendItems = legend.toString()
//
} else {
let legend = localStorage.legendItems.split(',')
let legendBooleans = []
legend.forEach(function(elem) {
var isTrueSet = (elem === 'true')
legendBooleans.push(isTrueSet)
})
legendBooleans.forEach(function(state, i) {
series[i].update({
visible: state
})
})
}
}
}
},
plotOptions: {
series: {
events: {
legendItemClick: function() {
var series = this.chart.series
var index = this.index
var legend = localStorage.legendItems.split(',')
var legendBooleans = []
legend.forEach(function(elem) {
var isTrueSet = (elem === 'true')
legendBooleans.push(isTrueSet)
})
// toggle series visibility flag and override it in local storage
legendBooleans[index] = !legendBooleans[index]
localStorage.legendItems = legendBooleans.toString()
}
}
}
},
If you have any questions, just ask.
Live example: https://jsfiddle.net/os961gke/
API Reference: https://api.highcharts.com/highcharts/plotOptions.series.events.legendItemClick
https://api.highcharts.com/highcharts/chart.events.load
To my great surprise Lightbox2(http://lokeshdhakar.com/projects/lightbox2/) does not support swipe gestures out of the box...
I was not able to find any add on in order to support this behavior. Anyone has any suggestions a side from changing the entire plugin? :)
To summary, you must hide the navigation buttons and implement swiping, moving and sliding effect on the image.
You will need :
jquery.event.move
jquery.event.swipe
jquery ui slide effect, you can package it in the jquery ui download builder
maybe there's a simplest way to get/implement all of these 3 small dependencies... but that way works for me.
in the lightbox script, add a new LightboxOptions enableSwipeOnTouchDevices and set it to true :
this.enableSwipeOnTouchDevices = true;
add the following blocks after the this.$lightbox.find('.lb-next').on('click'... block to create the swiping events:
this.$lightbox.find('.lb-image').on("swiperight",function() {
$('.lb-image').effect("slide", { "direction" : "right", "mode" : "hide"} ,function(){
if (self.currentImageIndex === 0) {
self.changeImage(self.album.length - 1);
} else {
self.changeImage(self.currentImageIndex - 1);
}
})
});
this.$lightbox.find('.lb-image').on("swipeleft",function() {
$('.lb-image').effect("slide", { "direction" : "left", "mode" : "hide"} ,function(){
if (self.currentImageIndex === self.album.length - 1) {
self.changeImage(0);
} else {
self.changeImage(self.currentImageIndex + 1);
}
})
});
and rewrite the updateNav function like this to hide the navigation buttons:
Lightbox.prototype.updateNav = function() {
// Check to see if the browser supports touch events. If so, we take the conservative approach
// and assume that mouse hover events are not supported and always show prev/next navigation
// arrows in image sets.
var alwaysShowNav = false;
var enableSwipe = false;
try {
document.createEvent("TouchEvent");
alwaysShowNav = (this.options.alwaysShowNavOnTouchDevices)? true: false;
enableSwipe = (this.options.enableSwipeOnTouchDevices)? true: false;
} catch (e) {}
//if swiping is enable, hide the two navigation buttons
if (! enableSwipe) {
this.$lightbox.find('.lb-nav').show();
if (this.album.length > 1) {
if (this.options.wrapAround) {
if (alwaysShowNav) {
this.$lightbox.find('.lb-prev, .lb-next').css('opacity', '1');
}
this.$lightbox.find('.lb-prev, .lb-next').show();
} else {
if (this.currentImageIndex > 0) {
this.$lightbox.find('.lb-prev').show();
if (alwaysShowNav) {
this.$lightbox.find('.lb-prev').css('opacity', '1');
}
}
if (this.currentImageIndex < this.album.length - 1) {
this.$lightbox.find('.lb-next').show();
if (alwaysShowNav) {
this.$lightbox.find('.lb-next').css('opacity', '1');
}
}
}
}
}
};
I've used jquery mobile to detect swipeleft and swiperight. Then bind them to click .lb-next and .lb-prev. It's working now.
Here is my codepen.
PEC's solution worked for me with one modification on a Jekyll site.
Instead of:
this.enableSwipeOnTouchDevices = true;
We added this to /_includes/scripts.html after the dependencies and lightbox.js:
<script>
lightbox.option({
'enableSwipeOnTouchDevices': true,
})
</script>
The PEC solution is good, but it doesn't work anymore with the current version of lightbox (2.11.2). The effect() method doesn't exists anymore.
So the swiping methods should be updated:
this.$lightbox.find('.lb-image').on("swiperight",function() {
if (self.currentImageIndex === 0) {
self.changeImage(self.album.length - 1);
} else {
self.changeImage(self.currentImageIndex - 1);
}
return false;
});
this.$lightbox.find('.lb-image').on("swipeleft",function() {
if (self.currentImageIndex === self.album.length - 1) {
self.changeImage(0);
} else {
self.changeImage(self.currentImageIndex + 1);
}
return false;
});
Less fancy, but shorter and works.
In short: 'catch' swipe gesture and then trigger 'click' on next/prev button based on swipe direction.
let touchstartX = 0;
let touchendX = 0;
function handleGesture() {
if (touchendX < touchstartX) $(".lb-prev").trigger("click");
if (touchendX > touchstartX) $(".lb-next").trigger("click");
}
$(document).on("touchstart", ".lb-nav", e => {
touchstartX = e.changedTouches[0].screenX;
});
$(document).on("touchend", ".lb-nav", e => {
touchendX = e.changedTouches[0].screenX;
handleGesture();
});