Skip to content

Commit 49ac14f

Browse files
authored
Merge pull request #2723 from plotly/slider-tips
Add Slider/RangeSlider tooltip.format and tooltip.style.
2 parents 9920073 + 06fb03a commit 49ac14f

File tree

9 files changed

+197
-15
lines changed

9 files changed

+197
-15
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ This project adheres to [Semantic Versioning](https://semver.org/).
66

77
## Added
88
- [#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)
9+
- [#2723](https://github.com/plotly/dash/pull/2723) Improve dcc Slider/RangeSlider tooltips. Fixes [#1846](https://github.com/plotly/dash/issues/1846)
10+
- Add `tooltip.format` a string for the format template, {value} will be formatted with the actual value.
11+
- Add `tooltip.style` a style object to give to the div of the tooltip.
912
- [#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")})`
1013

1114
## Fixed

components/dash-core-components/src/components/RangeSlider.react.js

+25
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,31 @@ RangeSlider.propTypes = {
125125
'bottomLeft',
126126
'bottomRight',
127127
]),
128+
/**
129+
* Template string to display the tooltip in.
130+
* Must contain `{value}`, which will be replaced with either
131+
* the default string representation of the value or the result of the
132+
* transform function if there is one.
133+
*/
134+
template: PropTypes.string,
135+
/**
136+
* Custom style for the tooltip.
137+
*/
138+
style: PropTypes.object,
139+
/**
140+
* Reference to a function in the `window.dccFunctions` namespace.
141+
* This can be added in a script in the asset folder.
142+
*
143+
* For example, in `assets/tooltip.js`:
144+
* ```
145+
* window.dccFunctions = window.dccFunctions || {};
146+
* window.dccFunctions.multByTen = function(value) {
147+
* return value * 10;
148+
* }
149+
* ```
150+
* Then in the component `tooltip={'transform': 'multByTen'}`
151+
*/
152+
transform: PropTypes.string,
128153
}),
129154

130155
/**

components/dash-core-components/src/components/Slider.react.js

+27
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import React, {Component, lazy, Suspense} from 'react';
22
import PropTypes from 'prop-types';
33
import slider from '../utils/LazyLoader/slider';
44

5+
import './css/sliders.css';
6+
57
const RealSlider = lazy(slider);
68

79
/**
@@ -105,6 +107,31 @@ Slider.propTypes = {
105107
'bottomLeft',
106108
'bottomRight',
107109
]),
110+
/**
111+
* Template string to display the tooltip in.
112+
* Must contain `{value}`, which will be replaced with either
113+
* the default string representation of the value or the result of the
114+
* transform function if there is one.
115+
*/
116+
template: PropTypes.string,
117+
/**
118+
* Custom style for the tooltip.
119+
*/
120+
style: PropTypes.object,
121+
/**
122+
* Reference to a function in the `window.dccFunctions` namespace.
123+
* This can be added in a script in the asset folder.
124+
*
125+
* For example, in `assets/tooltip.js`:
126+
* ```
127+
* window.dccFunctions = window.dccFunctions || {};
128+
* window.dccFunctions.multByTen = function(value) {
129+
* return value * 10;
130+
* }
131+
* ```
132+
* Then in the component `tooltip={'transform': 'multByTen'}`
133+
*/
134+
transform: PropTypes.string,
108135
}),
109136

110137
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/* Fix the default tooltip style height conflicting with the actual size of the tooltip. */
2+
.rc-slider-tooltip-content > .rc-slider-tooltip-inner {
3+
height: unset;
4+
min-height: 20px;
5+
}

components/dash-core-components/src/fragments/RangeSlider.react.js

+29-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, {Component} from 'react';
2-
import {assoc, pick, isNil} from 'ramda';
2+
import {assoc, pick, isNil, pipe, omit} from 'ramda';
33
import {Range, createSliderWithTooltip} from 'rc-slider';
44
import computeSliderStyle from '../utils/computeSliderStyle';
55

@@ -11,6 +11,10 @@ import {
1111
setUndefined,
1212
} from '../utils/computeSliderMarkers';
1313
import {propTypes, defaultProps} from '../components/RangeSlider.react';
14+
import {
15+
formatSliderTooltip,
16+
transformSliderTooltip,
17+
} from '../utils/formatSliderTooltip';
1418

1519
const sliderProps = [
1620
'min',
@@ -72,17 +76,33 @@ export default class RangeSlider extends Component {
7276
} = this.props;
7377
const value = this.state.value;
7478

75-
let tipProps;
76-
if (tooltip && tooltip.always_visible) {
79+
let tipProps, tipFormatter;
80+
if (tooltip) {
7781
/**
7882
* clone `tooltip` but with renamed key `always_visible` -> `visible`
79-
* the rc-tooltip API uses `visible`, but `always_visible is more semantic
83+
* the rc-tooltip API uses `visible`, but `always_visible` is more semantic
8084
* assigns the new (renamed) key to the old key and deletes the old key
8185
*/
82-
tipProps = assoc('visible', tooltip.always_visible, tooltip);
83-
delete tipProps.always_visible;
84-
} else {
85-
tipProps = tooltip;
86+
tipProps = pipe(
87+
assoc('visible', tooltip.always_visible),
88+
omit(['always_visible', 'template', 'style', 'transform'])
89+
)(tooltip);
90+
if (tooltip.template || tooltip.style || tooltip.transform) {
91+
tipFormatter = tipValue => {
92+
let t = tipValue;
93+
if (tooltip.transform) {
94+
t = transformSliderTooltip(tooltip.transform, tipValue);
95+
}
96+
return (
97+
<div style={tooltip.style}>
98+
{formatSliderTooltip(
99+
tooltip.template || '{value}',
100+
t
101+
)}
102+
</div>
103+
);
104+
};
105+
}
86106
}
87107

88108
return (
@@ -116,6 +136,7 @@ export default class RangeSlider extends Component {
116136
...tipProps,
117137
getTooltipContainer: node => node,
118138
}}
139+
tipFormatter={tipFormatter}
119140
style={{position: 'relative'}}
120141
value={value ? value : calcValue(min, max, value)}
121142
marks={sanitizeMarks({min, max, marks, step})}

components/dash-core-components/src/fragments/Slider.react.js

+28-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, {Component} from 'react';
22
import ReactSlider, {createSliderWithTooltip} from 'rc-slider';
3-
import {assoc, isNil, pick} from 'ramda';
3+
import {assoc, isNil, pick, pipe, omit} from 'ramda';
44
import computeSliderStyle from '../utils/computeSliderStyle';
55

66
import 'rc-slider/assets/index.css';
@@ -11,6 +11,10 @@ import {
1111
setUndefined,
1212
} from '../utils/computeSliderMarkers';
1313
import {propTypes, defaultProps} from '../components/Slider.react';
14+
import {
15+
formatSliderTooltip,
16+
transformSliderTooltip,
17+
} from '../utils/formatSliderTooltip';
1418

1519
const sliderProps = [
1620
'min',
@@ -72,17 +76,33 @@ export default class Slider extends Component {
7276
} = this.props;
7377
const value = this.state.value;
7478

75-
let tipProps;
76-
if (tooltip && tooltip.always_visible) {
79+
let tipProps, tipFormatter;
80+
if (tooltip) {
7781
/**
7882
* clone `tooltip` but with renamed key `always_visible` -> `visible`
7983
* the rc-tooltip API uses `visible`, but `always_visible` is more semantic
8084
* assigns the new (renamed) key to the old key and deletes the old key
8185
*/
82-
tipProps = assoc('visible', tooltip.always_visible, tooltip);
83-
delete tipProps.always_visible;
84-
} else {
85-
tipProps = tooltip;
86+
tipProps = pipe(
87+
assoc('visible', tooltip.always_visible),
88+
omit(['always_visible', 'template', 'style', 'transform'])
89+
)(tooltip);
90+
if (tooltip.template || tooltip.style || tooltip.transform) {
91+
tipFormatter = tipValue => {
92+
let t = tipValue;
93+
if (tooltip.transform) {
94+
t = transformSliderTooltip(tooltip.transform, tipValue);
95+
}
96+
return (
97+
<div style={tooltip.style}>
98+
{formatSliderTooltip(
99+
tooltip.template || '{value}',
100+
t
101+
)}
102+
</div>
103+
);
104+
};
105+
}
86106
}
87107

88108
return (
@@ -116,6 +136,7 @@ export default class Slider extends Component {
116136
...tipProps,
117137
getTooltipContainer: node => node,
118138
}}
139+
tipFormatter={tipFormatter}
119140
style={{position: 'relative'}}
120141
value={value}
121142
marks={sanitizeMarks({min, max, marks, step})}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {replace, path, split, concat, pipe} from 'ramda';
2+
3+
export const formatSliderTooltip = (template, value) => {
4+
return replace('{value}', value, template);
5+
};
6+
7+
export const transformSliderTooltip = (funcName, value) => {
8+
const func = pipe(
9+
split('.'),
10+
s => concat(['dccFunctions'], s),
11+
s => path(s, window)
12+
)(funcName);
13+
if (!func) {
14+
throw new Error(
15+
`Invalid func for slider tooltip transform: ${funcName}`
16+
);
17+
}
18+
return func(value);
19+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
window.dccFunctions = window.dccFunctions || {};
2+
window.dccFunctions.transformTooltip = function(value) {
3+
return "Transformed " + value
4+
}

components/dash-core-components/tests/integration/sliders/test_sliders.py

+57
Original file line numberDiff line numberDiff line change
@@ -559,3 +559,60 @@ def test_slsl015_range_slider_no_min_max(dash_dcc):
559559
)
560560

561561
assert dash_dcc.get_logs() == []
562+
563+
564+
def test_sls016_sliders_format_tooltips(dash_dcc):
565+
app = Dash(__name__)
566+
app.layout = html.Div(
567+
[
568+
dcc.Slider(
569+
value=34,
570+
min=20,
571+
max=100,
572+
id="slider",
573+
tooltip={
574+
"template": "Custom tooltip: {value}",
575+
"always_visible": True,
576+
"style": {"padding": "8px"},
577+
},
578+
),
579+
dcc.RangeSlider(
580+
value=[48, 60],
581+
min=20,
582+
max=100,
583+
id="range-slider",
584+
tooltip={"template": "Custom tooltip: {value}", "always_visible": True},
585+
),
586+
dcc.Slider(
587+
min=20,
588+
max=100,
589+
id="slider-transform",
590+
tooltip={"always_visible": True, "transform": "transformTooltip"},
591+
),
592+
],
593+
style={"padding": "12px", "marginTop": "48px"},
594+
)
595+
596+
dash_dcc.start_server(app)
597+
# dash_dcc.wait_for_element("#slider")
598+
599+
dash_dcc.wait_for_text_to_equal(
600+
"#slider .rc-slider-tooltip-content", "Custom tooltip: 34"
601+
)
602+
dash_dcc.wait_for_text_to_equal(
603+
"#range-slider .rc-slider-tooltip-content", "Custom tooltip: 48"
604+
)
605+
dash_dcc.wait_for_text_to_equal(
606+
"#range-slider > div:nth-child(1) > div:last-child .rc-slider-tooltip-content",
607+
"Custom tooltip: 60",
608+
)
609+
dash_dcc.wait_for_style_to_equal(
610+
"#slider .rc-slider-tooltip-inner > div", "padding", "8px"
611+
)
612+
dash_dcc.wait_for_text_to_equal(
613+
"#slider-transform .rc-slider-tooltip-content", "Transformed 20"
614+
)
615+
616+
dash_dcc.percy_snapshot("sliders-format-tooltips")
617+
618+
assert dash_dcc.get_logs() == []

0 commit comments

Comments
 (0)