Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature Request] dcc.RangeSlider tooltip - allow displaying something else than value #1846

Closed
vsisl opened this issue Nov 28, 2021 · 16 comments · Fixed by #2723
Closed

[Feature Request] dcc.RangeSlider tooltip - allow displaying something else than value #1846

vsisl opened this issue Nov 28, 2021 · 16 comments · Fixed by #2723
Assignees
Labels

Comments

@vsisl
Copy link

vsisl commented Nov 28, 2021

Problem

Currently, the tooltip of dcc.RangeSlider can be used to display the value property of the slider. It would be nice if it could display an arbitrary string. E.g. using a dictionary, where keys are the values of the slider and values are the strings to be displayed in the tooltip.

Solution(?)

Apparently, such functionality is present in the react slider.

http://react-component.github.io/slider/?path=/story/rc-slider–handle

<div style={wrapperStyle}>
      <p>Range with custom tooltip</p>
      <Range min={0} max={20} defaultValue={[3, 10]} tipFormatter={value => `${value}%`} />
</div>

See the tipFormatter property. Could this property be added to dcc.RangeSlider?

Example use-case

Imagine that your slider values are actually indexes of a list of the real values. You want to present the real values to the user instead of the indexes. You cannot use the marks property of the slider because there are too many values and it would look messy. Thus, you want to use the tooltip to display something else than the value property of the slider.

@vsisl vsisl changed the title dcc.RangeSlider tooltip - allow displaying something else than value [Feature Request] dcc.RangeSlider tooltip - allow displaying something else than value Nov 28, 2021
@araichev
Copy link

I second this feature request.
I would find it quite useful to be able to format the tooltip, e.g. with a thousands comma or with units.

@kenneth-liao
Copy link

Yes please! This is absolutely needed and completely enhances the user experience when working with values such as dates!

@OMBeau
Copy link

OMBeau commented Aug 7, 2022

This would greatly improve my dash app when working with date strings, and when marks overlap, such as this...

Screen Shot 2022-08-07 at 7 13 51 PM

.

@OMBeau
Copy link

OMBeau commented Aug 9, 2022

This would greatly improve my dash app when working with date strings, and when marks overlap, such as this...

Screen Shot 2022-08-07 at 7 13 51 PM

.

Temp workaround:
update marks with style = {"display": "none"} for marks not currently on.

ie

@callback(
Output("time_slider", "marks"),
Input("time_slider", "value"),
State("time_slider", "marks"),
prevent_initial_call=True,
)
def update_slider_marks(slider_value, current_marks):

for k, v in current_marks.items():

    v["style"] = {} if slider_value == int(k) else {"display": "none"}

    current_marks[k] = v

return current_marks

hope this helps while request remains open!

Screen Shot 2022-08-10 at 12 12 13 AM

@cmo26
Copy link

cmo26 commented Jan 16, 2023

yes we do need this feature, as mentioned by others, this is especially useful for date values.
As RangeSlider currently does not support date, we need to use some workaround that convert date into epoch values which works nicely. Unfortunately since tooltip is not configurable, the tooltip shows the epoch date which is not useful

@4l1fe
Copy link

4l1fe commented Feb 4, 2023

Join the issue. Currently, i've added two text components and update them with selected dates.

The downside is doing in on a server side is overhead.

@EstevaoSoaresDS
Copy link

I have the same problem here, this feature can be very useful!

@ameetdesh
Copy link

I second this feature request! I am using a numeric index mapped to a vector of datetimes, and would like to show the datetime as a tooltip (I am using markers=None) instead of numeric index.

@Nenrikido
Copy link

Nenrikido commented Apr 19, 2023

This feature would be very helpful, for now i tinkered with it and tried to find a CSS hack that uses the least amount of callbacks :

First, I thought of using tooltips, and updating their content by putting a :before pseudo-element inside it, with something like content: var(--tooltip-value) and updating the css var by having a callback with the slider's drag_value as input, and a parent element's style as output.
This works fine but i noticed that the tooltip are actually added to the DOM by appending them on hover, and there's no way to differenciate which corresponds to the start or the end handle, so you can't use :nth-child and the like to select which tooltip has which value.

Second more hacky way but that works in most scenarios :
Using marks for each element shifted by one to the left (each mark should display the value of the previous tick), then you need to add a mark to the end which has the value of the last tick,
then you can use this CSS :

.rc-slider-mark-text {
    white-space: nowrap;
    display: none;
}

.rc-slider-mark-text-active:nth-child(2),
.rc-slider-mark-text:not(.rc-slider-mark-text-active) + .rc-slider-mark-text-active + .rc-slider-mark-text-active,
.rc-slider-mark-text-active + .rc-slider-mark-text:not(.rc-slider-mark-text-active),
.rc-slider-mark-text-active:last-child  {
    display: block;
    color: #666;
}

Shifting to the left is mandatory if you want it to be compatible on most browsers,
if you don't care, you can use :has and you don't need to touch the inputs and outputs of your RangeSlider (caniuse) :

.rc-slider-mark-text {
    white-space: nowrap;
    display: none;
}

.rc-slider-mark-text-active:first-child,
.rc-slider-mark-text:not(.rc-slider-mark-text-active) + .rc-slider-mark-text-active,
.rc-slider-mark-text-active:not(:has(~ .rc-slider-mark-text-active)),
.rc-slider-mark-text-active:last-child  {
    display: block;
    color: #666;
}

This is a pure front-end solution that works nicely in most usecases where you want to emulate react's tipFormatter

Edit, fully functionnal workaround, with usecase :

Found out that by fusing the 2 tricks i described earlier, i made it work as i wanted, and here's the code i use to have a date RangeSlider with marks that follow the selected range :

RangeSlider init code :

from dash import html, dcc
from dateutil.rrule import rrule, DAILY
from datetime import datetime, timedelta

min_day: datetime
max_day: datetime
day_interval = (max_day - min_day).days

date_range_slider = html.Div(
    dcc.RangeSlider(
        min=0,
        max=day_interval,
        value=[0, day_interval],
        marks={
            i: f'{date - timedelta(days=1):%Y-%m-%d}' for i, date in enumerate(rrule(freq=DAILY, dtstart=min_day, until=max_day))
        },
    ),
    style={"--max-tooltip-val": f'"{max_day:%Y-%m-%d}"'}
)

Here's the CSS :

.rc-slider-mark-text {
    white-space: nowrap;
    display: none;
}

.rc-slider-mark-text-active:nth-child(2),
.rc-slider-mark-text:not(.rc-slider-mark-text-active) + .rc-slider-mark-text-active + .rc-slider-mark-text-active,
.rc-slider-mark-text-active + .rc-slider-mark-text:not(.rc-slider-mark-text-active),
.rc-slider-mark-text-active:last-child {
    display: block;
    color: #666;
}

.rc-slider-mark-text-active:last-child {
    font-size: 0;
}

.rc-slider-mark-text-active:last-child:before {
    content: var(--max-tooltip-val);
    font-size: 12px;
}

Note that you might wanna move the tooltips a bit to the left (as we take each next tooltip except for the last one), but i let that to your taste.

@apokrif333
Copy link

I have the same problem. I make a Dash table and on a desktop, all look fine. But if I open my table on a mobile device, the values on my sliders overlap each other. I need a tooltip with a financial format.

@FrederikVigen
Copy link

In case anyone is interested i spend some time digging into this with CSS
I do not find it possible for my tooltips to change on the slider when i move them with only CSS

But i did find a solution using JS, it is only possible to format dynamically the value from the slider though. Here is the code that would change values of dates forexample: 20230101 to 2023-01-01

console.log("js loaded")

function formatToolTip(tooltipElement, value) {
    strVal = value.toString()
    tooltipElement.innerHTML = `${strVal.slice(0,4)}-${strVal.slice(4,6)}-${strVal.slice(6,8)}`    //Value is the value from the sliders val, and format is how you want it formatted
}

var firstObserver = new MutationObserver((mutations) => {
    mutations.forEach(mutation => {
        if (mutation.type === 'attributes') {
            val = mutation.target.getAttribute('aria-valuenow')
            firstTooltip = document.querySelectorAll('.rc-slider-tooltip-inner')[0]
            formatToolTip(firstTooltip, val)
        }
    })
})

var secondObserver = new MutationObserver((mutations) => {
    mutations.forEach(mutation => {
        if (mutation.type === 'attributes') {
            val = mutation.target.getAttribute('aria-valuenow')
            secondToolTip = document.querySelectorAll('.rc-slider-tooltip-inner')[1]
            formatToolTip(secondToolTip, val)
        }
    })
})

const addEventListener = () => {
    var firstDot = document.getElementsByClassName("rc-slider-handle rc-slider-handle-1")[0];
    var secondDot = document.getElementsByClassName("rc-slider-handle rc-slider-handle-2")[0];
    if (!firstDot || !secondDot) {
        setTimeout(() => addEventListener(), 500)
        return
    }

    firstTooltip = document.querySelectorAll('.rc-slider-tooltip-inner')[0]
    secondToolTip = document.querySelectorAll('.rc-slider-tooltip-inner')[1]
    formatToolTip(firstTooltip, firstDot)
    formatToolTip(firstTooltip, secondDot)


    firstObserver.observe(firstDot, {attributes: true})
    secondObserver.observe(secondDot, {attributes: true})
}

addEventListener()

Just add this file to your assets under tooltip-fixer.js or whatever you like

Here is a gif of how it looks without any styling of css:
Recording 2023-05-04 at 14 49 48

@EnomisRekschil
Copy link

I second this would be very helpful

@FrederikVigen
Copy link

In case anyone is interested i spend some time digging into this with CSS I do not find it possible for my tooltips to change on the slider when i move them with only CSS

But i did find a solution using JS, it is only possible to format dynamically the value from the slider though. Here is the code that would change values of dates forexample: 20230101 to 2023-01-01

console.log("js loaded")

function formatToolTip(tooltipElement, value) {
    strVal = value.toString()
    tooltipElement.innerHTML = `${strVal.slice(0,4)}-${strVal.slice(4,6)}-${strVal.slice(6,8)}`    //Value is the value from the sliders val, and format is how you want it formatted
}

var firstObserver = new MutationObserver((mutations) => {
    mutations.forEach(mutation => {
        if (mutation.type === 'attributes') {
            val = mutation.target.getAttribute('aria-valuenow')
            firstTooltip = document.querySelectorAll('.rc-slider-tooltip-inner')[0]
            formatToolTip(firstTooltip, val)
        }
    })
})

var secondObserver = new MutationObserver((mutations) => {
    mutations.forEach(mutation => {
        if (mutation.type === 'attributes') {
            val = mutation.target.getAttribute('aria-valuenow')
            secondToolTip = document.querySelectorAll('.rc-slider-tooltip-inner')[1]
            formatToolTip(secondToolTip, val)
        }
    })
})

const addEventListener = () => {
    var firstDot = document.getElementsByClassName("rc-slider-handle rc-slider-handle-1")[0];
    var secondDot = document.getElementsByClassName("rc-slider-handle rc-slider-handle-2")[0];
    if (!firstDot || !secondDot) {
        setTimeout(() => addEventListener(), 500)
        return
    }

    firstTooltip = document.querySelectorAll('.rc-slider-tooltip-inner')[0]
    secondToolTip = document.querySelectorAll('.rc-slider-tooltip-inner')[1]
    formatToolTip(firstTooltip, firstDot)
    formatToolTip(firstTooltip, secondDot)


    firstObserver.observe(firstDot, {attributes: true})
    secondObserver.observe(secondDot, {attributes: true})
}

addEventListener()

Just add this file to your assets under tooltip-fixer.js or whatever you like

Here is a gif of how it looks without any styling of css: Recording 2023-05-04 at 14 49 48 Recording 2023-05-04 at 14 49 48

In case you need a more robust solution i have improved the code to observe when the tooltip is generated in the body aswell:

document.addEventListener("DOMContentLoaded", () => {
    waitForElm('.rc-slider-handle-1').then(elm => {
        createObserver(0).observe(elm, {attributes: true})
    })
    waitForElm('.rc-slider-handle-2').then(elm => {
        createObserver(1).observe(elm, {attributes: true})
    })
})

function waitForElm(selector, elementNumber) {
    return new Promise(resolve => {
        if (document.querySelector(selector)) {
            return resolve(document.querySelector(selector));
        }

        const observer = new MutationObserver(mutations => {
            if (document.querySelector(selector)) {
                resolve(document.querySelector(selector));
                observer.disconnect();
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
}

function formatToolTip(tooltipElement, value) {
    var dateObj = new Date(parseInt(value))
    var month = dateObj.getMonth() + 1
    var date = dateObj.getDate();

    tooltipElement.innerHTML = `${dateObj.getFullYear()}/${month < 10 ? '0' + month : month}/${date < 10 ? '0' + date : date}`    //Value is the value from the sliders val, and format is how you want it formatted
}

var createObserver = (tooltipNr) => new MutationObserver((mutations) => {
    mutations.forEach(mutation => {
        if (mutation.type === 'attributes') {
            val = mutation.target.getAttribute('aria-valuenow')
            tooltip = document.querySelectorAll('.rc-slider-tooltip-inner')[tooltipNr]
            formatToolTip(tooltip, val)
        }
    })
})

@lukasbommlerblackpoint
Copy link

Is someone still pushing that? Would be a very basic, yet great feature.

@yinchi
Copy link

yinchi commented Nov 5, 2023

I would also enjoy this feature very much -- want to append a "%" sign to show values as percentages.

Screenshot 2023-11-05 095030

(Hacked the CSS to use a slider to show performance against a target value -- disabled the slider on the Python side and increased the linewidth of the "track" and "rail". Also need to set the cursor back to default for the slider handle.)

EDIT: noticed that this issue was originally for dcc.RangeSlider, not dcc.Slider, but the logic should be almost the same.

@sebwog
Copy link

sebwog commented Jan 2, 2024

I'd also love this feature! My range slider is logaritmic so the input values x (the ones shown in the tooltip) are converted to:
f = 10^x
So I'd like to be able to convert the tooltip value to 10^x.
Screenshot 2024-01-02 at 14 18 36

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.