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

Add Slider/RangeSlider tooltip.format and tooltip.style. #2723

Merged
merged 7 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ This project adheres to [Semantic Versioning](https://semver.org/).

## Added
- [#2695](https://github.com/plotly/dash/pull/2695) Adds `triggered_id` to `dash_clientside.callback_context`. Fixes [#2692](https://github.com/plotly/dash/issues/2692)
- [#2723](https://github.com/plotly/dash/pull/2723) Improve dcc Slider/RangeSlider tooltips. Fixes [#1846](https://github.com/plotly/dash/issues/1846)
- Add `tooltip.format` a string for the format template, {value} will be formatted with the actual value.
- Add `tooltip.style` a style object to give to the div of the tooltip.
- [#2732](https://github.com/plotly/dash/pull/2732) Add special key `_dash_error` to `setProps`, allowing component developers to send error without throwing in render. Usage `props.setProps({_dash_error: new Error("custom error")})`

## Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,31 @@ RangeSlider.propTypes = {
'bottomLeft',
'bottomRight',
]),
/**
* Template string to display the tooltip in.
* Must contain `{value}`, which will be replaced with either
* the default string representation of the value or the result of the
* transform function if there is one.
*/
template: PropTypes.string,
/**
* Custom style for the tooltip.
*/
style: PropTypes.object,
/**
* Reference to a function in the `window.dccFunctions` namespace.
* This can be added in a script in the asset folder.
*
* For example, in `assets/tooltip.js`:
* ```
* window.dccFunctions = window.dccFunctions || {};
* window.dccFunctions.multByTen = function(value) {
* return value * 10;
* }
* ```
* Then in the component `tooltip={'transform': 'multByTen'
alexcjohnson marked this conversation as resolved.
Show resolved Hide resolved
*/
transform: PropTypes.string,
}),

/**
Expand Down
27 changes: 27 additions & 0 deletions components/dash-core-components/src/components/Slider.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React, {Component, lazy, Suspense} from 'react';
import PropTypes from 'prop-types';
import slider from '../utils/LazyLoader/slider';

import './css/sliders.css';

const RealSlider = lazy(slider);

/**
Expand Down Expand Up @@ -105,6 +107,31 @@ Slider.propTypes = {
'bottomLeft',
'bottomRight',
]),
/**
* Template string to display the tooltip in.
* Must contain `{value}`, which will be replaced with either
* the default string representation of the value or the result of the
* transform function if there is one.
*/
template: PropTypes.string,
/**
* Custom style for the tooltip.
*/
style: PropTypes.object,
/**
* Reference to a function in the `window.dccFunctions` namespace.
* This can be added in a scrip in the asset folder.
alexcjohnson marked this conversation as resolved.
Show resolved Hide resolved
*
* For example, in `assets/tooltip.js`:
* ```
* window.dccFunctions = window.dccFunctions || {};
* window.dccFunctions.multByTen = function(value) {
* return value * 10;
* }
* ```
* Then in the component `tooltip={'transform': 'multByTen'
alexcjohnson marked this conversation as resolved.
Show resolved Hide resolved
*/
transform: PropTypes.string,
}),

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/* Fix the default tooltip style height conflicting with the actual size of the tooltip. */
.rc-slider-tooltip-content > .rc-slider-tooltip-inner {
height: unset;
min-height: 20px;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {Component} from 'react';
import {assoc, pick, isNil} from 'ramda';
import {assoc, pick, isNil, pipe, omit} from 'ramda';
import {Range, createSliderWithTooltip} from 'rc-slider';
import computeSliderStyle from '../utils/computeSliderStyle';

Expand All @@ -11,6 +11,10 @@ import {
setUndefined,
} from '../utils/computeSliderMarkers';
import {propTypes, defaultProps} from '../components/RangeSlider.react';
import {
formatSliderTooltip,
transformSliderTooltip,
} from '../utils/formatSliderTooltip';

const sliderProps = [
'min',
Expand Down Expand Up @@ -72,17 +76,33 @@ export default class RangeSlider extends Component {
} = this.props;
const value = this.state.value;

let tipProps;
if (tooltip && tooltip.always_visible) {
let tipProps, tipFormatter;
if (tooltip) {
/**
* clone `tooltip` but with renamed key `always_visible` -> `visible`
* the rc-tooltip API uses `visible`, but `always_visible is more semantic
* the rc-tooltip API uses `visible`, but `always_visible` is more semantic
* assigns the new (renamed) key to the old key and deletes the old key
*/
tipProps = assoc('visible', tooltip.always_visible, tooltip);
delete tipProps.always_visible;
} else {
tipProps = tooltip;
tipProps = pipe(
assoc('visible', tooltip.always_visible),
omit(['always_visible', 'template', 'style', 'transform'])
)(tooltip);
if (tooltip.template || tooltip.style || tooltip.transform) {
tipFormatter = tipValue => {
let t = tipValue;
if (tooltip.transform) {
t = transformSliderTooltip(tooltip.transform, tipValue);
}
return (
<div style={tooltip.style}>
{formatSliderTooltip(
tooltip.template || '{value}',
t
)}
</div>
);
};
}
}

return (
Expand Down Expand Up @@ -116,6 +136,7 @@ export default class RangeSlider extends Component {
...tipProps,
getTooltipContainer: node => node,
}}
tipFormatter={tipFormatter}
style={{position: 'relative'}}
value={value ? value : calcValue(min, max, value)}
marks={sanitizeMarks({min, max, marks, step})}
Expand Down
35 changes: 28 additions & 7 deletions components/dash-core-components/src/fragments/Slider.react.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, {Component} from 'react';
import ReactSlider, {createSliderWithTooltip} from 'rc-slider';
import {assoc, isNil, pick} from 'ramda';
import {assoc, isNil, pick, pipe, omit} from 'ramda';
import computeSliderStyle from '../utils/computeSliderStyle';

import 'rc-slider/assets/index.css';
Expand All @@ -11,6 +11,10 @@ import {
setUndefined,
} from '../utils/computeSliderMarkers';
import {propTypes, defaultProps} from '../components/Slider.react';
import {
formatSliderTooltip,
transformSliderTooltip,
} from '../utils/formatSliderTooltip';

const sliderProps = [
'min',
Expand Down Expand Up @@ -72,17 +76,33 @@ export default class Slider extends Component {
} = this.props;
const value = this.state.value;

let tipProps;
if (tooltip && tooltip.always_visible) {
let tipProps, tipFormatter;
if (tooltip) {
/**
* clone `tooltip` but with renamed key `always_visible` -> `visible`
* the rc-tooltip API uses `visible`, but `always_visible` is more semantic
* assigns the new (renamed) key to the old key and deletes the old key
*/
tipProps = assoc('visible', tooltip.always_visible, tooltip);
delete tipProps.always_visible;
} else {
tipProps = tooltip;
tipProps = pipe(
assoc('visible', tooltip.always_visible),
omit(['always_visible', 'template', 'style', 'transform'])
)(tooltip);
if (tooltip.template || tooltip.style || tooltip.transform) {
tipFormatter = tipValue => {
let t = tipValue;
if (tooltip.transform) {
t = transformSliderTooltip(tooltip.transform, tipValue);
}
return (
<div style={tooltip.style}>
{formatSliderTooltip(
tooltip.template || '{value}',
t
)}
</div>
);
};
}
}

return (
Expand Down Expand Up @@ -116,6 +136,7 @@ export default class Slider extends Component {
...tipProps,
getTooltipContainer: node => node,
}}
tipFormatter={tipFormatter}
style={{position: 'relative'}}
value={value}
marks={sanitizeMarks({min, max, marks, step})}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {replace, path, split, concat, pipe} from 'ramda';

export const formatSliderTooltip = (template, value) => {
return replace('{value}', value, template);
};

export const transformSliderTooltip = (funcName, value) => {
const func = pipe(
split('.'),
s => concat(['dccFunctions'], s),
s => path(s, window)
)(funcName);
if (!func) {
throw new Error(
`Invalid func for slider tooltip transform: ${funcName}`
);
}
return func(value);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
window.dccFunctions = window.dccFunctions || {};
window.dccFunctions.transformTooltip = function(value) {
return "Transformed " + value
}
Original file line number Diff line number Diff line change
Expand Up @@ -559,3 +559,60 @@ def test_slsl015_range_slider_no_min_max(dash_dcc):
)

assert dash_dcc.get_logs() == []


def test_sls016_sliders_format_tooltips(dash_dcc):
app = Dash(__name__)
app.layout = html.Div(
[
dcc.Slider(
value=34,
min=20,
max=100,
id="slider",
tooltip={
"template": "Custom tooltip: {value}",
"always_visible": True,
"style": {"padding": "8px"},
},
),
dcc.RangeSlider(
value=[48, 60],
min=20,
max=100,
id="range-slider",
tooltip={"template": "Custom tooltip: {value}", "always_visible": True},
),
dcc.Slider(
min=20,
max=100,
id="slider-transform",
tooltip={"always_visible": True, "transform": "transformTooltip"},
),
],
style={"padding": "12px", "marginTop": "48px"},
)

dash_dcc.start_server(app)
# dash_dcc.wait_for_element("#slider")

dash_dcc.wait_for_text_to_equal(
"#slider .rc-slider-tooltip-content", "Custom tooltip: 34"
)
dash_dcc.wait_for_text_to_equal(
"#range-slider .rc-slider-tooltip-content", "Custom tooltip: 48"
)
dash_dcc.wait_for_text_to_equal(
"#range-slider > div:nth-child(1) > div:last-child .rc-slider-tooltip-content",
"Custom tooltip: 60",
)
dash_dcc.wait_for_style_to_equal(
"#slider .rc-slider-tooltip-inner > div", "padding", "8px"
)
dash_dcc.wait_for_text_to_equal(
"#slider-transform .rc-slider-tooltip-content", "Transformed 20"
)

dash_dcc.percy_snapshot("sliders-format-tooltips")

assert dash_dcc.get_logs() == []