یک روز قبل از تعطیلات عید فطر از ما خواسته شد نمودارهایی برای برنامهی تلویزیونی ماه عسل بسازیم. از آنجا که فرصت کم بود تصمیم گرفتیم از ابزاری کاملا آشنا استفاده کنیم: وب. همان HTML، CSS و Javascript دوستداشتنی خودمان. تا آن روز کارفرما فقط تجربهی استفاده از نرمافزارهای تولید شده با فلش را داشت. او را قانع کردیم که راهکار ما از نظر زمانی بهصرفهتر است و کافی است به جای اجرای برنامهی exe، مرورگر را باز کرده و آدرسی را در آن وارد کند. نتیجه کار بهتر از آن چیزی شد که کارفرما انتظار داشت. بخصوص زمانی که رفع اشکالها و بهینهسازیها را از راه دور انجام میدادیم، دیگر نیازی نبود لپتاپ یا حافظهی فلش برای نصب یا بروزرسانی برنامهی جدید بین استودیو و شرکت جابهجا شود.
این تجربه باعث شد از ما بخواهند برنامهی نود را هم به همین ترتیب بازسازی کنیم. فرزان در مورد روش طراحی نمودارها، انتخاب رنگها و کالیبره کردن برنامه برای تلویزیون توضیحات مفصلی داده که پیشنهاد میکنم قبل از ادامهی این مطلب آن را مطالعه کنید.
از آنجایی که تمام نمودارها و عددهایی که نمایش میدادیم از یک مجموعه داده استخراج میشدند تصمیم گرفته بودیم برای درک بهتر بیننده، تمامی نمودارهایمان بههم پیوسته باشند و در واقع یک نمودار به شکلی معنادار به نموداری دیگر تبدیل شود. ابزارهای مختلفی برای نمایش نمودار وجود دارد، همینطور تعداد زیادی موتور و کتابخانهی حرفهای برای ساخت انیمیشن با Javascript توسعه داده شده است. اما نتوانستیم ابزاری پیدا کنیم که هر دو را در کنار هم داشته باشد.
بعد از تحقیقات دو هفتهای به این نتیجه رسیدیم که از ابزاری استفاده کنیم که کمترین دخالت در نحوهی پیادهسازی را داشته باشد و در ضمن با دادههای زنده راحت کار کند. از خیلی وقت پیش با d3.js آشنا بودیم اما مطمئن نبودیم ابزار مناسبی برای این کار باشد. روزی که این صفحه را دیدم متقاعد شدم دست از تحقیق بیشتر برداریم و با استفاده از همان d3.js همه کارها را انجام دهیم.

d3.js کتابخانهی فوقالعادهای برای بازی با دادهها و نمایش آنهاست، غیر از نمودارهای شناخته شده، نمایش دادههای عجیب و غریبی با آن ساختهاند. پیشنهاد میکنم حتما مثالهای جذابش را ببینید.
اما این تنها یکی از مشکلات ما را حل میکرد و مساله ساخت یک سناریوی ترکیبی به قوت خودش باقی بود. برای ساخت سناریویی که طراحی کرده بودیم، لازم بود صحنهها بههم تبدیل شوند. مثلاً به این ترتیب که تمام میلههای یکی از نمودارها به هم نزدیک شده و تبدیل به یک مربع مشکی شود و در آن تعداد شرکتکنندهها نمایش داده شود.
با توجه به رویکردی که طراح d3.js داشته ، برای پیادهسازی transitionهای پشتهم مجبور هستید که از callbackها استفاده کنید و در نتیجه برنامه پر از nested call میشود.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var rect = chart.selectAll("rect") | |
.data(data, function(d) { return d.time; }); | |
rect.enter().insert("rect") | |
.attr("x", function(d, i) { return x(i + ۱) – ۰.۵; }) | |
.attr("y", function(d) { return h – y(d.value) – ۰.۵; }) | |
.attr("width", w) | |
.attr("height", function(d) { return y(d.value); }) | |
.transition() | |
.duration(۱۰۰۰) | |
.attr("x", function(d, i) { return x(i) – ۰.۵; }) | |
.each('end', function(){ | |
// callback code | |
}); |
اگر هر صحنهای ۲۰ transition داشته باشد و در کل ۵ صحنه داشته باشیم، با این روش نزدیک به ۱۰۰ callback داریم و تازه مشکل کنترل حرکت بین صحنهها – که برای برنامه لازم داشتیم – هم غیر قابل انجام میشد. حتی با وسواسیترین روشهای برنامهنویسی باز هم کد بزرگ و غیر قابل استفادهای از آب در میآمد. برای حل این مشکل به مرور با استفاده از امکانات Promise و رعایت قواعد
d3.js یک framework بسیار کوچک ساختیم و به کمک آن نمودارها را به صورت تعاملی یا بر اساس زمان به هم چسباندیم و همهی نمودارها را در قالب یک انیمیشن پیوسته نمایش دادیم.
در توضیح کاملتر این framework، به چند نکته دیگر میپردازم. در ساخت انیمیشن ۹۰ باید هر کدام از صحنهها را بر اساس طرح ماکت بازسازی میکردیم. برای این کار لازم بود جزئیترین انیمیشنها را دهها بار بازبینی کنیم، تغییر دهیم و دوباره اجرا کنیم تا کاملا شبیه مدل نمونه شوند. در نتیجه در framework متدهای کمکی نوشتیم که بتوانیم سرعت اجرا را زیاد کنیم یا در تستها فقط دو صحنهی متوالی را اجرا کنیم.
مسئلهی بعدی پرش از صحنهای به صحنهی دیگر بدون درنظر گرفتن سناریو بود. مثلا لازم بود اپراتور طبق درخواست کارگردان از صحنهی اول به صحنهی آخر بپرد. با این کار دیگر transitionها معنی نداشتند، چون بین صحنهی اول و صحنهی آخر چندین صحنه وجود داشت و نمودارهایشان بههم تبدیل میشدند. این امکان میتوانست خیلی پیچیده پیادهسازی شود، برای مثال صف انیمیشن در زمان اجرا و با انتخاب اپراتور تغییر کند، چند صحنه یا انیمیشن از آن حذف شود یا روشهای دیگر. اما با سادهسازی هر چه بیشتر در framework سناریو، روشی ساختیم که بتوان بهجای یک سناریو چندین سناریو داشته باشیم و موقع load شدن صفحه بر اساس URL یک سناریوی مشخص را دنبال کنیم. سعی میکنم با مثالی ساده این قضیه را بازسازی کنم.
فرض کنید سناریوی اصلی ما نمایش چندین صحنه پشت سر هم باشد.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var main_scenario = [ | |
scene1_start, | |
scene1_heartbeat, | |
scene1_end, | |
scene2_start, | |
scene2_end, | |
scene3_start, | |
scene3_heartbeat, | |
scene3_end | |
]; |
حالا اگر طبق پارامترهای URL بخواهیم فقط صحنهی ۲ به بعد را نمایش دهیم کافی است این آرایه به عنوان سناریو معرفی شود.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var second_scenario = [ | |
scene2_start_final, | |
scene2_end, | |
scene3_start, | |
scene3_heartbeat, | |
scene3_end | |
]; |
هر بار که پارامترهای URL با میانبرهای تعریف شده تغییر کند صفحه از اول load شده و آرایهی سناریو انتخاب میشود. البته سناریوها به مراتب از این مثال پیچیدهتر بودند و برای درست کردن همین چند آرایه از سناریوها نزدیک به ۱۰۰ خط کد نوشتیم.
هر کدام از آیتمهای این آرایهها در خود چندین گام انیمیشنی دارند که در پایان گام بعدی را صدا میزنند. اگر توجه کرده باشید در آرایهی second_scenario اولین صحنهی معرفی شده با scene2_start تفاوت دارد. دلیل این کار این است که در آرایهی اول scene2_start در واقع یک یا چند shape از scene1_end را تغییر میدهد، اما وقتی scene1_end وجود نداشته باشد، باید همان shape ورودی را خودمان از اول درست کنیم.
اگر بخواهید همه چیز را خودتان بسازید و پای ابزاری آماده در میان نباشد، با همین چالشها روبرو میشوید. فرقی هم ندارد که با Javascript بنویسید یا زبانی دیگر. البته لذتی که در ساخت یک framework یا کتابخانه هست در استفاده از ابزارهای آماده نیست. این انتخاب به زمان شما بستگی دارد. فرصت ما کوتاه بود، d3.js را انتخاب کردیم و این framework را کمکم کامل کردیم. با هر روشی امکان پشیمانی وسط راه وجود دارد، اما برای ما این از معدود دفعاتی بود که تا آخر از انتخابمان خوشحال بودیم. به همین دلیل بعد از پایان پروژه، framework را از دل پروژه بیرون کشیدیم و در گیتهاب با اسم d3-scenario و با پروانهی MIT منتشر کردیم. اگر پروژهای شبیه نمودارهای ۹۰ داشتید، پیشنهاد میکنم امتحانش کنید.