change display layout to node editor

This commit is contained in:
falsycat 2025-07-12 15:54:39 +09:00
parent 5804a6cbff
commit 84a2a50922
8 changed files with 368 additions and 107 deletions

236
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"@xyflow/react": "^12.8.2",
"i18next": "^25.3.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@ -1689,6 +1690,55 @@
"@babel/types": "^7.20.7"
}
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-drag": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-selection": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
"license": "MIT"
},
"node_modules/@types/d3-transition": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-zoom": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
"license": "MIT",
"dependencies": {
"@types/d3-interpolate": "*",
"@types/d3-selection": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@ -1700,14 +1750,14 @@
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.23",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
"integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@ -1745,6 +1795,38 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0"
}
},
"node_modules/@xyflow/react": {
"version": "12.8.2",
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.2.tgz",
"integrity": "sha512-VifLpxOy74ck283NQOtBn1e8igmB7xo7ADDKxyBHkKd8IKpyr16TgaYOhzqVwNMdB4NT+m++zfkic530L+gEXw==",
"license": "MIT",
"dependencies": {
"@xyflow/system": "0.0.66",
"classcat": "^5.0.3",
"zustand": "^4.4.0"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@xyflow/system": {
"version": "0.0.66",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.66.tgz",
"integrity": "sha512-TTxESDwPsATnuDMUeYYtKe4wt9v8bRO29dgYBhR8HyhSCzipnAdIL/1CDfFd+WqS1srVreo24u6zZeVIDk4r3Q==",
"license": "MIT",
"dependencies": {
"@types/d3-drag": "^3.0.7",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-selection": "^3.0.10",
"@types/d3-transition": "^3.0.8",
"@types/d3-zoom": "^3.0.8",
"d3-drag": "^3.0.0",
"d3-interpolate": "^3.0.1",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
@ -1829,6 +1911,12 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/classcat": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
"license": "MIT"
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@ -1840,9 +1928,114 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-dispatch": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-drag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-selection": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-selection": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-transition": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3",
"d3-dispatch": "1 - 3",
"d3-ease": "1 - 3",
"d3-interpolate": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"d3-selection": "2 - 3"
}
},
"node_modules/d3-zoom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-drag": "2 - 3",
"d3-interpolate": "1 - 3",
"d3-selection": "2 - 3",
"d3-transition": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@ -2485,6 +2678,15 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
@ -2575,6 +2777,34 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true,
"license": "ISC"
},
"node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
}
}
}

View File

@ -12,6 +12,7 @@
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"@xyflow/react": "^12.8.2",
"i18next": "^25.3.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",

View File

@ -10,23 +10,10 @@
},
"pages": {
"screening": {
"title": "銘柄スクリーニング",
"rerun": "再実行",
"universe": {
"title": "ユニバース",
"desc": "スクリーニング対象の銘柄の条件を設定してください",
"type": {
"listed": "全上場銘柄"
"nodes": {
"fetch_ToshoListed": {
"title": "東証上場銘柄一覧"
}
},
"evaluation": {
"title": "銘柄評価",
"desc": "銘柄一覧テーブルに表示する列を設定してください",
"columnName": "カラム名"
},
"filterOut": {
"title": "選別",
"desc": "銘柄の選別条件を設定してください"
}
}
}

View File

@ -1,96 +1,45 @@
import { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { useCallback } from "react";
import { Background, Controls, ReactFlow, Node, useNodesState, useEdgesState, addEdge, Connection } from "@xyflow/react";
import * as node from "./node/";
function Screening() {
const {t} = useTranslation();
return (
<>
<h1>{t("pages.screening.title")}</h1>
<UniverseSpec/>
<EvaluationSpec/>
<FilterOutSpec/>
</>
);
}
export default Screening;
import "@xyflow/react/dist/style.css";
function UniverseSpec() {
const {t} = useTranslation();
return (
<FilterBox
title={t("pages.screening.universe.title")}
desc={t("pages.screening.universe.desc")}
>
<select>
<option>{t("pages.screening.universe.type.listed")}</option>
</select>
<label><input type="checkbox" />{t("terms.toshoGrowth")}</label>
<label><input type="checkbox" />{t("terms.toshoStandard")}</label>
<label><input type="checkbox" />{t("terms.toshoPrime")}</label>
</FilterBox>
);
}
import { History, Impl as HistoryImpl } from "./context/History";
function EvaluationSpec() {
const {t} = useTranslation();
return (
<FilterBox
title={t("pages.screening.evaluation.title")}
desc={t("pages.screening.evaluation.desc")}
>
<ol>
{
[...Array(3)].map((_,i)=>(
<li>
<input type="text" placeholder={t("pages.screening.evaluation.columnName")} />
=
<select>
<option>{t("terms.stock.volume")}</option>
<option>{t("terms.stock.price")}</option>
</select>
<a href="#">remove</a>
</li>
))
}
<li>
<a href="#">add</a>
</li>
</ol>
</FilterBox>
);
}
function FilterOutSpec() {
const {t} = useTranslation();
return (
<FilterBox
title={t("pages.screening.filterOut.title")}
desc={t("pages.screening.filterOut.desc")}
>
<textarea />
</FilterBox>
);
}
type FilterBoxProps = {
title: string,
desc: string,
children: ReactNode,
const nodeTypes = {
fetch_ToshoListed: node.fetch.ToshoListed,
};
function FilterBox({title, desc, children}: FilterBoxProps) {
const {t} = useTranslation();
const initialNodes: Node[] = [
{id: "n1", type: "fetch_ToshoListed", position: {x:0, y:0}, data: {prime: true, standard: true, growth: true}},
{id: "n2", position: {x:0, y:100}, data: {label: "hi"}},
{id: "n3", position: {x:0, y:200}, data: {label: "hello"}},
];
export default function Screening() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onConnect = useCallback(
(connection: Connection) => setEdges((eds) => addEdge(connection, eds)),
[]
);
return (
<div>
<div>
<div>
<h2>{title}</h2>
<p>{desc}</p>
</div>
<div>
<a href="#">{t("pages.screening.rerun")}</a>
</div>
<History.Provider value={new HistoryImpl(setNodes)}>
<div style={{ position: "absolute", left: 0, right: 0, top: 0, bottom: 0 }}>
<ReactFlow
nodeTypes={nodeTypes}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
fitView
>
<Controls />
<Background />
</ReactFlow>
</div>
<div>{children}</div>
</div>
</History.Provider>
);
}

View File

@ -0,0 +1,22 @@
import { Dispatch, SetStateAction, createContext } from "react";
import { Node, } from "@xyflow/react";
export interface Iface {
push(id: string, data: any): void,
};
export const History = createContext<Iface|undefined>(undefined);
export default History;
export class Impl implements Iface {
setNodes: Dispatch<SetStateAction<Node[]>>;
constructor(setNodes: Dispatch<SetStateAction<Node[]>>) {
this.setNodes = setNodes;
}
push(id: string, data: any): void {
this.setNodes(
(nds)=>nds.map((n)=>n.id == id? {...n, data: data}: n)
);
}
};

14
src/pages/node/Base.tsx Normal file
View File

@ -0,0 +1,14 @@
import { ReactNode } from "react";
type Params = {
title: ReactNode,
children: ReactNode,
};
export default function Base(p: Params) {
return (
<div>
<div>{p.title}</div>
{p.children}
</div>
);
}

56
src/pages/node/fetch.tsx Normal file
View File

@ -0,0 +1,56 @@
import { useContext } from "react";
import { useTranslation } from "react-i18next";
import { Handle, Node, NodeProps, Position } from "@xyflow/react";
import Base from "./Base";
import History from "../context/History";
export type ToshoListed = Node<{
prime: boolean,
standard: boolean,
growth: boolean,
}, "ToshoListed">;
export function ToshoListed({id, data}: NodeProps<ToshoListed>) {
const {t} = useTranslation();
const history = useContext(History);
if (history === undefined) {
throw new Error("no History context");
}
return (
<Base title={t("pages.screening.nodes.fetch_ToshoListed.title")}>
<div>
<div><label>
<input
type="checkbox"
name="prime"
checked={data.prime}
onChange={(e)=>history.push(id, {...data, prime: e.target.checked})}
/>
{t("terms.toshoPrime")}
</label></div>
<div><label>
<input
type="checkbox"
name="standard"
checked={data.standard}
onChange={(e)=>history.push(id, {...data, standard: e.target.checked})}
/>
{t("terms.toshoStandard")}
</label></div>
<div><label>
<input
type="checkbox"
name="growth"
checked={data.growth}
onChange={(e)=>history.push(id, {...data, growth: e.target.checked})}
/>
{t("terms.toshoGrowth")}
</label></div>
</div>
<Handle type="source" position={Position.Right}/>
</Base>
);
}

2
src/pages/node/index.tsx Normal file
View File

@ -0,0 +1,2 @@
export { default as Base } from "./Base";
export * as fetch from "./fetch";