সরাসরি পর্বে যান
পর্ব ৩১ · ৭৯-এর মধ্যে · মডিউল ৫

Interactive chart — Plotly আর কখন দরকার

Interactive Charts with Plotly — When Interactivity Helps, Figures as JSON, Export, WebGL, Accessibility and Hover Privacy

Taught in Bangla, this lesson shows when an interactive chart helps and when a static one is better, Plotly Express versus Graph Objects, and the figure as data plus layout JSON. Real desktop runs measure write_html file sizes and WebGL switching, show hidden hover columns still leaking into the HTML, and in-page plotly.js charts add hover, zoom and a range slider.

এই পর্বে যা শিখবেন

  • প্রশ্ন দেখে ঠিক করা: স্থির chart যথেষ্ট, নাকি interactive chart লাগবে, নাকি পুরো dashboard
  • px.scatter, px.line, px.bar, px.histogram-এ color, hover_data, facet_col আর animation_frame; আর একই chart Graph Objects দিয়ে
  • Figure-এর ভেতরের গঠন: data, layout, trace আর JSON; ব্রাউজারে plotly.js দিয়ে নিজের হাতে hover, zoom আর range slider-সহ chart আঁকা
  • fig.show() আর renderer, write_html-এর তিন রকম, write_image দিয়ে PNG/SVG, আর অনেক point-এ WebGL
  • Interactive chart-এর accessibility-র সীমা, আর tooltip-এ ব্যক্তিগত তথ্য ফাঁস হওয়া ঠেকানো

মডিউল ৫-এর আগের পর্বগুলোতে matplotlib দিয়ে ছবি আঁকা (পর্ব ২৮ · matplotlib-এর শুরু), প্রশ্ন দেখে chart বাছাই (পর্ব ২৯ · কোন chart কখন) আর seaborn-এর পরিসংখ্যানের chart (পর্ব ৩০) এসেছে। সেগুলোর ফল একটি স্থির ছবি: PNG বা SVG, যা সবাই একইভাবে দেখে। এই পর্বে প্রশ্নটা অন্যরকম। বাজারঘরের (এই কোর্সের কাল্পনিক অনলাইন দোকান) ৬০০টি অর্ডারের মধ্যে কোন অর্ডারটি অস্বাভাবিক দেরিতে পৌঁছেছে, তা স্থির ছবিতে একটি বিন্দু মাত্র; কিন্তু বিন্দুর ওপর mouse রাখলে যদি order_id দেখা যায়, তাহলে সেখান থেকেই খোঁজ শুরু করা যায়। এই সুবিধার নাম interactivity, আর Python-এ এর সবচেয়ে প্রচলিত লাইব্রেরিগুলোর একটি Plotly.

একটি সতর্কতা আগেই। এই পাতার ভেতরের Python (Pyodide) Plotly-র Python package চালায় না। তাই এই পর্বের কাজ তিন ভাগে: (১) ডেস্কটপে Python 3.14.4, plotly 7.1.0 (লেখার সময়ের সর্বশেষ স্থির সংস্করণ, ভেতরে plotly.js 4.1.1), pandas 3.1.1 আর NumPy 2.5.4 দিয়ে সত্যিকারের run, যার আসল আউটপুট পাতায় দেওয়া; (২) পাতার ভেতরেই JavaScript-এ plotly.js 4.1.1 দিয়ে দুটি chart, যা আপনি নিজে hover আর zoom করতে পারবেন (প্রথমবার ইন্টারনেট থেকে প্রায় ১.৫ MB নামে); (৩) পাতার Python-এ pandas দিয়ে figure-এর JSON আর লেখা সারাংশ বানানো।

১ · কখন interactive chart সাহায্য করে, কখন ক্ষতি করে

ঢাকার একটি রিকশার রুটের কথা ভাবুন। কাউকে শুধু “মিরপুর থেকে ধানমন্ডি কোন পথে” বলতে হলে একটি ছাপা ম্যাপে একটি দাগই যথেষ্ট। কিন্তু নিজে যদি রোজ নতুন রাস্তা খুঁজতে হয়, তখন zoom করা যায় এমন ফোনের ম্যাপ লাগে। Chart-এর বেলায়ও একই কথা: উত্তর জানানোর জন্য স্থির chart, উত্তর খোঁজার জন্য interactive chart. ১৯৯৬ সালে Ben Shneiderman তাঁর “The Eyes Have It” প্রবন্ধে তথ্য খোঁজার একটি সূত্র দিয়েছিলেন, যা আজও বহুল উদ্ধৃত: “Overview first, zoom and filter, then details-on-demand” — আগে পুরো ছবি, তারপর zoom আর ছাঁকনি, শেষে চাইলে বিস্তারিত। Interactive chart এই তিনটি ধাপ এক জায়গায় দেয়।

যেখানে সাহায্য করে

  • অনেক point ঘাঁটা: ৬০০ অর্ডারের scatter-এ একটি দূরের বিন্দুর ওপর hover করে order_id জানা (EDA, পর্ব ৬২-এ বিস্তারিত)
  • Hover-এ বিস্তারিত: chart-এ না লিখেও প্রতিটি বিন্দুর জেলা, পণ্য, rating দেখা
  • লম্বা time series: ছয় মাসের দৈনিক অর্ডারে একটি সপ্তাহে zoom (range slider)
  • Dashboard: দোকানের ম্যানেজার রোজ দেখবেন, জেলা বা মাস বদলে বদলে

যেখানে ক্ষতি করে

  • রিপোর্ট, PDF আর ছাপা: কাগজে hover হয় না; লুকানো তথ্য পাঠক কখনো দেখেন না
  • Accessibility: hover মূলত mouse বা touch-নির্ভর; keyboard বা screen reader ব্যবহারকারী অনেক সময় বাদ পড়েন
  • ফাইলের আকার: একটি ছোট chart-এর HTML প্রায় ৫ MB হতে পারে (অংশ ৬)
  • উত্তর যখন একটিই: “কোন জেলায় বিক্রি বেশি” প্রশ্নে একটি সাজানো bar chart যথেষ্ট; zoom করার কিছু নেই

একটি সূক্ষ্ম ক্ষতিও আছে। Interactive chart পাঠকের ওপর কাজ চাপায়: কোথায় hover করবেন, কোন legend বন্ধ করবেন, তা পাঠককেই ঠিক করতে হয়। আপনি যে কথাটি বলতে চান, পাঠক হয়তো সেটি খুঁজেই পেলেন না। তাই রিপোর্টে মূল কথাটি chart-এর শিরোনাম আর লেখায় নিজে বলে দিন, interactivity থাকুক বাড়তি অনুসন্ধানের জন্য। নিচের ধাপে বাজারঘরের পাঁচটি বাস্তব প্রশ্ন দিয়ে সিদ্ধান্তটি অনুশীলন করুন।

📄 মাসিক PDF রিপোর্ট: কোন জেলায় বিক্রি বেশি?
🔍 ৬০০ অর্ডারে অস্বাভাবিক দেরি খোঁজা
📈 ছয় মাসের দৈনিক অর্ডার, একটি সপ্তাহ দেখা
📱 WhatsApp-এ পাঠানো একটি ছবি
🖥️ ম্যানেজারের রোজকার পর্যবেক্ষণ
  1. মাসিক PDF রিপোর্ট। প্রশ্নের উত্তর একটি: ঢাকা সবচেয়ে বেশি। বড় থেকে ছোট সাজানো bar chart, প্রতিটি bar-এ সংখ্যা লেখা। PDF-এ hover চলে না, তাই interactive chart-এর লুকানো তথ্য এখানে হারিয়ে যায়।
  2. অস্বাভাবিক দেরি খোঁজা। এটি অনুসন্ধান, উত্তর এখনো অজানা। scatter-এ x অক্ষে delivery_days, y অক্ষে টাকা; দূরের বিন্দুর ওপর hover করে order_id পেলে সেই অর্ডারটি pandas-এ খুলে দেখা যায় (পর্ব ২২-এর loc দিয়ে)।
  3. ছয় মাসের দৈনিক অর্ডার। ১৮০টি দিন এক ছবিতে ঘন হয়ে যায়। নিচে range slider রাখলে পুরো ছবিও থাকে, আর টেনে একটি সপ্তাহ বড় করেও দেখা যায়। অংশ ৫-এ এটি নিজে চালাবেন।
  4. WhatsApp-এ পাঠানো ছবি। পাঠক একটি ছবি পাবেন, HTML নয়। স্থির PNG, শিরোনামে মূল কথা (“এপ্রিলে সিলেটে অর্ডার দ্বিগুণ”), আর লেখা যেন ফোনের পর্দায় পড়া যায়।
  5. রোজকার পর্যবেক্ষণ। একই প্রশ্ন রোজ, শুধু তারিখ আর জেলা বদলায়। এখানে একক chart নয়, ছোট একটি web app বা dashboard মানায়, যেখানে ছাঁকনি থাকে, আর chart-এর পাশে সংখ্যার টেবিলও।

২ · Plotly-র দুই স্তর — Plotly Express আর Graph Objects

Plotly একটি পরিবার। এর কেন্দ্রে plotly.js, একটি JavaScript লাইব্রেরি, যা ব্রাউজারে chart আঁকে; সাধারণ chart SVG দিয়ে, অনেক point-এর chart WebGL দিয়ে। Python-এর plotly package নিজে কিছু আঁকে না; এটি একটি figure-এর বর্ণনা বানায় আর সেই বর্ণনা plotly.js-এর হাতে দেয়। Python package-এর ভেতরে দুটি স্তর:

স্তরকী করেকখন
plotly.express (px)একটি DataFrame আর কলামের নাম দিলে পুরো figure বানায়; color-এর প্রতিটি মানে আলাদা trace, legend, অক্ষের নাম, hover সব নিজেবেশিরভাগ কাজ, বিশেষ করে EDA
plotly.graph_objects (go)প্রতিটি trace আর layout-এর অংশ হাতে যোগ করা: go.Figure(), add_trace(go.Scatter(...))অস্বাভাবিক নকশা, দুই ধরনের trace একসঙ্গে, সূক্ষ্ম নিয়ন্ত্রণ

একই বাজারঘরের chart দুইভাবে। ডেটা বানানো হয়েছে seed 42 দিয়ে, ৬০০টি অর্ডার, পাঁচ জেলা, তিন ক্যাটাগরি; দাম পূর্ণসংখ্যা পয়সায় (price_paisa), chart-এ টাকা (amount_tk = qty × price_paisa / 100). ডেস্কটপে সত্যিকারের run (Python 3.14.4, plotly 7.1.0):

~/bazarghar-ai$ python bazarghar_plotly.py
python 3.14.4 | plotly 7.1.0 | plotly.js 4.1.1 | pandas 3.1.1
(600, 10)
import plotly.express as px, plotly.graph_objects as go

# ১) Plotly Express: এক লাইনে
fig = px.scatter(df, x="delivery_days", y="amount_tk", color="district",
                 hover_data=["order_id", "category", "rating"],
                 title="Order amount vs delivery days")

# ২) Graph Objects: একই chart, প্রতিটি জেলা হাতে যোগ করে
g = go.Figure()
for dist, part in df.groupby("district"):
    g.add_trace(go.Scatter(x=part.delivery_days, y=part.amount_tk,
                           mode="markers", name=dist))
g.update_layout(title="Same chart with graph_objects",
                xaxis_title="delivery_days", yaxis_title="amount_tk")

আসল আউটপুট (ডেস্কটপ, plotly 7.1.0), figure-এর ভেতরটা ছেপে দেখা:

type(fig): Figure
keys: ['data', 'layout']
traces: 5 ['Rajshahi', 'Dhaka', 'Chattogram', 'Khulna', 'Sylhet']
trace0.type: scatter | mode: markers | points: 94
hovertemplate: district=Rajshahi<br>delivery_days=%{x}<br>amount_tk=%{y}<br>order_id=%{customdata[0]}<br>category=%{customdata[1]}<br>rating=%{customdata[2]}<extra></extra>
customdata[0]: [1001, 'Grocery', 4]
layout.xaxis.title: delivery_days | legend title: district
go traces: 5

চারটি জিনিস লক্ষ করুন। এক: color="district" একটি trace-এ রং বদলায় না, পাঁচটি আলাদা trace বানায়, প্রতিটি জেলায় একটি; trace-এর ক্রম ডেটায় যে জেলা আগে এসেছে সেই ক্রমে (প্রথম অর্ডার রাজশাহীর). legend-এ একটি জেলায় ক্লিক করলে সেই trace লুকায়। দুই: hover_data-র কলামগুলো customdata নামের একটি লুকানো কলামে ঢোকে, আর hovertemplate লেখায় %{customdata[0]} দিয়ে সেগুলো ডাকা হয়। তিন: Graph Objects-এ পাঁচটি trace নিজে loop করে যোগ হয়েছে; গঠন একই। চার: অক্ষের নাম কলামের নামই; সুন্দর নাম চাইলে labels={"amount_tk": "Amount (taka)"} দিন।

কোনটি শিখবেন? শুরু করুন px দিয়ে; Plotly-র নিজের docs-ও এটিই সুপারিশ করে। যখন px দিয়ে কোনো কিছু হয় না, তখন px-এর figure নিয়েই fig.update_layout(...), fig.update_traces(...) বা fig.add_trace(go....) দিয়ে বদলান। px-এর ফল একটি সাধারণ go.Figure, তাই দুই স্তর মেশানো যায়। পর্ব ৩০-এর seaborn যেমন matplotlib-এর ওপর একটি সহজ স্তর, px তেমনি go-র ওপর।

৩ · Figure মানে data আর layout — একটি JSON

পরীক্ষার খাতায় একটি প্রশ্নপত্র আর একটি উত্তরপত্র থাকে; ছাপাখানা জানে কোন কাগজে কী ছাপতে হবে। Plotly-র figure-ও দুই ভাগের একটি নির্দেশপত্র: data হলো trace-এর তালিকা (কোন বিন্দু, কোন রেখা, কোন bar), আর layout হলো বাকি সব (শিরোনাম, অক্ষ, legend, margin আর রং)। এই নির্দেশপত্রটি JSON, তাই Python, R, Julia বা সরাসরি JavaScript, যেকোনো ভাষা থেকে একই chart বানানো যায়, আর plotly.js সব ক্ষেত্রে একই কাজ করে। নিচের ছবিতে পুরো পথটি দেখুন।

DataFrame থেকে ব্রাউজারের chart — Plotly-র পথ

Python only writes a JSON figure; plotly.js in the browser draws it with SVG or WebGL

Plotly figure-এর পথ: DataFrame থেকে JSON, তারপর ব্রাউজার বা ছবি pandas DataFrame px.scatter / go.Figure Figure = JSON data trace-এর তালিকা layout অক্ষ, শিরোনাম fig.show() write_html write_image Jupyter বাব্রাউজার .html ফাইল(plotly.js-সহ) Kaleido +Chrome → PNG ব্রাউজারে plotly.js SVG / WebGL · hover · zoom স্থির ছবি hover নেই Python কিছু আঁকে না; শুধু JSON লেখে।
ছবি ৩১.১ — Plotly-র কাজের পথ। হলুদ বাক্সের figure-টিই আসল জিনিস: একটি JSON নির্দেশপত্র। সবুজ দুই পথে সেটি ব্রাউজারে যায়, আর plotly.js সেখানে ছবি আঁকে, তাই hover, zoom আর legend-এ ক্লিক কাজ করে। লাল পথে Kaleido নামের সহায়ক একটি headless Chrome-এ একই chart এঁকে স্থির PNG বা SVG বানায়।

এই JSON দেখতে কেমন, তা পাতার Python-এই বানিয়ে দেখা যায়, Plotly ছাড়াই। নিচের কোড বাজারঘরের ২০০টি অর্ডার থেকে জেলাভিত্তিক বিক্রি (পর্ব ২৪-এর groupby) বের করে, তারপর ঠিক px.bar-এর মতো গঠনের একটি dict বানায়। প্রথমবার চালালে pandas নামতে কয়েক সেকেন্ড লাগতে পারে।

import json
import numpy as np, pandas as pd

rng = np.random.default_rng(42)
n = 200
df = pd.DataFrame({
    "district": rng.choice(["Dhaka", "Chattogram", "Khulna", "Rajshahi",
                            "Sylhet"], n, p=[0.4, 0.2, 0.15, 0.15, 0.1]),
    "qty": rng.integers(1, 6, n),
    "price_paisa": rng.integers(80, 2500, n) * 100,
})
df["amount_tk"] = df["qty"] * df["price_paisa"] // 100   # পয়সা থেকে টাকা
rev = df.groupby("district")["amount_tk"].sum().sort_values(ascending=False)

# Plotly-র figure: data (trace-এর list) + layout, সবই সাধারণ dict
fig = {
    "data": [{"type": "bar", "x": rev.index.tolist(),
              "y": [int(v) for v in rev],
              "hovertemplate": "%{x}: %{y:,} taka<extra></extra>"}],
    "layout": {"title": {"text": "Revenue by district (taka)"},
               "yaxis": {"title": {"text": "taka"}}},
}
for k, v in fig["data"][0].items():
    print(f"data[0].{k} =", json.dumps(v))
print("layout =", json.dumps(fig["layout"]))
text = json.dumps(fig)
print("JSON:", len(text), "characters | traces:", len(fig["data"]))

আসল আউটপুট (ব্রাউজারের Pyodide, pandas 3.0.2):

data[0].type = "bar"
data[0].x = ["Dhaka", "Chattogram", "Rajshahi", "Khulna", "Sylhet"]
data[0].y = [325151, 186954, 139575, 100543, 36192]
data[0].hovertemplate = "%{x}: %{y:,} taka<extra></extra>"
layout = {"title": {"text": "Revenue by district (taka)"}, "yaxis": {"title": {"text": "taka"}}}
JSON: 287 characters | traces: 1

একটি bar chart-এর পুরো নির্দেশ মাত্র কয়েকশো অক্ষর। data-য় একটি trace: ধরন "bar", x-এ পাঁচ জেলা বিক্রির ক্রমে, y-এ টাকায় মোট (ঢাকা ৩,২৫,১৫১ টাকা সবার ওপরে, সিলেট ৩৬,১৯২ টাকা সবার নিচে)। hovertemplate-এ %{x} আর %{y:,} হলো জায়গা-রাখা চিহ্ন, plotly.js প্রতিটি bar-এর মান বসাবে; শেষের <extra></extra> tooltip-এর পাশের trace-নামের বাক্সটি বন্ধ করে। y-এর মানগুলো int(v) দিয়ে Python-এর int বানানো হয়েছে, কারণ json.dumps NumPy-র সংখ্যা সরাসরি লিখতে পারে না (TypeError দেয়); Plotly-র Python package ভেতরে এই রূপান্তর নিজে করে। এই dict-টি JavaScript-এ Plotly.newPlot(div, fig.data, fig.layout)-কে দিলে হুবহু একটি bar chart আঁকা হতো, যা পরের অংশে নিজে করবেন।

px-এর বানানো figure-ও ঠিক এই রকম, শুধু অনেক বেশি ঘর ভরা। ডেস্কটপে চারটি অর্ডারের একটি ছোট px.scatter-এর fig.to_dict() ছাপলে (template বাদ দিয়ে) একটি চমক দেখা যায়। সত্যিকারের run, plotly 7.1.0:

{
 "data": [
  {
   "hovertemplate": "delivery_days=%{x}<br>amount_tk=%{y}<extra></extra>",
   "mode": "markers",
   "x": {
    "dtype": "i1",
    "bdata": "BAUCAQ=="
   },
   "y": {
    "dtype": "i2",
    "bdata": "qgf7E2gk5AI="
   },
   "type": "scatter"
   …
  }
 ],
 "layout": {
  "xaxis": { "title": { "text": "delivery_days" }, … },
  …
 }
}
template present: True

x আর y সংখ্যার তালিকা হিসেবে নেই, আছে dtype আর bdata হিসেবে: NumPy array-এর কাঁচা বাইট (পর্ব ১৩-এর data buffer), base64 লেখায়। "i1" মানে 1 বাইটের পূর্ণসংখ্যা (delivery_days 1–7), "i2" মানে 2 বাইটের (টাকা 740 থেকে 9320 পর্যন্ত)। plotly 6.0 থেকে Python package বড় array এভাবে পাঠায়, যাতে JSON ছোট আর দ্রুত হয়; plotly.js সেটি সরাসরি typed array হিসেবে পড়ে। আর template হলো রং, ফন্ট আর grid-এর একটি বড় default সেট, যা প্রতিটি figure-এ থাকে; এজন্যই চার বিন্দুর figure-ও কয়েক হাজার অক্ষরের।

1fig = px.scatter(df, ..., color="district",
                 hover_data=[...])2fig.data           # trace-এর tuple3fig.data[0]        # রাজশাহীর trace4fig.layout         # অক্ষ, legend, template5s = fig.to_json()6fig.write_html("orders.html",
               include_plotlyjs="cdn")
মান (Watch)
traces
trace0
layout
আকার
  1. Figure বানানো। px.scatter একটি go.Figure ফেরত দেয়। এই মুহূর্তে কোনো ছবি নেই; আছে শুধু একটি বর্ণনা, Python-এর memory-তে।
  2. data = trace-এর তালিকা। color="district" থাকায় পাঁচটি trace, প্রতিটি জেলায় একটি। ক্রম ডেটার ক্রমে: প্রথম অর্ডারটি রাজশাহীর, তাই trace 0 রাজশাহী।
  3. একটি trace খোলা। রাজশাহীর trace-এ 94টি বিন্দু: ৯৪টি x, ৯৪টি y, আর customdata-য় hover-এর তিনটি কলাম। প্রথম সারি [1001, 'Grocery', 4]: order_id, category, rating.
  4. layout. অক্ষের শিরোনাম কলামের নাম থেকে, legend-এর শিরোনাম color-এর কলাম থেকে। সঙ্গে একটি বড় template, যা রং আর ফন্ট ঠিক করে।
  5. JSON-এ রূপান্তর। fig.to_json() পুরো figure-কে একটি লেখা বানায়: আমাদের run-এ 28,770 অক্ষর, ৬০০ বিন্দু আর hover-এর কলামসহ।
  6. HTML-এ লেখা। JSON-টি একটি HTML পাতার ভেতর বসে, সঙ্গে plotly.js আনার একটি লিংক। ফাইল 29,774 বাইট: প্রায় পুরোটাই ডেটা। ব্রাউজার খুললে তবেই ছবি আঁকা হয়।

৪ · হাতে-কলমে — ব্রাউজারে plotly.js: hover, zoom আর legend

Figure যেহেতু JSON, তাই Python ছাড়াও সরাসরি JavaScript-এ একই গঠন লিখে plotly.js-কে দেওয়া যায়। নিচের কোড এই পাতার নিরাপদ একটি ফ্রেমে (sandboxed iframe) চলে: প্রথমে jsDelivr CDN থেকে plotly.js-এর একটি নির্দিষ্ট সংস্করণ (4.1.1, ডেস্কটপের plotly 7.1.0-এর সঙ্গে মেলানো) নামায়, তারপর বাজারঘরের ২০টি অর্ডারের scatter আঁকে। প্রতিটি জেলা একটি trace, ঠিক px-এর color-এর মতো। চালানোর পর chart-এ এগুলো করে দেখুন: একটি বিন্দুর ওপর mouse রাখুন বা ফোনে চাপুন (hover); mouse টেনে একটি আয়তক্ষেত্র আঁকুন (zoom), দুবার ক্লিক করলে আগের অবস্থা; legend-এ “Dhaka” চাপুন (trace লুকানো), দুবার চাপলে শুধু ঢাকা।

// বাজারঘরের ২০টি অর্ডার: [district, delivery_days, amount_tk, order_id]
const rows = [["Dhaka",1,2450,1011],["Dhaka",2,9320,1003],["Dhaka",2,4180,1017],
  ["Dhaka",3,6120,1024],["Dhaka",1,880,1030],["Dhaka",4,1962,1001],
  ["Chattogram",2,3560,1008],["Chattogram",3,7240,1015],["Chattogram",4,2890,1027],
  ["Chattogram",3,5310,1033],["Khulna",4,740,1004],["Khulna",5,3980,1019],
  ["Khulna",3,2210,1036],["Rajshahi",4,3310,1005],["Rajshahi",5,6020,1021],
  ["Rajshahi",6,1450,1039],["Sylhet",5,5115,1002],["Sylhet",6,8460,1012],
  ["Sylhet",7,2730,1028],["Sylhet",6,4390,1041]];
const symbols = ["circle", "square", "diamond", "triangle-up", "x"];

function draw() {
  const names = [...new Set(rows.map(r => r[0]))];
  const data = names.map((d, i) => {
    const p = rows.filter(r => r[0] === d);
    return { type: "scatter", mode: "markers", name: d,
      x: p.map(r => r[1]), y: p.map(r => r[2]), customdata: p.map(r => r[3]),
      marker: { size: 11, symbol: symbols[i] },
      hovertemplate: "order %{customdata}<br>%{x} days, %{y:,} taka" };
  });
  const layout = { title: { text: "Order amount vs delivery days" },
    xaxis: { title: { text: "delivery days" } }, yaxis: { title: { text: "amount (taka)" } },
    colorway: ["#0072B2", "#E69F00", "#009E73", "#CC79A7", "#56B4E9"],
    margin: { l: 60, r: 10, t: 40, b: 40 }, legend: { orientation: "h", y: -0.22 } };
  Plotly.newPlot("chart", data, layout, { responsive: true, displaylogo: false })
    .then(gd => {
      console.log("Plotly.js", Plotly.version, "| traces:", gd.data.length);
      console.log(gd.data.map(t => t.name + ": " + t.x.length).join(", "));
    });
}
const s = document.createElement("script");   // নির্দিষ্ট version, CDN থেকে
s.src = "https://cdn.jsdelivr.net/npm/plotly.js-dist-min@4.1.1/plotly.min.js";
s.onload = draw;
s.onerror = () => console.error("Plotly.js আনা যায়নি — ইন্টারনেট সংযোগ দেখুন");
document.head.appendChild(s);
console.log("Plotly.js আনা হচ্ছে…");

আসল আউটপুট (এই পাতার JavaScript runner, Edge):

Plotly.js আনা হচ্ছে…
Plotly.js 4.1.1 | traces: 5
Dhaka: 6, Chattogram: 4, Khulna: 3, Rajshahi: 3, Sylhet: 4

প্রথম লাইন ছাপা হয় তখনই, plotly.js নামার আগে; বাকি দুই লাইন আসে chart আঁকা শেষ হলে, কারণ Plotly.newPlot একটি Promise ফেরত দেয়। পাঁচটি trace, মোট ২০টি বিন্দু, ঠিক ডেটার মতো। কোডের তিনটি সিদ্ধান্ত খেয়াল করুন। রং: colorway-এ Okabe–Ito-র রং-অন্ধতা-সহনীয় পাঁচটি রং, Plotly-র default নয়। আকৃতি: প্রতিটি জেলার আলাদা marker (বৃত্ত, বর্গ, হীরা, ত্রিভুজ, ক্রস), যাতে শুধু রং দিয়ে জেলা চিনতে না হয়; সাদা-কালো ছাপাতেও আলাদা থাকে। Hover: hovertemplate-এ ঠিক যা দেখাতে চাই তা-ই, order_id, দিন আর টাকা; %{y:,} হাজারের ঘরে কমা বসায়। tooltip-এর পাশের ছোট বাক্সে trace-এর নাম (জেলা) Plotly নিজে দেখায়। সংস্করণ @4.1.1 লিখে বেঁধে দেওয়া হয়েছে, যাতে আগামী বছর নতুন plotly.js এলেও এই কোড একই আচরণ করে।

Chart-এর ওপরের ডান কোণের ছোট সরঞ্জামের সারিতে (modebar) ক্যামেরার আইকন chart-টি PNG হিসেবে নামায়; পাশের আইকনগুলো zoom, pan, box select আর lasso select. Box select দিয়ে বিন্দু বাছলে Plotly একটি plotly_selected event পাঠায়, যা dashboard-এ “বাছাই করা অর্ডারের টেবিল দেখানো”র মতো কাজে লাগে।

৫ · px-এর চার কাজের ঘোড়া — scatter, line, bar, histogram

পর্ব ২৯-এ প্রশ্ন দেখে chart বাছাইয়ের নিয়ম এসেছে: সম্পর্কের জন্য scatter, সময়ের জন্য line, তুলনার জন্য bar, distribution-এর জন্য histogram. px-এ চারটির ডাকার ধরন প্রায় এক: px.ধরন(df, x=..., y=..., color=...). কয়েকটি argument সব জায়গায় কাজ করে:

Argumentকী করেবাজারঘরে উদাহরণ
colorএকটি কলামের প্রতিটি মানে আলাদা রং ও trace (সংখ্যার কলাম হলে রঙের ক্রমিক স্কেল)color="district"
hover_datatooltip-এ বাড়তি কলাম; dict দিলে format বা লুকানোhover_data=["order_id"]
hover_nametooltip-এর শিরোনামে একটি কলাম, মোটা অক্ষরেhover_name="district"
facet_col / facet_rowপ্রতিটি মানে আলাদা ছোট chart, পাশাপাশি বা ওপর-নিচে (facet, small multiples)facet_col="category"
labelsকলামের নামের বদলে পড়ার মতো নামlabels={"amount_tk": "Amount (taka)"}
animation_frameএকটি কলামের প্রতিটি মানে একটি frame, নিচে play বোতাম আর slideranimation_frame="month"

ডেস্কটপে সত্যিকারের run (plotly 7.1.0): মাসিক বিক্রি ক্যাটাগরি অনুযায়ী, facet_col দিয়ে তিনটি পাশাপাশি chart, আর bar ও histogram-এর trace-এর ধরন।

m = (df.assign(month=df.date.dt.to_period("M").dt.to_timestamp())
       .groupby(["month", "category"], as_index=False).amount_tk.sum())
fl = px.line(m, x="month", y="amount_tk", facet_col="category",
             markers=True, title="Monthly revenue (taka)")
fb = px.bar(df.groupby("district", as_index=False).amount_tk.sum(),
            x="district", y="amount_tk")
fh = px.histogram(df, x="delivery_days", color="category", barmode="group")

       month     category  amount_tk
0 2026-01-01  Electronics   166228.0
1 2026-01-01      Fashion   136386.0
2 2026-01-01      Grocery   150016.0
3 2026-02-01  Electronics    80119.0
…
facet traces: 3 | xaxes: ['xaxis', 'xaxis2', 'xaxis3']
bar: bar | hist: histogram 3

facet_col তিনটি trace আর তিনটি আলাদা x অক্ষ (xaxis, xaxis2, xaxis3) বানিয়েছে; y অক্ষ default-এ সবার এক, তাই তিন ক্যাটাগরির উচ্চতা সরাসরি তুলনা করা যায়। মাসিক যোগফলের কাজটি pandas-এর (পর্ব ২৬-এ to_period আর resample); Plotly শুধু আঁকে। একই figure write_image দিয়ে স্থির ছবি বানালে এমন দেখায়:

তিনটি পাশাপাশি রেখা-চিত্র: Electronics, Fashion আর Grocery ক্যাটাগরির জানুয়ারি থেকে জুন ২০২৬-এর মাসিক বিক্রি, হাজার টাকায়। তিনটিরই y অক্ষ একই, প্রায় ৮০ থেকে ১৮৬ হাজার। Electronics মার্চে সর্বোচ্চ প্রায় ১৮৬ হাজার, ফেব্রুয়ারিতে সর্বনিম্ন প্রায় ৮০ হাজার।
ছবি ৩১.২ — সত্যিকারের run: plotly 7.1.0-এর px.line(..., facet_col="category"), write_image দিয়ে Kaleido 1.5.0 থেকে PNG, তারপর WebP. বাজারঘরের seed 42-এর কাল্পনিক ডেটা, ৬০০ অর্ডার; একই y অক্ষ বলে তিন ক্যাটাগরি সরাসরি তুলনীয়।

animation_frame নিয়ে একটি নোট। px.bar(m, x="district", y="orders", animation_frame="month") আমাদের run-এ দুটি frame বানিয়েছে (['Jan', 'Feb']), নিচে play বোতাম। Hans Rosling ২০০৬ সালের একটি বিখ্যাত বক্তৃতায় দেশগুলোর আয় আর আয়ুর চলমান bubble chart দেখিয়ে এই ধরনটি জনপ্রিয় করেন, আর Plotly-র docs-এর animation উদাহরণও সেই Gapminder ডেটা দিয়ে। কিন্তু animation-এ দুটি সময়ের তুলনা কঠিন, কারণ আগের frame চোখের সামনে থাকে না; বিশ্লেষণে facet_col প্রায়ই ভালো, animation বেশি মানায় উপস্থাপনায়। আর frame-এর মাঝে অক্ষের সীমা যেন না বদলায়, তা range_y দিয়ে বেঁধে দেওয়া ভালো।

লম্বা time series-এ Plotly-র সবচেয়ে কাজের জিনিস হলো x অক্ষের নিচে range slider. নিচের কোড বাজারঘরের ছয় মাসের (১৮০ দিন) দৈনিক অর্ডার বানায় একটি ছোট seed দেওয়া random generator দিয়ে: একটি সাধারণ স্তর, শুক্রবারে ভিড়, আর মার্চের মাঝামাঝি একটি কাল্পনিক উৎসবের সপ্তাহ। সঙ্গে ৭ দিনের চলমান গড় (পর্ব ২৬-এর rolling(7).mean()-এর হাতে লেখা রূপ). চালানোর পর নিচের ছোট chart-এর দুই প্রান্ত টেনে একটি সপ্তাহ বড় করে দেখুন, বা ওপরের “1m”, “3m” বোতাম চাপুন।

// ১৮০ দিনের দৈনিক অর্ডার, seed দেওয়া ছোট generator (mulberry32)
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0;
  let t = Math.imul(a ^ (a >>> 15), 1 | a);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
  return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const rand = mulberry32(42), days = [], orders = [];
for (let i = 0; i < 180; i++) {
  const d = new Date(Date.UTC(2026, 0, 1 + i));
  const fri = d.getUTCDay() === 5 ? 12 : 0;          // শুক্রবারের ভিড়
  const fest = i >= 74 && i <= 78 ? 35 : 0;          // কাল্পনিক উৎসব, ১৬–২০ মার্চ
  days.push(d.toISOString().slice(0, 10));
  orders.push(Math.round(40 + fri + fest + 10 * rand()));
}
const avg7 = orders.map((_, i) => i < 6 ? null :
  +(orders.slice(i - 6, i + 1).reduce((a, b) => a + b) / 7).toFixed(1));
function draw() {
  const data = [
    { type: "scatter", mode: "lines", name: "daily", x: days, y: orders, line: { color: "#9ca3af", width: 1 } },
    { type: "scatter", mode: "lines", name: "7-day mean", x: days, y: avg7, line: { color: "#0072B2", width: 2.5 } }];
  const layout = { title: { text: "Daily orders, 2026" }, margin: { l: 45, r: 10, t: 70, b: 10 },
    yaxis: { title: { text: "orders" } }, legend: { x: 1, xanchor: "right", y: 1, bgcolor: "rgba(255,255,255,0.8)" },
    xaxis: { rangeslider: { visible: true }, rangeselector: { buttons: [
      { count: 1, label: "1m", step: "month", stepmode: "backward" },
      { count: 3, label: "3m", step: "month", stepmode: "backward" }, { step: "all" }] } } };
  Plotly.newPlot("chart", data, layout, { responsive: true, displaylogo: false });
  const top = orders.indexOf(Math.max(...orders)), sum = orders.reduce((a, b) => a + b);
  console.log("days:", days.length, "| first:", days[0], "| last:", days[179]);
  console.log("max:", orders[top], "on", days[top], "| mean:", (sum / 180).toFixed(1));
}
const s = document.createElement("script");
s.src = "https://cdn.jsdelivr.net/npm/plotly.js-dist-min@4.1.1/plotly.min.js";
s.onload = draw; s.onerror = () => console.error("Plotly.js আনা যায়নি");
document.head.appendChild(s);

আসল আউটপুট (এই পাতার JavaScript runner, Edge):

days: 180 | first: 2026-01-01 | last: 2026-06-29
max: 91 on 2026-03-20 | mean: 47.5

১৮০ দিন, ১ জানুয়ারি থেকে ২৯ জুন ২০২৬. সবচেয়ে বেশি অর্ডার ৯১টি, ২০ মার্চ: সেদিন শুক্রবার আর কাল্পনিক উৎসবের শেষ দিন, তাই দুই ভিড় একসঙ্গে (40 + 12 + 35 + এলোমেলো অংশ)। গড়ে দিনে ৪৭.৫টি অর্ডার। ধূসর রেখা দৈনিক সংখ্যা, শুক্রবারের খাঁজসহ; নীল রেখা ৭ দিনের গড়, যাতে মার্চের উৎসবের ঢেউ পরিষ্কার। প্রথম ছয় দিনে গড়ের মান null, তাই নীল রেখা ৭ জানুয়ারি থেকে শুরু, ঠিক pandas-এর rolling(7)-এর NaN-এর মতো। নিচের range slider-এ পুরো ছয় মাস সবসময় দেখা যায়; দুই প্রান্ত টেনে মার্চের তৃতীয় সপ্তাহে যান, ওপরের chart সেই সাত দিন বড় করে দেখাবে। “1m” বোতাম শেষ এক মাস দেখায়।

৬ · দেখানো আর সংরক্ষণ — fig.show(), renderer, write_html আর write_image

fig.show() আর renderer. fig.show() নিজে কিছু আঁকে না; এটি figure-টিকে একটি renderer-এর হাতে দেয়, আর renderer ঠিক করে কোথায় দেখাবে। Plotly কোথায় চলছে দেখে default renderer বাছে। পর্ব ৮-এ দেখেছেন, Jupyter-এ একটি cell-এর আউটপুট আসলে কয়েকটি mimetype-এ রাখা তথ্য, আর frontend (JupyterLab, VS Code) ঠিক করে কোনটি দেখাবে। Plotly সেই ব্যবস্থাই ব্যবহার করে। আমাদের তিনটি সত্যিকারের run (plotly 7.1.0, ipykernel 7.4.0, nbclient 0.11.0 দিয়ে notebook চালিয়ে):

# সাধারণ .py script, terminal থেকে
default renderer: browser

# notebook, nbclient দিয়ে চালানো
0 stream: plotly_mimetype
1 display_data mimetypes: ['application/vnd.plotly.v1+json'] {'application/vnd.plotly.v1+json': 7677}
2 display_data mimetypes: ['image/png'] {'image/png': 18664}    # fig.show(renderer="png")

# একই notebook, VS Code-এর ভেতর থেকে চালালে
0 stream: vscode

Script-এ fig.show() একটি অস্থায়ী HTML বানিয়ে ব্রাউজার খোলে। Notebook-এ figure-টি application/vnd.plotly.v1+json mimetype-এ JSON হিসেবে ipynb ফাইলে বসে, আর JupyterLab বা VS Code সেটি plotly.js দিয়ে আঁকে। যেখানে এই mimetype বোঝে না (যেমন অনেক অনলাইন ipynb প্রিভিউ), সেখানে chart দেখা নাও যেতে পারে; এজন্য শেয়ার করার notebook-এ fig.show(renderer="png") বা pio.renderers.default = "png" একটি স্থির ছবি রাখে। আর পর্ব ৮-এর নিয়মটি মনে রাখুন: interactive chart notebook-কে ভারী করে, আর Git-এ diff অপাঠ্য করে; commit-এর আগে output মুছুন বা nbstripout ব্যবহার করুন।

write_html আর plotly.js কোথা থেকে আসবে। HTML ফাইলে figure-এর JSON থাকে, কিন্তু ছবি আঁকতে plotly.js লাগে। include_plotlyjs ঠিক করে সেটি কোথা থেকে আসবে। অংশ ২-এর ৬০০ অর্ডারের scatter দিয়ে সত্যিকারের run (plotly 7.1.0):

fig.write_html("out/scatter_embedded.html")                         # default: True
fig.write_html("out/scatter_cdn.html", include_plotlyjs="cdn")
fig.write_html("out/scatter_div.html", include_plotlyjs=False, full_html=False)
fig.write_html("out/dir/chart.html", include_plotlyjs="directory")

scatter_embedded.html 4849057 bytes
scatter_cdn.html 29774 bytes
scatter_div.html 29372 bytes
cdn script: ['https://cdn.plot.ly/plotly-4.1.1.min.js']
directory mode files: ['chart.html', 'plotly.min.js'] [7971, 4819433]
scatter.png 54583 bytes
scatter.svg 130726 bytes
include_plotlyjsআমাদের মাপকখনঝুঁকি
True (default)৪.৮৫ MBইন্টারনেট ছাড়া খুলতে হবে, ইমেইলে একটি ফাইলপ্রতিটি chart-এ ৪.৮ MB বাড়তি
"cdn"২৯.৮ KBওয়েবসাইট, অনলাইনে শেয়ারইন্টারনেট ছাড়া খালি পাতা; বাইরের সার্ভারের ওপর নির্ভরতা
"directory"৮ KB + একবার ৪.৮ MBএকই folder-এ অনেক chartplotly.min.js ফাইলটি সঙ্গে না গেলে ভাঙে
False + full_html=False২৯.৪ KB (শুধু <div>)নিজের পাতায় বসানো, যেখানে plotly.js আগেই আছেপাতায় plotly.js না থাকলে কিছুই আঁকে না

সংখ্যাগুলো থেকে একটি শিক্ষা: ৬০০ বিন্দুর chart-এ আসল ডেটা প্রায় ২৯ KB, আর লাইব্রেরি ৪.৮ MB, প্রায় ১৬০ গুণ। gzip করলে plotly.min.js আমাদের মাপে প্রায় ১.৪৭ MB, তবু ফোনের ডেটায় তা কম নয়। "cdn" লিংকে সংস্করণ বাঁধা থাকে (plotly-4.1.1.min.js), তাই ফাইলটি পরে একই আচরণ করে। একটি নিরাপত্তার দিক: CDN থেকে script আনা মানে আপনার পাতা একটি বাইরের সার্ভারকে বিশ্বাস করছে; সংবেদনশীল ভেতরের রিপোর্টে নিজের সার্ভারে plotly.js রাখা ("directory") বা embedded ফাইল নিরাপদ বিকল্প।

write_image দিয়ে স্থির ছবি। রিপোর্ট, PDF বা WhatsApp-এর জন্য fig.write_image("chart.png", width=800, height=450, scale=2). এটি Kaleido নামের আলাদা package দিয়ে কাজ করে, যা ভেতরে একটি headless Chrome বা Chromium চালিয়ে plotly.js দিয়ে ছবি আঁকে। Kaleido 1.x-এর জন্য যন্ত্রে Chrome থাকা লাগে; না থাকলে plotly-র docs-এ plotly_get_chrome কমান্ডের কথা আছে। আমাদের run-এ (Kaleido 1.5.0) PNG 54,583 বাইট আর SVG 130,726 বাইট এসেছে। SVG vector, তাই ছাপায় ঝকঝকে, কিন্তু ৬০০ বিন্দুর প্রতিটি একটি আলাদা বস্তু বলে PNG-এর চেয়ে বড়। scale=2 দিলে একই মাপের ছবিতে দ্বিগুণ pixel, ফোনের উচ্চ-ঘনত্বের পর্দায় ভালো।

৭ · অনেক point — গতি, ফাইলের আকার আর WebGL

SVG-তে প্রতিটি বিন্দু ব্রাউজারের পাতায় একটি আলাদা বস্তু (DOM element). কয়েক হাজার বিন্দু পর্যন্ত এটি ঠিক চলে, কিন্তু লাখের কাছে গেলে hover আর zoom ধীর হয়ে যায়, কারণ ব্রাউজারকে প্রতিটি বস্তু আলাদা করে সামলাতে হয়। WebGL ভিন্ন পথে আঁকে: সব বিন্দু একসঙ্গে GPU-তে পাঠিয়ে একটি ক্যানভাসে, ঠিক যেমন গেমের ছবি আঁকা হয়। Plotly-তে এর trace-এর নাম scattergl. px-এর render_mode="auto" (default) নিজেই বাছে; আমাদের সত্যিকারের run (plotly 7.1.0):

1000 points default -> trace type: scatter
1500 points default -> trace type: scattergl
50k points render_mode=webgl -> trace type: scattergl

# include_plotlyjs="cdn" দিয়ে HTML-এর আকার
1000 scatter 35206 bytes
10000 scattergl 280190 bytes
100000 scattergl 2727070 bytes

অর্থাৎ ১০০০-এর বেশি বিন্দু হলে px নিজে WebGL-এ যায়। কিন্তু ফাইলের আকার বিন্দুর সংখ্যার সঙ্গে প্রায় সমানুপাতিক বাড়ে: ১ লাখ বিন্দুতে CDN-এর ফাইলও ২.৭ MB, কারণ প্রতিটি বিন্দুর x আর y ফাইলের ভেতরে। WebGL-এর নিজের সীমাও আছে: ব্রাউজার একটি পাতায় একসঙ্গে সীমিত সংখ্যক WebGL context চালাতে দেয়, তাই এক পাতায় অনেকগুলো WebGL chart রাখলে কিছু খালি দেখাতে পারে (Plotly-র docs-এও এই সতর্কতা আছে); আর কিছু পুরোনো ফোন বা দূরবর্তী ডেস্কটপে WebGL দুর্বল। সমাধানের ক্রম মোটামুটি এমন:

  1. প্রশ্নটি আবার জিজ্ঞেস করুন: ১ লাখ বিন্দু কি সত্যিই দেখা দরকার, নাকি জেলাভিত্তিক গড় বা histogram প্রশ্নের উত্তর দেয়? (পর্ব ২৪-এর aggregation)
  2. ঘনত্ব দেখান: px.density_heatmap বা histogram; লাখ বিন্দু একটি কালো দাগ হয়ে যায়, ঘনত্বের রং বেশি বলে।
  3. নমুনা নিন: df.sample(5000, random_state=42), আর শিরোনামে লিখুন যে এটি নমুনা। অস্বাভাবিক বিন্দুগুলো (outlier) আলাদা করে সবসময় রাখুন, কারণ নমুনায় সেগুলো হারাতে পারে।
  4. শেষে WebGL: সব বিন্দু সত্যিই লাগলে render_mode="webgl", আর এক পাতায় কয়েকটির বেশি নয়। আরও বড় ডেটায় Datashader-এর মতো টুল আগে থেকেই ছবিকে pixel-এ গুটিয়ে আনে।

৮ · Hover data আর গোপনীয়তা — tooltip-এ কী যাবে

বাজারঘরের অর্ডারের টেবিলে প্রায়ই এমন কলাম থাকে যা chart-এ আসার কথা নয়: ক্রেতার নাম, ফোন নম্বর, ঠিকানা। “সব তথ্য hover-এ দিয়ে দিই, যার যা লাগে দেখবে” — সহজ, কিন্তু বিপজ্জনক। Interactive chart প্রায়ই একটি HTML ফাইল হয়ে ইমেইলে, Slack-এ বা ওয়েবসাইটে যায়, আর hover-এর প্রতিটি কলাম সেই ফাইলের ভেতরে লেখা থাকে, যে কেউ খুললেই পড়তে পারেন। এটি data protection-এর একটি সরাসরি প্রশ্ন। নিচের ছবি একটি সত্যিকারের run-এর screenshot, যেখানে hover_data=["order_id", "phone"] দেওয়া হয়েছে (নম্বরগুলো বানানো, “0000-” দিয়ে শুরু, কোনো আসল নম্বর নয়).

একটি Plotly scatter chart, x অক্ষে delivery_days আর y অক্ষে amount_tk. একটি বিন্দুর ওপর mouse রাখায় নীল tooltip খুলেছে, তাতে লেখা Dhaka, delivery_days=2, amount_tk=9320, order_id=1003 আর phone=0000-221419 লেখা।
ছবি ৩১.৩ — সত্যিকারের run: plotly 7.1.0-এর write_html(include_plotlyjs="cdn"), Edge-এ খুলে একটি বিন্দুর ওপর hover. tooltip-এ ফোন নম্বর পুরো দেখা যাচ্ছে। বানানো নম্বর, বাজারঘরের কাল্পনিক ডেটা।

সমাধান মনে হতে পারে hover_data={"phone": False}, যা px-এর docs অনুযায়ী কলামটিকে tooltip থেকে লুকায়। আমরা চারটি রূপ পরীক্ষা করেছি, আর প্রতিবার দেখেছি HTML ফাইলে নম্বরটি আছে কি না। নিচের ধাপে আসল ফলাফল।

1a = px.scatter(o, ..., hover_data=["phone"])2b = px.scatter(o, ...,
      hover_data={"phone": False})3c = px.scatter(o.drop(columns="phone"),
      ..., hover_data=["order_id"])4d = px.scatter(o, ..., hover_name="district",
      hover_data={"order_id": True,
                  "amount_tk": ":,.0f",
                  "delivery_days": False})
মান (Watch)
tooltip
custom
ফাইলে
  1. সরাসরি ফাঁস। hover_data=["phone"]: hovertemplate-এ phone=%{customdata[0]}, আর customdata-য় প্রতিটি অর্ডারের নম্বর। tooltip-এ দেখা যায়, ফাইলেও লেখা।
  2. লুকানো, কিন্তু মোছা নয়। {"phone": False} দিলে hovertemplate থেকে phone চলে গেছে, কিন্তু আমাদের run-এ customdata-য় এখনো '0000-611844', আর HTML ফাইলে খুঁজলে নম্বরটি পাওয়া গেছে (True). ফাইল খুলে source দেখলে যে কেউ পড়তে পারবেন।
  3. আগেই বাদ। o.drop(columns="phone") দিয়ে chart বানালে কলামটি figure-এ ঢোকার সুযোগই পায় না: customdata-য় শুধু 1001, HTML-এ নম্বর নেই (False).
  4. যত্ন করে বাছা। dict-এ শুধু যা দেখাতে চান: order_id দেখানো আর টাকা কমা-সহ (":,.0f"); আমাদের run-এ "delivery_days": False-ও ছিল, যা x অক্ষের মান tooltip থেকে সরায়. phone তালিকায় নেই, তাই মূল DataFrame-এ থাকলেও আমাদের run-এ ফাইলে আসেনি। তবু সবচেয়ে নিরাপদ অভ্যাস ধাপ ৩: আগে বাদ।

“লুকানো” আর “নেই” এক নয়। Interactive chart-এর ফাইল একটি ডেটার ফাইল, ছবি নয়। hover, legend-এ বন্ধ করা trace, বা zoom-এর বাইরের বিন্দু, সবই ফাইলের ভেতরে থাকে। তাই নিয়ম: chart বানানোর আগে DataFrame থেকে ব্যক্তিগত কলাম (নাম, ফোন, ঠিকানা, ক্রেতার id) বাদ দিন, বা প্রয়োজনে ছদ্মনাম দিন; hover-এর জন্য একটি “অনুমোদিত কলামের তালিকা” (allow-list) রাখুন; আর শেয়ারের আগে ফাইলটিতে একটি নমুনা নম্বর বা নাম খুঁজে দেখুন, ঠিক যেমন আমরা "0000-611844" in html দিয়ে দেখেছি। ব্যক্তিগত তথ্য সুরক্ষার বিস্তারিত নীতি সাইবার সিকিউরিটি কোর্সের privacy engineering পর্বে, আর AI-এর দিক থেকে পর্ব ৭২ · দায়িত্বশীল AI-এর অভ্যাস-এ আসবে।

অল্প বিন্দুর chart-এ আরেকটি ফাঁদ আছে। জেলা আর তারিখ মিলিয়ে কোনো বিন্দুতে যদি একটিমাত্র অর্ডার থাকে, তাহলে নাম-ফোন ছাড়াও hover-এর “সিলেট, ৩ মার্চ, ৮,৪৬০ টাকা, rating 1” একজন নির্দিষ্ট মানুষকে চিনিয়ে দিতে পারে, বিশেষ করে ছোট এলাকায়। এজন্য প্রকাশ্য chart-এ প্রায়ই সারাংশ (গড়, মোট, গণনা) দেখানো হয়, আর খুব ছোট গ্রুপ (যেমন ৫টির কম অর্ডার) একসঙ্গে মিশিয়ে দেওয়া হয়।

৯ · Accessibility আর অন্য টুল — টেবিল, লেখা আর বিকল্প

Accessibility. একজন দৃষ্টিপ্রতিবন্ধী পাঠক screen reader দিয়ে পাতা শোনেন; একজন হাত-কাঁপা বয়স্ক মানুষ mouse-এর বদলে keyboard ব্যবহার করেন; কেউ দেখেন ছোট ফোনে, রোদের মধ্যে। Interactive chart এঁদের সবার জন্য কঠিন। লেখার সময়ে Plotly-র tooltip মূলত mouse বা touch দিয়ে খোলে, keyboard দিয়ে এক বিন্দু থেকে আরেক বিন্দুতে যাওয়ার ব্যবস্থা সীমিত, আর screen reader chart-টিকে প্রায়ই একটি অজানা ছবি হিসেবে পায়। Web-এর accessibility নির্দেশিকা WCAG বলে, প্রতিটি অ-লেখা বিষয়বস্তুর একটি লেখা বিকল্প থাকা চাই, আর সব কাজ keyboard দিয়ে করা যাওয়া চাই (বিস্তারিত HTML ও CSS কোর্সের accessibility পর্বে). তাই নিয়ম সরল: প্রতিটি interactive chart-এর সঙ্গে একটি টেবিল বা লেখা সারাংশ, যাতে chart না দেখেও মূল কথা পাওয়া যায়। এর লাভ সবার: যিনি chart দেখছেন, তিনিও সংখ্যা মিলিয়ে নিতে পারেন। নিচের কোড অংশ ৩-এর একই ডেটা থেকে সেই টেবিল আর একটি বাক্য বানায়।

import numpy as np, pandas as pd
pd.set_option("display.width", 60)

rng = np.random.default_rng(42)
n = 200
df = pd.DataFrame({
    "district": rng.choice(["Dhaka", "Chattogram", "Khulna", "Rajshahi",
                            "Sylhet"], n, p=[0.4, 0.2, 0.15, 0.15, 0.1]),
    "qty": rng.integers(1, 6, n),
    "price_paisa": rng.integers(80, 2500, n) * 100,
})
df["amount_tk"] = df["qty"] * df["price_paisa"] // 100

# chart-এর পাশে দেওয়ার টেবিল: গণনা, মোট আর শতাংশ
t = (df.groupby("district")
       .agg(orders=("amount_tk", "size"), revenue_tk=("amount_tk", "sum"))
       .sort_values("revenue_tk", ascending=False))
t["share_pct"] = (100 * t["revenue_tk"] / t["revenue_tk"].sum()).round(1)
print(t)

# screen reader আর ছাপার জন্য এক বাক্যের সারাংশ
top, low = t.index[0], t.index[-1]
total = int(t["revenue_tk"].sum())
print(f"\nসারাংশ: {n}টি অর্ডারে মোট {total:,} টাকা; "
      f"সবচেয়ে বেশি {top} ({t.loc[top, 'share_pct']}%), "
      f"সবচেয়ে কম {low} ({t.loc[low, 'share_pct']}%).")

আসল আউটপুট (ব্রাউজারের Pyodide, pandas 3.0.2):

            orders  revenue_tk  share_pct
district                                 
Dhaka           79      325151       41.2
Chattogram      44      186954       23.7
Rajshahi        33      139575       17.7
Khulna          28      100543       12.8
Sylhet          16       36192        4.6

সারাংশ: 200টি অর্ডারে মোট 788,415 টাকা; সবচেয়ে বেশি Dhaka (41.2%), সবচেয়ে কম Sylhet (4.6%).

টেবিলে প্রতিটি জেলার অর্ডারের সংখ্যা, মোট টাকা আর মোটের শতাংশ, বড় থেকে ছোট সাজানো। ঢাকার ৭৯টি অর্ডার (২০০-এর ৩৯.৫%) মোট বিক্রির ৪১.২%, আর সিলেটের ১৬টি অর্ডার (৮%) মাত্র ৪.৬%: সিলেটে অর্ডারপিছু গড় বিক্রিও কম (৩৬,১৯২ / ১৬ ≈ ২,২৬২ টাকা, ঢাকায় ৩,২৫,১৫১ / ৭৯ ≈ ৪,১১৬ টাকা)। এই তথ্য একটি bar chart-এ hover করে খুঁজতে হতো; টেবিলে এক নজরে আছে। শতাংশের যোগফল ৪১.২ + ২৩.৭ + ১৭.৭ + ১২.৮ + ৪.৬ = ১০০.০, যা হাতে মিলিয়ে দেখার মতো একটি সহজ পরীক্ষা। শেষের বাক্যটি screen reader-এ এক নিঃশ্বাসে শোনা যায়, আর ছাপা কপিতে chart-এর নিচের ক্যাপশন হিসেবেও বসে।

এই টেবিলটি t.to_html() দিয়ে HTML টেবিল বানিয়ে chart-এর ঠিক নিচে বসানো যায়, আর বাক্যটি chart-এর শিরোনাম, ছবির alt লেখা বা chart-এর ওপরের অনুচ্ছেদে। আরও কয়েকটি অভ্যাস: chart-এর <div>-এ role="img" আর একটি aria-label দিন (Plotly এটি নিজে সবসময় দেয় না); ফন্ট যেন ফোনে পড়া যায় (layout.font.size); রং ছাড়াও আকৃতি বা লেখা দিয়ে পার্থক্য (অংশ ৪-এর marker); আর পর্ব ৩২ · সৎ chart-এ রঙের পছন্দ ও বিভ্রান্তিকর অক্ষ নিয়ে আরও বিস্তারিত আসবে।

অন্য টুল। Plotly একমাত্র পথ নয়। নিচের টেবিলটি একটি মোটা দাগের পরিচিতি; সংস্করণগুলো লেখার সময়ে PyPI-তে দেখা, আর প্রতিটির সক্ষমতা দ্রুত বদলায়, তাই কাজের আগে নিজের সংস্করণের docs দেখে নিন।

টুলধরনকখন মানায়
Altair (6.3.0)grammar of graphics: ডেটা, mark আর encoding ঘোষণা করে লেখা; ভেতরে Vega-Lite-এর JSONছোট-মাঝারি ডেটায় পরিষ্কার, যুক্তিসংগত chart; একাধিক chart যুক্ত করে ছাঁকনি
Bokeh (3.10.1)Python-কেন্দ্রিক interactive chart, নিজস্ব BokehJS দিয়ে ব্রাউজারে আঁকেবড় ডেটা, নিজের server-এ Python-এর সঙ্গে চলমান app
Streamlit (1.66.1)একটি Python script থেকে web app; নোট হিসেবেদ্রুত ভেতরের dashboard বা ডেমো
Dash (4.5.0)Plotly-র নির্মাতাদের web app framework, plotly.js-এর chart; নোট হিসেবেPlotly chart নিয়ে বড়, নিয়ন্ত্রিত dashboard
Panel (1.10.0)HoloViz প্রকল্পের dashboard টুল, অনেক plotting লাইব্রেরির সঙ্গে চলে; নোট হিসেবেnotebook থেকে app, নানা লাইব্রেরি মিশিয়ে

Dashboard-এর টুলগুলো আসলে web server চালায়: প্রতিটি পাঠক ছাঁকনি বদলালে server-এ Python আবার চলে। তাই এগুলো বানানো আর চালানোর প্রশ্নগুলো (কোথায় host, কে দেখতে পাবে, login) web-এর বিষয়, যা Full-Stack কোর্সের deployment পর্বে আছে। একটি দ্রুত নিয়ম: একজন মানুষ, একবার → notebook-এ chart; অনেক মানুষ, একবার → HTML ফাইল বা PNG; অনেক মানুষ, রোজ → dashboard.

আরও গভীরে। Plotly কোম্পানি ২০১২ সালে Montreal-এ শুরু, আর plotly.js ২০১৫ সালে MIT লাইসেন্সে খোলা উৎস হয়; ভেতরে এটি d3.js আর WebGL-এর জন্য stack.gl-এর অংশ ব্যবহার করে (বিস্তারিত সংস্করণভেদে বদলেছে). Plotly Express ২০১৯ সালে plotly 4.0-এর অংশ হয়, আর plotly 6.0 (২০২৫) থেকে DataFrame পড়ার কাজ narwhals-এর মাধ্যমে, যাতে pandas ছাড়া Polars-ও সরাসরি চলে। Grammar of graphics-এর ধারণা Leland Wilkinson-এর ১৯৯৯ সালের বই “The Grammar of Graphics” থেকে; Vega-Lite (Satyanarayan, Moritz, Wongsuphasawat, Heer, ২০১৭) সেই ধারণাকে interactive chart-এর JSON ভাষায় এনেছে, আর Altair তার Python রূপ।

পরীক্ষা ও ইন্টারভিউতে। বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এমন প্রশ্ন আসতে পারে:
● Plotly Express আর Graph Objects-এর পার্থক্য? — px এক লাইনে DataFrame থেকে পুরো figure বানায়; go প্রতিটি trace ও layout হাতে নিয়ন্ত্রণ দেয়; px-এর ফলও একটি go.Figure.
● Plotly figure-এর দুই প্রধান অংশ? — data (trace-এর তালিকা) আর layout (অক্ষ, শিরোনাম, legend), পুরোটা JSON.
● include_plotlyjs="cdn" কেন ফাইল ছোট করে? — ৪.৮ MB-এর plotly.js ফাইলের ভেতরে না রেখে CDN-এর লিংক দেয়; কিন্তু ইন্টারনেট ছাড়া chart খোলে না।
● ১ লাখ বিন্দুর scatter ধীর হলে? — aggregation বা density আগে, নমুনা দ্বিতীয়, শেষে scattergl (WebGL).
● hover_data={"col": False} কি কলামটি ফাইল থেকে সরায়? — না; tooltip থেকে লুকায়, customdata-য় থেকে যেতে পারে। আগে drop করুন।

১০ · AI-এর যুগে — Plotly-র কোডে AI সহকারীর ভুল ধরা

“বাজারঘরের অর্ডার দিয়ে একটি সুন্দর interactive dashboard বানাও” লিখলে AI সহকারী মুহূর্তে কোড দেবে, আর সেটি প্রায়ই চলবে, chart-ও সুন্দর দেখাবে। কিন্তু Plotly-র কোডে কিছু ভুল বারবার দেখা যায়, আর সেগুলোর বেশিরভাগ error দেয় না:

  • পুরোনো API: plotly.offline.iplot, init_notebook_mode, titlefont-এর মতো পুরোনো নাম, বা Kaleido-র আগের Orca; আর pandas-এর df.append বা NumPy-র np.float, যা বর্তমান সংস্করণে নেই।
  • বানানো parameter: px-এ অস্তিত্বহীন argument (যেমন px.scatter(..., tooltip=...)) বা ভুল জায়গায় layout-এর নাম; অনেক সময় Plotly error দেয়, কখনো কখনো ভুল নামের ঘর চুপচাপ উপেক্ষিত হয়।
  • সব কলাম hover-এ: “বিস্তারিত দেখাতে” পুরো DataFrame hover_data-য়, ফোন আর নামসহ; আর লুকাতে False, যা ফাইল থেকে সরায় না।
  • ফাইলের আকার উপেক্ষা: প্রতিটি chart-এ default write_html, প্রতিটি ৪.৮ MB; বা লাখ বিন্দুর scatter, aggregation ছাড়াই।
  • বিশ্বাসযোগ্য কিন্তু ভুল পরিসংখ্যান: chart-এর শিরোনামে “৪০% বৃদ্ধি” লেখা, যা ডেটা থেকে হিসাব করা নয়; train সেটের মেট্রিক dashboard-এ “model accuracy” নামে; বা warning চাপা দিয়ে warnings.filterwarnings("ignore").

প্রম্পটে সংস্করণ, ডেটার গঠন, কোথায় দেখানো হবে, আর কোন কলাম দেখানো নিষেধ, তা স্পষ্ট লিখুন। আসল ক্রেতার ডেটা নয়, বানানো ছোট নমুনা দিন।

আমি Python 3.14, plotly 7.1 আর pandas 3.0-এ কাজ করছি। আমার DataFrame orders-এ কলাম: order_id, date, district, category, qty, price_paisa (int64), delivery_days, rating, phone. বানানো নমুনা সারি: 1003, 2026-04-28, Dhaka, Fashion, 4, 233000, 2, 2, 0000-221419. ১. phone কলাম chart বানানোর আগেই drop করো; hover_data-য় শুধু order_id, category আর rating রাখো। hover_data={"col": False} দিয়ে লুকানো চলবে না। ২. টাকা = qty * price_paisa / 100; অক্ষের নাম labels দিয়ে লেখো, একক (taka) সহ। ৩. px.scatter: x = delivery_days, y = টাকা, color = district; রং-অন্ধতা-সহনীয় palette আর জেলাভেদে আলাদা symbol. ৪. write_html করো include_plotlyjs="cdn" দিয়ে; ফাইলের আকার ছাপো, আর ফাইলে "0000-" লেখা আছে কি না পরীক্ষা করে ছাপো। ৫. chart-এর সঙ্গে জেলাভিত্তিক গণনা আর মোট টাকার একটি pandas টেবিল আর এক বাক্যের সারাংশ দাও। কোনো warning চাপা দেবে না। কোনো function বা parameter নিয়ে নিশ্চিত না হলে বলো; বানিয়ে লিখবে না, আর plotly-র কোন docs পাতায় দেখেছ তা উল্লেখ করো।

AI-এর কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১“Overview first, zoom and filter, then details-on-demand” — Shneiderman-এর এই সূত্র কোথা থেকে এল, আর আজকের interactive chart-এ এটি কতটা খাটে?

Ben Shneiderman University of Maryland-এর Human-Computer Interaction Lab-এর একজন প্রতিষ্ঠাতা গবেষক। ১৯৯৬ সালে IEEE-র একটি visual language-এর সম্মেলনে তাঁর প্রবন্ধ “The Eyes Have It: A Task by Data Type Taxonomy for Information Visualizations” প্রকাশিত হয়। সেখানে তিনি বিভিন্ন ধরনের ডেটা (এক মাত্রা, দুই মাত্রা, time series, tree, network) আর তথ্য খোঁজার কাজের একটি শ্রেণিবিভাগ দেন, আর পুরো কথাকে এক লাইনে বাঁধেন, যাকে তিনি নিজে “Visual Information-Seeking Mantra” বলেছিলেন। তখন ব্যক্তিগত কম্পিউটারে সবে দ্রুত গ্রাফিক্স আসছে, আর গবেষকেরা খুঁজছিলেন মানুষ কীভাবে বড় ডেটার ভেতরে “হাঁটতে” পারে।

আজকের Plotly chart-এ সূত্রের তিন ধাপই সরাসরি দেখা যায়। Overview: chart প্রথমে সব ডেটা দেখায়। Zoom and filter: mouse টেনে zoom, legend-এ ক্লিক করে trace বন্ধ, range slider দিয়ে সময়ের একটি অংশ। Details-on-demand: hover-এ একটি বিন্দুর order_id আর টাকা।

কিন্তু সূত্রটি অনুসন্ধানের জন্য, ব্যাখ্যার জন্য নয়। পরে visualization গবেষণায় “exploratory” আর “explanatory” chart আলাদা করে দেখার রীতি জোরালো হয়েছে: প্রথমটিতে বিশ্লেষক নিজে উত্তর খোঁজেন, দ্বিতীয়টিতে তিনি অন্যকে একটি নির্দিষ্ট কথা বোঝান। দ্বিতীয় ক্ষেত্রে details-on-demand একটি ঝুঁকি, কারণ পাঠক হয়তো চাইবেনই না, আর মূল কথা লুকিয়ে থাকবে tooltip-এ। এজন্য অনেক সংবাদ প্রতিষ্ঠান মূল কথা স্থির অংশে দেখিয়ে interactivity রাখে বাড়তি স্তর হিসেবে।

বাজারঘরের কাজে এর অর্থ: EDA-র সময় (পর্ব ৬২) Shneiderman-এর পূর্ণ সূত্র, আর ম্যানেজারের রিপোর্টে শিরোনামে মূল কথা, স্থির chart, সঙ্গে চাইলে একটি interactive লিংক।

মূল উপলব্ধি: interactivity তথ্য খোঁজার একটি পদ্ধতি; যখন উত্তর জানা, তখন সেটি নিজের মুখে বলাই ভালো যোগাযোগ।

প্র ০২Plotly কেন Python-এ ছবি না এঁকে একটি JSON বানায়? এই নকশার লাভ আর দাম কী?

Plotly-র ইতিহাসে এর উত্তর আছে। কোম্পানিটি শুরুতে একটি অনলাইন chart সেবা চালাত: মানুষ ডেটা পাঠাত, আর ওয়েবসাইটে chart দেখা যেত। এমন ব্যবস্থায় স্বাভাবিক নকশা হলো, ছবি আঁকার কাজ এক জায়গায়, ব্রাউজারে, একটি JavaScript লাইব্রেরিতে; আর Python, R, MATLAB, Julia-র মতো প্রতিটি ভাষা শুধু একই বর্ণনা (JSON) লেখে। ২০১৫ সালে plotly.js খোলা উৎস হলে এই নকশাই অফলাইনে চলে আসে, আর plotly 4.0 (২০১৯) থেকে Python package default-এ পুরো অফলাইন, কোনো অ্যাকাউন্ট ছাড়া।

লাভ অনেক। এক: এক renderer, অনেক ভাষা; Python আর R-এর chart দেখতে হুবহু এক, আর একটি বাগ ঠিক হলে সব ভাষায় ঠিক হয়। দুই: figure একটি সাধারণ ডেটা, তাই সেটি সংরক্ষণ, পাঠানো, পরীক্ষা করা যায়; এই পর্বে fig.data[0].customdata ছেপে দেখা গেছে কোন কলাম ফাইলে যাচ্ছে। তিন: interactivity বিনামূল্যে, কারণ ব্রাউজারে ছবি আঁকার সময় plotly.js ডেটা হাতেই রাখে।

দাম তিনটি। প্রথম, ব্রাউজার বা ব্রাউজারের মতো কিছু ছাড়া ছবি হয় না; স্থির PNG বানাতেও Kaleido একটি headless Chrome চালায়, যা ভারী নির্ভরতা, আর কিছু সার্ভারে Chrome বসানো ঝামেলার। matplotlib সেখানে নিজেই Python-এ pixel আঁকে (Agg backend), কোনো ব্রাউজার ছাড়া, যা এই পাতার Pyodide-এ সরাসরি চলে। দ্বিতীয়, ফাইলের আকার: ছবি নয়, ডেটা যায়, তাই বিন্দু যত বেশি, ফাইল তত বড়; আমাদের run-এ ১ লাখ বিন্দুতে ২.৭ MB. তৃতীয়, গোপনীয়তা: ডেটা যায় বলে hover-এর প্রতিটি কলাম, এমনকি লুকানোটিও, পাঠকের হাতে পৌঁছায়। একটি স্থির PNG-তে যে তথ্য আঁকা হয়নি, তা ফাঁস হতে পারে না।

এই দাম-লাভ বোঝা থাকলে বাছাই সহজ: যেখানে ডেটা নিয়ে খেলা দরকার, সেখানে JSON-ভিত্তিক Plotly; যেখানে ছবি পাঠানো দরকার, সেখানে pixel-ভিত্তিক matplotlib বা write_image.

মূল উপলব্ধি: Plotly ছবি নয়, ডেটা পাঠায়; এজন্যই এটি interactive, আর এজন্যই এর ফাইলের আকার ও গোপনীয়তা নিয়ে আলাদা করে ভাবতে হয়।

প্র ০৩Interactive chart কি accessibility-র জন্য ভালো, না খারাপ? WCAG আর বাস্তব ব্যবহারকারীর দিক থেকে ভাবুন।

দুই দিকেই যুক্তি আছে, আর এটিই প্রশ্নটিকে আকর্ষণীয় করে। প্রথমে নিয়মের কাঠামো। W3C-র Web Content Accessibility Guidelines (WCAG), যার 2.0 সংস্করণ ২০০৮ সালে আর 2.1 সংস্করণ ২০১৮ সালে প্রকাশিত, চারটি নীতির ওপর দাঁড়ানো: বোধগম্য, পরিচালনাযোগ্য, বোঝার মতো আর টেকসই। এর দুটি নির্দেশ chart-এর সঙ্গে সরাসরি জড়িত: প্রতিটি অ-লেখা বিষয়বস্তুর একটি লেখা বিকল্প (text alternative), আর সব কার্যকারিতা keyboard দিয়ে পাওয়া। একটি স্থির PNG-এর জন্য প্রথমটি পূরণ করা তুলনামূলক সহজ: একটি ভালো alt লেখা আর পাশে টেবিল।

Interactive chart-এ কাজ কঠিন হয়। Hover-এর তথ্য mouse ছাড়া প্রায়ই পাওয়া যায় না; box select বা lasso keyboard দিয়ে করা প্রায় অসম্ভব; আর screen reader একটি SVG-এর ভেতরের শত শত বস্তু পড়তে গেলে অর্থহীন তালিকা শোনায়, বা কিছুই না। WebGL-এর ক্যানভাসে তো কোনো বস্তুই নেই, শুধু pixel. ফলে যে তথ্য আপনি hover-এ রেখেছেন, একজন screen reader ব্যবহারকারীর কাছে তা কার্যত অনুপস্থিত।

অন্যদিকে, interactivity কিছু মানুষের জন্য সাহায্যও। দুর্বল দৃষ্টির কেউ zoom করে ছোট বিন্দু বড় করে দেখতে পারেন; legend-এ trace বন্ধ করে জটিল chart সরল করা যায়; আর chart-কে শব্দে রূপান্তর (sonification) বা keyboard দিয়ে ডেটার ভেতরে চলার ব্যবস্থা নিয়ে গবেষণা আর কিছু লাইব্রেরিতে কাজ চলছে, যদিও মান টুলভেদে আলাদা।

বাস্তব নীতি তাই: interactivity যোগ করুন একটি স্তর হিসেবে, ভিত্তি হিসেবে নয়। ভিত্তি হলো লেখা সারাংশ, সংখ্যার টেবিল, আর শিরোনামে মূল কথা, যা এই পর্বের Pyodide কোড দেখিয়েছে। বাংলাদেশে অনেক ব্যবহারকারী কম দামের ফোনে, দুর্বল ইন্টারনেটে পাতা দেখেন; তাঁদের জন্য ৪.৮ MB-এর plotly.js না নামা পর্যন্ত chart-এর জায়গা খালি থাকে, আর টেবিলটি তখনই পড়া যায়। এটিও এক ধরনের accessibility.

মূল উপলব্ধি: interactive chart কিছু মানুষের জন্য দরজা খোলে, কিছু মানুষের জন্য বন্ধ করে; তাই chart-এর মূল কথা সবসময় লেখা আর টেবিলেও থাকা চাই।

প্র ০৪একটি interactive chart-এর HTML ফাইল ইমেইলে পাঠানো আর একটি PNG পাঠানোর মধ্যে গোপনীয়তার পার্থক্য কী? “চোখে দেখা যায় না” কেন নিরাপত্তা নয়?

একটি PNG হলো pixel-এর একটি গ্রিড। এতে যা আঁকা হয়েছে, শুধু তা-ই আছে; একটি বিন্দুর পেছনে কোন অর্ডার, কোন ক্রেতা, তা ছবিতে নেই। একটি Plotly HTML ফাইল উল্টো: এটি একটি ছোট ডেটাবেস, যার সঙ্গে একটি আঁকার প্রোগ্রাম জোড়া। প্রতিটি বিন্দুর x, y, আর customdata-য় hover-এর প্রতিটি কলাম ফাইলে লেখা থাকে। Legend-এ বন্ধ করা trace, zoom-এর বাইরের বিন্দু, আর False দিয়ে “লুকানো” hover কলাম, সবই সেখানে। এই পর্বের পরীক্ষায় hover_data={"phone": False} দেওয়ার পরেও HTML-এ ফোন নম্বর পাওয়া গেছে; ফাইলটি Notepad-এ খুলে Ctrl+F চাপলেই যথেষ্ট।

নিরাপত্তার ভাষায় এটি “security through obscurity”-র একটি রূপ: তথ্য লুকিয়ে রাখা, মুছে না ফেলা। এর ইতিহাস দীর্ঘ। বিভিন্ন দেশে সরকারি বা আদালতের PDF-এ কালো আয়তক্ষেত্র দিয়ে লেখা ঢেকে দেওয়া হয়েছে, কিন্তু নিচের লেখা মোছা হয়নি, আর কপি-পেস্টে সব বেরিয়ে এসেছে, এমন ঘটনা বহুবার সংবাদ হয়েছে। মূল শিক্ষা প্রতিবার এক: যে তথ্য পাঠকের কাছে যাওয়ার কথা নয়, তা ফাইলে থাকা চলবে না।

আরেকটি স্তর হলো পুনঃশনাক্তকরণ। নাম আর ফোন বাদ দিলেও অল্প কয়েকটি কলাম একসঙ্গে একজন মানুষকে চিনিয়ে দিতে পারে। Latanya Sweeney ১৯৯০-এর দশকের শেষে দেখিয়েছিলেন, যুক্তরাষ্ট্রে জন্মতারিখ, লিঙ্গ আর ZIP কোড একসঙ্গে বেশিরভাগ মানুষকে আলাদা করে চিনতে যথেষ্ট; তাঁর প্রকাশিত হিসাবে অনুপাতটি প্রায় ৮৭%, যদিও পরের গবেষকেরা ভিন্ন ডেটায় কিছুটা কম পেয়েছেন। বাজারঘরের chart-এ “ছোট একটি উপজেলা, একটি নির্দিষ্ট দিন, একটি দামি পণ্য” একই কাজ করতে পারে। এজন্য প্রকাশ্য chart-এ ছোট গ্রুপ মেশানো আর সারাংশ দেখানো নিরাপদ।

করণীয় তাই তিন ধাপে: chart বানানোর আগে কলাম বাদ (drop), hover-এর জন্য একটি অনুমোদিত তালিকা, আর পাঠানোর আগে ফাইলে নমুনা খোঁজা।

মূল উপলব্ধি: interactive chart-এর ফাইল একটি ডেটার ফাইল; যা চোখে দেখা যায় না, তা ফাইলে থাকতে পারে, তাই গোপনীয়তার নিয়ম ডেটার স্তরে প্রয়োগ করতে হয়, ছবির স্তরে নয়।

প্র ০৫Grammar of graphics (Altair, Vega-Lite) আর Plotly Express-এর মতো “chart-এর ধরন ধরে” লেখা — দুই চিন্তার পার্থক্য কী, আর কোনটি কখন ভালো?

১৯৯৯ সালে পরিসংখ্যানবিদ Leland Wilkinson তাঁর বই “The Grammar of Graphics”-এ একটি প্রস্তাব দেন: chart-কে “bar chart”, “pie chart”-এর মতো আলাদা আলাদা ধরনের তালিকা হিসেবে না দেখে, কয়েকটি স্বাধীন অংশের সমাহার হিসেবে দেখা যায়: ডেটা, কোন জ্যামিতিক চিহ্ন (বিন্দু, রেখা, আয়তক্ষেত্র), কোন কলাম কোন দৃশ্যমান গুণে (x অবস্থান, y অবস্থান, রং, আকার), কোন স্কেল, কোন স্থানাঙ্ক ব্যবস্থা, আর কোন ভাগে (facet). Hadley Wickham এই ধারণাকে R-এর ggplot2-তে (প্রথম প্রকাশ ২০০৭ সালে) জনপ্রিয় করেন, আর University of Washington-এর Interactive Data Lab-এর Vega-Lite (২০১৭ সালের প্রবন্ধ) একে JSON-এ, interactivity-সহ, প্রকাশ করে। Altair সেই Vega-Lite-এর Python রূপ।

এই চিন্তায় “bar chart” আলাদা কিছু নয়: আয়তক্ষেত্র চিহ্ন, x-এ জেলা, y-এ মোট টাকা। আর stacked bar, grouped bar, facet-করা bar সব একই ব্যাকরণের ভিন্ন বাক্য। সুবিধা হলো নতুন ধরনের chart-ও কয়েকটি নিয়ম মিলিয়ে বানানো যায়, আর নিয়মগুলো যুক্তিসংগত বলে ভুল chart বানানো কঠিন। Altair-এ দুটি chart যুক্ত করে একটিতে বাছাই করলে অন্যটি ছেঁকে দেখানোও একই ঘোষণার ভাষায় লেখা যায়।

Plotly Express মাঝামাঝি পথ নিয়েছে। এর function-গুলো chart-এর ধরন ধরে (px.bar, px.line), কিন্তু argument-গুলো ব্যাকরণের অংশের মতো: x, y, color, facet_col, animation_frame সব function-এ একই অর্থে কাজ করে। ফলে শেখা সহজ, কিন্তু একেবারে নতুন ধরনের chart বানাতে go-তে নামতে হয়।

কোনটি কখন? দ্রুত EDA আর পরিচিত chart-এ px সাধারণত দ্রুততম। যুক্ত chart, যুক্তিসংগত গঠন আর গবেষণার মতো পুনরুৎপাদনযোগ্য বর্ণনায় Altair বা Vega-Lite অনেকের পছন্দ। আর বড় ডেটায় দুটিরই সীমা আছে: Altair default-এ একটি নির্দিষ্ট সংখ্যার বেশি সারি নিতে আপত্তি জানায় (সংখ্যাটি সংস্করণভেদে দেখে নিন), আর Plotly-তে ফাইল বড় হয়। এ নিয়ে সম্প্রদায়ে মতভেদ আছে; অনেক বিশ্লেষক দুটিই কিছুটা জানেন।

মূল উপলব্ধি: grammar of graphics chart-কে অংশের ব্যাকরণ হিসেবে দেখে, px দেখে পরিচিত ধরনের তালিকা হিসেবে, কিন্তু একই ব্যাকরণের argument দিয়ে; ধারণাটি বুঝলে যেকোনো লাইব্রেরি দ্রুত শেখা যায়।

অনুশীলন

প্রথমটি হিসাবের, দ্বিতীয়টি আউটপুট অনুমানের, তৃতীয়টি নকশা ও সমালোচনার, শেষটি কোড লেখার।

  1. হিসাব — ফাইলের আকার: একটি দলের সাপ্তাহিক রিপোর্টে ১২টি Plotly chart, প্রতিটি আলাদা HTML ফাইল, প্রতিটিতে প্রায় ২৫ KB ডেটা। plotly.js-এর আকার ধরুন ৪.৮ MB. (ক) default write_html-এ মোট আকার কত? (খ) "cdn" দিলে? (গ) "directory" দিলে, সব একই folder-এ? (ঘ) রিপোর্টটি বছরে ৫২ বার ইমেইলে গেলে (ক) আর (গ)-এর মধ্যে বছরে কত পার্থক্য?

    (ক) প্রতিটি ফাইল ৪.৮ MB + ২৫ KB ≈ ৪.৮২৫ MB; ১২টিতে ≈ ৫৭.৯ MB. (খ) ১২ × ২৫ KB = ৩০০ KB ≈ ০.৩ MB, তবে প্রতিটি খোলার সময় ইন্টারনেট লাগবে (ব্রাউজার cache থাকলে plotly.js একবারই নামে). (গ) ১২ × ২৫ KB + একটি ৪.৮ MB-এর plotly.min.js = ৫.১ MB. (ঘ) প্রতি সপ্তাহে ৫৭.৯ − ৫.১ = ৫২.৮ MB কম; বছরে ৫২ × ৫২.৮ ≈ ২,৭৪৬ MB, প্রায় ২.৭ GB, প্রতিজন প্রাপকের জন্য।

    এই পর্বের সত্যিকারের run-এ সংখ্যাগুলো ছিল embedded 4,849,057 বাইট, CDN 29,774 বাইট, আর directory-তে chart 7,971 বাইট + plotly.min.js 4,819,433 বাইট; অনুমানগুলো সেই মাপের কাছাকাছি। শিক্ষা: অনেক chart একসঙ্গে পাঠালে একটি HTML-এ সবগুলো রাখা (একবার plotly.js), বা "directory", বা PNG-ই ভালো।

  2. আউটপুট অনুমান করুন: নিচের কোড plotly 7.1-এ চালালে তিনটি লাইনে কী ছাপা হবে? আগে লিখুন, তারপর নিজের যন্ত্রে মিলিয়ে দেখুন।
    import pandas as pd, plotly.express as px
    o = pd.DataFrame({"district": ["Dhaka", "Sylhet", "Dhaka", "Khulna"],
                      "days": [4, 5, 2, 1], "tk": [1962, 5115, 9320, 740]})
    f = px.bar(o, x="district", y="tk", color="district")
    print(len(f.data), [t.name for t in f.data])
    print(f.data[0].type, list(f.data[0].y))
    print(f.layout.yaxis.title.text)
    3 ['Dhaka', 'Sylhet', 'Khulna']
    bar [np.int64(1962), np.int64(9320)]
    tk

    চার সারি, কিন্তু জেলা তিনটি, তাই trace ৩টি; ক্রম ডেটায় প্রথম দেখা ক্রমে, ঢাকা আগে। ঢাকার trace-এ দুটি সারি, তাই y-তে দুটি মান, 1962 আর 9320; একই জেলার দুটি bar একই x-এ পড়ে, আর px.bar-এর default barmode="relative"-এ সেগুলো একটির ওপর আরেকটি বসে। মানগুলো NumPy-র int64 হিসেবে ছাপা হয়েছে, কারণ list() একটি NumPy array-কে তালিকায় বদলায় কিন্তু প্রতিটি উপাদান NumPy-র সংখ্যাই থাকে (NumPy 2-এ এভাবে np.int64(...) দেখায়). y অক্ষের নাম কলামের নাম, tk. আমাদের ডেস্কটপ run-এ (plotly 7.1.0, NumPy 2.5.4) ঠিক এই আউটপুট এসেছে।

  3. নকশা ও সমালোচনা: একজন সহকর্মী (বা AI সহকারী) বাজারঘরের বোর্ড মিটিংয়ের জন্য নিচের কোড দিয়েছেন। ফলের HTML ফাইলটি ইমেইলে সব পরিচালককে যাবে, আর মিটিংয়ে ছাপা কপিও দেওয়া হবে। অন্তত পাঁচটি সমস্যা খুঁজে একটি ভালো পরিকল্পনা দিন।
    fig = px.scatter(orders, x="date", y="amount_tk", color="customer_id",
                     hover_data=orders.columns, animation_frame="month")
    fig.update_layout(title="Sales are up 40%!")
    fig.write_html("board_report.html")
    • ব্যক্তিগত তথ্য ফাঁস: hover_data=orders.columns সব কলাম, ফোন আর নামসহ, ফাইলে লিখে দেয়; আর ফাইলটি ইমেইলে বহু মানুষের কাছে যাচ্ছে।
    • color="customer_id": শত শত ক্রেতা মানে শত শত trace আর অর্থহীন legend; ফাইলও বড় হয়, আর প্রতিটি ক্রেতার id প্রকাশ পায়।
    • প্রশ্নের সঙ্গে chart মেলে না: বোর্ডের প্রশ্ন “বিক্রি কতটা বেড়েছে”, যার উত্তর মাসিক মোটের একটি line বা bar; প্রতিটি অর্ডারের scatter আর animation এখানে অপ্রয়োজনীয়, আর ছাপায় animation দেখাই যায় না।
    • যাচাইহীন শিরোনাম: “40%” কোথা থেকে, কোন সময়ের তুলনায়, তা হিসাব করা নেই; শিরোনামের সংখ্যা ডেটা থেকে হিসাব করে, তুলনার সময়সহ লিখতে হয়।
    • ফাইলের আকার আর ছাপা: default write_html প্রায় ৫ MB; ছাপা কপির জন্য কোনো স্থির ছবি বা টেবিল নেই; একক (টাকা) অক্ষে লেখা নেই।

    ভালো পরিকল্পনা: orders.drop(columns=[...ব্যক্তিগত কলাম]); মাসিক মোট groupby দিয়ে; px.line(monthly, x="month", y="revenue_tk", markers=True, labels={...}); শিরোনাম হিসাব করা সংখ্যা দিয়ে, যেমন “জানুয়ারি থেকে জুনে মাসিক বিক্রি X% বেড়েছে”; ছাপার জন্য write_image দিয়ে PNG আর পাশে মাসিক সংখ্যার টেবিল; ইমেইলের HTML লাগলে include_plotlyjs="cdn", আর পাঠানোর আগে ফাইলে নমুনা ফোন নম্বর খোঁজা।

  4. কোড লিখুন — নিরাপদ hover আর আকারের পরীক্ষা: নিজের যন্ত্রে plotly install করে (পর্ব ০৭-এর virtual environment-এ) একটি function লিখুন safe_scatter(df, x, y, hover, out), যা (ক) শুধু [x, y] + hover কলাম রেখে বাকি সব বাদ দেয়, (খ) px.scatter বানায়, (গ) include_plotlyjs="cdn" দিয়ে লেখে, (ঘ) ফাইলের আকার আর একটি নিষিদ্ধ লেখা (যেমন "0000-") ফাইলে আছে কি না ছাপে।
    import os, pandas as pd, plotly.express as px
    
    def safe_scatter(df, x, y, hover, out, forbidden="0000-"):
        keep = [x, y] + [c for c in hover if c not in (x, y)]
        fig = px.scatter(df[keep], x=x, y=y, hover_data=hover)
        fig.write_html(out, include_plotlyjs="cdn")
        with open(out, encoding="utf-8") as f:
            leaked = forbidden in f.read()
        print(out, os.path.getsize(out), "bytes | leaked:", leaked)
        return fig
    
    o = pd.DataFrame({"order_id": [1001, 1002, 1003, 1004],
                      "delivery_days": [4, 5, 2, 1],
                      "amount_tk": [1962, 5115, 9320, 740],
                      "phone": ["0000-611844", "0000-596444",
                                "0000-221419", "0000-104377"]})
    safe_scatter(o, "delivery_days", "amount_tk", ["order_id"], "safe.html")
    safe_scatter(o, "delivery_days", "amount_tk", ["order_id", "phone"], "bad.html")

    আমাদের ডেস্কটপ run-এর আউটপুট (plotly 7.1.0):

    safe.html 8190 bytes | leaked: False
    bad.html 8259 bytes | leaked: True

    প্রথম ডাকে phone কলামটি keep-এ নেই, তাই figure-এ ঢোকেনি, আর ফাইলে "0000-" পাওয়া যায়নি। দ্বিতীয় ডাকে hover-এ phone চাওয়া হয়েছে, তাই function ঠিকই সতর্ক করেছে: leaked: True. দুই ফাইলের আকারের পার্থক্য মাত্র ৬৯ বাইট: চারটি নম্বরের লেখা। আকার দেখে ফাঁস ধরা যায় না, খুঁজে দেখতে হয়। বাস্তবে forbidden-এর জায়গায় কয়েকটি আসল নমুনা (একটি নাম, একটি নম্বরের প্রথম অংশ) দিয়ে পরীক্ষা করুন, আর এই পরীক্ষাটিকে একটি ছোট unit test বানিয়ে রাখুন (পর্ব ১১).

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমবাজারঘরের অর্ডার থেকে একটি interactive chart বানিয়ে শেয়ার করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ❓ প্রশ্ন ঠিক করা: স্থির chart যথেষ্ট, নাকি interactive দরকার
  2. ✂️ DataFrame থেকে ব্যক্তিগত কলাম drop করা
  3. 📊 px.scatter বা px.line-এ hover_data-য় শুধু অনুমোদিত কলাম
  4. 🔎 fig.data-য় trace আর বিন্দুর সংখ্যা মূল ডেটার সঙ্গে মেলানো
  5. 💾 write_html(include_plotlyjs="cdn"), সঙ্গে টেবিল বা লেখা সারাংশ
  6. 🧪 পাঠানোর আগে ফাইলের আকার দেখা আর নমুনা ফোন নম্বর খোঁজা

Plotly-তে px.scatter(df, x="days", y="tk", color="district"), যেখানে district কলামে পাঁচটি আলাদা জেলা — figure-এর data-য় কয়টি trace থাকবে?

  • ১টি, পাঁচ রঙের বিন্দুসহ
  • ৫টি, প্রতিটি জেলায় একটি
  • সারির সংখ্যার সমান
  • ২টি, x আর y-এর জন্য

একটি Plotly chart এমন HTML ফাইলে রাখতে হবে যা ইন্টারনেট ছাড়া একটি ল্যাপটপে খুলবে, আর একটিই ফাইল পাঠানো যাবে। কোনটি উপযুক্ত?

  • write_html(..., include_plotlyjs="cdn")
  • write_html(..., include_plotlyjs="directory")
  • write_html(...) default, plotly.js ভেতরে রেখে
  • write_html(..., include_plotlyjs=False, full_html=False)

plotly 7.1-এ px.scatter(o, x="days", y="tk", hover_data={"phone": False}) দিয়ে write_html করলে phone কলাম নিয়ে কোনটি সত্য?

  • tooltip-এ নেই, ফাইলেও নেই
  • tooltip-এ দেখা যায়
  • px একটি error দেয়
  • tooltip-এ নেই, কিন্তু ফাইলের customdata-য় আছে

৫০,০০০ বিন্দুর একটি Plotly scatter ব্রাউজারে খুব ধীর। নিচের কোনটি সাধারণত প্রথম বিবেচনা করা উচিত?

  • প্রশ্নের উত্তর aggregation বা density chart দিয়ে পাওয়া যায় কি না দেখা
  • include_plotlyjs="cdn" দেওয়া
  • write_image দিয়ে SVG বানিয়ে ব্রাউজারে খোলা
  • hover_data থেকে কয়েকটি কলাম বাদ দেওয়া

বাজারঘরের মাসিক বিক্রির chart একটি ছাপা PDF রিপোর্টে যাবে। কোন পদ্ধতি সবচেয়ে উপযুক্ত?

  • interactive HTML-এর লিংক PDF-এ বসানো, যাতে পাঠক hover করতে পারেন
  • write_image দিয়ে স্থির ছবি, শিরোনামে মূল কথা আর পাশে টেবিল
  • animation_frame="month" দিয়ে একটি animated bar chart
  • render_mode="webgl" দিয়ে scatter

Jupyter notebook-এ fig.show() চালালে plotly 7.1 সাধারণত figure-টিকে কীভাবে রাখে?

  • একটি PNG ছবি হিসেবে, Kaleido দিয়ে
  • একটি নতুন ব্রাউজার ট্যাবে, notebook-এ কিছু থাকে না
  • cell-এর আউটপুটে একটি বিশেষ mimetype-এ figure-এর JSON হিসেবে
  • একটি HTML ফাইল হিসেবে notebook-এর পাশের folder-এ

একটি interactive chart-কে screen reader আর keyboard ব্যবহারকারীর জন্য সবচেয়ে কার্যকরভাবে ব্যবহারযোগ্য করে কোনটি?

  • chart-এর সঙ্গে সংখ্যার টেবিল আর এক বাক্যের লেখা সারাংশ
  • hover_data-য় আরও কলাম যোগ করা
  • render_mode="webgl" দিয়ে দ্রুত করা
  • প্রতিটি trace-এ আরও উজ্জ্বল আলাদা রং

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. উত্তর খুঁজতে interactive, উত্তর জানাতে স্থির: অনেক point, hover-এর বিস্তারিত, লম্বা time series আর রোজকার dashboard-এ Plotly; রিপোর্ট, ছাপা আর এক-উত্তরের প্রশ্নে স্থির ছবি আর শিরোনামে মূল কথা।
  2. px আগে, go দরকার হলে: color প্রতিটি মানে আলাদা trace; hover_data, facet_col, labels সব px function-এ একই অর্থে।
  3. Figure = data + layout = JSON; Python লেখে, plotly.js আঁকে। fig.data ছেপে trace আর বিন্দু গুনে যাচাই করুন।
  4. আকার আর গতি: embedded HTML প্রায় ৪.৮ MB, "cdn" কয়েক দশ KB; ১০০০-এর বেশি বিন্দুতে px নিজে scattergl; বড় ডেটায় আগে aggregation.
  5. Hover-এর কলাম ফাইলে যায়: False লুকায়, মোছে না; ব্যক্তিগত কলাম আগেই drop, আর প্রতিটি chart-এর সঙ্গে টেবিল বা লেখা সারাংশ।

পরের পর্বে: chart দিয়ে কীভাবে অজান্তে মিথ্যা বলা হয়ে যায়। কাটা y অক্ষ, বিভ্রান্তিকর রং আর অনুপাত, আর একটি সৎ chart দিয়ে গল্প বলার নিয়ম।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান