Material-UI 滑块 - 使用刻度更改值
Material-UI Slider - Changing Values using Scale
我想在滑块上显示从 1000 到 1M 的范围并添加以下标记
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',
},
];
添加如下
<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>
这是结果:
我需要什么?
由于在范围开始时我显示了更多标记,因此易读性和用户体验很差。有没有一种方法可以使用 scale
以这样的方式显示范围,即需要 space 的 50-60% 来显示前 25% 的值,然后 space 显示休息?
下面是执行此操作的一种方法的工作示例。需要注意的关键是 min
、max
、step
和 value
(包括 marks
中的 value
)的值是线性的值。 scale
函数然后将这些转换为您要显示的非线性值。
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>
);
}
这是一个类似的示例,但针对范围滑块进行了修改:
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>
);
}
昨天没有时间处理这个问题,但 Ryan 的回答很有效。我正在考虑制作一个更通用的实现,您可以在其中传递范围,幻灯片将根据您发送的任何范围进行调整。
例如,如果您通过了:
[
{ value: 0 },
{ value: 5, label: 5 },
{ value: 10, label: 10 },
{ value: 15, label: 15 },
{ value: 20, label: 20 },
{ value: 100, label: 100 }
],
然后滑块 space 将被分成相等的部分,数组中的每个部分都获得相同的 space 但正如您所看到的,它们没有获得相同数量的值。在上面的示例中,前 80% 获得 20% 的值,后 20% 获得 80% 的值。
可以找到完整的演示 here
相关代码块如下:
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"
/>
);
}
得到一个工作示例,它允许值和缩放值的任意范围。
const [min, setMin] = useState(0);
const [max, setMax] = useState(0);
const prices = [
{
string: "0K",
label: "0K",
value: 0,
scaledValue: 800000,
},
{
string: ".2M",
label: ".2M",
value: 30,
scaledValue: 1200000,
},
{
string: "M",
label: "M",
value: 60,
scaledValue: 2000000,
},
{
string: "M",
label: "M",
value: 80,
scaledValue: 4000000,
},
{
string: "M",
label: "M",
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;
};
在你的组件中:
<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
如果您只需要 1 个值,可以删除最大值。
我想在滑块上显示从 1000 到 1M 的范围并添加以下标记
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',
},
];
添加如下
<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>
这是结果:
我需要什么?
由于在范围开始时我显示了更多标记,因此易读性和用户体验很差。有没有一种方法可以使用 scale
以这样的方式显示范围,即需要 space 的 50-60% 来显示前 25% 的值,然后 space 显示休息?
下面是执行此操作的一种方法的工作示例。需要注意的关键是 min
、max
、step
和 value
(包括 marks
中的 value
)的值是线性的值。 scale
函数然后将这些转换为您要显示的非线性值。
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>
);
}
这是一个类似的示例,但针对范围滑块进行了修改:
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>
);
}
昨天没有时间处理这个问题,但 Ryan 的回答很有效。我正在考虑制作一个更通用的实现,您可以在其中传递范围,幻灯片将根据您发送的任何范围进行调整。
例如,如果您通过了:
[
{ value: 0 },
{ value: 5, label: 5 },
{ value: 10, label: 10 },
{ value: 15, label: 15 },
{ value: 20, label: 20 },
{ value: 100, label: 100 }
],
然后滑块 space 将被分成相等的部分,数组中的每个部分都获得相同的 space 但正如您所看到的,它们没有获得相同数量的值。在上面的示例中,前 80% 获得 20% 的值,后 20% 获得 80% 的值。
可以找到完整的演示 here
相关代码块如下:
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"
/>
);
}
得到一个工作示例,它允许值和缩放值的任意范围。
const [min, setMin] = useState(0);
const [max, setMax] = useState(0);
const prices = [
{
string: "0K",
label: "0K",
value: 0,
scaledValue: 800000,
},
{
string: ".2M",
label: ".2M",
value: 30,
scaledValue: 1200000,
},
{
string: "M",
label: "M",
value: 60,
scaledValue: 2000000,
},
{
string: "M",
label: "M",
value: 80,
scaledValue: 4000000,
},
{
string: "M",
label: "M",
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;
};
在你的组件中:
<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
如果您只需要 1 个值,可以删除最大值。