Maemaemae

React データ可視化における閾値ベース正規化の実装

React データ可視化における閾値ベース正規化の実装

データ可視化において、様々な範囲の数値を統一された0-1の範囲に変換する「正規化」は重要な処理です。閾値を設定した正規化により、異常値の処理や表示範囲の制御が効率的に行えます。

閾値ベース正規化とは

基本概念

閾値ベース正規化は、データの最小値・最大値ではなく、事前定義した閾値を基準に正規化を行う手法です。

// 基本的な正規化式
normalizedValue = (value - thresholdLower) / (thresholdUpper - thresholdLower)

通常の正規化との違い

// 通常の正規化(Min-Max Normalization)
const minMaxNormalize = (value: number, min: number, max: number) => {
  return (value - min) / (max - min);
};

// 閾値ベース正規化(クランプ付き)
const thresholdNormalize = (
  value: number,
  thresholdLower: number,
  thresholdUpper: number
) => {
  return Math.max(0, Math.min(1,
    (value - thresholdLower) / (thresholdUpper - thresholdLower)
  ));
};

React での基本実装

useMemo による最適化

import React, { useMemo } from 'react';

interface NormalizationProps {
  cellBoxValue: number;
  thresholdLower: number;
  thresholdUpper: number;
}

const DataVisualization: React.FC<NormalizationProps> = ({
  cellBoxValue,
  thresholdLower,
  thresholdUpper
}) => {
  const normalizedValue = useMemo(() => {
    return Math.max(
      0,
      Math.min(
        1,
        (cellBoxValue - thresholdLower) / (thresholdUpper - thresholdLower)
      )
    );
  }, [cellBoxValue, thresholdLower, thresholdUpper]);

  return (
    <div>
      <p>元の値: {cellBoxValue}</p>
      <p>正規化値: {normalizedValue.toFixed(3)}</p>
      {/* 正規化値に基づく可視化要素 */}
    </div>
  );
};

カスタムフックでの抽象化

import { useMemo } from 'react';

interface UseThresholdNormalizationProps {
  value: number;
  thresholdLower: number;
  thresholdUpper: number;
  precision?: number;
}

const useThresholdNormalization = ({
  value,
  thresholdLower,
  thresholdUpper,
  precision = 3
}: UseThresholdNormalizationProps) => {
  const normalizedValue = useMemo(() => {
    const normalized = (value - thresholdLower) / (thresholdUpper - thresholdLower);
    return Math.max(0, Math.min(1, normalized));
  }, [value, thresholdLower, thresholdUpper]);

  const formattedValue = useMemo(() => {
    return Number(normalizedValue.toFixed(precision));
  }, [normalizedValue, precision]);

  return {
    normalizedValue: formattedValue,
    isMinThreshold: value <= thresholdLower,
    isMaxThreshold: value >= thresholdUpper,
    originalValue: value
  };
};

実用的な活用例

データ可視化での色制御

const DataVisualizationChart: React.FC = () => {
  const sensorData = [15, 25, 45, 65, 85, 95];
  const thresholds = { lower: 20, upper: 80 };

  const chartData = useMemo(() => {
    return sensorData.map((value, index) => {
      const normalized = Math.max(0, Math.min(1,
        (value - thresholds.lower) / (thresholds.upper - thresholds.lower)
      ));

      return {
        x: index,
        y: value,
        normalizedY: normalized,
        // 正規化値を使用して色を決定
        color: value < thresholds.lower ? '#3b82f6' :
               value > thresholds.upper ? '#ef4444' : '#10b981'
      };
    });
  }, [sensorData]);

  return (
    <svg width="400" height="200">
      {chartData.map((point, index) => (
        <circle
          key={index}
          cx={point.x * 60 + 50}
          cy={200 - (point.normalizedY * 150 + 25)}
          r="6"
          fill={point.color}
        />
      ))}
    </svg>
  );
};

アニメーション制御

import { motion } from 'framer-motion';

const AnimatedComponent: React.FC<{ value: number }> = ({ value }) => {
  const { normalizedValue } = useThresholdNormalization({
    value,
    thresholdLower: 0,
    thresholdUpper: 100
  });

  return (
    <motion.div
      animate={{
        scale: 0.5 + normalizedValue * 0.5, // 0.5-1.0の範囲
        opacity: 0.3 + normalizedValue * 0.7, // 0.3-1.0の範囲
      }}
      transition={{ duration: 0.3 }}
      style={{
        width: 100,
        height: 100,
        backgroundColor: `hsl(${120 * normalizedValue}, 70%, 50%)`,
        borderRadius: '50%'
      }}
    >
      {normalizedValue.toFixed(3)}
    </motion.div>
  );
};

複数データセットの処理

interface DataSet {
  name: string;
  values: number[];
  thresholds: { lower: number; upper: number };
}

const MultiDataNormalization: React.FC<{ datasets: DataSet[] }> = ({
  datasets
}) => {
  const normalizedDatasets = useMemo(() => {
    return datasets.map(dataset => ({
      ...dataset,
      normalizedValues: dataset.values.map(value =>
        Math.max(0, Math.min(1,
          (value - dataset.thresholds.lower) /
          (dataset.thresholds.upper - dataset.thresholds.lower)
        ))
      )
    }));
  }, [datasets]);

  return (
    <div className="grid gap-4">
      {normalizedDatasets.map((dataset, index) => (
        <div key={index} className="border p-4 rounded">
          <h3 className="font-bold">{dataset.name}</h3>
          <p className="text-sm text-gray-600">
            閾値: {dataset.thresholds.lower} - {dataset.thresholds.upper}
          </p>
          <div className="flex gap-2 mt-2">
            {dataset.normalizedValues.map((normalized, i) => (
              <div
                key={i}
                className="w-8 h-8 rounded flex items-center justify-center text-xs"
                style={{
                  backgroundColor: `rgba(59, 130, 246, ${normalized})`,
                  color: normalized > 0.5 ? 'white' : 'black'
                }}
              >
                {normalized.toFixed(2)}
              </div>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
};

動的閾値調整

const DynamicThresholdDemo: React.FC = () => {
  const [data] = useState([10, 25, 45, 60, 80, 95]);
  const [thresholdLower, setThresholdLower] = useState(20);
  const [thresholdUpper, setThresholdUpper] = useState(80);

  const normalizedData = useMemo(() => {
    return data.map(value => ({
      original: value,
      normalized: Math.max(0, Math.min(1,
        (value - thresholdLower) / (thresholdUpper - thresholdLower)
      ))
    }));
  }, [data, thresholdLower, thresholdUpper]);

  return (
    <div className="p-6">
      <div className="mb-4 space-y-2">
        <label className="block">
          下限閾値: {thresholdLower}
          <input
            type="range"
            min="0"
            max="50"
            value={thresholdLower}
            onChange={(e) => setThresholdLower(Number(e.target.value))}
            className="block w-full mt-1"
          />
        </label>

        <label className="block">
          上限閾値: {thresholdUpper}
          <input
            type="range"
            min="50"
            max="100"
            value={thresholdUpper}
            onChange={(e) => setThresholdUpper(Number(e.target.value))}
            className="block w-full mt-1"
          />
        </label>
      </div>

      <div className="grid grid-cols-6 gap-2">
        {normalizedData.map((item, index) => (
          <div key={index} className="text-center">
            <div
              className="w-12 h-12 rounded mx-auto mb-1"
              style={{
                backgroundColor: `hsl(${item.normalized * 120}, 70%, 50%)`,
              }}
            />
            <div className="text-xs">
              <div>{item.original}</div>
              <div className="text-gray-500">{item.normalized.toFixed(2)}</div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

パフォーマンス最適化

大量データの効率的処理

const batchNormalize = (
  values: number[],
  lower: number,
  upper: number
): number[] => {
  const range = upper - lower;

  if (range === 0) {
    return new Array(values.length).fill(0);
  }

  return values.map(value =>
    Math.max(0, Math.min(1, (value - lower) / range))
  );
};

// 使用例
const LargeDataVisualization: React.FC = () => {
  const [largeDataset] = useState(() =>
    Array.from({ length: 1000 }, () => Math.random() * 200)
  );

  const normalizedData = useMemo(() =>
    batchNormalize(largeDataset, 20, 180),
    [largeDataset]
  );

  return (
    <div className="grid grid-cols-20 gap-1">
      {normalizedData.slice(0, 100).map((value, index) => (
        <div
          key={index}
          className="w-4 h-4"
          style={{
            backgroundColor: `rgba(59, 130, 246, ${value})`,
          }}
        />
      ))}
    </div>
  );
};

まとめ

閾値ベース正規化は、データ可視化において柔軟で強力な手法です。

主要なメリット

実装のポイント

適切に実装された閾値正規化により、より直感的で使いやすいデータ可視化インターフェースを構築できます。

参考資料


この記事は React 18.x、TypeScript 5.x、Node.js 18.x以上を基準としています。

Reactデータ正規化データ可視化TypeScript数値処理