A place to cache linked articles (think custom and personal wayback machine)
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

5 лет назад
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang=fr>
  4. <!-- Has to be within the first 1024 bytes, hence before the <title>
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset=utf-8>
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name=viewport content="width=device-width,minimum-scale=1,initial-scale=1,shrink-to-fit=no">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>Pyxley: Python Powered Dashboards (archive) — David Larlet</title>
  13. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  14. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons/apple-touch-icon.png">
  15. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons/favicon-32x32.png">
  16. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons/favicon-16x16.png">
  17. <link rel="manifest" href="/manifest.json">
  18. <link rel="mask-icon" href="/static/david/icons/safari-pinned-tab.svg" color="#5bbad5">
  19. <link rel="shortcut icon" href="/static/david/icons/favicon.ico">
  20. <meta name="apple-mobile-web-app-title" content="David Larlet">
  21. <meta name="application-name" content="David Larlet">
  22. <meta name="msapplication-TileColor" content="#da532c">
  23. <meta name="msapplication-config" content="/static/david/icons/browserconfig.xml">
  24. <meta name="theme-color" content="#f0f0ea">
  25. <!-- That good ol' feed, subscribe :p. -->
  26. <link rel=alternate type="application/atom+xml" title=Feed href="/david/log/">
  27. <meta name="robots" content="noindex, nofollow">
  28. <meta content="origin-when-cross-origin" name="referrer">
  29. <!-- Canonical URL for SEO purposes -->
  30. <link rel="canonical" href="http://multithreaded.stitchfix.com/blog/2015/07/16/pyxley/">
  31. <style>
  32. /* http://meyerweb.com/eric/tools/css/reset/ */
  33. html, body, div, span,
  34. h1, h2, h3, h4, h5, h6, p, blockquote, pre,
  35. a, abbr, address, big, cite, code,
  36. del, dfn, em, img, ins,
  37. small, strike, strong, tt, var,
  38. dl, dt, dd, ol, ul, li,
  39. fieldset, form, label, legend,
  40. table, caption, tbody, tfoot, thead, tr, th, td,
  41. article, aside, canvas, details, embed,
  42. figure, figcaption, footer, header, hgroup,
  43. menu, nav, output, ruby, section, summary,
  44. time, mark, audio, video {
  45. margin: 0;
  46. padding: 0;
  47. border: 0;
  48. font-size: 100%;
  49. font: inherit;
  50. vertical-align: baseline;
  51. }
  52. /* HTML5 display-role reset for older browsers */
  53. article, aside, details, figcaption, figure,
  54. footer, header, hgroup, menu, nav, section { display: block; }
  55. body { line-height: 1; }
  56. blockquote, q { quotes: none; }
  57. blockquote:before, blockquote:after,
  58. q:before, q:after {
  59. content: '';
  60. content: none;
  61. }
  62. table {
  63. border-collapse: collapse;
  64. border-spacing: 0;
  65. }
  66. /* http://practicaltypography.com/equity.html */
  67. /* https://calendar.perfplanet.com/2016/no-font-face-bulletproof-syntax/ */
  68. /* https://www.filamentgroup.com/lab/js-web-fonts.html */
  69. @font-face {
  70. font-family: 'EquityTextB';
  71. src: url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff2') format('woff2'),
  72. url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff') format('woff');
  73. font-weight: 300;
  74. font-style: normal;
  75. font-display: swap;
  76. }
  77. @font-face {
  78. font-family: 'EquityTextB';
  79. src: url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff2') format('woff2'),
  80. url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff') format('woff');
  81. font-weight: 300;
  82. font-style: italic;
  83. font-display: swap;
  84. }
  85. @font-face {
  86. font-family: 'EquityTextB';
  87. src: url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff2') format('woff2'),
  88. url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff') format('woff');
  89. font-weight: 700;
  90. font-style: normal;
  91. font-display: swap;
  92. }
  93. @font-face {
  94. font-family: 'ConcourseT3';
  95. src: url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff2') format('woff2'),
  96. url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff') format('woff');
  97. font-weight: 300;
  98. font-style: normal;
  99. font-display: swap;
  100. }
  101. /* http://practice.typekit.com/lesson/caring-about-opentype-features/ */
  102. body {
  103. /* http://www.cssfontstack.com/ Palatino 99% Win 86% Mac */
  104. font-family: "EquityTextB", Palatino, serif;
  105. background-color: #f0f0ea;
  106. color: #07486c;
  107. font-kerning: normal;
  108. -moz-osx-font-smoothing: grayscale;
  109. -webkit-font-smoothing: subpixel-antialiased;
  110. text-rendering: optimizeLegibility;
  111. font-variant-ligatures: common-ligatures contextual;
  112. font-feature-settings: "kern", "liga", "clig", "calt";
  113. }
  114. pre, code, kbd, samp, var, tt {
  115. font-family: 'TriplicateT4c', monospace;
  116. }
  117. em {
  118. font-style: italic;
  119. color: #323a45;
  120. }
  121. strong {
  122. font-weight: bold;
  123. color: black;
  124. }
  125. nav {
  126. background-color: #323a45;
  127. color: #f0f0ea;
  128. display: flex;
  129. justify-content: space-around;
  130. padding: 1rem .5rem;
  131. }
  132. nav:last-child {
  133. border-bottom: 1vh solid #2d7474;
  134. }
  135. nav a {
  136. color: #f0f0ea;
  137. }
  138. nav abbr {
  139. border-bottom: 1px dotted white;
  140. }
  141. h1 {
  142. border-top: 1vh solid #2d7474;
  143. border-bottom: .2vh dotted #2d7474;
  144. background-color: #e3e1e1;
  145. color: #323a45;
  146. text-align: center;
  147. padding: 5rem 0 4rem 0;
  148. width: 100%;
  149. font-family: 'ConcourseT3';
  150. display: flex;
  151. flex-direction: column;
  152. }
  153. h1.single {
  154. padding-bottom: 10rem;
  155. }
  156. h1 span {
  157. position: absolute;
  158. top: 1vh;
  159. left: 20%;
  160. line-height: 0;
  161. }
  162. h1 span a {
  163. line-height: 1.7;
  164. padding: 1rem 1.2rem .6rem 1.2rem;
  165. border-radius: 0 0 6% 6%;
  166. background: #2d7474;
  167. font-size: 1.3rem;
  168. color: white;
  169. text-decoration: none;
  170. }
  171. h2 {
  172. margin: 4rem 0 1rem;
  173. border-top: .2vh solid #2d7474;
  174. padding-top: 1vh;
  175. }
  176. h3 {
  177. text-align: center;
  178. margin: 3rem 0 .75em;
  179. }
  180. hr {
  181. height: .4rem;
  182. width: .4rem;
  183. border-radius: .4rem;
  184. background: #07486c;
  185. margin: 2.5rem auto;
  186. }
  187. time {
  188. display: bloc;
  189. margin-left: 0 !important;
  190. }
  191. ul, ol {
  192. margin: 2rem;
  193. }
  194. ul {
  195. list-style-type: square;
  196. }
  197. a {
  198. text-decoration-skip-ink: auto;
  199. text-decoration-thickness: 0.05em;
  200. text-underline-offset: 0.09em;
  201. }
  202. article {
  203. max-width: 50rem;
  204. display: flex;
  205. flex-direction: column;
  206. margin: 2rem auto;
  207. }
  208. article.single {
  209. border-top: .2vh dotted #2d7474;
  210. margin: -6rem auto 1rem auto;
  211. background: #f0f0ea;
  212. padding: 2rem;
  213. }
  214. article p:last-child {
  215. margin-bottom: 1rem;
  216. }
  217. p {
  218. padding: 0 .5rem;
  219. margin-left: 3rem;
  220. }
  221. p + p,
  222. figure + p {
  223. margin-top: 2rem;
  224. }
  225. blockquote {
  226. background-color: #e3e1e1;
  227. border-left: .5vw solid #2d7474;
  228. display: flex;
  229. flex-direction: column;
  230. align-items: center;
  231. padding: 1rem;
  232. margin: 1.5rem;
  233. }
  234. blockquote cite {
  235. font-style: italic;
  236. }
  237. blockquote p {
  238. margin-left: 0;
  239. }
  240. figure {
  241. border-top: .2vh solid #2d7474;
  242. background-color: #e3e1e1;
  243. text-align: center;
  244. padding: 1.5rem 0;
  245. margin: 1rem 0 0;
  246. font-size: 1.5rem;
  247. width: 100%;
  248. }
  249. figure img {
  250. max-width: 250px;
  251. max-height: 250px;
  252. border: .5vw solid #323a45;
  253. padding: 1px;
  254. }
  255. figcaption {
  256. padding: 1rem;
  257. line-height: 1.4;
  258. }
  259. aside {
  260. display: flex;
  261. flex-direction: column;
  262. background-color: #e3e1e1;
  263. padding: 1rem 0;
  264. border-bottom: .2vh solid #07486c;
  265. }
  266. aside p {
  267. max-width: 50rem;
  268. margin: 0 auto;
  269. }
  270. /* https://fvsch.com/code/css-locks/ */
  271. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  272. font-size: 1rem;
  273. line-height: calc( 1.5em + 0.2 * 1rem );
  274. }
  275. h1 {
  276. font-size: 1.9rem;
  277. line-height: calc( 1.2em + 0.2 * 1rem );
  278. }
  279. h2 {
  280. font-size: 1.6rem;
  281. line-height: calc( 1.3em + 0.2 * 1rem );
  282. }
  283. h3 {
  284. font-size: 1.35rem;
  285. line-height: calc( 1.4em + 0.2 * 1rem );
  286. }
  287. @media (min-width: 20em) {
  288. /* The (100vw - 20rem) / (50 - 20) part
  289. resolves to 0-1rem, depending on the
  290. viewport width (between 20em and 50em). */
  291. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  292. font-size: calc( 1rem + .6 * (100vw - 20rem) / (50 - 20) );
  293. line-height: calc( 1.5em + 0.2 * (100vw - 50rem) / (20 - 50) );
  294. margin-left: 0;
  295. }
  296. h1 {
  297. font-size: calc( 1.9rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  298. line-height: calc( 1.2em + 0.2 * (100vw - 50rem) / (20 - 50) );
  299. }
  300. h2 {
  301. font-size: calc( 1.5rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  302. line-height: calc( 1.3em + 0.2 * (100vw - 50rem) / (20 - 50) );
  303. }
  304. h3 {
  305. font-size: calc( 1.35rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  306. line-height: calc( 1.4em + 0.2 * (100vw - 50rem) / (20 - 50) );
  307. }
  308. }
  309. @media (min-width: 50em) {
  310. /* The right part of the addition *must* be a
  311. rem value. In this example we *could* change
  312. the whole declaration to font-size:2.5rem,
  313. but if our baseline value was not expressed
  314. in rem we would have to use calc. */
  315. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  316. font-size: calc( 1rem + .6 * 1rem );
  317. line-height: 1.5em;
  318. }
  319. p, li, pre, details {
  320. margin-left: 3rem;
  321. }
  322. h1 {
  323. font-size: calc( 1.9rem + 1.5 * 1rem );
  324. line-height: 1.2em;
  325. }
  326. h2 {
  327. font-size: calc( 1.5rem + 1.5 * 1rem );
  328. line-height: 1.3em;
  329. }
  330. h3 {
  331. font-size: calc( 1.35rem + 1.5 * 1rem );
  332. line-height: 1.4em;
  333. }
  334. figure img {
  335. max-width: 500px;
  336. max-height: 500px;
  337. }
  338. }
  339. figure.unsquared {
  340. margin-bottom: 1.5rem;
  341. }
  342. figure.unsquared img {
  343. height: inherit;
  344. }
  345. @media print {
  346. body { font-size: 100%; }
  347. a:after { content: " (" attr(href) ")"; }
  348. a, a:link, a:visited, a:after {
  349. text-decoration: underline;
  350. text-shadow: none !important;
  351. background-image: none !important;
  352. background: white;
  353. color: black;
  354. }
  355. abbr[title] { border-bottom: 0; }
  356. abbr[title]:after { content: " (" attr(title) ")"; }
  357. img { page-break-inside: avoid; }
  358. @page { margin: 2cm .5cm; }
  359. h1, h2, h3 { page-break-after: avoid; }
  360. p3 { orphans: 3; widows: 3; }
  361. img {
  362. max-width: 250px !important;
  363. max-height: 250px !important;
  364. }
  365. nav, aside { display: none; }
  366. }
  367. ul.with_columns {
  368. column-count: 1;
  369. }
  370. @media (min-width: 20em) {
  371. ul.with_columns {
  372. column-count: 2;
  373. }
  374. }
  375. @media (min-width: 50em) {
  376. ul.with_columns {
  377. column-count: 3;
  378. }
  379. }
  380. ul.with_two_columns {
  381. column-count: 1;
  382. }
  383. @media (min-width: 20em) {
  384. ul.with_two_columns {
  385. column-count: 1;
  386. }
  387. }
  388. @media (min-width: 50em) {
  389. ul.with_two_columns {
  390. column-count: 2;
  391. }
  392. }
  393. .gallery {
  394. display: flex;
  395. flex-wrap: wrap;
  396. justify-content: space-around;
  397. }
  398. .gallery figure img {
  399. margin-left: 1rem;
  400. margin-right: 1rem;
  401. }
  402. .gallery figure figcaption {
  403. font-family: 'ConcourseT3'
  404. }
  405. footer {
  406. font-family: 'ConcourseT3';
  407. display: flex;
  408. flex-direction: column;
  409. border-top: 3px solid white;
  410. padding: 4rem 0;
  411. background-color: #07486c;
  412. color: white;
  413. }
  414. footer > * {
  415. max-width: 50rem;
  416. margin: 0 auto;
  417. }
  418. footer a {
  419. color: #f1c40f;
  420. }
  421. footer .avatar {
  422. width: 200px;
  423. height: 200px;
  424. border-radius: 50%;
  425. float: left;
  426. -webkit-shape-outside: circle();
  427. shape-outside: circle();
  428. margin-right: 2rem;
  429. padding: 2px 5px 5px 2px;
  430. background: white;
  431. border-left: 1px solid #f1c40f;
  432. border-top: 1px solid #f1c40f;
  433. border-right: 5px solid #f1c40f;
  434. border-bottom: 5px solid #f1c40f;
  435. }
  436. </style>
  437. <h1>
  438. <span><a id="jumper" href="#jumpto" title="Un peu perdu ?">?</a></span>
  439. Pyxley: Python Powered Dashboards (archive)
  440. <time>Pour la pérennité des contenus liés. Non-indexé, retrait sur simple email.</time>
  441. </h1>
  442. <section>
  443. <article>
  444. <h3><a href="http://multithreaded.stitchfix.com/blog/2015/07/16/pyxley/">Source originale du contenu</a></h3>
  445. <p>Web-based dashboards are the most straightforward way to share insights with clients and business partners. For R users, Shiny provides a framework that allows data scientists to create interactive web applications without having to write Javascript, HTML, or CSS. Despite Shiny’s utility and success as a dashboard framework, there is no equivalent in Python. There are packages in development, such as Spyre, but nothing that matches Shiny’s level of customization. We have written a Python package, called <a href="http://github.com/stitchfix/pyxley">Pyxley</a>, to not only help simplify the development of web-applications, but to provide a way to easily incorporate custom Javascript for maximum flexibility. This is enabled through Flask, PyReact, and Pandas.</p>
  446. <h2>Pyxley: React-powered Flask for interactive visualizations and reports</h2>
  447. <p>Most web frameworks follow the model-view-controller (MVC) pattern and lots of people use React.js as the V in MVC. For the M and C, Flask provides a great minimalistic framework for web applications that allows us to get a simple application running with only a little bit of code. Together, Flask and React.js provide a fast and flexible framework with a low barrier to entry. The focus of Pyxley is to provide a set of tools for quickly developing React-powered Flask applications.</p>
  448. <p>The Github repo has a few <a href="https://github.com/stitchfix/pyxley/tree/master/examples">examples</a>, but we are going to walk through the creation of a simple line chart. To get Pyxley running, you need to get a Flask setup going (see github link for details). For this blog, we will focus on the Pyxley components that are needed for a basic dashboard.</p>
  449. <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">from</span> <span class="nn">pyxley</span> <span class="kn">import</span> <span class="n">UILayout</span>
  450. <span class="kn">from</span> <span class="nn">pyxley.filters</span> <span class="kn">import</span> <span class="n">SelectButton</span>
  451. <span class="kn">from</span> <span class="nn">pyxley.charts.mg</span> <span class="kn">import</span> <span class="n">LineChart</span><span class="p">,</span> <span class="n">Figure</span>
  452. <span class="kn">from</span> <span class="nn">pyxley.utils</span> <span class="kn">import</span> <span class="n">FilterFrame</span>
  453. </code></pre></div>
  454. <h3>UI</h3>
  455. <p>Pyxley takes a modular approach where the developer specifies a list of filters and charts. We’ve created a class called <code>UILayout</code> that ties together all of the components for the user interface. This class is a wrapper for a parent React component composed of filters and charts. As such, we will need to provide the name of the parent component (e.g. <code>FilterChart</code>), the location of the source Javascript, and an HTML <code>div</code> name. The specifics of <code>FilterChart</code> can be found in the
  456. <a href="https://github.com/stitchfix/pyxleyJS/blob/master/src/layouts.js">pyxleyJS</a> repository.</p>
  457. <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">ui</span> <span class="o">=</span> <span class="n">UILayout</span><span class="p">(</span>
  458. <span class="s">"FilterChart"</span><span class="p">,</span>
  459. <span class="s">"./static/bower_components/pyxley/build/pyxley.js"</span><span class="p">,</span>
  460. <span class="s">"component_id"</span><span class="p">)</span>
  461. </code></pre></div>
  462. <h3>Filters</h3>
  463. <p>Pyxley is built upon the idea that you have some dataframe in memory that you want to filter with some user selected options. For this example, we have some activity data taken from my Fitbit. Assume for the sake of simplicity that the dataframe has been formatted so that it has the following columns: <code>Date</code>, <code>Metric</code>, and <code>Value</code>. The <code>Metric</code> column is a string indicating the type of data to display (e.g. <code>Steps</code>, <code>Calories Burned</code>), while <code>Date</code> and <code>Value</code> provide the actual time-series. This will allow us to select the desired <code>Metric</code> to plot.</p>
  464. <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="c"># items contains the possible selections</span>
  465. <span class="c"># Args: Label, Items, ColumnName, Default</span>
  466. <span class="n">btn</span> <span class="o">=</span> <span class="n">SelectButton</span><span class="p">(</span><span class="s">"Select Metric"</span><span class="p">,</span> <span class="n">items</span><span class="p">,</span> <span class="s">"Metric"</span><span class="p">,</span> <span class="s">"Steps"</span><span class="p">)</span>
  467. <span class="n">ui</span><span class="o">.</span><span class="n">add_filter</span><span class="p">(</span><span class="n">btn</span><span class="p">)</span>
  468. </code></pre></div>
  469. <p><img src="http://multithreaded.stitchfix.com/assets/images/blog/pyxley_dropdown.png" alt="Dropdown"/></p>
  470. <p>We now have a button, labeled <code>Select Metric</code>, containing a drop-down list of items that we will use to filter the dataframe.</p>
  471. <h3>Charts</h3>
  472. <p>MetricsGraphics.js is an awesome library for making beautiful D3 charts. We've written a few wrappers to make it easier to integrate in our app. For those of you familiar with matplotlib, we are going to write code that is somewhat similar to the mpl API. We start with a <code>Figure</code> object and supply two things: a unique path that Flask will use for request routing and a unique element id that will be used by the React component. The figure is customizable, so let’s code up the desired aesthetics via Pyxley’s API.</p>
  473. <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">fig</span> <span class="o">=</span> <span class="n">Figure</span><span class="p">(</span><span class="s">"/mgchart/"</span><span class="p">,</span> <span class="s">"mychartid"</span><span class="p">)</span>
  474. <span class="n">fig</span><span class="o">.</span><span class="n">graphics</span><span class="o">.</span><span class="n">transition_on_update</span><span class="p">(</span><span class="bp">True</span><span class="p">)</span>
  475. <span class="n">fig</span><span class="o">.</span><span class="n">graphics</span><span class="o">.</span><span class="n">animate_on_load</span><span class="p">()</span>
  476. <span class="n">fig</span><span class="o">.</span><span class="n">layout</span><span class="o">.</span><span class="n">set_size</span><span class="p">(</span><span class="n">width</span><span class="o">=</span><span class="mi">450</span><span class="p">,</span> <span class="n">height</span><span class="o">=</span><span class="mi">200</span><span class="p">)</span>
  477. <span class="n">fig</span><span class="o">.</span><span class="n">layout</span><span class="o">.</span><span class="n">set_margin</span><span class="p">(</span><span class="n">left</span><span class="o">=</span><span class="mi">40</span><span class="p">,</span> <span class="n">right</span><span class="o">=</span><span class="mi">40</span><span class="p">)</span>
  478. </code></pre></div>
  479. <p>All that remains is to pass our <code>Figure</code> object to the <code>LineChart</code> class and add it to the UI. In addition, we’ll supply the column name of the x-axis, a list of columns for the y-axis, some initial filter parameters, and provide an indicator that we are plotting a time-series.</p>
  480. <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">lc</span> <span class="o">=</span> <span class="n">LineChart</span><span class="p">(</span><span class="n">df</span><span class="p">,</span> <span class="n">fig</span><span class="p">,</span> <span class="s">"Date"</span><span class="p">,</span> <span class="p">[</span><span class="s">"Value"</span><span class="p">],</span> <span class="n">init_params</span><span class="o">=</span><span class="p">{</span><span class="s">"Metric"</span><span class="p">:</span> <span class="s">"Steps"</span><span class="p">},</span> <span class="n">timeseries</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
  481. <span class="n">ui</span><span class="o">.</span><span class="n">add_chart</span><span class="p">(</span><span class="n">lc</span><span class="p">)</span>
  482. </code></pre></div>
  483. <p><br/></p>
  484. <p><img src="http://multithreaded.stitchfix.com/assets/images/blog/pyxley_chart.png" alt="Chart"/></p>
  485. <h3>Transform to Javascript via PyReact</h3>
  486. <p>The final step is to compile the filters and charts into a single React jsx file, register it with the Flask app, and transform the jsx to Javascript using PyReact.</p>
  487. <div class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">ui</span><span class="o">.</span><span class="n">render_layout</span><span class="p">(</span><span class="n">app</span><span class="p">,</span> <span class="s">"./static/layout.js"</span><span class="p">)</span>
  488. </code></pre></div>
  489. <p>Including only the components mentioned above, we should get something that looks like:</p>
  490. <p><img src="http://multithreaded.stitchfix.com/assets/images/blog/pyxley_final.png" alt=""/></p>
  491. <h1>How Does it Work?</h1>
  492. <h2>Web Applications in Python</h2>
  493. <p>A very simple web application in Python might contain the following:</p>
  494. <ol>
  495. <li>Data source (e.g. SQLite database)</li>
  496. <li>HTML Template</li>
  497. <li>Request dispatching</li>
  498. <li>Javascript visualizations</li>
  499. </ol>
  500. <p>Flask provides a great framework for handling the first three components. It’s very easy to set up an application to serve some static content such as a table or a plot. However, any interactive components are left to the developer to implement in Javascript. Pyxley helps bridge the gap and provides a set of Flask helper functions and wrappers for some basic Javascript widgets and charts.</p>
  501. <h3>React to the Rescue!</h3>
  502. <p>React.js is the perfect front-end complement to Flask. In addition to providing a succinct way of composing the UI elements of an application, React provides a simple structure through which we will integrate with Python. We’ve provided some basic React components in <a href="https://github.com/stitchfix/pyxleyJS">PyxleyJS</a> that will handle most of the interaction between UI elements. The specifics of each component are passed through as React props. Integration with Python only requires a single template which is compiled into Javascript using PyReact.</p>
  503. <h3>PyxleyJS</h3>
  504. <p>We've simplified the process by making the assumption that most dashboards are a combination of filters and charts. Several useful components have been included in PyxleyJS. These components follow a very simple pattern: the user supplies props that determine which component is created. Let's look at the <code>Chart</code> interface.</p>
  505. <div class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kr">const</span> <span class="nx">Chart</span> <span class="o">=</span> <span class="nx">React</span><span class="p">.</span><span class="nx">createClass</span><span class="p">({</span>
  506. <span class="nx">getDefaultProps</span><span class="o">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
  507. <span class="k">return</span> <span class="p">{</span>
  508. <span class="nx">type</span><span class="o">:</span> <span class="nx">React</span><span class="p">.</span><span class="nx">PropTypes</span><span class="p">.</span><span class="nx">string</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
  509. <span class="nx">options</span><span class="o">:</span> <span class="nx">React</span><span class="p">.</span><span class="nx">PropTypes</span><span class="p">.</span><span class="nx">object</span>
  510. <span class="p">};</span>
  511. <span class="p">},</span>
  512. <span class="nx">update</span><span class="o">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">params</span><span class="p">)</span> <span class="p">{</span>
  513. <span class="k">this</span><span class="p">.</span><span class="nx">refs</span><span class="p">.</span><span class="nx">chart</span><span class="p">.</span><span class="nx">_update</span><span class="p">(</span><span class="nx">params</span><span class="p">);</span>
  514. <span class="p">},</span>
  515. <span class="nx">render</span><span class="o">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
  516. <span class="kd">var</span> <span class="nx">Z</span> <span class="o">=</span> <span class="nx">ChartFactory</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">type</span><span class="p">);</span>
  517. <span class="k">return</span> <span class="p">(</span>
  518. <span class="o">&lt;</span><span class="nx">Z</span> <span class="nx">ref</span><span class="o">=</span><span class="p">{</span><span class="s2">"chart"</span><span class="p">}</span> <span class="nx">options</span><span class="o">=</span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">options</span><span class="p">}</span> <span class="o">/&gt;</span>
  519. <span class="p">);</span>
  520. <span class="p">}</span>
  521. <span class="p">});</span>
  522. </code></pre></div>
  523. <p><code>Chart</code> has two props: <code>type</code> and <code>options</code>. These props are used to determine the specific component we wish to render. In Python, we only need to specify the type and options, reducing the need to write obscure Javascript templates for every charting package. Instead, we simply need to write a single React component that initializes when the component mounts and updates when we call the update method. Following the same pattern we can create the entire UI with just a single React component composed of several charts and filters.</p>
  524. <h3>Flask Integration</h3>
  525. <p>Through PyReact and Jinja2 templating, we now have a way to integrate our React components with Python. The remaining pieces to constructing our web applications are:</p>
  526. <ol>
  527. <li>Setting up RESTful APIs to our data</li>
  528. <li>Providing interfaces to the React components</li>
  529. <li>Collecting and compiling the components</li>
  530. </ol>
  531. <p>We wanted to accomplish the above while hiding as much complexity as possible from the user.
  532. Prioritizing simplicity led to the creation of the following two classes.</p>
  533. <ol>
  534. <li><code>UIComponent</code>
  535. a. Stores component type, options, and request routing functions
  536. b. Chart and Filter are subclasses</li>
  537. <li><code>UILayout</code>
  538. a. Stores charts, filters, and registers routes with the Flask app</li>
  539. </ol>
  540. <p>These classes only require the user to specify a unique identifier for the <code>div</code> component and a unique route for the request. The request route functions are determined by the type of chart or filter being created.</p>
  541. <h2>Pyxley and the Future</h2>
  542. <p><img src="http://multithreaded.stitchfix.com/assets/images/blog/pyxley_tweet.png" alt=""/></p>
  543. <p><center>@wesmkinn we agree :)</center></p>
  544. <p>While this initial release has only a few components, it introduces a very simple pattern that is easy to extend with minimal effort. More components will be added over time to both Pyxley and PyxleyJS and we welcome any contributions from the community and hope that this package makes developing dashboards in Python both simple and enjoyable.</p>
  545. </article>
  546. </section>
  547. <nav id="jumpto">
  548. <p>
  549. <a href="/david/blog/">Accueil du blog</a> |
  550. <a href="http://multithreaded.stitchfix.com/blog/2015/07/16/pyxley/">Source originale</a> |
  551. <a href="/david/stream/2019/">Accueil du flux</a>
  552. </p>
  553. </nav>
  554. <footer>
  555. <div>
  556. <img src="/static/david/david-larlet-avatar.jpg" loading="lazy" class="avatar" width="200" height="200">
  557. <p>
  558. Bonjour/Hi!
  559. Je suis <a href="/david/" title="Profil public">David&nbsp;Larlet</a>, je vis actuellement à Montréal et j’alimente cet espace depuis 15 ans. <br>
  560. Si tu as apprécié cette lecture, n’hésite pas à poursuivre ton exploration. Par exemple via les <a href="/david/blog/" title="Expériences bienveillantes">réflexions bimestrielles</a>, la <a href="/david/stream/2019/" title="Pensées (dés)articulées">veille hebdomadaire</a> ou en t’abonnant au <a href="/david/log/" title="S’abonner aux publications via RSS">flux RSS</a> (<a href="/david/blog/2019/flux-rss/" title="Tiens c’est quoi un flux RSS ?">so 2005</a>).
  561. </p>
  562. <p>
  563. Je m’intéresse à la place que je peux avoir dans ce monde. En tant qu’humain, en tant que membre d’une famille et en tant qu’associé d’une coopérative. De temps en temps, je fais aussi des <a href="https://github.com/davidbgk" title="Principalement sur Github mais aussi ailleurs">trucs techniques</a>. Et encore plus rarement, <a href="/david/talks/" title="En ce moment je laisse plutôt la place aux autres">j’en parle</a>.
  564. </p>
  565. <p>
  566. Voici quelques articles choisis :
  567. <a href="/david/blog/2019/faire-equipe/" title="Accéder à l’article complet">Faire équipe</a>,
  568. <a href="/david/blog/2018/bivouac-automnal/" title="Accéder à l’article complet">Bivouac automnal</a>,
  569. <a href="/david/blog/2018/commodite-effondrement/" title="Accéder à l’article complet">Commodité et effondrement</a>,
  570. <a href="/david/blog/2017/donnees-communs/" title="Accéder à l’article complet">Des données aux communs</a>,
  571. <a href="/david/blog/2016/accompagner-enfant/" title="Accéder à l’article complet">Accompagner un enfant</a>,
  572. <a href="/david/blog/2016/senior-developer/" title="Accéder à l’article complet">Senior developer</a>,
  573. <a href="/david/blog/2016/illusion-sociale/" title="Accéder à l’article complet">L’illusion sociale</a>,
  574. <a href="/david/blog/2016/instantane-scopyleft/" title="Accéder à l’article complet">Instantané Scopyleft</a>,
  575. <a href="/david/blog/2016/enseigner-web/" title="Accéder à l’article complet">Enseigner le Web</a>,
  576. <a href="/david/blog/2016/simplicite-defaut/" title="Accéder à l’article complet">Simplicité par défaut</a>,
  577. <a href="/david/blog/2016/minimalisme-esthetique/" title="Accéder à l’article complet">Minimalisme et esthétique</a>,
  578. <a href="/david/blog/2014/un-web-omni-present/" title="Accéder à l’article complet">Un web omni-présent</a>,
  579. <a href="/david/blog/2014/manifeste-developpeur/" title="Accéder à l’article complet">Manifeste de développeur</a>,
  580. <a href="/david/blog/2013/confort-convivialite/" title="Accéder à l’article complet">Confort et convivialité</a>,
  581. <a href="/david/blog/2013/testament-numerique/" title="Accéder à l’article complet">Testament numérique</a>,
  582. et <a href="/david/blog/" title="Accéder aux archives">bien d’autres…</a>
  583. </p>
  584. <p>
  585. On peut <a href="mailto:david%40larlet.fr" title="Envoyer un courriel">échanger par courriel</a>. Si éventuellement tu souhaites que l’on travaille ensemble, tu devrais commencer par consulter le <a href="http://larlet.com">profil dédié à mon activité professionnelle</a> et/ou contacter directement <a href="http://scopyleft.fr/">scopyleft</a>, la <abbr title="Société coopérative et participative">SCOP</abbr> dont je fais partie depuis six ans. Je recommande au préalable de lire <a href="/david/blog/2018/cout-site/" title="Attention ce qui va suivre peut vous choquer">combien coûte un site</a> et pourquoi je suis plutôt favorable à une <a href="/david/pro/devis/" title="Discutons-en !">non-demande de devis</a>.
  586. </p>
  587. <p>
  588. Je ne traque pas ta navigation mais mon
  589. <abbr title="Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33.184162340">hébergeur</abbr>
  590. conserve des logs d’accès.
  591. </p>
  592. </div>
  593. </footer>
  594. <script type="text/javascript">
  595. ;(_ => {
  596. const jumper = document.getElementById('jumper')
  597. jumper.addEventListener('click', e => {
  598. e.preventDefault()
  599. const anchor = e.target.getAttribute('href')
  600. const targetEl = document.getElementById(anchor.substring(1))
  601. targetEl.scrollIntoView({behavior: 'smooth'})
  602. })
  603. })()
  604. </script>