Skip to content

Commit

Permalink
feat(ui): enhance control panel components
Browse files Browse the repository at this point in the history
- Add NumberInput component with slider and input field
- Improve ColorTableInput with color scheme flip button
- Update single color picker to persist color state
  • Loading branch information
hongfaqiu committed Nov 1, 2024
1 parent 6f15c38 commit 4dab0d9
Showing 1 changed file with 191 additions and 37 deletions.
228 changes: 191 additions & 37 deletions example/src/components/ColorTableInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { useState } from 'react';
import { Select } from 'antd';
import React, { useState, useMemo } from 'react';
import { Select, ColorPicker } from 'antd';
import type { Color } from 'antd/es/color-picker';
import { SwapOutlined } from '@ant-design/icons';
import {
interpolateRainbow,
interpolateViridis,
Expand All @@ -16,9 +18,19 @@ import {
} from 'd3-scale-chromatic';
import styled from 'styled-components';

const Container = styled.div`
display: flex;
gap: 8px;
align-items: flex-start;
`;

const SelectContainer = styled.div`
flex: 1;
`;

const ColorPreview = styled.div`
display: flex;
height: 10px; // Reduced height from 20px to 10px
height: 10px;
width: 100%;
`;

Expand All @@ -27,6 +39,56 @@ const ColorSegment = styled.div<{ color: string }>`
background-color: ${(props) => props.color};
`;

const ColorSchemeOption = styled.div`
display: flex;
align-items: center;
gap: 8px;
`;

const ColorSchemeInfo = styled.div`
flex: 1;
`;

const ColorSchemePreview = styled.div`
display: flex;
align-items: center;
gap: 8px;
`;

const FlipButton = styled.button`
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
padding: 0;
background: transparent;
border: none;
cursor: pointer;
color: rgba(0, 0, 0, 0.45);
transition: all 0.3s;
border-radius: 4px;
margin-top: 4px;
&:hover {
color: rgba(0, 0, 0, 0.85);
background: rgba(0, 0, 0, 0.04);
}
&:active {
background: rgba(0, 0, 0, 0.08);
}
&:focus {
outline: none;
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
`;

interface ColorTableInputProps {
value?: string[];
onChange?: (value: string[]) => void;
Expand All @@ -46,8 +108,12 @@ const generateColorTable = (
return colors;
};

const generateSingleColorTable = (color: string): string[] => {
return Array(20).fill(color);
};

export const colorSchemes = [
{ label: 'White', value: 'white', interpolator: () => 'white' },
{ label: 'Single Color', value: 'single', interpolator: () => '#FFFFFF', isSingleColor: true },
{ label: 'Rainbow', value: 'rainbow', interpolator: interpolateRainbow, reverse: true },
{ label: 'Viridis', value: 'viridis', interpolator: interpolateViridis },
{ label: 'Cool', value: 'cool', interpolator: interpolateCool },
Expand All @@ -62,64 +128,152 @@ export const colorSchemes = [
{ label: 'Purples', value: 'purples', interpolator: interpolatePurples },
].map((item) => ({
...item,
colors: generateColorTable(item.interpolator, true),
colors: item.isSingleColor
? generateSingleColorTable(item.interpolator())
: generateColorTable(item.interpolator, item.reverse),
}));

const ColorTableInput: React.FC<ColorTableInputProps> = ({
value = [],
onChange,
}) => {
const [selectedScheme, setSelectedScheme] = useState(() => {
// Find matching color scheme
const matchingScheme = colorSchemes.find(
(scheme) => JSON.stringify(scheme.colors) === JSON.stringify(value),
);
return matchingScheme ? matchingScheme.value : 'rainbow';
});

const handleChange = (newValue: string) => {
const [singleColor, setSingleColor] = useState<string>(() => {
if (value.length > 0 && value.every(color => color === value[0])) {
return value[0];
}
return '#FFFFFF';
});

const [reversedSchemes, setReversedSchemes] = useState<Record<string, boolean>>({});

const currentSchemes = useMemo(() => {
return colorSchemes.map(scheme => {
let colors = scheme.isSingleColor
? generateSingleColorTable(singleColor)
: scheme.colors;

if (reversedSchemes[scheme.value]) {
colors = [...colors].reverse();
}

return {
...scheme,
colors
};
});
}, [singleColor, reversedSchemes]);

const handleSchemeChange = (newValue: string) => {
setSelectedScheme(newValue);
const scheme = colorSchemes.find((s) => s.value === newValue);
const scheme = currentSchemes.find((s) => s.value === newValue);
if (scheme) {
onChange?.(scheme.colors);
if (scheme.isSingleColor) {
const colors = generateSingleColorTable(singleColor);
onChange?.(colors);
} else {
onChange?.(scheme.colors);
}
}
};

const handleColorChange = (color: Color) => {
const hexColor = color.toHexString();
setSingleColor(hexColor);
if (selectedScheme === 'single') {
const colors = generateSingleColorTable(hexColor);
onChange?.(colors);
}
};

const handleFlip = (schemeValue: string, e: React.MouseEvent) => {
e.stopPropagation();
setReversedSchemes(prev => {
const newReversed = {
...prev,
[schemeValue]: !prev[schemeValue]
};

if (schemeValue === selectedScheme) {
const scheme = currentSchemes.find(s => s.value === schemeValue);
if (scheme) {
const newColors = [...scheme.colors].reverse();
onChange?.(newColors);
}
}

return newReversed;
});
};

const renderColorPreview = (scheme: (typeof colorSchemes)[0]) => {
const segments = 20;
const colors = scheme.isSingleColor
? generateSingleColorTable(singleColor)
: scheme.colors;

return (
<ColorPreview>
{Array.from({ length: segments }).map((_, i) => (
<ColorSegment
key={i}
color={scheme.colors[i]}
/>
))}
</ColorPreview>
<ColorSchemePreview>
<ColorPreview>
{Array.from({ length: segments }).map((_, i) => (
<ColorSegment
key={i}
color={colors[i]}
/>
))}
</ColorPreview>
{!scheme.isSingleColor && (
<FlipButton
onClick={(e) => handleFlip(scheme.value, e)}
title="Flip Colors"
>
<SwapOutlined />
</FlipButton>
)}
</ColorSchemePreview>
);
};

return (
<Select
style={{ width: '100%' }}
value={selectedScheme}
onChange={handleChange}
labelRender={(selectedValue) => {
const scheme = colorSchemes.find(
(s) => s.value === selectedValue.value,
);
return scheme ? renderColorPreview(scheme) : null;
}}
options={colorSchemes.map((scheme) => ({
value: scheme.value,
label: (
<div>
<div>{scheme.label}</div>
{renderColorPreview(scheme)}
</div>
),
}))}
/>
<Container>
<SelectContainer>
<Select
style={{ width: '100%' }}
value={selectedScheme}
onChange={handleSchemeChange}
labelRender={(selectedValue) => {
const scheme = currentSchemes.find(
(s) => s.value === selectedValue.value,
);
return scheme ? renderColorPreview(scheme) : null;
}}
options={currentSchemes.map((scheme) => ({
value: scheme.value,
label: (
<ColorSchemeOption>
<ColorSchemeInfo>
<div>{scheme.label}</div>
{renderColorPreview(scheme)}
</ColorSchemeInfo>
</ColorSchemeOption>
),
}))}
/>
</SelectContainer>
{selectedScheme === 'single' && (
<ColorPicker
value={singleColor}
onChange={handleColorChange}
size="small"
/>
)}
</Container>
);
};

Expand Down

0 comments on commit 4dab0d9

Please sign in to comment.