انتقل إلى المحتوى الرئيسي

الوحدة 3 — التخطيط: الأعمدة والتبويبات والشريط الجانبيّ

بعد وحدتَي المكوّنات، لدينا اللبنات. الآن نحتاج إلى الهيكل. تطبيق Streamlit عاديّ ينمو من عشرة عناصر إلى أربعين في أيّام، وبلا تخطيط واضح تتحوّل الصفحة إلى شريط عموديّ لا يقرأه أحد. تُقدّم هذه الوحدة الأدوات الستّ الأساسيّة للتنظيم البصريّ، مع تنبيه خاصّ باتّجاه القراءة العربيّة.

الأعمدة (st.columns)

st.columns(n) يُنشئ n عمودًا متساوي العرض. st.columns([2, 1]) يُنشئ عمودَين بنسبة 2:1 (الأوّل ضعف عرض الثاني). يعيد قائمة كائنات نستعملها إمّا بالفهرس (col1.metric(...)) أو بمدير سياق (with col1: ...).

import streamlit as st

st.header("لوحة قيادة الخطر — نظرة عامّة")

col1, col2, col3, col4 = st.columns(4)
col1.metric("عملاء نشطون", "18 742", "+312")
col2.metric("عملاء بخطر", "2 108", "-45")
col3.metric("قيمة معرّضة (USD)", "84 500", "-1 200")
col4.metric("متوسّط الأقدميّة (شهر)", "27")

الأعمدة لا تتداخل؛ محتواها يبقى ضمنها. لا يمكن أن يمتدّ عنصر عبر أعمدة (لا colspan في Streamlit). الحلّ حين تحتاج ذلك: عرض العنصر الأكبر قبل إنشاء الأعمدة، ثمّ الأعمدة تحته.

قاعدة عمليّة: من ثلاثة إلى أربعة أعمدة كحدّ أقصى لصفّ واحد. أكثر من ذلك يُنتج مقاييس صغيرة يصعب قراءتها على شاشة عاديّة، ومكرَّسة بلا حاجة على شاشات الجوّال.

التبويبات (st.tabs)

التبويبات تُقسّم الصفحة إلى أقسام يفتحها المستخدم عند الحاجة. الشيفرة أنيقة:

tab_apercu, tab_liste, tab_analyse = st.tabs(["نظرة عامّة", "قائمة العملاء", "تحليل مفصَّل"])

with tab_apercu:
st.subheader("مؤشّرات اليوم")
# المقاييس، الرسم الرئيسيّ

with tab_liste:
st.subheader("العملاء ذوو الخطر")
# جدول قابل للتصفية

with tab_analyse:
st.subheader("تفصيل حسب العرض")
# رسم توزيع، جدول تجميع

مهمّ: st.tabs يُنفّذ محتوى كلّ التبويبات في كلّ إعادة تنفيذ، حتّى التبويبات المخفيّة. إذا وضعت داخل تبويب حسابًا مكلفًا، سيُنفّذ حتّى لو لم يفتحه المستخدم. الحلّ: تخزين مؤقّت (الوحدة 5) لكلّ ما يستحقّه، أو تأجيل الحساب إلى ما بعد نقرة زرّ داخل التبويب.

الشريط الجانبيّ (st.sidebar)

الشريط الجانبيّ هو المكان القياسيّ للمرشّحات وضبط التطبيق العامّ. كلّ استدعاء st.sidebar.xxx يعرض المكوّن هناك:

st.sidebar.header("مرشّحات")

periode = st.sidebar.date_input(
"الفترة",
value=(pd.to_datetime("2025-01-01"), pd.to_datetime("2025-06-30")),
)
offres = st.sidebar.multiselect(
"أنواع العرض",
["Basic", "Standard", "Premium", "Family"],
default=["Standard", "Premium"],
)
seuil = st.sidebar.slider("عتبة الخطر", 0.0, 1.0, 0.5, 0.05)

st.sidebar.markdown("---")
st.sidebar.caption("v1.2.0 · تحديث كلّ ساعة")

قاعدة تصميم: الشريط الجانبيّ يُصبح ذاكرة الصفحة. المرشّحات تُطبَّق على كلّ التبويبات والأقسام. المستخدم يفتح التطبيق، يضبط مرشّحات مرّة، ويتصفّح. لا تضع فيه أزرار «سجّل»، ولا مكوّنات مرتبطة بقسم واحد؛ ذلك مكانه في المتن.

نمط بديل: with st.sidebar: ... يفتح مدير سياق يجعل كلّ ما بداخله يُوضع في الشريط. مفيد حين تضع عدّة مكوّنات معًا.

الحاويات (st.container)

الحاوية كتلة فارغة تسمح بتجميع مكوّنات، أو بحجز مكان في الصفحة نملؤه لاحقًا. حالتان يومتان لاستعمالها.

التجميع: حاوية بعنوان أو حدّ لعزل قسم:

with st.container(border=True):
st.subheader("تفاصيل العميل")
st.write("...")

الحجز المسبق: st.container مخزَّن في متغيّر، نملؤه بعد أسطر:

place_alerte = st.container()

# نتائج حسابات...
resultats = calculer()

if resultats["a_risque"]:
place_alerte.warning("عميل بخطر عالٍ")
else:
place_alerte.success("عميل آمن")

st.dataframe(resultats["details"])

هذا النمط مفيد حين تريد أن يظهر التنبيه قبل الجدول في الصفحة، لكنّ نتيجة التنبيه لا تُحسب إلّا بعد الحساب.

الموسّعات (st.expander)

st.expander("عنوان") يُنشئ قسمًا قابلًا للطيّ. مثاليّ للتفاصيل الاختياريّة (مساعدة، تعقّب، إعدادات متقدّمة) التي لا تريد إخفاءها كلّيًّا ولا فرضها على المستخدم:

with st.expander("كيف تُحسب درجة الخطر؟"):
st.markdown("""
درجة الخطر مخرَج نموذج **الغابة العشوائيّة** المُدرَّب على 24 شهرًا من البيانات التاريخيّة.
المتغيّرات الأكثر تأثيرًا: **الأقدميّة، عدد الشكاوى، المبلغ الشهريّ، نوع العرض**.
قيمة أعلى من 0.5 تُصنَّف عميلًا بخطر.
""")

with st.expander("سجلّ التغييرات في العميل"):
st.dataframe(historique)

الموسّعات مطويّة افتراضيًّا (expanded=False). لتظهر مفتوحة أوّل مرّة: expanded=True.

القراءة من اليمين إلى اليسار في Streamlit

Streamlit مُصمَّم افتراضيًّا لاتّجاه LTR، ولا يوفّر مفتاح direction=rtl جاهزًا. لتطبيق عربيّ حقيقيّ نستعمل ثلاث حيَل:

أوّلًا: حقن CSS مخصَّص عبر st.markdown(..., unsafe_allow_html=True) أو ملفّ .streamlit/config.toml غير كافٍ لهذا الغرض. الطريق المعتاد:

import streamlit as st

st.set_page_config(page_title="ترك العملاء", layout="wide")

st.markdown(
"""
<style>
/* الاتّجاه العامّ للصفحة */
html, body, [data-testid="stAppViewContainer"] { direction: rtl; text-align: right; }
/* الشريط الجانبيّ في الجهة اليمنى */
[data-testid="stSidebar"] { direction: rtl; }
</style>
""",
unsafe_allow_html=True,
)

ثانيًا: بعض المكوّنات (الجداول، الرسوم Plotly) تحافظ على اتّجاهها الداخليّ LTR حتّى مع RTL على المحيط؛ هذا مقبول للأرقام والرسوم، بل هو الأفضل. ولا تحاول عكسها بالقوّة.

ثالثًا: ترتيب الأعمدة في st.columns لا يتأثّر بـRTL. col1, col2, col3 = st.columns(3) يعطي col1 على اليسار دائمًا. إذا أردت العكس، اعكس الأسماء وتذكّر أنّ col1 سيبقى فعليًّا أوّل عمود في الشيفرة (يمين الشاشة عندك). قاعدة عمليّة: ضع دائمًا المقياس الأهمّ أوّلًا في القائمة، دون قلق بشأن الاتّجاه البصريّ.

حدود التبويبات المخفيّة

كلّ محتوى التبويبات يُنفَّذ في كلّ إعادة تنفيذ. إذا وضعت قراءة CSV بحجم 20 ميغابايت داخل تبويب مخفيّ، ستُقرأ في كلّ نقرة. القاعدة: أيّ حساب أثقل من 100 ملّي‌ثانية يجب أن يمرّ عبر st.cache_data (الوحدة 5)، حتّى داخل تبويب مخفيّ.

بنية الصفحة الرئيسيّة لتطبيقنا

نُجمِّع الآن الأدوات في بنية كاملة للخيط الأحمر:

import streamlit as st
import pandas as pd

st.set_page_config(page_title="ترك العملاء", page_icon=":telephone:", layout="wide")

# --- الشريط الجانبيّ: مرشّحات مشتركة بين كلّ التبويبات ---
with st.sidebar:
st.header("مرشّحات")
offres = st.multiselect("أنواع العرض", ["Basic", "Standard", "Premium", "Family"])
seuil = st.slider("عتبة الخطر", 0.0, 1.0, 0.5, 0.05)

# --- المتن: تبويبات ثلاث ---
st.title("لوحة قيادة ترك العملاء")

tab_apercu, tab_liste, tab_scoring = st.tabs(["نظرة عامّة", "قائمة الخطر", "تسجيل عميل"])

with tab_apercu:
col1, col2, col3 = st.columns(3)
col1.metric("عملاء بخطر", "2 108", "-45")
col2.metric("قيمة معرّضة (USD)", "84 500")
col3.metric("متوسّط الدرجة", "0.41")

with st.expander("منهجيّة الحساب"):
st.write("...")

with tab_liste:
st.info("جدول العملاء ذوي الخطر — سيأتي في الوحدة 4.")

with tab_scoring:
st.info("نموذج تسجيل عميل واحد — من الوحدة 2.")

هذه البنية تنمو معك: كلّ وحدة تُضيف محتوى تبويب أو مرشِّح، بلا إعادة كتابة الهيكل.

الخلاصة

  • st.columns للصفوف، st.tabs للأقسام الكبيرة، st.sidebar للمرشّحات المشتركة، st.container للتجميع أو الحجز المسبق، st.expander للتفاصيل الاختياريّة.
  • التبويبات تُنفَّذ كلّها في كلّ إعادة تنفيذ، حتّى المخفيّة؛ خزِّن الحسابات الثقيلة مؤقّتًا.
  • ترتيب الأعمدة col1, col2, col3 يبقى ثابتًا في الشيفرة؛ RTL لا يعكسه.
  • بناء عربيّ سليم: حقن CSS للـRTL على stAppViewContainer وstSidebar، وترك الجداول والرسوم بمحتواها LTR الطبيعيّ.

الوحدة التالية: الرسوم البيانية والجداول التفاعليّة لملء هذه التبويبات.