Reputation: 54949
I want to display a range on the slider from 1000 to 1M and add the following markers
const followersMarks = [
{
value: 1000,
label: '1k',
},
{
value: 5000,
label: '5k',
},
{
value: 10000,
label: '10k',
},
{
value: 25000,
label: '25k',
},
{
value: 50000,
label: '50k',
},
{
value: 100000,
label: '100k',
},
{
value: 250000,
label: '250k',
},
{
value: 500000,
label: '500k',
},
{
value: 1000000,
label: '1M',
},
];
Am adding it as follows
<Form.Group controlId="formGridState">
<Form.Label className="mb-15">Followers Range</Form.Label>
<Slider
value={followersRange}
onChange={handleChangeFollowersRange}
valueLabelDisplay="auto"
aria-labelledby="range-slider"
step="1000"
valueLabelDisplay="on"
marks={followersMarks}
min={1000}
max={1000000}
/>
</Form.Group>
This is the result:
What do I need?
Since at the start of the range I am displaying more markers the legibility and UX are bad. Is there a way to use scale
to show the range in such way that it takes 50-60% of the space to show the first 25% of the values and then space out the rest?
Upvotes: 9
Views: 17064
Reputation: 81036
Below is a working example of one way to do this. The key thing to note is that the values for min
, max
, step
, and value
(including value
within marks
) are linear values. The scale
function then translates these to the non-linear values you want to display.
import React from "react";
import Typography from "@material-ui/core/Typography";
import Slider from "@material-ui/core/Slider";
const followersMarks = [
{
value: 0,
scaledValue: 1000,
label: "1k"
},
{
value: 25,
scaledValue: 5000,
label: "5k"
},
{
value: 50,
scaledValue: 10000,
label: "10k"
},
{
value: 75,
scaledValue: 25000,
label: "25k"
},
{
value: 100,
scaledValue: 50000,
label: "50k"
},
{
value: 125,
scaledValue: 100000,
label: "100k"
},
{
value: 150,
scaledValue: 250000,
label: "250k"
},
{
value: 175,
scaledValue: 500000,
label: "500k"
},
{
value: 200,
scaledValue: 1000000,
label: "1M"
}
];
const scale = value => {
const previousMarkIndex = Math.floor(value / 25);
const previousMark = followersMarks[previousMarkIndex];
const remainder = value % 25;
if (remainder === 0) {
return previousMark.scaledValue;
}
const nextMark = followersMarks[previousMarkIndex + 1];
const increment = (nextMark.scaledValue - previousMark.scaledValue) / 25;
return remainder * increment + previousMark.scaledValue;
};
function numFormatter(num) {
if (num > 999 && num < 1000000) {
return (num / 1000).toFixed(0) + "K"; // convert to K for number from > 1000 < 1 million
} else if (num >= 1000000) {
return (num / 1000000).toFixed(0) + "M"; // convert to M for number from > 1 million
} else if (num < 900) {
return num; // if value < 1000, nothing to do
}
}
export default function NonLinearSlider() {
const [value, setValue] = React.useState(1);
const handleChange = (event, newValue) => {
setValue(newValue);
};
return (
<div>
<Typography id="non-linear-slider" gutterBottom>
Followers
</Typography>
<Slider
style={{ maxWidth: 500 }}
value={value}
min={0}
step={1}
max={200}
valueLabelFormat={numFormatter}
marks={followersMarks}
scale={scale}
onChange={handleChange}
valueLabelDisplay="auto"
aria-labelledby="non-linear-slider"
/>
<Typography>Value: {scale(value)}</Typography>
</div>
);
}
Here is a similar example, but modified for a range slider:
import React from "react";
import Typography from "@material-ui/core/Typography";
import Slider from "@material-ui/core/Slider";
const followersMarks = [
{
value: 0,
scaledValue: 1000,
label: "1k"
},
{
value: 25,
scaledValue: 5000,
label: "5k"
},
{
value: 50,
scaledValue: 10000,
label: "10k"
},
{
value: 75,
scaledValue: 25000,
label: "25k"
},
{
value: 100,
scaledValue: 50000,
label: "50k"
},
{
value: 125,
scaledValue: 100000,
label: "100k"
},
{
value: 150,
scaledValue: 250000,
label: "250k"
},
{
value: 175,
scaledValue: 500000,
label: "500k"
},
{
value: 200,
scaledValue: 1000000,
label: "1M"
}
];
const scaleValues = (valueArray) => {
return [scale(valueArray[0]), scale(valueArray[1])];
};
const scale = (value) => {
if (value === undefined) {
return undefined;
}
const previousMarkIndex = Math.floor(value / 25);
const previousMark = followersMarks[previousMarkIndex];
const remainder = value % 25;
if (remainder === 0) {
return previousMark.scaledValue;
}
const nextMark = followersMarks[previousMarkIndex + 1];
const increment = (nextMark.scaledValue - previousMark.scaledValue) / 25;
return remainder * increment + previousMark.scaledValue;
};
function numFormatter(num) {
if (num > 999 && num < 1000000) {
return (num / 1000).toFixed(0) + "K"; // convert to K for number from > 1000 < 1 million
} else if (num >= 1000000) {
return (num / 1000000).toFixed(0) + "M"; // convert to M for number from > 1 million
} else if (num < 900) {
return num; // if value < 1000, nothing to do
}
}
export default function NonLinearSlider() {
const [value, setValue] = React.useState([1, 25]);
const handleChange = (event, newValue) => {
setValue(newValue);
};
return (
<div>
<Typography id="non-linear-slider" gutterBottom>
Followers
</Typography>
<Slider
style={{ maxWidth: 500 }}
value={value}
min={0}
step={1}
max={200}
valueLabelFormat={numFormatter}
marks={followersMarks}
scale={scaleValues}
onChange={handleChange}
valueLabelDisplay="auto"
aria-labelledby="non-linear-slider"
/>
<Typography>Values: {JSON.stringify(scaleValues(value))}</Typography>
</div>
);
}
Upvotes: 14
Reputation: 11
Got a working example which allows arbitrary ranges for values and scaled values.
const [min, setMin] = useState(0);
const [max, setMax] = useState(0);
const prices = [
{
string: "$800K",
label: "$800K",
value: 0,
scaledValue: 800000,
},
{
string: "$1.2M",
label: "$1.2M",
value: 30,
scaledValue: 1200000,
},
{
string: "$2M",
label: "$2M",
value: 60,
scaledValue: 2000000,
},
{
string: "$4M",
label: "$4M",
value: 80,
scaledValue: 4000000,
},
{
string: "$20M",
label: "$20M",
value: 100,
scaledValue: 20000000,
},
];
function valuetext(value) {
return `$${value}`;
};
function valueLabelFormat(value) {
return prices.findIndex((prices) => prices.value === value) + 1;
};
const descale = (scaledValue) => {
const priceIndex = prices.findIndex(
(price) => price.scaledValue >= scaledValue,
);
const price = prices[priceIndex];
if (price.scaledValue === scaledValue) {
return price.value;
}
if (priceIndex === 0) {
return 0;
}
const m =
(price.scaledValue - prices[priceIndex - 1].scaledValue) /
(price.value - prices[priceIndex - 1].value || 1);
const dX = scaledValue - prices[priceIndex - 1].scaledValue;
return dX / m + prices[priceIndex - 1].value;
};
const scale = (value): number => {
const priceIndex = prices.findIndex((price) => price.value >= value);
const price = prices[priceIndex];
if (price.value === value) {
return price.scaledValue;
}
const m =
(price.scaledValue - prices[priceIndex - 1].scaledValue) /
(price.value - prices[priceIndex - 1].value || 1);
const dX = value - prices[priceIndex - 1].value;
return m * dX + prices[priceIndex - 1].scaledValue;
};
In your component:
<Slider
onChange={(e, value) => {
setMin(scale(value[0]));
setMax(scale(value[1]));
}}
value={[
descale(min),
descale(max) ||
prices.slice(-1)[0].value,
]}
scale={scale}
marks={prices}
defaultValue={[
prices[0].value,
prices[prices.length - 1].value,
]}
valueLabelFormat={valueLabelFormat}
getAriaValueText={valuetext}
min={0}
max={100}
/>
Result: Result
You can remove max if you want only 1 value.
Upvotes: 1
Reputation: 39280
Did not have time yesterday to work on this but Ryan's answer works. I was thinking of making a more general implementation where you can pass ranges and the slide will adjust according to any range you sent it.
For example if you pass:
[
{ value: 0 },
{ value: 5, label: 5 },
{ value: 10, label: 10 },
{ value: 15, label: 15 },
{ value: 20, label: 20 },
{ value: 100, label: 100 }
],
Then the slider space will be broken up into equal parts, each part in the array gets the same space but as you can see they don't get the same amount of values. In the example above the first 80% gets 20% of the values and the last 20% gets 80% of the values.
Full demo can be found here
Here are the relevant code blocks:
const zip = (...arrays) =>
[...new Array(Math.max(...arrays.map(a => a.length)))]
.map((_, i) => i)
.map(i => arrays.map(a => a[i]));
const createRange = ([fromMin, fromMax], [toMin, toMax]) => {
const fromRange = fromMax - fromMin;
const toRange = toMax - toMin;
return fromValue => ((fromValue - fromMin) / fromRange) * toRange + toMin;
};
const createScale = (min, max, marks) => {
const zippedMarks = zip([undefined].concat(marks), marks);
const zone = (max - min) / (marks.length - 1);
const zones = marks.map((_, i) => [i * zone + min, (i + 1) * zone + min]);
const ranges = zippedMarks
.filter(([a, b]) => a !== undefined && b !== undefined)
.map(([a, b], i) => [
createRange(zones[i], [a.value, b.value]),
zones[i],
createRange([a.value, b.value], zones[i]),
[a.value, b.value]
]);
const sliderValToScaled = sliderVal =>
ranges.find(([, [low, high]]) => sliderVal >= low && sliderVal <= high)[0](
sliderVal
);
const scaledValToSlider = scaledVal =>
ranges.find(
([, , , [low, high]]) => scaledVal >= low && scaledVal <= high
)[2](scaledVal);
return [sliderValToScaled, scaledValToSlider];
};
export default function NonLinearSlider({
marks = [{ value: 0 }, { value: 1 }],
steps = 200,
onChange = x => x,
value = 0,
numFormatter
}) {
const handleChange = (event, newValue) => {
onChange(scale(newValue));
};
const [scale, unscale] = React.useMemo(() => createScale(0, 1, marks), [
marks
]);
const followersMarks = marks
.filter(mark => mark.label)
.map(mark => ({
...mark,
value: unscale(mark.value)
}));
return (
<Slider
style={{ maxWidth: 500 }}
value={unscale(value)}
min={0}
step={1 / steps}
max={1}
valueLabelFormat={numFormatter}
marks={followersMarks}
scale={scale}
onChange={handleChange}
valueLabelDisplay="auto"
aria-labelledby="non-linear-slider"
/>
);
}
Upvotes: 2