Skip to content

Commit

Permalink
Merge pull request #2723 from plotly/slider-tips
Browse files Browse the repository at this point in the history
Add Slider/RangeSlider tooltip.format and tooltip.style.
  • Loading branch information
T4rk1n authored Jan 31, 2024
2 parents 9920073 + 06fb03a commit 49ac14f
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 15 deletions.
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'}`
*/
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 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'}`
*/
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;
}
37 changes: 29 additions & 8 deletions components/dash-core-components/src/fragments/RangeSlider.react.js
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
19 changes: 19 additions & 0 deletions components/dash-core-components/src/utils/formatSliderTooltip.js
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() == []

0 comments on commit 49ac14f

Please sign in to comment.