Black Lives Matter. Please consider donating to Black Girls Code today.

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.