心の可視化アプリ 改善と進捗、追加案
最新版:心の地図
from flask import Flask, request, render_template_string
import pandas as pd
import plotly.graph_objects as go
import random
import os
from datetime import datetime
app = Flask(__name__)
DATA_FILE = "emotion_path_data.json"
# 感情ベクトル方向(出力順を固定)
EMOTION_DEFS = [
{"key": "Q_LY", "name": "安心", "vec": (0, 1)},
{"key": "Q_Hope", "name": "希望", "vec": (0.7, 0.7)},
{"key": "Q_Fr", "name": "不安", "vec": (1, 0)},
{"key": "Rp", "name": "無気力", "vec": (0, -1)},
{"key": "Q_Lonely", "name": "孤独", "vec": (-1, 0)},
{"key": "Q_Anger", "name": "怒り", "vec": (-0.7, -0.7)},
{"key": "Q_Normal", "name": "いつも通り", "vec": (-0.1, 0.1)}
]
CHARACTERS = ["🐱", "🤖", "🐻", "🐥", "👦", "👧", "🐕️", "🐑"]
# 初期データ読み込み
emotion_keys = [d["key"] for d in EMOTION_DEFS]
cols = ["date", "Character"] + emotion_keys
if os.path.exists(DATA_FILE):
try:
df = pd.read_json(DATA_FILE)
except:
df = pd.DataFrame(columns=cols)
else:
df = pd.DataFrame(columns=cols)
HTML_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<title>エモ・ナビ 🧭 心の幾何学</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: sans-serif; background: #121212 ; color: #e0e0e0 ; text-align:center; margin:0; padding:10px; }
.container { max-width: 500px; margin: auto; background: #1e1e1e ; padding: 15px; border-radius: 20px; }
.slider-box { text-align: left; background: #2c2c2c ; padding: 10px; border-radius: 10px; margin: 10px 0; }
input[type="range"] { width: 60%; vertical-align: middle; }
.val-disp { display: inline-block; width: 30px; text-align: right; font-size: 0.8rem; color: #03dac6 ; }
label { display: inline-block; width: 80px; font-size: 0.9rem; }
button { width: 100%; padding: 12px; background: #bb86fc ; border: none; border-radius: 10px; font-weight: bold; cursor: pointer; color: #000 ; }
.feedback { font-size: 0.9rem; color: #03dac6 ; margin-top: 10px; text-align: left; border-left: 3px solid #bb86fc ; padding-left: 10px; }
.char-select { margin-bottom: 15px; display: flex; flex-wrap: wrap; justify-content: center; }
</style>
</head>
<body>
<div class="container">
<h2>心の幾何学すごろく 🧭</h2>
<form method="POST">
<div class="char-select">
{% for c in characters %}
<label style="width:auto; cursor:pointer; font-size:1.5rem; margin:5px;">
<input type="radio" name="character" value="{{ c }}" required> {{ c }}
</label>
{% endfor %}
</div>
<div class="slider-box">
{% for d in defs %}
<div>
<label>{{ d.name }}</label>
<input type="range" name="{{ d.key }}" min="0" max="5" step="0.5" value="1"
oninput="this.nextElementSibling.innerText = this.value">
<span class="val-disp">1</span>
</div>
{% endfor %}
<div style="margin-top:10px;">
<label>日付</label>
<input type="date" name="date" value="{{ today }}" style="background:#444; color:#fff; border:none; padding:5px; width:150px;">
</div>
</div>
<button type="submit">心の歩みを記録する</button>
</form>
{% if plot %}
<div style="margin-top:20px; background:#fff; border-radius:15px; overflow:hidden; display: flex; justify-content: center;">{{ plot|safe }}</div>
{% endif %}
{% if feedback %}
<div class="feedback">{{ feedback|safe }}</div>
{% endif %}
</div>
</body>
</html>
"""
@app.route("/", methods=["GET", "POST"])
def index():
global df
plot_div = ""
feedback = ""
today = datetime.today().strftime("%Y-%m-%d")
if request.method == "POST":
try:
# 1. 安全なデータ取得
entry = {
"date": request.form.get("date", today),
"Character": request.form.get("character", "🐱")
}
for d in EMOTION_DEFS:
entry[d["key"]] = float(request.form.get(d["key"], 0))
# データ更新
new_row = pd.DataFrame([entry])
df = pd.concat([df, new_row], ignore_index=True)
df = df.drop_duplicates(subset="date", keep="last").sort_values("date")
df.to_json(DATA_FILE, force_ascii=False)
# 2. 軌跡計算(すごろくロジック)
trail_points = [(0, 0)]
# 全データを通じて一本の線にする
for _, row in df.iterrows():
current_pos = trail_points[-1]
# 固定された順番で1日の感情を繋ぐ
for d in EMOTION_DEFS:
score = row[d["key"]]
if score > 0:
dx = d["vec"][0] * score
dy = d["vec"][1] * score
current_pos = (current_pos[0] + dx, current_pos[1] + dy)
trail_points.append(current_pos)
# 3. 演出
events = []
if random.random() < 0.3: events.append("🍀 足元に小さな幸運を見つけた。")
feedback = f"<b>{entry['date']}</b> の記録を完了。<br>" + "<br>".join(events)
# 4. Plotly描画
fig = go.Figure()
# 軌跡
fig.add_trace(go.Scatter(
x=[p[0] for p in trail_points],
y=[p[1] for p in trail_points],
mode="lines+markers",
line=dict(color="#6200ee", width=2),
marker=dict(size=3, color="#03dac6"),
name="Route"
))
# 最新のキャラ位置
last_p = trail_points[-1]
fig.add_trace(go.Scatter(
x=[last_p[0]], y=[last_p[1]],
mode="text",
text=[entry["Character"]],
textfont=dict(size=25),
name="Now"
))
# レイアウト調整
fig.update_layout(
showlegend=False,
margin=dict(l=10, r=10, t=10, b=10),
xaxis=dict(showgrid=True, zeroline=True, gridcolor="#eee", scaleanchor="y", scaleratio=1),
yaxis=dict(showgrid=True, zeroline=True, gridcolor="#eee"),
width=350, height=350,
plot_bgcolor='white'
)
plot_div = fig.to_html(full_html=False, config={'displayModeBar': False})
except Exception as e:
feedback = f"エラー: {e}"
return render_template_string(
HTML_TEMPLATE,
plot=plot_div,
feedback=feedback,
characters=CHARACTERS,
defs=EMOTION_DEFS,
today=today
)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=9500, debug=True)
イメージ

ここからさらに改良案として
背景画像の差し込み
日付や乱数で切り替えると自然。
例えば:
草原 → 緑ベースで小さな花や木
宇宙 → 星や惑星、暗め背景
絵本風 → パステルカラー、雲や小道
砂漠 → 黄土色の砂丘、サボテン少し
雨の日 → 雨粒や水たまり、少し暗め
やり方のイメージ:
Python
bg_options = [
{"src": "https://example.com/grass.png", "sizex": 20, "sizey": 20},
{"src": "https://example.com/space.png", "sizex": 20, "sizey": 20},
{"src": "https://example.com/storybook.png", "sizex": 20, "sizey": 20}
]
bg_choice = random.choice(bg_options)
fig.update_layout(images=[dict(
source=bg_choice["src"],
xref="x", yref="y",
x=-10, y=10,
sizex=20, sizey=20,
xanchor="left", yanchor="top",
layer="below"
)])
layer="below" にすることで軌跡とキャラが上に描画されます。
sizex/sizey で Plotly 座標系に合わせると自然に見えます。
日付ごとに乱数か、曜日で切り替えでも面白い。
入力画面で選択できるようにしても良し。
こうすると、1日ごとに「今日は宇宙を歩く日だ!」みたいな演出が可能になる。
また、選択or使用中の背景に応じたイベント
例)水中マップなら魚に声をかけられたりする
宇宙マップなら流れ星が横切るなど
なんか"ラッキー"な小さなイベント等