CSS animation in generated HTML table does not work

Hello,
Dash is an awesome tool, but for the first time I hit the wall with a problem, I can’t find a solution to. Python (and Dash) are just my hobby, so sorry in advance, if my problem is a stupid one. The same goes for the code itself :wink:
Essentialy, I wrote a really simple and basic app, which downloads a xml file with election data and generates several tables from the data in that file. Since the xml file will be updated once per minute, I used Interval component to re-download the file and generate an updated html table output.
Now, it would be nice, if rows, whose data has changed since last download, had a simple animation - short “blink”.
I created an local .css file in assets folder and so on, as specified in Dash documentation.
When I first run the app and generate output tables, the animation will run.
However, when automatic refresh via Interval component is triggered, animation won’t run.
I am out of ideas, any help, if possible, would be greatly appreciated.

Code (don’t want to bother with all of it, but if needed, I will post all of it, no problem):

CSS bit:

.row_normal{
  color: inherit;
  animation-name: row_change_notify;
  animation-duration: 4s;
  animation-timing-function: ease-in;
}

@keyframes row_change_notify {
  0% {background-color: #FAFAFA}
  25% {background-color: #81F79F}
  50% {background-color: #2EFE64}
  75% {background-color: #81F79F}
  90% {background-color: #CEF6D8}
}

callback function (now - for testing, the animation should run for all rows each time the callback is fired, no matter, whether some data has changed or not):

@app.callback(
    Output(component_id = 'outputWrap', component_property = 'children'),
    [Input(component_id = 'BttnShowOutput', component_property = 'n_clicks'),
    Input(component_id = 'updateButton', component_property = 'n_intervals')],
    [State(component_id = 'NUTSselectorDrop', component_property = 'value'),
    State(component_id = 'OBECSelectorDrop', component_property = 'value')]
)

def generate_output(clicks, n, nuts, obec):

    if ((clicks > 0) and (nuts is not None) and (obec is not None) and (n == 0)):
        print('manually firing callback "generate_output"')

        return get_OBEC_output(nuts, obec)

    elif ((clicks > 0) and (n > 0) and (nuts is not None) and (obec is not None)):
        print('refreshing - firing callback "generate_output"')
        save_xml(nuts)
        return get_OBEC_output(nuts, obec)

    else:
        return None

Function ‘save_xml’ :

def save_xml(value):
    '''saves grabbed xml data into xml file onto disc.'''
    try:
        print('grabbing selected xml data from web')
        grab = requests.get(LINK, params={'datumvoleb' : LINK_DATE, 'nuts' : value})

        print('saving selected xml data into xml file')
        with open(op.join(ASSETS_PATH, 'xml_data_temp.xml'), 'w', encoding = 'utf-8') as file:
            file.write(grab.text)

    except Exception as e:
        print('Error in f. "save_xml": ' + str(e))

Function ‘get_OBEC_output’ code:

def get_OBEC_output(nuts_code, obec_name):
    '''generates output for Dash dynamic component - HTML tables'''
    try:

        with open(op.join(ASSETS_PATH, 'xml_data_temp.xml'), 'r', encoding='utf-8') as file:
            parser = ET.XMLParser(encoding = 'utf-8')
            tree = ET.parse(file, parser)
            root = tree.getroot()

        obce = root.findall("{http://www.volby.cz/kv/}OBEC")

        obec = root.find("*[@NAZEVZAST='{}']".format(obec_name))
        strany = obec.findall(".//{http://www.volby.cz/kv/}VOLEBNI_STRANA")

        data = []
        data_obec = []
        zastupitel_data = []

        for obec in obce:
            nazev = obec.attrib['NAZEVZAST']
            ucast = obec.find(".//{http://www.volby.cz/kv/}UCAST")
            zpracovano = ucast.attrib['OKRSKY_ZPRAC_PROC']
            ucast_volicu = ucast.attrib['UCAST_PROC']

            data_obec.append({'Název obce' : nazev,
                        'Zpracováno hlasů (v %)' : zpracovano,
                        'Účast voličů (v %)' : ucast_volicu}
                        )

        for strana in strany:
            nazev = strana.attrib['NAZEV_STRANY']
            hlasy = strana.attrib['HLASY_PROC']
            kand_pocet = strana.attrib['KANDIDATU_POCET']
            zast_pocet = strana.attrib['ZASTUPITELE_POCET']
            zast_proc = strana.attrib['ZASTUPITELE_PROC']

            data.append({'Název strany' : nazev,
                        'Počet hlasů (v %)' : hlasy,
                        'Počet kandidátů' : kand_pocet,
                        'Počet zastupitelů' : zast_pocet,
                        'Počet zastupitelů (v %)' : zast_proc}
                        )

            zastupitele = strana.findall(".//{http://www.volby.cz/kv/}ZASTUPITEL")
            for zastupitel in zastupitele:
                strana_zast = nazev
                jmeno = zastupitel.attrib['JMENO']
                prijmeni = zastupitel.attrib['PRIJMENI']
                hlasy_abs = zastupitel.attrib['HLASY']
                hlasy_proc = zastupitel.attrib['HLASY_PROC']

                zastupitel_data.append({'Jméno' : jmeno,
                                        'Příjmení' : prijmeni,
                                        'Hlasy (absolutní počet)' : hlasy_abs,
                                        'Hlasy (v %)' : hlasy_proc,
                                        'Strana' : strana_zast}
                                        )

        print('printing output')

        return [html.Div(id = 'tableOKRESwrap',
                        children = [
            html.Table(
                [html.Tr(
                    [html.Th(item) for item in data_obec[0]]
                    )] +
                [html.Tr(
                    [html.Td(each) for each in list(row.values())],
                    className = 'row_normal',
                    # style = blink_style
                    ) for row in sorted(data_obec, key = lambda element: float(element['Účast voličů (v %)']), reverse = True)]
                )],
                style = {'width' : '50%', 'float' : 'left', 'margin' : '10px'}
            ),
            html.Div(id = 'tableOBECwrap',
                    children = [
            html.Div(
            html.Table(
                [html.Tr(
                    [html.Th(item) for item in data[0]]
                    )] +
                [html.Tr(
                    [html.Td(each) for each in list(row.values())],
                    className = 'row_normal'
                    ) for row in sorted(data, key = lambda element: int(element['Počet zastupitelů']), reverse = True)]
                ),
                style = {'margin' : '10px'}
            ),
            html.Div(
            html.Table(
                [html.Tr(
                    [html.Th(item) for item in zastupitel_data[0]]
                    )] +
                [html.Tr(
                    [html.Td(item) for item in list(row.values())],
                    className = 'row_normal',
                    ) for row in sorted(zastupitel_data, key = lambda element: int(element['Hlasy (absolutní počet)']), reverse = True)]
                ),
                style = {'margin' : '10px'}
                )
            ],
            style = {'width' : '50%', 'align-items' : 'top', 'margin' : '0px'}
            )
        ]

    except Exception as e:
        print('Error in f. "get_OBEC_output": ' + str(e))

This is a tricky one. Try adding a key to the elements that you are trying to animate where key is a random string. e.g. tr(className='row_change_notify', key=str(uuid.uuid4()). That’ll force React to rerender the entire row, which might trigger the animation. I recall reading something about this deep in one of the React issues (I can’t seem to find it now)

2 Likes

Hi @chriddyp ,
thanks a lot for fast reply. Your solution works perfectly. I just used key = str(random.randint(a, b)) for random string generation.
Thanks again, I would not figure this one out.

1 Like

Woohoo! That was a lucky guess.

Can you share a GIF of what the animation looks like?

Sure thing. Here it is.