diff --git a/demo/ts/app.tsx b/demo/ts/app.tsx index 5aa5d995b..8a0888e3b 100644 --- a/demo/ts/app.tsx +++ b/demo/ts/app.tsx @@ -203,7 +203,7 @@ class App extends React.Component { super(props); this.state = { - route: window.location.hash.substr(1), + route: window.location.hash.slice(1), }; if (this.state.route === "") { @@ -280,9 +280,7 @@ class App extends React.Component { ) : ( -
- -
+ )} diff --git a/demo/ts/components/theme-builder.tsx b/demo/ts/components/theme-builder.tsx deleted file mode 100644 index 8800596c6..000000000 --- a/demo/ts/components/theme-builder.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from "react"; -import { VictoryTheme } from "victory-core"; - -const ThemeBuilder = () => { - const [theme, setTheme] = React.useState(VictoryTheme.material); - - return
Theme Builder
; -}; -export default ThemeBuilder; diff --git a/demo/ts/components/theme-builder/config-preview.tsx b/demo/ts/components/theme-builder/config-preview.tsx new file mode 100644 index 000000000..8c415742e --- /dev/null +++ b/demo/ts/components/theme-builder/config-preview.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { VictoryThemeDefinition } from "victory-core/lib"; + +type ConfigPreviewProps = { + config: VictoryThemeDefinition; + onClose: () => void; +}; + +const containerStyles: React.CSSProperties = { + position: "fixed", + top: 0, + right: 0, + bottom: 0, + width: "100%", + padding: 20, + backgroundColor: "rgba(255, 255, 255, 0.9)", + zIndex: 1000, +}; + +const copyButtonContainerStyles: React.CSSProperties = { + display: "flex", + alignItems: "center", + gap: 10, + marginBottom: 20, +}; + +const copyStatusStyles: React.CSSProperties = { + color: "#666", + fontSize: 12, + fontStyle: "italic", +}; + +const codeStyles: React.CSSProperties = { + overflow: "auto", + maxHeight: 600, + border: "1px solid #ccc", + padding: 10, + backgroundColor: "#f9f9f9", +}; + +const closeButtonStyles: React.CSSProperties = { + position: "absolute", + top: 10, + right: 10, + background: "transparent", + border: "none", + fontSize: "20px", + cursor: "pointer", +}; + +const ConfigPreview = ({ config, onClose }: ConfigPreviewProps) => { + const [copyStatus, setCopyStatus] = React.useState(null); + + const handleCopyThemeConfig = () => { + navigator.clipboard + .writeText(JSON.stringify(config, null, 2)) + .then(() => { + setCopyStatus("Copied successfully."); + return "Theme config copied to clipboard"; + }) + .catch(() => { + setCopyStatus("Failed to copy."); + }); + }; + + const handleClose = () => { + onClose(); + }; + + return ( +
+
+ + {copyStatus && {copyStatus}} +
+

Theme Config Preview

+
{JSON.stringify(config, null, 2)}
+ +
+ ); +}; +export default ConfigPreview; diff --git a/demo/ts/components/theme-builder/custom-options.tsx b/demo/ts/components/theme-builder/custom-options.tsx new file mode 100644 index 000000000..aaef4a441 --- /dev/null +++ b/demo/ts/components/theme-builder/custom-options.tsx @@ -0,0 +1,76 @@ +import React from "react"; +import { ThemeOption } from "."; +import ConfigPreview from "./config-preview"; + +type ColorChangeArgs = { + event: React.ChangeEvent; + index: number; + colorScale: string; +}; + +type CustomOptionsProps = { + activeTheme: ThemeOption; + onColorChange: (args: ColorChangeArgs) => void; +}; + +const colorContainer: React.CSSProperties = { + display: "grid", + gridTemplateColumns: "repeat(4, 1fr)", + gap: 20, +}; + +const colorPickerStyles: React.CSSProperties = { + width: 50, + height: 50, +}; + +const CustomOptions = ({ activeTheme, onColorChange }: CustomOptionsProps) => { + const [showThemeConfigPreview, setShowThemeConfigPreview] = + React.useState(false); + + const onThemeConfigPreviewOpen = () => { + setShowThemeConfigPreview(true); + }; + + const onThemeConfigPreviewClose = () => { + setShowThemeConfigPreview(false); + }; + return ( + <> +
+ +
+

Color Scales

+

Qualitative

+
+ {activeTheme.config?.palette?.qualitative?.map((color, index) => ( + + onColorChange({ + event, + index, + colorScale: "qualitative", + }) + } + /> + ))} +
+
+
+ + {showThemeConfigPreview && activeTheme.config && ( + + )} + + ); +}; +export default CustomOptions; diff --git a/demo/ts/components/theme-builder/index.tsx b/demo/ts/components/theme-builder/index.tsx new file mode 100644 index 000000000..85905c4a1 --- /dev/null +++ b/demo/ts/components/theme-builder/index.tsx @@ -0,0 +1,228 @@ +import React from "react"; +import { + ColorScalePropType, + VictoryTheme, + VictoryThemeDefinition, +} from "victory-core"; +import ThemePicker from "./theme-picker"; +import { VictoryChart } from "victory-chart"; +import { VictoryAxis } from "victory-axis"; +import { VictoryStack } from "victory-stack"; +import { VictoryBar } from "victory-bar"; +import { VictoryArea } from "victory-area"; +import CustomOptions from "./custom-options"; + +export type ThemeOption = { + name: string; + config?: VictoryThemeDefinition; +}; + +const themes: ThemeOption[] = [ + { name: "Clean", config: VictoryTheme.clean }, + { name: "Material", config: VictoryTheme.material }, + { name: "Grayscale", config: VictoryTheme.grayscale }, +]; + +const sampleStackData = [ + { + x: 1, + y: 2, + }, + { + x: 2, + y: 3, + }, + { + x: 3, + y: 5, + }, + { + x: 4, + y: 4, + }, + { + x: 5, + y: 7, + }, +]; + +const containerStyles: React.CSSProperties = { + display: "flex", + flexDirection: "row", + flexWrap: "wrap", + alignItems: "flex-start", + justifyContent: "flex-start", + width: "100%", +}; + +const sidebarStyles: React.CSSProperties = { + height: "100%", + borderRight: "1px solid #ccc", + padding: "20px", + width: 300, +}; + +const tabContainerStyles: React.CSSProperties = { + display: "flex", + cursor: "pointer", + marginBottom: 20, + fontSize: 14, +}; + +const previewContainerStyles: React.CSSProperties = { + flex: 1, + padding: "0 20px", +}; + +const getTabStyles = (isActive): React.CSSProperties => ({ + padding: "10px 20px", + borderBottom: "2px solid lightgray", + fontWeight: "bold", + color: "gray", + ...(isActive && { + borderBottom: "2px solid black", + color: "#2165E3", + }), +}); + +const chartsGridStyles: React.CSSProperties = { + display: "grid", + gridTemplateColumns: "1fr 1fr", + gap: 20, +}; + +const chartStyle: { [key: string]: React.CSSProperties } = { + parent: { + border: "1px solid #ccc", + width: "100%", + height: 400, + display: "flex", + justifyContent: "center", + alignItems: "center", + }, +}; + +const tabConfig = [ + { + name: "themes", + label: "Default Themes", + Children: ThemePicker, + }, + { + name: "customize", + label: "Customize", + Children: CustomOptions, + }, +]; + +const ThemeBuilder = () => { + const [activeTheme, setActiveTheme] = React.useState(themes[0]); + const [activeColorScale] = React.useState( + "qualitative", + ); + const [activeTab, setActiveTab] = React.useState(tabConfig[0].name); + + const handleTabChange = (tabName: string) => { + setActiveTab(tabName); + }; + + const handleThemeSelect = (selectedTheme: ThemeOption) => { + if (!selectedTheme) return; + setActiveTheme(selectedTheme); + }; + + const handleColorChange = ({ event, index, colorScale }) => { + const newColor = event.target.value; + const customTheme = { + ...activeTheme, + name: "Custom", + config: { + ...activeTheme.config, + palette: { + ...activeTheme.config?.palette, + [colorScale]: activeTheme.config?.palette?.[colorScale]?.map( + (color, i) => (i === index ? newColor : color), + ), + }, + }, + }; + setActiveTheme(customTheme); + }; + + return ( +
+ +
+

Example Charts

+
+
+

Bar Chart

+ + + + + {[...Array(5)].map((_, i) => ( + + ))} + + +
+
+

Area Chart

+ + + + + {[...Array(5)].map((_, i) => ( + + ))} + + +
+
+
+
+ ); +}; +export default ThemeBuilder; diff --git a/demo/ts/components/theme-builder/theme-picker.tsx b/demo/ts/components/theme-builder/theme-picker.tsx new file mode 100644 index 000000000..26f8116f4 --- /dev/null +++ b/demo/ts/components/theme-builder/theme-picker.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import { ThemeOption } from "."; + +type ThemePickerProps = { + themes: ThemeOption[]; + activeTheme: ThemeOption; + onSelect?: (theme: ThemeOption) => void; +}; + +const buttonStyle: React.CSSProperties = { + padding: "5px 10px", + margin: "5px", + cursor: "pointer", + border: "none", + borderRadius: "5px", + background: "none", +}; + +const activeButtonStyle: React.CSSProperties = { + ...buttonStyle, + background: "#eee", + color: "#2165E3", +}; + +const ThemePicker = ({ themes, activeTheme, onSelect }: ThemePickerProps) => { + const handleSelect = (theme: ThemeOption) => { + if (onSelect) { + onSelect(theme); + } + }; + + return ( + <> + {themes.map((theme, i) => ( + + ))} + + ); +}; +export default ThemePicker;