Gantt HighCharts callback

Hi everyone,

Im trying to get a response from a Highcharts component implemented through a dash_alternative_viz component.

When I drag the in/out points of a Gantt Chart, it does not trigger a change on the ‘options’ property of the Gantt HighChart.

When I query the current options property via State, the options property does not return the current options. (press TEST STATE! button)

I there a simple way around this?

import dash
import dash_alternative_viz as dav
import dash_html_components as html
from dash.dependencies import Input, Output, State
import datetime, time


external_scripts = [  
    "https://code.highcharts.com/gantt/highcharts-gantt.js",
    "https://code.highcharts.com/gantt/modules/gantt.js", 
    "https://code.highcharts.com/modules/draggable-points.js"
]
app = dash.Dash(
    __name__,
    external_scripts=external_scripts
    )

def epoch_js(date_time):
    return int(time.mktime(date_time.timetuple())) * 1000


app.layout = html.Div(
    [
        html.Button(id="my_button", children="TEST STATE!"),
        dav.HighChart(
            id="my_highchart",
            constructorType = 'ganttChart', 
            options = {
                'title': {
                    'text': 'Interactive Gantt Chart',
                    'align': 'left'
                    },
                'xAxis': {
                    'min': epoch_js(datetime.datetime(2023, 8, 17)),
                    'max': epoch_js(datetime.datetime(2023, 10, 30)),
                    },
                'plotOptions': {
                    'series': {
                        'animation': False, # Do not animate dependency connectors
                        'dragDrop': {
                            'draggableX': True,
                            'draggableY': True,
                            'dragMinY': 0,
                            'dragMaxY': 2,
                            'dragPrecisionX': 86400000, # 1day = 1000 * 60 * 60 * 24 (millis)
                            },
                        'dataLabels': {
                            'enabled': True,
                            'format': '{point.name}',
                            'style': {
                                'cursor': 'default',
                                'pointerEvents': 'none'
                                }
                            },
                        'allowPointSelect': True,
                        }
                    },
                'series': [
                    {
                        'name': 'Project 1',
                        'data': [
                            {
                                'name': 'Start prototype',
                                'start': epoch_js(datetime.datetime(2023, 9, 1)),
                                'end'  : epoch_js(datetime.datetime(2023, 10, 20)),
                                'completed': {
                                    'amount': 0.25
                                    }
                                }
                            ]
                        }
                    ],
                }
            ),
        html.Div(id='output'),
        ]
    )

@app.callback(
    Output("my_highchart", "options"),
    Input("my_button", "n_clicks"),
    State("my_highchart", "options"),
    prevent_initial_callback = True,
    ) 
def apply(n_clicks, options):
    print(options)
    return options


@app.callback(  # THIS DOES NOT TIGGER AS EXPECTED !!!
    Output("output", "children"),
    Input("my_highchart", "options"),
    ) 
def get_options(options):
    print(options)
    return f'{options}'

if __name__ == "__main__":
   app.run_server( port = 8050, debug=True)

Maybe I need to add some js in the assets directory. But I don’t know how to start.

Thanks in advance!

Hey @popo,

Here is an idea:

1- Redefine your HighChart in dav.py file in a way that it takes “option parameters” as an attribute.
2- Wrap your dav.HighChart component with a html.Div component
3- Adjust your respective @callback in a way that it outputs children of that html.Div component.
4- Define a new callback function something like that:

     if n_clicks==None or n_clicks % 2 == 0:
       return HighChart() with options A

     else:
       return HighChart() with options B
1 Like

Thank you @Berbere

I may not have grasped exactly what you meant.
I more or less tried to replicate your answer without wrapping the highcharts in a separate Div and/or function. Returning the options works. I even went so far to specify the ‘series’ key. (this might come in handy for Partial updates)
I added some instructions on how to interact with the Gantt to make it easier to understand.

SET (return) the options works fine.
GET the options directly in a State or after user interaction is the problem.

I cannot query the data. Neither directly (1st n_clicks callback) nor after the user interacted. (2nd callback)

Note that Callback 1 does trigger Callback 2 though…

import dash
import dash_alternative_viz as dav
import dash_html_components as html
from dash.dependencies import Input, Output, State
import datetime, time


external_scripts = [  
    "https://code.highcharts.com/gantt/highcharts-gantt.js",
    "https://code.highcharts.com/gantt/modules/gantt.js", 
    "https://code.highcharts.com/modules/draggable-points.js"
]
app = dash.Dash(
    __name__,
    external_scripts=external_scripts
    )

def epoch_js(date_time):
    return int(time.mktime(date_time.timetuple())) * 1000

planning_data_a = [
    {
        'name': 'Project 1',
        'data': [
            {
                'name': 'Start prototype A',
                'start': epoch_js(datetime.datetime(2023, 9, 1)),
                'end'  : epoch_js(datetime.datetime(2023, 10, 20)),
                'completed': {
                    'amount': 0.25
                    }
                }
            ]
        }
    ]

planning_data_b = [
    {
        'name': 'Project 2',
        'data': [
            {
                'name': 'Start prototype B',
                'start': epoch_js(datetime.datetime(2023, 9, 10)),
                'end'  : epoch_js(datetime.datetime(2023, 10, 10)),
                'completed': {
                    'amount': 0.5
                    }
                }
            ]
        }
    ]


app.layout = html.Div(
    [
        html.Button(id="my_button", children="SET OPTIONS PROPERTY"),
        html.Div("Drag the task or it's extremities to update OPTIONS PROPERTY (does not work)"),
        dav.HighChart(
            id="my_highchart",
            constructorType = 'ganttChart', 
            options = {
                'title': {
                    'text': 'Interactive Gantt Chart',
                    'align': 'left'
                    },
                'xAxis': {
                    'min': epoch_js(datetime.datetime(2023, 8, 17)),
                    'max': epoch_js(datetime.datetime(2023, 10, 30)),
                    },
                'plotOptions': {
                    'series': {
                        'animation': False, # Do not animate dependency connectors
                        'dragDrop': {
                            'draggableX': True,
                            'draggableY': True,
                            'dragMinY': 0,
                            'dragMaxY': 2,
                            'dragPrecisionX': 86400000, # 1day = 1000 * 60 * 60 * 24 (millis)
                            },
                        'dataLabels': {
                            'enabled': True,
                            'format': '{point.name}',
                            'style': {
                                'cursor': 'default',
                                'pointerEvents': 'none'
                                }
                            },
                        'allowPointSelect': True,
                        }
                    },
                'series': [],
                }
            ),
        html.Div(id='output'),
        ]
    )
# SETTING THE OPTIONS 
# WORKS FINE(even key specific)
@app.callback(
    [Output("my_button", "n_clicks"),
    Output("my_highchart", "options")],    
    Input("my_button", "n_clicks"),
    State("my_highchart", "options"),
    prevent_initial_callback = True,
    ) 
def apply(n_clicks, options):
    if n_clicks and n_clicks>2:
        n_clicks=1
        options['series'] = planning_data_b
    else:
        n_clicks=2       
        options['series'] = planning_data_a

    return n_clicks, options


# GETTING THE OPTIONS AFTER USER INTERACTION
# DOES NOT WORK
@app.callback(  # THIS DOES NOT TIGGER AS EXPECTED !!!
    Output("output", "children"),
    Input("my_highchart", "options"),
    ) 
def get_options(options):
    print(options)
    return f'{options}'

if __name__ == "__main__":
   app.run_server( port = 8050, debug=False)


Could you please change your if statement something like below without updating and outputting the n_clicks. I feel like something fishy going on over there… just a guess tho. I will do my best to check it again more throughly tomorrow…

I reset the n_clicks (not very catholic indeed) just as a proof of concept. Your suggestion is of course better.
But this isn’t related to the issue at all.

The problem is that the dash_alternative_viz component doesn’t seem to trigger or update the options property as it should.

The aim is to trigger the 2nd callback when the user interacts with the Gantt Chart (not when the button is clicked).
To avoid the confusion, I removed the 1st callback.

import dash
import dash_alternative_viz as dav
import dash_html_components as html
from dash.dependencies import Input, Output, State
import datetime, time


external_scripts = [  
    "https://code.highcharts.com/gantt/highcharts-gantt.js",
    "https://code.highcharts.com/gantt/modules/gantt.js", 
    "https://code.highcharts.com/modules/draggable-points.js"
]
app = dash.Dash(
    __name__,
    external_scripts=external_scripts
    )

def epoch_js(date_time):
    return int(time.mktime(date_time.timetuple())) * 1000


app.layout = html.Div(
    [
        html.Div("Drag the task or it's extremities to update OPTIONS PROPERTY (does not work)"),
        dav.HighChart(
            id="my_highchart",
            constructorType = 'ganttChart', 
            options = {
                'title': {
                    'text': 'Interactive Gantt Chart',
                    'align': 'left'
                    },
                'xAxis': {
                    'min': epoch_js(datetime.datetime(2023, 8, 17)),
                    'max': epoch_js(datetime.datetime(2023, 10, 30)),
                    },
                'plotOptions': {
                    'series': {
                        'animation': False, # Do not animate dependency connectors
                        'dragDrop': {
                            'draggableX': True,
                            'draggableY': True,
                            'dragMinY': 0,
                            'dragMaxY': 2,
                            'dragPrecisionX': 86400000, # 1day = 1000 * 60 * 60 * 24 (millis)
                            },
                        'dataLabels': {
                            'enabled': True,
                            'format': '{point.name}',
                            'style': {
                                'cursor': 'default',
                                'pointerEvents': 'none'
                                }
                            },
                        'allowPointSelect': True,
                        }
                    },
                'series': [
                    {
                        'name': 'Project',
                        'data': [
                            {
                                'name': 'Start prototype',
                                'start': epoch_js(datetime.datetime(2023, 9, 10)),
                                'end'  : epoch_js(datetime.datetime(2023, 10, 10)),
                                'completed': {
                                    'amount': 0.5
                                    }
                                }
                            ]
                        }
                    ],
                }
            ),
        html.Div('This should update the options property data whenever the user interacts with the Gantt Chart'),
        html.Div(id='output'),
        ]
    )


# GETTING THE OPTIONS AFTER USER INTERACTION
# DOES NOT WORK
@app.callback(  # THIS DOES NOT TIGGER AS EXPECTED !!!
    Output("output", "children"),
    Input("my_highchart", "options"),
    prevent_initial_callback = True,
    ) 
def get_options(options):
    print(options)
    return f'{options}'

if __name__ == "__main__":
   app.run_server( port = 8050, debug=False)

Hey @popo again,

Could you please try to define your HighChart() function on your main python file instead of importing from dav and rerun your code? Lets see whether the problem is due to importing…

Cheers

@Berbere :
Ok, so I went to the HighChart.py definition source code on github
and just plain copied it in the main python file.

I would’ve never dared to do this myself… but it runs…
But it doesn’t solve the problem. the options property is not triggered when the user interacts with the Gantt

import dash
import dash_alternative_viz as dav
import dash_html_components as html
from dash.dependencies import Input, Output, State
import datetime, time

########################################################################################################
# FROM :  https://github.com/plotly/dash-alternative-viz/blob/master/dash_alternative_viz/HighChart.py
########################################################################################################
from dash.development.base_component import Component, _explicitize_args


class HighChart(Component):
    """A HighChart component.
HighChart renders Highcharts.js JSON

Keyword arguments:
- id (string; optional): The ID used to identify this component in Dash callbacks.
- constructorType (string; optional): 'chart', 'stockChart', 'mapChart', 'ganttChart'
- options (dict; optional): The highcharts chart description"""
    @_explicitize_args
    def __init__(self, id=Component.UNDEFINED, constructorType=Component.UNDEFINED, options=Component.UNDEFINED, **kwargs):
        self._prop_names = ['id', 'constructorType', 'options']
        self._type = 'HighChart'
        self._namespace = 'dash_alternative_viz'
        self._valid_wildcard_attributes =            []
        self.available_properties = ['id', 'constructorType', 'options']
        self.available_wildcard_properties =            []

        _explicit_args = kwargs.pop('_explicit_args')
        _locals = locals()
        _locals.update(kwargs)  # For wildcard attrs
        args = {k: _locals[k] for k in _explicit_args if k != 'children'}

        for k in []:
            if k not in args:
                raise TypeError(
                    'Required argument `' + k + '` was not specified.')
        super(HighChart, self).__init__(**args)

##############################################################################################
##############################################################################################

external_scripts = [  
    "https://code.highcharts.com/gantt/highcharts-gantt.js",
    "https://code.highcharts.com/gantt/modules/gantt.js", 
    "https://code.highcharts.com/modules/draggable-points.js"
]
app = dash.Dash(
    __name__,
    external_scripts=external_scripts
    )

def epoch_js(date_time):
    return int(time.mktime(date_time.timetuple())) * 1000


app.layout = html.Div(
    [
        html.Div("Drag the task or it's extremities to update OPTIONS PROPERTY (does not work)"),
        #dav.HighChart(
        HighChart(
            id="my_highchart",
            constructorType = 'ganttChart', 
            options = {
                'title': {
                    'text': 'Interactive Gantt Chart',
                    'align': 'left'
                    },
                'xAxis': {
                    'min': epoch_js(datetime.datetime(2023, 8, 17)),
                    'max': epoch_js(datetime.datetime(2023, 10, 30)),
                    },
                'plotOptions': {
                    'series': {
                        'animation': False, # Do not animate dependency connectors
                        'dragDrop': {
                            'draggableX': True,
                            'draggableY': True,
                            'dragMinY': 0,
                            'dragMaxY': 2,
                            'dragPrecisionX': 86400000, # 1day = 1000 * 60 * 60 * 24 (millis)
                            },
                        'dataLabels': {
                            'enabled': True,
                            'format': '{point.name}',
                            'style': {
                                'cursor': 'default',
                                'pointerEvents': 'none'
                                }
                            },
                        'allowPointSelect': True,
                        }
                    },
                'series': [
                    {
                        'name': 'Project',
                        'data': [
                            {
                                'name': 'Start prototype',
                                'start': epoch_js(datetime.datetime(2023, 9, 10)),
                                'end'  : epoch_js(datetime.datetime(2023, 10, 10)),
                                'completed': {
                                    'amount': 0.5
                                    }
                                }
                            ]
                        }
                    ],
                }
            ),
        html.Div('This should update the options property data whenever the user interacts with the Gantt Chart'),
        html.Div(id='output'),
        ]
    )


# GETTING THE OPTIONS AFTER USER INTERACTION
# DOES NOT WORK
@app.callback(  # THIS DOES NOT TIGGER AS EXPECTED !!!
    Output("output", "children"),
    Input("my_highchart", "options"),
    prevent_initial_callback = True,
    ) 
def get_options(options):
    print(options)
    return f'{options}'

if __name__ == "__main__":
   app.run_server( port = 8050, debug=False)

I am no expert at wrtiting dash components, but could it be that the HighChart.react.js only has a render( ) event and no onChange() event?

This onchange() event should in some way implement a setState and/or setProps?

… this is getting scary :cold_face:

Hey again @popo,

Tbh, I only have minimal knowledge on js/react. Hence, I wont be able to help you. But, last thing that I want to be make sure of is whether you add Highcharts’ corresponding js code in to your applications assets directory?

If you don’t, you may want to follow this documentation.

Alternatively, you also may want to create an issue on dash_alternative_viz’s GitHub page, to get a help from the creators of the project.

Cheers!!

@Berbere

dash_alternative_viz (dav) is a pip installed dash component… so there’s no need for anything in the assets dir (by default).

@popo

Yeah you are right, I somehow thought that is a separate py file created by you…

Hello both,

So, Highcharts and Gantt doesnt update the options when interacting, but you can get the data, here is a small example:

import dash
from dash import Input, Output, State, html
import datetime, time

########################################################################################################
# FROM :  https://github.com/plotly/dash-alternative-viz/blob/master/dash_alternative_viz/HighChart.py
########################################################################################################
from dash.development.base_component import Component, _explicitize_args


class HighChart(Component):
    """A HighChart component.
HighChart renders Highcharts.js JSON

Keyword arguments:
- id (string; optional): The ID used to identify this component in Dash callbacks.
- constructorType (string; optional): 'chart', 'stockChart', 'mapChart', 'ganttChart'
- options (dict; optional): The highcharts chart description"""
    @_explicitize_args
    def __init__(self, id=Component.UNDEFINED, constructorType=Component.UNDEFINED, options=Component.UNDEFINED, **kwargs):
        self._prop_names = ['id', 'constructorType', 'options']
        self._type = 'HighChart'
        self._namespace = 'dash_alternative_viz'
        self._valid_wildcard_attributes = []
        self.available_properties = ['id', 'constructorType', 'options']
        self.available_wildcard_properties = []

        _explicit_args = kwargs.pop('_explicit_args')
        _locals = locals()
        _locals.update(kwargs)  # For wildcard attrs
        args = {k: _locals[k] for k in _explicit_args if k != 'children'}

        for k in []:
            if k not in args:
                raise TypeError(
                    'Required argument `' + k + '` was not specified.')
        super(HighChart, self).__init__(**args)

##############################################################################################
##############################################################################################

external_scripts = [
    "https://code.highcharts.com/gantt/highcharts-gantt.js",
    "https://code.highcharts.com/gantt/modules/gantt.js",
    "https://code.highcharts.com/modules/draggable-points.js",
]
app = dash.Dash(
    __name__,
    external_scripts=external_scripts
    )

def epoch_js(date_time):
    return int(time.mktime(date_time.timetuple())) * 1000


app.layout = html.Div(
    [
        html.Div("Drag the task or it's extremities to update OPTIONS PROPERTY (does not work)"),
        html.Button(id='hidden_button'),
        HighChart(
            id="my_highchart",
            constructorType = 'ganttChart',
            options = {
                'title': {
                    'text': 'Interactive Gantt Chart',
                    'align': 'left'
                    },
                'xAxis': {
                    'min': epoch_js(datetime.datetime(2023, 8, 17)),
                    'max': epoch_js(datetime.datetime(2023, 10, 30)),
                    },
                'plotOptions': {
                    'series': {
                        'animation': False, # Do not animate dependency connectors
                        'dragDrop': {
                            'draggableX': True,
                            'draggableY': True,
                            'dragMinY': 0,
                            'dragMaxY': 2,
                            'dragPrecisionX': 86400000, # 1day = 1000 * 60 * 60 * 24 (millis)
                            },
                        'dataLabels': {
                            'enabled': True,
                            'format': '{point.name}',
                            'style': {
                                'cursor': 'default',
                                'pointerEvents': 'none'
                                }
                            },
                        'allowPointSelect': True,
                        }
                    },
                'series': [
                    {
                        'name': 'Project',
                        'data': [
                            {
                                'name': 'Start prototype',
                                'start': epoch_js(datetime.datetime(2023, 9, 10)),
                                'end'  : epoch_js(datetime.datetime(2023, 10, 10)),
                                'completed': {
                                    'amount': 0.5
                                    }
                                }
                            ]
                        }
                    ],
                }
            ),
        html.Div('This should update the options property data whenever the user interacts with the Gantt Chart'),
        html.Div(id='output'),
        ]
    )


### displays start and end
app.clientside_callback(
    """
        function (n) {
            if (n) {
                return localStorage.getItem("chart_data")
            }
            return window.dash_clientside.no_update
        }
    """,
    Output("output", "children"),
    Input('hidden_button', 'n_clicks'),
    prevent_initial_callback = True,
    )

## sets event listener upon redraw
app.clientside_callback(
    """
        function () {
            var newOpts = Highcharts.charts[0].getOptions()
            newOpts.chart['events'] = {'render': () => {
                var readSeries = Highcharts.charts[0].series
                var rowData = []
                readSeries.forEach((row) => {
                    var newData = []
                    row.data.forEach((data) => {
                        newData.push({'start': data.start, 'end': data.end})
                    })
                    rowData.push(newData)
                })
                localStorage.setItem("chart_data",
                    JSON.stringify(rowData)
                )
                document.querySelector('#hidden_button').click()
            }}
            Highcharts.charts[0].update(newOpts)
            return window.dash_clientside.no_update
        }        
    """,
    Output('my_highchart', 'id'), Input('my_highchart', 'id')
)

if __name__ == "__main__":
   app.run_server( port = 8050, debug=True)
1 Like

Thank you all mighty @jinnyzor :love_you_gesture:

It works and opens the path for further customization.

1 Like