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 显示休息?

下面是执行此操作的一种方法的工作示例。需要注意的关键是 minmaxstepvalue(包括 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 个值,可以删除最大值。