03.md 50.4 KB
Newer Older
W
wizardforcel 已提交
1
# 三、PixieApp 深入了解
W
wizardforcel 已提交
2 3 4 5 6 7 8

> “每个视觉都是开玩笑,直到第一个人完成它;一旦意识到,它就变得司空见惯。”

*Robert H Goddard*

在本章中,我们将对 PixieApp 框架进行技术上的深入研究。 您既可以将以下信息用作*入门*教程,又可以用作 PixieApp 编程模型的参考文档。

W
wizardforcel 已提交
9
在深入探讨 PixieApp 的基本概念(例如路由和请求)之前,我们将首先对它进行剖析。 为了帮助跟进,我们将逐步构建一个`Github Tracking`示例应用,该示例应用将在引入功能和最佳实践时应用这些功能和最佳实践,从构建数据分析到将其集成到 PixieApp 中。
W
wizardforcel 已提交
10 11 12 13 14 15 16 17 18

在本章的最后,您应该能够将学习到的经验教训应用到您自己的用例中,包括编写自己的 PixieApp。

# PixieApp 的剖析

### 注意

**注**:PixieApp 编程模型不需要任何 JavaScript 经验,但是,希望读者熟悉以下内容:

W
wizardforcel 已提交
19 20 21
*   [Python](https://www.python.org)
*   [HTML5](https://www.w3schools.com/html)
*   [CSS3](https://www.w3schools.com/css)
W
wizardforcel 已提交
22

W
wizardforcel 已提交
23
术语 **PixieApp** 代表 **Pixie 应用**,并且旨在强调其与 PixieDust 功能(尤其是`display()` API)的紧密集成。 其主要目标是使开发人员易于构建可以调用 Jupyter 笔记本中实现的数据分析的用户界面。
W
wizardforcel 已提交
24

W
wizardforcel 已提交
25
PixieApp 遵循[**单页应用**(**SPA**)设计模式](https://en.wikipedia.org/wiki/Single-page_application),它向用户显示并带有欢迎屏幕,该屏幕会动态更新以响应用户交互。 更新可以是部分刷新,例如在用户单击控件后更新图形,也可以是完全刷新,例如在多步过程中更新新屏幕。 在每种情况下,更新都是通过使用特定机制触发的路由在服务器端进行控制的,我们将在后面讨论。 触发后,路由将执行代码以处理请求,然后发出 HTML 片段,该片段将在客户端应用于正确的目标 [DOM 元素](https://www.w3schools.com/js/js_htmldom.asp)
W
wizardforcel 已提交
26 27 28 29 30 31 32

以下序列图显示了运行 PixieApp 时客户端和服务器端如何交互:

![Anatomy of a PixieApp](img/00052.jpeg)

序列图显示了 PixieApp 的信息流

W
wizardforcel 已提交
33
启动 PixieApp 时(通过调用`run`方法),将调用默认路由,并返回相应的 HTML 片段。 当用户与应用交互时,将执行更多请求,从而触发关联的路由,从而相应刷新 UI。
W
wizardforcel 已提交
34

W
wizardforcel 已提交
35
从实现的角度来看,PixieApp 只是一个普通的 Python 类,已经用`@PixieApp`装饰器装饰了。 在封面下,` PixieApp`装饰器检测该类以添加运行应用所需的方法和字段,例如`run`方法。
W
wizardforcel 已提交
36 37 38

### 注意

W
wizardforcel 已提交
39
[有关 Python 装饰器的更多信息,请参见](https://wiki.python.org/moin/PythonDecorators)
W
wizardforcel 已提交
40

W
wizardforcel 已提交
41
要开始启动,下面的代码显示了一个简单的`Hello World` PixieApp:
W
wizardforcel 已提交
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

```py
#import the pixieapp decorators
from pixiedust.display.app import *

@PixieApp   #decorator for making the class a PixieApp
class HelloWorldApp():
    @route()  #decorator for making a method a route (no arguments means default route)
    def main_screen(self):
        return """<div>Hello World</div>"""

#Instantiate the application and run it
app = HelloWorldApp()
app.run()
```

### 注意

W
wizardforcel 已提交
60
[您可以在此处找到代码](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode1.py)
W
wizardforcel 已提交
61

W
wizardforcel 已提交
62
上面的代码显示了 PixieApp 的结构,如何定义路由以及如何实例化和运行该应用。 由于 PixieApps 是常规的 Python 类,因此它们可以从其他类(包括其他 PixieApps)继承而来,这对于大型项目来说很方便,使代码模块化和可重用。
W
wizardforcel 已提交
63

W
wizardforcel 已提交
64
## 路由
W
wizardforcel 已提交
65 66 67

路由用于动态更新全部或部分客户端屏幕。 可以根据以下规则在任何类方法上使用`@route`装饰器轻松定义它们:

W
wizardforcel 已提交
68
*   需要一个路由方法来返回一个字符串,该字符串表示更新的 HTML 片段。
W
wizardforcel 已提交
69 70 71

    ### 注意

W
wizardforcel 已提交
72
    **注意**:允许在片段中使用 CSS 和 JavaScript。
W
wizardforcel 已提交
73

W
wizardforcel 已提交
74
*   `@route`装饰器可以具有一个或多个关键字参数,这些参数必须为字符串类型。 可以将这些关键字参数视为请求参数,PixieApp 框架在内部使用这些参数根据以下规则将的请求分发到最匹配的路由:
W
wizardforcel 已提交
75 76 77 78 79 80 81 82 83 84 85 86 87 88

    *   带有最多参数的路由始终首先​​被评估。
    *   所有参数都必须匹配才能选择路由。
    *   如果未找到路由,则默认路由被选为后备路由。
    *   可以使用通配符(即`*`)配置路由,在这种情况下,状态参数的任何值都将是匹配项。

        以下是一个示例:

        ```py
               @route(state1="value1", state2="value2")
        ```

*   PixieApp 必须具有一个且只有一个默认路由,即没有参数的路由,即`@route()`

W
wizardforcel 已提交
89
以不引起冲突的方式配置路由非常重要,尤其是在您的应用具有分层状态的情况下。 例如,与`state1="load"`关联的路由可能负责加载数据,然后与`(state1="load", state2="graph")`关联的第二路由可能负责绘制数据。 在这种情况下,同时指定了`state1``state2`的请求将匹配第二条路由,因为路由评估是从最具体到最不具体的,并在第一个匹配的路由处停止。
W
wizardforcel 已提交
90 91 92 93 94

为了明确起见,下图显示了如何将请求与路由匹配:

![Routes](img/00053.jpeg)

W
wizardforcel 已提交
95
将请求与路由匹配
W
wizardforcel 已提交
96

W
wizardforcel 已提交
97
定义为路由的方法的预期约定是返回 HTML 片段,其中可以包含 Jinja2 模板构造。 Jinja2 是功能强大的 Python 模板引擎,提供了丰富的功能来动态生成文本,包括对 Python 变量,方法和控制结构的访问,例如`if...else``the for`循环等。 涵盖的所有功能将超出本书的范围,但是让我们讨论一些经常使用的重要结构:
W
wizardforcel 已提交
98 99 100

### 注意

W
wizardforcel 已提交
101
[**注意**:如果您想了解有关 Jinja2 的更多信息,可以在这里阅读完整的文档](http://jinja.pocoo.org/docs/templates)
W
wizardforcel 已提交
102 103

*   **变量**:您可以使用双花括号来访问范围内的变量,例如`"<div>This is my variable {{my_var}}</div>"`。 在渲染期间,`my_var`变量将替换为其实际值。 您还可以使用`.`(点)表示法访问复杂对象,例如`"<div>This is a nested value {{my_var.sub_value}}</div>"`
W
wizardforcel 已提交
104
*   **`for`循环**:您可以使用`{%for ...%}...{%endfor%}`表示法通过迭代一系列项目(列表,元组,字典等)来动态生成文本,例如:
W
wizardforcel 已提交
105 106 107 108 109 110 111

    ```py
    {%for message in messages%}
    <li>{{message}}</li>
    {%endfor%}
    ```

W
wizardforcel 已提交
112
*   **`if`语句**:您可以使用`{%if ...%}...{%elif ...%}...{%else%}…{%endif%}`表示法有条件地输出文本,例如:
W
wizardforcel 已提交
113 114 115 116 117 118 119 120 121 122 123 124 125

    ```py
    {%if status.error%}
    <div class="error">{{status.error}}</div>
    {%elif status.warning%}
    <div class="warning">{{status.warning}}</div>
    {%else%}
    <div class="ok">{{status.message}}</div>
    {%endif%}
    ```

了解变量和方法如何进入路由返回的 JinJa2 模板字符串的范围也很重要。 PixieApp 自动提供对三种类型的变量和方法的访问:

W
wizardforcel 已提交
126
*   **类变量和方法**:可以使用`this`关键字进行访问。
W
wizardforcel 已提交
127 128 129

    ### 注意

W
wizardforcel 已提交
130
    **注意**:我们之所以不使用更具 Pythonic 风格的`self`关键字,是因为 Jinja2 本身已经采用了该关键字。
W
wizardforcel 已提交
131

W
wizardforcel 已提交
132
*   **方法参数**:当路由参数使用`*`值并且您希望在运行时可以访问该值时,此方法很有用。 在这种情况下,您可以使用与路由参数中定义的名称相同的名称向方法本身添加参数,并且 PixieApp 框架将自动传递正确的值。
W
wizardforcel 已提交
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147

    ### 注意

    **注意**:参数的顺序实际上并不重要。 您也不必使用路由中定义的每个参数,如果仅对使用参数的子集感兴趣,这将很方便。

    该变量也将在 Jinja2 模板字符串的范围内,如示例所示:

    ```py
    @route(state1="*", state2="*")
    def my_method(self, state1, state2):
        return "<div>State1 is {{state1}}. State2 is {{state2}}</div>"
    ```

    ### 注意

W
wizardforcel 已提交
148
    [您可以在此处找到代码文件](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode2.py)。
W
wizardforcel 已提交
149

W
wizardforcel 已提交
150
*   **方法**的局部变量:只要您将`@templateArgs`装饰器添加到方法中,PixieApp 就会自动将方法中定义的所有局部变量放在 Jinja2 模板字符串的范围内,如示例所示:
W
wizardforcel 已提交
151 152 153 154 155 156 157 158 159 160 161 162

    ```py
    @route()
    @templateArgs
    def main_screen(self):
        var1 = self.compute_something()
        var2 = self.compute_something_else()
        return "<div>var1 is {{var1}}. var2 is {{var2}}</div>"
    ```

    ### 注意

W
wizardforcel 已提交
163
    [您可以在此处找到代码](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode3.py)。
W
wizardforcel 已提交
164 165 166

## 生成对路由的请求

W
wizardforcel 已提交
167
如之前提到的,PixieApp 遵循 SPA 设计模式。 加载第一个屏幕后,与多页 Web 应用一样,使用动态请求而不是 URL 链接完成与服务器的所有后续交互。 有三种方法可以生成对路由的内核请求:
W
wizardforcel 已提交
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225

*   使用`pd_options`自定义属性来定义要传递到服务器的状态列表,如以下示例所示:

    ```py
    pd_options="state1=value1;state2=value2;..;staten=valuen"
    ```

*   如果已经有一个包含`pd_options`值的 JSON 对象(例如,调用`display()`的情况),则必须将其转换为`pd_options` HTML 属性期望的格式,这可能会很耗时。 在这种情况下,将`pd_options`指定为子元素会更方便,这允许将选项直接作为 JSON 对象传递(并避免转换数据的额外工作),如以下示例所示:

    ```py
    <div>
        <pd_options>
            {"state1":"value1","state2":"value2",...,
            "staten":"valuen"}
        </pd_options>
    </div>
    ```

*   通过调用`invoke_route`方法以编程方式,如以下示例所示:

    ```py
    self.invoke_route(self.route_method, state1='value1', state2='value2')
    ```

### 注意

**注意**:如果要从 Jinja2 模板字符串调用此方法,请记住使用`this`而不是`self`,因为`self`已经被 Jinja2 本身使用。

当需要根据用户选择动态计算中传递的状态值时,您需要使用`$val(arg)`特殊指令,该指令充当将在执行内核请求时解析的宏 。

`$val(arg)`指令采用一个参数,该参数可以是以下之一:

*   页面上 HTML 元素的 ID,例如输入或组合框,例如以下示例:

    ```py
    <div>
        <pd_options>
            {"state1":"$val(my_element_id)","state2":"value2"}
        <pd_options>
    </div>
    ```

*   必须返回所需值的 JavaScript 函数,如以下示例所示:

    ```py
    <script>
        function resValue(){
                return "my_query";
        }
    </script>
    ...
    <div pd_options="state1=$val(resValue)"></div>
    ```

### 注意

**注意**:大多数 PixieDust 自定义属性支持使用`$val`指令的动态值。

W
wizardforcel 已提交
226
## GitHub 项目跟踪示例应用
W
wizardforcel 已提交
227

W
wizardforcel 已提交
228
让我们将到目前为止所学到的应用于实现示例应用。 为了解决问题,我们想使用 [GitHub Rest API](https://developer.github.com/v3) 搜索项目并将结果加载到 Pandas `DataFrame`中进行分析。
W
wizardforcel 已提交
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266

初始代码显示了欢迎屏幕,其中带有一个简单的输入框以输入 GitHub 查询和一个按钮来提交请求:

```py
from pixiedust.display.app import *

@PixieApp
class GitHubTracking():
    @route()
    def main_screen(self):
        return """
<style>
    div.outer-wrapper {
        display: table;width:100%;height:300px;
    }
    div.inner-wrapper {
        display: table-cell;vertical-align: middle;height: 100%;width: 100%;
    }
</style>
<div class="outer-wrapper">
    <div class="inner-wrapper">
        <div class="col-sm-3"></div>
        <div class="input-group col-sm-6">
            <input id="query{{prefix}}" type="text" class="form-control" placeholder="Search projects on GitHub">
            <span class="input-group-btn">
                <button class="btn btn-default" type="button">Submit Query</button>
            </span>
        </div>
    </div>
</div>
"""

app = GitHubTracking()
app.run()
```

### 注意

W
wizardforcel 已提交
267
[您可以在此处找到代码文件](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode4.py)
W
wizardforcel 已提交
268 269 270

前面的代码中需要注意的几件事:

W
wizardforcel 已提交
271 272
*   Jupyter 笔记本提供了 [Bootstrap CSS 框架](https://getbootstrap.com/docs/3.3)[jQuery JS 框架](https://jquery.com)。我们可以轻松地在代码中使用,而无需安装它们。
*   [FontAwesome 图标](https://fontawesome.com)默认情况下在笔记本电脑中也可用。
W
wizardforcel 已提交
273 274 275 276 277 278
*   PixieApp 代码可以在笔记本的多个单元中执行。 由于我们依赖 DOM 元素 ID,因此重要的是要确保两个元素没有相同的 ID,这会导致不良的副作用。 为此,建议始终包含由 PixieDust 框架提供的唯一标识符`{{prefix}}`,例如`"query{{prefix}}"`

结果显示在以下屏幕截图中:

![A GitHub project tracking sample application](img/00054.jpeg)

W
wizardforcel 已提交
279
GitHub Tracking 应用的欢迎屏幕
W
wizardforcel 已提交
280 281 282

下一步是创建一个采用用户值并返回结果的新路由。 此路由将由**提交查询**按钮调用。

W
wizardforcel 已提交
283
为简单起见,以下代码未使用 Python 库与 GitHub 进行接口,例如 [PyGithub](http://pygithub.readthedocs.io/en/latest),相反,我们将直接调用 GitHub 网站上记录的 REST API:
W
wizardforcel 已提交
284 285 286

### 注意

W
wizardforcel 已提交
287
**注意**:当您看到以下表示法`[[GitHubTracking]]`时,这意味着该代码应添加到`GitHubTracking` PixieApp 类中,并且为避免一遍又一遍地重复周围的代码,它被省略了。 如有疑问,您可以始终参考本节末尾指定的完整笔记本。
W
wizardforcel 已提交
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310

```py
import requests
import pandas
[[GitHubTracking]]
@route(query="*")
@templateArgs
def do_search(self, query):
    response = requests.get( "https://api.github.com/search/repositories?q={}".format(query))
    frames = [pandas.DataFrame(response.json()['items'])]
    while response.ok and "next" in response.links:
        response = requests.get(response.links['next']['url'])
        frames.append(pandas.DataFrame(response.json()['items']))

    pdf = pandas.concat(frames)
    response = requests.get( "https://api.github.com/search/repositories?q={}".format(query))
    if not response.ok:
        return "<div>An Error occurred: {{response.text}}</div>"
    return """<h1><center>{{pdf|length}} repositories were found</center></h1>"""
```

### 注意

W
wizardforcel 已提交
311
[您可以在此处找到代码文件](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode5.py)
W
wizardforcel 已提交
312

W
wizardforcel 已提交
313
在前面的代码中,我们创建了一个名为`do_search`的路由,该路由带有一个名为`query`的参数,用于构建 GitHub 的 API URL。 使用[`requests` Python 模块](http://docs.python-requests.org)向此 URL 发出 GET 请求,我们获得了一个 JSON 有效负载,我们将其转换为 Pandas `DataFrame`。 根据 GitHub 文档,Search API 分页,并将下一页存储在链接的标题中。 该代码使用`while`循环遍历每个链接并将下一页加载到新的`DataFrame`中。 然后,我们将所有`DataFrame`合并为一个`pdf`。 我们剩下要做的就是构建将显示结果的 HTML 片段。 该片段使用 Ji​​nja2 表示法`{{...}}`来访问定义为局部变量的`pdf`变量,这仅能起作用是因为我们在`do_search`方法中使用了`@templateArgs`装饰器。 注意,我们还使用了名为`length`的 Jinja2 过滤器来显示找到的存储库数量:`{{pdf|length}}`
W
wizardforcel 已提交
314 315 316

### 注意

W
wizardforcel 已提交
317
[有关过滤器的更多信息,请访问以下网站](http://jinja.pocoo.org/docs/templates/#filters)
W
wizardforcel 已提交
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333

当用户单击**提交查询**按钮时,我们仍然需要调用`do_search`路由。 为此,我们将`pd_options`属性添加到`<button>`元素,如下所示:

```py
<div class="input-group col-sm-6">
    <input id="query{{prefix}}" type="text"
     class="form-control"
     placeholder="Search projects on GitHub">
    <span class="input-group-btn">
        <button class="btn btn-default" type="button" pd_options="query=$val(query{{prefix}})">
            Submit Query
        </button>
    </span>
</div>
```

W
wizardforcel 已提交
334
我们在`pd_options`属性中使用`$val()`指令来动态检索 ID 等于`"query{{prefix}}"`的输入框的值,并将其存储在`query`参数中。
W
wizardforcel 已提交
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393

## 在表格中显示搜索结果

前面的代码会一次加载所有数据,因此不建议这样做,因为我们可能会有大量匹配。 同样,一次性显示所有内容会使 UI 缓慢且不实用。 值得庆幸的是,我们可以使用以下步骤轻松地构建分页表,而无需花费太多精力:

1.  创建名为`do_retrieve_page`的路由,该路由以 URL 作为参数并返回表主体的 HTML 片段
2.  将第一个,上一个,下一个和最后一个 URL 保留为 PixieApp 类中的字段
3.  使用`First``Prev``Next``Last`按钮创建一个分页小部件(由于可用,我们将使用 Bootstrap)
4.  使用要显示的列标题创建表占位符

现在,我们将更新`do_search,`的代码,如下所示:

### 注意

**注意**:以下代码引用了`do_retrieve_page`方法,稍后我们将对其进行定义。 在添加`do_retrieve_page`方法之前,请不要尝试按原样运行此代码。

```py
[[GitHubTracking]]
@route(query="*")
@templateArgs
def do_search(self, query):
    self.first_url = "https://api.github.com/search/repositories?q={}".format(query)
    self.prev_url = None
    self.next_url = None
    self.last_url = None

    response = requests.get(self.first_url)
    if not response.ok:
        return "<div>An Error occurred: {{response.text}}</div>"

    total_count = response.json()['total_count']
    self.next_url = response.links.get('next', {}).get('url', None)
    self.last_url = response.links.get('last', {}).get('url', None)
    return """
<h1><center>{{total_count}} repositories were found</center></h1>
<ul class="pagination">
    <li><a href="#" pd_options="page=first_url" pd_target="body{{prefix}}">First</a></li>
    <li><a href="#" pd_options="page=prev_url" pd_target="body{{prefix}}">Prev</a></li>
    <li><a href="#" pd_options="page=next_url" pd_target="body{{prefix}}">Next</a></li>
    <li><a href="#" pd_options="page=last_url" pd_target="body{{prefix}}">Last</a></li>
</ul>
<table class="table">
    <thead>
        <tr>
            <th>Repo Name</th>
            <th>Lastname</th>
            <th>URL</th>
            <th>Stars</th>
        </tr>
    </thead>
    <tbody id="body{{prefix}}">
        {{this.invoke_route(this.do_retrieve_page, page='first_url')}}
    </tbody>
</table>
"""
```

### 注意

W
wizardforcel 已提交
394
[您可以在此处找到代码文件](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode6.py)
W
wizardforcel 已提交
395

W
wizardforcel 已提交
396
前面的代码示例显示了 PixieApps 的一个非常重要的属性,即您可以通过简单地将数据存储到类变量中来维护应用整个生命周期中的状态。 在这种情况下,我们使用`self.first_url``self.prev_url``self.next_url``self.last_url`。 这些变量对分页小部件中的每个按钮使用`pd_options`属性,并在每次调用`do_retrieve_page`路由时更新。 `do_search`返回的片段现在返回带有主体占位符的表,该表由`body{{prefix}},`标识,该表成为每个按钮的`pd_target`。 我们还使用`invoke_route`方法来确保在首次显示表格时获得第一页。
W
wizardforcel 已提交
397

W
wizardforcel 已提交
398
我们之前已经看到路由返回的 HTML 片段用于替换整个页面,但是在前面的代码中,我们使用`pd_target="body{{prefix}}"`属性表示 HTML 片段将被注入到表的 BODY 元素中,它具有`body{{prefix}}` ID。 如果需要,您还可以通过创建一个或多个`<target>`元素作为可点击源元素的子元素来为用户操作定义多个目标。 每个`<target>`元素本身都可以使用所有 PixieApp 自定义属性来配置内核请求。
W
wizardforcel 已提交
399 400 401 402 403 404 405 406 407 408

这是一个例子:

```py
<button type="button">Multiple Targets
    <target pd_target="elementid1" pd_options="state1=value1"></target>
    <target pd_target="elementid2" pd_options="state2=value2"></target>
</button>
```

W
wizardforcel 已提交
409
回到我们的 GitHub 示例应用,`do_retrieve_page`方法现在看起来像这样:
W
wizardforcel 已提交
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436

```py
[[GitHubTracking]]
@route(page="*")
@templateArgs
def do_retrieve_page(self, page):
    url = getattr(self, page)
    if url is None:
        return "<div>No more rows</div>"
    response = requests.get(url)
    self.prev_url = response.links.get('prev', {}).get('url', None)
    self.next_url = response.links.get('next', {}).get('url', None)
    items = response.json()['items']
    return """
{%for row in items%}
<tr>
    <td>{{row['name']}}</td>
    <td>{{row.get('owner',{}).get('login', 'N/A')}}</td>
    <td><a href="{{row['html_url']}}" target="_blank">{{row['html_url']}}</a></td>
    <td>{{row['stargazers_count']}}</td>
</tr>
{%endfor%}
        """
```

### 注意

W
wizardforcel 已提交
437
[您可以在此处找到代码文件](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode7.py)
W
wizardforcel 已提交
438

W
wizardforcel 已提交
439
`page`参数是一个字符串,其中包含我们要显示的`url`类变量的名称。 我们使用标准的[`getattr` Python 函数](https://docs.python.org/2/library/functions.html#getattr)从页面获取`url`值。 然后,我们在 GitHub API `url`上发出 GET 请求,以 JSON 格式检索有效负载,并将其传递给 Jinja2 模板以生成将插入表中的行集。 为此,我们使用 Jinja2 中可用的[`{%for…%}`循环控制结构](http://jinja.pocoo.org/docs/templates/#for)生成`<tr>``<td>` HTML 标记。
W
wizardforcel 已提交
440 441 442 443 444 445 446 447 448 449 450 451 452

以下屏幕快照显示了查询的搜索结果:`pixiedust`

![Displaying the search results in a table](img/00055.jpeg)

屏幕显示查询产生的 GitHub 存储库列表

### 注意

在第 1 部分中,我们展示了如何创建`GitHubTracking` PixieApp,如何调用 GitHub 查询 REST API 以及如何使用分页在表中显示结果。 您可以在此处找到带有源代码的完整笔记本:

`https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/GitHub%20Tracking%20Application/GitHub%20Sample%20Application%20-%20Part%201.ipynb`

W
wizardforcel 已提交
453
在的下一部分中,我们将探索更多 PixieApp 功能,这些功能将允许我们深入用户到特定存储库并可视化有关存储库的各种统计信息,从而使我们能够改进应用。
W
wizardforcel 已提交
454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499

第一步是在搜索结果表的每一行中添加一个按钮,以触发一条新路径以可视化所选存储库统计信息。

以下代码是`do_search`函数的一部分,并在表头中添加了新列:

```py
<thead>
    <tr>
        <th>Repo Name</th>
        <th>Lastname</th>
        <th>URL</th>
        <th>Stars</th>
 <th>Actions</th>
    </tr>
</thead>
```

为了完成该表,我们更新了`do_retrieve_page`方法以添加一个包含`<button>`元素的新单元格,该单元格具有与`analyse_repo_owner``analyse_repo_name`相匹配的新路径的`pd_options`参数。 这些参数的值是从`row`元素中提取的,该元素用于对从 GitHub 请求接收的有效负载进行迭代:

```py
{%for row in items%}
<tr>
    <td>{{row['name']}}</td>
    <td>{{row.get('owner',{}).get('login', 'N/A')}}</td>
    <td><a href="{{row['html_url']}}" target="_blank">{{row['html_url']}}</a></td>
    <td>{{row['stargazers_count']}}</td>
 <td>
 <button pd_options=
 "analyse_repo_owner={{row["owner"]["login"]}};
 analyse_repo_name={{row['name']}}"
 class="btn btn-default btn-sm" title="Analyze Repo">
 <i class="fa fa-line-chart"></i>
 </button>
 </td>
</tr>
{%endfor%}
```

有了此简单代码更改之后,通过再次运行单元重新启动 PixieApp,我们现在可以看到每个存储库的按钮,即使我们尚未实现相应的路由,也将在下一步实现。 提醒一下,当找不到匹配的路由时,将触发默认路由。

以下屏幕截图显示了带有添加按钮的表:

![Displaying the search results in a table](img/00056.jpeg)

为每一行添加操作按钮

W
wizardforcel 已提交
500
下一步是创建与“回购可视化”页面关联的路由。 该页面的设计非常简单:用户可以从组合框中选择要在页面上可视化的数据类型。 GitHub REST API 提供了对多种类型数据的访问,但是对于此示例应用,我们将使用提交活动数据,该数据属于统计信息类别(请参阅[这个页面](https://developer.github.com/v3/repos/statistics/#get-the-last-year-of-commit-activity-data),以获取此 API 的详细说明)。
W
wizardforcel 已提交
501 502 503

### 提示

W
wizardforcel 已提交
504
作为练习,请随时通过添加其他类型的 API 的可视化来改进此示例应用,例如 [Traffic API](https://developer.github.com/v3/repos/traffic)
W
wizardforcel 已提交
505

W
wizardforcel 已提交
506
还需要注意的是,即使大多数 GitHub API 都无需认证即可工作,但如果您不提供凭据,服务器可能会限制响应。 要验证请求的身份,您需要使用 GitHub 密码或通过选择 GitHub **设置**页面上的**开发人员设置**菜单,然后单击**个人**来生成个人访问令牌。 访问**令牌**菜单,然后单击**生成新令牌按钮**
W
wizardforcel 已提交
507

W
wizardforcel 已提交
508
在单独的笔记本单元中,我们将为 GitHub 用户 ID 和令牌创建两个变量:
W
wizardforcel 已提交
509 510 511 512 513 514

```py
github_user = "dtaieb"
github_token = "XXXXXXXXXX"
```

W
wizardforcel 已提交
515
这些变量将在以后用于验证请求。 请注意,即使这些变量是在其自己的单元中创建的,它们也对整个笔记本可见,包括 PixieApp 代码。
W
wizardforcel 已提交
516 517 518

为了提供良好的代码模块化和重用性,我们将在新类中实现 Repo Visualization 页面,并使我们的主要 PixieApp 类继承自该类并自动重用其路由。 当您开始拥有大型项目并将其分解为多个类时,请牢记这种模式。

W
wizardforcel 已提交
519
“回购可视化”页面的主要路径返回一个 HTML 片段,该片段具有一个下拉菜单和一个用于可视化的`<div>`占位符。 使用 [Bootstrap `dropdown`类](https://www.w3schools.com/bootstrap/bootstrap_dropdowns.asp)创建下拉菜单。 为了使代码更易于维护,菜单项是通过在元组数组上使用 [Jinja2 `{%for..` `%}`循环](https://docs.python.org/3/tutorial/datastructures.html#tuples-and-sequences)生成的,称为`analyses`的元组和序列,其中包含描述和将数据加载到 Pandas `DataFrame`中的函数。 再次在这里,我们在自己的单元格中创建此数组,该数组将在 PixieApp 类中引用:
W
wizardforcel 已提交
520 521 522 523 524 525 526

```py
analyses = [("Commit Activity", load_commit_activity)]
```

### 注意

W
wizardforcel 已提交
527
**注意**`load_commit_activity`函数将在本节稍后讨论。
W
wizardforcel 已提交
528

W
wizardforcel 已提交
529
出于此示例应用的目的,该数组仅包含一个与提交活动相关的元素,但是将来您可能添加的任何元素都将由 UI 自动选择。
W
wizardforcel 已提交
530

W
wizardforcel 已提交
531
`do_analyse_repo`路由具有两个参数:`analyse_repo_owner``analyse_repo_name,`,足以访问 GitHub API。 我们还需要将这些参数保存为类变量,因为在生成可视化效果的路由中将需要它们:
W
wizardforcel 已提交
532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565

```py
@PixieApp
class RepoAnalysis():
    @route(analyse_repo_owner="*", analyse_repo_name="*")
    @templateArgs
    def do_analyse_repo(self, analyse_repo_owner, analyse_repo_name):
        self._analyse_repo_owner = analyse_repo_owner
        self._analyse_repo_name = analyse_repo_name
        return """
<div class="container-fluid">
    <div class="dropdown center-block col-sm-2">
        <button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">
            Select Repo Data Set
            <span class="caret"></span>
        </button>
        <ul class="dropdown-menu" style="list-style:none;margin:0px;padding:0px">
            {%for analysis,_ in this.analyses%}
                <li>
                    <a href="#" pd_options="analyse_type={{analysis}}" pd_target="analyse_vis{{prefix}}"
                     style="text-decoration: none;background-color:transparent">
                        {{analysis}}
                    </a>
                </li>
            {%endfor%}
        </ul>
    </div>
    <div id="analyse_vis{{prefix}}" class="col-sm-10"></div>
</div>
"""
```

### 注意

W
wizardforcel 已提交
566
[您可以在此处找到代码文件](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode8.py)
W
wizardforcel 已提交
567 568 569 570 571 572 573 574 575 576

### 注意

上面的代码中有两点需要注意:

*   即使`analyses`变量未定义为类变量,Jinja2 模板也使用`this`关键字引用`analyses`数组。 之所以有效,是因为 PixieApp 的另一个重要功能:在笔记本本身定义的任何变量都可以被引用,就好像它们是 PixieApp 的类变量一样。
*   我将`analyse_repo_owner``analyse_repo_name`存储为具有不同名称的类变量,例如`_analyse_repo_owner``_analyse_repo_name`。 这很重要,因为使用相同的名称会对路由匹配算法产生副作用,该算法还会查看类变量以查找参数。 使用相同的名称将导致始终找到此路由,这不是理想的效果。

操作按钮链接由`<a>`标记定义,并使用`pd_options`访问具有一个称为`analyse_type`以及`pd_target`指向`"analyse_vis{{prefix}}"`占位符`<div>,`的自变量的路由。 在以下相同的 HTML 片段中定义。

W
wizardforcel 已提交
577
## 使用`pd_entity`属性调用 PixieDust `display()` API
W
wizardforcel 已提交
578

W
wizardforcel 已提交
579
当使用`pd_options`属性创建内核请求时,PixieApp 框架将当前 PixieApp 类用作目标。 但是,您可以通过指定`pd_entity`属性来更改此目标。 例如,您可以指向另一个 PixieApp,或更有趣的是,指向`display()` API 支持的数据结构,例如 Pandas 或 Spark `DataFrame`。 在这种情况下,如果您提供了`display()` API 预期的正确选项,则生成的输出将为图表本身(对于 Matplotlib,为图像;对于 Mapbox,为 IFRAME;对于 Mapbox,则为 SVG 散景)。 一种获取正确选项的简单方法是在其自己的单元格中调用`display()` API,使用菜单将其配置为所需的图表,然后通过单击**编辑元数据**复制可用的单元格元数据 JSON 片段。 按钮。 (您可能首先需要使用菜单**视图 | 单元格工具栏 | 编辑元数据**来启用按钮)。
W
wizardforcel 已提交
580

W
wizardforcel 已提交
581
您也可以指定`pd_entity`,不带任何值。 在这种情况下,PixieApp 框架将使用传递为用于启动 PixieApp 应用的`run`方法的第一个参数的实体。 例如,以`cars`为 Pandas 的`my_pixieapp.run(cars)`或通过`pixiedust.sampleData()`方法创建的 Spark `DataFrame``pd_entity`的值也可以是返回实体的函数调用。 当您要在渲染之前动态计算实体时,这很有用。 与其他变量一样,`pd_entity`的范围可以是 PixieApp 类或在笔记本中声明的任何变量。
W
wizardforcel 已提交
582

W
wizardforcel 已提交
583
例如,我们可以在其自己的单元格中创建一个函数,该函数将前缀作为参数并返回 Pandas `DataFrame`。 然后我们将其用作我的 PixieApp 中的`pd_entity`值,如以下代码所示:
W
wizardforcel 已提交
584 585 586 587 588 589 590 591 592 593

```py
def compute_pdf(key):
    return pandas.DataFrame([
        {"col{}".format(i): "{}{}-{}".format(key,i,j) for i in range(4)} for j in range(10)
    ])
```

### 注意

W
wizardforcel 已提交
594
[您可以在此处找到代码文件](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode9.py)
W
wizardforcel 已提交
595

W
wizardforcel 已提交
596
在前面的代码中,我们使用 [Python 列表推导](https://docs.python.org/2/tutorial/datastructures.html#list-comprehensions)快速基于`key`参数生成模拟数据。
W
wizardforcel 已提交
597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621

### 注意

Python 列表推导是我最喜欢的 Python 语言功能之一,因为它们使您可以使用表达简洁的语法来创建,转换和提取数据。

我可以然后创建一个 PixieApp,使用`compute_pdf`函数作为`pd_entity`将数据呈现为表格:

```py
from pixiedust.display.app import *
@PixieApp
class TestEntity():
    @route()
    def main_screen(self):
        return """
        <h1><center>
            Simple PixieApp with dynamically computed dataframe
        </center></h1>
        <div pd_entity="compute_pdf('prefix')" pd_options="handlerId=dataframe" pd_render_onload></div>
        """
test = TestEntity()
test.run()
```

### 注意

W
wizardforcel 已提交
622
[您可以在此处找到代码文件](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode10.py)
W
wizardforcel 已提交
623

W
wizardforcel 已提交
624
在前面的代码中,为简单起见,我将键硬编码为`'prefix'`,然后将其作为练习使用输入控件和`$val()`指令使其可用户定义。
W
wizardforcel 已提交
625

W
wizardforcel 已提交
626
值得注意的另一件事是在显示图表的 DIV 中使用`pd_render_onload`属性。 此属性告诉 PixieApp 在将元素加载到浏览器 DOM 中后立即执行该元素定义的内核请求。
W
wizardforcel 已提交
627 628 629 630 631

以下屏幕快照显示了先前 PixieApp 的结果:

![Invoking the PixieDust display() API using pd_entity attribute](img/00057.jpeg)

W
wizardforcel 已提交
632
在 PixieApp 中动态创建`DataFrame`
W
wizardforcel 已提交
633

W
wizardforcel 已提交
634
回到我们的`Github Tracking`应用,现在让我们将`pd_entity`值应用于从 GitHub Statistics API 加载的`DataFrame`。 我们创建了一个称为`load_commit_activity,`的方法,该方法负责将数据加载到 Pandas `DataFrame`中,并将其与显示图表所需的`pd_options`一起返回:
W
wizardforcel 已提交
635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662

```py
from datetime import datetime
import requests
import pandas
def load_commit_activity(owner, repo_name):
    response = requests.get(
        "https://api.github.com/repos/{}/{}/stats/commit_activity".format(owner, repo_name),
        auth=(github_user, github_token)
    ).json()
    pdf = pandas.DataFrame([
        {"total": item["total"], "week":datetime.fromtimestamp(item["week"])} for item in response
    ])

    return {
        "pdf":pdf,
        "chart_options": {
          "handlerId": "lineChart",
          "keyFields": "week",
          "valueFields": "total",
          "aggregation": "SUM",
          "rendererId": "bokeh"
        }
    }
```

### 注意

W
wizardforcel 已提交
663
[您可以在此处找到代码文件](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode11.py)
W
wizardforcel 已提交
664

W
wizardforcel 已提交
665
前面的代码将 GET 请求发送到 GitHub,并使用在笔记本开始时设置的`github_user``github_token`变量进行认证。 响应是一个 JSON 有效负载,我们将使用它创建一个 Pandas `DataFrame`。 在创建`DataFrame`之前,我们需要将 JSON 有效负载转换为正确的格式。 现在,有效负载如下所示:
W
wizardforcel 已提交
666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682

```py
[
{"days":[0,0,0,0,0,0,0],"total":0,"week":1485046800},
{"days":[0,0,0,0,0,0,0],"total":0,"week":1485651600},
{"days":[0,0,0,0,0,0,0],"total":0,"week":1486256400},
{"days":[0,0,0,0,0,0,0],"total":0,"week":1486861200}
...
]
```

我们需要删除`days`键,因为它不需要显示图表,并且,为了正确显示图表,我们需要将`week`键的值(它是 Unix 时间戳)转换为 Python `datetime`对象 。 这种转换是通过 Python 列表理解和简单的代码行完成的:

```py
[{"total": item["total"], "week":datetime.fromtimestamp(item["week"])} for item in response]
```

W
wizardforcel 已提交
683
在当前实现中,`load_commit_activity`函数在其自己的单元格中定义,但我们也可以将其定义为 PixieApp 的成员方法。 最佳实践是使用自己的单元非常方便,因为我们可以对功能进行单元测试并对其进行快速迭代,而不会产生每次运行完整应用的开销。
W
wizardforcel 已提交
684 685 686 687 688

要获取`pd_options`值,我们可以简单地使用示例回购信息运行该函数,然后在单独的单元格中调用`display()` API:

![Invoking the PixieDust display() API using pd_entity attribute](img/00058.jpeg)

W
wizardforcel 已提交
689
在单独的单元格中使用`display()`获取可视化配置
W
wizardforcel 已提交
690

W
wizardforcel 已提交
691
要获取上述图表,您需要选择**折线图**,然后在**选项**对话框中,将`week`列拖放到**键**框和`total`列到**值**框。 您还需要选择 Bokeh 作为渲染器。 完成后,请注意,PixieDust 将自动检测到`x`轴为日期时间,并将相应地调整渲染。
W
wizardforcel 已提交
692 693 694 695 696

使用**编辑元数据**按钮,我们现在可以复制图表选项 JSON 片段:

![Invoking the PixieDust display() API using pd_entity attribute](img/00059.jpeg)

W
wizardforcel 已提交
697
捕获`display()` JSON 配置
W
wizardforcel 已提交
698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734

然后在`load_commit_activity`有效载荷中返回它:

```py
return {
        "pdf":pdf,
        "chart_options": {
          "handlerId": "lineChart",
          "keyFields": "week",
          "valueFields": "total",
          "aggregation": "SUM",
          "rendererId": "bokeh"
        }
    }
```

现在,我们准备在`RepoAnalysis`类中实现`do_analyse_type`路由,如以下代码所示:

```py
[[RepoAnalysis]]
@route(analyse_type="*")
@templateArgs
def do_analyse_type(self, analyse_type):
    fn = [analysis_fn for a_type,analysis_fn in analyses if a_type == analyse_type]
    if len(fn) == 0:
        return "No loader function found for {{analyse_type}}"
    vis_info = fn[0](self._analyse_repo_owner, self._analyse_repo_name)
    self.pdf = vis_info["pdf"]
    return """
    <div pd_entity="pdf" pd_render_onload>
        <pd_options>{{vis_info["chart_options"] | tojson}}</pd_options>
    </div>
    """
```

### 注意

W
wizardforcel 已提交
735
[您可以在此处找到代码文件](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode12.py)
W
wizardforcel 已提交
736

W
wizardforcel 已提交
737
路由有一个名为`analyse_type,`的参数,我们将其用作在`analyses`数组中查找`load`函数的键(注意,我再次使用列表推导来快速进行搜索)。 然后,我们调用传递回购所有者和名称的此函数来获取`vis_info` JSON 有效负载,并将 Pandas `DataFrame`存储到名为`pdf`的类变量中。 然后,返回的 HTML 片段将`pdf`用作`pd_entity`值,将`vis_info["chart_options"]`用作`pd_optio` `ns`。 在这里,我使用[`tojson` Jinja2 过滤器](http://jinja.pocoo.org/docs/templates/#list-of-builtin-filters)来确保在生成的 HTML 中正确进行了转义 。 即使已在栈上声明了`vis_info`变量,也允许我使用它,因为我为函数使用了`@templateArgs`装饰器。
W
wizardforcel 已提交
738

W
wizardforcel 已提交
739
在测试改进的应用之前,要做的最后一件事是确保主要的`GitHubTracking` PixieApp 类继承自`RepoAnalysis` PixieApp:
W
wizardforcel 已提交
740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763

```py
@PixieApp
class GitHubTracking(RepoAnalysis):
    @route()
    def main_screen(self):
        <<Code omitted here>>

    @route(query="*")
    @templateArgs
    def do_search(self, query):
        <<Code omitted here>>

    @route(page="*")
    @templateArgs
    def do_retrieve_page(self, page):
        <<Code omitted here>>

app = GitHubTracking()
app.run()
```

### 注意

W
wizardforcel 已提交
764
[您可以在此处找到代码文件](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode13.py)
W
wizardforcel 已提交
765 766 767 768 769 770 771 772 773

“回购分析”页面的屏幕快照如下所示:

![Invoking the PixieDust display() API using pd_entity attribute](img/00060.jpeg)

GitHub 回购提交活动可视化

### 注意

W
wizardforcel 已提交
774
[如果您想进一步试验,可以在此处找到 GitHub 跟踪应用第 2 部分的完整笔记本](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/GitHub%20Tracking%20Application/GitHub%20Sample%20Application%20-%20Part%202.ipynb)
W
wizardforcel 已提交
775

W
wizardforcel 已提交
776
## 使用`pd_script`调用任意 Python 代码
W
wizardforcel 已提交
777 778 779

在此部分中,我们研究`pd_script`定制属性,该属性使您可以在触发内核请求时运行任意 Python 代码。 有一些规则可以控制 Python 代码的执行方式:

W
wizardforcel 已提交
780
*   该代码可以使用`self`关键字以及在笔记本中定义的任何变量,函数和类访问 PixieApp 类,如以下示例所示:
W
wizardforcel 已提交
781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807

    ```py
    <button type="submit" pd_script="self.state='value'">Click me</button>
    ```

*   如果指定了`pd_target`,则在`target`元素中将输出使用`print`函数的任何语句。 如果不存在`pd_target`,则不是这种情况。 换句话说,您不能使用`pd_script`进行整页刷新(您必须使用`pd_options`属性),例如:

    ```py
    from pixiedust.display.app import *

    def call_me():
        print("Hello from call_me")

    @PixieApp
    class Test():
        @route()
        def main_screen(self):
            return """
            <button type="submit" pd_script="call_me()" pd_target="target{{prefix}}">Click me</button>

            <div id="target{{prefix}}"></div>
            """
    Test().run()
    ```

    ### 注意

W
wizardforcel 已提交
808
    [您可以在此处找到的代码文件](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode14.py)。
W
wizardforcel 已提交
809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832

*   如果代码包含多行,建议使用`pd_script`子元素,它使您可以使用多行编写 Python 代码。 使用此格式时,请确保代码遵循缩进的 Python 语言规则,例如:

    ```py
    @PixieApp
    class Test():
        @route()
        def main_screen(self):
            return """
            <button type="submit" pd_script="call_me()" pd_target="target{{prefix}}">
                <pd_script>
                    self.name="some value"
                    print("This is a multi-line pd_script")
                </pd_script>
                Click me
            </button>

            <div id="target{{prefix}}"></div>
            """
    Test().run()
    ```

### 注意

W
wizardforcel 已提交
833
[您可以在此处找到代码文件](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode15.py)
W
wizardforcel 已提交
834

W
wizardforcel 已提交
835
`pd_script`的一种常见用例是在触发内核请求之前更新服务器上的某些状态。 通过添加复选框在折线图和数据统计摘要之间切换可视化,让我们将此技术应用于`Github Tracking`应用。
W
wizardforcel 已提交
836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902

`do_analyse_repo`返回的 HTML 片段中,我们添加了用于在图表和统计信息摘要之间切换的复选框元素:

```py
[[RepoAnalysis]]
...
return """
<div class="container-fluid">
    <div class="col-sm-2">
        <div class="dropdown center-block">
            <button class="btn btn-primary
             dropdown-toggle" type="button"
             data-toggle="dropdown">
                Select Repo Data Set
                <span class="caret"></span>
            </button>
            <ul class="dropdown-menu"
             style="list-style:none;margin:0px;padding:0px">
                {%for analysis,_ in this.analyses%}
                    <li>
                        <a href="#"
                        pd_options="analyse_type={{analysis}}"
                        pd_target="analyse_vis{{prefix}}"
                        style="text-decoration: none;background-color:transparent">
                            {{analysis}}
                        </a>
                    </li>
                {%endfor%}
            </ul>
        </div>
        <div class="checkbox">
            <label>
                <input id="show_stats{{prefix}}" type="checkbox"
                  pd_script="self.show_stats=('$val(show_stats{{prefix}})' == 'true')">
                Show Statistics
            </label>
        </div>
    </div>
    <div id="analyse_vis{{prefix}}" class="col-sm-10"></div>
</div>
"""
```

`checkbox`元素中,包含`pd_script`属性,该属性根据`checkbox`元素的状态修改服务器上的变量状态。 我们使用`$val()`指令检索`show_stats_{{prefix}}`元素的值,并将其与`true string`进行比较。 当用户单击复选框时,服务器上的状态将立即更改,并且当用户下次单击菜单时,将显示统计信息而不是图表。

现在,我们需要更改`do_analyse_type`路由以动态配置`pd_entity``chart_options`

```py
[[RepoAnalysis]]
@route(analyse_type="*")
@templateArgs
def do_analyse_type(self, analyse_type):
    fn = [analysis_fn for a_type,analysis_fn in analyses if a_type == analyse_type]
    if len(fn) == 0:
        return "No loader function found for {{analyse_type}}"
    vis_info = fn[0](self._analyse_repo_owner, self._analyse_repo_name)
    self.pdf = vis_info["pdf"]
    chart_options = {"handlerId":"dataframe"} if self.show_stats else vis_info["chart_options"]
    return """
    <div pd_entity="get_pdf()" pd_render_onload>
        <pd_options>{{chart_options | tojson}}</pd_options>
    </div>
    """
```

### 注意

W
wizardforcel 已提交
903
[您可以在这里找到文件](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode16.py)
W
wizardforcel 已提交
904 905 906

`chart_options`现在是一个局部变量,如果`show_stats``true`,则包含显示为表格的选项;如果不是,则包含常规折线图选项。

W
wizardforcel 已提交
907
`pd_entity`现在设置为`get_pdf()`方法,该方法负责基于`show_stats`变量返回适当的`DataFrame`
W
wizardforcel 已提交
908 909 910 911 912 913 914 915 916 917 918 919

```py
def get_pdf(self):
    if self.show_stats:
        summary = self.pdf.describe()
        summary.insert(0, "Stat", summary.index)
        return summary
    return self.pdf
```

### 注意

W
wizardforcel 已提交
920
[您可以在此处找到代码文件](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode17.py)
W
wizardforcel 已提交
921

W
wizardforcel 已提交
922
我们使用 Pandas [`describe()`方法](https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.describe.html)返回包含摘要的`DataFrame`统计信息,例如计数,均值,标准差等。 我们还确保此`DataFrame`的第一列包含统计信息的名称。
W
wizardforcel 已提交
923 924 925

我们需要做的最后一个更改是初始化`show_stats`变量,因为如果不这样做,那么第一次检查它时,我们会得到`AttributeError`异常。

W
wizardforcel 已提交
926
由于使用`@PixieApp`装饰器的内部机制,因此无法使用`__init__`方法来初始化变量。 相反,PixieApp 编程模型要求您使用一种称为`setup,`的方法,该方法可以确保在应用启动时被调用:
W
wizardforcel 已提交
927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947

```py
@PixieApp
class RepoAnalysis():
    def setup(self):
        self.show_stats = False
    ...
```

### 注意

**注意**:如果您有一个从其他 PixieApps 继承的类,则 PixieApp 框架将使用它们的出现顺序自动从基类中调用所有`setup`函数。

以下屏幕截图显示了正在显示的摘要统计信息:

![Invoking arbitrary Python code with pd_script](img/00061.jpeg)

GitHub 存储库的摘要统计信息

### 注意

W
wizardforcel 已提交
948
[您可以在此处找到`Github Tracking`应用第 3 部分的完整笔记本](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/GitHub%20Tracking%20Application/GitHub%20Sample%20Application%20-%20Part%203.ipynb)
W
wizardforcel 已提交
949

W
wizardforcel 已提交
950
## 使用`pd_refresh`使应用响应更快
W
wizardforcel 已提交
951 952 953 954 955 956 957

我们希望通过使**显示统计信息**按钮直接显示统计信息表格,而不是让用户再次单击菜单来改善用户体验。 类似于加载**提交活动**的菜单,我们可以向复选框添加`pd_options`属性,其中`pd_target`属性指向`analyse_vis{{prefix}}`元素。 无需在触发新显示的每个控件中复制`pd_options`,我们可以将其添加到`analyse_vis{{prefix}}`一次,并使用`pd_refresh`属性对其进行更新。

下图显示了两种设计之间的差异:

![Making the application more responsive with pd_refresh](img/00062.jpeg)

W
wizardforcel 已提交
958
有和没有`pd_refresh`的序列图
W
wizardforcel 已提交
959 960 961 962 963

在这两种情况下,步骤 1 都是在服务器端更新某些状态。 在步骤 2 中显示的**控件**调用路由的情况下,请求规范存储在控件本身中,触发步骤 3,该步骤将生成 HTML 片段并将其注入目标元素中 。 使用`pd_refresh`,控件不知道`pd_options`来调用路由,相反,它仅使用`pd_refresh`来向目标元素发信号,从而依次调用路由。 在这种设计中,我们只需要在目标元素中指定一次请求,并且用户控件只需要在触发刷新之前更新状态即可。 这使实现更易于维护。

为了更好地理解两种设计之间的差异,让我们比较`RepoAnalysis`类中的两种实现。

W
wizardforcel 已提交
964
对于**分析**菜单,更改如下:
W
wizardforcel 已提交
965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033

之前,控件触发了`analyse_type`路由,将`{{analysis}}`选择作为内核请求的一部分传递给了`analyse_vis{{prefix}}`

```py
<a href="#" pd_options="analyse_type={{analysis}}"
            pd_target="analyse_vis{{prefix}}"
            style="text-decoration: none;background-color:transparent">
      {{analysis}}
</a>
```

之后,控件现在将选择状态存储为类字段,并要求`analyse_vis{{prefix}}`元素刷新自身:

```py
<a href="#" pd_script="self.analyse_type='{{analysis}}'"
 pd_refresh="analyse_vis{{prefix}}"
 style="text-decoration: none;background-color:transparent">
    {{analysis}}
</a>
```

同样,**显示统计信息**复选框的更改如下:

在复选框之前,只需在类中设置`show_stats`状态即可; 用户必须再次单击菜单以获得可视化效果:

```py
<div class="checkbox">
    <label>
        <input type="checkbox"
         id="show_stats{{prefix}}"
pd_script="self.show_stats='$val(show_stats{{prefix}})'=='true'">
        Show Statistics
    </label>
</div>
```

之后,由于具有`pd_refresh`属性,因此一旦选中该复选框,可视化文件就会更新:

```py
<div class="checkbox">
    <label>
        <input type="checkbox"
         id="show_stats{{prefix}}"
  pd_script="self.show_stats='$val(show_stats{{prefix}})'=='true'"
         pd_refresh="analyse_vis{{prefix}}">
         Show Statistics
    </label>
</div>
```

最后,`analyse_vis{{prefix}}`元素的更改如下:

之前,该元素不知道如何更新自身,它依靠其他控件将请求定向到适当的路由:

```py
<div id="analyse_vis{{prefix}}" class="col-sm-10"></div>
```

之后,元素将携带内核配置以进行自我更新。 任何控件现在都可以更改状态并调用刷新:

```py
<div id="analyse_vis{{prefix}}" class="col-sm-10"
     pd_options="display_analysis=true"
     pd_target="analyse_vis{{prefix}}">
</div>
```

### 注意

W
wizardforcel 已提交
1034
[您可以在以下位置找到`Github Tracking`应用第 4 部分的完整笔记本](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/GitHub%20Tracking%20Application/GitHub%20Sample%20Application%20-%20Part%204.ipynb)
W
wizardforcel 已提交
1035 1036 1037 1038 1039 1040

## 创建可重用的小部件

PixieApp 编程模型提供了一种机制,用于将 HTML 和复杂 UI 构造的逻辑打包到一个小部件中,可以轻松地从其他 PixieApps 中调用该小部件。 创建窗口小部件的步骤如下:

1.  创建一个将包含小部件的 PixieApp 类。
W
wizardforcel 已提交
1041
2.  C 创建一个带有特殊`widget`属性的路由,如示例所示:
W
wizardforcel 已提交
1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073

    ```py
    @route(widget="my_widget")
    ```

    这将是小部件的起始路径。

3.  创建从小部件 PixieApp 类继承的使用者 PixieApp 类。
4.  通过使用`pd_widget`属性从`<div>`元素调用窗口小部件。

这是的示例,介绍如何创建小部件和使用者 PixieApp 类:

```py
from pixiedust.display.app import *

@PixieApp
class WidgetApp():
    @route(widget="my_widget")
    def widget_main_screen(self):
        return "<div>Hello World Widget</div>"

@PixieApp
class ConsumerApp(WidgetApp):
    @route()
    def main_screen(self):
        return """<div pd_widget="my_widget"></div>"""

ConsumerApp.run()
```

### 注意

W
wizardforcel 已提交
1074
[您可以在此处找到代码](https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode18.py)
W
wizardforcel 已提交
1075

W
wizardforcel 已提交
1076
# 总结
W
wizardforcel 已提交
1077

W
wizardforcel 已提交
1078
在本章中,我们介绍了 PixieApp 编程模型的基本构建模块,使您可以直接在笔记本中创建强大的工具和仪表板。
W
wizardforcel 已提交
1079

W
wizardforcel 已提交
1080
我们还通过展示如何构建`Github Tracking`示例应用(包括详细的代码示例)来说明 PixieApp 的概念和技术。 最佳做法和更高级的 PixieApp 概念将在第 5 章,“最佳做法和高级 PixieDust 概念”中进行介绍,包括事件,流和调试。
W
wizardforcel 已提交
1081

W
wizardforcel 已提交
1082
到目前为止,您应该希望对 Jupyter 笔记本,PixieDust 和 PixieApps 如何使数据科学家和开发人员能够通过单一工具(例如 Jupyter 笔记本)进行协作来帮助弥合数据科学家和开发人员之间的差距有所了解。
W
wizardforcel 已提交
1083

W
wizardforcel 已提交
1084
在下一章中,我们将展示如何从笔记本中释放 PixieApp 并使用 PixieGateway 微服务服务器将其发布为 Web 应用。