From bb2de73d8f1e792b9dfda2c428d7c551f3861a5d Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Sat, 15 Aug 2026 22:31:57 +0200 Subject: [PATCH 01/13] save snapshot to metadata --- src/components/icon/export.tsx | 18 ++++++++ src/document/factory.tsx | 19 ++++++++ src/specta_model.ts | 10 +++- src/specta_widget.ts | 83 +++++++++++++++++++++++++++++++++- src/tool.ts | 24 ++++++++++ 5 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 src/components/icon/export.tsx diff --git a/src/components/icon/export.tsx b/src/components/icon/export.tsx new file mode 100644 index 0000000..8657fc4 --- /dev/null +++ b/src/components/icon/export.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +export const ExportIcon = ({ ...props }: React.SVGProps) => ( + + + +); diff --git a/src/document/factory.tsx b/src/document/factory.tsx index 2357578..3d079b4 100644 --- a/src/document/factory.tsx +++ b/src/document/factory.tsx @@ -4,6 +4,8 @@ import { INotebookModel } from '@jupyterlab/notebook'; import { Panel } from '@lumino/widgets'; import * as React from 'react'; +import { ExportIcon } from '../components/icon/export'; +import { IconButton } from '../components/iconButton'; import { SpectaWidgetFactory } from '../specta_widget_factory'; import { ISpectaLayoutRegistry, @@ -53,6 +55,7 @@ export class NotebookGridWidgetFactory extends ABCWidgetFactory< nbPath: path }); const isSpecta = isSpectaApp(); + let topbarTarget: ISpectaTopbarWidget | undefined; if (!spectaConfig.hideTopbar) { const title = ; let topbarWidget: ISpectaTopbarWidget | undefined = undefined; @@ -88,6 +91,7 @@ export class NotebookGridWidgetFactory extends ABCWidgetFactory< } localTopbar.addReactWidget(menu, 'right', 10000); content.addWidget(localTopbar); + topbarTarget = localTopbar; } else { if (this._spectaTopbar.addReactWidget) { const titleWidget = this._spectaTopbar.addReactWidget( @@ -111,6 +115,21 @@ export class NotebookGridWidgetFactory extends ABCWidgetFactory< if (spectaWidget) { content.addWidget(spectaWidget); + if (topbarTarget?.addReactWidget) { + const button = ( + await spectaWidget.saveSnapshot()} + icon={ + + } + /> + ); + topbarTarget.addReactWidget(button, 'right', 9999); + } } }); diff --git a/src/specta_model.ts b/src/specta_model.ts index 99b4603..5b1142d 100644 --- a/src/specta_model.ts +++ b/src/specta_model.ts @@ -31,7 +31,7 @@ import { createNotebookPanel } from './create_notebook_panel'; import { SpectaCellOutput } from './specta_cell_output'; -import { emitResizeEvent, readCellConfig } from './tool'; +import { emitResizeEvent, ISpectaSnapshotData, readCellConfig } from './tool'; import { ISignal, Signal } from '@lumino/signaling'; export class AppModel { @@ -223,6 +223,14 @@ export class AppModel { return rep; } + async saveSnapshotToMetadata(snapshot: ISpectaSnapshotData): Promise { + if (this.options.context) { + console.log('saveSnapshotToMetadata', this.options.context.path); + this.options.context.model.setMetadata('spectaSnapshot', snapshot); + await this.options.context.save(); + this.options.context.model.dirty = false; + } + } private _kernelReady = new PromiseDelegate(); private _notebookPanel?: NotebookPanel; diff --git a/src/specta_widget.ts b/src/specta_widget.ts index ad1c699..baf7d6b 100644 --- a/src/specta_widget.ts +++ b/src/specta_widget.ts @@ -10,7 +10,15 @@ import { ISpectaLayout, ISpectaLayoutRegistry } from './token'; -import { emitResizeEvent, hideAppLoadingIndicator, isSpectaApp } from './tool'; +import { + emitResizeEvent, + hideAppLoadingIndicator, + ISpectaSnapshotData, + isSpectaApp, + IWidgetManagerLike, + WIDGET_VIEW_MIMETYPE +} from './tool'; +import { SimplifiedOutputArea } from '@jupyterlab/outputarea'; export class AppWidget extends Panel { constructor(options: AppWidget.IOptions) { @@ -165,6 +173,79 @@ export class AppWidget extends Panel { this.removeSpinner(); } + async saveSnapshot(): Promise { + const notebook = this._model.context?.model.toJSON() as any; + if (notebook?.['metadata']?.['spectaSnapshot']) { + delete notebook['metadata']['spectaSnapshot']; + } + const snapshot: ISpectaSnapshotData = { + notebook: this._model.context?.model.toJSON(), + outputModels: {}, + widgetStates: null + }; + const allCodeCellOutputs = this._outputs.filter( + el => !el.info.hidden && el.info.cellModel?.cell_type === 'code' + ); + await Promise.all( + allCodeCellOutputs.map( + it => (it.cellOutput as SimplifiedOutputArea).future.done + ) + ); + for (const [idx, el] of this._outputs.entries()) { + if (el.info.hidden || el.info.cellModel?.cell_type !== 'code') { + continue; + } + const output = el.cellOutput as SimplifiedOutputArea; + + const outputModels = output.model.toJSON(); + snapshot.outputModels[idx] = outputModels; + if (!snapshot.widgetStates) { + for (let index = 0; index < outputModels.length; index++) { + const data = + (outputModels[index]?.data as Record) ?? {}; + const viewSpec = data[WIDGET_VIEW_MIMETYPE] as + | { + model_id?: string; + version_major?: number; + version_minor?: number; + } + | undefined; + if (!viewSpec?.model_id) { + continue; + } + + const outputWidget = output.widgets[index]; + if (!outputWidget) { + continue; + } + for (const child of outputWidget.children()) { + const renderer = child as Widget & { + mimeType?: string; + _manager?: { promise: Promise }; + }; + if (renderer.mimeType !== WIDGET_VIEW_MIMETYPE) { + continue; + } + if (renderer._manager) { + try { + const wm = await renderer._manager.promise; + snapshot.widgetStates = await wm.get_state(); + break; + } catch (e) { + continue; + } + } + } + if (snapshot.widgetStates) { + break; + } + } + } + } + console.log('done', snapshot); + await this._model.saveSnapshotToMetadata(snapshot); + } + protected onCloseRequest(msg: Message): void { this._model.dispose(); super.onCloseRequest(msg); diff --git a/src/tool.ts b/src/tool.ts index b4d3e0a..f7e7f6f 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -13,6 +13,7 @@ import { CommandRegistry } from '@lumino/commands'; import { NotebookGridWidgetFactory } from './document/factory'; import { PlainbNotebookModelFactory } from './document/plainb_factory'; import { SpectaWidgetFactory } from './specta_widget_factory'; + import { ISpectaAppConfig, ISpectaCellConfig, @@ -22,6 +23,7 @@ import { ISpectaUiSwitcher, ISpectaUrlFactory } from './token'; +import { PartialJSONValue } from '@lumino/coreutils'; export const PLAINB_PREFIX = 'ptjnb-'; @@ -453,3 +455,25 @@ export function openDocument( shell.add(widget, 'main'); } } + +export const WIDGET_STATE_MIMETYPE = + 'application/vnd.jupyter.widget-state+json'; +export const WIDGET_VIEW_MIMETYPE = 'application/vnd.jupyter.widget-view+json'; + +export interface IWidgetManagerState { + version_major: number; + version_minor: number; + state: { [modelId: string]: unknown }; +} + +export interface IWidgetManagerLike { + get_state(options?: { + drop_defaults?: boolean; + }): Promise; +} + +export interface ISpectaSnapshotData { + notebook?: PartialJSONValue; + outputModels: Record; + widgetStates: IWidgetManagerState | null; +} From bf4de7e4e9f941a24b5a41bbd65b1787a6b5e703 Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Sat, 15 Aug 2026 22:51:35 +0200 Subject: [PATCH 02/13] update example --- demo/files/blog.ipynb | 2033 +++++++++++++++++++++++++++++++++-------- src/specta_widget.ts | 3 +- 2 files changed, 1642 insertions(+), 394 deletions(-) diff --git a/demo/files/blog.ipynb b/demo/files/blog.ipynb index 71f6d7b..0f1c008 100644 --- a/demo/files/blog.ipynb +++ b/demo/files/blog.ipynb @@ -1,394 +1,5 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "a3abf27e", - "metadata": {}, - "source": [ - "# Data Visualization in Jupyter Notebooks using Apache Echarts\n" - ] - }, - { - "cell_type": "markdown", - "id": "21b59e76", - "metadata": { - "specta": { - "outputSize":"Full" - } - }, - "source": [ - "![Banner](./top.jpeg \"Banner\")\n" - ] - }, - { - "cell_type": "markdown", - "id": "bf93b954", - "metadata": {}, - "source": [ - "\n", - "\n", - "In the realm of data science and visualization, Jupyter Notebook has emerged as a powerful tool for data analysis and storytelling. Integrating interactive and aesthetically pleasing charts can significantly enhance the presentation of data insights." - ] - }, - { - "cell_type": "markdown", - "id": "a3826498", - "metadata": {}, - "source": [ - "[Apache Echarts](https://echarts.apache.org/en/index.html) is one of the most versatile libraries for creating interactive charts. This blog post explores how to leverage ipecharts, a new Python library that seamlessly integrates Echarts into Jupyter Notebooks, to craft stunning visualizations within your notebooks.\n", - "\n", - "*Disclaimer: I am the author of this library.*" - ] - }, - { - "cell_type": "markdown", - "id": "1a2ffcfd", - "metadata": {}, - "source": [ - "

• • •

" - ] - }, - { - "cell_type": "markdown", - "id": "e2569313", - "metadata": {}, - "source": [ - "## Motivation\n", - "\n", - "`ipecharts` is not the first attempt to make Echarts available on Jupyter Notebooks. pyecharts is a popular open-source library that allows you to create interactive charts in Python and supports both notebooks and standalone Python scripts." - ] - }, - { - "cell_type": "markdown", - "id": "330ee266", - "metadata": {}, - "source": [ - "While pyechartscan create charts in the notebooks, it does not use the Jupyter Widgets system but instead injects HTML code into the notebook to render the charts. This approach makes using pyecharts in other Jupyter applications or interacting with other widgets libraries harder." - ] - }, - { - "cell_type": "markdown", - "id": "6e320817", - "metadata": {}, - "source": [ - "On the other hand, ipecharts adopts the native way of creating interactive charts in Jupyter Notebooks by using Jupyter Widgets. It makes the created charts compatible with a wide range of tools and libraries in the Jupyter ecosystem." - ] - }, - { - "cell_type": "markdown", - "id": "250f141f", - "metadata": {}, - "source": [ - "

• • •

" - ] - }, - { - "cell_type": "markdown", - "id": "fc22d007", - "metadata": {}, - "source": [ - "## Getting started with ipecharts" - ] - }, - { - "cell_type": "markdown", - "id": "b05b1479", - "metadata": {}, - "source": [ - "### Installation\n", - "\n", - "`ipecharts` is available on PyPI and conda-forge:\n", - "\n", - "```bash\n", - "# Installing with pip\n", - "pip install ipecharts\n", - "\n", - "# Installing with conda\n", - "conda install -c conda-forge ipecharts\n", - "```\n", - "\n", - "It requires ipywidgets ≥8.0 and does not work with Jupyter Notebook <7 . More detailed documentation is available at Read the Docs. You can also try it live in this JupyterLite instance." - ] - }, - { - "cell_type": "markdown", - "id": "ce613931", - "metadata": {}, - "source": [ - "### Creating a simple line plot\n", - "\n", - "ipechart is a very slim wrapper outside of the Echarts Javascript library so translating the Javascript version of a chart into ipechartswidget is straightforward. Let’s begin with a basic line example from Echarts official documentation:\n", - "\n", - "```typescript\n", - "// Example from https://echarts.apache.org/examples/en/editor.html?c=line-simple&lang=ts\n", - "import * as echarts from 'echarts';\n", - "\n", - "type EChartsOption = echarts.EChartsOption;\n", - "\n", - "var chartDom = document.getElementById('main')!;\n", - "var myChart = echarts.init(chartDom);\n", - "var option: EChartsOption;\n", - "\n", - "option = {\n", - " xAxis: {\n", - " type: 'category',\n", - " data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']\n", - " },\n", - " yAxis: {\n", - " type: 'value'\n", - " },\n", - " series: [\n", - " {\n", - " data: [150, 230, 224, 218, 135, 147, 260],\n", - " type: 'line'\n", - " }\n", - " ]\n", - "};\n", - "\n", - "option && myChart.setOption(option);\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "7824f42f", - "metadata": {}, - "source": [ - "The entry point of a chart in ipecharts is the EchartWidget class:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3de1a807", - "metadata": { - "specta": { - "showOutput": "No", - "showSource": "Yes" - }, - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - } - }, - "outputs": [], - "source": [ - "from ipecharts import EChartsWidget\n", - "chart = EChartsWidget()" - ] - }, - { - "cell_type": "markdown", - "id": "503e0e22", - "metadata": {}, - "source": [ - "Just as in the Javascript example, we need to set the option of this chart. For all top-level keys of the Echarts option and the entries of series, ipecharts provides Python class counterparts with the same name. Here is the equivalent of the above option object defined with ipecharts classes:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d8741975", - "metadata": { - "specta": { - "showOutput": "No", - "showSource": "Yes" - }, - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - } - }, - "outputs": [], - "source": [ - "from ipecharts.option import Option, XAxis, YAxis\n", - "from ipecharts.option.series import Line\n", - "\n", - "xAxis = XAxis(\n", - " type=\"category\",\n", - " data=[\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"],\n", - ")\n", - "yAxis = YAxis(type=\"value\")\n", - "line = Line(data=[150, 230, 224, 218, 135, 147, 260])\n", - "\n", - "option = Option()\n", - "option.xAxis = xAxis\n", - "option.yAxis = yAxis\n", - "option.series = [line]" - ] - }, - { - "cell_type": "markdown", - "id": "c001b376", - "metadata": {}, - "source": [ - "All classes here are based on traitlets so you can initialize the instance by using keyword arguments or by setting the property values. Finally, updating the option value of our chart gives us the same chart as the Javascript \n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2b2c01c4", - "metadata": { - "specta": { - "showSource": "Yes", - "outputSize":"Big" - }, - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - } - }, - "outputs": [], - "source": [ - "chart.option = option\n", - "chart" - ] - }, - { - "cell_type": "markdown", - "id": "19dea3c1", - "metadata": {}, - "source": [ - "### Adding Interactivity" - ] - }, - { - "cell_type": "markdown", - "id": "1105f857", - "metadata": {}, - "source": [ - "By using traitlets to configure your chart, any change in the option properties will be applied to the chart automatically. We will use the Button widget of ipywidgets to change the line data dynamically.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a5fb539d", - "metadata": { - "specta": { - "showSource": "Yes" - }, - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - } - }, - "outputs": [], - "source": [ - "from ipywidgets.widgets import Button\n", - "from numpy.random import randint\n", - "\n", - "def update_line_data(b): \n", - " line.data = randint(0, 300, 7).tolist()\n", - "\n", - "button = Button(description=\"Generate data\")\n", - "button.on_click(update_line_data)\n", - "\n", - "display(button, chart)" - ] - }, - { - "cell_type": "markdown", - "id": "0e491acc", - "metadata": {}, - "source": [ - "In the on_click callback of the button, we update the data property of the line instance, the changed signal is propagated up to the top-level widget and the chart will be updated automatically." - ] - }, - { - "cell_type": "markdown", - "id": "86b1b92d", - "metadata": {}, - "source": [ - "### Creating charts without using traitlets configuration" - ] - }, - { - "cell_type": "markdown", - "id": "2e5acf08", - "metadata": {}, - "source": [ - "In many situations, we simply want to display the data without adding interactivity. For this use case, users can convert any option object used by a Javascript chart to a Python dictionary and pass it to the EchartRawWidget of ipecharts.\n", - "\n", - "Here is the equivalent of the Two Value-Axes in Polar example from the official Echarts documentation using EchartRawWidget:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "933a5a49", - "metadata": { - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - } - }, - "outputs": [], - "source": [ - "from ipecharts import EChartsRawWidget\n", - "import math\n", - "\n", - "data = []\n", - "for i in range(101):\n", - " theta = (i / 100) * 360\n", - " r = 5 * (1 + math.sin((theta / 180) * math.pi))\n", - " data.append([r, theta])\n", - "\n", - "option = {\n", - " \"title\": {\"text\": \"Two Value-Axes in Polar\"},\n", - " \"legend\": {\"data\": [\"line\"]},\n", - " \"polar\": {},\n", - " \"tooltip\": {\"trigger\": \"axis\", \"axisPointer\": {\"type\": \"cross\"}},\n", - " \"angleAxis\": {\"type\": \"value\", \"startAngle\": 0},\n", - " \"radiusAxis\": {},\n", - " \"series\": [\n", - " {\"coordinateSystem\": \"polar\", \"name\": \"line\", \"type\": \"line\", \"data\": data}\n", - " ],\n", - "}\n", - "EChartsRawWidget(option=option)" - ] - }, - { - "cell_type": "markdown", - "id": "213d1b6b", - "metadata": {}, - "source": [ - "## What’s next\n", - "\n", - "In this first version, I focused on generating the option configuration class to be able to translate the Javascript charts to the Python ones without too many changes. Echarts has a lot of other customizations in theming, managing maps, or chart animation… These aspects will be addressed in future releases.\n", - "\n", - "You can follow the development of this library on GitHub. Stay tuned and happy charting!" - ] - }, - { - "cell_type": "markdown", - "id": "b7129618", - "metadata": {}, - "source": [ - "## About the author\n", - "\n", - "Duc Trung Le is an open-source developer who works on this project in his free time." - ] - }, - { - "cell_type": "markdown", - "id": "1dd7aa92", - "metadata": {}, - "source": [] - } - ], - "metadata": { + "metadata": { "kernelspec": { "name": "xpython", "display_name": "Python 3.13 (XPython)", @@ -404,8 +15,1644 @@ "hideTopbar": "No", "slidesTheme": null, "defaultLayout": "article" + }, + "spectaSnapshot": { + "notebook": { + "metadata": { + "kernelspec": { + "name": "xpython", + "display_name": "Python 3.13 (XPython)", + "language": "python" + }, + "language_info": { + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "version": "3.13.1" + }, + "specta": { + "hideTopbar": "No", + "slidesTheme": null, + "defaultLayout": "article" + } + }, + "nbformat_minor": 5, + "nbformat": 4, + "cells": [ + { + "id": "a3abf27e", + "cell_type": "markdown", + "source": "# Data Visualization in Jupyter Notebooks using Apache Echarts\n", + "metadata": {} + }, + { + "id": "21b59e76", + "cell_type": "markdown", + "source": "![Banner](./top.jpeg \"Banner\")\n", + "metadata": { + "specta": { + "outputSize": "Full" + } + } + }, + { + "id": "bf93b954", + "cell_type": "markdown", + "source": "\n\nIn the realm of data science and visualization, Jupyter Notebook has emerged as a powerful tool for data analysis and storytelling. Integrating interactive and aesthetically pleasing charts can significantly enhance the presentation of data insights.", + "metadata": {} + }, + { + "id": "a3826498", + "cell_type": "markdown", + "source": "[Apache Echarts](https://echarts.apache.org/en/index.html) is one of the most versatile libraries for creating interactive charts. This blog post explores how to leverage ipecharts, a new Python library that seamlessly integrates Echarts into Jupyter Notebooks, to craft stunning visualizations within your notebooks.\n\n*Disclaimer: I am the author of this library.*", + "metadata": {} + }, + { + "id": "1a2ffcfd", + "cell_type": "markdown", + "source": "

• • •

", + "metadata": {} + }, + { + "id": "e2569313", + "cell_type": "markdown", + "source": "## Motivation\n\n`ipecharts` is not the first attempt to make Echarts available on Jupyter Notebooks. pyecharts is a popular open-source library that allows you to create interactive charts in Python and supports both notebooks and standalone Python scripts.", + "metadata": {} + }, + { + "id": "330ee266", + "cell_type": "markdown", + "source": "While pyechartscan create charts in the notebooks, it does not use the Jupyter Widgets system but instead injects HTML code into the notebook to render the charts. This approach makes using pyecharts in other Jupyter applications or interacting with other widgets libraries harder.", + "metadata": {} + }, + { + "id": "6e320817", + "cell_type": "markdown", + "source": "On the other hand, ipecharts adopts the native way of creating interactive charts in Jupyter Notebooks by using Jupyter Widgets. It makes the created charts compatible with a wide range of tools and libraries in the Jupyter ecosystem.", + "metadata": {} + }, + { + "id": "250f141f", + "cell_type": "markdown", + "source": "

• • •

", + "metadata": {} + }, + { + "id": "fc22d007", + "cell_type": "markdown", + "source": "## Getting started with ipecharts", + "metadata": {} + }, + { + "id": "b05b1479", + "cell_type": "markdown", + "source": "### Installation\n\n`ipecharts` is available on PyPI and conda-forge:\n\n```bash\n# Installing with pip\npip install ipecharts\n\n# Installing with conda\nconda install -c conda-forge ipecharts\n```\n\nIt requires ipywidgets ≥8.0 and does not work with Jupyter Notebook <7 . More detailed documentation is available at Read the Docs. You can also try it live in this JupyterLite instance.", + "metadata": {} + }, + { + "id": "ce613931", + "cell_type": "markdown", + "source": "### Creating a simple line plot\n\nipechart is a very slim wrapper outside of the Echarts Javascript library so translating the Javascript version of a chart into ipechartswidget is straightforward. Let’s begin with a basic line example from Echarts official documentation:\n\n```typescript\n// Example from https://echarts.apache.org/examples/en/editor.html?c=line-simple&lang=ts\nimport * as echarts from 'echarts';\n\ntype EChartsOption = echarts.EChartsOption;\n\nvar chartDom = document.getElementById('main')!;\nvar myChart = echarts.init(chartDom);\nvar option: EChartsOption;\n\noption = {\n xAxis: {\n type: 'category',\n data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']\n },\n yAxis: {\n type: 'value'\n },\n series: [\n {\n data: [150, 230, 224, 218, 135, 147, 260],\n type: 'line'\n }\n ]\n};\n\noption && myChart.setOption(option);\n```", + "metadata": {} + }, + { + "id": "7824f42f", + "cell_type": "markdown", + "source": "The entry point of a chart in ipecharts is the EchartWidget class:\n", + "metadata": {} + }, + { + "id": "3de1a807", + "cell_type": "code", + "source": "from ipecharts import EChartsWidget\nchart = EChartsWidget()", + "metadata": { + "specta": { + "showOutput": "No", + "showSource": "Yes" + }, + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + }, + "trusted": true + }, + "outputs": [], + "execution_count": null + }, + { + "id": "503e0e22", + "cell_type": "markdown", + "source": "Just as in the Javascript example, we need to set the option of this chart. For all top-level keys of the Echarts option and the entries of series, ipecharts provides Python class counterparts with the same name. Here is the equivalent of the above option object defined with ipecharts classes:\n", + "metadata": {} + }, + { + "id": "d8741975", + "cell_type": "code", + "source": "from ipecharts.option import Option, XAxis, YAxis\nfrom ipecharts.option.series import Line\n\nxAxis = XAxis(\n type=\"category\",\n data=[\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"],\n)\nyAxis = YAxis(type=\"value\")\nline = Line(data=[150, 230, 224, 218, 135, 147, 260])\n\noption = Option()\noption.xAxis = xAxis\noption.yAxis = yAxis\noption.series = [line]", + "metadata": { + "specta": { + "showOutput": "No", + "showSource": "Yes" + }, + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + }, + "trusted": true + }, + "outputs": [], + "execution_count": null + }, + { + "id": "c001b376", + "cell_type": "markdown", + "source": "All classes here are based on traitlets so you can initialize the instance by using keyword arguments or by setting the property values. Finally, updating the option value of our chart gives us the same chart as the Javascript \n", + "metadata": {} + }, + { + "id": "2b2c01c4", + "cell_type": "code", + "source": "chart.option = option\nchart", + "metadata": { + "specta": { + "showSource": "Yes", + "outputSize": "Big" + }, + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + }, + "trusted": true + }, + "outputs": [], + "execution_count": null + }, + { + "id": "19dea3c1", + "cell_type": "markdown", + "source": "### Adding Interactivity", + "metadata": {} + }, + { + "id": "1105f857", + "cell_type": "markdown", + "source": "By using traitlets to configure your chart, any change in the option properties will be applied to the chart automatically. We will use the Button widget of ipywidgets to change the line data dynamically.\n", + "metadata": {} + }, + { + "id": "a5fb539d", + "cell_type": "code", + "source": "from ipywidgets.widgets import Button\nfrom numpy.random import randint\n\ndef update_line_data(b): \n line.data = randint(0, 300, 7).tolist()\n\nbutton = Button(description=\"Generate data\")\nbutton.on_click(update_line_data)\n\ndisplay(button, chart)", + "metadata": { + "specta": { + "showSource": "Yes" + }, + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + }, + "trusted": true + }, + "outputs": [], + "execution_count": null + }, + { + "id": "0e491acc", + "cell_type": "markdown", + "source": "In the on_click callback of the button, we update the data property of the line instance, the changed signal is propagated up to the top-level widget and the chart will be updated automatically.", + "metadata": {} + }, + { + "id": "86b1b92d", + "cell_type": "markdown", + "source": "### Creating charts without using traitlets configuration", + "metadata": {} + }, + { + "id": "2e5acf08", + "cell_type": "markdown", + "source": "In many situations, we simply want to display the data without adding interactivity. For this use case, users can convert any option object used by a Javascript chart to a Python dictionary and pass it to the EchartRawWidget of ipecharts.\n\nHere is the equivalent of the Two Value-Axes in Polar example from the official Echarts documentation using EchartRawWidget:", + "metadata": {} + }, + { + "id": "933a5a49", + "cell_type": "code", + "source": "from ipecharts import EChartsRawWidget\nimport math\n\ndata = []\nfor i in range(101):\n theta = (i / 100) * 360\n r = 5 * (1 + math.sin((theta / 180) * math.pi))\n data.append([r, theta])\n\noption = {\n \"title\": {\"text\": \"Two Value-Axes in Polar\"},\n \"legend\": {\"data\": [\"line\"]},\n \"polar\": {},\n \"tooltip\": {\"trigger\": \"axis\", \"axisPointer\": {\"type\": \"cross\"}},\n \"angleAxis\": {\"type\": \"value\", \"startAngle\": 0},\n \"radiusAxis\": {},\n \"series\": [\n {\"coordinateSystem\": \"polar\", \"name\": \"line\", \"type\": \"line\", \"data\": data}\n ],\n}\nEChartsRawWidget(option=option)", + "metadata": { + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + }, + "trusted": true + }, + "outputs": [], + "execution_count": null + }, + { + "id": "213d1b6b", + "cell_type": "markdown", + "source": "## What’s next\n\nIn this first version, I focused on generating the option configuration class to be able to translate the Javascript charts to the Python ones without too many changes. Echarts has a lot of other customizations in theming, managing maps, or chart animation… These aspects will be addressed in future releases.\n\nYou can follow the development of this library on GitHub. Stay tuned and happy charting!", + "metadata": {} + }, + { + "id": "b7129618", + "cell_type": "markdown", + "source": "## About the author\n\nDuc Trung Le is an open-source developer who works on this project in his free time.", + "metadata": {} + }, + { + "id": "1dd7aa92", + "cell_type": "markdown", + "source": "", + "metadata": {} + } + ] + }, + "outputModels": { + "13": [], + "15": [], + "17": [ + { + "execution_count": 3, + "output_type": "execute_result", + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8ef8a9c0b511443095cd141ed6825f00", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": "EChartsWidget(option=Option(angleAxis=None, aria=None, axisPointer=None, brush=None, calendar=None, dataset=No…" + }, + "metadata": {} + } + ], + "20": [ + { + "output_type": "display_data", + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "fac759f0dffc4ea0a4e521b41967fcc2", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": "Button(description='Generate data', style=ButtonStyle())" + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8ef8a9c0b511443095cd141ed6825f00", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": "EChartsWidget(option=Option(angleAxis=None, aria=None, axisPointer=None, brush=None, calendar=None, dataset=No…" + }, + "metadata": {} + } + ], + "24": [ + { + "execution_count": 5, + "output_type": "execute_result", + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "de46f5c303604f4c9ab2e2cabe2104c5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": "EChartsRawWidget(option={'title': {'text': 'Two Value-Axes in Polar'}, 'legend': {'data': ['line']}, 'polar': …" + }, + "metadata": {} + } + ] + }, + "widgetStates": { + "version_major": 2, + "version_minor": 0, + "state": { + "d37c3cea873f476ab913515d32aa1c55": { + "model_name": "LayoutModel", + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_name": "LayoutModel", + "_model_module_version": "2.0.0", + "_view_module": "@jupyter-widgets/base", + "_view_name": "LayoutView", + "_view_module_version": "2.0.0", + "_view_count": null, + "align_content": null, + "align_items": null, + "align_self": null, + "border_top": null, + "border_right": null, + "border_bottom": null, + "border_left": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "overflow": null, + "order": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null, + "object_fit": null, + "object_position": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_gap": null, + "grid_template_rows": null, + "grid_template_columns": null, + "grid_template_areas": null, + "grid_row": null, + "grid_column": null, + "grid_area": null + } + }, + "ba9ae00c016648e498bb3b7b916017a7": { + "model_name": "OptionModel", + "model_module": "ipecharts", + "model_module_version": "^1.3.1", + "state": { + "_model_module": "ipecharts", + "_model_name": "OptionModel", + "_model_module_version": "^1.3.1", + "_view_module": "ipecharts", + "_view_name": null, + "_view_module_version": "^1.3.1", + "_view_count": null, + "title": null, + "legend": null, + "grid": null, + "xAxis": null, + "yAxis": null, + "polar": null, + "radiusAxis": null, + "angleAxis": null, + "radar": null, + "tooltip": null, + "axisPointer": null, + "toolbox": null, + "brush": null, + "geo": null, + "parallel": null, + "parallelAxis": null, + "singleAxis": null, + "timeline": null, + "graphic": null, + "calendar": null, + "matrix": null, + "thumbnail": null, + "dataset": null, + "aria": null, + "color": null, + "textStyle": null, + "stateAnimation": null, + "options": null, + "globe": null, + "geo3D": null, + "mapbox3D": null, + "grid3D": null, + "xAxis3D": null, + "yAxis3D": null, + "zAxis3D": null, + "series": null, + "animation": null, + "animationDelay": null, + "animationDelayUpdate": null, + "animationDuration": null, + "animationDurationUpdate": null, + "animationEasing": null, + "animationEasingUpdate": null, + "animationThreshold": null, + "backgroundColor": null, + "blendMode": null, + "darkMode": null, + "dataZoom": null, + "hoverLayerThreshold": null, + "media": null, + "richInheritPlainLabel": null, + "unknown_attrs": null, + "useUTC": null, + "visualMap": null + } + }, + "8ef8a9c0b511443095cd141ed6825f00": { + "model_name": "EChartsWidgetModel", + "model_module": "ipecharts", + "model_module_version": "^1.3.1", + "state": { + "_model_module": "ipecharts", + "_model_name": "EChartsWidgetModel", + "_model_module_version": "^1.3.1", + "_view_module": "ipecharts", + "_view_name": "EChartsWidgetView", + "_view_module_version": "^1.3.1", + "_view_count": null, + "_dom_classes": [], + "tabbable": null, + "tooltip": null, + "option": "IPY_MODEL_3c7a4305598942668441673e8f27e6e5", + "style": {}, + "theme": null, + "device_pixel_ratio": 0, + "renderer": "canvas", + "use_dirty_rect": false, + "use_coarse_pointer": false, + "pointer_size": null, + "width": "auto", + "height": "auto", + "locale": "EN", + "layout": "IPY_MODEL_d37c3cea873f476ab913515d32aa1c55" + } + }, + "8239b0d0e21546ad94621863fb75223c": { + "model_name": "XAxisModel", + "model_module": "ipecharts", + "model_module_version": "^1.3.1", + "state": { + "_model_module": "ipecharts", + "_model_name": "XAxisModel", + "_model_module_version": "^1.3.1", + "_view_module": "ipecharts", + "_view_name": null, + "_view_module_version": "^1.3.1", + "_view_count": null, + "alignTicks": null, + "animation": null, + "animationDelay": null, + "animationDelayUpdate": null, + "animationDuration": null, + "animationDurationUpdate": null, + "animationEasing": null, + "animationEasingUpdate": null, + "animationThreshold": null, + "axisLabel": null, + "axisLine": null, + "axisPointer": null, + "axisTick": null, + "boundaryGap": null, + "breakArea": null, + "breakLabelLayout": null, + "breaks": null, + "data": [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun" + ], + "gridIndex": null, + "id": null, + "interval": null, + "inverse": null, + "jitter": null, + "jitterMargin": null, + "jitterOverlap": null, + "logBase": null, + "max": null, + "maxInterval": null, + "min": null, + "minInterval": null, + "minorSplitLine": null, + "minorTick": null, + "name": null, + "nameGap": null, + "nameLocation": null, + "nameMoveOverlap": null, + "nameRotate": null, + "nameTextStyle": null, + "nameTruncate": null, + "offset": null, + "position": null, + "scale": null, + "show": null, + "silent": null, + "splitArea": null, + "splitLine": null, + "splitNumber": null, + "startValue": null, + "tooltip": null, + "triggerEvent": null, + "type": "category", + "unknown_attrs": null, + "z": null, + "zlevel": null + } + }, + "80096350f0824f4fbd4af2e262bc921c": { + "model_name": "YAxisModel", + "model_module": "ipecharts", + "model_module_version": "^1.3.1", + "state": { + "_model_module": "ipecharts", + "_model_name": "YAxisModel", + "_model_module_version": "^1.3.1", + "_view_module": "ipecharts", + "_view_name": null, + "_view_module_version": "^1.3.1", + "_view_count": null, + "alignTicks": null, + "animation": null, + "animationDelay": null, + "animationDelayUpdate": null, + "animationDuration": null, + "animationDurationUpdate": null, + "animationEasing": null, + "animationEasingUpdate": null, + "animationThreshold": null, + "axisLabel": null, + "axisLine": null, + "axisPointer": null, + "axisTick": null, + "boundaryGap": null, + "breakArea": null, + "breakLabelLayout": null, + "breaks": null, + "data": null, + "gridIndex": null, + "id": null, + "interval": null, + "inverse": null, + "jitter": null, + "jitterMargin": null, + "jitterOverlap": null, + "logBase": null, + "max": null, + "maxInterval": null, + "min": null, + "minInterval": null, + "minorSplitLine": null, + "minorTick": null, + "name": null, + "nameGap": null, + "nameLocation": null, + "nameMoveOverlap": null, + "nameRotate": null, + "nameTextStyle": null, + "nameTruncate": null, + "offset": null, + "position": null, + "scale": null, + "show": null, + "silent": null, + "splitArea": null, + "splitLine": null, + "splitNumber": null, + "startValue": null, + "tooltip": null, + "triggerEvent": null, + "type": "value", + "unknown_attrs": null, + "z": null, + "zlevel": null + } + }, + "0156a999c271416896382a081c413928": { + "model_name": "LineModel", + "model_module": "ipecharts", + "model_module_version": "^1.3.1", + "state": { + "_model_module": "ipecharts", + "_model_name": "LineModel", + "_model_module_version": "^1.3.1", + "_view_module": "ipecharts", + "_view_name": null, + "_view_module_version": "^1.3.1", + "_view_count": null, + "animation": null, + "animationDelay": null, + "animationDelayUpdate": null, + "animationDuration": null, + "animationDurationUpdate": null, + "animationEasing": null, + "animationEasingUpdate": null, + "animationThreshold": null, + "areaStyle": null, + "blur": null, + "clip": null, + "colorBy": null, + "connectNulls": null, + "coord": null, + "coordinateSystem": null, + "coordinateSystemUsage": null, + "cursor": null, + "data": [ + 150, + 230, + 224, + 218, + 135, + 147, + 260 + ], + "dataGroupId": null, + "datasetIndex": null, + "dimensions": null, + "emphasis": null, + "encode": null, + "endLabel": null, + "id": null, + "itemStyle": null, + "label": null, + "labelLayout": null, + "labelLine": null, + "legendHoverLink": null, + "lineStyle": null, + "markArea": null, + "markLine": null, + "markPoint": null, + "name": null, + "polarId": null, + "polarIndex": null, + "sampling": null, + "select": null, + "selectedMode": null, + "seriesLayoutBy": null, + "showAllSymbol": null, + "showSymbol": null, + "silent": null, + "singleAxisId": null, + "singleAxisIndex": null, + "smooth": null, + "smoothMonotone": null, + "stack": null, + "stackOrder": null, + "stackStrategy": null, + "step": null, + "symbol": null, + "symbolKeepAspect": null, + "symbolOffset": null, + "symbolRotate": null, + "symbolSize": null, + "tooltip": null, + "triggerLineEvent": null, + "type": "line", + "universalTransition": null, + "unknown_attrs": null, + "xAxisId": null, + "xAxisIndex": null, + "yAxisId": null, + "yAxisIndex": null, + "z": null, + "zlevel": null + } + }, + "3c7a4305598942668441673e8f27e6e5": { + "model_name": "OptionModel", + "model_module": "ipecharts", + "model_module_version": "^1.3.1", + "state": { + "_model_module": "ipecharts", + "_model_name": "OptionModel", + "_model_module_version": "^1.3.1", + "_view_module": "ipecharts", + "_view_name": null, + "_view_module_version": "^1.3.1", + "_view_count": null, + "title": null, + "legend": null, + "grid": null, + "xAxis": "IPY_MODEL_8239b0d0e21546ad94621863fb75223c", + "yAxis": "IPY_MODEL_80096350f0824f4fbd4af2e262bc921c", + "polar": null, + "radiusAxis": null, + "angleAxis": null, + "radar": null, + "tooltip": null, + "axisPointer": null, + "toolbox": null, + "brush": null, + "geo": null, + "parallel": null, + "parallelAxis": null, + "singleAxis": null, + "timeline": null, + "graphic": null, + "calendar": null, + "matrix": null, + "thumbnail": null, + "dataset": null, + "aria": null, + "color": null, + "textStyle": null, + "stateAnimation": null, + "options": null, + "globe": null, + "geo3D": null, + "mapbox3D": null, + "grid3D": null, + "xAxis3D": null, + "yAxis3D": null, + "zAxis3D": null, + "series": [ + "IPY_MODEL_0156a999c271416896382a081c413928" + ], + "animation": null, + "animationDelay": null, + "animationDelayUpdate": null, + "animationDuration": null, + "animationDurationUpdate": null, + "animationEasing": null, + "animationEasingUpdate": null, + "animationThreshold": null, + "backgroundColor": null, + "blendMode": null, + "darkMode": null, + "dataZoom": null, + "hoverLayerThreshold": null, + "media": null, + "richInheritPlainLabel": null, + "unknown_attrs": null, + "useUTC": null, + "visualMap": null + } + }, + "f23f2211a78245f3bd178f0c140b2faa": { + "model_name": "LayoutModel", + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_name": "LayoutModel", + "_model_module_version": "2.0.0", + "_view_module": "@jupyter-widgets/base", + "_view_name": "LayoutView", + "_view_module_version": "2.0.0", + "_view_count": null, + "align_content": null, + "align_items": null, + "align_self": null, + "border_top": null, + "border_right": null, + "border_bottom": null, + "border_left": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "overflow": null, + "order": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null, + "object_fit": null, + "object_position": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_gap": null, + "grid_template_rows": null, + "grid_template_columns": null, + "grid_template_areas": null, + "grid_row": null, + "grid_column": null, + "grid_area": null + } + }, + "a4823901725e4c9a89eff2129dd039ce": { + "model_name": "ButtonStyleModel", + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_name": "ButtonStyleModel", + "_model_module_version": "2.0.0", + "_view_module": "@jupyter-widgets/base", + "_view_name": "StyleView", + "_view_module_version": "2.0.0", + "_view_count": null, + "button_color": null, + "font_family": null, + "font_size": null, + "font_style": null, + "font_variant": null, + "font_weight": null, + "text_color": null, + "text_decoration": null + } + }, + "fac759f0dffc4ea0a4e521b41967fcc2": { + "model_name": "ButtonModel", + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_name": "ButtonModel", + "_model_module_version": "2.0.0", + "_view_module": "@jupyter-widgets/controls", + "_view_name": "ButtonView", + "_view_module_version": "2.0.0", + "_view_count": null, + "_dom_classes": [], + "tabbable": null, + "tooltip": null, + "description": "Generate data", + "disabled": false, + "icon": "", + "button_style": "", + "style": "IPY_MODEL_a4823901725e4c9a89eff2129dd039ce", + "layout": "IPY_MODEL_f23f2211a78245f3bd178f0c140b2faa" + } + }, + "aeaccb172d914c56bac3ddf013f3cb95": { + "model_name": "LayoutModel", + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_name": "LayoutModel", + "_model_module_version": "2.0.0", + "_view_module": "@jupyter-widgets/base", + "_view_name": "LayoutView", + "_view_module_version": "2.0.0", + "_view_count": null, + "align_content": null, + "align_items": null, + "align_self": null, + "border_top": null, + "border_right": null, + "border_bottom": null, + "border_left": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "overflow": null, + "order": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null, + "object_fit": null, + "object_position": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_gap": null, + "grid_template_rows": null, + "grid_template_columns": null, + "grid_template_areas": null, + "grid_row": null, + "grid_column": null, + "grid_area": null + } + }, + "de46f5c303604f4c9ab2e2cabe2104c5": { + "model_name": "EChartsRawWidgetModel", + "model_module": "ipecharts", + "model_module_version": "^1.3.1", + "state": { + "_model_module": "ipecharts", + "_model_name": "EChartsRawWidgetModel", + "_model_module_version": "^1.3.1", + "_view_module": "ipecharts", + "_view_name": "EChartsRawWidgetView", + "_view_module_version": "^1.3.1", + "_view_count": null, + "_dom_classes": [], + "tabbable": null, + "tooltip": null, + "option": { + "angleAxis": { + "startAngle": 0, + "type": "value" + }, + "legend": { + "data": [ + "line" + ] + }, + "polar": {}, + "radiusAxis": {}, + "series": [ + { + "coordinateSystem": "polar", + "data": [ + [ + 5, + 0 + ], + [ + 5.313952597646567, + 3.6 + ], + [ + 5.626666167821521, + 7.2 + ], + [ + 5.936906572928623, + 10.799999999999999 + ], + [ + 6.2434494358242745, + 14.4 + ], + [ + 6.545084971874737, + 18 + ], + [ + 6.84062276342339, + 21.599999999999998 + ], + [ + 7.1288964578253635, + 25.200000000000003 + ], + [ + 7.408768370508576, + 28.8 + ], + [ + 7.679133974894983, + 32.4 + ], + [ + 7.938926261462366, + 36 + ], + [ + 8.18711994874345, + 39.6 + ], + [ + 8.422735529643443, + 43.199999999999996 + ], + [ + 8.644843137107058, + 46.800000000000004 + ], + [ + 8.852566213878946, + 50.400000000000006 + ], + [ + 9.045084971874736, + 54 + ], + [ + 9.221639627510076, + 57.6 + ], + [ + 9.381533400219318, + 61.2 + ], + [ + 9.524135262330098, + 64.8 + ], + [ + 9.648882429441256, + 68.4 + ], + [ + 9.755282581475768, + 72 + ], + [ + 9.842915805643155, + 75.6 + ], + [ + 9.911436253643442, + 79.2 + ], + [ + 9.96057350657239, + 82.8 + ], + [ + 9.990133642141357, + 86.39999999999999 + ], + [ + 10, + 90 + ], + [ + 9.990133642141357, + 93.60000000000001 + ], + [ + 9.96057350657239, + 97.2 + ], + [ + 9.911436253643442, + 100.80000000000001 + ], + [ + 9.842915805643155, + 104.39999999999999 + ], + [ + 9.755282581475768, + 108 + ], + [ + 9.648882429441256, + 111.6 + ], + [ + 9.524135262330098, + 115.2 + ], + [ + 9.381533400219318, + 118.80000000000001 + ], + [ + 9.221639627510074, + 122.4 + ], + [ + 9.045084971874736, + 125.99999999999999 + ], + [ + 8.852566213878946, + 129.6 + ], + [ + 8.644843137107056, + 133.2 + ], + [ + 8.422735529643443, + 136.8 + ], + [ + 8.18711994874345, + 140.4 + ], + [ + 7.938926261462367, + 144 + ], + [ + 7.6791339748949845, + 147.6 + ], + [ + 7.408768370508579, + 151.2 + ], + [ + 7.128896457825363, + 154.8 + ], + [ + 6.840622763423391, + 158.4 + ], + [ + 6.545084971874737, + 162 + ], + [ + 6.243449435824276, + 165.6 + ], + [ + 5.936906572928625, + 169.2 + ], + [ + 5.626666167821525, + 172.79999999999998 + ], + [ + 5.313952597646567, + 176.4 + ], + [ + 5.000000000000001, + 180 + ], + [ + 4.6860474023534335, + 183.6 + ], + [ + 4.373333832178479, + 187.20000000000002 + ], + [ + 4.063093427071376, + 190.8 + ], + [ + 3.7565505641757246, + 194.4 + ], + [ + 3.454915028125262, + 198.00000000000003 + ], + [ + 3.159377236576608, + 201.60000000000002 + ], + [ + 2.871103542174639, + 205.2 + ], + [ + 2.591231629491425, + 208.79999999999998 + ], + [ + 2.3208660251050177, + 212.39999999999998 + ], + [ + 2.061073738537635, + 216 + ], + [ + 1.8128800512565517, + 219.6 + ], + [ + 1.5772644703565564, + 223.2 + ], + [ + 1.3551568628929433, + 226.8 + ], + [ + 1.147433786121053, + 230.4 + ], + [ + 0.9549150281252633, + 234 + ], + [ + 0.7783603724899235, + 237.60000000000002 + ], + [ + 0.6184665997806821, + 241.20000000000002 + ], + [ + 0.475864737669901, + 244.8 + ], + [ + 0.3511175705587444, + 248.39999999999998 + ], + [ + 0.24471741852423234, + 251.99999999999997 + ], + [ + 0.15708419435684517, + 255.6 + ], + [ + 0.0885637463565564, + 259.2 + ], + [ + 0.039426493427611176, + 262.8 + ], + [ + 0.009866357858642205, + 266.4 + ], + [ + 0, + 270 + ], + [ + 0.009866357858642205, + 273.6 + ], + [ + 0.03942649342761062, + 277.2 + ], + [ + 0.0885637463565564, + 280.8 + ], + [ + 0.15708419435684573, + 284.40000000000003 + ], + [ + 0.2447174185242318, + 288 + ], + [ + 0.3511175705587438, + 291.6 + ], + [ + 0.47586473766990045, + 295.2 + ], + [ + 0.6184665997806832, + 298.8 + ], + [ + 0.7783603724899224, + 302.4 + ], + [ + 0.9549150281252622, + 306 + ], + [ + 1.1474337861210548, + 309.6 + ], + [ + 1.3551568628929422, + 313.2 + ], + [ + 1.5772644703565553, + 316.8 + ], + [ + 1.8128800512565486, + 320.4 + ], + [ + 2.061073738537633, + 324 + ], + [ + 2.320866025105018, + 327.6 + ], + [ + 2.5912316294914195, + 331.2 + ], + [ + 2.871103542174639, + 334.8 + ], + [ + 3.1593772365766064, + 338.4 + ], + [ + 3.454915028125262, + 342 + ], + [ + 3.7565505641757193, + 345.59999999999997 + ], + [ + 4.063093427071377, + 349.2 + ], + [ + 4.373333832178476, + 352.8 + ], + [ + 4.6860474023534335, + 356.4 + ], + [ + 4.999999999999999, + 360 + ] + ], + "name": "line", + "type": "line" + } + ], + "title": { + "text": "Two Value-Axes in Polar" + }, + "tooltip": { + "axisPointer": { + "type": "cross" + }, + "trigger": "axis" + } + }, + "style": {}, + "theme": null, + "device_pixel_ratio": 0, + "renderer": "canvas", + "use_dirty_rect": false, + "use_coarse_pointer": false, + "pointer_size": null, + "width": "auto", + "height": "auto", + "locale": "EN", + "layout": "IPY_MODEL_aeaccb172d914c56bac3ddf013f3cb95" + } + } + } + } + } + }, + "nbformat_minor": 5, + "nbformat": 4, + "cells": [ + { + "id": "a3abf27e", + "cell_type": "markdown", + "source": "# Data Visualization in Jupyter Notebooks using Apache Echarts\n", + "metadata": {} + }, + { + "id": "21b59e76", + "cell_type": "markdown", + "source": "![Banner](./top.jpeg \"Banner\")\n", + "metadata": { + "specta": { + "outputSize": "Full" + } + } + }, + { + "id": "bf93b954", + "cell_type": "markdown", + "source": "\n\nIn the realm of data science and visualization, Jupyter Notebook has emerged as a powerful tool for data analysis and storytelling. Integrating interactive and aesthetically pleasing charts can significantly enhance the presentation of data insights.", + "metadata": {} + }, + { + "id": "a3826498", + "cell_type": "markdown", + "source": "[Apache Echarts](https://echarts.apache.org/en/index.html) is one of the most versatile libraries for creating interactive charts. This blog post explores how to leverage ipecharts, a new Python library that seamlessly integrates Echarts into Jupyter Notebooks, to craft stunning visualizations within your notebooks.\n\n*Disclaimer: I am the author of this library.*", + "metadata": {} + }, + { + "id": "1a2ffcfd", + "cell_type": "markdown", + "source": "

• • •

", + "metadata": {} + }, + { + "id": "e2569313", + "cell_type": "markdown", + "source": "## Motivation\n\n`ipecharts` is not the first attempt to make Echarts available on Jupyter Notebooks. pyecharts is a popular open-source library that allows you to create interactive charts in Python and supports both notebooks and standalone Python scripts.", + "metadata": {} + }, + { + "id": "330ee266", + "cell_type": "markdown", + "source": "While pyechartscan create charts in the notebooks, it does not use the Jupyter Widgets system but instead injects HTML code into the notebook to render the charts. This approach makes using pyecharts in other Jupyter applications or interacting with other widgets libraries harder.", + "metadata": {} + }, + { + "id": "6e320817", + "cell_type": "markdown", + "source": "On the other hand, ipecharts adopts the native way of creating interactive charts in Jupyter Notebooks by using Jupyter Widgets. It makes the created charts compatible with a wide range of tools and libraries in the Jupyter ecosystem.", + "metadata": {} + }, + { + "id": "250f141f", + "cell_type": "markdown", + "source": "

• • •

", + "metadata": {} + }, + { + "id": "fc22d007", + "cell_type": "markdown", + "source": "## Getting started with ipecharts", + "metadata": {} + }, + { + "id": "b05b1479", + "cell_type": "markdown", + "source": "### Installation\n\n`ipecharts` is available on PyPI and conda-forge:\n\n```bash\n# Installing with pip\npip install ipecharts\n\n# Installing with conda\nconda install -c conda-forge ipecharts\n```\n\nIt requires ipywidgets ≥8.0 and does not work with Jupyter Notebook <7 . More detailed documentation is available at Read the Docs. You can also try it live in this JupyterLite instance.", + "metadata": {} + }, + { + "id": "ce613931", + "cell_type": "markdown", + "source": "### Creating a simple line plot\n\nipechart is a very slim wrapper outside of the Echarts Javascript library so translating the Javascript version of a chart into ipechartswidget is straightforward. Let’s begin with a basic line example from Echarts official documentation:\n\n```typescript\n// Example from https://echarts.apache.org/examples/en/editor.html?c=line-simple&lang=ts\nimport * as echarts from 'echarts';\n\ntype EChartsOption = echarts.EChartsOption;\n\nvar chartDom = document.getElementById('main')!;\nvar myChart = echarts.init(chartDom);\nvar option: EChartsOption;\n\noption = {\n xAxis: {\n type: 'category',\n data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']\n },\n yAxis: {\n type: 'value'\n },\n series: [\n {\n data: [150, 230, 224, 218, 135, 147, 260],\n type: 'line'\n }\n ]\n};\n\noption && myChart.setOption(option);\n```", + "metadata": {} + }, + { + "id": "7824f42f", + "cell_type": "markdown", + "source": "The entry point of a chart in ipecharts is the EchartWidget class:\n", + "metadata": {} + }, + { + "id": "3de1a807", + "cell_type": "code", + "source": "from ipecharts import EChartsWidget\nchart = EChartsWidget()", + "metadata": { + "specta": { + "showOutput": "No", + "showSource": "Yes" + }, + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + }, + "trusted": true + }, + "outputs": [], + "execution_count": null + }, + { + "id": "503e0e22", + "cell_type": "markdown", + "source": "Just as in the Javascript example, we need to set the option of this chart. For all top-level keys of the Echarts option and the entries of series, ipecharts provides Python class counterparts with the same name. Here is the equivalent of the above option object defined with ipecharts classes:\n", + "metadata": {} + }, + { + "id": "d8741975", + "cell_type": "code", + "source": "from ipecharts.option import Option, XAxis, YAxis\nfrom ipecharts.option.series import Line\n\nxAxis = XAxis(\n type=\"category\",\n data=[\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"],\n)\nyAxis = YAxis(type=\"value\")\nline = Line(data=[150, 230, 224, 218, 135, 147, 260])\n\noption = Option()\noption.xAxis = xAxis\noption.yAxis = yAxis\noption.series = [line]", + "metadata": { + "specta": { + "showOutput": "No", + "showSource": "Yes" + }, + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + }, + "trusted": true + }, + "outputs": [], + "execution_count": null + }, + { + "id": "c001b376", + "cell_type": "markdown", + "source": "All classes here are based on traitlets so you can initialize the instance by using keyword arguments or by setting the property values. Finally, updating the option value of our chart gives us the same chart as the Javascript \n", + "metadata": {} + }, + { + "id": "2b2c01c4", + "cell_type": "code", + "source": "chart.option = option\nchart", + "metadata": { + "specta": { + "showSource": "Yes", + "outputSize": "Big" + }, + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + }, + "trusted": true + }, + "outputs": [], + "execution_count": null + }, + { + "id": "19dea3c1", + "cell_type": "markdown", + "source": "### Adding Interactivity", + "metadata": {} + }, + { + "id": "1105f857", + "cell_type": "markdown", + "source": "By using traitlets to configure your chart, any change in the option properties will be applied to the chart automatically. We will use the Button widget of ipywidgets to change the line data dynamically.\n", + "metadata": {} + }, + { + "id": "a5fb539d", + "cell_type": "code", + "source": "from ipywidgets.widgets import Button\nfrom numpy.random import randint\n\ndef update_line_data(b): \n line.data = randint(0, 300, 7).tolist()\n\nbutton = Button(description=\"Generate data\")\nbutton.on_click(update_line_data)\n\ndisplay(button, chart)", + "metadata": { + "specta": { + "showSource": "Yes" + }, + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + }, + "trusted": true + }, + "outputs": [], + "execution_count": null + }, + { + "id": "0e491acc", + "cell_type": "markdown", + "source": "In the on_click callback of the button, we update the data property of the line instance, the changed signal is propagated up to the top-level widget and the chart will be updated automatically.", + "metadata": {} + }, + { + "id": "86b1b92d", + "cell_type": "markdown", + "source": "### Creating charts without using traitlets configuration", + "metadata": {} + }, + { + "id": "2e5acf08", + "cell_type": "markdown", + "source": "In many situations, we simply want to display the data without adding interactivity. For this use case, users can convert any option object used by a Javascript chart to a Python dictionary and pass it to the EchartRawWidget of ipecharts.\n\nHere is the equivalent of the Two Value-Axes in Polar example from the official Echarts documentation using EchartRawWidget:", + "metadata": {} + }, + { + "id": "933a5a49", + "cell_type": "code", + "source": "from ipecharts import EChartsRawWidget\nimport math\n\ndata = []\nfor i in range(101):\n theta = (i / 100) * 360\n r = 5 * (1 + math.sin((theta / 180) * math.pi))\n data.append([r, theta])\n\noption = {\n \"title\": {\"text\": \"Two Value-Axes in Polar\"},\n \"legend\": {\"data\": [\"line\"]},\n \"polar\": {},\n \"tooltip\": {\"trigger\": \"axis\", \"axisPointer\": {\"type\": \"cross\"}},\n \"angleAxis\": {\"type\": \"value\", \"startAngle\": 0},\n \"radiusAxis\": {},\n \"series\": [\n {\"coordinateSystem\": \"polar\", \"name\": \"line\", \"type\": \"line\", \"data\": data}\n ],\n}\nEChartsRawWidget(option=option)", + "metadata": { + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + }, + "trusted": true + }, + "outputs": [], + "execution_count": null + }, + { + "id": "213d1b6b", + "cell_type": "markdown", + "source": "## What’s next\n\nIn this first version, I focused on generating the option configuration class to be able to translate the Javascript charts to the Python ones without too many changes. Echarts has a lot of other customizations in theming, managing maps, or chart animation… These aspects will be addressed in future releases.\n\nYou can follow the development of this library on GitHub. Stay tuned and happy charting!", + "metadata": {} + }, + { + "id": "b7129618", + "cell_type": "markdown", + "source": "## About the author\n\nDuc Trung Le is an open-source developer who works on this project in his free time.", + "metadata": {} + }, + { + "id": "1dd7aa92", + "cell_type": "markdown", + "source": "", + "metadata": {} } - }, - "nbformat": 4, - "nbformat_minor": 5 + ] } diff --git a/src/specta_widget.ts b/src/specta_widget.ts index baf7d6b..255d347 100644 --- a/src/specta_widget.ts +++ b/src/specta_widget.ts @@ -175,11 +175,12 @@ export class AppWidget extends Panel { async saveSnapshot(): Promise { const notebook = this._model.context?.model.toJSON() as any; + if (notebook?.['metadata']?.['spectaSnapshot']) { delete notebook['metadata']['spectaSnapshot']; } const snapshot: ISpectaSnapshotData = { - notebook: this._model.context?.model.toJSON(), + notebook, outputModels: {}, widgetStates: null }; From f430f26bd669a98cab8b6b3af7d777a8f86ed1b0 Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Sun, 16 Aug 2026 23:49:39 +0200 Subject: [PATCH 03/13] wip --- demo/files/blog.ipynb | 1397 ---------------------------------- src/document/factory.tsx | 14 +- src/document/widget.ts | 9 + src/snapshot/tools.ts | 31 + src/specta_model.ts | 123 ++- src/specta_widget.ts | 48 +- src/token.ts | 1 + src/tool.ts | 40 +- src/topbar/menuComponent.tsx | 3 + src/topbar/settingDialog.tsx | 91 ++- src/topbar/topbarWidget.tsx | 4 +- style/base.css | 4 + 12 files changed, 301 insertions(+), 1464 deletions(-) create mode 100644 src/snapshot/tools.ts diff --git a/demo/files/blog.ipynb b/demo/files/blog.ipynb index 0f1c008..604b792 100644 --- a/demo/files/blog.ipynb +++ b/demo/files/blog.ipynb @@ -15,1403 +15,6 @@ "hideTopbar": "No", "slidesTheme": null, "defaultLayout": "article" - }, - "spectaSnapshot": { - "notebook": { - "metadata": { - "kernelspec": { - "name": "xpython", - "display_name": "Python 3.13 (XPython)", - "language": "python" - }, - "language_info": { - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "version": "3.13.1" - }, - "specta": { - "hideTopbar": "No", - "slidesTheme": null, - "defaultLayout": "article" - } - }, - "nbformat_minor": 5, - "nbformat": 4, - "cells": [ - { - "id": "a3abf27e", - "cell_type": "markdown", - "source": "# Data Visualization in Jupyter Notebooks using Apache Echarts\n", - "metadata": {} - }, - { - "id": "21b59e76", - "cell_type": "markdown", - "source": "![Banner](./top.jpeg \"Banner\")\n", - "metadata": { - "specta": { - "outputSize": "Full" - } - } - }, - { - "id": "bf93b954", - "cell_type": "markdown", - "source": "\n\nIn the realm of data science and visualization, Jupyter Notebook has emerged as a powerful tool for data analysis and storytelling. Integrating interactive and aesthetically pleasing charts can significantly enhance the presentation of data insights.", - "metadata": {} - }, - { - "id": "a3826498", - "cell_type": "markdown", - "source": "[Apache Echarts](https://echarts.apache.org/en/index.html) is one of the most versatile libraries for creating interactive charts. This blog post explores how to leverage ipecharts, a new Python library that seamlessly integrates Echarts into Jupyter Notebooks, to craft stunning visualizations within your notebooks.\n\n*Disclaimer: I am the author of this library.*", - "metadata": {} - }, - { - "id": "1a2ffcfd", - "cell_type": "markdown", - "source": "

• • •

", - "metadata": {} - }, - { - "id": "e2569313", - "cell_type": "markdown", - "source": "## Motivation\n\n`ipecharts` is not the first attempt to make Echarts available on Jupyter Notebooks. pyecharts is a popular open-source library that allows you to create interactive charts in Python and supports both notebooks and standalone Python scripts.", - "metadata": {} - }, - { - "id": "330ee266", - "cell_type": "markdown", - "source": "While pyechartscan create charts in the notebooks, it does not use the Jupyter Widgets system but instead injects HTML code into the notebook to render the charts. This approach makes using pyecharts in other Jupyter applications or interacting with other widgets libraries harder.", - "metadata": {} - }, - { - "id": "6e320817", - "cell_type": "markdown", - "source": "On the other hand, ipecharts adopts the native way of creating interactive charts in Jupyter Notebooks by using Jupyter Widgets. It makes the created charts compatible with a wide range of tools and libraries in the Jupyter ecosystem.", - "metadata": {} - }, - { - "id": "250f141f", - "cell_type": "markdown", - "source": "

• • •

", - "metadata": {} - }, - { - "id": "fc22d007", - "cell_type": "markdown", - "source": "## Getting started with ipecharts", - "metadata": {} - }, - { - "id": "b05b1479", - "cell_type": "markdown", - "source": "### Installation\n\n`ipecharts` is available on PyPI and conda-forge:\n\n```bash\n# Installing with pip\npip install ipecharts\n\n# Installing with conda\nconda install -c conda-forge ipecharts\n```\n\nIt requires ipywidgets ≥8.0 and does not work with Jupyter Notebook <7 . More detailed documentation is available at Read the Docs. You can also try it live in this JupyterLite instance.", - "metadata": {} - }, - { - "id": "ce613931", - "cell_type": "markdown", - "source": "### Creating a simple line plot\n\nipechart is a very slim wrapper outside of the Echarts Javascript library so translating the Javascript version of a chart into ipechartswidget is straightforward. Let’s begin with a basic line example from Echarts official documentation:\n\n```typescript\n// Example from https://echarts.apache.org/examples/en/editor.html?c=line-simple&lang=ts\nimport * as echarts from 'echarts';\n\ntype EChartsOption = echarts.EChartsOption;\n\nvar chartDom = document.getElementById('main')!;\nvar myChart = echarts.init(chartDom);\nvar option: EChartsOption;\n\noption = {\n xAxis: {\n type: 'category',\n data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']\n },\n yAxis: {\n type: 'value'\n },\n series: [\n {\n data: [150, 230, 224, 218, 135, 147, 260],\n type: 'line'\n }\n ]\n};\n\noption && myChart.setOption(option);\n```", - "metadata": {} - }, - { - "id": "7824f42f", - "cell_type": "markdown", - "source": "The entry point of a chart in ipecharts is the EchartWidget class:\n", - "metadata": {} - }, - { - "id": "3de1a807", - "cell_type": "code", - "source": "from ipecharts import EChartsWidget\nchart = EChartsWidget()", - "metadata": { - "specta": { - "showOutput": "No", - "showSource": "Yes" - }, - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - }, - "trusted": true - }, - "outputs": [], - "execution_count": null - }, - { - "id": "503e0e22", - "cell_type": "markdown", - "source": "Just as in the Javascript example, we need to set the option of this chart. For all top-level keys of the Echarts option and the entries of series, ipecharts provides Python class counterparts with the same name. Here is the equivalent of the above option object defined with ipecharts classes:\n", - "metadata": {} - }, - { - "id": "d8741975", - "cell_type": "code", - "source": "from ipecharts.option import Option, XAxis, YAxis\nfrom ipecharts.option.series import Line\n\nxAxis = XAxis(\n type=\"category\",\n data=[\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"],\n)\nyAxis = YAxis(type=\"value\")\nline = Line(data=[150, 230, 224, 218, 135, 147, 260])\n\noption = Option()\noption.xAxis = xAxis\noption.yAxis = yAxis\noption.series = [line]", - "metadata": { - "specta": { - "showOutput": "No", - "showSource": "Yes" - }, - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - }, - "trusted": true - }, - "outputs": [], - "execution_count": null - }, - { - "id": "c001b376", - "cell_type": "markdown", - "source": "All classes here are based on traitlets so you can initialize the instance by using keyword arguments or by setting the property values. Finally, updating the option value of our chart gives us the same chart as the Javascript \n", - "metadata": {} - }, - { - "id": "2b2c01c4", - "cell_type": "code", - "source": "chart.option = option\nchart", - "metadata": { - "specta": { - "showSource": "Yes", - "outputSize": "Big" - }, - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - }, - "trusted": true - }, - "outputs": [], - "execution_count": null - }, - { - "id": "19dea3c1", - "cell_type": "markdown", - "source": "### Adding Interactivity", - "metadata": {} - }, - { - "id": "1105f857", - "cell_type": "markdown", - "source": "By using traitlets to configure your chart, any change in the option properties will be applied to the chart automatically. We will use the Button widget of ipywidgets to change the line data dynamically.\n", - "metadata": {} - }, - { - "id": "a5fb539d", - "cell_type": "code", - "source": "from ipywidgets.widgets import Button\nfrom numpy.random import randint\n\ndef update_line_data(b): \n line.data = randint(0, 300, 7).tolist()\n\nbutton = Button(description=\"Generate data\")\nbutton.on_click(update_line_data)\n\ndisplay(button, chart)", - "metadata": { - "specta": { - "showSource": "Yes" - }, - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - }, - "trusted": true - }, - "outputs": [], - "execution_count": null - }, - { - "id": "0e491acc", - "cell_type": "markdown", - "source": "In the on_click callback of the button, we update the data property of the line instance, the changed signal is propagated up to the top-level widget and the chart will be updated automatically.", - "metadata": {} - }, - { - "id": "86b1b92d", - "cell_type": "markdown", - "source": "### Creating charts without using traitlets configuration", - "metadata": {} - }, - { - "id": "2e5acf08", - "cell_type": "markdown", - "source": "In many situations, we simply want to display the data without adding interactivity. For this use case, users can convert any option object used by a Javascript chart to a Python dictionary and pass it to the EchartRawWidget of ipecharts.\n\nHere is the equivalent of the Two Value-Axes in Polar example from the official Echarts documentation using EchartRawWidget:", - "metadata": {} - }, - { - "id": "933a5a49", - "cell_type": "code", - "source": "from ipecharts import EChartsRawWidget\nimport math\n\ndata = []\nfor i in range(101):\n theta = (i / 100) * 360\n r = 5 * (1 + math.sin((theta / 180) * math.pi))\n data.append([r, theta])\n\noption = {\n \"title\": {\"text\": \"Two Value-Axes in Polar\"},\n \"legend\": {\"data\": [\"line\"]},\n \"polar\": {},\n \"tooltip\": {\"trigger\": \"axis\", \"axisPointer\": {\"type\": \"cross\"}},\n \"angleAxis\": {\"type\": \"value\", \"startAngle\": 0},\n \"radiusAxis\": {},\n \"series\": [\n {\"coordinateSystem\": \"polar\", \"name\": \"line\", \"type\": \"line\", \"data\": data}\n ],\n}\nEChartsRawWidget(option=option)", - "metadata": { - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - }, - "trusted": true - }, - "outputs": [], - "execution_count": null - }, - { - "id": "213d1b6b", - "cell_type": "markdown", - "source": "## What’s next\n\nIn this first version, I focused on generating the option configuration class to be able to translate the Javascript charts to the Python ones without too many changes. Echarts has a lot of other customizations in theming, managing maps, or chart animation… These aspects will be addressed in future releases.\n\nYou can follow the development of this library on GitHub. Stay tuned and happy charting!", - "metadata": {} - }, - { - "id": "b7129618", - "cell_type": "markdown", - "source": "## About the author\n\nDuc Trung Le is an open-source developer who works on this project in his free time.", - "metadata": {} - }, - { - "id": "1dd7aa92", - "cell_type": "markdown", - "source": "", - "metadata": {} - } - ] - }, - "outputModels": { - "13": [], - "15": [], - "17": [ - { - "execution_count": 3, - "output_type": "execute_result", - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "8ef8a9c0b511443095cd141ed6825f00", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": "EChartsWidget(option=Option(angleAxis=None, aria=None, axisPointer=None, brush=None, calendar=None, dataset=No…" - }, - "metadata": {} - } - ], - "20": [ - { - "output_type": "display_data", - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "fac759f0dffc4ea0a4e521b41967fcc2", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": "Button(description='Generate data', style=ButtonStyle())" - }, - "metadata": {} - }, - { - "output_type": "display_data", - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "8ef8a9c0b511443095cd141ed6825f00", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": "EChartsWidget(option=Option(angleAxis=None, aria=None, axisPointer=None, brush=None, calendar=None, dataset=No…" - }, - "metadata": {} - } - ], - "24": [ - { - "execution_count": 5, - "output_type": "execute_result", - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "de46f5c303604f4c9ab2e2cabe2104c5", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": "EChartsRawWidget(option={'title': {'text': 'Two Value-Axes in Polar'}, 'legend': {'data': ['line']}, 'polar': …" - }, - "metadata": {} - } - ] - }, - "widgetStates": { - "version_major": 2, - "version_minor": 0, - "state": { - "d37c3cea873f476ab913515d32aa1c55": { - "model_name": "LayoutModel", - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_name": "LayoutModel", - "_model_module_version": "2.0.0", - "_view_module": "@jupyter-widgets/base", - "_view_name": "LayoutView", - "_view_module_version": "2.0.0", - "_view_count": null, - "align_content": null, - "align_items": null, - "align_self": null, - "border_top": null, - "border_right": null, - "border_bottom": null, - "border_left": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "overflow": null, - "order": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null, - "object_fit": null, - "object_position": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_gap": null, - "grid_template_rows": null, - "grid_template_columns": null, - "grid_template_areas": null, - "grid_row": null, - "grid_column": null, - "grid_area": null - } - }, - "ba9ae00c016648e498bb3b7b916017a7": { - "model_name": "OptionModel", - "model_module": "ipecharts", - "model_module_version": "^1.3.1", - "state": { - "_model_module": "ipecharts", - "_model_name": "OptionModel", - "_model_module_version": "^1.3.1", - "_view_module": "ipecharts", - "_view_name": null, - "_view_module_version": "^1.3.1", - "_view_count": null, - "title": null, - "legend": null, - "grid": null, - "xAxis": null, - "yAxis": null, - "polar": null, - "radiusAxis": null, - "angleAxis": null, - "radar": null, - "tooltip": null, - "axisPointer": null, - "toolbox": null, - "brush": null, - "geo": null, - "parallel": null, - "parallelAxis": null, - "singleAxis": null, - "timeline": null, - "graphic": null, - "calendar": null, - "matrix": null, - "thumbnail": null, - "dataset": null, - "aria": null, - "color": null, - "textStyle": null, - "stateAnimation": null, - "options": null, - "globe": null, - "geo3D": null, - "mapbox3D": null, - "grid3D": null, - "xAxis3D": null, - "yAxis3D": null, - "zAxis3D": null, - "series": null, - "animation": null, - "animationDelay": null, - "animationDelayUpdate": null, - "animationDuration": null, - "animationDurationUpdate": null, - "animationEasing": null, - "animationEasingUpdate": null, - "animationThreshold": null, - "backgroundColor": null, - "blendMode": null, - "darkMode": null, - "dataZoom": null, - "hoverLayerThreshold": null, - "media": null, - "richInheritPlainLabel": null, - "unknown_attrs": null, - "useUTC": null, - "visualMap": null - } - }, - "8ef8a9c0b511443095cd141ed6825f00": { - "model_name": "EChartsWidgetModel", - "model_module": "ipecharts", - "model_module_version": "^1.3.1", - "state": { - "_model_module": "ipecharts", - "_model_name": "EChartsWidgetModel", - "_model_module_version": "^1.3.1", - "_view_module": "ipecharts", - "_view_name": "EChartsWidgetView", - "_view_module_version": "^1.3.1", - "_view_count": null, - "_dom_classes": [], - "tabbable": null, - "tooltip": null, - "option": "IPY_MODEL_3c7a4305598942668441673e8f27e6e5", - "style": {}, - "theme": null, - "device_pixel_ratio": 0, - "renderer": "canvas", - "use_dirty_rect": false, - "use_coarse_pointer": false, - "pointer_size": null, - "width": "auto", - "height": "auto", - "locale": "EN", - "layout": "IPY_MODEL_d37c3cea873f476ab913515d32aa1c55" - } - }, - "8239b0d0e21546ad94621863fb75223c": { - "model_name": "XAxisModel", - "model_module": "ipecharts", - "model_module_version": "^1.3.1", - "state": { - "_model_module": "ipecharts", - "_model_name": "XAxisModel", - "_model_module_version": "^1.3.1", - "_view_module": "ipecharts", - "_view_name": null, - "_view_module_version": "^1.3.1", - "_view_count": null, - "alignTicks": null, - "animation": null, - "animationDelay": null, - "animationDelayUpdate": null, - "animationDuration": null, - "animationDurationUpdate": null, - "animationEasing": null, - "animationEasingUpdate": null, - "animationThreshold": null, - "axisLabel": null, - "axisLine": null, - "axisPointer": null, - "axisTick": null, - "boundaryGap": null, - "breakArea": null, - "breakLabelLayout": null, - "breaks": null, - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ], - "gridIndex": null, - "id": null, - "interval": null, - "inverse": null, - "jitter": null, - "jitterMargin": null, - "jitterOverlap": null, - "logBase": null, - "max": null, - "maxInterval": null, - "min": null, - "minInterval": null, - "minorSplitLine": null, - "minorTick": null, - "name": null, - "nameGap": null, - "nameLocation": null, - "nameMoveOverlap": null, - "nameRotate": null, - "nameTextStyle": null, - "nameTruncate": null, - "offset": null, - "position": null, - "scale": null, - "show": null, - "silent": null, - "splitArea": null, - "splitLine": null, - "splitNumber": null, - "startValue": null, - "tooltip": null, - "triggerEvent": null, - "type": "category", - "unknown_attrs": null, - "z": null, - "zlevel": null - } - }, - "80096350f0824f4fbd4af2e262bc921c": { - "model_name": "YAxisModel", - "model_module": "ipecharts", - "model_module_version": "^1.3.1", - "state": { - "_model_module": "ipecharts", - "_model_name": "YAxisModel", - "_model_module_version": "^1.3.1", - "_view_module": "ipecharts", - "_view_name": null, - "_view_module_version": "^1.3.1", - "_view_count": null, - "alignTicks": null, - "animation": null, - "animationDelay": null, - "animationDelayUpdate": null, - "animationDuration": null, - "animationDurationUpdate": null, - "animationEasing": null, - "animationEasingUpdate": null, - "animationThreshold": null, - "axisLabel": null, - "axisLine": null, - "axisPointer": null, - "axisTick": null, - "boundaryGap": null, - "breakArea": null, - "breakLabelLayout": null, - "breaks": null, - "data": null, - "gridIndex": null, - "id": null, - "interval": null, - "inverse": null, - "jitter": null, - "jitterMargin": null, - "jitterOverlap": null, - "logBase": null, - "max": null, - "maxInterval": null, - "min": null, - "minInterval": null, - "minorSplitLine": null, - "minorTick": null, - "name": null, - "nameGap": null, - "nameLocation": null, - "nameMoveOverlap": null, - "nameRotate": null, - "nameTextStyle": null, - "nameTruncate": null, - "offset": null, - "position": null, - "scale": null, - "show": null, - "silent": null, - "splitArea": null, - "splitLine": null, - "splitNumber": null, - "startValue": null, - "tooltip": null, - "triggerEvent": null, - "type": "value", - "unknown_attrs": null, - "z": null, - "zlevel": null - } - }, - "0156a999c271416896382a081c413928": { - "model_name": "LineModel", - "model_module": "ipecharts", - "model_module_version": "^1.3.1", - "state": { - "_model_module": "ipecharts", - "_model_name": "LineModel", - "_model_module_version": "^1.3.1", - "_view_module": "ipecharts", - "_view_name": null, - "_view_module_version": "^1.3.1", - "_view_count": null, - "animation": null, - "animationDelay": null, - "animationDelayUpdate": null, - "animationDuration": null, - "animationDurationUpdate": null, - "animationEasing": null, - "animationEasingUpdate": null, - "animationThreshold": null, - "areaStyle": null, - "blur": null, - "clip": null, - "colorBy": null, - "connectNulls": null, - "coord": null, - "coordinateSystem": null, - "coordinateSystemUsage": null, - "cursor": null, - "data": [ - 150, - 230, - 224, - 218, - 135, - 147, - 260 - ], - "dataGroupId": null, - "datasetIndex": null, - "dimensions": null, - "emphasis": null, - "encode": null, - "endLabel": null, - "id": null, - "itemStyle": null, - "label": null, - "labelLayout": null, - "labelLine": null, - "legendHoverLink": null, - "lineStyle": null, - "markArea": null, - "markLine": null, - "markPoint": null, - "name": null, - "polarId": null, - "polarIndex": null, - "sampling": null, - "select": null, - "selectedMode": null, - "seriesLayoutBy": null, - "showAllSymbol": null, - "showSymbol": null, - "silent": null, - "singleAxisId": null, - "singleAxisIndex": null, - "smooth": null, - "smoothMonotone": null, - "stack": null, - "stackOrder": null, - "stackStrategy": null, - "step": null, - "symbol": null, - "symbolKeepAspect": null, - "symbolOffset": null, - "symbolRotate": null, - "symbolSize": null, - "tooltip": null, - "triggerLineEvent": null, - "type": "line", - "universalTransition": null, - "unknown_attrs": null, - "xAxisId": null, - "xAxisIndex": null, - "yAxisId": null, - "yAxisIndex": null, - "z": null, - "zlevel": null - } - }, - "3c7a4305598942668441673e8f27e6e5": { - "model_name": "OptionModel", - "model_module": "ipecharts", - "model_module_version": "^1.3.1", - "state": { - "_model_module": "ipecharts", - "_model_name": "OptionModel", - "_model_module_version": "^1.3.1", - "_view_module": "ipecharts", - "_view_name": null, - "_view_module_version": "^1.3.1", - "_view_count": null, - "title": null, - "legend": null, - "grid": null, - "xAxis": "IPY_MODEL_8239b0d0e21546ad94621863fb75223c", - "yAxis": "IPY_MODEL_80096350f0824f4fbd4af2e262bc921c", - "polar": null, - "radiusAxis": null, - "angleAxis": null, - "radar": null, - "tooltip": null, - "axisPointer": null, - "toolbox": null, - "brush": null, - "geo": null, - "parallel": null, - "parallelAxis": null, - "singleAxis": null, - "timeline": null, - "graphic": null, - "calendar": null, - "matrix": null, - "thumbnail": null, - "dataset": null, - "aria": null, - "color": null, - "textStyle": null, - "stateAnimation": null, - "options": null, - "globe": null, - "geo3D": null, - "mapbox3D": null, - "grid3D": null, - "xAxis3D": null, - "yAxis3D": null, - "zAxis3D": null, - "series": [ - "IPY_MODEL_0156a999c271416896382a081c413928" - ], - "animation": null, - "animationDelay": null, - "animationDelayUpdate": null, - "animationDuration": null, - "animationDurationUpdate": null, - "animationEasing": null, - "animationEasingUpdate": null, - "animationThreshold": null, - "backgroundColor": null, - "blendMode": null, - "darkMode": null, - "dataZoom": null, - "hoverLayerThreshold": null, - "media": null, - "richInheritPlainLabel": null, - "unknown_attrs": null, - "useUTC": null, - "visualMap": null - } - }, - "f23f2211a78245f3bd178f0c140b2faa": { - "model_name": "LayoutModel", - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_name": "LayoutModel", - "_model_module_version": "2.0.0", - "_view_module": "@jupyter-widgets/base", - "_view_name": "LayoutView", - "_view_module_version": "2.0.0", - "_view_count": null, - "align_content": null, - "align_items": null, - "align_self": null, - "border_top": null, - "border_right": null, - "border_bottom": null, - "border_left": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "overflow": null, - "order": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null, - "object_fit": null, - "object_position": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_gap": null, - "grid_template_rows": null, - "grid_template_columns": null, - "grid_template_areas": null, - "grid_row": null, - "grid_column": null, - "grid_area": null - } - }, - "a4823901725e4c9a89eff2129dd039ce": { - "model_name": "ButtonStyleModel", - "model_module": "@jupyter-widgets/controls", - "model_module_version": "2.0.0", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_name": "ButtonStyleModel", - "_model_module_version": "2.0.0", - "_view_module": "@jupyter-widgets/base", - "_view_name": "StyleView", - "_view_module_version": "2.0.0", - "_view_count": null, - "button_color": null, - "font_family": null, - "font_size": null, - "font_style": null, - "font_variant": null, - "font_weight": null, - "text_color": null, - "text_decoration": null - } - }, - "fac759f0dffc4ea0a4e521b41967fcc2": { - "model_name": "ButtonModel", - "model_module": "@jupyter-widgets/controls", - "model_module_version": "2.0.0", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_name": "ButtonModel", - "_model_module_version": "2.0.0", - "_view_module": "@jupyter-widgets/controls", - "_view_name": "ButtonView", - "_view_module_version": "2.0.0", - "_view_count": null, - "_dom_classes": [], - "tabbable": null, - "tooltip": null, - "description": "Generate data", - "disabled": false, - "icon": "", - "button_style": "", - "style": "IPY_MODEL_a4823901725e4c9a89eff2129dd039ce", - "layout": "IPY_MODEL_f23f2211a78245f3bd178f0c140b2faa" - } - }, - "aeaccb172d914c56bac3ddf013f3cb95": { - "model_name": "LayoutModel", - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_name": "LayoutModel", - "_model_module_version": "2.0.0", - "_view_module": "@jupyter-widgets/base", - "_view_name": "LayoutView", - "_view_module_version": "2.0.0", - "_view_count": null, - "align_content": null, - "align_items": null, - "align_self": null, - "border_top": null, - "border_right": null, - "border_bottom": null, - "border_left": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "overflow": null, - "order": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null, - "object_fit": null, - "object_position": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_gap": null, - "grid_template_rows": null, - "grid_template_columns": null, - "grid_template_areas": null, - "grid_row": null, - "grid_column": null, - "grid_area": null - } - }, - "de46f5c303604f4c9ab2e2cabe2104c5": { - "model_name": "EChartsRawWidgetModel", - "model_module": "ipecharts", - "model_module_version": "^1.3.1", - "state": { - "_model_module": "ipecharts", - "_model_name": "EChartsRawWidgetModel", - "_model_module_version": "^1.3.1", - "_view_module": "ipecharts", - "_view_name": "EChartsRawWidgetView", - "_view_module_version": "^1.3.1", - "_view_count": null, - "_dom_classes": [], - "tabbable": null, - "tooltip": null, - "option": { - "angleAxis": { - "startAngle": 0, - "type": "value" - }, - "legend": { - "data": [ - "line" - ] - }, - "polar": {}, - "radiusAxis": {}, - "series": [ - { - "coordinateSystem": "polar", - "data": [ - [ - 5, - 0 - ], - [ - 5.313952597646567, - 3.6 - ], - [ - 5.626666167821521, - 7.2 - ], - [ - 5.936906572928623, - 10.799999999999999 - ], - [ - 6.2434494358242745, - 14.4 - ], - [ - 6.545084971874737, - 18 - ], - [ - 6.84062276342339, - 21.599999999999998 - ], - [ - 7.1288964578253635, - 25.200000000000003 - ], - [ - 7.408768370508576, - 28.8 - ], - [ - 7.679133974894983, - 32.4 - ], - [ - 7.938926261462366, - 36 - ], - [ - 8.18711994874345, - 39.6 - ], - [ - 8.422735529643443, - 43.199999999999996 - ], - [ - 8.644843137107058, - 46.800000000000004 - ], - [ - 8.852566213878946, - 50.400000000000006 - ], - [ - 9.045084971874736, - 54 - ], - [ - 9.221639627510076, - 57.6 - ], - [ - 9.381533400219318, - 61.2 - ], - [ - 9.524135262330098, - 64.8 - ], - [ - 9.648882429441256, - 68.4 - ], - [ - 9.755282581475768, - 72 - ], - [ - 9.842915805643155, - 75.6 - ], - [ - 9.911436253643442, - 79.2 - ], - [ - 9.96057350657239, - 82.8 - ], - [ - 9.990133642141357, - 86.39999999999999 - ], - [ - 10, - 90 - ], - [ - 9.990133642141357, - 93.60000000000001 - ], - [ - 9.96057350657239, - 97.2 - ], - [ - 9.911436253643442, - 100.80000000000001 - ], - [ - 9.842915805643155, - 104.39999999999999 - ], - [ - 9.755282581475768, - 108 - ], - [ - 9.648882429441256, - 111.6 - ], - [ - 9.524135262330098, - 115.2 - ], - [ - 9.381533400219318, - 118.80000000000001 - ], - [ - 9.221639627510074, - 122.4 - ], - [ - 9.045084971874736, - 125.99999999999999 - ], - [ - 8.852566213878946, - 129.6 - ], - [ - 8.644843137107056, - 133.2 - ], - [ - 8.422735529643443, - 136.8 - ], - [ - 8.18711994874345, - 140.4 - ], - [ - 7.938926261462367, - 144 - ], - [ - 7.6791339748949845, - 147.6 - ], - [ - 7.408768370508579, - 151.2 - ], - [ - 7.128896457825363, - 154.8 - ], - [ - 6.840622763423391, - 158.4 - ], - [ - 6.545084971874737, - 162 - ], - [ - 6.243449435824276, - 165.6 - ], - [ - 5.936906572928625, - 169.2 - ], - [ - 5.626666167821525, - 172.79999999999998 - ], - [ - 5.313952597646567, - 176.4 - ], - [ - 5.000000000000001, - 180 - ], - [ - 4.6860474023534335, - 183.6 - ], - [ - 4.373333832178479, - 187.20000000000002 - ], - [ - 4.063093427071376, - 190.8 - ], - [ - 3.7565505641757246, - 194.4 - ], - [ - 3.454915028125262, - 198.00000000000003 - ], - [ - 3.159377236576608, - 201.60000000000002 - ], - [ - 2.871103542174639, - 205.2 - ], - [ - 2.591231629491425, - 208.79999999999998 - ], - [ - 2.3208660251050177, - 212.39999999999998 - ], - [ - 2.061073738537635, - 216 - ], - [ - 1.8128800512565517, - 219.6 - ], - [ - 1.5772644703565564, - 223.2 - ], - [ - 1.3551568628929433, - 226.8 - ], - [ - 1.147433786121053, - 230.4 - ], - [ - 0.9549150281252633, - 234 - ], - [ - 0.7783603724899235, - 237.60000000000002 - ], - [ - 0.6184665997806821, - 241.20000000000002 - ], - [ - 0.475864737669901, - 244.8 - ], - [ - 0.3511175705587444, - 248.39999999999998 - ], - [ - 0.24471741852423234, - 251.99999999999997 - ], - [ - 0.15708419435684517, - 255.6 - ], - [ - 0.0885637463565564, - 259.2 - ], - [ - 0.039426493427611176, - 262.8 - ], - [ - 0.009866357858642205, - 266.4 - ], - [ - 0, - 270 - ], - [ - 0.009866357858642205, - 273.6 - ], - [ - 0.03942649342761062, - 277.2 - ], - [ - 0.0885637463565564, - 280.8 - ], - [ - 0.15708419435684573, - 284.40000000000003 - ], - [ - 0.2447174185242318, - 288 - ], - [ - 0.3511175705587438, - 291.6 - ], - [ - 0.47586473766990045, - 295.2 - ], - [ - 0.6184665997806832, - 298.8 - ], - [ - 0.7783603724899224, - 302.4 - ], - [ - 0.9549150281252622, - 306 - ], - [ - 1.1474337861210548, - 309.6 - ], - [ - 1.3551568628929422, - 313.2 - ], - [ - 1.5772644703565553, - 316.8 - ], - [ - 1.8128800512565486, - 320.4 - ], - [ - 2.061073738537633, - 324 - ], - [ - 2.320866025105018, - 327.6 - ], - [ - 2.5912316294914195, - 331.2 - ], - [ - 2.871103542174639, - 334.8 - ], - [ - 3.1593772365766064, - 338.4 - ], - [ - 3.454915028125262, - 342 - ], - [ - 3.7565505641757193, - 345.59999999999997 - ], - [ - 4.063093427071377, - 349.2 - ], - [ - 4.373333832178476, - 352.8 - ], - [ - 4.6860474023534335, - 356.4 - ], - [ - 4.999999999999999, - 360 - ] - ], - "name": "line", - "type": "line" - } - ], - "title": { - "text": "Two Value-Axes in Polar" - }, - "tooltip": { - "axisPointer": { - "type": "cross" - }, - "trigger": "axis" - } - }, - "style": {}, - "theme": null, - "device_pixel_ratio": 0, - "renderer": "canvas", - "use_dirty_rect": false, - "use_coarse_pointer": false, - "pointer_size": null, - "width": "auto", - "height": "auto", - "locale": "EN", - "layout": "IPY_MODEL_aeaccb172d914c56bac3ddf013f3cb95" - } - } - } - } } }, "nbformat_minor": 5, diff --git a/src/document/factory.tsx b/src/document/factory.tsx index 3d079b4..b39a7fb 100644 --- a/src/document/factory.tsx +++ b/src/document/factory.tsx @@ -54,6 +54,10 @@ export class NotebookGridWidgetFactory extends ABCWidgetFactory< nbMetadata: context.model.metadata, nbPath: path }); + + const spectaWidget = await this._spectaWidgetFactory.createNew({ + context + }); const isSpecta = isSpectaApp(); let topbarTarget: ISpectaTopbarWidget | undefined; if (!spectaConfig.hideTopbar) { @@ -69,7 +73,6 @@ export class NotebookGridWidgetFactory extends ABCWidgetFactory< } else { topbarWidget = this._spectaTopbar; } - const topbar = topbarWidget as TopbarWidget | undefined; const menu = ( ); @@ -102,6 +106,7 @@ export class NotebookGridWidgetFactory extends ABCWidgetFactory< if (titleWidget) { titleWidget.addClass('specta-topbar-title-wrapper'); } + console.log('im here adding menu to topbar'); this._spectaTopbar.addReactWidget(menu, 'right', 10000); } } @@ -109,10 +114,7 @@ export class NotebookGridWidgetFactory extends ABCWidgetFactory< this._shell.hideTopBar(); } - const spectaWidget = await this._spectaWidgetFactory.createNew({ - context - }); - + console.log('im here', topbarTarget); if (spectaWidget) { content.addWidget(spectaWidget); if (topbarTarget?.addReactWidget) { diff --git a/src/document/widget.ts b/src/document/widget.ts index 44a76d6..8dc9535 100644 --- a/src/document/widget.ts +++ b/src/document/widget.ts @@ -8,6 +8,7 @@ import { import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { ServiceManager } from '@jupyterlab/services'; import { Widget } from '@lumino/widgets'; +import { AppWidget } from '../specta_widget'; export interface ISpectaOptions { manager: ServiceManager; @@ -25,6 +26,14 @@ export class NotebookSpectaDocWidget extends DocumentWidget< super(options); } + getSpectaWidget(): AppWidget | undefined { + for (const element of this.content.children()) { + if (element instanceof AppWidget) { + return element; + } + } + } + dispose(): void { this.content.dispose(); super.dispose(); diff --git a/src/snapshot/tools.ts b/src/snapshot/tools.ts new file mode 100644 index 0000000..1992e02 --- /dev/null +++ b/src/snapshot/tools.ts @@ -0,0 +1,31 @@ +import { type INotebookContent } from '@jupyterlab/nbformat'; +export const WIDGET_STATE_MIMETYPE = + 'application/vnd.jupyter.widget-state+json'; +export const WIDGET_VIEW_MIMETYPE = 'application/vnd.jupyter.widget-view+json'; + +export const SPECTA_SNAPSHOT_KEY = 'spectaSnapshot'; + +export interface IWidgetManagerState { + version_major: number; + version_minor: number; + state: { [modelId: string]: unknown }; +} + +export interface IWidgetManagerLike { + get_state(options?: { + drop_defaults?: boolean; + }): Promise; +} + +export interface ISpectaSnapshotData { + timestamp: number; + hash: string; + notebook?: INotebookContent; + widgetStates: IWidgetManagerState | null; +} + +export type INotebookContentWithSnapshot = INotebookContent & { + metadata: INotebookContent['metadata'] & { + [SPECTA_SNAPSHOT_KEY]: ISpectaSnapshotData | undefined; + }; +}; diff --git a/src/specta_model.ts b/src/specta_model.ts index 5b1142d..08223d2 100644 --- a/src/specta_model.ts +++ b/src/specta_model.ts @@ -24,26 +24,35 @@ import { OutputAreaModel, SimplifiedOutputArea } from '@jupyterlab/outputarea'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { KernelSpec, ServiceManager } from '@jupyterlab/services'; import { IExecuteReplyMsg } from '@jupyterlab/services/lib/kernel/messages'; -import { PartialJSONValue, PromiseDelegate } from '@lumino/coreutils'; +import { PromiseDelegate } from '@lumino/coreutils'; import { createNotebookContext, createNotebookPanel } from './create_notebook_panel'; import { SpectaCellOutput } from './specta_cell_output'; -import { emitResizeEvent, ISpectaSnapshotData, readCellConfig } from './tool'; +import { computeHash, emitResizeEvent, readCellConfig } from './tool'; +import { + ISpectaSnapshotData, + SPECTA_SNAPSHOT_KEY, + WIDGET_STATE_MIMETYPE, + INotebookContentWithSnapshot +} from './snapshot/tools'; import { ISignal, Signal } from '@lumino/signaling'; export class AppModel { constructor(private options: AppModel.IOptions) { - this._notebookModelJson = options.context.model.toJSON(); + this._notebookModelJson = options.context.model.toJSON() as any; + this._staticRender = Boolean( + this._notebookModelJson.metadata[SPECTA_SNAPSHOT_KEY] + ); this._filePath = options.context.localPath; this._kernelPreference = { - shouldStart: true, - canStart: true, + shouldStart: !this._staticRender, + canStart: !this._staticRender, shutdownOnDispose: true, name: options.context.model.defaultKernelName, - autoStartDefault: true, + autoStartDefault: !this._staticRender, language: options.context.model.defaultKernelLanguage }; this._manager = options.manager; @@ -70,6 +79,18 @@ export class AppModel { return this._fileChanged; } + get staticRender() { + return this._staticRender; + } + + set staticRender(v: boolean) { + this._staticRender = v; + } + + get snapshotData(): ISpectaSnapshotData | undefined { + return this._notebookModelJson.metadata[SPECTA_SNAPSHOT_KEY]; + } + dispose(): void { if (this.isDisposed) { return; @@ -99,15 +120,44 @@ export class AppModel { kernelPreference: this._kernelPreference, filePath: this._filePath }); - this._context.model.fromJSON(this._notebookModelJson); + if (this._staticRender) { + const notebookModel = JSON.parse( + JSON.stringify(this._notebookModelJson) + ) as INotebookContentWithSnapshot; + const snapshot = notebookModel.metadata.spectaSnapshot; + if (!snapshot) { + throw new Error('Snapshot not found'); + } + notebookModel.metadata['widgets'] = { + [WIDGET_STATE_MIMETYPE]: snapshot.widgetStates + } as any; + this._context.model.fromJSON(notebookModel); + this._notebookPanel = createNotebookPanel({ + context: this._context!, + rendermime: this.options.rendermime, + editorServices: this.options.editorServices + }); + await this._context.sessionContext.initialize(); + (this._context.sessionContext as any)._session = { + kernel: { + id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + registerCommTarget: () => {}, + handleComms: false, + requestCommInfo: async () => ({ content: { status: undefined } }) + } + }; - this._notebookPanel = createNotebookPanel({ - context: this._context!, - rendermime: this.options.rendermime, - editorServices: this.options.editorServices - }); - (this.options.tracker as any).add(this._notebookPanel); - await this._context.sessionContext.initialize(); + (this.options.tracker as any).add(this._notebookPanel); + } else { + this._context.model.fromJSON(this._notebookModelJson); + this._notebookPanel = createNotebookPanel({ + context: this._context!, + rendermime: this.options.rendermime, + editorServices: this.options.editorServices + }); + (this.options.tracker as any).add(this._notebookPanel); + await this._context.sessionContext.initialize(); + } } createCell(cellModel: ICellModel): SpectaCellOutput { @@ -120,6 +170,7 @@ export class AppModel { switch (cellModel.type) { case 'code': { let sourceCell: CodeCell | undefined; + cellModel.sharedModel.transact(() => { (cellModel as CodeCellModel).clearExecution(); }, false); @@ -138,7 +189,12 @@ export class AppModel { sourceCell.syncEditable = false; sourceCell.readOnly = true; } + const outputareamodel = new OutputAreaModel({ trusted: true }); + + if (this._staticRender && cellModelJson.outputs) { + outputareamodel.fromJSON(cellModelJson.outputs as any); + } const out = new SimplifiedOutputArea({ model: outputareamodel, rendermime: this.options.rendermime @@ -203,6 +259,11 @@ export class AppModel { if (cell.type !== 'code' || !this._context) { return; } + + if (this._staticRender) { + outputWrapper.removePlaceholder(); + return; + } const specs = this._kernelSpecManager.specs; if (specs && Object.keys(specs.kernelspecs).length !== 0) { this._kernelReady.resolve(); @@ -210,6 +271,7 @@ export class AppModel { await this._kernelReady.promise; } const output = outputWrapper.cellOutput as SimplifiedOutputArea; + const source = cell.sharedModel.source; const rep = await SimplifiedOutputArea.execute( source, @@ -223,25 +285,52 @@ export class AppModel { return rep; } + getSnapshot(): ISpectaSnapshotData | undefined { + return this._notebookModelJson.metadata[SPECTA_SNAPSHOT_KEY]; + } + + snapshotStatus(): 'out-of-sync' | 'in-sync' | 'not-exist' { + console.log('ggggggg', this); + const sn = this.getSnapshot(); + if (!sn) { + return 'not-exist'; + } + const hash = computeHash( + this._notebookModelJson.cells.map(it => it.source).join('\n') + ); + if (hash === sn.hash) { + return 'in-sync'; + } + return 'out-of-sync'; + } async saveSnapshotToMetadata(snapshot: ISpectaSnapshotData): Promise { if (this.options.context) { - console.log('saveSnapshotToMetadata', this.options.context.path); - this.options.context.model.setMetadata('spectaSnapshot', snapshot); + this.options.context.model.setMetadata(SPECTA_SNAPSHOT_KEY, snapshot); + await this.options.context.save(); + this.options.context.model.dirty = false; + } + } + + async deleteSnapshot(): Promise { + if (this.options.context) { + this.options.context.model.deleteMetadata(SPECTA_SNAPSHOT_KEY); await this.options.context.save(); this.options.context.model.dirty = false; } } + private _kernelReady = new PromiseDelegate(); private _notebookPanel?: NotebookPanel; private _context?: DocumentRegistry.IContext; - private _notebookModelJson: PartialJSONValue; + private _notebookModelJson: INotebookContentWithSnapshot; private _isDisposed = false; private _manager: ServiceManager.IManager; private _kernelPreference: ISessionContext.IKernelPreference; private _fileChanged = new Signal(this); private _filePath: string; private _kernelSpecManager: KernelSpec.IManager; + private _staticRender = false; } export namespace AppModel { diff --git a/src/specta_widget.ts b/src/specta_widget.ts index 255d347..f196d80 100644 --- a/src/specta_widget.ts +++ b/src/specta_widget.ts @@ -11,14 +11,19 @@ import { ISpectaLayoutRegistry } from './token'; import { + computeHash, emitResizeEvent, hideAppLoadingIndicator, + isSpectaApp +} from './tool'; +import { ISpectaSnapshotData, - isSpectaApp, IWidgetManagerLike, + SPECTA_SNAPSHOT_KEY, WIDGET_VIEW_MIMETYPE -} from './tool'; +} from './snapshot/tools'; import { SimplifiedOutputArea } from '@jupyterlab/outputarea'; +import { INotebookContent } from '@jupyterlab/nbformat'; export class AppWidget extends Panel { constructor(options: AppWidget.IOptions) { @@ -111,7 +116,6 @@ export class AppWidget extends Panel { } for (const cell of cellList) { const src = cell.sharedModel.source; - if (src.length === 0) { continue; } @@ -173,33 +177,40 @@ export class AppWidget extends Panel { this.removeSpinner(); } - async saveSnapshot(): Promise { - const notebook = this._model.context?.model.toJSON() as any; + async saveSnapshot(): Promise { + if (this._model.staticRender) { + return; + } + + await Promise.all( + this._outputs.map(it => { + if ((it.cellOutput as any)?.future?.done) { + return (it.cellOutput as SimplifiedOutputArea).future.done; + } + return Promise.resolve(); + }) + ); + const notebook = this._model.context?.model.toJSON() as INotebookContent; - if (notebook?.['metadata']?.['spectaSnapshot']) { - delete notebook['metadata']['spectaSnapshot']; + if (notebook.metadata?.[SPECTA_SNAPSHOT_KEY]) { + delete notebook['metadata'][SPECTA_SNAPSHOT_KEY]; } + const allCodeSources = notebook.cells.map(it => it.source).join('\n'); + + const timestamp = Date.now(); const snapshot: ISpectaSnapshotData = { + hash: computeHash(allCodeSources), + timestamp, notebook, - outputModels: {}, widgetStates: null }; - const allCodeCellOutputs = this._outputs.filter( - el => !el.info.hidden && el.info.cellModel?.cell_type === 'code' - ); - await Promise.all( - allCodeCellOutputs.map( - it => (it.cellOutput as SimplifiedOutputArea).future.done - ) - ); - for (const [idx, el] of this._outputs.entries()) { + for (const el of this._outputs) { if (el.info.hidden || el.info.cellModel?.cell_type !== 'code') { continue; } const output = el.cellOutput as SimplifiedOutputArea; const outputModels = output.model.toJSON(); - snapshot.outputModels[idx] = outputModels; if (!snapshot.widgetStates) { for (let index = 0; index < outputModels.length; index++) { const data = @@ -245,6 +256,7 @@ export class AppWidget extends Panel { } console.log('done', snapshot); await this._model.saveSnapshotToMetadata(snapshot); + return timestamp; } protected onCloseRequest(msg: Message): void { diff --git a/src/token.ts b/src/token.ts index 9400a79..3666208 100644 --- a/src/token.ts +++ b/src/token.ts @@ -110,6 +110,7 @@ export interface ISpectaTopbarWidget { addSettingsWidget?: (widget: ISpectaWidget) => void; settingsWidgets?: ISpectaWidget[]; setSettingsIcon?: (icon: JSX.Element) => void; + settingsIconChanged?: ISignal; customIcon?: JSX.Element; } export const ISpectaTopbarWidgetToken = new Token( diff --git a/src/tool.ts b/src/tool.ts index f7e7f6f..2e53e7d 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -23,7 +23,6 @@ import { ISpectaUiSwitcher, ISpectaUrlFactory } from './token'; -import { PartialJSONValue } from '@lumino/coreutils'; export const PLAINB_PREFIX = 'ptjnb-'; @@ -456,24 +455,23 @@ export function openDocument( } } -export const WIDGET_STATE_MIMETYPE = - 'application/vnd.jupyter.widget-state+json'; -export const WIDGET_VIEW_MIMETYPE = 'application/vnd.jupyter.widget-view+json'; - -export interface IWidgetManagerState { - version_major: number; - version_minor: number; - state: { [modelId: string]: unknown }; -} - -export interface IWidgetManagerLike { - get_state(options?: { - drop_defaults?: boolean; - }): Promise; -} - -export interface ISpectaSnapshotData { - notebook?: PartialJSONValue; - outputModels: Record; - widgetStates: IWidgetManagerState | null; +export function computeHash(str: string): string { + const s = str.replace(/\s+/g, ' ').trim(); + let h1 = 0xdeadbeef, + h2 = 0x41c6ce57; + for (let i = 0; i < s.length; i++) { + const c = s.charCodeAt(i); + h1 = Math.imul(h1 ^ c, 2654435761); + h2 = Math.imul(h2 ^ c, 1597334677); + } + h1 = + Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ + Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = + Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ + Math.imul(h1 ^ (h1 >>> 13), 3266489909); + return ( + (h2 >>> 0).toString(16).padStart(8, '0') + + (h1 >>> 0).toString(16).padStart(8, '0') + ); } diff --git a/src/topbar/menuComponent.tsx b/src/topbar/menuComponent.tsx index 6915144..a883cc5 100644 --- a/src/topbar/menuComponent.tsx +++ b/src/topbar/menuComponent.tsx @@ -6,6 +6,7 @@ import { GearIcon } from '../components/icon/gear'; import { IconButton } from '../components/iconButton'; import { SettingContent } from './settingDialog'; import { ISpectaUiSwitcher, ITopbarConfig, ISpectaWidget } from '../token'; +import { AppWidget } from '../specta_widget'; interface IProps { config?: ITopbarConfig; @@ -13,6 +14,7 @@ interface IProps { settingsWidgets?: ISpectaWidget[]; uiSwitcher?: ISpectaUiSwitcher | null; currentPath?: string | null; + spectaWidget?: AppWidget; currentUi?: string; settingsIconChanged?: ISignal; customIcon?: JSX.Element; @@ -81,6 +83,7 @@ export function MenuComponent(props: IProps): JSX.Element { uiSwitcher={props.uiSwitcher} currentPath={props.currentPath} currentUi={props.currentUi} + spectaWidget={props.spectaWidget} /> )} diff --git a/src/topbar/settingDialog.tsx b/src/topbar/settingDialog.tsx index 67a16f8..2ec93d5 100644 --- a/src/topbar/settingDialog.tsx +++ b/src/topbar/settingDialog.tsx @@ -1,5 +1,11 @@ import { IThemeManager } from '@jupyterlab/apputils'; -import React, { useState, useEffect, useCallback, useRef } from 'react'; +import React, { + useState, + useEffect, + useCallback, + useRef, + useMemo +} from 'react'; import { Divider } from '../components/divider'; import { ISpectaLayoutRegistry, @@ -8,6 +14,7 @@ import { ISpectaWidget } from '../token'; import { Widget } from '@lumino/widgets'; +import { AppWidget } from '../specta_widget'; export const SettingContent = (props: { config?: ITopbarConfig; @@ -17,6 +24,7 @@ export const SettingContent = (props: { uiSwitcher?: ISpectaUiSwitcher | null; currentPath?: string | null; currentUi?: string; + spectaWidget?: AppWidget; }) => { const { themeManager, layoutRegistry, settingsWidgets } = props; const [themeOptions, setThemeOptions] = useState([ @@ -132,6 +140,36 @@ export const SettingContent = (props: { }, [uiSwitcher, currentPath] ); + + const [snapshotStatus, setSnapshotStatus] = useState< + 'out-of-sync' | 'in-sync' | 'not-exist' + >(props.spectaWidget?.model.snapshotStatus() ?? 'not-exist'); + + const deleteSnapshot = useCallback(async () => { + if (snapshotStatus === 'not-exist') { + return; + } + await props.spectaWidget?.model?.deleteSnapshot(); + setCurrentTimestamp(undefined); + setSnapshotStatus('not-exist'); + }, [props.spectaWidget, snapshotStatus]); + + const createSnapshot = useCallback(async () => { + const timestamp = await props.spectaWidget?.saveSnapshot(); + if (timestamp) { + setSnapshotStatus('in-sync'); + } + setCurrentTimestamp(timestamp); + }, [props.spectaWidget]); + + const snapshot = useMemo( + () => props.spectaWidget?.model?.getSnapshot(), + [props.spectaWidget] + ); + const [currentTimestamp, setCurrentTimestamp] = useState( + snapshot?.timestamp + ); + return (

@@ -172,7 +210,9 @@ export const SettingContent = (props: { : true) && themeManager && (

- +
)} +
+ +
+
+ Last snapshot:{' '} + {currentTimestamp + ? new Date(currentTimestamp).toLocaleString() + : 'Unavailable'} +
+
+ {snapshotStatus === 'not-exist' + ? 'No snapshot found' + : snapshotStatus === 'out-of-sync' + ? 'Snapshot is out of sync with the notebook' + : ''} +
+ +
+ + +
+
+
{settingsWidgets && settingsWidgets.length > 0 && (
diff --git a/src/topbar/topbarWidget.tsx b/src/topbar/topbarWidget.tsx index f079707..1574cc7 100644 --- a/src/topbar/topbarWidget.tsx +++ b/src/topbar/topbarWidget.tsx @@ -2,10 +2,10 @@ import { ReactWidget } from '@jupyterlab/apputils'; import { Panel, Widget } from '@lumino/widgets'; import { Signal, ISignal } from '@lumino/signaling'; -import { ITopbarConfig, ISpectaWidget } from '../token'; +import { ITopbarConfig, ISpectaWidget, ISpectaTopbarWidget } from '../token'; import { RankedPanel } from './rankedPanel'; -export class TopbarWidget extends Panel { +export class TopbarWidget extends Panel implements ISpectaTopbarWidget { constructor(options: TopbarWidget.IOptions) { super(options); this._config = { diff --git a/style/base.css b/style/base.css index badef84..f8421a5 100644 --- a/style/base.css +++ b/style/base.css @@ -233,3 +233,7 @@ display: block; } } + +.specta-cell-output .jupyter-widgets-disconnected::before { + display: none !important; +} From 2ec569c6b27ec7aef10a0063f8dbe744e65b3f46 Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Mon, 17 Aug 2026 11:15:11 +0200 Subject: [PATCH 04/13] render work --- demo/files/blog.ipynb | 1398 ++++++++++++++++++++++++++++++++++ src/document/factory.tsx | 2 - src/specta_model.ts | 37 +- src/specta_widget.ts | 4 +- src/topbar/settingDialog.tsx | 14 +- 5 files changed, 1437 insertions(+), 18 deletions(-) diff --git a/demo/files/blog.ipynb b/demo/files/blog.ipynb index 604b792..7df24e1 100644 --- a/demo/files/blog.ipynb +++ b/demo/files/blog.ipynb @@ -15,6 +15,1404 @@ "hideTopbar": "No", "slidesTheme": null, "defaultLayout": "article" + }, + "spectaSnapshot": { + "hash": "876caa76358b7da8", + "timestamp": 1786957478149, + "notebook": { + "metadata": { + "kernelspec": { + "name": "xpython", + "display_name": "Python 3.13 (XPython)", + "language": "python" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.1" + }, + "specta": { + "hideTopbar": "No", + "slidesTheme": null, + "defaultLayout": "article" + } + }, + "nbformat_minor": 5, + "nbformat": 4, + "cells": [ + { + "id": "a3abf27e", + "cell_type": "markdown", + "source": "# Data Visualization in Jupyter Notebooks using Apache Echarts\n", + "metadata": {} + }, + { + "id": "21b59e76", + "cell_type": "markdown", + "source": "![Banner](./top.jpeg \"Banner\")\n", + "metadata": { + "specta": { + "outputSize": "Full" + } + } + }, + { + "id": "bf93b954", + "cell_type": "markdown", + "source": "\n\nIn the realm of data science and visualization, Jupyter Notebook has emerged as a powerful tool for data analysis and storytelling. Integrating interactive and aesthetically pleasing charts can significantly enhance the presentation of data insights.", + "metadata": {} + }, + { + "id": "a3826498", + "cell_type": "markdown", + "source": "[Apache Echarts](https://echarts.apache.org/en/index.html) is one of the most versatile libraries for creating interactive charts. This blog post explores how to leverage ipecharts, a new Python library that seamlessly integrates Echarts into Jupyter Notebooks, to craft stunning visualizations within your notebooks.\n\n*Disclaimer: I am the author of this library.*", + "metadata": {} + }, + { + "id": "1a2ffcfd", + "cell_type": "markdown", + "source": "

• • •

", + "metadata": {} + }, + { + "id": "e2569313", + "cell_type": "markdown", + "source": "## Motivation\n\n`ipecharts` is not the first attempt to make Echarts available on Jupyter Notebooks. pyecharts is a popular open-source library that allows you to create interactive charts in Python and supports both notebooks and standalone Python scripts.", + "metadata": {} + }, + { + "id": "330ee266", + "cell_type": "markdown", + "source": "While pyechartscan create charts in the notebooks, it does not use the Jupyter Widgets system but instead injects HTML code into the notebook to render the charts. This approach makes using pyecharts in other Jupyter applications or interacting with other widgets libraries harder.", + "metadata": {} + }, + { + "id": "6e320817", + "cell_type": "markdown", + "source": "On the other hand, ipecharts adopts the native way of creating interactive charts in Jupyter Notebooks by using Jupyter Widgets. It makes the created charts compatible with a wide range of tools and libraries in the Jupyter ecosystem.", + "metadata": {} + }, + { + "id": "250f141f", + "cell_type": "markdown", + "source": "

• • •

", + "metadata": {} + }, + { + "id": "fc22d007", + "cell_type": "markdown", + "source": "## Getting started with ipecharts", + "metadata": {} + }, + { + "id": "b05b1479", + "cell_type": "markdown", + "source": "### Installation\n\n`ipecharts` is available on PyPI and conda-forge:\n\n```bash\n# Installing with pip\npip install ipecharts\n\n# Installing with conda\nconda install -c conda-forge ipecharts\n```\n\nIt requires ipywidgets ≥8.0 and does not work with Jupyter Notebook <7 . More detailed documentation is available at Read the Docs. You can also try it live in this JupyterLite instance.", + "metadata": {} + }, + { + "id": "ce613931", + "cell_type": "markdown", + "source": "### Creating a simple line plot\n\nipechart is a very slim wrapper outside of the Echarts Javascript library so translating the Javascript version of a chart into ipechartswidget is straightforward. Let’s begin with a basic line example from Echarts official documentation:\n\n```typescript\n// Example from https://echarts.apache.org/examples/en/editor.html?c=line-simple&lang=ts\nimport * as echarts from 'echarts';\n\ntype EChartsOption = echarts.EChartsOption;\n\nvar chartDom = document.getElementById('main')!;\nvar myChart = echarts.init(chartDom);\nvar option: EChartsOption;\n\noption = {\n xAxis: {\n type: 'category',\n data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']\n },\n yAxis: {\n type: 'value'\n },\n series: [\n {\n data: [150, 230, 224, 218, 135, 147, 260],\n type: 'line'\n }\n ]\n};\n\noption && myChart.setOption(option);\n```", + "metadata": {} + }, + { + "id": "7824f42f", + "cell_type": "markdown", + "source": "The entry point of a chart in ipecharts is the EchartWidget class:\n", + "metadata": {} + }, + { + "id": "3de1a807", + "cell_type": "code", + "source": "from ipecharts import EChartsWidget\nchart = EChartsWidget()", + "metadata": { + "specta": { + "showOutput": "No", + "showSource": "Yes" + }, + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + }, + "trusted": true + }, + "outputs": [], + "execution_count": null + }, + { + "id": "503e0e22", + "cell_type": "markdown", + "source": "Just as in the Javascript example, we need to set the option of this chart. For all top-level keys of the Echarts option and the entries of series, ipecharts provides Python class counterparts with the same name. Here is the equivalent of the above option object defined with ipecharts classes:\n", + "metadata": {} + }, + { + "id": "d8741975", + "cell_type": "code", + "source": "from ipecharts.option import Option, XAxis, YAxis\nfrom ipecharts.option.series import Line\n\nxAxis = XAxis(\n type=\"category\",\n data=[\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"],\n)\nyAxis = YAxis(type=\"value\")\nline = Line(data=[150, 230, 224, 218, 135, 147, 260])\n\noption = Option()\noption.xAxis = xAxis\noption.yAxis = yAxis\noption.series = [line]", + "metadata": { + "specta": { + "showOutput": "No", + "showSource": "Yes" + }, + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + }, + "trusted": true + }, + "outputs": [], + "execution_count": null + }, + { + "id": "c001b376", + "cell_type": "markdown", + "source": "All classes here are based on traitlets so you can initialize the instance by using keyword arguments or by setting the property values. Finally, updating the option value of our chart gives us the same chart as the Javascript \n", + "metadata": {} + }, + { + "id": "2b2c01c4", + "cell_type": "code", + "source": "chart.option = option\nchart", + "metadata": { + "specta": { + "showSource": "Yes", + "outputSize": "Big" + }, + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + }, + "trusted": true + }, + "outputs": [ + { + "execution_count": 3, + "output_type": "execute_result", + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "0d0ea8bc061e402e9701a0fcb405d171", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": "EChartsWidget(option=Option(angleAxis=None, aria=None, axisPointer=None, brush=None, calendar=None, dataset=No…" + }, + "metadata": {} + } + ], + "execution_count": null + }, + { + "id": "19dea3c1", + "cell_type": "markdown", + "source": "### Adding Interactivity", + "metadata": {} + }, + { + "id": "1105f857", + "cell_type": "markdown", + "source": "By using traitlets to configure your chart, any change in the option properties will be applied to the chart automatically. We will use the Button widget of ipywidgets to change the line data dynamically.\n", + "metadata": {} + }, + { + "id": "a5fb539d", + "cell_type": "code", + "source": "from ipywidgets.widgets import Button\nfrom numpy.random import randint\n\ndef update_line_data(b): \n line.data = randint(0, 300, 7).tolist()\n\nbutton = Button(description=\"Generate data\")\nbutton.on_click(update_line_data)\n\ndisplay(button, chart)", + "metadata": { + "specta": { + "showSource": "Yes" + }, + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + }, + "trusted": true + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9fa96cec05854444baf5920a57a3ab39", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": "Button(description='Generate data', style=ButtonStyle())" + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "0d0ea8bc061e402e9701a0fcb405d171", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": "EChartsWidget(option=Option(angleAxis=None, aria=None, axisPointer=None, brush=None, calendar=None, dataset=No…" + }, + "metadata": {} + } + ], + "execution_count": null + }, + { + "id": "0e491acc", + "cell_type": "markdown", + "source": "In the on_click callback of the button, we update the data property of the line instance, the changed signal is propagated up to the top-level widget and the chart will be updated automatically.", + "metadata": {} + }, + { + "id": "86b1b92d", + "cell_type": "markdown", + "source": "### Creating charts without using traitlets configuration", + "metadata": {} + }, + { + "id": "2e5acf08", + "cell_type": "markdown", + "source": "In many situations, we simply want to display the data without adding interactivity. For this use case, users can convert any option object used by a Javascript chart to a Python dictionary and pass it to the EchartRawWidget of ipecharts.\n\nHere is the equivalent of the Two Value-Axes in Polar example from the official Echarts documentation using EchartRawWidget:", + "metadata": {} + }, + { + "id": "933a5a49", + "cell_type": "code", + "source": "from ipecharts import EChartsRawWidget\nimport math\n\ndata = []\nfor i in range(101):\n theta = (i / 100) * 360\n r = 5 * (1 + math.sin((theta / 180) * math.pi))\n data.append([r, theta])\n\noption = {\n \"title\": {\"text\": \"Two Value-Axes in Polar\"},\n \"legend\": {\"data\": [\"line\"]},\n \"polar\": {},\n \"tooltip\": {\"trigger\": \"axis\", \"axisPointer\": {\"type\": \"cross\"}},\n \"angleAxis\": {\"type\": \"value\", \"startAngle\": 0},\n \"radiusAxis\": {},\n \"series\": [\n {\"coordinateSystem\": \"polar\", \"name\": \"line\", \"type\": \"line\", \"data\": data}\n ],\n}\nEChartsRawWidget(option=option)", + "metadata": { + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + }, + "trusted": true + }, + "outputs": [ + { + "execution_count": 5, + "output_type": "execute_result", + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "1330ce9e375e43d295c0bc50a128481c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": "EChartsRawWidget(option={'title': {'text': 'Two Value-Axes in Polar'}, 'legend': {'data': ['line']}, 'polar': …" + }, + "metadata": {} + } + ], + "execution_count": null + }, + { + "id": "213d1b6b", + "cell_type": "markdown", + "source": "## What’s next\n\nIn this first version, I focused on generating the option configuration class to be able to translate the Javascript charts to the Python ones without too many changes. Echarts has a lot of other customizations in theming, managing maps, or chart animation… These aspects will be addressed in future releases.\n\nYou can follow the development of this library on GitHub. Stay tuned and happy charting!", + "metadata": {} + }, + { + "id": "b7129618", + "cell_type": "markdown", + "source": "## About the author\n\nDuc Trung Le is an open-source developer who works on this project in his free time.", + "metadata": {} + }, + { + "id": "1dd7aa92", + "cell_type": "markdown", + "source": "", + "metadata": {} + } + ] + }, + "widgetStates": { + "version_major": 2, + "version_minor": 0, + "state": { + "f688e0a29ef04335b8ff42090fdec2f1": { + "model_name": "LayoutModel", + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_name": "LayoutModel", + "_model_module_version": "2.0.0", + "_view_module": "@jupyter-widgets/base", + "_view_name": "LayoutView", + "_view_module_version": "2.0.0", + "_view_count": null, + "align_content": null, + "align_items": null, + "align_self": null, + "border_top": null, + "border_right": null, + "border_bottom": null, + "border_left": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "overflow": null, + "order": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null, + "object_fit": null, + "object_position": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_gap": null, + "grid_template_rows": null, + "grid_template_columns": null, + "grid_template_areas": null, + "grid_row": null, + "grid_column": null, + "grid_area": null + } + }, + "619a85547b9849d4994fedf0e7636143": { + "model_name": "OptionModel", + "model_module": "ipecharts", + "model_module_version": "^1.3.1", + "state": { + "_model_module": "ipecharts", + "_model_name": "OptionModel", + "_model_module_version": "^1.3.1", + "_view_module": "ipecharts", + "_view_name": null, + "_view_module_version": "^1.3.1", + "_view_count": null, + "title": null, + "legend": null, + "grid": null, + "xAxis": null, + "yAxis": null, + "polar": null, + "radiusAxis": null, + "angleAxis": null, + "radar": null, + "tooltip": null, + "axisPointer": null, + "toolbox": null, + "brush": null, + "geo": null, + "parallel": null, + "parallelAxis": null, + "singleAxis": null, + "timeline": null, + "graphic": null, + "calendar": null, + "matrix": null, + "thumbnail": null, + "dataset": null, + "aria": null, + "color": null, + "textStyle": null, + "stateAnimation": null, + "options": null, + "globe": null, + "geo3D": null, + "mapbox3D": null, + "grid3D": null, + "xAxis3D": null, + "yAxis3D": null, + "zAxis3D": null, + "series": null, + "animation": null, + "animationDelay": null, + "animationDelayUpdate": null, + "animationDuration": null, + "animationDurationUpdate": null, + "animationEasing": null, + "animationEasingUpdate": null, + "animationThreshold": null, + "backgroundColor": null, + "blendMode": null, + "darkMode": null, + "dataZoom": null, + "hoverLayerThreshold": null, + "media": null, + "richInheritPlainLabel": null, + "unknown_attrs": null, + "useUTC": null, + "visualMap": null + } + }, + "0d0ea8bc061e402e9701a0fcb405d171": { + "model_name": "EChartsWidgetModel", + "model_module": "ipecharts", + "model_module_version": "^1.3.1", + "state": { + "_model_module": "ipecharts", + "_model_name": "EChartsWidgetModel", + "_model_module_version": "^1.3.1", + "_view_module": "ipecharts", + "_view_name": "EChartsWidgetView", + "_view_module_version": "^1.3.1", + "_view_count": null, + "_dom_classes": [], + "tabbable": null, + "tooltip": null, + "option": "IPY_MODEL_e5b40f7a9b844d3c9a8cb0ff71dde706", + "style": {}, + "theme": null, + "device_pixel_ratio": 0, + "renderer": "canvas", + "use_dirty_rect": false, + "use_coarse_pointer": false, + "pointer_size": null, + "width": "auto", + "height": "auto", + "locale": "EN", + "layout": "IPY_MODEL_f688e0a29ef04335b8ff42090fdec2f1" + } + }, + "6cbc12529969445d90bb73f252ab2c64": { + "model_name": "XAxisModel", + "model_module": "ipecharts", + "model_module_version": "^1.3.1", + "state": { + "_model_module": "ipecharts", + "_model_name": "XAxisModel", + "_model_module_version": "^1.3.1", + "_view_module": "ipecharts", + "_view_name": null, + "_view_module_version": "^1.3.1", + "_view_count": null, + "alignTicks": null, + "animation": null, + "animationDelay": null, + "animationDelayUpdate": null, + "animationDuration": null, + "animationDurationUpdate": null, + "animationEasing": null, + "animationEasingUpdate": null, + "animationThreshold": null, + "axisLabel": null, + "axisLine": null, + "axisPointer": null, + "axisTick": null, + "boundaryGap": null, + "breakArea": null, + "breakLabelLayout": null, + "breaks": null, + "data": [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun" + ], + "gridIndex": null, + "id": null, + "interval": null, + "inverse": null, + "jitter": null, + "jitterMargin": null, + "jitterOverlap": null, + "logBase": null, + "max": null, + "maxInterval": null, + "min": null, + "minInterval": null, + "minorSplitLine": null, + "minorTick": null, + "name": null, + "nameGap": null, + "nameLocation": null, + "nameMoveOverlap": null, + "nameRotate": null, + "nameTextStyle": null, + "nameTruncate": null, + "offset": null, + "position": null, + "scale": null, + "show": null, + "silent": null, + "splitArea": null, + "splitLine": null, + "splitNumber": null, + "startValue": null, + "tooltip": null, + "triggerEvent": null, + "type": "category", + "unknown_attrs": null, + "z": null, + "zlevel": null + } + }, + "b48fef8b930f45e199de52e65ef105f3": { + "model_name": "YAxisModel", + "model_module": "ipecharts", + "model_module_version": "^1.3.1", + "state": { + "_model_module": "ipecharts", + "_model_name": "YAxisModel", + "_model_module_version": "^1.3.1", + "_view_module": "ipecharts", + "_view_name": null, + "_view_module_version": "^1.3.1", + "_view_count": null, + "alignTicks": null, + "animation": null, + "animationDelay": null, + "animationDelayUpdate": null, + "animationDuration": null, + "animationDurationUpdate": null, + "animationEasing": null, + "animationEasingUpdate": null, + "animationThreshold": null, + "axisLabel": null, + "axisLine": null, + "axisPointer": null, + "axisTick": null, + "boundaryGap": null, + "breakArea": null, + "breakLabelLayout": null, + "breaks": null, + "data": null, + "gridIndex": null, + "id": null, + "interval": null, + "inverse": null, + "jitter": null, + "jitterMargin": null, + "jitterOverlap": null, + "logBase": null, + "max": null, + "maxInterval": null, + "min": null, + "minInterval": null, + "minorSplitLine": null, + "minorTick": null, + "name": null, + "nameGap": null, + "nameLocation": null, + "nameMoveOverlap": null, + "nameRotate": null, + "nameTextStyle": null, + "nameTruncate": null, + "offset": null, + "position": null, + "scale": null, + "show": null, + "silent": null, + "splitArea": null, + "splitLine": null, + "splitNumber": null, + "startValue": null, + "tooltip": null, + "triggerEvent": null, + "type": "value", + "unknown_attrs": null, + "z": null, + "zlevel": null + } + }, + "cc7bee19101045eea60644bdd587192b": { + "model_name": "LineModel", + "model_module": "ipecharts", + "model_module_version": "^1.3.1", + "state": { + "_model_module": "ipecharts", + "_model_name": "LineModel", + "_model_module_version": "^1.3.1", + "_view_module": "ipecharts", + "_view_name": null, + "_view_module_version": "^1.3.1", + "_view_count": null, + "animation": null, + "animationDelay": null, + "animationDelayUpdate": null, + "animationDuration": null, + "animationDurationUpdate": null, + "animationEasing": null, + "animationEasingUpdate": null, + "animationThreshold": null, + "areaStyle": null, + "blur": null, + "clip": null, + "colorBy": null, + "connectNulls": null, + "coord": null, + "coordinateSystem": null, + "coordinateSystemUsage": null, + "cursor": null, + "data": [ + 150, + 230, + 224, + 218, + 135, + 147, + 260 + ], + "dataGroupId": null, + "datasetIndex": null, + "dimensions": null, + "emphasis": null, + "encode": null, + "endLabel": null, + "id": null, + "itemStyle": null, + "label": null, + "labelLayout": null, + "labelLine": null, + "legendHoverLink": null, + "lineStyle": null, + "markArea": null, + "markLine": null, + "markPoint": null, + "name": null, + "polarId": null, + "polarIndex": null, + "sampling": null, + "select": null, + "selectedMode": null, + "seriesLayoutBy": null, + "showAllSymbol": null, + "showSymbol": null, + "silent": null, + "singleAxisId": null, + "singleAxisIndex": null, + "smooth": null, + "smoothMonotone": null, + "stack": null, + "stackOrder": null, + "stackStrategy": null, + "step": null, + "symbol": null, + "symbolKeepAspect": null, + "symbolOffset": null, + "symbolRotate": null, + "symbolSize": null, + "tooltip": null, + "triggerLineEvent": null, + "type": "line", + "universalTransition": null, + "unknown_attrs": null, + "xAxisId": null, + "xAxisIndex": null, + "yAxisId": null, + "yAxisIndex": null, + "z": null, + "zlevel": null + } + }, + "e5b40f7a9b844d3c9a8cb0ff71dde706": { + "model_name": "OptionModel", + "model_module": "ipecharts", + "model_module_version": "^1.3.1", + "state": { + "_model_module": "ipecharts", + "_model_name": "OptionModel", + "_model_module_version": "^1.3.1", + "_view_module": "ipecharts", + "_view_name": null, + "_view_module_version": "^1.3.1", + "_view_count": null, + "title": null, + "legend": null, + "grid": null, + "xAxis": "IPY_MODEL_6cbc12529969445d90bb73f252ab2c64", + "yAxis": "IPY_MODEL_b48fef8b930f45e199de52e65ef105f3", + "polar": null, + "radiusAxis": null, + "angleAxis": null, + "radar": null, + "tooltip": null, + "axisPointer": null, + "toolbox": null, + "brush": null, + "geo": null, + "parallel": null, + "parallelAxis": null, + "singleAxis": null, + "timeline": null, + "graphic": null, + "calendar": null, + "matrix": null, + "thumbnail": null, + "dataset": null, + "aria": null, + "color": null, + "textStyle": null, + "stateAnimation": null, + "options": null, + "globe": null, + "geo3D": null, + "mapbox3D": null, + "grid3D": null, + "xAxis3D": null, + "yAxis3D": null, + "zAxis3D": null, + "series": [ + "IPY_MODEL_cc7bee19101045eea60644bdd587192b" + ], + "animation": null, + "animationDelay": null, + "animationDelayUpdate": null, + "animationDuration": null, + "animationDurationUpdate": null, + "animationEasing": null, + "animationEasingUpdate": null, + "animationThreshold": null, + "backgroundColor": null, + "blendMode": null, + "darkMode": null, + "dataZoom": null, + "hoverLayerThreshold": null, + "media": null, + "richInheritPlainLabel": null, + "unknown_attrs": null, + "useUTC": null, + "visualMap": null + } + }, + "8674ae6d260147bcb87344ec55e143b6": { + "model_name": "LayoutModel", + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_name": "LayoutModel", + "_model_module_version": "2.0.0", + "_view_module": "@jupyter-widgets/base", + "_view_name": "LayoutView", + "_view_module_version": "2.0.0", + "_view_count": null, + "align_content": null, + "align_items": null, + "align_self": null, + "border_top": null, + "border_right": null, + "border_bottom": null, + "border_left": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "overflow": null, + "order": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null, + "object_fit": null, + "object_position": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_gap": null, + "grid_template_rows": null, + "grid_template_columns": null, + "grid_template_areas": null, + "grid_row": null, + "grid_column": null, + "grid_area": null + } + }, + "3170f445049b498e9b7147a6dd08425f": { + "model_name": "ButtonStyleModel", + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_name": "ButtonStyleModel", + "_model_module_version": "2.0.0", + "_view_module": "@jupyter-widgets/base", + "_view_name": "StyleView", + "_view_module_version": "2.0.0", + "_view_count": null, + "button_color": null, + "font_family": null, + "font_size": null, + "font_style": null, + "font_variant": null, + "font_weight": null, + "text_color": null, + "text_decoration": null + } + }, + "9fa96cec05854444baf5920a57a3ab39": { + "model_name": "ButtonModel", + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_name": "ButtonModel", + "_model_module_version": "2.0.0", + "_view_module": "@jupyter-widgets/controls", + "_view_name": "ButtonView", + "_view_module_version": "2.0.0", + "_view_count": null, + "_dom_classes": [], + "tabbable": null, + "tooltip": null, + "description": "Generate data", + "disabled": false, + "icon": "", + "button_style": "", + "style": "IPY_MODEL_3170f445049b498e9b7147a6dd08425f", + "layout": "IPY_MODEL_8674ae6d260147bcb87344ec55e143b6" + } + }, + "458c5f41c29f4afe9e65e5850ace77fa": { + "model_name": "LayoutModel", + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_name": "LayoutModel", + "_model_module_version": "2.0.0", + "_view_module": "@jupyter-widgets/base", + "_view_name": "LayoutView", + "_view_module_version": "2.0.0", + "_view_count": null, + "align_content": null, + "align_items": null, + "align_self": null, + "border_top": null, + "border_right": null, + "border_bottom": null, + "border_left": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "overflow": null, + "order": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null, + "object_fit": null, + "object_position": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_gap": null, + "grid_template_rows": null, + "grid_template_columns": null, + "grid_template_areas": null, + "grid_row": null, + "grid_column": null, + "grid_area": null + } + }, + "1330ce9e375e43d295c0bc50a128481c": { + "model_name": "EChartsRawWidgetModel", + "model_module": "ipecharts", + "model_module_version": "^1.3.1", + "state": { + "_model_module": "ipecharts", + "_model_name": "EChartsRawWidgetModel", + "_model_module_version": "^1.3.1", + "_view_module": "ipecharts", + "_view_name": "EChartsRawWidgetView", + "_view_module_version": "^1.3.1", + "_view_count": null, + "_dom_classes": [], + "tabbable": null, + "tooltip": null, + "option": { + "angleAxis": { + "startAngle": 0, + "type": "value" + }, + "legend": { + "data": [ + "line" + ] + }, + "polar": {}, + "radiusAxis": {}, + "series": [ + { + "coordinateSystem": "polar", + "data": [ + [ + 5, + 0 + ], + [ + 5.313952597646567, + 3.6 + ], + [ + 5.626666167821521, + 7.2 + ], + [ + 5.936906572928623, + 10.799999999999999 + ], + [ + 6.2434494358242745, + 14.4 + ], + [ + 6.545084971874737, + 18 + ], + [ + 6.84062276342339, + 21.599999999999998 + ], + [ + 7.1288964578253635, + 25.200000000000003 + ], + [ + 7.408768370508576, + 28.8 + ], + [ + 7.679133974894983, + 32.4 + ], + [ + 7.938926261462366, + 36 + ], + [ + 8.18711994874345, + 39.6 + ], + [ + 8.422735529643443, + 43.199999999999996 + ], + [ + 8.644843137107058, + 46.800000000000004 + ], + [ + 8.852566213878946, + 50.400000000000006 + ], + [ + 9.045084971874736, + 54 + ], + [ + 9.221639627510076, + 57.6 + ], + [ + 9.381533400219318, + 61.2 + ], + [ + 9.524135262330098, + 64.8 + ], + [ + 9.648882429441256, + 68.4 + ], + [ + 9.755282581475768, + 72 + ], + [ + 9.842915805643155, + 75.6 + ], + [ + 9.911436253643442, + 79.2 + ], + [ + 9.96057350657239, + 82.8 + ], + [ + 9.990133642141357, + 86.39999999999999 + ], + [ + 10, + 90 + ], + [ + 9.990133642141357, + 93.60000000000001 + ], + [ + 9.96057350657239, + 97.2 + ], + [ + 9.911436253643442, + 100.80000000000001 + ], + [ + 9.842915805643155, + 104.39999999999999 + ], + [ + 9.755282581475768, + 108 + ], + [ + 9.648882429441256, + 111.6 + ], + [ + 9.524135262330098, + 115.2 + ], + [ + 9.381533400219318, + 118.80000000000001 + ], + [ + 9.221639627510074, + 122.4 + ], + [ + 9.045084971874736, + 125.99999999999999 + ], + [ + 8.852566213878946, + 129.6 + ], + [ + 8.644843137107056, + 133.2 + ], + [ + 8.422735529643443, + 136.8 + ], + [ + 8.18711994874345, + 140.4 + ], + [ + 7.938926261462367, + 144 + ], + [ + 7.6791339748949845, + 147.6 + ], + [ + 7.408768370508579, + 151.2 + ], + [ + 7.128896457825363, + 154.8 + ], + [ + 6.840622763423391, + 158.4 + ], + [ + 6.545084971874737, + 162 + ], + [ + 6.243449435824276, + 165.6 + ], + [ + 5.936906572928625, + 169.2 + ], + [ + 5.626666167821525, + 172.79999999999998 + ], + [ + 5.313952597646567, + 176.4 + ], + [ + 5.000000000000001, + 180 + ], + [ + 4.6860474023534335, + 183.6 + ], + [ + 4.373333832178479, + 187.20000000000002 + ], + [ + 4.063093427071376, + 190.8 + ], + [ + 3.7565505641757246, + 194.4 + ], + [ + 3.454915028125262, + 198.00000000000003 + ], + [ + 3.159377236576608, + 201.60000000000002 + ], + [ + 2.871103542174639, + 205.2 + ], + [ + 2.591231629491425, + 208.79999999999998 + ], + [ + 2.3208660251050177, + 212.39999999999998 + ], + [ + 2.061073738537635, + 216 + ], + [ + 1.8128800512565517, + 219.6 + ], + [ + 1.5772644703565564, + 223.2 + ], + [ + 1.3551568628929433, + 226.8 + ], + [ + 1.147433786121053, + 230.4 + ], + [ + 0.9549150281252633, + 234 + ], + [ + 0.7783603724899235, + 237.60000000000002 + ], + [ + 0.6184665997806821, + 241.20000000000002 + ], + [ + 0.475864737669901, + 244.8 + ], + [ + 0.3511175705587444, + 248.39999999999998 + ], + [ + 0.24471741852423234, + 251.99999999999997 + ], + [ + 0.15708419435684517, + 255.6 + ], + [ + 0.0885637463565564, + 259.2 + ], + [ + 0.039426493427611176, + 262.8 + ], + [ + 0.009866357858642205, + 266.4 + ], + [ + 0, + 270 + ], + [ + 0.009866357858642205, + 273.6 + ], + [ + 0.03942649342761062, + 277.2 + ], + [ + 0.0885637463565564, + 280.8 + ], + [ + 0.15708419435684573, + 284.40000000000003 + ], + [ + 0.2447174185242318, + 288 + ], + [ + 0.3511175705587438, + 291.6 + ], + [ + 0.47586473766990045, + 295.2 + ], + [ + 0.6184665997806832, + 298.8 + ], + [ + 0.7783603724899224, + 302.4 + ], + [ + 0.9549150281252622, + 306 + ], + [ + 1.1474337861210548, + 309.6 + ], + [ + 1.3551568628929422, + 313.2 + ], + [ + 1.5772644703565553, + 316.8 + ], + [ + 1.8128800512565486, + 320.4 + ], + [ + 2.061073738537633, + 324 + ], + [ + 2.320866025105018, + 327.6 + ], + [ + 2.5912316294914195, + 331.2 + ], + [ + 2.871103542174639, + 334.8 + ], + [ + 3.1593772365766064, + 338.4 + ], + [ + 3.454915028125262, + 342 + ], + [ + 3.7565505641757193, + 345.59999999999997 + ], + [ + 4.063093427071377, + 349.2 + ], + [ + 4.373333832178476, + 352.8 + ], + [ + 4.6860474023534335, + 356.4 + ], + [ + 4.999999999999999, + 360 + ] + ], + "name": "line", + "type": "line" + } + ], + "title": { + "text": "Two Value-Axes in Polar" + }, + "tooltip": { + "axisPointer": { + "type": "cross" + }, + "trigger": "axis" + } + }, + "style": {}, + "theme": null, + "device_pixel_ratio": 0, + "renderer": "canvas", + "use_dirty_rect": false, + "use_coarse_pointer": false, + "pointer_size": null, + "width": "auto", + "height": "auto", + "locale": "EN", + "layout": "IPY_MODEL_458c5f41c29f4afe9e65e5850ace77fa" + } + } + } + } } }, "nbformat_minor": 5, diff --git a/src/document/factory.tsx b/src/document/factory.tsx index b39a7fb..15a0b41 100644 --- a/src/document/factory.tsx +++ b/src/document/factory.tsx @@ -106,7 +106,6 @@ export class NotebookGridWidgetFactory extends ABCWidgetFactory< if (titleWidget) { titleWidget.addClass('specta-topbar-title-wrapper'); } - console.log('im here adding menu to topbar'); this._spectaTopbar.addReactWidget(menu, 'right', 10000); } } @@ -114,7 +113,6 @@ export class NotebookGridWidgetFactory extends ABCWidgetFactory< this._shell.hideTopBar(); } - console.log('im here', topbarTarget); if (spectaWidget) { content.addWidget(spectaWidget); if (topbarTarget?.addReactWidget) { diff --git a/src/specta_model.ts b/src/specta_model.ts index 08223d2..666f316 100644 --- a/src/specta_model.ts +++ b/src/specta_model.ts @@ -1,4 +1,4 @@ -import { ISessionContext } from '@jupyterlab/apputils'; +import { Dialog, ISessionContext, showDialog } from '@jupyterlab/apputils'; import { CodeCell, CodeCellModel, @@ -24,7 +24,7 @@ import { OutputAreaModel, SimplifiedOutputArea } from '@jupyterlab/outputarea'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { KernelSpec, ServiceManager } from '@jupyterlab/services'; import { IExecuteReplyMsg } from '@jupyterlab/services/lib/kernel/messages'; -import { PromiseDelegate } from '@lumino/coreutils'; +import { PromiseDelegate, UUID } from '@lumino/coreutils'; import { createNotebookContext, @@ -121,16 +121,37 @@ export class AppModel { filePath: this._filePath }); if (this._staticRender) { - const notebookModel = JSON.parse( - JSON.stringify(this._notebookModelJson) - ) as INotebookContentWithSnapshot; - const snapshot = notebookModel.metadata.spectaSnapshot; + const snapshotStatus = this.snapshotStatus(); + if (snapshotStatus === 'out-of-sync') { + const response = await showDialog({ + body: 'Do you want to use existing snapshot or re-run the notebook using a kernel?', + title: 'Snapshot out of sync', + buttons: [ + Dialog.cancelButton({ label: 'Continue' }), + Dialog.okButton({ label: 'Activate kernel' }) + ] + }); + if (response.button.accept) { + this._staticRender = false; + } + } + } + if (this._staticRender) { + const snapshot = this.getSnapshot(); if (!snapshot) { throw new Error('Snapshot not found'); } + if (!snapshot?.notebook) { + throw new Error('Snapshot notebook not found'); + } + const notebookModel = JSON.parse( + JSON.stringify(snapshot.notebook) + ) as INotebookContentWithSnapshot; + notebookModel.metadata['widgets'] = { [WIDGET_STATE_MIMETYPE]: snapshot.widgetStates } as any; + this._context.model.fromJSON(notebookModel); this._notebookPanel = createNotebookPanel({ context: this._context!, @@ -138,9 +159,10 @@ export class AppModel { editorServices: this.options.editorServices }); await this._context.sessionContext.initialize(); + const kernelUUID = UUID.uuid4(); (this._context.sessionContext as any)._session = { kernel: { - id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + id: kernelUUID, registerCommTarget: () => {}, handleComms: false, requestCommInfo: async () => ({ content: { status: undefined } }) @@ -290,7 +312,6 @@ export class AppModel { } snapshotStatus(): 'out-of-sync' | 'in-sync' | 'not-exist' { - console.log('ggggggg', this); const sn = this.getSnapshot(); if (!sn) { return 'not-exist'; diff --git a/src/specta_widget.ts b/src/specta_widget.ts index f196d80..860fbbf 100644 --- a/src/specta_widget.ts +++ b/src/specta_widget.ts @@ -204,13 +204,14 @@ export class AppWidget extends Panel { notebook, widgetStates: null }; - for (const el of this._outputs) { + for (const [idx, el] of this._outputs.entries()) { if (el.info.hidden || el.info.cellModel?.cell_type !== 'code') { continue; } const output = el.cellOutput as SimplifiedOutputArea; const outputModels = output.model.toJSON(); + notebook.cells[idx].outputs = outputModels; if (!snapshot.widgetStates) { for (let index = 0; index < outputModels.length; index++) { const data = @@ -254,7 +255,6 @@ export class AppWidget extends Panel { } } } - console.log('done', snapshot); await this._model.saveSnapshotToMetadata(snapshot); return timestamp; } diff --git a/src/topbar/settingDialog.tsx b/src/topbar/settingDialog.tsx index 2ec93d5..2d49b6b 100644 --- a/src/topbar/settingDialog.tsx +++ b/src/topbar/settingDialog.tsx @@ -274,12 +274,14 @@ export const SettingContent = (props: { flexDirection: 'column' }} > -
- Last snapshot:{' '} - {currentTimestamp - ? new Date(currentTimestamp).toLocaleString() - : 'Unavailable'} -
+ {snapshotStatus !== 'not-exist' && ( +
+ Last snapshot:{' '} + {currentTimestamp + ? new Date(currentTimestamp).toLocaleString() + : 'Unavailable'} +
+ )}
{snapshotStatus === 'not-exist' ? 'No snapshot found' From 4ce00d97155eff0b2ed2b32c0426b232be5eca71 Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Mon, 17 Aug 2026 16:51:39 +0200 Subject: [PATCH 05/13] done --- src/document/factory.tsx | 24 ++---- src/specta_model.ts | 46 +++++++----- src/specta_widget.ts | 17 ++++- src/tool.ts | 3 +- src/topbar/index.tsx | 1 + src/topbar/menuComponent.tsx | 4 + src/topbar/settingDialog.tsx | 142 +++++++++++++++++++++++++---------- 7 files changed, 157 insertions(+), 80 deletions(-) diff --git a/src/document/factory.tsx b/src/document/factory.tsx index 15a0b41..c16b4f2 100644 --- a/src/document/factory.tsx +++ b/src/document/factory.tsx @@ -4,8 +4,6 @@ import { INotebookModel } from '@jupyterlab/notebook'; import { Panel } from '@lumino/widgets'; import * as React from 'react'; -import { ExportIcon } from '../components/icon/export'; -import { IconButton } from '../components/iconButton'; import { SpectaWidgetFactory } from '../specta_widget_factory'; import { ISpectaLayoutRegistry, @@ -27,6 +25,7 @@ interface IOptions extends DocumentRegistry.IWidgetFactoryOptions { spectaLayoutRegistry: ISpectaLayoutRegistry; spectaTopbar: ISpectaTopbarWidget; uiSwitcher?: ISpectaUiSwitcher | null; + supportStaticRendering?: boolean; } export class NotebookGridWidgetFactory extends ABCWidgetFactory< @@ -40,6 +39,7 @@ export class NotebookGridWidgetFactory extends ABCWidgetFactory< this._themeManager = options.themeManager; this._spectaTopbar = options.spectaTopbar; this._uiSwitcher = options.uiSwitcher; + this._supportStaticRendering = options.supportStaticRendering; } protected createNewWidget( @@ -59,7 +59,6 @@ export class NotebookGridWidgetFactory extends ABCWidgetFactory< context }); const isSpecta = isSpectaApp(); - let topbarTarget: ISpectaTopbarWidget | undefined; if (!spectaConfig.hideTopbar) { const title = ; let topbarWidget: ISpectaTopbarWidget | undefined = undefined; @@ -85,6 +84,8 @@ export class NotebookGridWidgetFactory extends ABCWidgetFactory< settingsIconChanged={topbarWidget?.settingsIconChanged} customIcon={topbarWidget?.customIcon} spectaWidget={spectaWidget} + isSpectaApp={isSpecta} + enableStaticRenderingConfig={Boolean(this._supportStaticRendering)} /> ); @@ -95,7 +96,6 @@ export class NotebookGridWidgetFactory extends ABCWidgetFactory< } localTopbar.addReactWidget(menu, 'right', 10000); content.addWidget(localTopbar); - topbarTarget = localTopbar; } else { if (this._spectaTopbar.addReactWidget) { const titleWidget = this._spectaTopbar.addReactWidget( @@ -115,21 +115,6 @@ export class NotebookGridWidgetFactory extends ABCWidgetFactory< if (spectaWidget) { content.addWidget(spectaWidget); - if (topbarTarget?.addReactWidget) { - const button = ( - await spectaWidget.saveSnapshot()} - icon={ - - } - /> - ); - topbarTarget.addReactWidget(button, 'right', 9999); - } } }); @@ -146,4 +131,5 @@ export class NotebookGridWidgetFactory extends ABCWidgetFactory< private _themeManager?: IThemeManager; private _spectaTopbar: ISpectaTopbarWidget; private _uiSwitcher?: ISpectaUiSwitcher | null; + private _supportStaticRendering?: boolean; } diff --git a/src/specta_model.ts b/src/specta_model.ts index 666f316..eeeb619 100644 --- a/src/specta_model.ts +++ b/src/specta_model.ts @@ -1,4 +1,4 @@ -import { Dialog, ISessionContext, showDialog } from '@jupyterlab/apputils'; +import { Dialog, showDialog } from '@jupyterlab/apputils'; import { CodeCell, CodeCellModel, @@ -47,14 +47,7 @@ export class AppModel { this._notebookModelJson.metadata[SPECTA_SNAPSHOT_KEY] ); this._filePath = options.context.localPath; - this._kernelPreference = { - shouldStart: !this._staticRender, - canStart: !this._staticRender, - shutdownOnDispose: true, - name: options.context.model.defaultKernelName, - autoStartDefault: !this._staticRender, - language: options.context.model.defaultKernelLanguage - }; + this._manager = options.manager; options.context.fileChanged.connect(e => { this._fileChanged.emit(e.model.cells); @@ -83,10 +76,6 @@ export class AppModel { return this._staticRender; } - set staticRender(v: boolean) { - this._staticRender = v; - } - get snapshotData(): ISpectaSnapshotData | undefined { return this._notebookModelJson.metadata[SPECTA_SNAPSHOT_KEY]; } @@ -115,11 +104,6 @@ export class AppModel { return this._notebookPanel; } async initialize(): Promise { - this._context = await createNotebookContext({ - manager: this._manager, - kernelPreference: this._kernelPreference, - filePath: this._filePath - }); if (this._staticRender) { const snapshotStatus = this.snapshotStatus(); if (snapshotStatus === 'out-of-sync') { @@ -133,9 +117,24 @@ export class AppModel { }); if (response.button.accept) { this._staticRender = false; + } else { + this._staticRender = true; } } } + const kernelPreference = { + shouldStart: !this._staticRender, + canStart: !this._staticRender, + shutdownOnDispose: true, + name: this.options.context.model.defaultKernelName, + autoStartDefault: !this._staticRender, + language: this.options.context.model.defaultKernelLanguage + }; + this._context = await createNotebookContext({ + manager: this._manager, + kernelPreference: kernelPreference, + filePath: this._filePath + }); if (this._staticRender) { const snapshot = this.getSnapshot(); if (!snapshot) { @@ -274,6 +273,16 @@ export class AppModel { return item; } + async turnOffStaticRender() { + if (!this._staticRender) { + return; + } + this._staticRender = false; + this._notebookPanel?.dispose(); + + await this.initialize(); + } + async executeCell( cell: ICellModel, outputWrapper: SpectaCellOutput @@ -347,7 +356,6 @@ export class AppModel { private _notebookModelJson: INotebookContentWithSnapshot; private _isDisposed = false; private _manager: ServiceManager.IManager; - private _kernelPreference: ISessionContext.IKernelPreference; private _fileChanged = new Signal(this); private _filePath: string; private _kernelSpecManager: KernelSpec.IManager; diff --git a/src/specta_widget.ts b/src/specta_widget.ts index 860fbbf..b81a8ba 100644 --- a/src/specta_widget.ts +++ b/src/specta_widget.ts @@ -62,7 +62,9 @@ export class AppWidget extends Panel { this ); this._model.fileChanged.connect((_, newCells) => { - this.rerender(newCells); + if (!this._model.staticRender) { + this.rerender(newCells); + } }); } @@ -150,7 +152,10 @@ export class AppWidget extends Panel { }); } - async rerender(newCells: CellList): Promise { + async rerender(newCells?: CellList): Promise { + if (!newCells) { + newCells = this.model.cells; + } this.addSpinner(); for (const element of this._outputs) { element.dispose(); @@ -177,6 +182,14 @@ export class AppWidget extends Panel { this.removeSpinner(); } + async turnOffStaticRender() { + if (!this._model.staticRender) { + return; + } + await this._model.turnOffStaticRender(); + await this.rerender(); + } + async saveSnapshot(): Promise { if (this._model.staticRender) { return; diff --git a/src/tool.ts b/src/tool.ts index 2e53e7d..23b2de4 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -95,7 +95,8 @@ export function registerDocumentFactory(options: { themeManager, spectaLayoutRegistry, spectaTopbar, - uiSwitcher + uiSwitcher, + supportStaticRendering: true }); // Registering the widget factory diff --git a/src/topbar/index.tsx b/src/topbar/index.tsx index 69382a4..ff7fc15 100644 --- a/src/topbar/index.tsx +++ b/src/topbar/index.tsx @@ -76,6 +76,7 @@ export const topbarPlugin: JupyterFrontEndPlugin< currentUi={isSpecta ? 'specta' : 'lab'} settingsIconChanged={widget.settingsIconChanged} customIcon={widget.customIcon} + enableStaticRenderingConfig={false} /> ); widget.addReactWidget(menu, 'right', 10000); diff --git a/src/topbar/menuComponent.tsx b/src/topbar/menuComponent.tsx index a883cc5..7c6b917 100644 --- a/src/topbar/menuComponent.tsx +++ b/src/topbar/menuComponent.tsx @@ -18,6 +18,8 @@ interface IProps { currentUi?: string; settingsIconChanged?: ISignal; customIcon?: JSX.Element; + isSpectaApp?: boolean; + enableStaticRenderingConfig?: boolean; } export function MenuComponent(props: IProps): JSX.Element { @@ -84,6 +86,8 @@ export function MenuComponent(props: IProps): JSX.Element { currentPath={props.currentPath} currentUi={props.currentUi} spectaWidget={props.spectaWidget} + isSpectaApp={props.isSpectaApp} + enableStaticRenderingConfig={props.enableStaticRenderingConfig} />
)} diff --git a/src/topbar/settingDialog.tsx b/src/topbar/settingDialog.tsx index 2d49b6b..ca7e8df 100644 --- a/src/topbar/settingDialog.tsx +++ b/src/topbar/settingDialog.tsx @@ -25,6 +25,8 @@ export const SettingContent = (props: { currentPath?: string | null; currentUi?: string; spectaWidget?: AppWidget; + isSpectaApp?: boolean; + enableStaticRenderingConfig?: boolean; }) => { const { themeManager, layoutRegistry, settingsWidgets } = props; const [themeOptions, setThemeOptions] = useState([ @@ -130,6 +132,9 @@ export const SettingContent = (props: { [settingsWidgets] ); + const [isStaticRendering, setIsStaticRendering] = useState( + Boolean(props.spectaWidget?.model?.staticRender) + ); const { uiSwitcher, currentPath } = props; const onUiChange = useCallback( (e: React.ChangeEvent) => { @@ -155,12 +160,17 @@ export const SettingContent = (props: { }, [props.spectaWidget, snapshotStatus]); const createSnapshot = useCallback(async () => { + if (isStaticRendering) { + return; + } + setCreatingSnapshot(true); const timestamp = await props.spectaWidget?.saveSnapshot(); if (timestamp) { setSnapshotStatus('in-sync'); } setCurrentTimestamp(timestamp); - }, [props.spectaWidget]); + setCreatingSnapshot(false); + }, [props.spectaWidget, isStaticRendering]); const snapshot = useMemo( () => props.spectaWidget?.model?.getSnapshot(), @@ -170,6 +180,13 @@ export const SettingContent = (props: { snapshot?.timestamp ); + const activateKernel = useCallback(async () => { + await props.spectaWidget?.turnOffStaticRender(); + setIsStaticRendering(false); + }, [props.spectaWidget]); + + const [creatingSnapshot, setCreatingSnapshot] = useState(false); + return (

@@ -262,51 +279,98 @@ export const SettingContent = (props: {

)} -
- -
- {snapshotStatus !== 'not-exist' && ( + {props.enableStaticRenderingConfig && ( +
+ +
+ {snapshotStatus !== 'not-exist' && ( +
+ Last snapshot:{' '} + {currentTimestamp + ? new Date(currentTimestamp).toLocaleString() + : 'Unavailable'} +
+ )}
- Last snapshot:{' '} - {currentTimestamp - ? new Date(currentTimestamp).toLocaleString() - : 'Unavailable'} + {snapshotStatus === 'not-exist' + ? 'No snapshot found' + : snapshotStatus === 'out-of-sync' + ? 'Snapshot is out of sync with the notebook' + : ''}
- )} -
- {snapshotStatus === 'not-exist' - ? 'No snapshot found' - : snapshotStatus === 'out-of-sync' - ? 'Snapshot is out of sync with the notebook' - : ''} -
-
- - + +
+
- Create snapshot - + +
-
+ )} {settingsWidgets && settingsWidgets.length > 0 && (
From 1f6913c79a879023e579904245d6daab25e23e86 Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Mon, 17 Aug 2026 18:52:40 +0200 Subject: [PATCH 06/13] refactor --- src/snapshot/tools.ts | 27 +++++++++++++ src/specta_cell_output.tsx | 3 ++ src/specta_model.ts | 77 ++++++++++++++++++-------------------- src/specta_widget.ts | 46 ++++++++++++++--------- src/tool.ts | 26 ++++--------- 5 files changed, 102 insertions(+), 77 deletions(-) diff --git a/src/snapshot/tools.ts b/src/snapshot/tools.ts index 1992e02..622de26 100644 --- a/src/snapshot/tools.ts +++ b/src/snapshot/tools.ts @@ -1,4 +1,5 @@ import { type INotebookContent } from '@jupyterlab/nbformat'; + export const WIDGET_STATE_MIMETYPE = 'application/vnd.jupyter.widget-state+json'; export const WIDGET_VIEW_MIMETYPE = 'application/vnd.jupyter.widget-view+json'; @@ -29,3 +30,29 @@ export type INotebookContentWithSnapshot = INotebookContent & { [SPECTA_SNAPSHOT_KEY]: ISpectaSnapshotData | undefined; }; }; +export function computeHash(str: string): string { + const s = str.replace(/\s+/g, ' ').trim(); + let h1 = 0xdeadbeef, + h2 = 0x41c6ce57; + for (let i = 0; i < s.length; i++) { + const c = s.charCodeAt(i); + h1 = Math.imul(h1 ^ c, 2654435761); + h2 = Math.imul(h2 ^ c, 1597334677); + } + h1 = + Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ + Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = + Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ + Math.imul(h1 ^ (h1 >>> 13), 3266489909); + return ( + (h2 >>> 0).toString(16).padStart(8, '0') + + (h1 >>> 0).toString(16).padStart(8, '0') + ); +} + +export function snapshotHash(sources: Array): string { + return computeHash( + sources.map(s => (Array.isArray(s) ? s.join('') : s)).join('\n') + ); +} diff --git a/src/specta_cell_output.tsx b/src/specta_cell_output.tsx index 3e5ab1c..04b447e 100644 --- a/src/specta_cell_output.tsx +++ b/src/specta_cell_output.tsx @@ -9,6 +9,7 @@ import { ISpectaCellConfig } from './token'; export interface ICellInfo { hidden?: boolean; cellModel?: nbformat.ICell; + cellIndex?: number; } export class SpectaCellOutput extends Panel { constructor({ @@ -57,6 +58,8 @@ export class SpectaCellOutput extends Panel { } readonly cellIdentity: string; + executionDone: Promise = Promise.resolve(); + get cellOutput(): Widget { return this._cellOutput; } diff --git a/src/specta_model.ts b/src/specta_model.ts index eeeb619..d5f2c4e 100644 --- a/src/specta_model.ts +++ b/src/specta_model.ts @@ -23,7 +23,6 @@ import { import { OutputAreaModel, SimplifiedOutputArea } from '@jupyterlab/outputarea'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { KernelSpec, ServiceManager } from '@jupyterlab/services'; -import { IExecuteReplyMsg } from '@jupyterlab/services/lib/kernel/messages'; import { PromiseDelegate, UUID } from '@lumino/coreutils'; import { @@ -31,26 +30,28 @@ import { createNotebookPanel } from './create_notebook_panel'; import { SpectaCellOutput } from './specta_cell_output'; -import { computeHash, emitResizeEvent, readCellConfig } from './tool'; +import { emitResizeEvent, readCellConfig } from './tool'; import { ISpectaSnapshotData, SPECTA_SNAPSHOT_KEY, WIDGET_STATE_MIMETYPE, - INotebookContentWithSnapshot + INotebookContentWithSnapshot, + snapshotHash } from './snapshot/tools'; import { ISignal, Signal } from '@lumino/signaling'; export class AppModel { constructor(private options: AppModel.IOptions) { - this._notebookModelJson = options.context.model.toJSON() as any; - this._staticRender = Boolean( - this._notebookModelJson.metadata[SPECTA_SNAPSHOT_KEY] - ); + this._staticRender = Boolean(this.getSnapshot()); this._filePath = options.context.localPath; - this._manager = options.manager; - options.context.fileChanged.connect(e => { - this._fileChanged.emit(e.model.cells); + + options.context.fileChanged.connect(context => { + if (!this._context || this._staticRender) { + return; + } + this._context.model.fromJSON(this._documentJson()); + this._fileChanged.emit(this._context.model.cells); }); this._kernelSpecManager = options.kernelSpecManager; const specs = this._kernelSpecManager.specs; @@ -76,10 +77,6 @@ export class AppModel { return this._staticRender; } - get snapshotData(): ISpectaSnapshotData | undefined { - return this._notebookModelJson.metadata[SPECTA_SNAPSHOT_KEY]; - } - dispose(): void { if (this.isDisposed) { return; @@ -137,15 +134,10 @@ export class AppModel { }); if (this._staticRender) { const snapshot = this.getSnapshot(); - if (!snapshot) { - throw new Error('Snapshot not found'); - } if (!snapshot?.notebook) { throw new Error('Snapshot notebook not found'); } - const notebookModel = JSON.parse( - JSON.stringify(snapshot.notebook) - ) as INotebookContentWithSnapshot; + const notebookModel = snapshot.notebook as INotebookContentWithSnapshot; notebookModel.metadata['widgets'] = { [WIDGET_STATE_MIMETYPE]: snapshot.widgetStates @@ -170,7 +162,7 @@ export class AppModel { (this.options.tracker as any).add(this._notebookPanel); } else { - this._context.model.fromJSON(this._notebookModelJson); + this._context.model.fromJSON(this._documentJson()); this._notebookPanel = createNotebookPanel({ context: this._context!, rendermime: this.options.rendermime, @@ -286,7 +278,7 @@ export class AppModel { async executeCell( cell: ICellModel, outputWrapper: SpectaCellOutput - ): Promise { + ): Promise { if (cell.type !== 'code' || !this._context) { return; } @@ -313,11 +305,13 @@ export class AppModel { emitResizeEvent(); outputWrapper.removePlaceholder(); }); - return rep; + return Promise.all([rep, output.future.done]); } getSnapshot(): ISpectaSnapshotData | undefined { - return this._notebookModelJson.metadata[SPECTA_SNAPSHOT_KEY]; + return this.options.context.model.getMetadata( + SPECTA_SNAPSHOT_KEY + ) as unknown as ISpectaSnapshotData | undefined; } snapshotStatus(): 'out-of-sync' | 'in-sync' | 'not-exist' { @@ -325,35 +319,36 @@ export class AppModel { if (!sn) { return 'not-exist'; } - const hash = computeHash( - this._notebookModelJson.cells.map(it => it.source).join('\n') + + const sources = Array.from(this.options.context.model.cells, cell => + cell.sharedModel.getSource() ); - if (hash === sn.hash) { - return 'in-sync'; - } - return 'out-of-sync'; + return snapshotHash(sources) === sn.hash ? 'in-sync' : 'out-of-sync'; } async saveSnapshotToMetadata(snapshot: ISpectaSnapshotData): Promise { - if (this.options.context) { - this.options.context.model.setMetadata(SPECTA_SNAPSHOT_KEY, snapshot); - await this.options.context.save(); - this.options.context.model.dirty = false; - } + this.options.context.model.setMetadata(SPECTA_SNAPSHOT_KEY, snapshot); + await this.options.context.save(); + this.options.context.model.dirty = false; } async deleteSnapshot(): Promise { - if (this.options.context) { - this.options.context.model.deleteMetadata(SPECTA_SNAPSHOT_KEY); - await this.options.context.save(); - this.options.context.model.dirty = false; - } + this.options.context.model.deleteMetadata(SPECTA_SNAPSHOT_KEY); + await this.options.context.save(); + this.options.context.model.dirty = false; + } + + private _documentJson(): INotebookContentWithSnapshot { + const json = + this.options.context.model.toJSON() as INotebookContentWithSnapshot; + delete json.metadata[SPECTA_SNAPSHOT_KEY]; + return json; } private _kernelReady = new PromiseDelegate(); private _notebookPanel?: NotebookPanel; private _context?: DocumentRegistry.IContext; - private _notebookModelJson: INotebookContentWithSnapshot; + private _isDisposed = false; private _manager: ServiceManager.IManager; private _fileChanged = new Signal(this); diff --git a/src/specta_widget.ts b/src/specta_widget.ts index b81a8ba..ee56394 100644 --- a/src/specta_widget.ts +++ b/src/specta_widget.ts @@ -11,14 +11,15 @@ import { ISpectaLayoutRegistry } from './token'; import { - computeHash, emitResizeEvent, hideAppLoadingIndicator, - isSpectaApp + isSpectaApp, + nextFrame } from './tool'; import { ISpectaSnapshotData, IWidgetManagerLike, + snapshotHash, SPECTA_SNAPSHOT_KEY, WIDGET_VIEW_MIMETYPE } from './snapshot/tools'; @@ -116,14 +117,19 @@ export class AppWidget extends Panel { if (!cellList) { return outs; } + let index = 0; for (const cell of cellList) { const src = cell.sharedModel.source; if (src.length === 0) { + index++; continue; } const el = this._model.createCell(cell); - this._model.executeCell(cell, el); - + el.info.cellIndex = index++; + el.executionDone = this._model + .executeCell(cell, el) + .then(() => undefined) + .catch(() => undefined); outs.push(el); } return outs; @@ -195,36 +201,42 @@ export class AppWidget extends Panel { return; } - await Promise.all( - this._outputs.map(it => { - if ((it.cellOutput as any)?.future?.done) { - return (it.cellOutput as SimplifiedOutputArea).future.done; - } - return Promise.resolve(); - }) - ); + const outputs = this._outputs; + await Promise.all(outputs.map(el => el.executionDone)); + if (outputs !== this._outputs) { + // A rerender landed while we were waiting; these widgets are disposed. + return; + } + await nextFrame(); const notebook = this._model.context?.model.toJSON() as INotebookContent; if (notebook.metadata?.[SPECTA_SNAPSHOT_KEY]) { delete notebook['metadata'][SPECTA_SNAPSHOT_KEY]; } - const allCodeSources = notebook.cells.map(it => it.source).join('\n'); const timestamp = Date.now(); const snapshot: ISpectaSnapshotData = { - hash: computeHash(allCodeSources), + hash: snapshotHash(notebook.cells.map(it => it.source)), timestamp, notebook, widgetStates: null }; - for (const [idx, el] of this._outputs.entries()) { - if (el.info.hidden || el.info.cellModel?.cell_type !== 'code') { + for (const el of outputs) { + if ( + el.info.hidden || + el.info.cellModel?.cell_type !== 'code' || + el.info.cellIndex === undefined + ) { continue; } const output = el.cellOutput as SimplifiedOutputArea; const outputModels = output.model.toJSON(); - notebook.cells[idx].outputs = outputModels; + const target = notebook.cells[el.info.cellIndex]; + if (!target) { + continue; + } + target.outputs = outputModels; if (!snapshot.widgetStates) { for (let index = 0; index < outputModels.length; index++) { const data = diff --git a/src/tool.ts b/src/tool.ts index 23b2de4..9f88f56 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -456,23 +456,11 @@ export function openDocument( } } -export function computeHash(str: string): string { - const s = str.replace(/\s+/g, ' ').trim(); - let h1 = 0xdeadbeef, - h2 = 0x41c6ce57; - for (let i = 0; i < s.length; i++) { - const c = s.charCodeAt(i); - h1 = Math.imul(h1 ^ c, 2654435761); - h2 = Math.imul(h2 ^ c, 1597334677); - } - h1 = - Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ - Math.imul(h2 ^ (h2 >>> 13), 3266489909); - h2 = - Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ - Math.imul(h1 ^ (h1 >>> 13), 3266489909); - return ( - (h2 >>> 0).toString(16).padStart(8, '0') + - (h1 >>> 0).toString(16).padStart(8, '0') - ); +export function nextFrame(timeoutMs = 50): Promise { + return new Promise(resolve => { + if (typeof requestAnimationFrame !== 'undefined') { + requestAnimationFrame(() => resolve()); + } + setTimeout(resolve, timeoutMs); + }); } From 51896976afe089527d136c88f916ff1f33279dc7 Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Mon, 17 Aug 2026 20:37:26 +0200 Subject: [PATCH 07/13] refactor setting dialog --- src/specta_model.ts | 13 ++++- src/topbar/settingDialog.tsx | 102 +++++++++++++++++++++-------------- 2 files changed, 74 insertions(+), 41 deletions(-) diff --git a/src/specta_model.ts b/src/specta_model.ts index d5f2c4e..97b683b 100644 --- a/src/specta_model.ts +++ b/src/specta_model.ts @@ -52,6 +52,7 @@ export class AppModel { } this._context.model.fromJSON(this._documentJson()); this._fileChanged.emit(this._context.model.cells); + this._snapshotChanged.emit(); }); this._kernelSpecManager = options.kernelSpecManager; const specs = this._kernelSpecManager.specs; @@ -76,6 +77,9 @@ export class AppModel { get staticRender() { return this._staticRender; } + get snapshotChanged(): ISignal { + return this._snapshotChanged; + } dispose(): void { if (this.isDisposed) { @@ -84,6 +88,7 @@ export class AppModel { this._isDisposed = true; this._context?.dispose(); this._notebookPanel?.dispose(); + Signal.clearData(this); } get rendermime(): IRenderMimeRegistry { @@ -109,7 +114,7 @@ export class AppModel { title: 'Snapshot out of sync', buttons: [ Dialog.cancelButton({ label: 'Continue' }), - Dialog.okButton({ label: 'Activate kernel' }) + Dialog.okButton({ label: 'Render with kernel' }) ] }); if (response.button.accept) { @@ -171,6 +176,7 @@ export class AppModel { (this.options.tracker as any).add(this._notebookPanel); await this._context.sessionContext.initialize(); } + this._snapshotChanged.emit(); } createCell(cellModel: ICellModel): SpectaCellOutput { @@ -273,6 +279,7 @@ export class AppModel { this._notebookPanel?.dispose(); await this.initialize(); + this._snapshotChanged.emit(); } async executeCell( @@ -329,12 +336,14 @@ export class AppModel { this.options.context.model.setMetadata(SPECTA_SNAPSHOT_KEY, snapshot); await this.options.context.save(); this.options.context.model.dirty = false; + this._snapshotChanged.emit(); } async deleteSnapshot(): Promise { this.options.context.model.deleteMetadata(SPECTA_SNAPSHOT_KEY); await this.options.context.save(); this.options.context.model.dirty = false; + this._snapshotChanged.emit(); } private _documentJson(): INotebookContentWithSnapshot { @@ -355,6 +364,8 @@ export class AppModel { private _filePath: string; private _kernelSpecManager: KernelSpec.IManager; private _staticRender = false; + + private _snapshotChanged = new Signal(this); } export namespace AppModel { diff --git a/src/topbar/settingDialog.tsx b/src/topbar/settingDialog.tsx index ca7e8df..68aa788 100644 --- a/src/topbar/settingDialog.tsx +++ b/src/topbar/settingDialog.tsx @@ -1,11 +1,5 @@ import { IThemeManager } from '@jupyterlab/apputils'; -import React, { - useState, - useEffect, - useCallback, - useRef, - useMemo -} from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Divider } from '../components/divider'; import { ISpectaLayoutRegistry, @@ -14,8 +8,22 @@ import { ISpectaWidget } from '../token'; import { Widget } from '@lumino/widgets'; -import { AppWidget } from '../specta_widget'; +import type { AppWidget } from '../specta_widget'; +import type { AppModel } from '../specta_model'; +type ISnapshotState = { + status: 'out-of-sync' | 'in-sync' | 'not-exist'; + timestamp?: number; + staticRender: boolean; +}; + +function readSnapshotState(model?: AppModel): ISnapshotState { + return { + status: model?.snapshotStatus() ?? 'not-exist', + timestamp: model?.getSnapshot()?.timestamp, + staticRender: Boolean(model?.staticRender) + }; +} export const SettingContent = (props: { config?: ITopbarConfig; themeManager?: IThemeManager; @@ -132,9 +140,6 @@ export const SettingContent = (props: { [settingsWidgets] ); - const [isStaticRendering, setIsStaticRendering] = useState( - Boolean(props.spectaWidget?.model?.staticRender) - ); const { uiSwitcher, currentPath } = props; const onUiChange = useCallback( (e: React.ChangeEvent) => { @@ -146,46 +151,59 @@ export const SettingContent = (props: { [uiSwitcher, currentPath] ); - const [snapshotStatus, setSnapshotStatus] = useState< - 'out-of-sync' | 'in-sync' | 'not-exist' - >(props.spectaWidget?.model.snapshotStatus() ?? 'not-exist'); + const model = props.spectaWidget?.model; + const [snapshotState, setSnapshotState] = useState(() => + readSnapshotState(model) + ); + const [creatingSnapshot, setCreatingSnapshot] = useState(false); + + // Names kept so the JSX below is untouched. + const { + status: snapshotStatus, + timestamp: currentTimestamp, + staticRender: isStaticRendering + } = snapshotState; + + useEffect(() => { + if (!model) { + return; + } + const handler = () => setSnapshotState(readSnapshotState(model)); + model.snapshotChanged.connect(handler); + return () => { + model.snapshotChanged.disconnect(handler); + }; + }, [model]); + + const creatingRef = useRef(false); const deleteSnapshot = useCallback(async () => { if (snapshotStatus === 'not-exist') { return; } - await props.spectaWidget?.model?.deleteSnapshot(); - setCurrentTimestamp(undefined); - setSnapshotStatus('not-exist'); - }, [props.spectaWidget, snapshotStatus]); + await model?.deleteSnapshot(); + }, [model, snapshotStatus]); const createSnapshot = useCallback(async () => { - if (isStaticRendering) { + if (isStaticRendering || creatingRef.current) { return; } + creatingRef.current = true; setCreatingSnapshot(true); - const timestamp = await props.spectaWidget?.saveSnapshot(); - if (timestamp) { - setSnapshotStatus('in-sync'); + try { + await props.spectaWidget?.saveSnapshot(); + } finally { + creatingRef.current = false; + setCreatingSnapshot(false); } - setCurrentTimestamp(timestamp); - setCreatingSnapshot(false); }, [props.spectaWidget, isStaticRendering]); - const snapshot = useMemo( - () => props.spectaWidget?.model?.getSnapshot(), - [props.spectaWidget] - ); - const [currentTimestamp, setCurrentTimestamp] = useState( - snapshot?.timestamp - ); - const activateKernel = useCallback(async () => { + if (!isStaticRendering) { + return; + } await props.spectaWidget?.turnOffStaticRender(); - setIsStaticRendering(false); - }, [props.spectaWidget]); - - const [creatingSnapshot, setCreatingSnapshot] = useState(false); + }, [props.spectaWidget, isStaticRendering]); return (
@@ -335,11 +353,14 @@ export const SettingContent = (props: { className="jp-mod-styled jp-mod-accept" onClick={createSnapshot} style={{ - cursor: isStaticRendering ? 'not-allowed' : 'pointer', + cursor: + isStaticRendering || creatingSnapshot + ? 'not-allowed' + : 'pointer', flexGrow: 1, - opacity: isStaticRendering ? 0.5 : 1 + opacity: isStaticRendering || creatingSnapshot ? 0.5 : 1 }} - disabled={isStaticRendering} + disabled={isStaticRendering || creatingSnapshot} title={ isStaticRendering ? 'Cannot create snapshot in static rendering mode' @@ -358,6 +379,7 @@ export const SettingContent = (props: {
From e46ffb5b00fbee2a0993c42bcd8d18267b1ce238 Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Mon, 17 Aug 2026 23:18:16 +0200 Subject: [PATCH 08/13] add snapshot version --- demo/files/blog.ipynb | 1 + src/snapshot/tools.ts | 47 ++++++++++++++- src/specta_model.ts | 134 ++++++++++++++++++++++++++---------------- src/specta_widget.ts | 15 +++-- 4 files changed, 139 insertions(+), 58 deletions(-) diff --git a/demo/files/blog.ipynb b/demo/files/blog.ipynb index 7df24e1..aa94469 100644 --- a/demo/files/blog.ipynb +++ b/demo/files/blog.ipynb @@ -17,6 +17,7 @@ "defaultLayout": "article" }, "spectaSnapshot": { + "version": 1, "hash": "876caa76358b7da8", "timestamp": 1786957478149, "notebook": { diff --git a/src/snapshot/tools.ts b/src/snapshot/tools.ts index 622de26..93124b2 100644 --- a/src/snapshot/tools.ts +++ b/src/snapshot/tools.ts @@ -6,6 +6,16 @@ export const WIDGET_VIEW_MIMETYPE = 'application/vnd.jupyter.widget-view+json'; export const SPECTA_SNAPSHOT_KEY = 'spectaSnapshot'; +/** + * Format version of the snapshot persisted in notebook metadata. + * + * Bump this whenever `ISpectaSnapshotData` changes shape. Snapshots carrying + * any other version are rejected by `parseSnapshot`, so a notebook written by + * a different Specta falls back to rendering with a kernel rather than being + * read as the wrong format. + */ +export const SPECTA_SNAPSHOT_VERSION = 1; + export interface IWidgetManagerState { version_major: number; version_minor: number; @@ -19,12 +29,47 @@ export interface IWidgetManagerLike { } export interface ISpectaSnapshotData { + version: number; timestamp: number; hash: string; - notebook?: INotebookContent; + notebook: INotebookContent; widgetStates: IWidgetManagerState | null; } +/** + * Read a snapshot out of raw notebook metadata. + * + * This is the only place that knows the persisted shape. Anything unusable — + * absent, malformed, unversioned, or written by a newer Specta — yields + * `undefined` so the caller can fall back to rendering with a kernel. + */ +export function parseSnapshot(raw: unknown): ISpectaSnapshotData | undefined { + if (!raw || typeof raw !== 'object') { + return undefined; + } + const data = raw as Partial; + if (data.version === undefined || data.version > SPECTA_SNAPSHOT_VERSION) { + console.warn( + `Specta: ignoring snapshot of version ${String(data.version)}, ` + + `expected ${SPECTA_SNAPSHOT_VERSION}.` + ); + return undefined; + } + if (typeof data.hash !== 'string' || typeof data.timestamp !== 'number') { + return undefined; + } + if (!data.notebook || !Array.isArray(data.notebook.cells)) { + return undefined; + } + return { + version: data.version, + timestamp: data.timestamp, + hash: data.hash, + notebook: data.notebook, + widgetStates: data.widgetStates ?? null + }; +} + export type INotebookContentWithSnapshot = INotebookContent & { metadata: INotebookContent['metadata'] & { [SPECTA_SNAPSHOT_KEY]: ISpectaSnapshotData | undefined; diff --git a/src/specta_model.ts b/src/specta_model.ts index 97b683b..6df34c9 100644 --- a/src/specta_model.ts +++ b/src/specta_model.ts @@ -36,9 +36,11 @@ import { SPECTA_SNAPSHOT_KEY, WIDGET_STATE_MIMETYPE, INotebookContentWithSnapshot, + parseSnapshot, snapshotHash } from './snapshot/tools'; import { ISignal, Signal } from '@lumino/signaling'; +import { INotebookContent } from '@jupyterlab/nbformat'; export class AppModel { constructor(private options: AppModel.IOptions) { @@ -46,12 +48,11 @@ export class AppModel { this._filePath = options.context.localPath; this._manager = options.manager; - options.context.fileChanged.connect(context => { - if (!this._context || this._staticRender) { - return; + options.context.fileChanged.connect(() => { + const cells = this.resyncSandbox(); + if (cells) { + this._fileChanged.emit(cells); } - this._context.model.fromJSON(this._documentJson()); - this._fileChanged.emit(this._context.model.cells); this._snapshotChanged.emit(); }); this._kernelSpecManager = options.kernelSpecManager; @@ -86,7 +87,7 @@ export class AppModel { return; } this._isDisposed = true; - this._context?.dispose(); + this._sandboxContext?.dispose(); this._notebookPanel?.dispose(); Signal.clearData(this); } @@ -96,67 +97,71 @@ export class AppModel { } get cells(): CellList | undefined { - return this._context?.model.cells; + return this._sandboxContext?.model.cells; } - get context(): DocumentRegistry.IContext | undefined { - return this._context; + /** + * The sandbox context the preview renders from — a throwaway clone at a + * random path whose `save()` is a no-op. Not the document context, which + * lives at `options.context` and is what gets written to disk. + */ + get sandboxContext(): DocumentRegistry.IContext | undefined { + return this._sandboxContext; } get panel(): NotebookPanel | undefined { return this._notebookPanel; } async initialize(): Promise { - if (this._staticRender) { - const snapshotStatus = this.snapshotStatus(); - if (snapshotStatus === 'out-of-sync') { - const response = await showDialog({ - body: 'Do you want to use existing snapshot or re-run the notebook using a kernel?', - title: 'Snapshot out of sync', - buttons: [ - Dialog.cancelButton({ label: 'Continue' }), - Dialog.okButton({ label: 'Render with kernel' }) - ] - }); - if (response.button.accept) { - this._staticRender = false; - } else { - this._staticRender = true; - } + let snapshot = this._staticRender ? this.getSnapshot() : undefined; + if (this._staticRender && !snapshot) { + // Unreadable snapshot, wrong version, or malformed. + this._staticRender = false; + } + if (snapshot && this.snapshotStatus() === 'out-of-sync') { + const response = await showDialog({ + body: 'Do you want to use existing snapshot or re-run the notebook using a kernel?', + title: 'Snapshot out of sync', + buttons: [ + Dialog.cancelButton({ label: 'Continue' }), + Dialog.okButton({ label: 'Render with kernel' }) + ] + }); + if (response.button.accept) { + this._staticRender = false; + snapshot = undefined; } } const kernelPreference = { shouldStart: !this._staticRender, canStart: !this._staticRender, - shutdownOnDispose: true, + shutdownOnDispose: !this._staticRender, name: this.options.context.model.defaultKernelName, autoStartDefault: !this._staticRender, language: this.options.context.model.defaultKernelLanguage }; - this._context = await createNotebookContext({ + this._sandboxContext = await createNotebookContext({ manager: this._manager, kernelPreference: kernelPreference, filePath: this._filePath }); - if (this._staticRender) { - const snapshot = this.getSnapshot(); - if (!snapshot?.notebook) { - throw new Error('Snapshot notebook not found'); - } + this._sandboxJson = undefined; + if (this._staticRender && snapshot) { const notebookModel = snapshot.notebook as INotebookContentWithSnapshot; notebookModel.metadata['widgets'] = { [WIDGET_STATE_MIMETYPE]: snapshot.widgetStates } as any; - this._context.model.fromJSON(notebookModel); + this._sandboxContext.model.fromJSON(notebookModel); this._notebookPanel = createNotebookPanel({ - context: this._context!, + context: this._sandboxContext!, rendermime: this.options.rendermime, editorServices: this.options.editorServices }); - await this._context.sessionContext.initialize(); + await this._sandboxContext.sessionContext.initialize(); const kernelUUID = UUID.uuid4(); - (this._context.sessionContext as any)._session = { + (this._sandboxContext.sessionContext as any)._session = { + dispose: () => {}, kernel: { id: kernelUUID, registerCommTarget: () => {}, @@ -167,14 +172,14 @@ export class AppModel { (this.options.tracker as any).add(this._notebookPanel); } else { - this._context.model.fromJSON(this._documentJson()); + this.resyncSandbox(); this._notebookPanel = createNotebookPanel({ - context: this._context!, + context: this._sandboxContext!, rendermime: this.options.rendermime, editorServices: this.options.editorServices }); (this.options.tracker as any).add(this._notebookPanel); - await this._context.sessionContext.initialize(); + await this._sandboxContext.sessionContext.initialize(); } this._snapshotChanged.emit(); } @@ -277,16 +282,39 @@ export class AppModel { } this._staticRender = false; this._notebookPanel?.dispose(); - + this._notebookPanel = undefined; + this._sandboxContext?.dispose(); + this._sandboxContext = undefined; await this.initialize(); this._snapshotChanged.emit(); } + /** + * Bring the sandbox in line with the document, if it has drifted. + * + * Returns the re-seeded cell list, or `undefined` when the sandbox already + * matches — which is the case for metadata-only writes such as our own + * snapshot save, so those no longer re-execute the notebook. + */ + resyncSandbox(): CellList | undefined { + if (!this._sandboxContext || this._staticRender) { + return undefined; + } + const json = this._documentJson(); + const serialized = JSON.stringify(json); + if (serialized === this._sandboxJson) { + return undefined; + } + this._sandboxJson = serialized; + this._sandboxContext.model.fromJSON(json); + return this._sandboxContext.model.cells; + } + async executeCell( cell: ICellModel, outputWrapper: SpectaCellOutput ): Promise { - if (cell.type !== 'code' || !this._context) { + if (cell.type !== 'code' || !this._sandboxContext) { return; } @@ -306,7 +334,7 @@ export class AppModel { const rep = await SimplifiedOutputArea.execute( source, output, - this._context.sessionContext + this._sandboxContext.sessionContext ); output.future.done.then(() => { emitResizeEvent(); @@ -316,9 +344,9 @@ export class AppModel { } getSnapshot(): ISpectaSnapshotData | undefined { - return this.options.context.model.getMetadata( - SPECTA_SNAPSHOT_KEY - ) as unknown as ISpectaSnapshotData | undefined; + return parseSnapshot( + this.options.context.model.getMetadata(SPECTA_SNAPSHOT_KEY) + ); } snapshotStatus(): 'out-of-sync' | 'in-sync' | 'not-exist' { @@ -327,10 +355,12 @@ export class AppModel { return 'not-exist'; } - const sources = Array.from(this.options.context.model.cells, cell => - cell.sharedModel.getSource() + const documentHash = snapshotHash( + Array.from(this.options.context.model.cells, cell => + cell.sharedModel.getSource() + ) ); - return snapshotHash(sources) === sn.hash ? 'in-sync' : 'out-of-sync'; + return documentHash === sn.hash ? 'in-sync' : 'out-of-sync'; } async saveSnapshotToMetadata(snapshot: ISpectaSnapshotData): Promise { this.options.context.model.setMetadata(SPECTA_SNAPSHOT_KEY, snapshot); @@ -346,9 +376,8 @@ export class AppModel { this._snapshotChanged.emit(); } - private _documentJson(): INotebookContentWithSnapshot { - const json = - this.options.context.model.toJSON() as INotebookContentWithSnapshot; + private _documentJson(): INotebookContent { + const json = this.options.context.model.toJSON() as INotebookContent; delete json.metadata[SPECTA_SNAPSHOT_KEY]; return json; } @@ -356,7 +385,7 @@ export class AppModel { private _kernelReady = new PromiseDelegate(); private _notebookPanel?: NotebookPanel; - private _context?: DocumentRegistry.IContext; + private _sandboxContext?: DocumentRegistry.IContext; private _isDisposed = false; private _manager: ServiceManager.IManager; @@ -364,6 +393,7 @@ export class AppModel { private _filePath: string; private _kernelSpecManager: KernelSpec.IManager; private _staticRender = false; + private _sandboxJson?: string; private _snapshotChanged = new Signal(this); } diff --git a/src/specta_widget.ts b/src/specta_widget.ts index ee56394..db3f2fc 100644 --- a/src/specta_widget.ts +++ b/src/specta_widget.ts @@ -21,6 +21,7 @@ import { IWidgetManagerLike, snapshotHash, SPECTA_SNAPSHOT_KEY, + SPECTA_SNAPSHOT_VERSION, WIDGET_VIEW_MIMETYPE } from './snapshot/tools'; import { SimplifiedOutputArea } from '@jupyterlab/outputarea'; @@ -152,7 +153,7 @@ export class AppWidget extends Panel { await spectaLayout.render({ host: this._host, items: this._outputs, - notebook: this._model.context?.model.toJSON() as any, + notebook: this._model.sandboxContext?.model.toJSON() as any, readyCallback, spectaConfig: this._spectaAppConfig }); @@ -179,7 +180,7 @@ export class AppWidget extends Panel { await spectaLayout.render({ host: this._host, items: this._outputs, - notebook: this._model.context?.model.toJSON() as any, + notebook: this._model.sandboxContext?.model.toJSON() as any, readyCallback: async () => {}, spectaConfig: this._spectaAppConfig }); @@ -200,7 +201,9 @@ export class AppWidget extends Panel { if (this._model.staticRender) { return; } - + if (this._model.resyncSandbox()) { + await this.rerender(); + } const outputs = this._outputs; await Promise.all(outputs.map(el => el.executionDone)); if (outputs !== this._outputs) { @@ -208,7 +211,8 @@ export class AppWidget extends Panel { return; } await nextFrame(); - const notebook = this._model.context?.model.toJSON() as INotebookContent; + const notebook = + this._model.sandboxContext?.model.toJSON() as INotebookContent; if (notebook.metadata?.[SPECTA_SNAPSHOT_KEY]) { delete notebook['metadata'][SPECTA_SNAPSHOT_KEY]; @@ -216,6 +220,7 @@ export class AppWidget extends Panel { const timestamp = Date.now(); const snapshot: ISpectaSnapshotData = { + version: SPECTA_SNAPSHOT_VERSION, hash: snapshotHash(notebook.cells.map(it => it.source)), timestamp, notebook, @@ -303,7 +308,7 @@ export class AppWidget extends Panel { layout.render({ host: this._host, items: this._outputs, - notebook: this._model.context?.model.toJSON() as any, + notebook: this._model.sandboxContext?.model.toJSON() as any, readyCallback: async () => {}, spectaConfig: this._spectaAppConfig }); From c1c57dad7874c3cab16593f8d9eb539def310837 Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Mon, 17 Aug 2026 23:52:14 +0200 Subject: [PATCH 09/13] refactor --- package-lock.json | 2 +- src/document/widget.ts | 9 -- src/snapshot/capture.ts | 109 ++++++++++++++++ src/snapshot/{tools.ts => format.ts} | 44 +------ src/snapshot/hash.ts | 33 +++++ src/specta_model.ts | 14 +-- src/specta_widget.ts | 90 +------------- src/topbar/settingDialog.tsx | 167 +------------------------ src/topbar/staticRenderingSection.tsx | 171 ++++++++++++++++++++++++++ 9 files changed, 337 insertions(+), 302 deletions(-) create mode 100644 src/snapshot/capture.ts rename src/snapshot/{tools.ts => format.ts} (71%) create mode 100644 src/snapshot/hash.ts create mode 100644 src/topbar/staticRenderingSection.tsx diff --git a/package-lock.json b/package-lock.json index ade3a03..8888623 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,7 +92,7 @@ "eslint-plugin-react-hooks": "^5.0.0", "fs-extra": "^11.3.0", "handlebars": "^4.7.8", - "html-webpack-plugin": "^5.6.3", + "html-webpack-plugin": "^5.6.8", "ignore-loader": "^0.1.2", "json-loader": "^0.5.7", "npm-run-all": "^4.1.5", diff --git a/src/document/widget.ts b/src/document/widget.ts index 8dc9535..44a76d6 100644 --- a/src/document/widget.ts +++ b/src/document/widget.ts @@ -8,7 +8,6 @@ import { import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { ServiceManager } from '@jupyterlab/services'; import { Widget } from '@lumino/widgets'; -import { AppWidget } from '../specta_widget'; export interface ISpectaOptions { manager: ServiceManager; @@ -26,14 +25,6 @@ export class NotebookSpectaDocWidget extends DocumentWidget< super(options); } - getSpectaWidget(): AppWidget | undefined { - for (const element of this.content.children()) { - if (element instanceof AppWidget) { - return element; - } - } - } - dispose(): void { this.content.dispose(); super.dispose(); diff --git a/src/snapshot/capture.ts b/src/snapshot/capture.ts new file mode 100644 index 0000000..00cfcd4 --- /dev/null +++ b/src/snapshot/capture.ts @@ -0,0 +1,109 @@ +import { DocumentRegistry } from '@jupyterlab/docregistry'; +import { INotebookContent } from '@jupyterlab/nbformat'; +import * as nbformat from '@jupyterlab/nbformat'; +import { INotebookModel } from '@jupyterlab/notebook'; +import { SimplifiedOutputArea } from '@jupyterlab/outputarea'; +import { Widget } from '@lumino/widgets'; + +import { SpectaCellOutput } from '../specta_cell_output'; +import { + ISpectaSnapshotData, + IWidgetManagerState, + SPECTA_SNAPSHOT_VERSION, + WIDGET_VIEW_MIMETYPE +} from './format'; +import { snapshotHash } from './hash'; + +/** + * The part of the ipywidgets manager we need: its serialized model state. + */ +interface IWidgetManagerLike { + get_state(options?: { + drop_defaults?: boolean; + }): Promise; +} + +/** + * Harvest the ipywidgets model state backing an output area, so widget views + * can be rebuilt later without a kernel. + * One manager serves the whole notebook, so the first one found answers for + * every widget. Returns `null` if the area renders no widget views. + */ +export async function collectWidgetState( + area: SimplifiedOutputArea, + outputs: nbformat.IOutput[] +): Promise { + for (let index = 0; index < outputs.length; index++) { + const data = (outputs[index]?.data as Record) ?? {}; + const viewSpec = data[WIDGET_VIEW_MIMETYPE] as + { model_id?: string } | undefined; + if (!viewSpec?.model_id) { + continue; + } + + const outputWidget = area.widgets[index]; + if (!outputWidget) { + continue; + } + for (const child of outputWidget.children()) { + const renderer = child as Widget & { + mimeType?: string; + _manager?: { promise: Promise }; + }; + if (renderer.mimeType !== WIDGET_VIEW_MIMETYPE || !renderer._manager) { + continue; + } + try { + const manager = await renderer._manager.promise; + return await manager.get_state(); + } catch (e) { + continue; + } + } + } + return null; +} + +/** + * Build a snapshot from the sandbox notebook and the outputs currently on + * screen. + */ +export async function captureSnapshot(options: { + sandbox: DocumentRegistry.IContext; + outputs: SpectaCellOutput[]; +}): Promise { + const { sandbox, outputs } = options; + const notebook = sandbox.model.toJSON() as INotebookContent; + + const snapshot: ISpectaSnapshotData = { + version: SPECTA_SNAPSHOT_VERSION, + hash: snapshotHash(notebook.cells.map(cell => cell.source)), + timestamp: Date.now(), + notebook, + widgetStates: null + }; + + for (const el of outputs) { + if ( + el.info.hidden || + el.info.cellModel?.cell_type !== 'code' || + el.info.cellIndex === undefined + ) { + continue; + } + const target = notebook.cells[el.info.cellIndex]; + if (!target) { + continue; + } + + const area = el.cellOutput as SimplifiedOutputArea; + const outputModels = area.model.toJSON(); + target.outputs = outputModels; + + if (!snapshot.widgetStates) { + snapshot.widgetStates = await collectWidgetState(area, outputModels); + } + } + + return snapshot; +} diff --git a/src/snapshot/tools.ts b/src/snapshot/format.ts similarity index 71% rename from src/snapshot/tools.ts rename to src/snapshot/format.ts index 93124b2..d40c82f 100644 --- a/src/snapshot/tools.ts +++ b/src/snapshot/format.ts @@ -22,12 +22,6 @@ export interface IWidgetManagerState { state: { [modelId: string]: unknown }; } -export interface IWidgetManagerLike { - get_state(options?: { - drop_defaults?: boolean; - }): Promise; -} - export interface ISpectaSnapshotData { version: number; timestamp: number; @@ -36,6 +30,12 @@ export interface ISpectaSnapshotData { widgetStates: IWidgetManagerState | null; } +export type INotebookContentWithSnapshot = INotebookContent & { + metadata: INotebookContent['metadata'] & { + [SPECTA_SNAPSHOT_KEY]: ISpectaSnapshotData | undefined; + }; +}; + /** * Read a snapshot out of raw notebook metadata. * @@ -69,35 +69,3 @@ export function parseSnapshot(raw: unknown): ISpectaSnapshotData | undefined { widgetStates: data.widgetStates ?? null }; } - -export type INotebookContentWithSnapshot = INotebookContent & { - metadata: INotebookContent['metadata'] & { - [SPECTA_SNAPSHOT_KEY]: ISpectaSnapshotData | undefined; - }; -}; -export function computeHash(str: string): string { - const s = str.replace(/\s+/g, ' ').trim(); - let h1 = 0xdeadbeef, - h2 = 0x41c6ce57; - for (let i = 0; i < s.length; i++) { - const c = s.charCodeAt(i); - h1 = Math.imul(h1 ^ c, 2654435761); - h2 = Math.imul(h2 ^ c, 1597334677); - } - h1 = - Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ - Math.imul(h2 ^ (h2 >>> 13), 3266489909); - h2 = - Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ - Math.imul(h1 ^ (h1 >>> 13), 3266489909); - return ( - (h2 >>> 0).toString(16).padStart(8, '0') + - (h1 >>> 0).toString(16).padStart(8, '0') - ); -} - -export function snapshotHash(sources: Array): string { - return computeHash( - sources.map(s => (Array.isArray(s) ? s.join('') : s)).join('\n') - ); -} diff --git a/src/snapshot/hash.ts b/src/snapshot/hash.ts new file mode 100644 index 0000000..eace7dc --- /dev/null +++ b/src/snapshot/hash.ts @@ -0,0 +1,33 @@ +export function computeHash(str: string): string { + const s = str.replace(/\s+/g, ' ').trim(); + let h1 = 0xdeadbeef, + h2 = 0x41c6ce57; + for (let i = 0; i < s.length; i++) { + const c = s.charCodeAt(i); + h1 = Math.imul(h1 ^ c, 2654435761); + h2 = Math.imul(h2 ^ c, 1597334677); + } + h1 = + Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ + Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = + Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ + Math.imul(h1 ^ (h1 >>> 13), 3266489909); + return ( + (h2 >>> 0).toString(16).padStart(8, '0') + + (h1 >>> 0).toString(16).padStart(8, '0') + ); +} + +/** + * Identity of a notebook's code, used to tell whether a snapshot still matches + * the notebook it was taken from. + * + * Accepts either shape nbformat allows for `source`. Note that `computeHash` + * collapses runs of whitespace, so the result is insensitive to reindentation. + */ +export function snapshotHash(sources: Array): string { + return computeHash( + sources.map(s => (Array.isArray(s) ? s.join('') : s)).join('\n') + ); +} diff --git a/src/specta_model.ts b/src/specta_model.ts index 6df34c9..8011b7a 100644 --- a/src/specta_model.ts +++ b/src/specta_model.ts @@ -35,10 +35,9 @@ import { ISpectaSnapshotData, SPECTA_SNAPSHOT_KEY, WIDGET_STATE_MIMETYPE, - INotebookContentWithSnapshot, - parseSnapshot, - snapshotHash -} from './snapshot/tools'; + parseSnapshot +} from './snapshot/format'; +import { snapshotHash } from './snapshot/hash'; import { ISignal, Signal } from '@lumino/signaling'; import { INotebookContent } from '@jupyterlab/nbformat'; @@ -146,8 +145,7 @@ export class AppModel { }); this._sandboxJson = undefined; if (this._staticRender && snapshot) { - const notebookModel = snapshot.notebook as INotebookContentWithSnapshot; - + const notebookModel = snapshot.notebook; notebookModel.metadata['widgets'] = { [WIDGET_STATE_MIMETYPE]: snapshot.widgetStates } as any; @@ -159,11 +157,11 @@ export class AppModel { editorServices: this.options.editorServices }); await this._sandboxContext.sessionContext.initialize(); - const kernelUUID = UUID.uuid4(); + (this._sandboxContext.sessionContext as any)._session = { dispose: () => {}, kernel: { - id: kernelUUID, + id: UUID.uuid4(), registerCommTarget: () => {}, handleComms: false, requestCommInfo: async () => ({ content: { status: undefined } }) diff --git a/src/specta_widget.ts b/src/specta_widget.ts index db3f2fc..ee166aa 100644 --- a/src/specta_widget.ts +++ b/src/specta_widget.ts @@ -16,16 +16,7 @@ import { isSpectaApp, nextFrame } from './tool'; -import { - ISpectaSnapshotData, - IWidgetManagerLike, - snapshotHash, - SPECTA_SNAPSHOT_KEY, - SPECTA_SNAPSHOT_VERSION, - WIDGET_VIEW_MIMETYPE -} from './snapshot/tools'; -import { SimplifiedOutputArea } from '@jupyterlab/outputarea'; -import { INotebookContent } from '@jupyterlab/nbformat'; +import { captureSnapshot } from './snapshot/capture'; export class AppWidget extends Panel { constructor(options: AppWidget.IOptions) { @@ -211,82 +202,13 @@ export class AppWidget extends Panel { return; } await nextFrame(); - const notebook = - this._model.sandboxContext?.model.toJSON() as INotebookContent; - - if (notebook.metadata?.[SPECTA_SNAPSHOT_KEY]) { - delete notebook['metadata'][SPECTA_SNAPSHOT_KEY]; - } - - const timestamp = Date.now(); - const snapshot: ISpectaSnapshotData = { - version: SPECTA_SNAPSHOT_VERSION, - hash: snapshotHash(notebook.cells.map(it => it.source)), - timestamp, - notebook, - widgetStates: null - }; - for (const el of outputs) { - if ( - el.info.hidden || - el.info.cellModel?.cell_type !== 'code' || - el.info.cellIndex === undefined - ) { - continue; - } - const output = el.cellOutput as SimplifiedOutputArea; - - const outputModels = output.model.toJSON(); - const target = notebook.cells[el.info.cellIndex]; - if (!target) { - continue; - } - target.outputs = outputModels; - if (!snapshot.widgetStates) { - for (let index = 0; index < outputModels.length; index++) { - const data = - (outputModels[index]?.data as Record) ?? {}; - const viewSpec = data[WIDGET_VIEW_MIMETYPE] as - | { - model_id?: string; - version_major?: number; - version_minor?: number; - } - | undefined; - if (!viewSpec?.model_id) { - continue; - } - - const outputWidget = output.widgets[index]; - if (!outputWidget) { - continue; - } - for (const child of outputWidget.children()) { - const renderer = child as Widget & { - mimeType?: string; - _manager?: { promise: Promise }; - }; - if (renderer.mimeType !== WIDGET_VIEW_MIMETYPE) { - continue; - } - if (renderer._manager) { - try { - const wm = await renderer._manager.promise; - snapshot.widgetStates = await wm.get_state(); - break; - } catch (e) { - continue; - } - } - } - if (snapshot.widgetStates) { - break; - } - } - } + const sandbox = this._model.sandboxContext; + if (!sandbox) { + return; } + const snapshot = await captureSnapshot({ sandbox, outputs }); await this._model.saveSnapshotToMetadata(snapshot); - return timestamp; + return snapshot.timestamp; } protected onCloseRequest(msg: Message): void { diff --git a/src/topbar/settingDialog.tsx b/src/topbar/settingDialog.tsx index 68aa788..17f8bfb 100644 --- a/src/topbar/settingDialog.tsx +++ b/src/topbar/settingDialog.tsx @@ -9,21 +9,8 @@ import { } from '../token'; import { Widget } from '@lumino/widgets'; import type { AppWidget } from '../specta_widget'; -import type { AppModel } from '../specta_model'; +import { StaticRenderingSection } from './staticRenderingSection'; -type ISnapshotState = { - status: 'out-of-sync' | 'in-sync' | 'not-exist'; - timestamp?: number; - staticRender: boolean; -}; - -function readSnapshotState(model?: AppModel): ISnapshotState { - return { - status: model?.snapshotStatus() ?? 'not-exist', - timestamp: model?.getSnapshot()?.timestamp, - staticRender: Boolean(model?.staticRender) - }; -} export const SettingContent = (props: { config?: ITopbarConfig; themeManager?: IThemeManager; @@ -151,60 +138,6 @@ export const SettingContent = (props: { [uiSwitcher, currentPath] ); - const model = props.spectaWidget?.model; - const [snapshotState, setSnapshotState] = useState(() => - readSnapshotState(model) - ); - const [creatingSnapshot, setCreatingSnapshot] = useState(false); - - // Names kept so the JSX below is untouched. - const { - status: snapshotStatus, - timestamp: currentTimestamp, - staticRender: isStaticRendering - } = snapshotState; - - useEffect(() => { - if (!model) { - return; - } - const handler = () => setSnapshotState(readSnapshotState(model)); - model.snapshotChanged.connect(handler); - return () => { - model.snapshotChanged.disconnect(handler); - }; - }, [model]); - - const creatingRef = useRef(false); - - const deleteSnapshot = useCallback(async () => { - if (snapshotStatus === 'not-exist') { - return; - } - await model?.deleteSnapshot(); - }, [model, snapshotStatus]); - - const createSnapshot = useCallback(async () => { - if (isStaticRendering || creatingRef.current) { - return; - } - creatingRef.current = true; - setCreatingSnapshot(true); - try { - await props.spectaWidget?.saveSnapshot(); - } finally { - creatingRef.current = false; - setCreatingSnapshot(false); - } - }, [props.spectaWidget, isStaticRendering]); - - const activateKernel = useCallback(async () => { - if (!isStaticRendering) { - return; - } - await props.spectaWidget?.turnOffStaticRender(); - }, [props.spectaWidget, isStaticRendering]); - return (

@@ -298,100 +231,10 @@ export const SettingContent = (props: {

)} {props.enableStaticRenderingConfig && ( -
- -
- {snapshotStatus !== 'not-exist' && ( -
- Last snapshot:{' '} - {currentTimestamp - ? new Date(currentTimestamp).toLocaleString() - : 'Unavailable'} -
- )} -
- {snapshotStatus === 'not-exist' - ? 'No snapshot found' - : snapshotStatus === 'out-of-sync' - ? 'Snapshot is out of sync with the notebook' - : ''} -
- -
- - -
-
- -
-
-
+ )} {settingsWidgets && settingsWidgets.length > 0 && (
diff --git a/src/topbar/staticRenderingSection.tsx b/src/topbar/staticRenderingSection.tsx new file mode 100644 index 0000000..248cc8a --- /dev/null +++ b/src/topbar/staticRenderingSection.tsx @@ -0,0 +1,171 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; + +import type { AppModel } from '../specta_model'; +import type { AppWidget } from '../specta_widget'; + +type ISnapshotState = { + status: 'out-of-sync' | 'in-sync' | 'not-exist'; + timestamp?: number; + staticRender: boolean; +}; + +function readSnapshotState(model?: AppModel): ISnapshotState { + return { + status: model?.snapshotStatus() ?? 'not-exist', + timestamp: model?.getSnapshot()?.timestamp, + staticRender: Boolean(model?.staticRender) + }; +} + +export const StaticRenderingSection = (props: { + spectaWidget?: AppWidget; + isSpectaApp?: boolean; +}) => { + const model = props.spectaWidget?.model; + const [snapshotState, setSnapshotState] = useState(() => + readSnapshotState(model) + ); + const [creatingSnapshot, setCreatingSnapshot] = useState(false); + + const { + status: snapshotStatus, + timestamp: currentTimestamp, + staticRender: isStaticRendering + } = snapshotState; + + useEffect(() => { + if (!model) { + return; + } + const handler = () => setSnapshotState(readSnapshotState(model)); + model.snapshotChanged.connect(handler); + return () => { + model.snapshotChanged.disconnect(handler); + }; + }, [model]); + + const creatingRef = useRef(false); + + const deleteSnapshot = useCallback(async () => { + if (snapshotStatus === 'not-exist') { + return; + } + await model?.deleteSnapshot(); + }, [model, snapshotStatus]); + + const createSnapshot = useCallback(async () => { + if (isStaticRendering || creatingRef.current) { + return; + } + creatingRef.current = true; + setCreatingSnapshot(true); + try { + await props.spectaWidget?.saveSnapshot(); + } finally { + creatingRef.current = false; + setCreatingSnapshot(false); + } + }, [props.spectaWidget, isStaticRendering]); + + const activateKernel = useCallback(async () => { + if (!isStaticRendering) { + return; + } + await props.spectaWidget?.turnOffStaticRender(); + }, [props.spectaWidget, isStaticRendering]); + + return ( +
+ +
+ {snapshotStatus !== 'not-exist' && ( +
+ Last snapshot:{' '} + {currentTimestamp + ? new Date(currentTimestamp).toLocaleString() + : 'Unavailable'} +
+ )} +
+ {snapshotStatus === 'not-exist' + ? 'No snapshot found' + : snapshotStatus === 'out-of-sync' + ? 'Snapshot is out of sync with the notebook' + : ''} +
+ +
+ + +
+
+ +
+
+
+ ); +}; From de5b04a5b7aa118745cbc48bc0a4264dc34b7b32 Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Mon, 17 Aug 2026 23:57:46 +0200 Subject: [PATCH 10/13] lint --- src/layout/article.ts | 6 +++--- src/layout/default.ts | 6 +++--- src/layout/slides.ts | 6 +++--- src/snapshot/capture.ts | 6 +++--- src/specta_widget.ts | 2 +- src/topbar/menuComponent.tsx | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/layout/article.ts b/src/layout/article.ts index 322d158..4bc8332 100644 --- a/src/layout/article.ts +++ b/src/layout/article.ts @@ -1,6 +1,6 @@ import { Panel, Widget } from '@lumino/widgets'; -import { SpectaCellOutput } from '../specta_cell_output'; -import * as nbformat from '@jupyterlab/nbformat'; +import type { SpectaCellOutput } from '../specta_cell_output'; +import { INotebookContent } from '@jupyterlab/nbformat'; import { ISpectaLayout } from '../token'; class HostPanel extends Panel { @@ -21,7 +21,7 @@ export class ArticleLayout implements ISpectaLayout { async render(options: { host: Panel; items: SpectaCellOutput[]; - notebook: nbformat.INotebookContent; + notebook: INotebookContent; readyCallback: () => Promise; }): Promise { const { host, items, readyCallback } = options; diff --git a/src/layout/default.ts b/src/layout/default.ts index 16f7a9a..5215012 100644 --- a/src/layout/default.ts +++ b/src/layout/default.ts @@ -1,13 +1,13 @@ import { Panel } from '@lumino/widgets'; -import { SpectaCellOutput } from '../specta_cell_output'; -import * as nbformat from '@jupyterlab/nbformat'; +import type { SpectaCellOutput } from '../specta_cell_output'; +import { INotebookContent } from '@jupyterlab/nbformat'; import { ISpectaLayout } from '../token'; export class DashboardLayout implements ISpectaLayout { async render(options: { host: Panel; items: SpectaCellOutput[]; - notebook: nbformat.INotebookContent; + notebook: INotebookContent; readyCallback: () => Promise; }): Promise { const { host, items, readyCallback } = options; diff --git a/src/layout/slides.ts b/src/layout/slides.ts index f05352c..4e83226 100644 --- a/src/layout/slides.ts +++ b/src/layout/slides.ts @@ -1,6 +1,6 @@ import { Panel, Widget } from '@lumino/widgets'; -import { SpectaCellOutput } from '../specta_cell_output'; -import * as nbformat from '@jupyterlab/nbformat'; +import type { SpectaCellOutput } from '../specta_cell_output'; +import { INotebookContent } from '@jupyterlab/nbformat'; import { ISpectaAppConfig, ISpectaLayout } from '../token'; import Reveal from 'reveal.js'; import { emitResizeEvent, setRevealTheme } from '../tool'; @@ -72,7 +72,7 @@ export class SlidesLayout implements ISpectaLayout { async render(options: { host: Panel; items: SpectaCellOutput[]; - notebook: nbformat.INotebookContent; + notebook: INotebookContent; readyCallback: () => Promise; spectaConfig: ISpectaAppConfig; }): Promise { diff --git a/src/snapshot/capture.ts b/src/snapshot/capture.ts index 00cfcd4..3c08cfd 100644 --- a/src/snapshot/capture.ts +++ b/src/snapshot/capture.ts @@ -1,11 +1,11 @@ import { DocumentRegistry } from '@jupyterlab/docregistry'; import { INotebookContent } from '@jupyterlab/nbformat'; -import * as nbformat from '@jupyterlab/nbformat'; +import { IOutput } from '@jupyterlab/nbformat'; import { INotebookModel } from '@jupyterlab/notebook'; import { SimplifiedOutputArea } from '@jupyterlab/outputarea'; import { Widget } from '@lumino/widgets'; -import { SpectaCellOutput } from '../specta_cell_output'; +import type { SpectaCellOutput } from '../specta_cell_output'; import { ISpectaSnapshotData, IWidgetManagerState, @@ -31,7 +31,7 @@ interface IWidgetManagerLike { */ export async function collectWidgetState( area: SimplifiedOutputArea, - outputs: nbformat.IOutput[] + outputs: IOutput[] ): Promise { for (let index = 0; index < outputs.length; index++) { const data = (outputs[index]?.data as Record) ?? {}; diff --git a/src/specta_widget.ts b/src/specta_widget.ts index ee166aa..ddb7ffd 100644 --- a/src/specta_widget.ts +++ b/src/specta_widget.ts @@ -4,7 +4,7 @@ import { Message } from '@lumino/messaging'; import { Panel, Widget } from '@lumino/widgets'; import { SpectaCellOutput } from './specta_cell_output'; -import { AppModel } from './specta_model'; +import type { AppModel } from './specta_model'; import { ISpectaAppConfig, ISpectaLayout, diff --git a/src/topbar/menuComponent.tsx b/src/topbar/menuComponent.tsx index 7c6b917..c03a938 100644 --- a/src/topbar/menuComponent.tsx +++ b/src/topbar/menuComponent.tsx @@ -6,7 +6,7 @@ import { GearIcon } from '../components/icon/gear'; import { IconButton } from '../components/iconButton'; import { SettingContent } from './settingDialog'; import { ISpectaUiSwitcher, ITopbarConfig, ISpectaWidget } from '../token'; -import { AppWidget } from '../specta_widget'; +import type { AppWidget } from '../specta_widget'; interface IProps { config?: ITopbarConfig; From 96b0475317891a2f2d648c734c88d179cf9252ba Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Tue, 18 Aug 2026 00:10:08 +0200 Subject: [PATCH 11/13] wording --- src/components/icon/export.tsx | 18 ------------------ src/snapshot/index.ts | 3 +++ src/specta_model.ts | 13 +++++++------ src/specta_widget.ts | 2 +- src/topbar/staticRenderingSection.tsx | 12 ++++++------ 5 files changed, 17 insertions(+), 31 deletions(-) delete mode 100644 src/components/icon/export.tsx create mode 100644 src/snapshot/index.ts diff --git a/src/components/icon/export.tsx b/src/components/icon/export.tsx deleted file mode 100644 index 8657fc4..0000000 --- a/src/components/icon/export.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; - -export const ExportIcon = ({ ...props }: React.SVGProps) => ( - - - -); diff --git a/src/snapshot/index.ts b/src/snapshot/index.ts new file mode 100644 index 0000000..0866f02 --- /dev/null +++ b/src/snapshot/index.ts @@ -0,0 +1,3 @@ +export * from './format'; +export * from './hash'; +export * from './capture'; diff --git a/src/specta_model.ts b/src/specta_model.ts index 8011b7a..9cfe79e 100644 --- a/src/specta_model.ts +++ b/src/specta_model.ts @@ -35,9 +35,10 @@ import { ISpectaSnapshotData, SPECTA_SNAPSHOT_KEY, WIDGET_STATE_MIMETYPE, - parseSnapshot -} from './snapshot/format'; -import { snapshotHash } from './snapshot/hash'; + parseSnapshot, + snapshotHash +} from './snapshot'; + import { ISignal, Signal } from '@lumino/signaling'; import { INotebookContent } from '@jupyterlab/nbformat'; @@ -118,10 +119,10 @@ export class AppModel { } if (snapshot && this.snapshotStatus() === 'out-of-sync') { const response = await showDialog({ - body: 'Do you want to use existing snapshot or re-run the notebook using a kernel?', - title: 'Snapshot out of sync', + body: 'Do you want to use existing cache or re-run the notebook using a kernel?', + title: 'Render cache out of sync', buttons: [ - Dialog.cancelButton({ label: 'Continue' }), + Dialog.cancelButton({ label: 'Use cache' }), Dialog.okButton({ label: 'Render with kernel' }) ] }); diff --git a/src/specta_widget.ts b/src/specta_widget.ts index ddb7ffd..61b709e 100644 --- a/src/specta_widget.ts +++ b/src/specta_widget.ts @@ -16,7 +16,7 @@ import { isSpectaApp, nextFrame } from './tool'; -import { captureSnapshot } from './snapshot/capture'; +import { captureSnapshot } from './snapshot'; export class AppWidget extends Panel { constructor(options: AppWidget.IOptions) { diff --git a/src/topbar/staticRenderingSection.tsx b/src/topbar/staticRenderingSection.tsx index 248cc8a..cae5f83 100644 --- a/src/topbar/staticRenderingSection.tsx +++ b/src/topbar/staticRenderingSection.tsx @@ -89,7 +89,7 @@ export const StaticRenderingSection = (props: { > {snapshotStatus !== 'not-exist' && (
- Last snapshot:{' '} + Last cache:{' '} {currentTimestamp ? new Date(currentTimestamp).toLocaleString() : 'Unavailable'} @@ -97,9 +97,9 @@ export const StaticRenderingSection = (props: { )}
{snapshotStatus === 'not-exist' - ? 'No snapshot found' + ? 'No render cache found' : snapshotStatus === 'out-of-sync' - ? 'Snapshot is out of sync with the notebook' + ? 'Render cache is out of sync with the notebook' : ''}
@@ -122,7 +122,7 @@ export const StaticRenderingSection = (props: { opacity: snapshotStatus === 'not-exist' ? 0.5 : 1 }} > - Delete snapshot + Clear cache
Date: Tue, 18 Aug 2026 14:20:04 +0200 Subject: [PATCH 12/13] update readme --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/README.md b/README.md index 68aca5a..1eb17c3 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,10 @@ View any Jupyter-supported file using Specta's clean viewer with all Jupyter UI A `specta` preview can be launched directly from JupyterLab, letting users verify how their documents will look when published. +### Static Rendering (no kernel required) + +Execute a notebook once while authoring and store the result — outputs and `ipywidgets` state — inside the notebook itself, so readers see the rendered document immediately without a kernel ever starting. See [Static rendering](#static-rendering). + ## Try it online! You can try it online by clicking on this badge: @@ -34,6 +38,8 @@ You can try it online by clicking on this badge: ## Installation and Usage +### Installation + You can install `specta` using `pip` or `conda` ```bash @@ -44,6 +50,8 @@ pip install specta conda install -c conda-forge specta ``` +### Building and serving your app + Once installed, you can build your JupyterLite app, a `specta` app will be included automatically in the output directory of `jupyterlite`: ``` @@ -52,8 +60,48 @@ jupyter lite build Then serve the contents of the output directory (by default `./_output`) using any static file server. You can access the `Specta` app at the `/specta/` path. +Every file in your JupyterLite contents is reachable through Specta by appending its path, for example `/specta/index.html?path=blog.ipynb`. Which layout is used, whether the top bar is shown, and the rest of the appearance are controlled by the configuration described in [Specta Configuration](#specta-configuration). + If you want to disable specta loading spinner, you can set the environment variable `SPECTA_NO_LOADING_SCREEN` to `1`before calling jupyterlite build command +### Previewing from JupyterLab + +While authoring, you don't have to rebuild the site to see the result. In JupyterLab, right-click the file in the file browser and choose **Open With ▸ Specta**: the document is rendered in a panel with the same layouts and the same top bar as the deployed app, using a real kernel. This preview is also where you manage the render cache described below. + +### Static rendering + +By default Specta starts a kernel and re-executes the notebook every time a reader opens it. If the interactive features of the notebook are not important, you can save time and bandwidth by using the **static rendering** mode + +**Static rendering** removes the kernel from that path. You execute the notebook once while authoring and save a _render cache_: the outputs, plus the state of any `ipywidgets` in the document, are stored inside the notebook itself. When a reader later opens that notebook, Specta rebuilds the rendered document directly from the cache and never starts a kernel. + +#### Saving a render cache + +The render cache is created from the JupyterLab/JupyterLite preview, not from the deployed app: + +1. Open the notebook in JupyterLab/JupyterLite and launch the Specta preview. +2. Wait for the notebook to finish executing, so the outputs you want to capture are on screen. +3. Open the settings dialog in the top bar and find the **Static rendering** section. +4. Click **Save cache**. + +The cache is written into the notebook's metadata and the file is saved. From then on, opening that notebook in Specta — in the preview or in the built app — renders it statically. Rebuild your site with `jupyter lite build` to publish it. + +Because the cache lives in the notebook file, there is no sidecar file to keep in sync: copying, committing, or downloading the `.ipynb` carries the rendered result with it. + +#### Keeping the cache in sync + +Specta records a hash of the notebook's code when it saves the cache, and compares it every time the document is opened. The **Static rendering** section of the settings dialog shows the current state: + +- **No render cache found** – the notebook has never been cached; it will render with a kernel. +- **Render cache is out of sync with the notebook** – the code has changed since the cache was saved. Specta asks whether to use the existing cache anyway or to re-run the notebook with a kernel. +- Otherwise the cache matches the notebook, along with the time it was last saved. + +Two other actions are available: + +- **Clear cache** (preview only) removes the cache from the notebook, returning it to kernel rendering. +- **Render with kernel** is available in the preview _and_ in the deployed app. It lets a reader looking at a statically rendered document start a kernel on demand, for example to interact with widgets whose behaviour depends on running Python. + +Static rendering applies to notebooks. Other Jupyter-supported files rendered by Specta's clean viewer do not execute code and so have no cache to save. + ## Specta Configuration ### Available layouts From 4909f517a0306d8e907978b543c685f56bf98b8a Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Tue, 18 Aug 2026 22:31:00 +0200 Subject: [PATCH 13/13] add demo --- demo/environment.yml | 1 + demo/files/blog.ipynb | 2035 ++++++----------------------- demo/files/static-rendering.ipynb | 1503 +++++++++++++++++++++ demo/jupyter-lite.json | 7 + 4 files changed, 1904 insertions(+), 1642 deletions(-) create mode 100644 demo/files/static-rendering.ipynb diff --git a/demo/environment.yml b/demo/environment.yml index 47f419a..2547c2f 100644 --- a/demo/environment.yml +++ b/demo/environment.yml @@ -7,3 +7,4 @@ dependencies: - ipywidgets - ipecharts - matplotlib + - numpy diff --git a/demo/files/blog.ipynb b/demo/files/blog.ipynb index aa94469..71f6d7b 100644 --- a/demo/files/blog.ipynb +++ b/demo/files/blog.ipynb @@ -1,5 +1,394 @@ { - "metadata": { + "cells": [ + { + "cell_type": "markdown", + "id": "a3abf27e", + "metadata": {}, + "source": [ + "# Data Visualization in Jupyter Notebooks using Apache Echarts\n" + ] + }, + { + "cell_type": "markdown", + "id": "21b59e76", + "metadata": { + "specta": { + "outputSize":"Full" + } + }, + "source": [ + "![Banner](./top.jpeg \"Banner\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "bf93b954", + "metadata": {}, + "source": [ + "\n", + "\n", + "In the realm of data science and visualization, Jupyter Notebook has emerged as a powerful tool for data analysis and storytelling. Integrating interactive and aesthetically pleasing charts can significantly enhance the presentation of data insights." + ] + }, + { + "cell_type": "markdown", + "id": "a3826498", + "metadata": {}, + "source": [ + "[Apache Echarts](https://echarts.apache.org/en/index.html) is one of the most versatile libraries for creating interactive charts. This blog post explores how to leverage ipecharts, a new Python library that seamlessly integrates Echarts into Jupyter Notebooks, to craft stunning visualizations within your notebooks.\n", + "\n", + "*Disclaimer: I am the author of this library.*" + ] + }, + { + "cell_type": "markdown", + "id": "1a2ffcfd", + "metadata": {}, + "source": [ + "

• • •

" + ] + }, + { + "cell_type": "markdown", + "id": "e2569313", + "metadata": {}, + "source": [ + "## Motivation\n", + "\n", + "`ipecharts` is not the first attempt to make Echarts available on Jupyter Notebooks. pyecharts is a popular open-source library that allows you to create interactive charts in Python and supports both notebooks and standalone Python scripts." + ] + }, + { + "cell_type": "markdown", + "id": "330ee266", + "metadata": {}, + "source": [ + "While pyechartscan create charts in the notebooks, it does not use the Jupyter Widgets system but instead injects HTML code into the notebook to render the charts. This approach makes using pyecharts in other Jupyter applications or interacting with other widgets libraries harder." + ] + }, + { + "cell_type": "markdown", + "id": "6e320817", + "metadata": {}, + "source": [ + "On the other hand, ipecharts adopts the native way of creating interactive charts in Jupyter Notebooks by using Jupyter Widgets. It makes the created charts compatible with a wide range of tools and libraries in the Jupyter ecosystem." + ] + }, + { + "cell_type": "markdown", + "id": "250f141f", + "metadata": {}, + "source": [ + "

• • •

" + ] + }, + { + "cell_type": "markdown", + "id": "fc22d007", + "metadata": {}, + "source": [ + "## Getting started with ipecharts" + ] + }, + { + "cell_type": "markdown", + "id": "b05b1479", + "metadata": {}, + "source": [ + "### Installation\n", + "\n", + "`ipecharts` is available on PyPI and conda-forge:\n", + "\n", + "```bash\n", + "# Installing with pip\n", + "pip install ipecharts\n", + "\n", + "# Installing with conda\n", + "conda install -c conda-forge ipecharts\n", + "```\n", + "\n", + "It requires ipywidgets ≥8.0 and does not work with Jupyter Notebook <7 . More detailed documentation is available at Read the Docs. You can also try it live in this JupyterLite instance." + ] + }, + { + "cell_type": "markdown", + "id": "ce613931", + "metadata": {}, + "source": [ + "### Creating a simple line plot\n", + "\n", + "ipechart is a very slim wrapper outside of the Echarts Javascript library so translating the Javascript version of a chart into ipechartswidget is straightforward. Let’s begin with a basic line example from Echarts official documentation:\n", + "\n", + "```typescript\n", + "// Example from https://echarts.apache.org/examples/en/editor.html?c=line-simple&lang=ts\n", + "import * as echarts from 'echarts';\n", + "\n", + "type EChartsOption = echarts.EChartsOption;\n", + "\n", + "var chartDom = document.getElementById('main')!;\n", + "var myChart = echarts.init(chartDom);\n", + "var option: EChartsOption;\n", + "\n", + "option = {\n", + " xAxis: {\n", + " type: 'category',\n", + " data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']\n", + " },\n", + " yAxis: {\n", + " type: 'value'\n", + " },\n", + " series: [\n", + " {\n", + " data: [150, 230, 224, 218, 135, 147, 260],\n", + " type: 'line'\n", + " }\n", + " ]\n", + "};\n", + "\n", + "option && myChart.setOption(option);\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "7824f42f", + "metadata": {}, + "source": [ + "The entry point of a chart in ipecharts is the EchartWidget class:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3de1a807", + "metadata": { + "specta": { + "showOutput": "No", + "showSource": "Yes" + }, + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + } + }, + "outputs": [], + "source": [ + "from ipecharts import EChartsWidget\n", + "chart = EChartsWidget()" + ] + }, + { + "cell_type": "markdown", + "id": "503e0e22", + "metadata": {}, + "source": [ + "Just as in the Javascript example, we need to set the option of this chart. For all top-level keys of the Echarts option and the entries of series, ipecharts provides Python class counterparts with the same name. Here is the equivalent of the above option object defined with ipecharts classes:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8741975", + "metadata": { + "specta": { + "showOutput": "No", + "showSource": "Yes" + }, + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + } + }, + "outputs": [], + "source": [ + "from ipecharts.option import Option, XAxis, YAxis\n", + "from ipecharts.option.series import Line\n", + "\n", + "xAxis = XAxis(\n", + " type=\"category\",\n", + " data=[\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"],\n", + ")\n", + "yAxis = YAxis(type=\"value\")\n", + "line = Line(data=[150, 230, 224, 218, 135, 147, 260])\n", + "\n", + "option = Option()\n", + "option.xAxis = xAxis\n", + "option.yAxis = yAxis\n", + "option.series = [line]" + ] + }, + { + "cell_type": "markdown", + "id": "c001b376", + "metadata": {}, + "source": [ + "All classes here are based on traitlets so you can initialize the instance by using keyword arguments or by setting the property values. Finally, updating the option value of our chart gives us the same chart as the Javascript \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b2c01c4", + "metadata": { + "specta": { + "showSource": "Yes", + "outputSize":"Big" + }, + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + } + }, + "outputs": [], + "source": [ + "chart.option = option\n", + "chart" + ] + }, + { + "cell_type": "markdown", + "id": "19dea3c1", + "metadata": {}, + "source": [ + "### Adding Interactivity" + ] + }, + { + "cell_type": "markdown", + "id": "1105f857", + "metadata": {}, + "source": [ + "By using traitlets to configure your chart, any change in the option properties will be applied to the chart automatically. We will use the Button widget of ipywidgets to change the line data dynamically.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5fb539d", + "metadata": { + "specta": { + "showSource": "Yes" + }, + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + } + }, + "outputs": [], + "source": [ + "from ipywidgets.widgets import Button\n", + "from numpy.random import randint\n", + "\n", + "def update_line_data(b): \n", + " line.data = randint(0, 300, 7).tolist()\n", + "\n", + "button = Button(description=\"Generate data\")\n", + "button.on_click(update_line_data)\n", + "\n", + "display(button, chart)" + ] + }, + { + "cell_type": "markdown", + "id": "0e491acc", + "metadata": {}, + "source": [ + "In the on_click callback of the button, we update the data property of the line instance, the changed signal is propagated up to the top-level widget and the chart will be updated automatically." + ] + }, + { + "cell_type": "markdown", + "id": "86b1b92d", + "metadata": {}, + "source": [ + "### Creating charts without using traitlets configuration" + ] + }, + { + "cell_type": "markdown", + "id": "2e5acf08", + "metadata": {}, + "source": [ + "In many situations, we simply want to display the data without adding interactivity. For this use case, users can convert any option object used by a Javascript chart to a Python dictionary and pass it to the EchartRawWidget of ipecharts.\n", + "\n", + "Here is the equivalent of the Two Value-Axes in Polar example from the official Echarts documentation using EchartRawWidget:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "933a5a49", + "metadata": { + "tags": [ + "specta:visible" + ], + "vscode": { + "languageId": "plaintext" + } + }, + "outputs": [], + "source": [ + "from ipecharts import EChartsRawWidget\n", + "import math\n", + "\n", + "data = []\n", + "for i in range(101):\n", + " theta = (i / 100) * 360\n", + " r = 5 * (1 + math.sin((theta / 180) * math.pi))\n", + " data.append([r, theta])\n", + "\n", + "option = {\n", + " \"title\": {\"text\": \"Two Value-Axes in Polar\"},\n", + " \"legend\": {\"data\": [\"line\"]},\n", + " \"polar\": {},\n", + " \"tooltip\": {\"trigger\": \"axis\", \"axisPointer\": {\"type\": \"cross\"}},\n", + " \"angleAxis\": {\"type\": \"value\", \"startAngle\": 0},\n", + " \"radiusAxis\": {},\n", + " \"series\": [\n", + " {\"coordinateSystem\": \"polar\", \"name\": \"line\", \"type\": \"line\", \"data\": data}\n", + " ],\n", + "}\n", + "EChartsRawWidget(option=option)" + ] + }, + { + "cell_type": "markdown", + "id": "213d1b6b", + "metadata": {}, + "source": [ + "## What’s next\n", + "\n", + "In this first version, I focused on generating the option configuration class to be able to translate the Javascript charts to the Python ones without too many changes. Echarts has a lot of other customizations in theming, managing maps, or chart animation… These aspects will be addressed in future releases.\n", + "\n", + "You can follow the development of this library on GitHub. Stay tuned and happy charting!" + ] + }, + { + "cell_type": "markdown", + "id": "b7129618", + "metadata": {}, + "source": [ + "## About the author\n", + "\n", + "Duc Trung Le is an open-source developer who works on this project in his free time." + ] + }, + { + "cell_type": "markdown", + "id": "1dd7aa92", + "metadata": {}, + "source": [] + } + ], + "metadata": { "kernelspec": { "name": "xpython", "display_name": "Python 3.13 (XPython)", @@ -15,1646 +404,8 @@ "hideTopbar": "No", "slidesTheme": null, "defaultLayout": "article" - }, - "spectaSnapshot": { - "version": 1, - "hash": "876caa76358b7da8", - "timestamp": 1786957478149, - "notebook": { - "metadata": { - "kernelspec": { - "name": "xpython", - "display_name": "Python 3.13 (XPython)", - "language": "python" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.13.1" - }, - "specta": { - "hideTopbar": "No", - "slidesTheme": null, - "defaultLayout": "article" - } - }, - "nbformat_minor": 5, - "nbformat": 4, - "cells": [ - { - "id": "a3abf27e", - "cell_type": "markdown", - "source": "# Data Visualization in Jupyter Notebooks using Apache Echarts\n", - "metadata": {} - }, - { - "id": "21b59e76", - "cell_type": "markdown", - "source": "![Banner](./top.jpeg \"Banner\")\n", - "metadata": { - "specta": { - "outputSize": "Full" - } - } - }, - { - "id": "bf93b954", - "cell_type": "markdown", - "source": "\n\nIn the realm of data science and visualization, Jupyter Notebook has emerged as a powerful tool for data analysis and storytelling. Integrating interactive and aesthetically pleasing charts can significantly enhance the presentation of data insights.", - "metadata": {} - }, - { - "id": "a3826498", - "cell_type": "markdown", - "source": "[Apache Echarts](https://echarts.apache.org/en/index.html) is one of the most versatile libraries for creating interactive charts. This blog post explores how to leverage ipecharts, a new Python library that seamlessly integrates Echarts into Jupyter Notebooks, to craft stunning visualizations within your notebooks.\n\n*Disclaimer: I am the author of this library.*", - "metadata": {} - }, - { - "id": "1a2ffcfd", - "cell_type": "markdown", - "source": "

• • •

", - "metadata": {} - }, - { - "id": "e2569313", - "cell_type": "markdown", - "source": "## Motivation\n\n`ipecharts` is not the first attempt to make Echarts available on Jupyter Notebooks. pyecharts is a popular open-source library that allows you to create interactive charts in Python and supports both notebooks and standalone Python scripts.", - "metadata": {} - }, - { - "id": "330ee266", - "cell_type": "markdown", - "source": "While pyechartscan create charts in the notebooks, it does not use the Jupyter Widgets system but instead injects HTML code into the notebook to render the charts. This approach makes using pyecharts in other Jupyter applications or interacting with other widgets libraries harder.", - "metadata": {} - }, - { - "id": "6e320817", - "cell_type": "markdown", - "source": "On the other hand, ipecharts adopts the native way of creating interactive charts in Jupyter Notebooks by using Jupyter Widgets. It makes the created charts compatible with a wide range of tools and libraries in the Jupyter ecosystem.", - "metadata": {} - }, - { - "id": "250f141f", - "cell_type": "markdown", - "source": "

• • •

", - "metadata": {} - }, - { - "id": "fc22d007", - "cell_type": "markdown", - "source": "## Getting started with ipecharts", - "metadata": {} - }, - { - "id": "b05b1479", - "cell_type": "markdown", - "source": "### Installation\n\n`ipecharts` is available on PyPI and conda-forge:\n\n```bash\n# Installing with pip\npip install ipecharts\n\n# Installing with conda\nconda install -c conda-forge ipecharts\n```\n\nIt requires ipywidgets ≥8.0 and does not work with Jupyter Notebook <7 . More detailed documentation is available at Read the Docs. You can also try it live in this JupyterLite instance.", - "metadata": {} - }, - { - "id": "ce613931", - "cell_type": "markdown", - "source": "### Creating a simple line plot\n\nipechart is a very slim wrapper outside of the Echarts Javascript library so translating the Javascript version of a chart into ipechartswidget is straightforward. Let’s begin with a basic line example from Echarts official documentation:\n\n```typescript\n// Example from https://echarts.apache.org/examples/en/editor.html?c=line-simple&lang=ts\nimport * as echarts from 'echarts';\n\ntype EChartsOption = echarts.EChartsOption;\n\nvar chartDom = document.getElementById('main')!;\nvar myChart = echarts.init(chartDom);\nvar option: EChartsOption;\n\noption = {\n xAxis: {\n type: 'category',\n data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']\n },\n yAxis: {\n type: 'value'\n },\n series: [\n {\n data: [150, 230, 224, 218, 135, 147, 260],\n type: 'line'\n }\n ]\n};\n\noption && myChart.setOption(option);\n```", - "metadata": {} - }, - { - "id": "7824f42f", - "cell_type": "markdown", - "source": "The entry point of a chart in ipecharts is the EchartWidget class:\n", - "metadata": {} - }, - { - "id": "3de1a807", - "cell_type": "code", - "source": "from ipecharts import EChartsWidget\nchart = EChartsWidget()", - "metadata": { - "specta": { - "showOutput": "No", - "showSource": "Yes" - }, - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - }, - "trusted": true - }, - "outputs": [], - "execution_count": null - }, - { - "id": "503e0e22", - "cell_type": "markdown", - "source": "Just as in the Javascript example, we need to set the option of this chart. For all top-level keys of the Echarts option and the entries of series, ipecharts provides Python class counterparts with the same name. Here is the equivalent of the above option object defined with ipecharts classes:\n", - "metadata": {} - }, - { - "id": "d8741975", - "cell_type": "code", - "source": "from ipecharts.option import Option, XAxis, YAxis\nfrom ipecharts.option.series import Line\n\nxAxis = XAxis(\n type=\"category\",\n data=[\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"],\n)\nyAxis = YAxis(type=\"value\")\nline = Line(data=[150, 230, 224, 218, 135, 147, 260])\n\noption = Option()\noption.xAxis = xAxis\noption.yAxis = yAxis\noption.series = [line]", - "metadata": { - "specta": { - "showOutput": "No", - "showSource": "Yes" - }, - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - }, - "trusted": true - }, - "outputs": [], - "execution_count": null - }, - { - "id": "c001b376", - "cell_type": "markdown", - "source": "All classes here are based on traitlets so you can initialize the instance by using keyword arguments or by setting the property values. Finally, updating the option value of our chart gives us the same chart as the Javascript \n", - "metadata": {} - }, - { - "id": "2b2c01c4", - "cell_type": "code", - "source": "chart.option = option\nchart", - "metadata": { - "specta": { - "showSource": "Yes", - "outputSize": "Big" - }, - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - }, - "trusted": true - }, - "outputs": [ - { - "execution_count": 3, - "output_type": "execute_result", - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0d0ea8bc061e402e9701a0fcb405d171", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": "EChartsWidget(option=Option(angleAxis=None, aria=None, axisPointer=None, brush=None, calendar=None, dataset=No…" - }, - "metadata": {} - } - ], - "execution_count": null - }, - { - "id": "19dea3c1", - "cell_type": "markdown", - "source": "### Adding Interactivity", - "metadata": {} - }, - { - "id": "1105f857", - "cell_type": "markdown", - "source": "By using traitlets to configure your chart, any change in the option properties will be applied to the chart automatically. We will use the Button widget of ipywidgets to change the line data dynamically.\n", - "metadata": {} - }, - { - "id": "a5fb539d", - "cell_type": "code", - "source": "from ipywidgets.widgets import Button\nfrom numpy.random import randint\n\ndef update_line_data(b): \n line.data = randint(0, 300, 7).tolist()\n\nbutton = Button(description=\"Generate data\")\nbutton.on_click(update_line_data)\n\ndisplay(button, chart)", - "metadata": { - "specta": { - "showSource": "Yes" - }, - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - }, - "trusted": true - }, - "outputs": [ - { - "output_type": "display_data", - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9fa96cec05854444baf5920a57a3ab39", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": "Button(description='Generate data', style=ButtonStyle())" - }, - "metadata": {} - }, - { - "output_type": "display_data", - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0d0ea8bc061e402e9701a0fcb405d171", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": "EChartsWidget(option=Option(angleAxis=None, aria=None, axisPointer=None, brush=None, calendar=None, dataset=No…" - }, - "metadata": {} - } - ], - "execution_count": null - }, - { - "id": "0e491acc", - "cell_type": "markdown", - "source": "In the on_click callback of the button, we update the data property of the line instance, the changed signal is propagated up to the top-level widget and the chart will be updated automatically.", - "metadata": {} - }, - { - "id": "86b1b92d", - "cell_type": "markdown", - "source": "### Creating charts without using traitlets configuration", - "metadata": {} - }, - { - "id": "2e5acf08", - "cell_type": "markdown", - "source": "In many situations, we simply want to display the data without adding interactivity. For this use case, users can convert any option object used by a Javascript chart to a Python dictionary and pass it to the EchartRawWidget of ipecharts.\n\nHere is the equivalent of the Two Value-Axes in Polar example from the official Echarts documentation using EchartRawWidget:", - "metadata": {} - }, - { - "id": "933a5a49", - "cell_type": "code", - "source": "from ipecharts import EChartsRawWidget\nimport math\n\ndata = []\nfor i in range(101):\n theta = (i / 100) * 360\n r = 5 * (1 + math.sin((theta / 180) * math.pi))\n data.append([r, theta])\n\noption = {\n \"title\": {\"text\": \"Two Value-Axes in Polar\"},\n \"legend\": {\"data\": [\"line\"]},\n \"polar\": {},\n \"tooltip\": {\"trigger\": \"axis\", \"axisPointer\": {\"type\": \"cross\"}},\n \"angleAxis\": {\"type\": \"value\", \"startAngle\": 0},\n \"radiusAxis\": {},\n \"series\": [\n {\"coordinateSystem\": \"polar\", \"name\": \"line\", \"type\": \"line\", \"data\": data}\n ],\n}\nEChartsRawWidget(option=option)", - "metadata": { - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - }, - "trusted": true - }, - "outputs": [ - { - "execution_count": 5, - "output_type": "execute_result", - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "1330ce9e375e43d295c0bc50a128481c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": "EChartsRawWidget(option={'title': {'text': 'Two Value-Axes in Polar'}, 'legend': {'data': ['line']}, 'polar': …" - }, - "metadata": {} - } - ], - "execution_count": null - }, - { - "id": "213d1b6b", - "cell_type": "markdown", - "source": "## What’s next\n\nIn this first version, I focused on generating the option configuration class to be able to translate the Javascript charts to the Python ones without too many changes. Echarts has a lot of other customizations in theming, managing maps, or chart animation… These aspects will be addressed in future releases.\n\nYou can follow the development of this library on GitHub. Stay tuned and happy charting!", - "metadata": {} - }, - { - "id": "b7129618", - "cell_type": "markdown", - "source": "## About the author\n\nDuc Trung Le is an open-source developer who works on this project in his free time.", - "metadata": {} - }, - { - "id": "1dd7aa92", - "cell_type": "markdown", - "source": "", - "metadata": {} - } - ] - }, - "widgetStates": { - "version_major": 2, - "version_minor": 0, - "state": { - "f688e0a29ef04335b8ff42090fdec2f1": { - "model_name": "LayoutModel", - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_name": "LayoutModel", - "_model_module_version": "2.0.0", - "_view_module": "@jupyter-widgets/base", - "_view_name": "LayoutView", - "_view_module_version": "2.0.0", - "_view_count": null, - "align_content": null, - "align_items": null, - "align_self": null, - "border_top": null, - "border_right": null, - "border_bottom": null, - "border_left": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "overflow": null, - "order": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null, - "object_fit": null, - "object_position": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_gap": null, - "grid_template_rows": null, - "grid_template_columns": null, - "grid_template_areas": null, - "grid_row": null, - "grid_column": null, - "grid_area": null - } - }, - "619a85547b9849d4994fedf0e7636143": { - "model_name": "OptionModel", - "model_module": "ipecharts", - "model_module_version": "^1.3.1", - "state": { - "_model_module": "ipecharts", - "_model_name": "OptionModel", - "_model_module_version": "^1.3.1", - "_view_module": "ipecharts", - "_view_name": null, - "_view_module_version": "^1.3.1", - "_view_count": null, - "title": null, - "legend": null, - "grid": null, - "xAxis": null, - "yAxis": null, - "polar": null, - "radiusAxis": null, - "angleAxis": null, - "radar": null, - "tooltip": null, - "axisPointer": null, - "toolbox": null, - "brush": null, - "geo": null, - "parallel": null, - "parallelAxis": null, - "singleAxis": null, - "timeline": null, - "graphic": null, - "calendar": null, - "matrix": null, - "thumbnail": null, - "dataset": null, - "aria": null, - "color": null, - "textStyle": null, - "stateAnimation": null, - "options": null, - "globe": null, - "geo3D": null, - "mapbox3D": null, - "grid3D": null, - "xAxis3D": null, - "yAxis3D": null, - "zAxis3D": null, - "series": null, - "animation": null, - "animationDelay": null, - "animationDelayUpdate": null, - "animationDuration": null, - "animationDurationUpdate": null, - "animationEasing": null, - "animationEasingUpdate": null, - "animationThreshold": null, - "backgroundColor": null, - "blendMode": null, - "darkMode": null, - "dataZoom": null, - "hoverLayerThreshold": null, - "media": null, - "richInheritPlainLabel": null, - "unknown_attrs": null, - "useUTC": null, - "visualMap": null - } - }, - "0d0ea8bc061e402e9701a0fcb405d171": { - "model_name": "EChartsWidgetModel", - "model_module": "ipecharts", - "model_module_version": "^1.3.1", - "state": { - "_model_module": "ipecharts", - "_model_name": "EChartsWidgetModel", - "_model_module_version": "^1.3.1", - "_view_module": "ipecharts", - "_view_name": "EChartsWidgetView", - "_view_module_version": "^1.3.1", - "_view_count": null, - "_dom_classes": [], - "tabbable": null, - "tooltip": null, - "option": "IPY_MODEL_e5b40f7a9b844d3c9a8cb0ff71dde706", - "style": {}, - "theme": null, - "device_pixel_ratio": 0, - "renderer": "canvas", - "use_dirty_rect": false, - "use_coarse_pointer": false, - "pointer_size": null, - "width": "auto", - "height": "auto", - "locale": "EN", - "layout": "IPY_MODEL_f688e0a29ef04335b8ff42090fdec2f1" - } - }, - "6cbc12529969445d90bb73f252ab2c64": { - "model_name": "XAxisModel", - "model_module": "ipecharts", - "model_module_version": "^1.3.1", - "state": { - "_model_module": "ipecharts", - "_model_name": "XAxisModel", - "_model_module_version": "^1.3.1", - "_view_module": "ipecharts", - "_view_name": null, - "_view_module_version": "^1.3.1", - "_view_count": null, - "alignTicks": null, - "animation": null, - "animationDelay": null, - "animationDelayUpdate": null, - "animationDuration": null, - "animationDurationUpdate": null, - "animationEasing": null, - "animationEasingUpdate": null, - "animationThreshold": null, - "axisLabel": null, - "axisLine": null, - "axisPointer": null, - "axisTick": null, - "boundaryGap": null, - "breakArea": null, - "breakLabelLayout": null, - "breaks": null, - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ], - "gridIndex": null, - "id": null, - "interval": null, - "inverse": null, - "jitter": null, - "jitterMargin": null, - "jitterOverlap": null, - "logBase": null, - "max": null, - "maxInterval": null, - "min": null, - "minInterval": null, - "minorSplitLine": null, - "minorTick": null, - "name": null, - "nameGap": null, - "nameLocation": null, - "nameMoveOverlap": null, - "nameRotate": null, - "nameTextStyle": null, - "nameTruncate": null, - "offset": null, - "position": null, - "scale": null, - "show": null, - "silent": null, - "splitArea": null, - "splitLine": null, - "splitNumber": null, - "startValue": null, - "tooltip": null, - "triggerEvent": null, - "type": "category", - "unknown_attrs": null, - "z": null, - "zlevel": null - } - }, - "b48fef8b930f45e199de52e65ef105f3": { - "model_name": "YAxisModel", - "model_module": "ipecharts", - "model_module_version": "^1.3.1", - "state": { - "_model_module": "ipecharts", - "_model_name": "YAxisModel", - "_model_module_version": "^1.3.1", - "_view_module": "ipecharts", - "_view_name": null, - "_view_module_version": "^1.3.1", - "_view_count": null, - "alignTicks": null, - "animation": null, - "animationDelay": null, - "animationDelayUpdate": null, - "animationDuration": null, - "animationDurationUpdate": null, - "animationEasing": null, - "animationEasingUpdate": null, - "animationThreshold": null, - "axisLabel": null, - "axisLine": null, - "axisPointer": null, - "axisTick": null, - "boundaryGap": null, - "breakArea": null, - "breakLabelLayout": null, - "breaks": null, - "data": null, - "gridIndex": null, - "id": null, - "interval": null, - "inverse": null, - "jitter": null, - "jitterMargin": null, - "jitterOverlap": null, - "logBase": null, - "max": null, - "maxInterval": null, - "min": null, - "minInterval": null, - "minorSplitLine": null, - "minorTick": null, - "name": null, - "nameGap": null, - "nameLocation": null, - "nameMoveOverlap": null, - "nameRotate": null, - "nameTextStyle": null, - "nameTruncate": null, - "offset": null, - "position": null, - "scale": null, - "show": null, - "silent": null, - "splitArea": null, - "splitLine": null, - "splitNumber": null, - "startValue": null, - "tooltip": null, - "triggerEvent": null, - "type": "value", - "unknown_attrs": null, - "z": null, - "zlevel": null - } - }, - "cc7bee19101045eea60644bdd587192b": { - "model_name": "LineModel", - "model_module": "ipecharts", - "model_module_version": "^1.3.1", - "state": { - "_model_module": "ipecharts", - "_model_name": "LineModel", - "_model_module_version": "^1.3.1", - "_view_module": "ipecharts", - "_view_name": null, - "_view_module_version": "^1.3.1", - "_view_count": null, - "animation": null, - "animationDelay": null, - "animationDelayUpdate": null, - "animationDuration": null, - "animationDurationUpdate": null, - "animationEasing": null, - "animationEasingUpdate": null, - "animationThreshold": null, - "areaStyle": null, - "blur": null, - "clip": null, - "colorBy": null, - "connectNulls": null, - "coord": null, - "coordinateSystem": null, - "coordinateSystemUsage": null, - "cursor": null, - "data": [ - 150, - 230, - 224, - 218, - 135, - 147, - 260 - ], - "dataGroupId": null, - "datasetIndex": null, - "dimensions": null, - "emphasis": null, - "encode": null, - "endLabel": null, - "id": null, - "itemStyle": null, - "label": null, - "labelLayout": null, - "labelLine": null, - "legendHoverLink": null, - "lineStyle": null, - "markArea": null, - "markLine": null, - "markPoint": null, - "name": null, - "polarId": null, - "polarIndex": null, - "sampling": null, - "select": null, - "selectedMode": null, - "seriesLayoutBy": null, - "showAllSymbol": null, - "showSymbol": null, - "silent": null, - "singleAxisId": null, - "singleAxisIndex": null, - "smooth": null, - "smoothMonotone": null, - "stack": null, - "stackOrder": null, - "stackStrategy": null, - "step": null, - "symbol": null, - "symbolKeepAspect": null, - "symbolOffset": null, - "symbolRotate": null, - "symbolSize": null, - "tooltip": null, - "triggerLineEvent": null, - "type": "line", - "universalTransition": null, - "unknown_attrs": null, - "xAxisId": null, - "xAxisIndex": null, - "yAxisId": null, - "yAxisIndex": null, - "z": null, - "zlevel": null - } - }, - "e5b40f7a9b844d3c9a8cb0ff71dde706": { - "model_name": "OptionModel", - "model_module": "ipecharts", - "model_module_version": "^1.3.1", - "state": { - "_model_module": "ipecharts", - "_model_name": "OptionModel", - "_model_module_version": "^1.3.1", - "_view_module": "ipecharts", - "_view_name": null, - "_view_module_version": "^1.3.1", - "_view_count": null, - "title": null, - "legend": null, - "grid": null, - "xAxis": "IPY_MODEL_6cbc12529969445d90bb73f252ab2c64", - "yAxis": "IPY_MODEL_b48fef8b930f45e199de52e65ef105f3", - "polar": null, - "radiusAxis": null, - "angleAxis": null, - "radar": null, - "tooltip": null, - "axisPointer": null, - "toolbox": null, - "brush": null, - "geo": null, - "parallel": null, - "parallelAxis": null, - "singleAxis": null, - "timeline": null, - "graphic": null, - "calendar": null, - "matrix": null, - "thumbnail": null, - "dataset": null, - "aria": null, - "color": null, - "textStyle": null, - "stateAnimation": null, - "options": null, - "globe": null, - "geo3D": null, - "mapbox3D": null, - "grid3D": null, - "xAxis3D": null, - "yAxis3D": null, - "zAxis3D": null, - "series": [ - "IPY_MODEL_cc7bee19101045eea60644bdd587192b" - ], - "animation": null, - "animationDelay": null, - "animationDelayUpdate": null, - "animationDuration": null, - "animationDurationUpdate": null, - "animationEasing": null, - "animationEasingUpdate": null, - "animationThreshold": null, - "backgroundColor": null, - "blendMode": null, - "darkMode": null, - "dataZoom": null, - "hoverLayerThreshold": null, - "media": null, - "richInheritPlainLabel": null, - "unknown_attrs": null, - "useUTC": null, - "visualMap": null - } - }, - "8674ae6d260147bcb87344ec55e143b6": { - "model_name": "LayoutModel", - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_name": "LayoutModel", - "_model_module_version": "2.0.0", - "_view_module": "@jupyter-widgets/base", - "_view_name": "LayoutView", - "_view_module_version": "2.0.0", - "_view_count": null, - "align_content": null, - "align_items": null, - "align_self": null, - "border_top": null, - "border_right": null, - "border_bottom": null, - "border_left": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "overflow": null, - "order": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null, - "object_fit": null, - "object_position": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_gap": null, - "grid_template_rows": null, - "grid_template_columns": null, - "grid_template_areas": null, - "grid_row": null, - "grid_column": null, - "grid_area": null - } - }, - "3170f445049b498e9b7147a6dd08425f": { - "model_name": "ButtonStyleModel", - "model_module": "@jupyter-widgets/controls", - "model_module_version": "2.0.0", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_name": "ButtonStyleModel", - "_model_module_version": "2.0.0", - "_view_module": "@jupyter-widgets/base", - "_view_name": "StyleView", - "_view_module_version": "2.0.0", - "_view_count": null, - "button_color": null, - "font_family": null, - "font_size": null, - "font_style": null, - "font_variant": null, - "font_weight": null, - "text_color": null, - "text_decoration": null - } - }, - "9fa96cec05854444baf5920a57a3ab39": { - "model_name": "ButtonModel", - "model_module": "@jupyter-widgets/controls", - "model_module_version": "2.0.0", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_name": "ButtonModel", - "_model_module_version": "2.0.0", - "_view_module": "@jupyter-widgets/controls", - "_view_name": "ButtonView", - "_view_module_version": "2.0.0", - "_view_count": null, - "_dom_classes": [], - "tabbable": null, - "tooltip": null, - "description": "Generate data", - "disabled": false, - "icon": "", - "button_style": "", - "style": "IPY_MODEL_3170f445049b498e9b7147a6dd08425f", - "layout": "IPY_MODEL_8674ae6d260147bcb87344ec55e143b6" - } - }, - "458c5f41c29f4afe9e65e5850ace77fa": { - "model_name": "LayoutModel", - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_name": "LayoutModel", - "_model_module_version": "2.0.0", - "_view_module": "@jupyter-widgets/base", - "_view_name": "LayoutView", - "_view_module_version": "2.0.0", - "_view_count": null, - "align_content": null, - "align_items": null, - "align_self": null, - "border_top": null, - "border_right": null, - "border_bottom": null, - "border_left": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "overflow": null, - "order": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null, - "object_fit": null, - "object_position": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_gap": null, - "grid_template_rows": null, - "grid_template_columns": null, - "grid_template_areas": null, - "grid_row": null, - "grid_column": null, - "grid_area": null - } - }, - "1330ce9e375e43d295c0bc50a128481c": { - "model_name": "EChartsRawWidgetModel", - "model_module": "ipecharts", - "model_module_version": "^1.3.1", - "state": { - "_model_module": "ipecharts", - "_model_name": "EChartsRawWidgetModel", - "_model_module_version": "^1.3.1", - "_view_module": "ipecharts", - "_view_name": "EChartsRawWidgetView", - "_view_module_version": "^1.3.1", - "_view_count": null, - "_dom_classes": [], - "tabbable": null, - "tooltip": null, - "option": { - "angleAxis": { - "startAngle": 0, - "type": "value" - }, - "legend": { - "data": [ - "line" - ] - }, - "polar": {}, - "radiusAxis": {}, - "series": [ - { - "coordinateSystem": "polar", - "data": [ - [ - 5, - 0 - ], - [ - 5.313952597646567, - 3.6 - ], - [ - 5.626666167821521, - 7.2 - ], - [ - 5.936906572928623, - 10.799999999999999 - ], - [ - 6.2434494358242745, - 14.4 - ], - [ - 6.545084971874737, - 18 - ], - [ - 6.84062276342339, - 21.599999999999998 - ], - [ - 7.1288964578253635, - 25.200000000000003 - ], - [ - 7.408768370508576, - 28.8 - ], - [ - 7.679133974894983, - 32.4 - ], - [ - 7.938926261462366, - 36 - ], - [ - 8.18711994874345, - 39.6 - ], - [ - 8.422735529643443, - 43.199999999999996 - ], - [ - 8.644843137107058, - 46.800000000000004 - ], - [ - 8.852566213878946, - 50.400000000000006 - ], - [ - 9.045084971874736, - 54 - ], - [ - 9.221639627510076, - 57.6 - ], - [ - 9.381533400219318, - 61.2 - ], - [ - 9.524135262330098, - 64.8 - ], - [ - 9.648882429441256, - 68.4 - ], - [ - 9.755282581475768, - 72 - ], - [ - 9.842915805643155, - 75.6 - ], - [ - 9.911436253643442, - 79.2 - ], - [ - 9.96057350657239, - 82.8 - ], - [ - 9.990133642141357, - 86.39999999999999 - ], - [ - 10, - 90 - ], - [ - 9.990133642141357, - 93.60000000000001 - ], - [ - 9.96057350657239, - 97.2 - ], - [ - 9.911436253643442, - 100.80000000000001 - ], - [ - 9.842915805643155, - 104.39999999999999 - ], - [ - 9.755282581475768, - 108 - ], - [ - 9.648882429441256, - 111.6 - ], - [ - 9.524135262330098, - 115.2 - ], - [ - 9.381533400219318, - 118.80000000000001 - ], - [ - 9.221639627510074, - 122.4 - ], - [ - 9.045084971874736, - 125.99999999999999 - ], - [ - 8.852566213878946, - 129.6 - ], - [ - 8.644843137107056, - 133.2 - ], - [ - 8.422735529643443, - 136.8 - ], - [ - 8.18711994874345, - 140.4 - ], - [ - 7.938926261462367, - 144 - ], - [ - 7.6791339748949845, - 147.6 - ], - [ - 7.408768370508579, - 151.2 - ], - [ - 7.128896457825363, - 154.8 - ], - [ - 6.840622763423391, - 158.4 - ], - [ - 6.545084971874737, - 162 - ], - [ - 6.243449435824276, - 165.6 - ], - [ - 5.936906572928625, - 169.2 - ], - [ - 5.626666167821525, - 172.79999999999998 - ], - [ - 5.313952597646567, - 176.4 - ], - [ - 5.000000000000001, - 180 - ], - [ - 4.6860474023534335, - 183.6 - ], - [ - 4.373333832178479, - 187.20000000000002 - ], - [ - 4.063093427071376, - 190.8 - ], - [ - 3.7565505641757246, - 194.4 - ], - [ - 3.454915028125262, - 198.00000000000003 - ], - [ - 3.159377236576608, - 201.60000000000002 - ], - [ - 2.871103542174639, - 205.2 - ], - [ - 2.591231629491425, - 208.79999999999998 - ], - [ - 2.3208660251050177, - 212.39999999999998 - ], - [ - 2.061073738537635, - 216 - ], - [ - 1.8128800512565517, - 219.6 - ], - [ - 1.5772644703565564, - 223.2 - ], - [ - 1.3551568628929433, - 226.8 - ], - [ - 1.147433786121053, - 230.4 - ], - [ - 0.9549150281252633, - 234 - ], - [ - 0.7783603724899235, - 237.60000000000002 - ], - [ - 0.6184665997806821, - 241.20000000000002 - ], - [ - 0.475864737669901, - 244.8 - ], - [ - 0.3511175705587444, - 248.39999999999998 - ], - [ - 0.24471741852423234, - 251.99999999999997 - ], - [ - 0.15708419435684517, - 255.6 - ], - [ - 0.0885637463565564, - 259.2 - ], - [ - 0.039426493427611176, - 262.8 - ], - [ - 0.009866357858642205, - 266.4 - ], - [ - 0, - 270 - ], - [ - 0.009866357858642205, - 273.6 - ], - [ - 0.03942649342761062, - 277.2 - ], - [ - 0.0885637463565564, - 280.8 - ], - [ - 0.15708419435684573, - 284.40000000000003 - ], - [ - 0.2447174185242318, - 288 - ], - [ - 0.3511175705587438, - 291.6 - ], - [ - 0.47586473766990045, - 295.2 - ], - [ - 0.6184665997806832, - 298.8 - ], - [ - 0.7783603724899224, - 302.4 - ], - [ - 0.9549150281252622, - 306 - ], - [ - 1.1474337861210548, - 309.6 - ], - [ - 1.3551568628929422, - 313.2 - ], - [ - 1.5772644703565553, - 316.8 - ], - [ - 1.8128800512565486, - 320.4 - ], - [ - 2.061073738537633, - 324 - ], - [ - 2.320866025105018, - 327.6 - ], - [ - 2.5912316294914195, - 331.2 - ], - [ - 2.871103542174639, - 334.8 - ], - [ - 3.1593772365766064, - 338.4 - ], - [ - 3.454915028125262, - 342 - ], - [ - 3.7565505641757193, - 345.59999999999997 - ], - [ - 4.063093427071377, - 349.2 - ], - [ - 4.373333832178476, - 352.8 - ], - [ - 4.6860474023534335, - 356.4 - ], - [ - 4.999999999999999, - 360 - ] - ], - "name": "line", - "type": "line" - } - ], - "title": { - "text": "Two Value-Axes in Polar" - }, - "tooltip": { - "axisPointer": { - "type": "cross" - }, - "trigger": "axis" - } - }, - "style": {}, - "theme": null, - "device_pixel_ratio": 0, - "renderer": "canvas", - "use_dirty_rect": false, - "use_coarse_pointer": false, - "pointer_size": null, - "width": "auto", - "height": "auto", - "locale": "EN", - "layout": "IPY_MODEL_458c5f41c29f4afe9e65e5850ace77fa" - } - } - } - } - } - }, - "nbformat_minor": 5, - "nbformat": 4, - "cells": [ - { - "id": "a3abf27e", - "cell_type": "markdown", - "source": "# Data Visualization in Jupyter Notebooks using Apache Echarts\n", - "metadata": {} - }, - { - "id": "21b59e76", - "cell_type": "markdown", - "source": "![Banner](./top.jpeg \"Banner\")\n", - "metadata": { - "specta": { - "outputSize": "Full" - } - } - }, - { - "id": "bf93b954", - "cell_type": "markdown", - "source": "\n\nIn the realm of data science and visualization, Jupyter Notebook has emerged as a powerful tool for data analysis and storytelling. Integrating interactive and aesthetically pleasing charts can significantly enhance the presentation of data insights.", - "metadata": {} - }, - { - "id": "a3826498", - "cell_type": "markdown", - "source": "[Apache Echarts](https://echarts.apache.org/en/index.html) is one of the most versatile libraries for creating interactive charts. This blog post explores how to leverage ipecharts, a new Python library that seamlessly integrates Echarts into Jupyter Notebooks, to craft stunning visualizations within your notebooks.\n\n*Disclaimer: I am the author of this library.*", - "metadata": {} - }, - { - "id": "1a2ffcfd", - "cell_type": "markdown", - "source": "

• • •

", - "metadata": {} - }, - { - "id": "e2569313", - "cell_type": "markdown", - "source": "## Motivation\n\n`ipecharts` is not the first attempt to make Echarts available on Jupyter Notebooks. pyecharts is a popular open-source library that allows you to create interactive charts in Python and supports both notebooks and standalone Python scripts.", - "metadata": {} - }, - { - "id": "330ee266", - "cell_type": "markdown", - "source": "While pyechartscan create charts in the notebooks, it does not use the Jupyter Widgets system but instead injects HTML code into the notebook to render the charts. This approach makes using pyecharts in other Jupyter applications or interacting with other widgets libraries harder.", - "metadata": {} - }, - { - "id": "6e320817", - "cell_type": "markdown", - "source": "On the other hand, ipecharts adopts the native way of creating interactive charts in Jupyter Notebooks by using Jupyter Widgets. It makes the created charts compatible with a wide range of tools and libraries in the Jupyter ecosystem.", - "metadata": {} - }, - { - "id": "250f141f", - "cell_type": "markdown", - "source": "

• • •

", - "metadata": {} - }, - { - "id": "fc22d007", - "cell_type": "markdown", - "source": "## Getting started with ipecharts", - "metadata": {} - }, - { - "id": "b05b1479", - "cell_type": "markdown", - "source": "### Installation\n\n`ipecharts` is available on PyPI and conda-forge:\n\n```bash\n# Installing with pip\npip install ipecharts\n\n# Installing with conda\nconda install -c conda-forge ipecharts\n```\n\nIt requires ipywidgets ≥8.0 and does not work with Jupyter Notebook <7 . More detailed documentation is available at Read the Docs. You can also try it live in this JupyterLite instance.", - "metadata": {} - }, - { - "id": "ce613931", - "cell_type": "markdown", - "source": "### Creating a simple line plot\n\nipechart is a very slim wrapper outside of the Echarts Javascript library so translating the Javascript version of a chart into ipechartswidget is straightforward. Let’s begin with a basic line example from Echarts official documentation:\n\n```typescript\n// Example from https://echarts.apache.org/examples/en/editor.html?c=line-simple&lang=ts\nimport * as echarts from 'echarts';\n\ntype EChartsOption = echarts.EChartsOption;\n\nvar chartDom = document.getElementById('main')!;\nvar myChart = echarts.init(chartDom);\nvar option: EChartsOption;\n\noption = {\n xAxis: {\n type: 'category',\n data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']\n },\n yAxis: {\n type: 'value'\n },\n series: [\n {\n data: [150, 230, 224, 218, 135, 147, 260],\n type: 'line'\n }\n ]\n};\n\noption && myChart.setOption(option);\n```", - "metadata": {} - }, - { - "id": "7824f42f", - "cell_type": "markdown", - "source": "The entry point of a chart in ipecharts is the EchartWidget class:\n", - "metadata": {} - }, - { - "id": "3de1a807", - "cell_type": "code", - "source": "from ipecharts import EChartsWidget\nchart = EChartsWidget()", - "metadata": { - "specta": { - "showOutput": "No", - "showSource": "Yes" - }, - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - }, - "trusted": true - }, - "outputs": [], - "execution_count": null - }, - { - "id": "503e0e22", - "cell_type": "markdown", - "source": "Just as in the Javascript example, we need to set the option of this chart. For all top-level keys of the Echarts option and the entries of series, ipecharts provides Python class counterparts with the same name. Here is the equivalent of the above option object defined with ipecharts classes:\n", - "metadata": {} - }, - { - "id": "d8741975", - "cell_type": "code", - "source": "from ipecharts.option import Option, XAxis, YAxis\nfrom ipecharts.option.series import Line\n\nxAxis = XAxis(\n type=\"category\",\n data=[\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"],\n)\nyAxis = YAxis(type=\"value\")\nline = Line(data=[150, 230, 224, 218, 135, 147, 260])\n\noption = Option()\noption.xAxis = xAxis\noption.yAxis = yAxis\noption.series = [line]", - "metadata": { - "specta": { - "showOutput": "No", - "showSource": "Yes" - }, - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - }, - "trusted": true - }, - "outputs": [], - "execution_count": null - }, - { - "id": "c001b376", - "cell_type": "markdown", - "source": "All classes here are based on traitlets so you can initialize the instance by using keyword arguments or by setting the property values. Finally, updating the option value of our chart gives us the same chart as the Javascript \n", - "metadata": {} - }, - { - "id": "2b2c01c4", - "cell_type": "code", - "source": "chart.option = option\nchart", - "metadata": { - "specta": { - "showSource": "Yes", - "outputSize": "Big" - }, - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - }, - "trusted": true - }, - "outputs": [], - "execution_count": null - }, - { - "id": "19dea3c1", - "cell_type": "markdown", - "source": "### Adding Interactivity", - "metadata": {} - }, - { - "id": "1105f857", - "cell_type": "markdown", - "source": "By using traitlets to configure your chart, any change in the option properties will be applied to the chart automatically. We will use the Button widget of ipywidgets to change the line data dynamically.\n", - "metadata": {} - }, - { - "id": "a5fb539d", - "cell_type": "code", - "source": "from ipywidgets.widgets import Button\nfrom numpy.random import randint\n\ndef update_line_data(b): \n line.data = randint(0, 300, 7).tolist()\n\nbutton = Button(description=\"Generate data\")\nbutton.on_click(update_line_data)\n\ndisplay(button, chart)", - "metadata": { - "specta": { - "showSource": "Yes" - }, - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - }, - "trusted": true - }, - "outputs": [], - "execution_count": null - }, - { - "id": "0e491acc", - "cell_type": "markdown", - "source": "In the on_click callback of the button, we update the data property of the line instance, the changed signal is propagated up to the top-level widget and the chart will be updated automatically.", - "metadata": {} - }, - { - "id": "86b1b92d", - "cell_type": "markdown", - "source": "### Creating charts without using traitlets configuration", - "metadata": {} - }, - { - "id": "2e5acf08", - "cell_type": "markdown", - "source": "In many situations, we simply want to display the data without adding interactivity. For this use case, users can convert any option object used by a Javascript chart to a Python dictionary and pass it to the EchartRawWidget of ipecharts.\n\nHere is the equivalent of the Two Value-Axes in Polar example from the official Echarts documentation using EchartRawWidget:", - "metadata": {} - }, - { - "id": "933a5a49", - "cell_type": "code", - "source": "from ipecharts import EChartsRawWidget\nimport math\n\ndata = []\nfor i in range(101):\n theta = (i / 100) * 360\n r = 5 * (1 + math.sin((theta / 180) * math.pi))\n data.append([r, theta])\n\noption = {\n \"title\": {\"text\": \"Two Value-Axes in Polar\"},\n \"legend\": {\"data\": [\"line\"]},\n \"polar\": {},\n \"tooltip\": {\"trigger\": \"axis\", \"axisPointer\": {\"type\": \"cross\"}},\n \"angleAxis\": {\"type\": \"value\", \"startAngle\": 0},\n \"radiusAxis\": {},\n \"series\": [\n {\"coordinateSystem\": \"polar\", \"name\": \"line\", \"type\": \"line\", \"data\": data}\n ],\n}\nEChartsRawWidget(option=option)", - "metadata": { - "tags": [ - "specta:visible" - ], - "vscode": { - "languageId": "plaintext" - }, - "trusted": true - }, - "outputs": [], - "execution_count": null - }, - { - "id": "213d1b6b", - "cell_type": "markdown", - "source": "## What’s next\n\nIn this first version, I focused on generating the option configuration class to be able to translate the Javascript charts to the Python ones without too many changes. Echarts has a lot of other customizations in theming, managing maps, or chart animation… These aspects will be addressed in future releases.\n\nYou can follow the development of this library on GitHub. Stay tuned and happy charting!", - "metadata": {} - }, - { - "id": "b7129618", - "cell_type": "markdown", - "source": "## About the author\n\nDuc Trung Le is an open-source developer who works on this project in his free time.", - "metadata": {} - }, - { - "id": "1dd7aa92", - "cell_type": "markdown", - "source": "", - "metadata": {} } - ] + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/demo/files/static-rendering.ipynb b/demo/files/static-rendering.ipynb new file mode 100644 index 0000000..0826657 --- /dev/null +++ b/demo/files/static-rendering.ipynb @@ -0,0 +1,1503 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Rendered without a kernel\n", + "\n", + "Everything below — the plots, the numbers, the widgets — was produced by a\n", + "Monte Carlo simulation that ran **once**, while this notebook was being\n", + "written. The result was then stored inside the notebook file itself as a\n", + "*render cache*.\n", + "\n", + "You are looking at that cache. No Python kernel was downloaded, started, or\n", + "executed to show you this page: Specta rebuilt the document straight from the\n", + "stored outputs. That is what **static rendering** does.\n", + "\n", + "The simulation takes a few seconds to run. Multiply that by every reader who\n", + "ever opens the page — and by the seconds it takes to fetch and boot a\n", + "WebAssembly Python kernel first — and it is work worth doing exactly once.\n" + ], + "id": "cell-00" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "specta": { + "showSource": "Yes", + "showOutput": "No" + } + }, + "outputs": [], + "source": [ + "import time\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from ipywidgets import HTML, HBox, IntSlider, Layout, Output, VBox\n", + "\n", + "N_PATHS = 20_000\n", + "N_STEPS = 250\n", + "START = 100.0\n", + "DRIFT = 0.06\n", + "VOLATILITY = 0.22\n", + "\n", + "rng = np.random.default_rng(20260818)" + ], + "id": "cell-01" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The expensive part\n", + "\n", + "Twenty thousand price paths over 250 steps: five million normal draws, plus the\n", + "cumulative product that turns them into trajectories.\n" + ], + "id": "cell-02" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "specta": { + "showSource": "Yes", + "showOutput": "Yes" + } + }, + "outputs": [], + "source": [ + "started = time.perf_counter()\n", + "\n", + "dt = 1.0 / N_STEPS\n", + "shocks = rng.normal(\n", + " loc=(DRIFT - 0.5 * VOLATILITY**2) * dt,\n", + " scale=VOLATILITY * np.sqrt(dt),\n", + " size=(N_PATHS, N_STEPS),\n", + ")\n", + "paths = START * np.exp(np.cumsum(shocks, axis=1))\n", + "paths = np.hstack([np.full((N_PATHS, 1), START), paths])\n", + "\n", + "elapsed = time.perf_counter() - started\n", + "print(f\"{N_PATHS:,} paths x {N_STEPS} steps simulated in {elapsed:.2f} s\")" + ], + "id": "cell-03" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Where the paths end up\n", + "\n", + "The shaded bands are the 10–90% and 25–75% ranges across all paths; the solid\n", + "line is the median. A handful of individual paths are drawn on top to show how\n", + "much any single one wanders away from it.\n" + ], + "id": "cell-04" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "specta": { + "showSource": "No", + "showOutput": "Yes", + "outputSize": "Big" + } + }, + "outputs": [], + "source": [ + "steps = np.arange(N_STEPS + 1)\n", + "low, q25, median, q75, high = np.percentile(paths, [10, 25, 50, 75, 90], axis=0)\n", + "\n", + "fig, ax = plt.subplots(figsize=(9, 4.5))\n", + "ax.fill_between(steps, low, high, color=\"#4c72b0\", alpha=0.18, label=\"10-90%\")\n", + "ax.fill_between(steps, q25, q75, color=\"#4c72b0\", alpha=0.32, label=\"25-75%\")\n", + "ax.plot(steps, median, color=\"#1f3d7a\", lw=2, label=\"median\")\n", + "for path in paths[:8]:\n", + " ax.plot(steps, path, color=\"#c44e52\", lw=0.7, alpha=0.6)\n", + "\n", + "ax.set_xlabel(\"step\")\n", + "ax.set_ylabel(\"value\")\n", + "ax.set_title(\"20,000 simulated paths\")\n", + "ax.legend(loc=\"upper left\", frameon=False)\n", + "ax.spines[[\"top\", \"right\"]].set_visible(False)\n", + "fig.tight_layout()\n", + "plt.show()" + ], + "id": "cell-05" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Widgets survive the cache too\n", + "\n", + "The render cache stores the state of every `ipywidgets` model alongside the\n", + "outputs, so widgets are rebuilt looking exactly as they did when the cache was\n", + "saved. That is what makes static rendering usable for dashboards rather than\n", + "only for notebooks full of text and images.\n" + ], + "id": "cell-06" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "specta": { + "showSource": "Yes", + "showOutput": "Yes" + } + }, + "outputs": [], + "source": [ + "final = paths[:, -1]\n", + "\n", + "def card(label, value):\n", + " return HTML(\n", + " f\"
\"\n", + " f\"
{label}
\"\n", + " f\"
{value}
\"\n", + " )\n", + "\n", + "HBox(\n", + " [\n", + " card(\"median\", f\"{np.median(final):.1f}\"),\n", + " card(\"mean\", f\"{final.mean():.1f}\"),\n", + " card(\"5th pct\", f\"{np.percentile(final, 5):.1f}\"),\n", + " card(\"95th pct\", f\"{np.percentile(final, 95):.1f}\"),\n", + " card(\"above start\", f\"{(final > START).mean():.0%}\"),\n", + " ],\n", + " layout=Layout(flex_flow=\"row wrap\", grid_gap=\"10px\", margin=\"4px 0 12px 0\"),\n", + ")" + ], + "id": "cell-07" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### What a cache cannot do\n", + "\n", + "The slider below is real — it was rendered from stored widget state, and you can\n", + "drag it. But the readout next to it only changes when Python is there to answer,\n", + "and in a statically rendered page there is no kernel listening.\n", + "\n", + "Open the settings menu in the top bar and press **Render with kernel** to start\n", + "one; the slider becomes live, at the cost of the startup you just skipped.\n" + ], + "id": "cell-08" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "specta": { + "showSource": "Yes", + "showOutput": "Yes" + } + }, + "outputs": [], + "source": [ + "slider = IntSlider(value=50, min=1, max=99, description=\"percentile:\")\n", + "readout = Output()\n", + "\n", + "def show_percentile(change=None):\n", + " readout.clear_output()\n", + " with readout:\n", + " print(f\"percentile {slider.value}: {np.percentile(final, slider.value):.2f}\")\n", + "\n", + "slider.observe(show_percentile, names=\"value\")\n", + "show_percentile()\n", + "\n", + "VBox([slider, readout])" + ], + "id": "cell-09" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Distribution of outcomes" + ], + "id": "cell-10" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "specta": { + "showSource": "No", + "showOutput": "Yes", + "outputSize": "Big" + } + }, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(9, 3.6))\n", + "ax.hist(final, bins=80, color=\"#4c72b0\", alpha=0.85)\n", + "ax.axvline(START, color=\"#c44e52\", lw=1.5, ls=\"--\", label=\"start\")\n", + "ax.axvline(np.median(final), color=\"#1f3d7a\", lw=1.5, label=\"median\")\n", + "ax.set_xlabel(\"value at final step\")\n", + "ax.set_ylabel(\"paths\")\n", + "ax.legend(frameon=False)\n", + "ax.spines[[\"top\", \"right\"]].set_visible(False)\n", + "fig.tight_layout()\n", + "plt.show()" + ], + "id": "cell-11" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Refreshing the cache\n", + "\n", + "The cache lives in this notebook's metadata, so it travels with the file — no\n", + "sidecar to keep in sync.\n", + "\n", + "To rebuild it, open the notebook in JupyterLab with **Open With ▸ Specta**, wait\n", + "for it to finish executing, then press **Save cache** in the settings menu.\n", + "Specta stores a hash of the code alongside the cache: edit any cell and the\n", + "settings menu will report the cache as out of sync, and offer to re-run the\n", + "notebook with a kernel instead of showing you a stale page.\n" + ], + "id": "cell-12" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.13 (XPython)", + "language": "python", + "name": "xpython" + }, + "language_info": { + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "version": "3.13.1" + }, + "specta": { + "defaultLayout": "article", + "hideTopbar": "No" + }, + "spectaSnapshot": { + "version": 1, + "hash": "d83b82dde397eac0", + "timestamp": 1787084607961, + "notebook": { + "cells": [ + { + "cell_type": "markdown", + "id": "cell-00", + "metadata": {}, + "source": [ + "# Rendered without a kernel\n", + "\n", + "Everything below — the plots, the numbers, the widgets — was produced by a\n", + "Monte Carlo simulation that ran **once**, while this notebook was being\n", + "written. The result was then stored inside the notebook file itself as a\n", + "*render cache*.\n", + "\n", + "You are looking at that cache. No Python kernel was downloaded, started, or\n", + "executed to show you this page: Specta rebuilt the document straight from the\n", + "stored outputs. That is what **static rendering** does.\n", + "\n", + "The simulation takes a few seconds to run. Multiply that by every reader who\n", + "ever opens the page — and by the seconds it takes to fetch and boot a\n", + "WebAssembly Python kernel first — and it is work worth doing exactly once.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-01", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-18T20:23:15.340622Z", + "iopub.status.busy": "2026-08-18T20:23:15.340288Z", + "iopub.status.idle": "2026-08-18T20:23:15.659751Z", + "shell.execute_reply": "2026-08-18T20:23:15.659200Z" + }, + "specta": { + "showOutput": "No", + "showSource": "Yes" + } + }, + "outputs": [], + "source": [ + "import time\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from ipywidgets import HTML, HBox, IntSlider, Layout, Output, VBox\n", + "\n", + "N_PATHS = 20_000\n", + "N_STEPS = 250\n", + "START = 100.0\n", + "DRIFT = 0.06\n", + "VOLATILITY = 0.22\n", + "\n", + "rng = np.random.default_rng(20260818)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-02", + "metadata": {}, + "source": [ + "## The expensive part\n", + "\n", + "Twenty thousand price paths over 250 steps: five million normal draws, plus the\n", + "cumulative product that turns them into trajectories.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-03", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-18T20:23:15.661641Z", + "iopub.status.busy": "2026-08-18T20:23:15.661265Z", + "iopub.status.idle": "2026-08-18T20:23:15.789362Z", + "shell.execute_reply": "2026-08-18T20:23:15.788785Z" + }, + "specta": { + "showOutput": "Yes", + "showSource": "Yes" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "20,000 paths x 250 steps simulated in 0.12 s\n" + ] + } + ], + "source": [ + "started = time.perf_counter()\n", + "\n", + "dt = 1.0 / N_STEPS\n", + "shocks = rng.normal(\n", + " loc=(DRIFT - 0.5 * VOLATILITY**2) * dt,\n", + " scale=VOLATILITY * np.sqrt(dt),\n", + " size=(N_PATHS, N_STEPS),\n", + ")\n", + "paths = START * np.exp(np.cumsum(shocks, axis=1))\n", + "paths = np.hstack([np.full((N_PATHS, 1), START), paths])\n", + "\n", + "elapsed = time.perf_counter() - started\n", + "print(f\"{N_PATHS:,} paths x {N_STEPS} steps simulated in {elapsed:.2f} s\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-04", + "metadata": {}, + "source": [ + "## Where the paths end up\n", + "\n", + "The shaded bands are the 10–90% and 25–75% ranges across all paths; the solid\n", + "line is the median. A handful of individual paths are drawn on top to show how\n", + "much any single one wanders away from it.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-05", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-18T20:23:15.790821Z", + "iopub.status.busy": "2026-08-18T20:23:15.790564Z", + "iopub.status.idle": "2026-08-18T20:23:16.018375Z", + "shell.execute_reply": "2026-08-18T20:23:16.017944Z" + }, + "specta": { + "outputSize": "Big", + "showOutput": "Yes", + "showSource": "No" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA3kAAAG4CAYAAAD42y7tAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzsvQd8o3l17n/Um7uneGZsj6f33nZm+87uznb6Uu6lJEAglIQ/JQlJuCEktNBSuKRwIbQEAgm7wML2Njs7vffiGffera636P95zmtpJFm2JVvu5wtaj1VfvZKl9/mdc57HFI1GoyQIgiAIgiAIgiDMCMyTvQGCIAiCIAiCIAhC7hCRJwiCIAiCIAiCMIMQkScIgiAIgiAIgjCDEJEnCIIgCIIgCIIwgxCRJwiCIAiCIAiCMIMQkScIgiAIgiAIgjCDEJEnCIIgCIIgCIIwgxCRJwiCIAiCIAiCMIMQkScIgiAIgiAIgjCDEJEnCIIwA3nppZfo93//92n16tXk8Xho0aJF9IY3vIFOnDiR9vonT56ke++9l/Ly8qioqIje/OY3040bNzJ+vGxu/0//9E+8XQ6Hg5YsWUJ//dd/TYqiDLpee3s7ve9976M5c+aQ2+2m3bt304svvki5oqqqiu9/MvjBD35AJpOJamtrs77twYMH6fOf/zz19vbmfLuwP7BfJoMvfelL9OSTTw65r44fPz4p2yUIgjAdEZEnCIIwA/nnf/5nFhB//Md/TL/73e/oH/7hH1g03XLLLSwAE7l8+TLdddddFIlE6Oc//zl9//vfp6tXr9Ltt99OHR0dIz5WNrf/4he/yNsEEfjss8/SRz7yET64/+hHP5p0vXA4THv37mVRh23/1a9+RfPnz6cHHniAXn311ZzsoyeeeII+97nP0XQDIg/CeDxE3mQylMgTBEEQssc6itsIgiAIU5z/+3//L82bNy/pPAik5cuX88H0PffcEz////yf/8NVtaeeeooKCgr4vG3bttGKFSvo61//On31q18d9rEyvX1XVxf97d/+LX3wgx/kbQAQh6ji/eVf/iV94hOfoLVr1/L53/ve9+j8+fMsaFDBA3fffTdt2rSJ/uRP/oSOHDky5n20ZcuWMd+HIAiCIExFpJInCIIwA0kVeACtlBBRDQ0N8fNUVWVx9pa3vCUu0MDixYtZVKHaNRzZ3P6ZZ56hUChEv/d7v5d0H/g9Go0mVXFwu1WrVsUFHrBarfS///f/pqNHj1JTU9Ow23Xq1Cl65JFHeD9AgC5cuJAefvhhamxsHLJd85VXXuG2wP/8z/+kP/3TP6UFCxbwPnv00Uepra2NvF4v/cEf/AG3j+KE7fb5fPHbo3KK26O9MBWcjxbL4Xj++ee5pba8vJycTicL8g996EPU2dkZvw7u4zOf+Qz/G62uuF+csO0x/uu//ov3G9p0sf379u3j/ZEKthP7GPtnzZo19KMf/YgyBfsO+xev08aNG3l7ly5dSv/4j/+YdD283p/61Kdo8+bNVFhYSCUlJbxtqMym7h+/308//OEP488JCwCJYP//4R/+Ie/70tJSrgY3NzcnXQdVatwOl7tcLqqsrOT3ZiAQyPi5CYIgzASkkicIgjBL6Ovr49m5xCre9evXKRgM8oF6KjgPwgMH6jiIT0c2t0dlDmzYsCHpehBTOHCPXQ7wb7R7prtPcOHCBZ4zTAfEwn333cciCBVNtHm2trbSyy+/zEJhJP78z/+cBSpEEITbpz/9aXrnO9/JIhOVxJ/+9KcsmnC9/Pz8QcJmtGBfQgB94AMfYEGEx/7mN79Jt912G507d45sNhtf1t3dzXONv/zlL3nfgVgFFBVSVEUhQPETLbRf+9rXeF9CHMeuh+eG60BUfuMb3+D3BgQk2mTN5szWf0+fPs3VV9yurKyM/uM//oNbcfGY2GcA94ftxe94vXDZCy+8wALt3//93+k973kPX+/QoUP8vsR+j7XQJi4aADx3CHWIcCxUQOxC9Mfaj7G/cDmeK1qGMRuKxQAsLuBxMdcpCIIwa4gKgiAIs4L/9b/+V9RqtUaPHz8eP+/111+P4qvgpz/96aDrf+lLX+LLmpubh7zPbG7/wQ9+MOpwONLez8qVK6P3339//HebzRb90Ic+NOh6Bw8e5Pv8z//8zyG3Cc8P13nyySejw7F48eLoe9/73vjvL7/8Mt/u0UcfTbreJz7xCT7/j/7oj5LOf+Mb3xgtKSmJ/15TU8PX+/d///dBj4Xz/+qv/ir+O66D83CbdOi6HlUUJVpXV8fX+9WvfhW/7Gtf+1ra29bX1/Pr+/GPfzzpfK/XGy0rK4s+/vjj/LumadGFCxdGt27dyo8To7a2lvc79stI4Domkyl6+vTppPPvu+++aEFBQdTv96e9naqq/Lze//73R7ds2ZJ0mcfjSXo9UvfVRz7ykaTz/+7v/o7Pb2lp4d//+7//m39P3SZBEITZiLRrCoIgzAJQHUGl5Vvf+hbPy6WC9rihGO6ybG+fzeOMdpvQ5lhcXMwtl//yL/9CFy9epGxAG2IiaGUEqBKlno8qVWLL5liAMc6HP/xhqqio4KohKndoewWXLl0a8fYwskH7LKpj+Bk7oYp65513xls6r1y5wm2O73rXu5L2Ix5rz549GW/vunXruLKZCO6zv7+fK8YxfvGLX9Ctt97KraOx54WZy0yeUyKPPfZY2qpuXV0d/0RLqN1u55ZatH1m4w4rCIIw0xCRJwiCMMOBEyMMT+Bs+bGPfSzpMswuxUxRUoGAgQhA29tQZHN7XBetm+nmo3BdzGsl3u9Q9wkSr5sKWh3hwImDfrRUQoxgJu+v/uqv0kY1pJJ63xAOw52P5zRWdF2n+++/n1swYSwDV1G0Vx4+fJgvR0vsSGBuEOzYsYOFVOIJc3qx2b7YfkWLZSrpzhuK4W4feww8n8cff5xbNX/yk59wW+axY8c43iPb/RZ7r8XALGHivlm2bBm3gmIOE26t+B0nuLMKgiDMNmQmTxAEYYYLPMxM4QTBkwoOgmFQgZmvVHAeqmJDzeNle/vYLB7O37VrV/x6mJeDAFm/fn38PFx3qPsEiddNB27/s5/9jA1dzp49yzNoX/jCF3hb/+zP/ozGg9jzxBxaIunEaiqYQTxz5gxv53vf+974+dXV1Rk/PuYawX//93/HK4DDiSXs91TSnTcUw90+9hgQdpiNhMhMrBqm7qNcgXk8nDRN41w9zC5ibhBzme94xzvG5TEFQRCmIlLJEwRBmKH8zd/8DYs7GHCgipUOtM/BPRIVl0RTkvr6ejYqgUHGcGRze0Q4QAiluk/Gwq7f+MY3xs9705vexPl7iVEJaD2EaIBARGUuE3C/aClEmyoqiolthLkGQgLPD6IykVQnyaG2M7E6FeNf//VfB103tYIVAy6aeD1g4LJ9+/a0JwBHTRi2wEDGGBekeNsjIisyBeY3EKaJwBQFZjRbt26NPy9UPBMFHoRgun2C55VJxTITLBYLv09gvAPG83UXBEGYikglTxAEYQYCx0Tk10FYYZYs1vYXA6HoidU+tPhhFg1VLrTR4baoDMH+PhHY06MVMlEcZHp7tDtCcGI+EP9GeyJa9yBE4ZwYc34EaOfDAfrb3vY2+spXvsIteN/5znd4ngwtecOBSAdcF6IRtv7YVohQhIfDdXO8gJCB2yOcHVHhhLhEyyWEz0isXr2ab4P9h+3F/vnNb37D7qSpxCqiaENE1Q/tmBBuiDVAtfIv/uIveB4Nrz1mE9HGie1ApAJeK7hnYgEA+xxiGrmF2Dcxl8xMgdDGnBxuB9EIAY7tRS5izMkS7wnse4Tev/Wtb2VXTDw2rn/t2rVBzwtzg3jeuBxiEc8rUzB/CadNvN8RnYD3IV4LcO+992Z8P4IgCDOCyXZ+EQRBEHLPnXfeyU6DQ53SOVLu3bs36na72R0RzpHV1dWDrrdt2zZ2ahzt7cE//MM/sJum3W6PVlZWsutkJBIZdL3W1tboe97zHnawdDqd0VtuuSX6/PPPj/jcL1++HH3nO98ZXbZsWdTlckULCwujO3fujP7gBz/IyF3zF7/4RVp3x2PHjiWdj+3G+R0dHfHz+vr6oh/4wAei8+fPZ7dIOHXCtTITd82LFy+yO2V+fn60uLg4+ra3vY0dM1NvCz772c+yQ6bZbObLse0x4Cp699138+sAN1M8z7e+9a3RF154Iek+/t//+3/RFStW8OuA1+P73/8+749M3TUffvhhdrRct24d30dVVVX0m9/85qDrfuUrX+HLsC1r1qyJfve7343vu0TginnrrbfyewiX4T083P6PvV6x537o0KHom970Jt42PFZpaSnfx69//esRn48gCMJMw4T/TLbQFARBEKY+aMdEhenv//7v2dhCmL2gaoi5SFRNBUEQhKmHzOQJgiAIGbF//352SUR7nyAIgiAIUxcReYIgCEJGYNaptrY2Hh0gCIIgCMLURNo1BUEQBEEQBEEQZhBSyRMEQRAEQRAEQZhBiMgTBEEQBEEQBEGYQZgne4gfIbrI2kG+0JNPPjnkdT/0oQ/xdeDqlkg4HKaPf/zjnMeEDCBk9jQ2Nk7A1guCIAiCIAiCIEw9JlXk+f1+Dov99re/Pez1IP6OHDnCYjCVT3ziE/TEE0/Qz372Mzpw4AD5fD4OX9U0LePtQIpEf39/UrivIAiCIAiCIAjCdMQ6mQ/+4IMP8mk4mpqa6GMf+xg9++yz7OyWSF9fH33ve9+jH//4x3TvvffyeT/5yU+ooqKCXnjhBdq3b1/G2U+FhYV8fwUFBWN4RoIgCIIgCIIgCJPLlJ7J03Wd3v3ud9NnPvMZWrdu3aDLT5w4QYqi0P333x8/D9U+BLQePHhwgrdWEARBEARBEARhllfyRuKrX/0qWa1W+qM/+qO0l7e2tnJeU3FxcdL58+fP58uGAnN8OMVAq6YgCIIgCIIgCMJMYMpW8lCl+4d/+Af6wQ9+wIYr2YDZuuFu8+Uvf5nbM2MntHcKgiAIgiAIgiDMBKasyHvttdeovb2dKisruZqHU11dHX3qU5+iqqoqvk5ZWRlFIhHq6elJui1uh2reUHz2s5/l+bvYqaGhYdyfjyAIgiAIgiAIwqwWeZjFO3v2LJ0+fTp+wrwd5vNgwgK2bdtGNpuNnn/++fjtWlpa6Pz587Rnz54h79vhcLDBSuJJEARBEARBEARhJjCpM3mIO6iuro7/XlNTw2KupKSEK3ilpaVJ14egQ/Vu1apV/DtaLd///vdzdQ/Xxe0+/elP04YNG+Jum4IgCIIgCIIgCLOJSRV5x48fp7vvvjv++yc/+Un++d73vpdn8TLhW9/6FrdyPv744xQMBmnv3r18W4vFMm7bLQiCIAiCIAiCMFUxRSUBnN01JSdPEARBEARBEISZwJSdyRMEQRAEQRAEQRCyR0SeIAiCIAiCIAjCDEJEniAIgiAIgiAIwgxCRJ4gCIIgCIIgCMIMYlLdNaczqqaTpkcn7PEsZhNZLaLJBUEQBEEQBEEYHlENoxR41xv76HJt94Sd8Hh43GzYv38/PfrooxwibzKZ6Mknn0y6HMaqn//85/lyl8tFd911F124cGHE+z158iTdd999VFRUxPmEf/AHf8CZh4nU19fzY3s8HpozZw790R/9EUUikfjltbW1dMcdd1BeXh7deeedVFdXl3T7hx9+mP7nf/4nq+crCIIgCIIgCIKIvFGBCl4oopLVYiKn3TLuJzwOHi/byqHf76dNmzbRt7/97bSX/93f/R1985vf5MuPHTvGQfMQb16vd8j7bG5u5qD55cuX05EjR+iZZ55hYfi+973v5v7RNBZpePwDBw7Qz372MxZsCK2PgX8vWrSITp06xY+LEPsYuD5yDt/ylrdk9XwFQRAEQRAEQZCcvFHl5IUVjatrEGB22/iHrkcUjUIRjVZXlZBjlI+HSt4TTzxBb3zjG+NVPFTwPvGJT9Cf/umf8nnhcJjmz59PX/3qV+lDH/pQ2vv5t3/7N/rc5z5HLS0tZDYbheDTp0/Tli1b6Nq1ayz+nn76aXrkkUeooaGBHyMm3CAE29vbeR+vXbuWBeYDDzzA14fIg1js7e2l7du300svvUSVlZWj3GOCIAiCIAiCMHp0PUqBsErhiEolBU4+lp5OSLvmLKWmpoZaW1vp/vvvj5/ncDi4dfLgwYND3g5C0G63xwUeQKsnQNUOHDp0iNavXx8XeGDfvn182xMnTvDvqDC+8MILpOs6Pffcc7Rx40Y+H2LvYx/7mAg8QRAEQRAEYULQ9Cj5AhHq9Yappz9E7T0Bqm7opWv1PdTc4aeImt3I1FRARN4sBQIPoHKXCH6PXZaOe+65hy//2te+xjN2PT099Od//ud8Gap7sftOvd/i4mIWh7H7/vrXv06XL1+mqqoqrgDid8wQnjlzht7znvfQ448/TkuXLqUPf/jDSbN8giAIgiAIgpBtVU7TdPa3YPPEgZOi6tTdH6Ibjb10pb6Hqht76EZzH9W39lMwopLdZiY9OnFGi7lE3DVnOamlZ7Rxxs6DwPrJT34SvwzmKuvWraMf/vCH9MlPfpI++9nP8uwcTFUg6vDvoe439b4xj/fUU0/FL0OVD9W+H/3oR/S3f/u3lJ+fT1euXOF2zn/913+lj3/84+Py/AVBEARBEISZiabp1OsLU2dvkAVdjNhhKvSb4bNhpkKPnSwpTvYYmVI1jaYjUsmbpcDsBKRW7TAzF6vCfeELX+B5u9gpxrve9S6+XVNTE3V1dbFDZ0dHBy1ZsiR+36n3i4qfoiiDKnwxvvjFL3Lr6NatW+mVV15h0xWbzUZvfvOb+XdBEARBEARBSC0geAMR6vOFqd8f4ZZL/Oz1hqmrL8ju9DXNfextYTab4icUHXDCv4vznVSY5xgk8KY7UsmbpUCQQYw9//zzbJoC0Bb56quvsvEKmDdvHp+GIibYvv/975PT6WRnTrB7924WbWjfXLBgAZ+HuTvM/G3btm3Q/Vy6dIl++tOfstNmzJ0TghDgJ34XBEEQBEEQhFj7JcRdd1+QerwR0qOo0pkIBboo3WyvRIUOIg5ibrYhIm8Gg/bK6urqJLMVVORKSkrY2ATOml/60pdoxYoVfMK/3W43V+qGA5ELe/bs4Yw7iMTPfOYz9JWvfIVz8wAqcnDPfPe7382ze93d3Wyo8sEPfnCQeylWYJCz961vfYvvD9x666303e9+l1auXMntm+985zvHZf8IgiAIgiAIkweOA1UtSoqqxUUZKmqmATMUzMNFdUO24boRRadgWCGvXyFvMMLXy3PbyWY1DxoNmu2IyBsD2YaTT/TjHD9+nO6+++7475ijA+9973vpBz/4Af3Jn/wJBYNB+shHPsLtlLt27eKKG+bhhuPo0aP0V3/1VywiV69ezTNzEHQxMJv329/+lu8Xgg3umxCOMFdJF8mAiiAiF2Kg/RPXx/ZgJu+jH/3oqJ6/IAiCIAiCMDWAaAuEFApHNBZ1iCQLhVVS1ChpunGsazZB5BlDc7o2IPJwAX5GjfsAMEQp8NhZFCYiAu8mpigk7ywn25w8iC70+GJQc6Jw2q20rLxw0JtZEARBEARBEKYiEGXBkEK+oEK93hD5Q2q82mYxGyer1czHt7HqHU64Ds/PDczOoTiHy2PzdBNFJAdZ1ZOFVPJGs9MsZhZcsdWEiYD/CETgCYIgCIIgTGu47bCrmxxzSmkmgepcIKQakQNR4kodhF0gpPHMHAoWhXl2siRkLaeSYNQujBEReaPdcRasOox19wuCIAiCIAizCaWvjxp+/gsq2bGdirdvmxEthqjUNbf7qD9gzMnhP9B6Dm6rtGXsXAkBrHV2kHXu0MZ/QmaIyBMEQRAEQRCECSLU0kqeJVXkr60jPaJQ6Z5bJlzoRbp7+Ke9pDjr27IQG2ipRFMbYgua2v2k6joVFzi4xXK06H295Hvuacq7/0ERemNERJ4gCIIgCIIgTKTIq6qivGVLqeW3T1Pn/gM0987bJ3T/954+TcGmZqp4+9vIbLcP4Xqps5slfhr/NubTwhGVHTFRqUNrJkLGnXYLFXscY94upb6OzIWFFDp7mvL23n9zezSV1JYWspVXjPkxZgvi4iEIgiAIgiAIE0SorY2c8+ezuFrwyEMUbGkh342aCd3/obZ2shUXU8drB+LnaZrOoeINrf10tb6HLtf20JW6HrrW0Es1zf3U1OHjyyHwYIBis5lZ3BXl28njsuVku5SGenLfcivpAT+p7W3x84Mnj5N//8uk+/05eZzZgIg8QRAEQRAEQZgAtFCItECAbMVGtrDZZqN5d99Jna8dIC0cHndjFMzO9XR7ydfTT9Zdt1FPfTM1nr5IrV1+FnYQdO29Qa7a2awmKsizUWmhc+DkosI8Bws6l8PKbpN2m2VYI5Vs0Pr7KapEyFI6h5zrN1Lo3Bk+P1JXQ2pbK9mXraBIzfWcPNZsQNo1BUEQBEEQBGGCKmiO+fOTZvBQ1UPrZtehIzTvrjty+njBsMoOl76Awq2WEVUjva2VdKuHAp0Biq7eSr0vvkqWux8gZ0EeFec7uEo3GSiN9WSrqOR9Y1u8hELnz1H4+jUKnT5FeXvv4+v4X32JHOs2zAizmvFGKnmCIAiCIAiCMAGEWlvJWTZ/0Pklu3ZSsKGBgs3NY34MzNMhZLy5w0fX6nu5zTIYUbkyV5TnoLyIl/LLy7gyN2fxIiratpVsB18ge2s959FNFpjHs1Us5n+bzGZybthEwcMHybV5K1mKivlkcjq5qieMjFTyBEEQBEEQBGGCRF7xtm2Dzkfb5pw7bqeO/QfYDAWVKrRXovoWCmsUCCucmYysOZvVqNHEjE8SxV04opGXq3Yqt1yitTLf40p6LK2rk+xLlsZ/d6xczU6WgWNHKHK9msWVZd58FloThe738bydZc7c+Hm2ysXkse0l26Ly+Hn2pSsoUn2NbGULJmzbpisi8kYJ/ngweDpRWC3GH7YgCIIgCIIw/YjqOoXbO8g5P30GnKO8nBSdqOliNYUK51IwZAi1KEV57i0WMh6/P1OUTGRicWecQWQ2m8lqNZHDbqECz2DXzJjIs2zflXSepbiE8u57gJQb1yl45hTpPh/ZKivJuX4TmV3JInE8gOGKraIiSVji34kCD9irllDozEnSw2EyOxzGc9d1MkmK+iBENYxS4B0828zDqxNFnstGezYuzFjoffnLX6Zf/vKXdPnyZXK5XLRnzx766le/SqtWrYpf533vex/98Ic/TLrdrl276PDhw0Pe7yuvvEJ333132suOHj1KO3bs4H+n65X+53/+Z/rwhz/M/66traX3vOc9dPLkSdq2bRv96Ec/osWLjRI9ePjhh+n3f//36S1veUtGz1cQBEEQBGEq42/rIMrLp5BGpKuKkTPHeXOYnVPYudI7p5L0IyfJcdvdLNTcrrHlzqWiBwL80+x2D7oMx272Zcv5BJEXOnuKwpcukGvrdpoIkedYv3HE65lsNrItqqDI9WtkdroofOUSbEEp/5E3jPs2TjdE5I0CVPAg8OxWM7sKjTdYxcHjZVM5fPXVV+mjH/0oiy5VVekv/uIv6P7776eLFy+Sx+OJX++BBx6gf//3f4//bk+TlZIIxGJLS0vSeZ/73OfohRdeoO3bkz8EcL+4/xiFhYXxf3/qU5+iRYsW0fe+9z36y7/8S/r0pz9Nv/jFL/iyn/3sZ2SxWETgCYIgCIIwrUBmHHLkFE3nylp04DjOG4iQ70I1aRYP9dX1cFUuVoCDhMM/OY5g5XLyXz1HeXqYLPaCnG+f1tXB7pUjYc7LI8f6TeR/8Vlybtk2rkYnWl8v6T4vWeeXZXR9+/IV5Hv+Ga7yOTdvpeDh17nd0+zJG7dtnI6IyBsDEHgT1UIZUfWsrv/MM88MElzz5s2jEydO0B133HRucjgcVFaW2R9VTAQmXl9RFPr1r39NH/vYxwZ9ABQVFQ1535cuXaJvfvObtGLFCq4oQuSB3t5eFn0vvfRSxtskCIIgCIIwWaAi1+sNU1d/yGixVDVDtZmMy9BSacUcXU8HuSsqye6x8THTUBU6x/KVFLl2mVzbduZ8W9XOzqS5t+GwFBSQyeUmraOdrPMGm8XkivCFc+RYvTbjGUDMDxa+9R1kGihMKGULSGltIceyFeO2jdMRcdecJfT19fHPkpKSQe2XEH8rV66kD37wg9Te3p7V/ULgdXZ2slBLBcJvzpw5XE38l3/5F9L1m0J106ZNXP3Dec899xxt3GiU6CH2cLvKyspRPlNBEARBEISJAeYoje0+qmnuI18gwvNwcLAsKXRyHEF+Tyt52uvJ2dlE5u5Ocsybz/N1w7Vg2lespEjNDYoquR8LwjyetaQ04+tjBg7bMl5o/X0s0Owrbo4TZUJM4AHrgoWktozdlXSmISJvFoBVpE9+8pN022230fr16+PnP/jgg/Qf//EfXDX7xje+QceOHaN77rmHwlmEcaLdct++fVRRUZF0/t/8zd9w+yWE3Dve8Q5uz/zSl74Uv/zrX/86zwtWVVXRtWvX+Pf9+/fTmTNneFbv8ccfp6VLl/IMXyQSydGeEARBEARBGD2qpnP2HObnuvqCdKOpn9q6/OxiiaBwdHjFcub03h4KnTpOWm83qU2NZF1YTqaEkZmhMLvcZFu4KOfB3zB+0bq7MmrXjGFbXEVKQx1FNY3Gg/D5s0YVzzr6zjhr2QJSW1tuGtCMkSi30k6cueJ4Ie2aswBUxs6ePUsHDhxIOv/tb397/N8Qf5ipg/nJb3/7W3rzm9/MAusnP/lJ/Do+ny/p9o2NjfTss8/Sz3/+80GPiZbLGJs3b+afX/jCF+LnYx7vqaeeil8HwhJiEQYsf/u3f0v5+fl05coVnun713/9V/r4xz+ek30hCIIgCIKQCZqmU1iJxRio5A8pPF+HubuYTwLcz4sLnWkrc5G6GrIvW0HOjcZxUDagshU8dpjjDXKF3t9HZreHzUsyBYITzpuolNnKkxf0x4rW309KSwsV7LhlTPcDAxY8LwhYaxYCdrj2UcQ5uHftpumMiLwZDsQRWipRJSsvT7ahTWXBggUs8lBZi4my2KxcOjDnV1paSo899tiI23HLLbdQf38/tbW10fz5g/u6v/jFL7IxzNatW+kDH/gACz2bzcZiE5VGEXmCIAiCIOSyzTIY1igYUjgoHAN0rNOiRKquk6pGSdU0iig6aXqUL7NazJxRh6odMuuGMyNBJUiprSXP3feOavswN6cHQ+yGmc4JczSo7W1kmZO9CLJXLaVIbU3ORZ4xi7cmK9E5bDWvpXnMIi+q6xS5dsWoeq5cRZR30zRwuiEib4aCDxcIoyeeeILn7pYsWTLibbq6uqihoYHFHsCsHk5D3T9EHlorIcZG4tSpU+R0OtmMJZ0Jy09/+lO+DtA0jQ1dAH7id0EQBEEQhNGCClwgpHIMFpwuETAOgxR05aEaZxzbGNdFuyU6LvEz320jiyX76Sats4PnxiwJzuLZAAFpW2AIF0QajBW1o51C586S5657sr6traKSgqeO84xgLgQZ0ENBUpoaqGB7bqKyMJcH0UgpMQxRBMRfOEfRYJBcO5KzAdOhNjWQuaCQ21SDp06Q/fbs99dUQUTeDAXxCf/5n/9Jv/rVr7j1sbW1NR5jgNw8tF5+/vOf55gCiDrk1v35n/85G6W86U1vGvH+UV2rqamh97///YMu+81vfsOPt3v3bn6sl19+mSMc/uAP/oDdPFP/+HD+t771LcrLM6xvb731Vvrud7/LZjBo33znO9+Zs/0iCIIgCMLMBMcU/pDKDpcwaozNxvX7I9TviwwEixNX4+w2c84z6BJRUPmqGnmBfTisCxeR0tgwZpGndnWSf/8r5LntjlFVuiBWMSOI9lHXrj1DBo9DSOoBP5kcTq4+WgoKh98/FZU5E43WefMo8FpXkhCNqioFDr9O0UCAYxqcW7eRyTK89Alfu8qtsqhahi9f5Fk/Kklf8JjqiMgbA/iwmKqPg+BxcNdddyWdj+obnDCRQ3fu3DkWUYgtgNBDyPl//dd/sSjMxHAFmXlr1qwZdBkqe9/5znfY7AXumTBQQesnhGcq//Zv/8btm4888kj8PIjPd73rXRzMjpm8dLcTBEEQBEEAqM75gwp194fIF1C43dLM6XPovkSrpYlcDisVuxzjmveW1PJXX0f5Dzw05hbE4PGjfH+ZxgvEtwFB634fxx+gIuXefWvGOXTpcO3aTYFDB8n/8vPkvv1uMqcs2qOt1P/qSywG9VCI5+M8d9w9ZPRC5MZ1cm3PXUQExBtaXNGSivw8tauTgkcPkaVkDrn33sbbpra0DNtyqvm8pPf18nWwv12bt5H/9Aky3XUz83k6YYrOBPuYMYJZMVS4EDNQUFCQ0YfJwbPNHFA+UeS5bLRn48IJy+UTBEEQBEGYauCwFdnBiqKxKUq/L0z9AYXNUNB26XbauFI3mSgtzRQ+d4by7n9wzPflfea35Nq2g7PhMn78xgYKHDnIjpWW0rlcCbQtWDjmbeHWx3NnKFJ7g/LuuZ8D02MEjh4mk8NBrk1b+HdUwRCP4N452LxE6+nmymL+Y2/KqegOXTxvVN5UlfRgkJzrN5Jt6TJ+jPC1Kxwf4b7l1iFvHzx9gmczXZu3xp9v37NPU7RyGa27cxs5bOkrmFMVUQyjAEILgivmrDQR4INLBJ4gCIIgCDMNXY+yYAtHjM4li8UICo8d/6Mcga4mtGL6AmGKKFFSNMzTRTlzzuW08uzcRFTpJqpVMwbEGRuKZCjyUPULnjxGntvuHFPlLh3Yv3AKhZjzv/Yyi1hU0FABQ8xC/qNvvLndlVUUevo3FN2+a1AVElU8CM9cv15o/0Tl0r5yNbe6Jj4uqnOhs6eHrIoiIiJy/Trl7Xsw+flu30UhdXrWw0TkjRIRXIIgCIIgCKOsCEU0drX0Bw3hFlLgaBkbT0kWeUDDwbnJRHarmRx2C3ksN/PopgrsqFlzgw1FnFuMatBYgVgJnjg2KIaB2zF7e/jfiDiIEbleTeb8gpwLvEQgorTubgoePUKuW/ZQ+Oxpzroz22+2cPJMXmGREb2wqDy5lbW2hvL3ja2VNR2W/ALy3HnPkFEQ5rx8NsRJ10KqNDWSpbiYLHnJI0vmwiIyDyw+TDdE5AmCIAiCIAjjbogSjqgcJI75OWTPIcbAbBowQXFYyOaxx6+P6h3m6dgpxURcsZvKoD0QM2CYTcu7dx9nt+UCBJfrmBULhcjsdPLjhK9eJqW+li+PRhTy3HEXV/qimkrhC2fJc8f4OkLyjOPOXeR79mkKnTxOalsr5W3fxSYnjvUb40LJtngJKXU1SSJPbW4kS1FRUqvnRGGrqCSloT6tyIOrJhw1ZxIi8gRBEARBEIRxCRPvD0Souy/EDpfIm0PxDTNzTrtlyBZLnGecPbUqdUMBUep77nccfO6+fX3WJinDgftCVU5tbeaWwtDpU2RfspTbMc1FxaS1t/F8W97e+0htbWVRaCm5WdkbL9Cm6b7jLvI9/RQ5N20htaGe2zARP2BZu56vY6tcTKEzJ9nlEvOB8VbNJctoMrCVV5D/5RfIuXV70vuOK7AtzeTcaMwTzhRE5AmCIAiCIAg5AyYofb4wdfYGuB0TbZUIEJ9sQ5TxIhoKYrCQjT7GA27ZPHKIRV3evfdzG2T8svll5N65i/yvvMitkHl776eJAhU7zOFFyUS+3/6aXFu3k9JYTzQg8uDAaZ0zl1shIfhg2qL19ZF7d+WYHhdxCMGjh8lz196sIhgsiHQwW9hB01JUHD8fvyMmwuzx0ExCRJ4gCIIgCIIwLGitRIsl2ihdaK20Wrgy5wtEqNcb5hk7q9XMpilwHw+EFHYjLMp3TLnZuVyj9fTw7NZ4Ya+s4soZhFK6KqGtYjHp4TDp/f1JAnAiQFsq2jTtS5dxvlzo3BnSI+H4fB4MaJTaG+x6qfX2GGYtY8zGQ7g54iHghunecUtWt7VVVLBJTKLIQxUvF+6jU41JXVLZv38/Pfroo7Rw4UIumz755JNJlyMvbfXq1eTxeKi4uJjuvfdeOnLkSNJ1wuEwffzjH+cQb1zvscceo8bGxgl+JoIgCIIgCDMHtLBhfq6jJ0jVjb10ubaHrtb10LWGHv73jaZeqm7o5cu6+kMUUlTyBsLcmhnVo1RS4KR8t33GCzwA8ZJofpJrIIrsVUuGbQN1LF/JlbSJIHD0EPlfe4XC16/xjBvaRFHFREg6t5a2tMSva1tUwQIvGgxwlTE1Xy9bdJ+PRVnevodIbW7if2eDffESdtFE1TMGu5eWicjLKX6/nzZt2kTf/va3016+cuVKvgyh3QcOHKCqqiq6//77qaOjI36dT3ziE/TEE0/Qz372M76Oz+fjYG1Nm55OOIIgCIIgCJMh6pADjNm5zt4g1Tb30dX6Hqpt6SN/MEJWq4mrcjjh37heWFGpMM9OxfkOFnQFHuNytGbCHXO2AJdLODPOBmCyAtHGcQVtrRzX4NqxK16dQ2spzFVi4HwIMvcdd8fn8sYC8vcgaOGWicw7tLFGI5GMb28pLiFzfj4p9XX8O8xqhnLcnO5MmTB0VPIg1t74xpsZG0OFlr/wwgu0d+9eDi+fO3cu/fjHP6a3v/3tfJ3m5maqqKig3/3ud7Rv375xCUMXBEEQBEGYKRl1XjZHCcZDxSHPUIFzO61kn2YB0JOB97e/Jvettye1AM5E2GDmmafIuX4Ti7x0wF3U+/RTVPDmt+U8Bw8Oo96nnqT8R94Qdy8NnjhKUVUj967BoetDobQ0Uej0Scp74BE2tAlfvDDkLCPyGdGmvLqqZNqFoU+bCdhIJEL/9m//xmIM1T9w4sQJUhSFq3sx0Pq5fv16Onjw4CRu7ezirrvu4opqDFRc//7v/35St0kQBEEQBEp2EFQ1ji/ADF1XX5DauwN0o7mPrjf2Uo83zI6XJQUOKil0ckVOBN7IwPESEQdwlZzpIAMQlTlrecWQ10E+ntnlIq27K+ePH7l6mdstE+Mp4IiJOAk4eGaKFa2Z0Sg7k6K11DoD5/GmhfHKU089Re94xzsoEAjQggUL6Pnnn+f5O9Da2kp2u53n9RKZP38+XzYUmOPDKbGSJ+SOY8eO8XykIAiCIAgTL+ZUzRB0qMqFFY1n64IhVOlwvm7k0A1c32oxUb7HTlbLtFn3n1Lo/X0cPp7L2ISpCEQU4hC47XKECp11UTmpTY1kLTWO13OB2tVJ4WtXB4WoQ3Ra5szlipytPDPXTpPJRI7V6yh86QLP+Ln33E4zkSkv8u6++246ffo0dXZ20ne/+116/PHH2Xxl3rx5Q94GH17DvQG//OUv01//9V+P0xYLaKEVBEEQBGF8MSpzKjtZQrypuk6KAnEXJU3XWewhTRytlzaLmSMMXE7blA8Wn27OmrNhHi988TybqmQi3GyIfDh+lJwbN+fE1CZ05hTvZ9f2HWlD1JF/pzQ0ZCzyAILPOcMP2Y0z9PWb8n/lqAgtX76cbrnlFvre975HVquVf4KysjJu4+zp6Um6TXt7O1fzhuKzn/0sz9/FTg0NDTRT2yjhPIpWSlQ7sU/Q8grDm9/7vd+j/Px8WrZsGT399NPx21y8eJEeeughysvL4+u/+93vZoEdA7d9z3vew5ejsvqNb3xj0OOmtmt+85vfpA0bNvBriXnJj3zkI2yQE+MHP/gBFRUV0bPPPktr1qzh+37ggQeoJcGdSRAEQRBmG5gH6ukP8cwcKnKILICgQ6tlXUs/u1zC3bK500fd/SHyBRQWdjarifJcNiotdFJpoYuK852U57aTw24VgTcezpozfBZPaWygyI1qcm7ZltH1EciOiAM9GEg6H46W3t/9hnPuRkIPBSlw5BCHl1sXLOI8PrRqpgMOnkpzY5Jj5kjACdSxdj0LxFzPDk4VprzIS1eli7Vabtu2jWw2G7dwxoAwOH/+PO3Zs2fI+3A4HGywkniaqfzwhz/k9tajR4+y4PvDP/xDetvb3sb75+TJk2xOAyGHdljsuzvvvJM2b95Mx48fp2eeeYba2tq4ehrjM5/5DL388stskvPcc8/RK6+8wrORw2E2m+kf//Ef+XXB9rz00kv0J3/yJ0nXweN//etfZxMdRGvU19fTpz/96XHbL4IgCIIwlY5tIOjCAycIudYuP7tbXm/q5eiCK7U9dLmum8+rae5noYdMOszQQchhhq7AY2dnSxZz0n45IWg93WQex/iEyUbr7max5bnjHna0zAS0riK7T6mtSTofkQe6t59NT4Y9zr9yic1sTA475T/yRnKsXMWibNg5QE8eu2Rmg2PVGnLtzC5nbzoxqe2aqOZUV1fHf6+pqeHWzJKSEiotLaUvfvGLnHuHilFXVxd95zvf4Qw8iBQAE5b3v//99KlPfYqvj9tBGKBqhEy98eLRD/5f6ui+WYmaKOaW5NFvvvvRrG4Dk5q//Mu/jFcwv/KVr7Do++AHP8jn/Z//83/on//5n+ns2bPsSLp161b60pe+FL/997//fa6+Xb16lU1tUEX90Y9+RPfddx9fDtFWXl4+7DYkmrIsWbKE/uZv/obFJl7PGDDQ+Zd/+ReuLIKPfexj9IUvfCGr5yoIgiAIEwEqapqmk47ZtqjhUIl/88+Bf+M6mHOzW80cEh6LFOAGyoTrYF6uzxemUFjj82LXQdsl3C2RN8e/qzo/pttp47ZLYSpV8iY2gHyigFOmf/9L7FxpKclOyCIcHfEG9tVr45WySO0Ncm7dQeFL50ltbxsUW8AxHqdPktreSnn3PUiWLIow3LLZ2JB1FIJphlbxJl3koVqEmbsYn/zkJ/nne9/7Xj7gv3z5MosItAtCxO3YsYNee+01WrduXfw23/rWt7iFE9WmYDDI0Qpo/7MMo/jHCgRea8f0MGvZuHFj/N/YJ9iPEMExYm2taHFFRQ5VOrRLpnL9+nXev2iP3b37pk0thPWqVauG3QbcJ4QjWkFhcqNieDcU4tbPmEGL2+2OCzwAYY9tEgRBEISpAqps/b4IdfUFKKLAvAQiD8KOWKDx4SL+EzWEGv5rIhOLPPw0zonGhR5EHs6BaIM9u8lkiDccd9o89vjj4pZs3z7NLNxnOnowyC9WotvjTEHtaKfAwdfYoAQCKltghhKN6uyyiTk+ZNkhX8+9aw+ZHA4KnjzO+XmJIit84RwbqHgQmm7PLjQd2+jf/zK3lM5k4TZtRB5mxoaL6fvlL3854n04nU76p3/6Jz5NZEVtMhjN46KdNRG88RPPi/0h6LrOp0cffZS++tWvDrofiK5r165l/fh1dXU84/fhD3+YK3gQhQitRwUW1bvhtnOKRDgKgiAIsxSusEU0w8xE03nmDYHhDruFT/gKxfcVqnQwN0kHxJ+qJs8K8W3MxLeTA9Lpi9bbPePm8TDXFj5/liLXr5Fr1x42URkNeF/bl63gWT6IvEhDHUcXwA0TGXtwtkQguX1xFQeSh69c5vZOz737shZ4wFxYxCsrcDu14N/C1HfXnIpk2zI5XUCr5v/8z/+wcQqqo6nAAAdi7PDhw1RZaTgYwfQGrZyY5RuqWovKHQxaMJsHfv7zn4/zMxEEQRCE0YOWy46eALV0+UnV9LiIQ24czEyyEWa4reTNzWRnzZIZJfD8r77E7++8Bx8Zc4XSXrWUvE//hqJbt3PGnmON0YmH+3dt2Ub+A6+yayfm9GDW4tl7H5mdzlE9FhcxBlo2ReQZSFO3EOejH/0odXd30zvf+U42arlx4wabq/z+7/8+aZrGbZyowMF85cUXX2Qjlfe9731x8ZYOtGBC5KHSivuDsQpacQVBEARh6uXL6RQKq9TY7qWGdt+AqHNRcYGTCvMc5HJYpfImxNFn0Dwe3v/BIwfJZLVyFl4uWlBhiGIpKaXI1Suk9fUlhY5jds69+zY2Pil46zsoDy2aGRq7DIWtcjFXDlEZFETkCQnAWOX1119nQQfXzfXr19Mf//Efs8FNTMh97WtfozvuuIMNcWBuc9ttt7HL6VDAqRMRCmgBxf39x3/8B+cUCoIgCMJkgVk4zNf5AhHq7A1SLeII6rrpcm03XanvobbuABV4bCzqBGE2VPKQRWcEg9+W02B3+9LlFDx9ktsyU+/XtmAht3Lm6vGsc+fx/YVOn8rJ/U13TFEZfGIzEAgZZObN5DgFQRAEQZhtIJoADpbhgZ9wsVQ1nIzKHca/LQgLt5o5kgDtlfi3zMoJw6FHwuT91S+p4C1vz6komgzCVy9ztc1z/wOjmocbjqimUf+T/0Oeu+7JKEh9zI8XiXAWn+uWPWQrW5CTzw/M5a6uKjHMj6YRskQlCIIgCMKMA4Ku1xuirr4QH6TBlQFzdYg1wMlpx0/bIMOUqKqKwBNGROvoYAfJ6S7wIvV1FL54gfLuy73AA8i3K3jjm8lkmRjJYbLbyb37Vgocep0sDz0yLs9puiAiTxAEQRCEGQOCxLv7Q9TdF6awog5kzTkyEm5aXy9XAcz5+WRbsIhdALPN3RJmB2pbK1nnl9F0Bll1wWNHKA+GJwORVuPBRAm8GHhdbJWLyf/Cs+x+anI4ybpwEbeHziZE5AmCIAiCMOXwBxXq9Ya5pVJFzI8W5cA4VN4g1zQNOXM6izeH3UoOm5kDw3u8Yf7pcVkp3+PKznjixDFybd/Jsz1KcxMFXt9P7lvvEKEnDELtaCPX1h3Tds9gQcP/2qvkuf3OGRcDAZybt5LatpCi4TDpwQAFDh2ggje+ddpXXrNBRJ4gCIIgCFMGCLTO3gC19wT531aLkSUXK8QZEarReD4dApfRmmkEixN5XDYqSAgSzxS1uYmiwSBne+FAEAe+OAUOv075Dz7K+V6CwO8+RSG9r4+dI6fzHJ5jzdoZu4BhMpuTKnf4+1abGshWsZhmCyLyBEEQBEGYFGBqEAipFAwrLOjY9TKikjegjFqsjTYfLHjyGLl23JK00o8gaGV+GYVOnyTXjl0Tsi3C1EftbOdcN8ybTVf03l6yL15CswX7ilUUvnZVRJ4gCIIgCMJ4oKiILlCozx8hrz/CQg/9l6jUoTIHp8uSQif/e6KIXL1MloKitG58aMnz/vbXxnzewOVo7RT3zdmL2t4+rStgeP9qnPE389o0h8JWXkHB40dI83nJkpdPswGp5AmCIAiCMG5wwHhE4wod5uz6/REKRVSerXParVTsyswUZTyreKEL5yjvvgfTXo42Tdix+199CQOB7L4JY5b8hx6bVfM9wk20tlae+ZquRAN+Mtns7EQ5WzCZzZzZF6m+Rq5p/Nplg4g8QRAEQRByBlouNU3nNkyvP0z9gQiFIzrpUZ2rc04H3C6dU6YShrY1s8tNlmFyclHhK3jz40a50WIh3/PPsLvibHPrE5D7phpVsNLSaR7iPnuqeDHsy1eQ77mnyblx86xYoBGRJwiCIAhC1i2XwbBGobBKEczSDThgKopGMMGEEybaME1crbNQQZ6NLFP0oIrnq+bOHfF6icYr9iXLKHK9WkTeLETr7CRLccmExwLkEohU8yxq1YyBNk20qCqNDWSvnPkGLNP3HSoIgiAIwoTA8QJhGKSo3G6JtsuworHTpTkWa8Bul8a/bVYTuV2OCZ2rGy1aZwdZs6zI2auWUOjMSdIj4VkdtjwbUdtbp/U8Xkzk2SqraDZiX7aclBvVw4q8qKaR2tHO1fpwczPpZeVEVbfSdENEniAIgiAISaAyF5udi83ThRSjUme1mMlht1BR/vQQcSOBgznHxs1Z3QZVPXberKslx4pV47ZtwtQ0XXGsXU/TGbRromVxNmJbWE7Bo4e57TZdNVbr76PAgVfJZLWRdcECcmzcQpH86Vn1FJEnCIIgCELcJAXirr0nQP6Awq6XcLuEsHM7LGSboEiDiUIPBimqamT25GV9W9vS5VzNE5E3e2BXyq5Oss4Zub13qoDYgKgSIeeAMIW4iQYDZJ4lDpPpFmiQb6i2tpJtUXnSZZHaGxQ8cZxcW7aRfeky4zy0nUc0mo6IyBMEQRCEGYyuG62W/pBCEUXn81B/s1nNZLOZyWa1kKrq5AtGONYgGFL4PK7Uof9yBqN1tpN17txRmcBY55dRNBgira+XLIVF47J9wtRC7+8nk8udNJ85ldH6+yl09hQbBjlWr2WzEQ1GQwWFs8J4ZCgg7pTmxiSRF66+SuHLFynv3vtnzN+ziDxBEARBmMHCrqc/HA8bR1UORAf+E6UomU1m0qP4aSKH3UzF+c4ZL+5iqB0dZBllVQbCEKv9kRvXeeVfmPlo3Z1knSaumjxHe+QgOTduIaXuBqktzSxq2Bl0FjprJmJdWE7hl56n6HYj7xL7KnzxPLlvvWPGCDwgIk8QBEEQpjkQcDBCwcwc5ui8foUCA8IO83Nup40rd0MJQhSypkqkwUSidnaMSaAhd8v77O/IuX5jUnUHhg2R+lpy77glR1sqTAW07i5u9ZsORK5e5lxHxAaYLGaK3KhmkafPshD0dHBcisVMOqrwRcWkNjeRyekia+kcmkmIyBMEQRCEaQBWmyHacMK/9agRZeALKNxqaWTRcY2O7DbzsMIukdlStUvnoMcHvCUlo74Pc14eG7CEr14m57oNNysoJ49RNBQiEpE3o9C6u8lWMfWt9zWfl0Lnz1Levod48QbbHDx5nPRwmE1XpsNzmAgDFqW5iUVe+MolcqxaTTMNEXmCIAiCMIWDxX2BCHkDihFbENHYHAVCgv8XRTY3suisUzqLbqpWZcyFhWPOO3Ns2Ei+Z58mx8rVXM1T6uuILFaKKgqfpsv8ljA8UV0nraebM/KmOqGTx9loBblwcTdYzKHV1Qxk5M2clsTRYl1UTuGzp0lDdbOvd0YKXxF5giAIgjDFQMg4hF1XX4D8IZVMZCKr1UR2u4U8Fuusba/MeT7enHljvh8cSNvKy41qwNr1bHTh3rWHKyewY59pLWCzFd3bTyb31DddQaswhBzmy1JbiwOHXieTxSLZjkRknTuPAn29FDp3huwrVs1IIxoReYIgCIIwyaA6B6tuZNL1eUPUHzDCxp12CxXm2aVCNx77vLODbIuX5OS+nOs2kvfZ3/IMFKzpEZaNuR+9v49IRN6MQOvqIusUn8eLtQq7tm5nMZeIZSDAfTpUIicCk8VC1rIFpDQ2kGv7LpqJiMgTBEEQhAkGc3VwvwyEFPIGIvE2TEQZWCxmcjmtVDDDMummEtFIhNT2NnJt25mT++PZvIpKCp0+SfkPPGKcV1DIFvbCzEDrgenK1K7KKjXXudJoXVSR3g125SoowUnZtqmIfelyMucXkNnppJmIiDxBEARBmADgYglR1+uLUJ8vxMIO5imcV2c1k8du49BxYfyrHYHDr5O9aimZ3e6c3a9zwyY2cYgZuVgKCylSW5Oz+xcmF7Wri1yVVVP2ZcD8Z+jMKXLfec+QrdyxQHTBAHOKqYHoMwkReYIgCIIwTpEG4YhKmhblWINgWGPzFIgMpwNtmDM/bHwqErl8kfRQiNy33ZnT+zW73Gy+Ev+9oJD0vr6cPsZsRO3qJK2jncO8J9N0ZapHD8DwB5mPU72lVJg4ROQJgiAIQg6AqIMTZr8vzGYpEQWRBjoapTCqxVW6fI9U6yYTtaOdwpcvUt6+h8fdaAGzebrfxwJhJpo6TBTh82fZTASOpY4VKydlGzBbaXZ7prTpitrVwTNmghBDRJ4gCIIgDEMskw7tlrEcOtTf8C+YpeCErLp+f4RCiko2i5nsNotEGkyh109rb6NIXQ1XO1DBy2Wb5nDGDnBjhNCz5BfQbAb5bNj32Yo03ecjtbOT8978Lz1vzD4uWEiTkY9nKS2d8sYw9mUrJnszhCmEiDxBEARh1oNQ8UBIjYs4KDhF0ykYQpulwnl1uk43Lx8ARik4B4Ual8NKpS6nRBtMobDzyPVqCl88TyaXk+yLl5DzoccmRODFsAy0bM52kRc6cZTnEzGvmE2kRLj6KtmXLiNLYRGLc//+V8hz994Jb0nUujrJkuYxg6dPcovuRL6nhnqvo9o4ldtJhYlHRJ4gCIIwq9AQV6Dq7GaJ2TlusUQVLqJxhQ7EpJzFbGJTFMzO2axwqEtuu7O6bWSWvDrSI2HSvd4pkQmHyl2k+hqFz58hS+kc8txxd9wMZaIxHDb7yEaD3Q5nC0pTI8/VubbvpNDZ05R3971DXjextTUm0vP2PRjPNXPv3EWBV18ik8PJYdb2Jcs4qmK8UXu6yVWVHLcB51QsICAg3XPX3kld3OGA84LCQbEJwuxGRJ4gCIIws1v19CgLulBYI18QzpZhUrQoV+EAjs1QhSsucIhgGyVKbQ0fwOc/+iYyOxw0mQ6DcM7Ug0E+8J7sTDBU8tSONpotf2tqYz1Fbtwgx9p1LMrwegSPHSb3ntvZFATzkJiLxGWpBE+fIKXmBnnu3MuiXKmvJWtpKYfNx7BVLCZreSXfh++pJyl06gQVv+8DE2O6kvJewvY5Vq3mdtLIjWpyTGKrpNbdxQsagpCIiDxBEARhWsOVGxUiTuUTDFDQVokqHcQdZunUAVGH1XaH3Uxuh5WsbpO0VuawkmCyWil09hS5d9yS9jpo1zNZLWQrrxy3bfC/9grZFpaT+9Y7poTZibmggLTr12imo/m8FDx2hKLBINmXr6DA66+RdZ4h5FBxQzg8cG7cTKEzJ8mzd1/S317o4nlSm5vJuWUb+V5+gdw7b6Hw1SscS5EKbsetiSWlfB3d7yezxzO+piuePH5/J4IZQ9fOW8i+YhX5nn+GbPMX8MzgZMDtpGmEszC7EZEnCIIgTJs2S1TlogPCDiIuEFSoPxChYEijiKrx9XBsjxZKtFjGfjrtZmmtHM/XpreXXDt3U/DIQdKWr0yqeqASEjp1nNv2SNPIWrZw0AFzLggcPEDOdRs44HiqYC4sZJEwk4G49r34nLHvV65mcY3XIHzhHL/mefc9EL+ubfESCp8/x26ZtgEnyPC1qxSpvkp59z1IZpeL5+/8r77MJXZrGpMVCMrQmdOUd98+UpqauDro2rYjN5XItlZuOU500YShSeo8Hlpwo5EIV88gOp3rNnIF2bP3/klZOEIlz7FqzYQ/rjC1EZEnCIIgTFkzFF9QYfOTACp0nDmHeblogulJlGxWCznsFspzW6UyN1nuo3293ILn3LiFgsePkudeo1KDuaXg0UNkstsp/8FHuXUvfPVyzkOZo6pKus9LtqqlNJUw2x1EJjPn8pmdTpppQMAHDh0g15btbJASAyLeuWkLnxJhQbR5CwX2v0wm7BtEjJhMlHfvAyzwABYI8h54iKKh0KC/Zzxe8NDr5NywkVth7UuXUujCWa4Axiq3cPJUmxpIbW8jtaODnOs38OzeSC6eweNH+Db25SvJtXV7koDSQwHSA4G4wQqqeLbKxfHts69aTZH6WlJqb4z4WLnGeO/7yFxYNKGPK0x9ROQJgiAIUwbEEfhDCnn9N81QaEDIwQDFYTcOqnBsZXHZJEx8ChAN+Mlks3P1w7Z0GTsiBg68yhUeqHJUdxxr1hmtsus3cWubY8WqeLUEB+U4QB9LBhnML8xFxVOiRTMVS2GB0fI3A0UeMuzQymhbkrm4Rrtu/mNvwuoAh5GY7DYyWZIPR81OFxFOA8CEBVXByNXLRBYLt0jyfVUsZiEHUYd/473ke/5pdpm0cvtkPqktzcMKL7QRB08cJcfa9eTatZu8v/sNV8ViLaAQbzAV8kcUysPihcXC83iuXXvi94H3NqqJAbQLVywel0r1dHzvT6WFKNK1Qe+zmc7seraCIAjClGm9RJUuzCLOcLPEPB2EHWbqYmYoJQUOqc5Ng1ZNS1FR/GDXfcsePiDHPBWqC4nVGDgh2srLKXz5Ajk3bCaltYUCr+9n0Yd5rVFvQ1cnm3RMRWIOm5hLg/CFYJkKLqRjBY6ZkevXKO/BR7L+G2URl+njdHZw+6aluJgXDGyLyuOPh+qxyeXilk+0AftfeZHsFYvjFURUkv2vvDDs/aPd03P7XfG5QbwXUR1079xNuqJwK2nhO99DSkMdV6kda9ZSNKIMauHEa4r7CF+6kHaWEKDFE1XtXIJ20omOlJhuRK5dIaWuNql1eDYgIk8QBEGYMGGHypw/GKHu/hD5Q6qxmM9EeX7OCWHnEmE3nWD79oR8LsxU4TQUzvUbyfvMbxE6yK6EaI+DYBjTNnR3pZ3fmgqww2ZjAykN9SxGzfkFlL/vIZrulRG0TSIWIRvBNlrnVgirdC2+7Cip6xxx4H/5Ba7gORIWC8z5+SyshmqXZedMmLgkLBA4Vq+l/t88SdrqdbwYgdfLXrmYbAsXke+537GpTGKrZiLOTVvJ+8xTHEqemp0Hwel75ikqeNs7c7pwpXV3Ttn3/lQgqqk8H4q2Vs3bP6syK6W2KwiCIOQUVOJ6+kPU0umj5g7jVNfaT5fre+hqfQ/Vt/nYDbMwz06lhc6Bk4uKC5xcvZvMvCkhezCPl00IM9r70D6HCk3eA49wmDREGrdUjRKIxKlqIY/oAEQ62BdXUcEb3sL7C9EC0xnd209RXePWxPFGbW0h2xAiBm2RENG43OT2kGvHrqTPD/wb7wuI63RwG63bk9TGh0obRCXmR0PnzrChTOyx3HfczbN5tpTMvBho8eRK4JlTgy5DRRCvOxxIc4mK+ISSqfnenwpErlfz3yCEN2YmZxNSyRMEQRByMkuH9ss+b4i8AZUiqsrnxw648MNhs1C+20YWi6wvzrRKHuaZsoGNMhIPxs1migYCZBqFFT4qNTDpQMVlKmKdM5fyH3wk/jtEBwTuUMJlOqB1dvLzGm8gjvVwKKlSnApaNhFdgApc2svnzCEN+3tRedp5NmTypYKFh8iVS/xaJVbJkNlX8Ka3Djv/hvlT71NPsviKtVGimhSpuc6RGjAISq3yjRYWjTCEmYBA+OlIVNO4iofMTODf/wrPBc+WhUT5phUEQRhoP/Jdv0Hdx09Q/+UrFGhs5FYeIU0mnaKRNxChHm+IOnqCVN/aT1fqeuhGYy/1+iJkt5mopMCozuEnTsX5TnI7ReDNNLjdzevNWmClHmShEqH1dI0+CLqkdNocuCE/Di6O0xmIVMuc8c9lU9tbec5tuNfWMncuh6MPeXnp3CHbgbXu7kEh57GqXd6Dj3IramqFeCSDExgIoW0zdOJYvDqt1NWxKIYghchLBJVBzBKOppKN9xFE6nR57080kRvV/PrhNcbJZLOy4J8tiMgTBGHWo/R7qeW3T1PPyVM83xFqaaGOV1+j/gsXZ/2+ATj48AcVbr+sbuhlQXeVRV0f1bX2UWdvkCwWE5UUOqko30EOu7RczhbiQdEWy5juBzNRalfXGNrVpo/xhHVeGWltrTSd0bog8sa/RVBtbSXrQJ7eUFjnzOMD96FEErYT7ZrpLjcqeenfO2jb5Hm9AVOhbIDbKGbAMIcJwteusCMo3D4Rd5CIUlfDRkXZvif0SJhjHxyr12W9fbOnineenOtvmuDYlizjiupsQdo1BUGYtaiBAAu5vvMXqHjLZircuCG+Stt/6TKF2qb3avto0GGGoWr8E8dEqqazSUqfL0yqFiW7zUx2q5ncLgcbpQizG62vd9hWukzBgXb4yqXRbUNXJ4dsTxdYdLDLpjotLd05l83rzWoOcyzzeJiPGw6OOjCZKOr3kykvb/DldgeZnE7S+/vJUlgYPx+ij02DitM/D8xOQpSN5jWKRyocOsDbFw0Fue0zGgmT2tyUdN3IQOYeqk4jCdpU4xvMRNrKK7LevtlA5MZ1dmRNbMe1Vy0h729/TdFtO8e8MDUdmH6fLoIgCGNE6e+n7iNHKVDfQHkrVlD5W95MtoL8pOvYS0pmTSUPoePegEKBkEI9l65QuK2drJu3o6hJelQns9lMHqeV7LaZ/6UoGGh9fRR4/VWONbAuqhiyHSwxPmEs4EAsZr6SbesZLOQRxj1dgGhA6xjPtc0vo+kGt8ei9W2cc9m44qVrGbUCo3UULaT2NCIPoFWSq4+JIg+i0GozAuuHrBAPbuXMFLSZYvHC/8pLLFTxvkbVW0uo5Ol+Pz9Pz5338BwfZuwyyYuMXL7ImYDu27fSdAZVNSzQ5Pq9hDby8IVz5L79zqTzzS43vyZqc+OEmAZNNtKuKQjCrKPr0GGyeDy0+N3/i+becdsggQfsJcUU6ekZk+PfVAXPCcIORilwvuR5uqZeausOkNLSQqbaanJ0tbL7pTFP5xCBN8tQ6mu4ihE6e5rnhWD/ng69tycnFR3MPsXMV7JBDwWNitgoDFsmEwgAzJtN23m8CXAyVdtaWQRnIvqtZWWkNDYMP5eXMos1lOlK/PKurjE/TzYYMpvIvnTZzUgH/02Rh1B1W0Ulxzugiofg9ZHA/GHo0gXy3HrHtA5Ah7gNHDzA7pfjIR4tRUVp8yjxWozHY05FJvXdsX//fnr00Udp4cKF/Ef85JNPxi9TFIX+9E//lDZs2EAej4ev8573vIeam5uT7iMcDtPHP/5xmjNnDl/vscceo8bGxkl4NoIgTAe0UIiCjU1UsmM7mYcJpTXbbGRxu0n1Jg/JTwcg4BAqjhiDrr4gtfcEeJ6usc1LNc19dKWumy7XGnN1zZ1+dr6MGaRYA17y7LyFIiePEkXCMtA/S8EskWPdBo444Hyw558hpSX5+xeg3S1XbXtsdd+d3VweH4hPI9OVGJb5ZaQmtIOHLp6n0PmzNB3A/Bss6ccbta0l4/ZF++IlpLY2c3VryEpeZ+dgkZfGdCW2EMYis3RszxNunPlvfGs8S9DkcBpusJoab9VEBh8/h6XLSRlBfOD5IafPfcutRpvqNEZpaWIjmvD5MzmNFOEq3vmz5BgikB6iGkY8qKLOdCZV5Pn9ftq0aRN9+9vfHnRZIBCgkydP0uc+9zn++ctf/pKuXr3KIi6RT3ziE/TEE0/Qz372Mzpw4AD5fD565JFHSNO0CXwmgiBMF7xXr5FnSRWLuJFAy2akq5umSzYdDFBQkYOAu9bQQ9eb+qi2uZ8aWr0s5tp7A/HZOpvVRAV5Ns6og+slDpLZKbGvl+zLlpFt6XIe6hdmH5rPS9Fw2BBPZjM5Vq0hz513U/Dw6zznkmTfHg7nrIrGbVTZijzY1E/RfLzhYNGB9lRNo0hdDYUvnielvo6mA5iBHO/4hLjIyrCdFS2OmE1ThjDVMBcVsasl5glT207TwYsNJhpyXi8bUnP70LKJKlasVdMybz5fBkGr+31DVs2xTwIHX2NTFyy8THfUpkZuY8XzHu08bjqUmhtkLigc8nMB7dL2qqUUuX6NZjqTOpP34IMP8ikdhYWF9Pzzzyed90//9E+0c+dOqq+vp8rKSurr66Pvfe979OMf/5juvfdevs5PfvITqqiooBdeeIH27ds3Ic9DEITpg/fyFZpz256MrusoLaFwVxeLwqlasQtFNBZuPf1hCkZUNkVx2C2jMkbBQRAChfEl6Fy/kXzP/jZppVmYPVU8W3ll0sEpDuo9e/eR/+UXuMJicrq4ImEuLMpZFY3NVy5nNweLFjyET083YNEPw5rI1csUuniB8u7dR77nns54Jmsi4aD5gWpprPqRq5y34cLWyWJhQZQpCLsOHjlE9lVrBr0nsVjBc5BYFBgQVcM5a6KdD5W18agQo2WTRV5/H1eVYi2X+Blzf3Rt2jLoduFLF4g0lZxDVKimE6hkou3Ufesd/Bp4n/kt2ZeviFc8R32/cMe+cJbcu28b9nr2FSvJ/+Jz5Fi/cVq3vI7EtHpmEHX4gysaGPI+ceIEt3Xef//98eugrXP9+vV08ODBIe8HLZ79/f1JJ0EQZj7hzi7SFYWcCzJsASotpcgobd3Hs2KHbLrqxt54y2VbV4AjDOYUOqkwz0FOu3VUzpdsojGwcg3nMdeOWyh06oTkBc5wUisHLPIqKgddz1JQQHn7HuSDMswQ4WA13cHoaMH94sA70zlYzONxVSnDlr6pBsRG8NQJcu++lVteYyHpUwm8F3zP/DaeLcaCb0JaNduyNqWJbddQOWiGuY/RmaGHQvy5ZnK50goQzMrZlyyl8QDB7VhQS7eABmEZqb5K4eqr8b8DVHsxg4dwdoiimSBKjHy/Ul7QgJC3L1nGRiljvt+WZl6AQBto4mJorzdMLZ1+au3y8wiDz+ykiN1NPdU1fBmPNXRjrMFPDW1eqmvp5+/YSzVddK66ky7VdvH9TDemjbtmKBSiP/uzP6N3vetdVFBgOC21traS3W6n4pRy+vz58/myofjyl79Mf/3Xfz3u2ywIwtTCe/ky5a9amfHqrL20hCJHuqeASYpOEVWnfl+Y4wyCYdWIMrBZyO10kNmcm9VmHSvbCfNV+KKEGx3aWqZjtUTIrDUTrn6eu/ZyC5geDHIVJdZClgpW2tG+OR5AOGJxIRrwkymDCo5SW8MOeaiKTUdwQI8D3VjrHf7etM52si1YSFMBVO0CRw8bwgP2/rx9E2O6wo8zL7uwdXyuoxqEz6vEg/wY5vxCbkfn+x/4rEv3XQADF1Su4cQ4Hpg9+VxRTGzVTFpIue8BChw+yO279mXL2fzIUlBEnvseGLdtmmgQI2FNaDnF/C8+h1CFxRxjNmiazh0teCl1XqBazN+Rvd4wdfQG+XsTi6O4Hl8J+9lsIqephFyHTlL3WhtHBqWCq2KxVFV1ipqIIopONLZC44QzLT4ZUa17xzveQbqu03e+850Rrz+SBfNnP/tZ+uQnPxn/HZU8tHgKgjBzwWqo91o1VbztLRnfxlZQQCrmJlSVzBN4IBkLH+/1RcgXCJOiGkIPcQYuh5Xn6MajjQgmGmh5SsS5aQu7K8KRbDpmegnDA0GHykLg0Ot8cIlWTNvC8kmrFkD0QFAg4Hm4tkX8jeB6qDZPV3BAj1P897lzKXzxAk0FUOXC/JdjzToKXzxHSl0tzzlhTg75b+MNt+GuzT7kG+2Ood88kbbtFZVnpbF+5FbNG9cHfQ7mEvy9hc6dTmrVTMRSUMjtu5hTi1y5TK7tu6aM8M8VCH9HbEQMs8PB77XQmVPsGhoDwiwQVo3vPz1KGk6azvmtWPj0BRT+rjSqbFGad+YKBbffSf6LrSz0rBYzf2cW5Tv437HPDs6B9Sym/KbLZLKpZHLnDfmdGggq1BeI0HTEOh0E3uOPP041NTX00ksvxat4oKysjCKRCPX09CRV89rb22nPnqFnbhx4MznS56IIgjAz6b98mRxzSsk6RI5SOnhGoqiIlJ5ecswdn9VrfGmFI8aXGL648DucMb3+CH8RYb7OZjOTy2khyzgfeKdzSsQci2XufApfvULONdkfdAlTG93rI9uCRUae1/6XyOxw8sHWZIEVfbRt9T/531wxcm3fyQe9qXBbp6ZNSOvgRGGdM48C3fuNNsJR/q1jbgvVobGa0YQvnCWyWrnFETl19hWruI3QZDIPKY5yBRwko+EQi8psgViwli3kKAJHilCz5BcYs36xkPUVKwc/diDAVTbbHXeP4RmMsI35+SyWh5sbw+vPn7cz8DOX28P1aPz1hWjzh1Tyzakk7eQZai+pJdVTyOdBwEVQhYMpWEqxDZLMajU6WjwuG5n7eylqtZHfbCeb1UwFHnda4YbzMN5AFjOpi6rI2VJPkeUzbz9PeZEXE3jXrl2jl19+mUpLkz9Ytm3bRjabjQ1acD3Q0tJC58+fp7/7u7+bpK0WBGEqgQPBriNHyX+jhsoefCDr28fMV3Il8iDm0DoCYYcVQm/sS0yLEv6Hry6rxcRfWviimijYKTESSeuU6Ny4iXzPP0uO5SunnCmEMDYwG4SDTrSF4eCW2/IynFkdDyBOrHfczXNRoXNnKHzpIrl37R50PVjNY45nukUnDIcxn+QhHbOxowjh1rz9POPnWLV6TCKPLegvXaT8R9/Erp+oOKHiGL5ymfL23vRAGC9iLaGjfW1tFRXc6pgq8vDZFg2FuEqo9/eTddHgDq5I7Q2yVS7mtuHxwmSzk9bXS5ay7GYOpwtYnFQ0nRQWZ+isw7kmXsRENqvv/AVSdQ+FLrbxZfhORLslLncXVJLr0BHqWX8LWSyGgMvz2LgKN9Kcub2vnWjBIp5LzxRl4WJynzxAkWVr462cM4lJFXmIO6iuvpkJgmrd6dOnqaSkhA1U3vrWt3J8wlNPPcWRCLE5O1yOWTw4cL7//e+nT33qUywAcf6nP/1pztaLuW0KgjC7QCuG5g+QFgyQ6g9Qz8lTZHE5qfxtbyHLKCr4Y41RwPaEIxr5QwpX6IyVSaP1EgcxjoFVyFgryWTBVbwhnBJRSbGVl7NrmWvzNpoSr3F7G1caMKNiwizXDDAjmCyRZx2YfXJu28FtalOhLRfb4Fi1lry/+zVFt+9MOujm2IH6Wsp/8FGaaVjmziO1s31UIg+zW/aqJaS2tKS9DAY1MWfJ4YCZDaos+LtCe6Pnrnt5ISBw5DBb/GfjeDlakTeWiAbrvDIKHj86aHQH/4bQC54+ydb9qZ8Z/L66eoU841jFA2pHG5kxd4Y4B+vUXjSD8AqFVWPfmfDZa5xnnKK8j2Pn4XoQcWiT5K4ULUr6gHkMbouKnSkQoDl1tRRevJIiEZWXNTFTXuDBoqaFqHgluQ83kEP3klY88ns1EUtnK4VXbszqNlFPPukOF1l6OkgryW4GdDowqZ/kx48fp7vvvvnHFJuTe+9730uf//zn6de//jX/vnnz5qTboap311138b+/9a1vkdVq5UpeMBikvXv30g9+8AOyjOMqjCAIUxPV56f2l19mF01bfj6HmeevWE4F69eNelUYDpuBhoaMrouKHE7hgZ/4ssMKJVYqccKwN9ovkU833q2XoxF5w2VCOTdtJd/Tv+Gh9snMJcN2wgyCD5Bg0hEMovTAVYfpasAx6ZW8AaMDtpkfhbgYL8wuF7cPwzEPGWiJ8zxoIx5vG//JAIYhSmMjOVauzup2aF/Fwgf+Dvp/9T+kBwNxkw5U6RFNAUOSvH0Pj7jfYG0PMcgmJch1KygwTE2qqihSc4PjVcZ/Hm/9mN43JoeDtz+1/RxVNLWpgfLu2jvodogusBQVjfvfgNLQYOxfv3/MkQFjziKE8MJrPGDeBaGG762YcUmfL0KRAVfJ2FcoxBtO3C+ZAC5H94l1oAJncZrIgvxVInKfO0K27naKOlykFxWRadEicqY7TjeZKbx8PTmqL1CgNHORZ0J7b9BPemH2uYbqwkqyNteJyMs1EGrDWSVnYqPsdDo5Pw8nQRBmL77q69Tx2gEq2ryJFjz8UM4qO+ywOUIlD0Pf7d1Bdr7EF2JsBRmiDl94Tgg7j52mMlpPTzw+YSjnQ8xHBQ4doPwHH5nwag/nH509xaYIrs1b2WAhJtz9r77MFQcE3ApZ7FMYEPh8cZE3FbFVLaEIXDQHRB4brly5RPYsRdB0mssLnT6Z9e1gWOFYv4kXOqzzF/DMGdpZgdLSzKICkQT+115hU4/h2hF5Xm3VGnaZTMxLhMsm/tYwk8ezeuMgUPB3nouwdTxXzL2lijwsEmFfpC4IcYvqhXPkvu1OGk/wOGprMztL6l4v0UQ4leo6ef0KfzepAwuOvmCEfEGVWypZyJtufpeFFZ3/zmLfXXkumyHoBg7JIQjx3ZbxwqmqkK2ng/x3PUJkHrkAo80pI9Pl02QK+inqcJK95orRTjlCFU+FKDRl/72vzC8nT/UFCqvKlK+sZossewqCMO3x19by3N3CRx9hc5VcYnW7jfbAYJCFDqp0aFPBlyJmBFCpa+3ykTegcNtlUQ4jDcbbOl/v6YnnobGz5gi5UKjiRerqeFZqIts22env0AGKhsOU//Ab2FwhEfvSpRS+dlVEXrb7NRziltepXAHF+5OzGgfcEpXaGxTVNZ6bmonwTCzEN6o8aeZj06G0tpDW30fugTZDzFQmibz6Wt5fWBhBzl3o5HFy7dg1vMiaN59C58+Sa8vNv3NUT+2rVnNVEJVDk9VGzi3bDJfIHM0zofqGdtCxzv5C6OK9khj3AVMVGK/gcyxdFQ8tquPdpcDCs7CIrEXFXEUfLZjphhEJdjv2PGbf8L2kpVTm+nxGPpw3YBh5sU6LognCRHZU3axmPi86cBmcKAs8lpx+h1m8faTnF2Uk8BjEFpTMI0t3B+l5BWS/cYnU+eX876GwdraQWjZKl3ybnat41vYmUhdW0Uxi6n6yC4IgZEiwsYkK163NucDjeToMj3sKqO7CDYrMKeNVTrS4QOCZzCbS0IZpMVFJoXNUAeSTRej0Ka5+uXfuJtuSpaSncdZMBw4Ofb/79YS1bbLAe30/pvnZcjtdBcK6sJxbOHEQNxNb+MYLVBKmchUPmO0Oss6dzy2a1oULucrluXPvjDJcSYQ7AAbm8uyeJcOGScNJE6INt3Ht3B3vXrCVLaDw2dNGN5Sucbura+ctfD33rj3kRds1WjLTZMlBvOE9EVVVFiGp7qUxx0de+OrsoOCxw6TUXCfXtp1EVgsbmxDafkfhjMnPC6YrOXBMxZxp8OihpLk8iFNUI6PBwOAq3vmz417FAypXRyvIZHfwazxSBc4fVPmnsaFEgZBKnX1B6vNHDBdWSDzkww3EAsTy3gyRZuKqHapxRXkOFnSTgbm/h7SCoqxuo5XMJWtnK2m6xu8nCLDIUCIPLrs9nRRatz3pbEtXG1m62imycsOIj6csqCR73TUReYIgCFONUGsbzbk9N7lG+JLEPAKMUrAKii/V8NzFpO9/jbOK3Ju3UF5+Pjth4rvX7MSq6NSar8ukiocV5fwHHib/yy+QHgryDEsmq+eoojm3bmdjg7z7HxzXg20WeAde5ZVd9+13DdmCC+GHSgWc8ZxjmOWZbfBBfBaRIpOFraqKlJobbFiBxYWpNDc4HqBVUevoIFo8tMhDlQ1GKsgJTF3YYGMUq5X0/j4j2L50DotlgL9xCB2loT6tyMPnAlodleZG/rwb8m/OZOLb5z3wCIsnCEdUhHkWzuejgre+Y1Qt82y6Mm/srpNoJcWJF6+KS3hGEc6xnrvvo8BrryRdF+8txESMx6KVkTd4gBcETU4XRerrKO/+B3iWWK+9Mej6mOXu9YWp3xdhMYfvH2REx0Cet81q4oqbxWbj7yFoeW6hTKjgGVU7o+VyshdELP09pM7NzrFXK55LjqvnKGoykbJoCVnbGimy9GZV1hQJk63WaON0oLUTXQkJrZqmUICcF04QaSpFlq0hGmG8QJszn8wXTxotoq7MKujTgel1ZCIIgpCCrigU6ekZUxUPX4hwvWzr8tPV+m66Wt9DdS39/AWLVdA5K5fSvLe8ldwlhRR+4WnS2lvZOAVD5tNN4IHIpYucEYXKneee+4xsraLMD5xtlWhpibJN+XiHMbPAu/WOEQ8Y0Zqm3Lie0Sy3YMDzePlTu5IHbIsq2AwEVRDnxmQjtpkIRJna3TXk5dxSCXOSFauGrFxDAKJlE3+jqe2JqCQpTenNpFjkzZtPKkRgudHKPRyc57Z2PRW+7Z1U8Ka3Uf5Dj5HZ7WGBOepK3tzcZB/G5vJA+Pw5si9fwQsEaFNGREd81vf8WXJu2ETjAaqoiLawLCgnzeYgfflq8pkc5I3aKNDVw3PcHT0Bauny05Xabjp6sY1OX+2gmuY+rsLle2xUWuiKn8pK3fzT7bSxiZfTbmXBB5dmVOog8nDCv+FWOdkCDyC/TivIzhAl6nRx5p21s42U8qVk0jQy+W+2t9pqr5KtqY7yn/oPcp49QlrxHHKdPmgYckV1cp47RpHFK0gvLOW2z0QgEK1tTURKQsC5yUzKggqyNY/fd9pkIO2agiBMa8LtHeSYMyfrXCPMLnDFLqhQry9CwbDCbZicy+NOE2lgs5Fzw2aylM6l4PEjZH3osWlp24+g4UhdDeU/8gb+HfMhefc+QFEMnWcIt4dt3cEijFuPcuxmDKEWPPw6f2EPV8FLPTCORnWeM5zplZ6cxieUTV4mXqagQgSjFWtpKZnsU9vAKBdw5am3Z8hQ9HjEwTCVd27ZvHaVsw9ReU/EXFjEJSGEUiP/Lkk8drST4nZztd+9YOHotr+0lB83k/bvwSHoYa6q5QJrWRkbNfEsMSI3HnmjMa+Wn8+tytg+zO3h93RVzRG3NxqNd3twq+RAyyScJ5ETh+8T66lTZGu4Qe1lm0ilYp6b0y+28tzl/NYeaj3fxFUmIyGVyO2y0dwi17SY684IzNIq4VFVx7SiErJfv0S6J4+UeYvI1tZEkaWrjSpew3VjINFs4hiE4OY95LhxiVwnD5BeUERRm52UyuU8B2jtaCEtoZJobW0gW301OVSF5/wiVavY7EWpWE7uoy9RpGIZ0UDlGzhaaskVQTV1FU03ROQJgjCtCbW2knNBZu09GE4PDrRhoiUGpikQFBB2WBnNJHzctnBR3I4cK+nTjci1K1yJS3TGsxRmPz+DgyK47EWuXiYH5nRySOj4UdKDQfLclX4GLx1s8b5kGYWrr/BrhHY0hLtjjk8YZiZv+cppsXtcm7bQbAGiNlYNSyeUMI+HKtVwWFDFeu1Vnk1LdcHE34ptUTlHCVgKbv7tqu2txn3Pm0/5+x4etfkJVyK7OrktNMlVsrmJXSWHWrTBbB87X+ao+mSZV0bqkUNcqeOq54BhE0Qkv/cLCo221+27+Tshlu2G74k4AwZb0FyxrDj8RNQAqm+IGUi6PrdODsxWairNa6wnxWQmi6+PzMWl5Hai2oa7NXEG63ybQtGi3IjaqYjF22uYroziNY3ancY/TGZS5y8i56WTLPIg0KLuPNJcHrJaLOyO6Tp3lIJbbyP79Yss4gK77jEMXOaWkbvmMoXR4TGwDZa+boosX0fqvIVkbW/mtlDENURdbp7Nc+D6q25Wdu3tTaTPmZ5GTyLyBEGY9iKvYN3aEat2EHWdvUEOa9WuXCSbKUqFW7eMKq/OtXU7+V56nt0cx+oCN5GgRSl89TJX7nIBZ+c9/wzZli2Pz/yMFbTlYR4IbV/ZxjTAHdT7219zGyK7Mp48PmQ1RCAj2HqKG6/MVoxqWHd6kdfWSo7Vw3/m4e8R95HOSRJYyysofOFs0gKN7/lnyYq5YxgcjUFo8eJPzfXkbW5pJv+BV8mSl8+VRSzEJF0O18+L5yl/30Ojfly0N94M6kYMAJFqtlOo+jqZ7nuU+roDRqVNt5Je30pqUzeFfVHqaNNIaWpLFmsxBkpsCQkCrBUwJme3makgz052hHinAdlrtsICzm/LV3ykOJJz36L5hWTxe0ktyq1h2FSCTVcg8kYB5vFMaKtFh0ZBMZkUhcx93WRrquH7tAT8PK8XWbKaPK8+xa8QxBvP4A3M50WdboraHbwdemHJzW1avo6rfHDthDA0+wwHUL6vg89RpHK5UX1UFbL6+ii8bHq+RiLyBEGYtqAKB9OVeXuTqzX4Ike7DL70YTXd2RsibyDMMwqFHjsFetooGgqSedvWUT0uDrwwJxS6cI4z26YDeiRMwUOvcxtXYovWWMD9ICAZ8y4Qvjl5PU+f5Lmr0YhnGE4UvO2dN930rlxiwZer5zuTgOBHpdPkHFgtF6YUEEpoeaSlRgxC6jxeJu2FntvuIpMrfZYdKmaB13v5cwGCEM60aAP13PX2MVfSkLep9/VRFHNUA5V4VPFQjUWrKBZf0A2Bzwx8lmIbYLAE5082jUkB2W1sgBVBhptxwhZiHho/cRly33A5nChhThIzK/FEC0j3lJC/pjd+f+6+KNm8reTwdlNg9VYWaYi/yTT7LTb3O9J1ba31pHvy43NpqaBVEBU+YzpwZsKmK/MWje62AR8LLzMiGAqKSZm/iFxnDvP9WZtrWaRFqlbijcBizhTwc+tmalaeOncBt2xGIPIiYTKpCl+fQbWvrIKsrY0UgRi12XmWD2HsoQ07ydrdTgpul2n8wxRDRJ4gCNMWpaeXLG43t+GgfSbmiukLRCiiIDdIJz2qcxtmcb6T5xyQt4V5F3aB6+vNem4kBoSIF1ECi8pHNc8xkWCVHAdRaNN05rjtDQHM3qeeJPvKVbxKPxZwIBhVImQbQ6h54oEXtofdBUXkDSIWgj4VjBmEIaphdbXp5/EKh5/HizFczh7EFxuTNDdxR0LwxFG+TywCjRVU4PHegtCLzccqLU3cOs0ZcWULKHK9mrshLAsWkR4IUnRBBQWL5lJ/XzBekYPTJKIC/AHFiLIZqLTF3rGstUzE4gyf8TAbsdusZEaEA/dXEpmKNvJ18xI/F0xzyNlyhfSiEjItyv75ZvI3Y8IiorePDUHUBZUcAZBO5CHfbSZj9vaStmJox2NrUy2pCyrSiigIRKWs3MjLKyhmcWdvuEHqnPk8k6cgF28gvJwFs7eP1AFRnSrynBdPcJUP98kmMAmvIe7Hfep1vhznYzbP8dKT5IqEiIIBCuaVkCWUHLsxXRCRJwjCtASrqf0NTaQWFLMTGcLIseKLL2A4jTnsZvLgYCNlgJ3DaOfMNYbuGxtGLfLMLhevPCOkGwc0EE8TkRs3mvZH//5XyL1rN5uk5BoIbLR8cX7ZGHKm2OUOVbwt23ImPHj2ZgyBwzOZ6ZCRN5sZynzFcL8ce8QAQDeC0tjIgi905hR57ro3Z+3nsbk8iDyYuOiqSorTQ719Qer3R6hLLSSlahc56qu5YtNbtJLUi203A7tBFH5XZv48L8p3DDbDyuQ51l0jHfNbCcYbustNtrZG6t9yK40XsPyHKOH5ryXGHBk7P1pvHnbreYVk9vbTjEWJkEmJ3KyapaIqhvgK+CiSIgRhroL4A7RT2hpvkFK1ktst/Xvu46pb1GzhmIUYen4ht1wSlQ96GD2/iEzoWgj6WeTF2jZjoPoHJ0+0guponUU2n66Ttb2FrE01ZMkvpoCiEdH0mwsWkScIwrQhFDGqdcGQyqLOe+EGaSVzyOGPsI10gWdk5z21rYVXklF9Q9abc72x0jsaIJpgJICcpcD+l1noJZoNTAVCZ0+Ta9uOcRF4MRyrVpP3N79iQTnaqqZSW8Otg5gJyhUs8voGt0kJhrPmdIhPmK0MZb4CY5SR5vEyxbqonJ2CIRzNJaVkT2kNHQpU2VBtQ6UN5lVolezzhinCi2xGFc3uM5O1+QaFtEIy118n0vKo/2Ib3wZCzmG3ktVipdDSNVxUcSHnzWLcNleLPBAKjusXuc0vUeRZOttYJMCFcbywtdRTaNUmsrYYLZssQmBCUnxzIRCzYlCy2E7j3zPQdAX7eIjX0+zrZ6Fra20gdU5Z0r4xAtSLuRIKIYjcO56vg9lKVyvP6iEwPYaG+2mqSb8hJtNAy2YrCzkF7pkpoJqH7QgXlbIRS2TJKq7AOs8fo55lm8kfhsibfojIEwRhSoODiZgbJuIOYtU6tOeYe7uocMsmsuZl/gWptraSe/dtZC4q4jkU3e8ftq1pJLDKbl+2nLOdYFyANqvRVgdzDUSXHvBzUPh4gvYs5+atFDx1gvLue2BUB2nhi+fJtWt3TtsHIWKGygOb7XAlbxSuqsLEkRpFkM08XqZVePvyleyuG750kTP3EAMAIYb5toiqx6MBVFXnz18YV0HYaZrGcQCxFkpU2yDQomQ4FofteZTffY0X44raW9m1MBZPMxqzq9GAKp5aPIeraXF0jRy1V7hChDZAXMaCbPXmJCE4JlTVCNV2OPmEVkQIFq4iJQiZeDXP158kWGYEUZ0sna2k5Q/9XWjB8y4qZZdL54XjFLhlb7z9Ml5xs9pIWbDYmJFbv4Ore2a0b6L6llAhhIhGu+ZQqGXlnKdn6e2i8IoNaS93H3mZxaalt5O3xV5XTeqcBWzyoloSMvWmESLyBEGYMuDgwDioMDKIvP4IB8X6QgrZLGZyOqyU77axENBDIfJqClkKMj9Q1UNB0oMBMhcXG0JxIBTYsXL1mLcd2+HavoP8r71K+Q+M3n48l4TOnSHnug0T4i5pW1zFZgoIJIfozQYYgOB1QRttLjHysGZwO9QYnTVRyRGmuPlKF8xXlifM4xWN+bMFbsMQcFgwiyxdR+FL50lxFFJtdQfPNEcUo1IXd5scmH1D6zsW1/BZbHdYh6+8ueeQ+xLy0czkCfaSf8Eustkm8JATWWrNtRTYtZfch18wgq9tdrI11hpujxYLV4iUhVUsQK0dzTkTeeagjy3+zX4v6R7D9EnPL+b5u9Q0UsySoc1wpog8+41LHDQOkWvtbKXA9juGvC47WuYVkFY6n3PqHFfOUnj1Zn5tzH09pFQsJVMwQPb6axS1GO2UaP9Ee7FamuJU6nCxAIy9zsaZOs9GovqH1s7gxl3k2f80uU/s5xZaztGL3d7p5jw+CMEQ2ngtVhapqW2k0w0ReYIwBfDX1ZPq9VLh+tzmjU0HUYfV4WBY4zDyQEjhAwsYo2GoPqyqvEJcUuDkrKJEOHMpy0wlVPFgNhC7DUQehEkuRB6wL17C7VTBY4fJtfu2STW1wDwMBM5YTEyyAc/Vvec28r3wLFdGswnZNmZ3SnO+v9DuFg0GJUYhDZrM5E078xWlvo6s85MPbkcCi2W+gEI9/SHq9oa4QseB3Gi5RC6cHqXSyzfIv2Apab2hAfMSw21yNDNwccxmFjiYp2KHydiB9wRhr69m18So08Utg2y4UVhK9tornKemowqEeW2zhYWE+9grSVlqY8EQd/nxn0ArLGYBlAraDJMqjdMYCCoYogS33UG6y0UFv/4JmdLFUgxg9vdz/h0IL19PzvNHybP/t1zBg6ALr9tGlp6OeKXOcfUsaYUlFDUlz+MZD27iqijcSmOXWZvryXHtHPlv3cfvP3M4RMrS1fxYCD2HwE+ckYThChvFoNoK19mAlwPZKTQ9WzWBiDxBmESU/n7qfO11Urxe0gIB8lRVkTVv9K2D0wWsIOPAo6s/RP2+CDtgYpUYVtbGarHxoe1xDxZ3gNuBLl8g19Ydwz4O2psINt4DK9+xebwY1rIyChx8LW4jnguwTXDdRJvVeBmxZLK9YQQAr52YKl5iNdNz+11s9JK39754m5keDpPJbh9SxKFCkesqHsBzN6EFze8jS77EKCT+/UQDY2tTFibWfCVUc4P8tXUUvW0vKf4wL35BhMF1Eu2T+ImWSog2/FR1o9USM8z4jMW8nH3gNuxGabeQx2ojS1SnPMVH1sXlSQe8uQDiyl53jQ+eJxQ49DbVcBUPaAUlZOnrMdwuS+Zy9SgRhGCz8cZAVtpYYXHnNkQeCwZ+DA+ZlHBypWmgkmdrGuyiOh2xttZzGyw7XaJVE2It5E9/5WjUaFPNG+jEsVoptHkPt7qiXdKMOATk26Git2gJh5brTifZMd+pa2krn8bc402RZ2trYNMdx7XzFF671Zjzg0h0uUkrmUe2lrqk+TwWnAOi09rVztcx4hhE5AmCkCWRnh5qevLXVLx1C5WtX0ddR45S3/nzVHrLrqzup+/ceTLb7ZS/auWUfA1woGEciKgcc8AzHWGN5z4sFhPPaKAFKBsQrIsPX8v84V3m2FSguYk8d9zDLm9qaws51qxPmiWD0YdSV0eOFbnZf2glQdCvBhOScRB5Wl8vV8sK3vz40KKpv49FpnsMbpejBfNCrh27yP/KS7xv1fZWnv/y3H7nkMHMEHn2ZSvGZXsg7vD4IvJugr8Jc1FxPMNMmHy8gQj1esMswPF3jT9tVNyiipkaXj5C5muXqGPdbgrX9JPF4uXPTLRLqqrGrZUcC8eJ3VFeKOPPhoF4AY/bRiW29HmIlq4OQ9jkWOABzKGhsqOWTmDEjKaR89IpFhuo4vFZhSVcUUTweHDT7rQ3gy0/DFlyJfLYFRLtmeVLblaaEOCNShHEwwCodqKixa0rUyTOBI6SUXd+XKBmRDRKtqY6Cm0wFl4x54i8OZiZpIPdMyGgUiu8Viu3bsZklaW/myLL1pEpHBwQzj6KevLSGtUYVdGu+P2b+3u5iuc5/CIpvV1cMQ0PZPZFKpYZ7brlS9Pud9yPVjT13LKzRSp5gjBJBBoaKW/ZMiraZLg7Fm3cQA2/+B8q3raVzBnOXPiqr1PXocPkWrRwSok8HKhgBRmGKTiFFJ0PRmKGKTi5XY60VbpMCF+6QI6164Zt70PbpNrcTM7N28j38gvkWLPWOABKcRTEzJrvpRc4wNeao2oSqlJKQz2Nh18anlc0FCKtp5usJaVpr4PLLGhlnaSDePuA0Us0HGInQFRQlZbmtCIP7xW1s5NcO9MffOVkLk9iFJIIXzhHzrWzqzV8IogZlKCaxtXShE41/j0+d4wxN+NyzMe19wSpozfArZSpn2nFJjd5Lpwm3+ZbyTOnmAotZr5/npnT9AFXYaMDYjTAfEQbJxHGVRO7c1xdLBOBAyPCsrW8giRzDbT/WU+8xhUeVHvSbuucMm6nVJasGvN2oM0PFaTEdk1+jIJio5qUIPIgaqI2u+EeiWqf30v2xhsUXrWJckZs/iGD7wPM0jmvGG2RwW23Z/wQaK8ki9kQyapClq42dhe1N1wfVL1MnMcbFnT4oDKH+6xYxuYrkaVryBwOpn+a+YW87wDmAtW5C/lxw6s2kvPyaX5uMRGP9wSqt5budp4JHPR8fGglnf4zyyLyBGGSCLW2kWdJ1c0/xrw8cleUU/+lyyz4RiLY0kodrx2gBQ8/RK3PPR9fAZ4scOARRrUuolGfN8QhtrDaRluR22EhWwbxBpmgdnfxQftQVSEQ1VQKHDnIFSWElUPA+V99iVs1U/cRWqLct95O/v0vU969+7IychkKa+lcCp06SeMBKoRwwUNVciiRFwu6Ho6oqlI0HB63lr2Y0GNMJnbvS7sdAT+LUeQOjgccyizmK0mLBGiftZZXjsv+ng1gdrgPLZCKUUULKyoLNHad5JlilnBsWJI4kYTjbEP8Geey6NONQkK+x07F+YOrbWbzStKrqsiRYApitZjGNi+X2NbY1kjBTbfQeID8Mf+e+42KDRbYIHpGOrAfJZilcp06SJGqlUYLXsLnPLf9BXwUXjn09ypcHnFgT4qCcL7RbwjMPtBqCEEFp8gBt0iAQG9rW0Ma85UBh02Hk1znjvJ+ilStMpw5x4qqkOvU61wxRMviSLCQqlrFlU8ITryGmQCTG55xM5k4+J2FrN3BYhEVNIjoRLiVdYT3AvYJWiu5uof7086yiAutSh97hOeIbcZrgP0cqTIEO8QeQtf5LyYmdBF6Xr6UK82DRB63kvbdbCWdxojIE4RJItzWNqg1s2jTJmp95lk2YBlulgqzfG3PPkfz77uXq3i4ruYPTPg8H0QcjFPQaoSAWxz0oMUIm+5x2ngGJNeEL17g8O3h9k/o/DlulYTAA5bCIsp/8FHjKCsNtrIFPEvnf/kFyrv/QTLji2UMmDweimqa4eQ5xvtKRW1vJ8f6jWzCQGvXDynyLHOGbzUJnT/L+Vj5+x6i8Yarp1GdQ5EtKeITVTxYxY/fYxcY7b1CPKrCuXb9pC4ITbWqG6pg6CqIxQdAuAEIKXQdoGIWhIgLq9TVFzS6EyI66wiccHurGe2TJnaeNNkMIyLewwm7Gefw9Qf2fewlGO614HDmcdkBOrnOHuF8sFy0KA7JgGCyttSR8+JJFmBhOBaac/vdAION8PJ1pC5Ms/gXCZNJ1wwHxiGqmezyWDSHrN3tcTOQ0cBujg4nmVE1ShFIWkERm4egpTSxqsYzbL4+NhnhTD383tVKKkTTWFAiLPDgHImK1UggkggVudDabfx5jbnGyMoMcmRVlWfm/APVU1tLA0Uql8UrZmlFHkTUCO9tIyuv5KYoq1xGjsunh26jtFiM59rTaYjIknk3FxnXbDXaYhM3u6ycRa0pYLihJr6GLM6ngEP2WBGRJwiTgOrz8zC9Nf/mBwtwzJ1D1oIC8l2/Qfkrhh5W779wifLXrCF3ufFl5Jg7l8IdHeMu8mJtmFix5tVsf4QPfHgbcuHIloELI4SJ+5Y9w86sRaqvUf7DjyadP5LtuL1qCd82dO4suXeObWUbB21o/dQ6O8lckbuKCTL98OVrr1pKoVMnuGKJucJB1/N5yb546AMEvPeUmutczUsnvHIN74+yBaS2tJBlRf6EmK4kCkxtClfyQpcu8LzgeIbVJ7bx4uS+/S6aDeDzKhBSqc8f5p9YlOL4AEXnGWGeeYP7H0Qe/m6wjwZMSwDm2XDCVSD8sEbEreZOKxXmWaa1UIbYQLtaZFlugtWHBdl0Ny6zqyVMRtzHXqXg+h0ZV4lGAhUfzGqpC9L/DWEuTJm3iIWU8W11E7TwuU4fpEjlctIwlwdxNQaRd9NZ0zdI5EGAILfPdeaQUT0d+OyGqLNfv8iLkDCLQdg3qmFjEnlKhNwnD5BaMo8iy9eR57Wn2UWUK2PpiEb5PRGBULNY2OzEfeRFnocbqc3T2t5oCKqBtlMWcAPVMbSosllK6n7y9RvzcMMAsxy4ksaf0oJKimJxdxin1ti+VDF7Z755LIL5TG1gRvPmRuB5VpGtEWL2ZpV3plTxgIg8QZgEQm1t5Jyf3v6/eMtm6jx4iPKWLR2yWhVsaqLSW28KHcc8Q+Qltn/2XbjIB/Gxmb/RgtYjHBD5gyr1ekPkD6vx+Tqn3UJFeY5Rz4Nkc7AWqb5KobNnWOCZhjEJQBujvaqKzKkf6BngWLWavE/9iqJbto05iwrh6GpnB9lyKPIQbm6ZO5+3jY1kOjq4Cpltu6ba3MgVLrSxKrU1ZFk/tvdIJljLFpLSWD/I4AYiz7lpy7g9rhlD+oFARjEKQ4nm8QLbhPk4tLN5HI6chVwPRejieaMKPo0NV/BZAMFltEkaBk6oxmFxCW2M+FxC6yQiWWBk0h+IUDiiG275A4YkeBtwlW6g6hZro7RiMcJhYUMTfKJpHPatx2eJp7OoS4RNSHq7KLD9ztybfWgqH2RHlq6Nm7lA2GGuCkIAc3HW5jpyH3+V5/VgysKOjAi+HiW2+mqKVMBAwzyEIUgNRZas4gpT6mXOS6dZPMAoJLDtdnLfuDymKIWbzpr9g+f/8N5ct50cl05xhS0IN0mrzWjXDAaMTDmbjdTCEq5YQRyPquKpqnz/yJJjEW8ysRslKoWqK/2Yg7W1kUURgsnjoqiolKxtjemro7GnFPCRveaKkW/H99NgzLINbDc7bPYfSzaWibXujiDyYbqCrLybZ1hHFL48l9fWaIjTDOAg9vPHKUI3RR5C1Yea3ZxuiMgThEkg1NpKzrL0eUeuinKyetw8m1e4bvAqqx6JUKS3l5zzblY/UMnrP38+6Xr9Fy6S0u8le3ERuSsrs3LCxEESDp4CYZUPlsIwTlE0suV4vi4T0PYYPHKQtL4+ytv34IhVJ1S7cGA/GiAMIUYitTfIsWJsA/hoFw2dPU25RO1oI+s8QwhgO9XW5kEiD8IBbaKIDhgKVDrty1dylSt46HVyDiHyICoRfYB217GCfEK4nSbOjmJb2SRmiNnCnMUouFykB/zDvnfCly+R0txIeffcl7PHDp4+wWLbsXwlx15AXCWKSCxIWIqKyLlhsxE7ce/9OdnX6dADAX489zgZ3OQafBZxHICmsyOvD+6TaJPEIpNmnI9Tagc23lpcnSMT2W1G1a1olFU3q9VEVmOSZ0Zhq6um0MZd4+KoiVY5e8MNPlBmEUNRFgGo4jEmE6mLqlg4oGJi6WrnShrP7o0mS0+JcNWLs9CGMgTB39qCxdyalyicIGwQaxDespvMx/rIDPOTMUYpcJsg3Dxb6jmnbxDcOriFq2auk69TcNttLHb8e+7jlkEYlsAdFEIRQjzJpCUTdJ2c547w9scEHj//4jkcTTCUYEMYvFKePMuIap79xuUhb8Pbev44t99y5U5VyN5Yw6HjcWx2itocRkvkgKhD9ZTdMYdbUFNVrjxmO8OJChwWD7TizL5TILBNkRC7ccYcO/H6zwTTFTDzPr0EYRoQamkl54L09v84GCnds4d6jh0nLRxOa7jCVcCE1Xi0eYbaO+ID/arPR6rfTwsfeZDaXnyZZ/iGAgdSmKlr7vDR5bpuulLfQ9WNvdTQ5uXKHVazXXYLlRa5qMBjJ5t1YqsAkevVpIdClHf/Axm1FSITbSxmIo6Vqyhy7Up8X44WCBcIGM7qyxA8T2Ts6cH07mEcyzBQ7Ym1P6aCqhXeG6ETx9I+NkQw2l5tFRW8jVFkDvX2pFzHR/4Dr5LvxecofOUy5QIYq8AwRu+5+Vh6Xy+LoOEqs7kzX/EOK4BCF85y7MRYX/fExQmIaa3TCPMNHNhPgUMHk66j1NeSrbKKBbBr+07yv/wix1+MB5Eb1WRfvGTMFepcw/NuYXQJhKmpw0cXa7ro8PkWOnS+hY6cb6FjF1rp1JV2utbQy9fB5xEqcHkuG80pdNH8Enf8NK/YZZxX6qH5pW4qLnCSw26dMRW4nICD50ho3AxQrF1tFF62lsPGnWcPk72uOm02XSxSQKlayWIIQjDp4qCfRcRIoNWOD8hTBaKqkK3uGhuZRCpXGC6WcLzs7zUuVyI8xxeCGYnJTOrCSq4wKgsrydZgODSOzVmzf+hKFYTeyo28TzCnCGIzYdhfaHlkw5LO1uweHC2Xl06yiA2v3pQk2FBBxezhkNuNaIcUF1QIN7xXLB0tg+bZrc21vO1oO42JQDhYqnMXsMFM0v0UFicFvvO+GaEdElETXE1LV50dBjjFBrfemvntTCaOikjcN4ajp1TyBEEYBbqqckaeYxhjDEdpCXmWLKHek6eodPctg1o1YbaSiNXtTjJfQWiup2oxOcvKqGTnDmp95jla9KY3JEUzYBW8+fAJnqsLL1pCJrOFHHajUmd126bEgVEs9Nx96x0Zt9GhYmNyj17kWSCiUEXo7BhT+xwOpllc9PZya2UmRK5cMgTH+TPk3pH8uqMSxFXKgUoPBBrEGJwSzQ4HCzWlrpbbRHEfkboavo17921JbYqRG9d4/jC2PzHfh8qla/M2/j2Mttgzp7itz750OcdV5Arr/AWktDbH94dhujL+WUSxrDwa3NnKBE8eJ8fK1dwSHA0Gh62CZora0sTtT6hAa/39LGjRioWWVVt5pTEX2dhAzk2G4x3PUGoa+Z5/hvc9oidyFWTP7c7Xr5Hnjrsp16Di7w8pFAgiAzPCFTjTgJEJWhyNNkpzvF0SB1WRCG4DsyaFuwZUTSNFi5IO0yaLied72QwFRiZWM+Xn2TOKW4nlywlDY1jX4+B5fHYUKnPKhp2kLF7OVR7EEqBKNRyRJavJfeh5bs1DjIDhCHmQq2z+2x4ceiZM19ltMbjl1uRt6GhmAaLOKePLYgJTmV/OVUOOFDCbuVIVq9jhMk/1BfIvX08ezKKF1wxp1DJSJY8DMmyOtFluSUJv9SZynThA9torvA/gDAkBhBbCqM1mGJhkYnwykAuHNlCTEjH2R4rI4XgGdFGkm8tTFaOShX2frup45SxFay6TsnjlQDtnlBzVF7m9NVads7bU83srsGPwZwybr/T3xMWgxQuRN4KzZn/3TdOVbDBbsq7CasXz2JgGRixoN4awHbQvpinSrikIEwxm5+ylpSPOxRTv2E4N//VzKli3lmwFNz8QQ80tNOf25C+1VPMViLxYqyd+htvaqfO1A1Ry5508y4KV85arteQ9coKsc+aQ9dplcm3ZRrbFVVNC3MVQmxqMFsoshIDuG327JsDztw9U88Y6IwWHS8zlZSLyoorCAgsxDr4Xnydt1VqyJLzuWkcHG5TEDvzxE7/DbdS6YAEFXn+NHydw9gwLioI3voWCJ45R8NgRcu28hZ8XhAUqo567743fr23xEnYVhdiAyAxfvUz5+x4mc14ei0U9h5UlVB/Dly/GXUG1rg5DVI8zmD8cKkZBaW0hrbuT3Lv38Hwgqq+oOI6VSM0Ncm7YxAY52KcIe0c4fODAq5xhyIYzhUVJ0RH2pcuMttZjh8lXW8OvUy6iJbS2VjLZHRwXkg34rDAcc40ZOGA4SprYeKm7P0z97DSpkqYbRiU3x26MPDgMt3FGd8p9Q++hK8BqNXPWW77FCPcWxhdL/0CFZBxABcqkKsYBvMlEoXXbyVy5PMm5MB0QQ0rlcp7lC6/bRs5zxzicHO2TaHuMB4qnaTHUPXmDBAOMPuAQqSXETgBUDXFCy6YpEkmOKLDZueIId01lwWKy1V+nCFxAswF5cNEoWXq7WWAOi6aS88JxCq3ZQu7TBzlOwNrZxs81arGx2IMjaKrzY9r90NZEjitn2EQEs4dp5/hiFas0c3moXA0l/FHNC+y+l6uq7AyKdmjMEbrcSe2XjqvnjFy9NMc13L7aXHfz8bgdctGIpitjMcDJBrVkLrnqrw1sGyqwxvt3JiAiTxAmo1VziHm8RKxuF5uwdB08TGUP3M/noX0z0tvHgi6VmPmKq3wRZ/CV7TNWT3FwZt60jRr/5wlqjh4jqlhCkYhCdPggFd16KzkqKnn2KnjkkOHcuMSwPp4KIFsNVY1sWuR4VdIxthhy7IP+s6dJaWrkih6yxZwbN/MBeDbAYRNzULRy5Pm+8LUrZFtYTpaiYg56D509RZ7b7oxfjtcoVXRCAPhefIYrbp477mLRp/7iP0nt6+XAdFRuIOAC+19mAajBndNiTZr7shQWkslhp8DB11jg5N33QDz2AbNsEJ845aLNzzpvPgVe38+VQ4ggtEc61+cw9HcIMHuI+cW0857HDpNr+y6ubJqLithhNRa9MVpQXTVcYG9lt1ZUCAseezOLR7Rnhk4e54MIW2KWYGxbPR5y33kPBY8epkjNdY47GC0QWphnC164RJaFVRxzwuHzPM+G2UjDLdJuNSpnseMaxKJ09YWoozdICkTeQPZbKogMcNmtVFLgFIE2TTD7eke0rh8tXA0pmXvzANlszthQBe6WnoPPk+ncMTJFdRZY5r4ecl48weIl3UG3ramOlEUpDo2qarQeDjfLZobV/uDFE4g7e/01Cq3bYThLQjAl5NyNRMxMxNrZOqJrKXL7rO0tZAqHKLhhB8/n4TliNpHFbXMtC0XcFwTwUGBmDmYnwc27B7VJpqKiZbNn8Fwe5icRyM4B4nj9UltfIRDnlJHa38uC0BQOkjr/5rwhu3IuWT1kdY7n3mDQcuMSV1GxDQg15+ON65e4ioj3ShSvi93B4hvziImB9uNJ1J1HJk3jKiebroxTK/NkICJPECYYCLD81ZmZehRuWM8umcGmZm7RDLW0sEBM18YVM1/x1dWTde5c8itRCvoC1NUb5IM2y/Y9pO1/kfLmzyN7Qx3p8+aywAMQD85Nmyl87eqUEXmYG0MlyZrFATfaGdGqOdZqJASNffkKCl04x8Ym5oJCFnzZijyILtzHSMDVEVU0z97743OB3t9c5n0Qq2JiHi/VhRLVGbT4ee7cS9aBrDmIO+fW7Sza8t/wFvLctZcz9WDGEj5/Nq2xh33Jcp7ZQhUx0ZWUnQjz8zmSIdsq0FD7FdUsOHqismW74+4JcXqEuQkqqkpLM9kWGK3OeihIgf2vsPC0LTRWjC1FJew8OlbQNguhiOeL1xaV5Vh1EK9h/1O/4spi4VvfPnQ1eekyrsIOJ/IgvFCVR1UNPyHenHYrt10jJqC920+9vV4qvlhNHdsqyXSplUdrMIebWFFDO2U81w0LDhwXEGXTEsSiQMyhXZKrwQOzOVOp4i9kV8njsPCxAtfKhhukVCyJtwZau9pHrmANuWFWCi9bQ/aaqxTYeRffJzICo1Y7z6alVuWQZWb29pA6N7mt3dLTTlph6Yi2/+lA1cqMGTkshMxdwPN+XPnLZh4PnSdd7SMKaVQ98RgITUflEe6aCtoF7Q7SbTaujqlVK9nIZCiRhxlCtEkGdtzJtxvx+RXPIXvN4BlrGNfYaq+yEHNcCvFMITuhls7jKlysMqjn5fNjYr4ukOBeCWEUXjXMYp3ZzDOI5nCQq4DIM4QYRhA5zGCUhYvJhLI/nGwjYRbLqOSmE+LjgsnE4hbvHcN0Z2bM4wEReYIwgeAACfEJc++6I6Pr4wAYM3mdrx+k8re+mYJNLUnzeDhYC/IBnkYB3UHd1xuJvBp/ObbX9RiD2HYLFRc6yVzkosjOXRR67RWOVkjNkbMuLKfAkUNs+jHaFjG0A4bOnTHClsdY+TGqeNnNJWEebyymK4nEZtQAKnnB08aAfLZtgtFwmAVFaqQDqmYwAzC53BR49SUyl5SSpcD4ckFVyblxEwWPHmJRRIpquFCmhIZHgwGe74o1w6HihrD0vPsepPCFs6Q2N3H+mn3Zcp4Ns1y6iGmRJIdLYF+1mk/pDtzN+YVsBpILkQc8k5DRBpGFqmbg4AFSKxdzBQ1GKBDyjnUJ1tlFRUakwRjhCtymLUbLYn8fV2hjwGQGojJ0ujVeMR1qgSDKTro9FLS6eXYNYguCDGIOTpOdvSH++0dL5cC9x8O3ORLAaqbSnlayl5dTSWkBh33j4liUQEwoGi6VeGcYb6U8N4Rd+r87EXfTmIH2v5Gs6zMBB+KOK6e5GqMsXmG0KXa388H8aIE9vrqgMmmeDLN99vpqCqaIPIibRKv++PkdrZx3NyoQITCnjA/2lcoV5Dp5gAO4M40xQDaeCZ/VqIaN8L0FUYeWR2TYoYrHAjH2ukDgoipntbHoYZGbIp65fbL6ghG7kIHAS57L8/O/YaiCNk/M3KFaatZUnrlDpqC1v4fs1RdZ9EDAofrHjp+dbaQXz7nZ6sqtr+Hk1tc0wFE1EWwDBGdg591TYv5NKzHm8lBFxXzmTEFEniBMIEpPL5ufwCglU5B913fuHHmvXGXTlaLbbqM+X5irc/gJgadHdT5ww1yMqame8jZvIXu+Y9ABGQd+9/Zw1SdVdEBQQhDA8c+xas2onh+qUZHLFykaDiVZtUPkhM6cjs+GjQQMKdS2FnLvys7uPTqG+IThgLjSe3u4vS+byhNXZBYv4Tm0RNEIwQbnSjbiqLnBjpjOHbuSxJetaqnRKonVTY+VPHfeM8h8RuuG8JtjzM2VzmFTDyNDr5QrZTDbiIVsh69eYtHMzqEp5iLDvSaYCxzOmXK6gIpd3kOPcBuk/6Xnyb3n9kEB5KjYomqZ7eucCARxNODnqi/m7kxofUrNkVSNSGbFH6CI2UYRVSMLZiwHrocYE8y5BT1zyPvaaepesIzbK3kbzZitNAQZcirdLhsVpfytJ1bb3Jca+CCNjUzitbqE52w2kX00WVzC9EDX2DnQ2tHCmXQ835XyeqNlEAe4qNBAoKDCNJI7Ibdmzl3ApiEQBjD8gFHJSAf7I5LyuLhvx7Xz3IIZN9RAFbG5jkIbdiTfFgsbXW0UQJvlKIG4QrUTghOukNwSmpjVNoLwRSujUrZ85OsG/Vwxw2sBh0rsNwi/GKgEwqwE5iyOS6cpwMLReN3M/T1saoMWzZHm9QZVrIrnkLW9mauDyC+EsNTdHvLd9yZe4HFcv0guuGZu2cMtlVgUQHg9qluYVbP2dJB/IBOP7zIUJB0GNdm4YHI+4Slu8ZwKAg/gfR+L2JBKniAIo6L/4kXKX5V5+0fsQC1/x05q+s3vKByKUI/PTApmrgYO8go8N1fd/fPhrOgnR8HQK7WuzYabXzrgtBg8c3JUIk/zeTlsOW/fQ+R/9SU2tECrIw6YA6+9yu1y9hUryTpMJhpETfDEUZ4/Y1GTZTVQD4wtPmEoeF6roJDFGebsssGxfiPHIjhWruGWTxiaoIXTsWkL6Qg3zy8k54aNbJICgRybQUQFc6jXgaMRkIeH13rtenZvBDBVwf0j2w7VKrhGouWVrBZuIcx/5A28b7MxFzEXFHBFMFtQ/YQTKJw/Rwuep//F58i957aciHez3cFOrWwjn+a9hX3O7anefp6NHA2R+jqiRZUUCKkUPn+B9CUryN/eRYGeAAs1RVFJv1ZDoeIKqnn1OHnLlpCKudkBR0roMMzRQqfZLMU0t/U0FSxbTTab1agMcjUOgd5DC/OY4MOBMQwexmsGS5ja2GqvkL32Kh+0wu0Rs1NwEkw62D59kMUEt+cVFLPhiP3qORYBcJ8cqu0RwhHVHa1oDleDMHuH8O2cYzLzvB4be2zZw0KHs+8w75fiogiRhfbHsQgHCDtri2ESgkBtruYtrBw+0y32+D5jXk0rHblllTPgYvvLZqfQ6s3kef1ZroziOaNNEvOBEFp6fgEHvitVq1jguU4fotC6baMKkFfKl5Lz8ilulcS8H6IuuH3XYnweYkEILZUQgpH8IhaR4TWbyXn2CAW338liPjFeALOFEInZgCosZuczFc8TQdTp5vcOV2CzmMOc6kglTxAmCF1RyHv1GlW8/W2ZRQdEjDByVOu8fisF8krJYg9Tvt1Cebb04b7c2gfzkVEC1z9UlTQc5OZnPnyM7UWFxLluAx8cu3bt4QBz60OPUfDkMW5JxJwZTEiGEnnIiPM9+1uyLaqg/AcfHVV2Gpw1s52byxTMOXKsQpYiD62vCMMOHDtEWmcXrxRCcPH8gTVWobNwOyGeP4RRbEZscMi7h8Vz8NABcu2+jcxFxWycEqmtMcKu21r5+fN7w2LlKiJm7XCghqoVqrdou0Q1N1NzEbScav2XsnrOmCVEpRJmNXhPjBbMIUIshq9eYffXXMD7ZpjFA7x/uTV2BJEXMzDB7Bt+ovqGvMnQ5QbyFcyh0Jk6Kj1/jTo230Fz6i5Q65V2ippMZA14qTSokXdxFRVXnyF98Qqyeiws3mLmJhw5ABFX7CJHvYWiIR/ptiLedksWs3DWloaB9jeZn5ttwI4fmWuBW+41ZptUlQqe+gmp825Wr2HYYVJV8t/xcPw9gtZLLA5AHNrrrnHm3aD3UFTnWSoIDcLsWms9G2VwwPo4ADGA+0f1CtU7dtyEeUjK+9rS2TL6mcABYLrBMQio6OQVcIUHgggCKx0IVMesG8QPZs5gMpJJNROVPAiLpNBwp9twuiwo5rlCc99R3tfhFRvJffQlrqShAoZsv9T2zYyfX1EpvycsbU28T7kdNQUtv4j3cQx13iI2S3Ede4VFsDkSIi3xeWQjqjFCUn3BEOxZZuCNN1rJPK5qzyRE5AnCBOG9epVc5eVkHabSpKg4UIQ1eYjbMdkZ00zkdljJg9gEOIfZh/6zRdbXWEAlA5b6MMawbMjc9VCpvcHzQ/aBx0cFTylbSL7nn+YPcjg2QoCEr1yK2+enAjECkYpQ6NGSy5m8dDNSSkM9jaYuhWqb/9++Q65t27mimf/Gt3BrjH3p0nhbICprbrhhvvISee7emySGIfD6f/MEFbzxrewGCZESvniehQgWD/S+Pm4JZYGX4IyGmTPfKy/yv/PuvMd4HsXFpDQ0ZP680cLo7R80xzecsyRiAlCFROVyLCIvgtbh1Wv4veFcv3FCgrwhnLXeXp5zg3DDz4iqc2g3ZmBRdcPiCyp1hvOk4VQZE2hl/X1kXrSUinuaybSoggrmFJEzz03z3SbOp7J5m8i8aBHRwjKyN1wgPdDHB17cSmlJ015VVs6ud1hV54NrmDrAOXCkWdWoTrbWBsOUQZh1OK5fYHfIuHmF1coVF3Nfp/G7pnFrXnDDzkFiCRWy0IadXDVCBc3ecJ0C2+7g+wAIFOcD+4HPmtCarZw/x4Yn44HJTKH1Ozhc3XHhhNGSuXtw9h4iCNgRcywgZ82Tb1jpFxSzyHUffdmoaqa6TiJQ/eIJXlCD4QqqYhmJTHyWhjCTl/xdxeYf3R3GPB5Mm1xuYzsQGl+xnFyopm26ZdQCLxGTGqHI0tXx5ztI6PqSI2fCK9aTy9tHCmb5fP2kzV0Yn63TsV/QepBJlmUoQNE0VdipgLKoynD6nEFMLRktCNMAHOzCCCWMqkwWt+k7d4HdMhPBQSOc8Xr6Q9TY7qUrdT10o7GXfIEIt2KWFjqpON9JDruVzFbboDm68cBWtYQt7mNzPbz9GMju6uSKEf6d+tzQngmHTszSYU4MoPKCMHBUqFCxwkwUKmEcc5AurPnaFXKsGNsXNMLBx2MmD1jnzOPK0mgx2awUQVh5Xj5X9hwrVg6asYOTpvuWPeR/5UXDmGUArR9tdxo7ZqISB2t+CGa0MwYOv05qdxeFr18jy9z53K4ZAyIQLbAY1owZp8BBUuvtzmK7bYbleAYrnHgdsY1YKHBu3sqzghB9o4HDwiGqV63lEHW8J8cLCLleb5iaOnzUGDRT7ZV6OnKhlY5dbKMTl9vpXHUnXazp5r/P60191NETpEhEZSMUj9NGJQUOml/ipvnFLnKrQbIXFpCrtY70xcu4KofVfeReARyg8oGgyUTKwiqeixkOmEug4mJtqiX3wefJee4oWbpaR3xOEIM4qJ8qMy/CxGHu7WIhlpQvB2Mdi40P0NEqiFZAtAQO1/IHsYHsM8yOwdI/qVWz+GZHA+7Df/tDo3K0zPxJmSm0YRdXyyAmBwWNKwq3S6KqNlZ4Lq+vh/+Nvx/8DaKqmQq7aJbO59k4mIhwkHkmAgx5epivS9lfWjFy+jpu/l5YytU2EKlaSf7d96YXeGjxzBLcL9q4OW4iRXBBXKLtNKkrCLOD2+/gFl1UieNnY2Sg5jI7dGb2uN2jajOdCHRUUHMgoKcSUskThCyJdHaR71o1nzBfV7x9G5upDAcCzFEli+XjoULX7w9TR0+AQhGNFE3neRwIu5JC56Q62LEYMJnI9+zvuKUwqips1IE2Qp4D8/ZzyHMMCD+1o4Mvg1V/sKWJ8vY9zIYdEHiJYoGrJIgFmJc8u4E2TpPTOSYHR3YHTDEUySWxCmGsbTIbIFAcq9ey06ZzhLZDtFG6d97CQs9zz30ceaD39SPQjJ1LSz74Ea76sZNmWwsL/0j1NXLvRqU3wsI6dbuxzfHf8/PZoAZiPVVkDoWlsID0/v4RFxkwU4j3Ado08X5H+DkiCUYTy4EFATMqD3l55Fi1mgKHD5J9+cq0fxtodUSFTRk4abpRdcOJFyuM/3M1Du2VwYgRNwADExTgQmGVQopGuhYli2qh+TAnQgacwxaPF8gEtGxxDmFfN7dsxVarYyvjOADFPBHmmICyoJI8N56lsLpxyDkQ3BYHtAh+RjXD4u1lE43YSvpQcEsb2uyE2QW3w503ctoSDFbYUdHpIrWsghzXznEoNpwNR8RkYoHhOnPYmKEymcna006RyhXJ18vCBXnUWCwU3HIrf8akYu1uM/6ucmAipBUUkaXn5iIuqoPuwy/y7FpiKya3h84tY0fGyIqNHCcxUlZd3HQlzeILfz6cP2Y8P7OZRRjn5GFmDjlyaUxWUOW311yiwI67MpobjG97bxdFqlaxsyYHoSdiMh6LHT9TBCCqfnA7jT+X/m4OlofrJrudjvS4fV3jV/EVBiEiTxCyJFBfT3krVxhB5YcOU8PP/5sWvekNQzpm4sCy4+QZsi5byW2YqBrgJ1q+HDYL51DhQHKqgAPavL37uPWRWwmtVj7YxkE7xIL3mafYuZGFBmbxDh1g10bXDmMeI3D0EKktTSzyUkEbJ8/lpYg8ruItH1sVD/lwMBzJJnIhWzhsHAYyWYg8rlJipmzHrkHPeygQiwAB4nv+Ga6QooqKGAWILCN7TSOLx8PisfBNb6Pg8aO8bVp7G1nLUg7+FZXFJbt0ms2GuQhMZHp74xl8I8ExCt7+EbcfpiOurdvjrwGiA5SmplGJPM6aGwgLxz6IWqzkq61ncwcIMp5ZDSmGQItohqhDyDeEG5woB9IEUiO8eVwR+8CMy40rIRC82Okw/g6jLnJaTaRZomxYkw1Y4eaqB2Z4ErLIcGCEmRac9EJYow989drsXAngEOIUi/FEggkH4zjIdF+/ROHh2qNUhS3WQ6uTcxWFKQBeN1UZ3PqXI2C3jzk7iDlkmmGBANUJLA7ggJ1Dx1972hAsGVZ5cTtUha1tzaTOW8Ah5bGFigkHIi7VHTTo57ZSzKrlAr2gJKlyB9dQVN1RrQvHnCWjOlflIys2kPPCca6aYnYto6cQDHC1bBBWm9Eq2t/DLdxsanPt/LCtkNa2RqP19spZCmf4/FGlw3uEq7t2583PowS0gYWpQSLPnc+Om7xNA5U5fHZBjA77mRR77v09svg0gYjIE4QsCdQ3UPGObWRxuWjePXdTz8lT1PrMc7ToDY/G56swW9fni3C1zt/dR4GrtWResolMzWjZinJgcWnB5FbsRjILSZeVh6oQWipDZ05x5QgH4hCDsP+PAdMQVJbSOUOisoO8OVR6YiCkG+YasLQfCyxKx6lVMwZMV7TOdqLFQx+QpxJr8bTMTXC1ywB75WIWN1pXB0Xqasi2sIJooZmU5iauxhmC02ihxSyeUnMdJa2kdk1kHqISaykpMZxBB0Qd5vKMKI1MRV4+V/KGAyJS7+vlx4phXbiQjXdiAnOkUG/EBvDcm6aTfqWaQjvvouD1Tp5/M1vnku21o9S5eidX3/h5mI3cOLRN2ixmroQnBnePCpOJq2cWbv3K0mQn4KMoIix6uym06WZIM+7P1niDrF3WQQ6EqLbB5GJIkQcHxAvHKQy7cU++4QLndHFFEAeC6UDrFM/tTcAMo5AdtqYaDhEP3LI354Y4aL1jY44NO4wctesXKWq1UrT+Or8vuS3PZqfg1tsGV29GILJ4JRtmRB0Ow2J+FMZY4wGMMuCAiSB1LJjkAt2Tx9EAHHUy8DxRzfQcfI4ii1ewOIbQ5WqcrrFZCip6GW9zKH0lD+DvFi2bEQTBu+D4aGUjGHyGDN5QnaMs8F5ynXp9wGgpORYmHfh80opL+TNOLyga3oAmFewPm91o5zSZyRwJs2MnqokcczFcJVPTjOcyg8LGpzpT469UEKYJWjhM4a5uci24GcxatGUzz+d17H9tIMNOoY7eABunoEIQvXaZPKtWkadkZszGwOLf+9STbMUfOn2SBYBt3s0vOMxPBQ69nrYdkDPdkDenKHETDdj+wwVyNG6aieg+H1ccxxMIq8jJ4WeoUoEzpH3lqlGJDhbb5ZXcgum+7XZSm5tJaahjsYzVbERSqI0NLLiDR4+wA2iiyIu5bWJOUW1pThB5JaT3GDMnmZqv4HUaDohGvBcSX3NUHtmdE66kCVXAcEQlfwjmJQr1+yPsIIvKHAxOgL2vmwqCUerqUclq0bl10jJ/EeXXXaK5VpUoC+fX0cAzdN7+UYk8rH5jgD+x2oDVb87QUhRSEsQf0ErnkfnCiXhAcbq2KliO46CQW/Dwus5dwCvnOBAcqlUTB+XC1MPa3sIHyBDimbS3JRGbk07zWQLRz9b6a7eS7vSQ+9grbNKBg3hkvdmvX+SDcTDU4sBw8KJB9BzfT7Z/F+OGorC4gSkKcu1yBsLI8wu5+skzfmiBvXaO5+3w/MPrd/DfHyqkyOtTIKyyaBNFJQ8toenA4zkunTJmePEdGg6yYE8n8uBwCsdNLPxgXtF14jW+XywGDQduhyohxGliHEIiuF8sSKTdxgFjmiies6awYQwqdGx8M4zIYxGI5yG5nBPG1OkRE4RpAMLIXQsXJAUlo/Lg2LmbOmqb6OJLR6i2tY/bxmCakm8nMjXWkmuNcXCWKyI11zmTbjKAOHNu3EL+l18gc2EhmWAIk9CayWHcxSWktg82KcF+4wpUexv/DrGHqh/EylgZT2fNGAgZh5NlqvnMsDb7LU1ky6LyN+g+IhEjbsHhJGt5OYs1VAdxPiqiiGBAvqHJ6eDKTaLIUlubWRDCtRT/jj+P4uzMV/D6at6+Ya+jdXaSpXRucpsq3CfnzKfuazfY1ORqfTcdv9RGRy620cnLbXThRhe1dPpJU3UqbbhIFR3XaKHWS2XBdnJWVdG8EjfPqBbkOcjjdlC0cik5mtMfeOQSDSLP15fW0AIr58MdxHA7UkKrJoNqCla/YcueegBmMrODJpww02Gru8r3hzm8GCzy2m++nkl3FwoY8384KBemFqrCM0yYrbRfv5S1YYbj4kmypTEAwfuSBd767bxo4Dx3hOet2ODCZOZWQv+dD4/NlASzeZUruOV4qry3YAYD0aBU5X5BA5mB+Fvmx2mq4eoa5vQgZPD3Ze1sYZdIVOjZeTMLhosdgIBGNh5aP2OfIwicTwcH3A9UELEfcDsO9M7QdMXi7RnS5TKdw2b8Moi8gI/nhCEGue0cLcEjGEJhVhlmP8LEISJPELIgUFdProoKrj60dvmpurGXrtR1041WHwU37SH18nkqDPdTntvOlZvItatkq6hM2/o4WnDwHDp/ltSmxkl77WxLl/G8FFoz4TqZWqWyLlzEYiQdbMbR2sJ5bwgJt1VUjDp4OpEoKnnj3K4JkcrmMQMOopm4fWJOECHco0WDJX9BIe9j3A9aOCM1N1jUIlMQRi0QYXjuibNvLDA5kH6hkfHX3W04bXJMQBFpPT1JDqrDgftms5Y0hgdos2zrDlBXXSP1mN1U19pPF2u66OjFNjp8oZXO+RzUfO4qC7q6Vi/5QwrZLWYWb2WlHppT5KKS7gZyBH3chgi3SaySp6ty4GAKTpPcRjWOYBU/SeQpCq+uu84dJee5Y1x1SweEGmah0uVk4aCJWzXTVGHQsskBwSmvBw6yLN4+Cq/YwNbescdFpdGEVX7MxqTA832YDZoIIwwhK6zd7XxwjQNiNtNpy8yRMCbk8P6ypjmQdty4zCHWEAgQghAQ4xE0jfc2qoHISpsKwPBkvEQDO2z293DbJgR5YMutpBWVcLsqxzg013GFNLh5z4iVs7RB6EPNQ5rN7GCpzl9kOHduuZXbeykUSL5eNMrzl9qcm11FSlk5G+oMu3igKvy5wSKOK3npRR6qg2wkleazNhYxgYWmmODH68DtnbjNMCJvqjprzlTkW0AQMkRVNeqprqUOWyFda+ihxnYf+YMRPvguynNQaVkJFdx+JwVf3096KMjmGJGrl7m9MZdAPJlMZm6Py/QgPdfgOcPGHy2SlnmDV3VtCxaS0pxehEJ0RKqvclg6QtNd20afizfRlbzYXB5aVTMBwgrVv7GAGIJEExtbxWKOM8BcXWzxAFU+tLsmZtLpXq8xM5GXx9W9xAoqxCIbuARSDhyGgHON7E7q7+zhBQ7Mmnb1BelKbTcdu9RGZ651UPv1RrriNXPMAKpziBiAmHPPnUMeG9FCrY/mFTr5b8XltLL5STy0ueYKBysri1dSaPMe8t/xUNqVbognHFQgfHk84YMYHLBEo7ya7zn8Ah98wcIc2VIIZcbBTFKmEmYfuztYkKUDB27cxpnu8fhAy8SVwERQtYlgNd9iMap3Ha038/P495vVvUQjBhyMC1MPC1xRBw7K0XqbcTUvqpPzyhluxUQ0QpK1Peac+rpILSrh1kWTphkGJOMx7202U3jNlinTbgcBhmy68SBWyXNcPWOY1HjyWUibVIU/f7TieRTYvTd70YLPlHCQzVwyQS+ew4tDyMhLJDYvl9QZEAtUx3tkOKFVUGx8duF1HGoBErPJXLG7OZdnba41zFg8BXy+taeD1PkDRl9wAy2Zy2Y0wz22VPImFpnJE4RhgBEEKhWYr+tpbid/UCOHyU4F7vSOmHCP1FasosDrr5GtsooP8DHPlEtYOK5bT6GzZ7hShNy1yQJiJ114OapdqH7Ecus0n5fn99y7byNzcTE5t+0w5vByaAwxmmiDUYeiZ5jZpnV3DRkLoUfCGVX4YpW8GKgMoy0SzpUxuMpXMOCAOTB3xy6mCbOjRgW1mSt//DxQkezppqjLxfNwCP6ORRDghFZLGAhhTg4Olm4fke90DYVL5nHBCQsMMEDJc9toXr6V8swqmRbNZWGZSmTVJm45Ml84zgcCqFzF8oicF05QePnajJ3+IuVLyXn5lNEilWm0QShAjqtnKbQxeR5uSCxWrrSY/P2G6cnKjbyyDlDJgNhyH3+VD5Sw0o6DJrjxwVkTW2TGCnvpvKR9MazzHkTbggqepQsPzLTgABYtYf5VG43bz13Aj6FULrv5e80VUhbftLLHCr05FORqkTDFgBtjZysFlhuLfmidxN+etbWR1BGiLlAxgnEKxDtaBy39MM6YG7ekRwXcfeIAv0d5bnOKGnrlGsyrxcPecww+j1DJgphCey1js1No3XaefQut3zYqsQujGM74y6LSHlm+nhyXT3GreKxyhvgG/gxNea3VknlGoPoQ4hP5mXjvDFfFi8FibiAUnoPfL5/hDovwyg0sJFGRVEtvzuPj36gupltkYiMbuHmOkygX0iOVPEFIDfYOKhx2jFbMq3W9dL2xl9vRtOYmyquqpKL8Aav1IXCs28BtfcHjR8ixNjn8fKzgIB7B14aAhHlG5nNVuQatf1xlSiNiIDpg5a80N7P48r/4HM+RKU2NfBnCwHMp8G5W8sa3XTNeyUOoewZVVBZ5Q1TyvL/5FTuLjgQy8hIXCrj90+3iObtEcB3MC8ZQWppJLZ5Hnb1BamjzUoe1kHqu13GbMX5vVe109UINHTmP0O9WOnGpjU5f7eC2yqv1PVTf2s/ve5iiYO6U8vOpQA9SYZ6D/wZKC10cAI4wcKt3wKUtjcADOPgM7riL/LfuI3XuIs5Z8hx4lpxnjrBteDamCWwaYTLFQ4Izgc1LUAXLcAYKbXTsTogMKU9BXOAxJhOF1m3lWRy1aA65Th3kAxvbjct8AIR5KMzFuF9/jmy1Vw27/AxQyir5gD+2jbb6arZtj+XnGQdmvfF2KM7U8vWxgI1ha2vklq3ZcpA/nYi5MSZWcCJL1rCzamqbbhJKhOw3LlF45SZ+XfE+wEF8YrXX0tHKFeYIBOQseu1hYKJnWBHLGpOJF3RCa5PFHEQWFnZG6+SJlutsq4+Yu0N7JccpDIDPMyz0pMLVtJ4hOk10bSA/s2IgUmP4BWg9z2jLBPbGGywqMYfHuTNYCLNakyqJHAeDSl6a9zMvTBRJq+ZEI5U8QRiYK/IFFOrh/DqFVC1KNquJHHYr5bmNqANfVxvZ147ceonrunbfRpbr13h+Kpcgb40FEgxMYJ7R08Pte5MBQs0hNBNNaBKBfT7y78KXL3DLKloKI9evcTRAruFZs2g058IxHcgHNJktPKNmSnCy9L++n5xr1scjBCAC8fokRgrEt1dVKYqKS18fmdPlJQ1TycNzRRuoNaVNVnXnkb+tg8J588jnDxNdukHtzkoKDRiFmKJRmtfeSx0XGkl3OMmlOqig5QZFy5aRy2GlAheRJRzkgXp8cacG71rKFnJ7mb5s9eB9gjacggy+wG12rljhxDMdrQ2G4182B6Z88LWMD26jAyYGaFFTyquGFJloE8IBDlaTM6kYWtsauErnuH6J+t/wnkGXYyVed+XxSn9w825ynT5IJrOJoiYTRR1uCm69lQUZ9hdas9DmNRKwS8dBlbW1yTBiaWukwPY7Ep6EJd4OxSvlZgtFlqzmGcHgttu5MoD9yQelwpSDA+wT5qeAFluw6OkY0swE1Vt13sK4uyJEnv3GRf63rbGGK9T+PfePWA2cceDvGQZY45Q3CFg0pyHWhTAahgpCHw7O1kSkJ2a8QwF2tYT7ZrqsQnbNPHfUaOlN+W6GMERrOEdAeHtJWTj8dzE7bHYjmkczYj+23U4OmM20NXBuohlidWARCqCqikqdUZ1OruaZezP8jpiCOOuuksod0mPL8p11lbz9+/fTo48+SgsXLuQD4yeffDLp8l/+8pe0b98+mjNnDl9++vTpQfcRDofp4x//OF/H4/HQY489Ro2Nk2dIIUwf4ICJ+aK6ln6uXNS19FEwopLbaWNnzAKPg8PK8d6D4QRETaZh1maHg5w5ruKxE2XtDbIvX5nUbjdZqB1tZJ079P5AJQ8OmwhOd6xcTbbyCq5sobKXayZqHi81FD3++KEgZwYizy5GNBjkA3/ECAzeXqP6gly54cD7Lsr5fx6OG8AcXMu1Ouqzeuhak5fftzD+OXG5jS52KNR44TrVv3qIAq++RIrDTY48N80tNipu80o95Fy4kBZEvfx7weJy8oT6qfT6aSo+tZ/y9/+WXGcOc+Cvo3qwcysEBgQLZugG7Q+E92ZpxsBucMvXjardSimr4G1xXL/Abn+wo3eePcoHI+nAbA2EWdrcp1QQNN/VbhyIqQpF7YMPItm9snQuWfxevt/ArntIKZ3P7VRoZeLnl1/EwclsFDPEdqWCtlBYtcNkA3OQqYJUnZPsqqlULuftg607r7hr2vA5VcKkgQrIoCw1dqxcTrb66+lvpKncnomMtvhZMLjw9vEih632CldP1PLs3B1nAmh7xGLVdKtcwiwmbRD6cGAGv3ge6S43fz67j71qLI6la/lEdQ3xD1jYSgGmVrG5YEs/4gxGaNfkvNB+zt+D2Q7mEnF73A+cPLmambL/0c6KhQd8ht28I43n96brPJ6tt5NU5/SMwBq1yKuurqZnn32WgsEg/z4aAwi/30+bNm2ib3/720Nefuutt9JXvvKVIe/jE5/4BD3xxBP0s5/9jA4cOEA+n48eeeQR0hIHkwVhAIQs93rD1Njupcv1PVTd0ENdfSFy2MxUWuSiAo+ds+1S0Xt7OassNfdtIlEa61lkopIEjEreJIq89va0piuJQrfgDW+Ji11U/OxVSyhyY/i8tdGge/vHPSNvUCh6gvkK2lAhupXGhozm8aIDbZqo0iWiR6OcIecNRKi7P0Rtje3Up1np5NVOo6XycjvVXbhBTZqb2ynrW73U0ObjKrSpuJRchflUnO8g+8rVpO+6g5x2KweDx8AAv+PKGfK8/GvyvPY0W/rj4DO0ahP5736UAnvu41YkbgdLbW2EFfuCSm73STtQP4pVWh7+H415kNXKwiq4/U4Kr9vG24wDHtfpw8mmFEDXWQBhJg5VypGAUDPpUeMABg6IzXVDWJDP5ZVwHPBg9doGV8v55Ul5VhCwMHBIZ5CSDghD3CfEdrp2MLRncYRD7DmidXTtNha5mNnhCt80O+idEYzQBoyFEZOupw0gR9UWfz/pnFJtOLgunps8x2Sx8PsE7byo8KlzFw5ZwZ7JcFV+vFo1xxF21hzFXBoqvXifwD01vGpjcpUx5f3H102JekGbKKp3vNCAlm9NHXGBjfevqvA8dSx3k6uHMJnq6eQZ5FTw+RfauJONYvC+R5eH+8hLvGiW7ULglEDXyertoUh+8exo1+zq6qK3v/3t9NJLL3GF49q1a7R06VL6wAc+QEVFRfSNb3wj4/t68MEH+TQU7373u/lnbW368OG+vj763ve+Rz/+8Y/p3nvv5fN+8pOfUEVFBb3wwgtcBRQEACMJzBZ1cki5yu9du81MBXn2uMvfcKg9XWQpnVwzA4iGxEqiye3mtj89HGZBNZEg/kD3eYet5IHU6hqqev5XXyLH+o2jCgdPBxaYwpcukn3FxLVSWObO5azCGEpDPVcTo6rGwg3zcVpP+nk8XY9SoLefwlYHBZraqb28n41NUKkLRTReiEC7sKbr5OhqJZdipUBQISdaKvMs5Lnh5fkbd3HqgYKDwpt3D7vdaKFhwwebg4US7LE9rz9jWP7HDhRtdv7y5hmiFAMPiDz3yQNJ5g48UD8gaLLFdXy/Ed48VqMQs5kzwhyXTpPrzCEKbr3t5kX+fq6I4cA41b0yHWiBMwV9FNq91zioqr5IalllUusTRB6CkbEy7T6+nyuLaKP0775v0P1h3hBCMNPga27BPHuYlIWV6dtd55eT++hLFFozsN9gBrFhJ6/uh1dtzugxhNzBAvvSKQpu2p3+fcxB2ucpMpRRkNnCOXb2huvJbb3RKNkaqim0esugm+Bv2IkZKcQxTJHMuokGlXx9nExXcgacNH19ZOntNmZlbXZu1xypTTIdPBccCZO97mpSm6a1qZbbJ4Nbb795XURpJMzvASO0vZLfb/j8Ykffkb6D4bCJRavowCz0wHmo5tlvXB6cBzoAFibCK9azORUeD6J0WOOpKYy5v4c0dx5FJ3GBfyxkvfzz//1//x9ZrVaqr68n90BFAUD4PfPMMzSRnDhxghRFofvvvz9+Hlo/169fTwcPHpzQbRGmrokKjCYQeVDT0sdOgUUFDir+/9n7DxjH0vU6FP3IzUxWzp1zznnyzJlwso50bMm6tnClAz3bDzaeYUt6AgQDCrYh+0qAZFjPMmxcwbJly/a1ZUknn8mpe3p6OuccKufIvDc3H9b6uVmbLMYK3VXdXEBN9VSxGHb81/etb616rwT97ooI3mxX5gmTvInxnM4QSBL+/3Gbr8AVMnb6FCMUYNtfDdDtcvh8YgyVDk21A86c0dMn+bqFgLzANGytFxA4Xi243Wem1WwdZLT371KeqdXXsasHJEfGJOIN0eTk5qNxuXJ3hAHgn18bkJu3+uSREZCR3iFKLhEUPhPV2ckLzIxJfcDF/LhWTZdQewuNTtiVS5skKvOebXBkJIDWfnO56FLp7smVi2HhiEyvfECuA4kU5Duo0KKCzJvgfCq0dK6bpmxoUeBQ9u6sWNtCfLUpvL9mMYOhHDvwgjBT4r17RRI79nMmEYsVSKTytw87l40tfAzm6OAQmoKpRr6RAbIKWzvEOYO8rcriKvg2gnXs4qDqnw98xsSW3eK/+gXJBd4zJJrhl79e1kihhsWXDMJxEMQc3VdEJOQDcl0QEn3DrAtqPkDyVP7jrEkP886cWkHiiEWnQ0/wODTg4voMgp285erUmE6TBAVOvU0ZuTY5KsFT71CWWyoIveRTeryib97Ja5JrPBNTkEqJ9/4NcULJkgluB9Axw7U1ezylTUXyMlJNOGOWc9a0AHVA/myi0bWenelSnwMzonAljZx4fcUSPAD7zmiYO/u4UlA1NX377bcp01yzJrcquXXrVnn0aK6sZSkxODgoHo9HmppyFxgdHR38XTFgjg9fFqanZxcENax8YgcTlWjckImZOEkeOiNYIDfX+3Kka9UARhf2oOnHjayJR15ouCXZhD3+40L87Bna+Ls65jd87tm0VZJ37zBuohIgUB7zb+G3fyTBV74kWp1NDmeaErt4jll7i9UZrAQOp1Nt+/ExiYcjkpwOS3L9VomHTTHPX5fpdIuE7nTLoGeN6OjoaU7REHPgdIimOSRk6uLobBNfdFQ66lxZ8wAQgeDVzzhvgXkukBJ7pR43Z7qZVUmuSwHV2MDpd7lQtd4HpIIezOVt2jnn8bjBhz76vqTqGtR70VzzCl7GHB+qwjmh4wsFogg61jArLhnaxR854erW0CJmANl3peWanrvXJO10qU5lRnoEUsvoAnxGzaU6l1jgZBaY2FfB979L2V1+ZRwkjIv8WEQCp9+X6PHXyi7wYImfaulk5R4dOy6yNmzPCVhPta2SSFM74x28t0FKD4g8BtOhJQELF1NqBnIJTTQWHejQ3bhAgobZSHTX4LSajEf5MxQdcKxg/9AYp4SkErI4nHPu/u5sRAYIATMSC1zXHAzqoBfHvAjDU9PJCzy5+KBSgJRWGxuUOLq7uEY6HJQuYt4WxS37uVwtUpBsw1mzfTXnNVHAQsEJr5mwoh6cGn8GSSVMYqBOQLHKCm2HsybmeytBoW4d3j9mScsV9+brQLqcoE2MSbRt5eaOVt3Jw5ycvYNnYXR0VLyPWTJWakFcasH3L//lv5SGhobsF+SdNaxsIM8LphT3+qbkds+kPOifovxNmaj42bWbL8EDkYBBBsKnSz5O1yV+6QK7O4sN5M3BOh9fduA9Pc65PL3nESMcfAfmSogqBebykNdWrDOXv+0hiwS5g3Nl+J0fiz7Ql50B1h/cpzslwteXGpBPomiA4wwznUPilxvnb8vD90/KqPjkgbeDUuDU5KToExPiSJvS0NIgnS1BaWv0SzOCwOu8UhfwiCeVFIfPPxu6nQFkPbj5ogrL4fXITI5FNaqKi52Bhhs25nogF8sxd8D7ytj122GE6hm+DEln5MQbEt97lNl11QIdNswILirJy8iaQPKsWT+8DjKjmE0FN74ikQaYD/RdP08ZpLUgR2fMqSdpOgPJ5ew8XnN28Z0KNdDlL9/pju6ho4MSefErlFKhM2M9Ryloo0PM2MPiCq6JcNIDCUeHMgcuF00OMD/JLtAKAzrSvoufSfCjH5AcwVBiWcMwxHflDBfMkM25BrpJNEDAAcjfokdf4fxlEPEZ3fcUCVy3OWdOsxjQZbH2I/Y1iiAsHBSA5ayITLRndQZTBaE/ebmm5/aVnC4a/o2iECTU3O+Z/QOCFT/wvERe+uqC9hlk4sjPVLNytyWxeRfln5CLIzfQTrDQZfaffo/Hql0KzKLKArv+yW17leTzaUbaZNHNWKGuoPMieS+//LL85//8n7P/DzJlmqb8/u//vrz22mvyONHZ2SnJZFImJmZPMGB4eJjdvGL4jd/4Dc7zWV89PSvvBlmDMKwZ7pj9I2FK3h70TUk4mhS/R6PUDe6YhUxUqgWy4JC/Vsp0BaQj+vlndL/EzFkai0m782J/38LeQ55U0wK7SZO5x7+F+NXLi2pywi7p+XMSOP78ggxoEHPgXr2WBC1flhl+/52cbWf094lWX8/unWfzFgm++IrEL5yTme/9pcSvXJT45QviO7hwy3jIJHE8MfR+Oi79o2HmJCI37uwNSCwH5TTy5G4o85PrD8alP+WT1PCQBHrvSvrgCWla3S6hdIL5da3DDyjvdbkKx0vgZox8J4bN5pC8US7wYweepxySrpW24XYSjKbFl45gxg+LV0owAaeTi0g4ouUDjpYW0fQMPFKhzPPowKDDhnk1fv4K8+sqAc0t0iBZU1yYozsKqSbnS0Cqi5ivMNMO8yY2WR2eC2QNC3ks3CCNZKfNPhMzPsIFer6rJRwvSQA8XjUL2diicvDKAFIskF/1pjyS3LqHC7ZCbnkkevuOM8/PLlFdCUDnAYtgZCcicqKQPHg5ATlf2MZOSu/eZhEGJNvucIiuGuZBY/tP8DiBsZHdGXPOc44M0AgJwPHBOJB4jDmSlNbBKn96Yva8zL6XCXbe2TF8RrGUQegVv4fIjLgHHrFIgYgBEC8UAuI7DxSXki6wW23FFPiunqVUl905l1uZYuE92LrjuJ/guE1u2jVLyFKGuv88hkzZlQ5neFpt75WkMMhD1Ss1kLlXX31Vzp49S4L167/+63Lt2jUZHx+XkydPyuPE4cOHxe12yzvvvCM/93M/x58NDAzI1atX5fd+7/eK/h06jsul61hDdUAo83QkwYU4uiZJhJekRXxejZ2SaiR7kY8/oAGIq0hYdSWB1hZApjCjVfeNb0nszOcS+egDCb7ymiQfPpD4xQu86Nb/jb81b0mhylub+x6Qn2bOzEg6lcrJqwNRSty8zjk1z6YtshjA58PGRnzAQgHChrB4uEBa2yRx/Sq3deL6NfHt3Z/drpB3WoDxTN3Xfordy+SD++LetKXs/svv7Cf0FJ0rxyZjJHY6TE5gdgJHxZTJL2R/AygSQGKJLrDTIQz+bgw5+XyO0GoJXTspDuSbMRMozZun0bJdfNfPZd3ICsGZiLODNreTN6aG8l1uOka6+x/O5hCl0/w9F5aLDCwaYIDiv3JGIse/xIUIyCaiBHLmKVIpVo+jh17iohzGKXr7GkmHqpRN4bNMjUti12FK1RwwrbF1LBcu2UQ3r4/kSBkMZDpzMJRBdzQ/ZgCB08hCam7LyQdMoWsZniIZwQwL5HTcR3CxzACh6YkdB3O6LiToM5PsclpAJT304fckFnuxqMQOxw+t4fMq5EpqWnieEO8N0Qu+y5/TcTQ/G2u5AtuRrqgw+nG5+bnxtRAp21KCXfa2Ls4nJXC9heyuCMnA/kMnpxzcgz08TiF9wzGB53f1d7OjF82Y+JAEaq5ZMyGYecxMSgzPv4IXn8s6CL1CIMtS71pPKTfcJOFCiX0IOfVSAq+Bmb/IC2/lyu7PfMhjCTPCuA7OfO3nOfPsvXaW1zwUDmAgguLis+jIWi0ody2QRfhUk7xdu3bJ5cuX5d/9u38nmqZRvvntb39b/uE//IfS1VXdXBDiDhDFYOHBgwfMwmtubpZ169aROMLgpb9f5QLdunUr28HDF6SWv/zLvyy/+qu/Ki0tLfy7X/u1X5O9e/dm3TZreHrcMdGlG52KyVQ4KZrTIX6vJgG/d94yTGNkWJyPHlZA8sYLBlpnfz85QSIXeusr7HD5jz8nsdMnZfov/5doDY0SeuMtiaBDFY+Lwz+/mxJIDZwpC82GkehNTeW8R7g9Ama4vGV8pYDBiXvVmkWZfdPa2klM6Rja0ipmLCZ69yMJvfFlCb/3trg3bGTHz0Cn7PmX5v59U7P4bZ1NEDkcI/gC8dd1k5mH6PTORJIka3CndGtOGZ1Ss5ouzSEuzUnyhjw7D+bmPJr6GX5YBsxdi0cZP6DkNw7mH8EUAf8264tIWUA6bSTPjbgCwNAp08rKaNwe0W1EEYv8tMe3ZAs7zG6AvPiufkFZEbpJgUd3JAHZY2afu/ofMFRdRxdh6x5+/tCH35XIy1+rKp+NJiSaS4WKI9NpZkqMxSJ5lGyuoUNl2uVmhIGFdKBwJ8/Tc5/7Yo4UFtsac3iJuCQ3bicJBiHMkjDTpAwTBNkO773rkti0g90/2KXDhABfkMH6v/hIYide52e3Ojk0VVi3hSSaM5h55xjeGyWoxT5v11pxDfeKa7BbDLg4LnNAGutABqFFdh2OrO37cg31RucZJkWEpklaWyDBQKFjfFg5FeI823GAkQj+859yzpPXl5kpFoR4bUgm+DPr3HmWCd7jCEIvi3SanXm4+uI4hlRXOVhW75xZLVAIBMG1F4vw71RTiwQ/e4eFOZhHqXy9VomeeIMFBc/9GyR9lgFLDeVJHmNpVjDmpbkCwfqd3/mdBb84uoF2ieev/Mqv8Psv/uIvyp/+6Z/Kd7/7XfnOd76T/f3P//zP8/tv/dZvyW//9m/z33/4h39It0908pDZ9/rrr/NvQUBreDrcMbFQh4lKLJHiwryxrrLYg5LPreuSTiSYa+YvI/fDDJq/iHMjZsYin34k/iNHs4YgIF7+Ey+IZwQ5ch0kRSBiqalJcc6X5KGbeOR4wd9Zc3l2kgezEt+efZK4c1sWC5CcencoQ4qFghEWm7dK8t5dkrzErRvi2biJxjK+3fskdvYMzWTca9fnOHhiLm4mojNLTk8pUodjJBI32JUz7J043IRdTvG4NJrx0L3SNEn22pv8WSKnpFAuMQP11X0GSF4Q6J0JvwbYmUsmKH8sVgFkBwCLE4cz08lTMjt0tpgjVKTCyqriEkg17Uhu3in+8ydVLtLGHWiSk3ha3S3frctKIpYx+YgdflmCH36Pi51kFSSPjpeZx2OmTc3lVRYxUAmsWRh0QpObd5cmSwic7rnHWRfTP1fCZHXzQIJNzPVh32WuP9xnMH+wLTa10UEuxNHZC37yE5VlBuKCDmPnGsZX+C6eYgcTxjoJdGZTBiV6WPQj927O56HMtLQzKDrHqOBDArvc57Qw0wkzHPv7NJYzyct0ngvtm/kC3TgcA3BKhfyT4dKNzcytjB5T6yJ3731GL2DfI6oB5A8d4kqdEZ9WPJEgdMza2+5FlEenzdmCD6Mwqp9Nnq/ywihA1CDLpNIAv7NvG7gowyBozUZKTLPqkBqKg8qZUboZS+oZInkff/xx2Zm9SgHZZ6kQ9V/6pV/iVyn4fD75oz/6I37V8PRIMtF9Gct07XCMeN0OaQy5F428Q+II+SO6a1auWVHTlcm5rpYWaPwRDIlnfW71HETP7j6pNTbSvEXm4YKJHDzKMQsYHgF4ncStm+LesIGdRHQWzUhEPFu3KSOYMkZElZLi1NiouDrn56hZCHArnfnBX4u5b78k792Ruq98Q/1823ZJ3L8j0UsXxf3Sl0jyQd6mwwkZmoixo4tjRL0x3L8c7NCB0Pk87oo7cRbQSaHkrcoFHAwWsLC2V1MtWR0WbCVnSTKSNNysQfrQwVGGHq2lrZxbF2/7F35zyJs7KoEzH3ARzuH9O1f5PQ3Z6tREjgwNXTgQT5IpG9ktB+V42Zx9jvyIgsVy2UTl2t7Jo1wzjyy5+x7RWAWdAbtUM/s3oQZxDfdzkc25OHZiVRAzpJqpVjU/hzBrdPAoqd0DQ5RRNRtjez0szilPDdZRNsVtmSGMsdZORagLSNDMQJCSVs4uFikCwGo/7fGINjpQWC6Gokf/o4KLwychfaR5jQ3o5LEDauscLxdwv2BhDJK/SHCNZmYvkX3YuVbNKAaCYjS1s/CDaxIKEtHn31JmHg9vk0TQYbeYSuCZik94fFJNnOeQQ0Oqa7lNYt8w+3IZHat0Ay5j8rNosviVgFRKnUtVFCBzlDNujzLsihU263oqSR6IWT7sC8hUagVT3hqeKLiQjyRlZCImM1ElyQwF3JTaRT/7VJJ+v/gPLE4lNYVcmVCdOFvb2M3TdjUUnUMDuYJ0sBABjF+/wry4cnDWNzIce17vNWO6UoyouTdsEmNwUKKfnZLACy8xnsCzZSsJn8PrZX5bMYJYKYyhAUosF2K4kg90NRGmjvlFR3uXjCUdMjE6LtPouNVvEu/YTZkY1MUcGGSAeMoUSnQbQ14SukV5DzOTlO859Oov4q7hAUnmRQeg44JuRPkqdGaB4nCQWIAIgBxAElgQmXm8xJY9stQAAQXRg4EADGAgR+R2mp7kwirfehvRESCF1YDzeFv3ZkkU5JqLDSzA3APdOYtBkDxFljJEwkxxFi924Dl2wbDIzgcknCgEYIEdfvWbdMj0XTrNLiYc7eI7DrCTiccwRgPFAk1Ti8L123JcS/lZJ0YlsbvwdayoHb5T437h4raEZT5MPkAGYgVIHo47zIpGWjsKEsnHCZiS2DusAKTOIL6cmazAjbK6zLIbkly3dd4xE6rL3rzoxCG5ZdfsOfT5+5J2eRgcjaICZNEsrni8JMDOa2e5/yG3S2ZiFp5VMD5hHsewq/+hpP2hqhQR+Bvv3euS2HmI5ziuByz8DPUyIqGG5QvXcB8jTCIvw9G0ujUDXItX+jweUPVKCU6W9i84WSIE/ejRo8zQq2F5gbNK0zMSvndfxs+eE3Mei9mlBiR3o5MxudszIff7JiWe1CnJhN08CB46bZjXgtPiYgHkTaurE9eatWL0FndX5cxYkZk9kENFVMqH0apO3tSiOmtaAPnzHzsh6VhU4pfOS/LRAxqbAM5QiPELC4Xe1yfu1QuT00FGCWklTE8GxiLSPTgtA8F2Gb7fLdecbXLh9og8GpzhY5J1TRI78IIE/R5pCHmlpdEvnS0B/nuxCB4AR0ksjB1GkovgiqHrqhtly7CzG3uUghMLdZu5BKV4mL1BqHgRq2ba54MQ+h9P+C8WQVhIem9f4hwSjEzSgaByi8zrpqMLARME5sdVAtOk1MmSOXFboENVIK5hIaBz4/Nv5lba8d4516TeK7snvgAXbZyHLNDJQy5V9MWviA6XRM5JbqVxge/K58q5s76RzwOTFT4Gr6En1bwVigBmKhvboOYP5xf+nm/SUwip1i5xJJPinJxbULJmEQu6dD5OmCke7/YOqwWa/SyyyyYW6SB5C3HvxDazOs+LAlw/EIOQeU7K71q7JO12cw4Xr+d+dHc2msTpZMwJFq3OmVmp87OK+Qahu3sfMk6g4sc/vCWeB7cleuRlSq0x5+a78gVVFdgni1qMqGHRoU1PiENPMJamWsA1daXP4wFVl+VhdpKPN998k26V/+Sf/BM5d+7cYr23GhYB/d/9nuhT0+LraBcjHBGnxyON+yqXVS11tt3kTFJGJqMSievidWvSVOebI7VLXL0s3r37JXHjupixKHPRFgozPCOu9k66NUanp8QYHRWtpWVOt0yZrrQUJM94X5Xa9zsb1EzefKSTDDsvkwMHZ83AS69K+Cc/4GeytpEzWEdpqlRARO3Q+3opUXUGg3zPRn+veHfvqXi/xpMpSeBLT2Xm4ZJ0QzUMNTdnqS2dEhT/lqMidQ3S5nVVJbNcMAw9K4fCjBg6ZSAzlcA1NkiJXz7hyVr0l5CcWVK/7N8E1JwYpYJFQs6Rt7bkUs08cLE5OS6hj3/ARSWz2zDvlQ+4I0JaNvBQ9I3FZaoWMN/Gz2ptO0QbsJs3OYc0LxjOzGvgvEvC7MavJLXRGTElze4dO2SYi4Qcr5JOtcMhiZ0HaJCB9+vANSkezekOuDPOnnhOS8KLThCOD8g6Sfys91YhVNzGNGcDS7235IatKqcrz0SGslFY8k9NiNgdUx8zmNEFyVgB6T0s4dEppQnNIgDnGrowkETTlKbC8zsfIF10vV0kuCaGVZfAdgwktu0VR2bui507yMxs+xAkw3v9vPqbRZSNrkTgfKNDZDWAJH5mkov+SoCOOwyZ4FhLyV5GHQDHYXTydXSGa1jWcMJkZtUGEjYU6yr+OxTCDGPJZ+AfBxZNe9XW1pZ1v6xhecAIhyU5PiEbfun/JLFIjI7JwA9/JA27d+XY7T9OoJsTjulc9E/OJEgAfB5Nmut9BV0y2cUbHJD6oyc4G2cMDIhn08KlKiA+zs1bOTsHB8ypv/gfUv+Nb83pVkFi6V471wiAXUWHg+YglcCJm4TTWdRhk7OpyPQqICcC0fTuKk+wnD6fhN6ELMH2M3bySlf/CwWeR8+c5rYJvvq6Iitut2g2m3zsx2jCIJFj7ICZ5r8nIwmJRHVJGibJnAUPDFDcGkPp58zMNReXny0l3IO9XExRDtXYombeKiV5w/2srM/9hZuLMDrhFbGCR8fQPo9khurEc/96dtaj+PzfY15UOBwS33+CnbdytvzoQrj7e3JIHjoypj84R17IrkheJyKFubwHt5ShxGI75pmmeK+fU8HkcAENotsaZvfNcjgFESgk1SwKp6bs/02TckxGKtjkQK6BR9m5TPV6iuRRdmnFOFRpnoHnAekuB6NznXjvXsu6MVoAuTTaOimVfJIoJX1MNbYp+3ccc3nmWsgdxHmQdrmUrHHrnrLzRQiDxjntuT8t7r4H85PXIVcsGlb5i4sEFXifl+WL+Z/MsQ/ZNtxH7YUiLDgdcF/Nd4B9BkHJe/72KwOllGikkoIzliVkzygAInYArrn2cwiAnBavrzM2p4ZlC8xDh6d4DwucelspRSq8t+BaQRXGMpq3fGwkD/EJ+YtTZNP9q3/1r2T/fpVtVcPyQLSnVwJr12Y7R97WFvE0N0v47j2p2148w2spAOdDmKiMTaFrp8KuQe5aymTbsYu3czfJj6tzlRiD/YtD8sIz4qyrY56cMTRIQqM/epBD8hJ3bjGCoJBUMnHtivj27K2qKwcyaRZx2ARpRG5c3Td+ek7eHeSWcOesBM682TuQPGNwoOL3aIyNSvTM5xJ6/S1uo/D774rW0irpti4a4UTjhkyFEzIVSZLU2YkcAHktOrL1QUXmFiNuYamAC7k1F0bzkBsXKq8IwwURrlsFYDkhpoqSvLmdPHSaii7eIO1il2vh+YRVA/uvgoKQvmodc6Jmf6BCgTFjFTvySs5cHDumeQs0VOXx9zAxscdGLBiGQVklyBW7hWND3N7u7jv8NRbVIGuh9/5qVhqXBzgcIihd37CdnzPbfUHXz5kW10CPxPfNmtGAEEC+au0v1TkM5xBadrOqJXl4332Pyj9Q02aLFraOHYglTFe8RUjU40LJjrlbdT5ZCLBX0dNpSqsR/J5GcW10kCYt+P9ioLQxGpZkS7ukmtoovcsnvhW9XxidINZksbYXDHDGhiRabP42k7M3Bw4niQVjVJ5xoJNXrfEKihvonpqJGOeMjRIkD3Nc6JjPIeJ8IpfEDz4/n7ddw2MECnlpX4DnOwqy7oEe0SuZZUXXf7hfIpm1wUpH1VetAwcOyMGDB/nd+vfXvvY1BqP/yZ/8ydK8yxrmhWhPjwTW5Vabmg4dkInzynHxcQCduoHRsNzunpCHA1Ps8MA4A527gM9dkgSkplQXz7tFLfpcXV2iDwws+L2nDYNukQ6vT2LnzzKTDYQP4dogVXj++NXLDBMPvfGVOd01SEZTMzPiWlOd1TdIHiSbhQCiiaw4uEzakZqcJMEDCZ0PYC5TaVYezGim3n9PknuOSG9ck5uJgNxfs096bj6U61GfnL85LNcfjMvQRJTbqC7oZhRBR3Mg+4X9im6d26Uta4KHqq5djoFFHOfeMrNTpYAFAt3tilQFy81OOeeQvBCJAy3lCwALQixSq5X3PU6ATHAuLxbl/3t67vLGCtmd/8KnqorK6vg5RVjzF0+aRkkdFvKUui4G9KT4z3+igt73HaPcDkYs7KzFItz3kF/h9+gsFpxVM3SGDie37GE0QvDUO/yecxw5ldzUTgp1RiY4C8YfYAGvIiOqgz1uoxywmIXczA4QHrp7opM4j9dfFGQMhErNt4HoIz9wbtyAj/sMEkbEe+Dz8Zwt+CQp8d66zE6Ma2KUXXD8vavvYdVvGdKtYrOy8wGKAGmoOkp1kooguXUvZ0KfdWAmD7O083F0VXmMmWzSfKRSNFHCNf5xmFzVsHTgzHy9UowYvPY/qnAWr1updJ6SHMqqO3kILLfD6XRSqokogxqWD+D8GOvpldYXX8z5ua+rS5xer0QfPpLgxqWz0o7GlcHG+FRCErohAZ+rbNcuH8mH90nwLJKFOTNIEs2JiZLh5BV18UJ1NFxBlwvW/YnmWxJ59ycy+ed/Ju41aymrDL35FXEWqBYaQ0Pi6lD5d9WAc3kTE0WD2f1HjkniyiXxbNrCbDjO/V27siDDE0Xy5hIOuFWCgENyie+IJTBPfSjRhi6ZnNQkPTFOoubyhCRy4k1mUba6tcc7M7eE4EIceW/WPkRmXX2j6iCUkQFRqtlafEaynPlKNuPJgtMpkeffKlqZhiX+457HqxaQzZkej8rywg21554k128Xo62L5C1wXhE9mpgcf31ud9DQxYRcTXOzS1puH5QDyCbIJQbnQQgYqdC+Sry3LzO8Ht07koCMyyU6iQgTR4aUXQKI2TZse8xD4QsEz3v3qkQz+wMBw3qnysCzyAhkwJHn3pjdNnnHAwihJ4/EVIRMtqIkE2VnsuZ0phEenYjzGAPBomSyGvMOWJFPT4hZ4YwKSBrdYrfmLpRpeAMDoRILdL1jDY8X/m1mu86ZSdU0yqmsEPF8uHvvKRdEkPfxEXbsja714um+I3qJDlrBzzI1zrD5xQI6rCzazAfLrHCG8x0ul6rTqfEY08YG2UXBdWBJ7Poxz4og9Gqy3phzOCaJXYf495Az26M6aMzT/0h1egNBiSO7ssh8dA0rA5otaiQFlYyhFDElFRSWYqCI+/FKRNVH8fr1izd8XMPSITE8LK76enEFcheOICbs5p27IIEN6xe92wJZJpwyRyZjdM0M+l1SF5x/ALh3246cn8GARB/sXxjJwzxeXZ3ELp5n/AFIpG/XHnH4fCp+YN0Gca9bX3A+DjCGB2luUi3QyYMktFBnETJO5Malhockcfsm3w8IXjqZEO+efZV/Nrip6qkseYvGdHGOTsu9mwOipxwMt+bjTJOmKEldyS1dKV06pyYkvee4tPk9BecjnxroupJjvLC3YPejHMHQpsdLzs8xcLuYmxcW24iZyVugFHXNTJvs5KGCv6zhcLBCDidDzBJBjok5QxBYzKZBtslMvSLbFhEKuPmmWtp4k10IyYNzp//CSc415YQTu9x8XuwbLkrT6SzRcSaiJIOYB8MNHotTyGpRDCApzYDv6+ZF5Q4aDLHqHz2WiRVCnueNiwy3tleBMeuHLqeVcafkmpMlzXmKweoSM5S91OPQmYZEFJ1pl5ukl8cYXr+hmcdUqWO4oBX53WsSeemrlT1+qFfNkWIu0UboKZkr41KJbQ8ZLTpolvEIyHViW+51MLl2czZEPEeCqevieXhHokdfUVlXLjfJLZxhgx//oOLPbA9BT2xfvFEUDcfOIs73PSmgIAIjGKuohX2Acx/nCGZxA+c+YecM51W10uSSr4tiRZVB6Oye4jjIHCdpKAfwM5xP4Wlec3C8sPjwNN/7HjMoc1+9cd7RJQsBilI6FBUAinxd62nqNKcoZOjs3KIY6US8DlxTnyL32opI3r/5N/+m4if8R//oHy3k/dSwmPN4eVJNC4H162Xko08kFYmIKzTXMny+5A4Zd8MTUVrgQ7JXH1xYu5v5cHnOliB5ietXRSowIin6vMi+wyIMJ3frbEXVs3a9xM99kSV4ZjIh8bNnxHfkmDJOycAYHppDPiuBE3LNyblyzdToCEkrZvG8+w5I+Cc/FCfs6u/ekdBXvlZQqolZOGxzErm4cq+MJwy6Wlq/sxwsO9MuCY9OiqCqmrmB4VvA65bGkJqbQxVT6+wSX+Dpd21zD3Yrh8I8OQa6H557N0r/MUhaNKLcAYs9JOPeWLKLV2whkTZp2c38PS50J5R5SZH5vuUEdMpQEXcmE8qJ0OFQ28HhUNECZayudcQ0aC52OuYddmzo4j/3Cd0v7fNoFnDTRy6eGW4QA/N12A9w3oxFJbl5l7j7uyVw9mN27SSZ5AIl531gsdC5liQGZAVEz5LdYcYThMawFhYWMA+IAHQQLUhzcdwhwxKy3SolZ8xhBMkr11FDZxodO0gjWzuVmyhmP3GcNzRzP1UDdNIQF2HJI/H5MaNYdOZsfISkEi609v3A91OBcYjeuUbcQ72SaGxhdwXyWrMhb+GFEHFkIaJrvFnlzQHo1uFYxLZ2dd+jKyyAOUAE3jvHhsXM/KwcrFiVxQzehlQW72+lw53p2idBvlMpHh88vjPXNpBqSOT8F05RNlvSFbYKMIKmWqlmZh7PAgtS4yOiB+vEc+eqJDftmJd8toYS0JMsDOE69NhdKhHTQnOrhuyPkms2SvDz96kuyRYRzZT4L36mHJ5xn3V7eKw+TUS/IpL3h3/4hxU9GRaLNZK3PBDt7pGW544X3U+epiZJTk4umOTBKRMumZMzcYklDZpuVCvLLATMpwH5JiWu9naJfjKqZurmWR2CfBEEDs6Y9vepzF26RO/tFveadRJ5/10xIxHRuh9l5wJN3GDicRK2agGZLBdf8ViODBRSTa1NXXTgYOleu57h755X35Rp3SmxSITduXgS3Tn1XTfSkkqpThy4HILjYXSiaQ66WIb8btE0RQ69jY3SpBmSCnlLVupR6XoqYOhczBUMfLbkGAUkXpi7YYelhL09K9YgeCVmJLkghKQOHbs8WWK2Cl0EqDSCLKAjBlcwdEOQfbYSYGKmECSnfTUjBLDwxvsv/4cgzmExNmwXT99Dkih3/8OsO2U1AIlA16AQwQNoTX/1LLtE0efe4s9Atih/dLk5mA9zC++NC+K/dlYmf+Y7c54DZAFB5zgWrEox9isKBMjTKrRAsBw2U5kcPst8JTUPkucoUkCY81nRmbZIHhY8mdfGYhYdl4pNSNKmktBilmlyjJ1ozCnqa7cUNOVhh0TTGD7uGuzJJXkVRhGAvAW++IixAujiMVqjQJgxQ8TPfJgl4/hM9u6rNjFMt9Fs1hwkuw9uSqxCksf50Yzka1GQTrNzlHoKOnmQ5FrGVTgO5kgzIalds4lzUSR6+47NX6ZqA6Mlqix6YR7P7uiK9wF5NpyNMasaLxQNU0Pl5lbXzpIo2zu2VLOkTZL/xQQyQJF5yZnjdFpih1+ac83lfToz656Fxyuxvcdo8BU9+iqvF3DfxTUwvvfYU0XsqiZ5+XN4NSxvpBIJSU5Miq+juOTJ3dDA/DyZ57gXunVwW8TcXSqVFp9Xk5b6hZO7ciHkDlT6W9vYTZvvrBrkmmYiIZ6ds9VfC+71GyV55xa7aAg5d+3tksSNq1mSZ0AG2179PF5+KHoOyRseEufWndyWmGWMNG+QsFEnkUFDksYQ5+cAjMOBuJHMOR3i8bqkPlh+Tg4Vdbr9FXsAOpqTYxLfM+sQuFKBBTukemm3V138C+XfQKZXqJugaSqLDFr+It0GLvps1cHCbwJGHPXiGumfE6aab7pSyOQjduhFdl7850/yx7FDL8hKABbDMI9BpwgLJyzUPQ9vl/07Ok0GQuzAuPsfSfi1b4r/0ueUWxZa2JcC5bZNJTpFTiffJ/aNJZHFe82JT3C5xWkYktywTXw3Lyrbfdv5TqmdmSaBYdWXMs0LnBErNoNkOWxa5yCPMziwSnUEHtvJPVZZsC+2A2IHrM+Y7aBBWot9hPnTti4SOHTDisUL4HxgWHfHarWwcjrFYZp0Kk0V6Ei5Jka4iLZmIC3JKCNiKowiABGFsYY2Mabm8YpkXOFx2O6Bz9/jYg0znXrXelVoATmdGJX4rsM5Ek/fzUsSAxmvAMhVW1SpYTKujukVbuqAhTvuKaYtCqYYIH2L7T8h/kun2WHn3N7EiKA6Gd9ztCL3XgtQnPAaiWiXKoD7mz17Ec633lsXqRhIbt39xJxmVzxSKfFf+ozHA4qnCZvjtGt0gN0xOKEuFijFv/QZizq4PoOkoTOef44i95ZxPHmgadOG7XRcxvUJ53exwtzTgtqR/RQi1tsn/tVdJbPw3A31ok9V77AGSWDP0Izc6Z2UkYmY+L0uaW4o75Q5H6mms0B0AeDZvEViF85KOpmc33PPTIs5PcVIhny4V6+W1NgoTVJ8h45QHgqXT3T0+LdD85vHs4xOEt6AjPYOSv9omNvxUf+kDN3rkQvDKTl/c4julT3jSYn4G9iRa67zZp0r25qUeyVksJDDej2VhYfjQlvUhS5TcaOcYoUPmuMz+s9+LHrHWtWRg6Njofwbu+FKHiDtwrxeMXB2LFR+0YebHYw9SCpLma7YoEw+OkgQ0f0AScWNaKXM74C44POhk0I3Q3SJ0BXV9bJSTSLjqglyCxMW96Ncp9lKwNgAmyyr4PsMNczOyMFNb7g/K2Xkc0CamIhJ9Phrma5VnrQRxiFeH7tVIC84ptBBIikt9pp5jqsgJ5z/qBIqEL3CTh4701PKNCUSzvmMlHJOjbOD77t6lgHPxfYTSVZLR6YzOMrnRAcTcr1CgNEJO2+YgWxuy55P2syEWoxVuKCGZBYB5iChpYyH0PGNvPx1EgYYpFj7Ae+TEjwbocLiEPvWc/uK2v9lwG7rIpI8znKukPO5FFzDA4p4V1iEweI6vvcoZ7SwDYz2NTx/fDfOV+amm06zYOF5cIuL8mI5iwWhJ0mu7RJ7FgQysvmCeac1VLBTTZIl3M9QNMG1hOqVzO9QBMK9lmZLiwReCzrWSHLLbkoucb5jLjofNIkqMlenoxjnD4qn+57EUNhCFM5TjHl9ut7eXvnud78r3d3djE6w4w/+4A8W673VME9EH3UXncez4G5skNhA5Q5vupGS8am4DE/E6JYZCnjEu8CZu3Ikz7NhY8HfedZvlNToqEROfizBV75UVbxAOpUiwUP2W6G8OnQKQ1/7pjiDIUVaHQ7O6CW7H4pv524xRoYksLm8hXXKNCUSM9jxnI4kZGImwVw594QpWk+vTBvqAuSamZRmp1ccXo80ezTRlqCiCJLnLhGATKnmCr/RgeBhliqxeRftkrXwlOqk2WU4yL8ZGZBInoGDHbgpBU6/pxaKBartII/FpIB2YDEb33OE1WsO9GfmPThrVqCTh5/nm3yA7Nkro8sVkJY6E3HmzKE75tCV+QI7mpn5xFKLMhBhED10MC1JoHNqjPJHHJcVO/SBzGARnT+7VQBYaLsGusVhGOK9+oUkN2W6+nDeu31ZufA5XZT3BM58oLqUtnMEi0ZH5vUo04T5SokFLxaYdgtvdA4hF6sWlCSieFFAClywMx2q57blPrBtR3RcfVe/EBno5nZHJ5IyuAIdbCzWYHpCqWgywW4cju3A5+/PlTbDqAT28xmjEkpvex+qc7LCeTwLOM+Cd65Iqq6pfOcLx1pdY05FnwvCPHkgjiXIcdG5xDZhJ6mESyMq/ZCMLhbQdUiF6mWlwzXSJ8kq8yxxbseOzcpkjfYuzs/SnKOM4ymuMTgOeZ5V2QXNRnbkFfZQHDD9szOENZQHiDbuoYgAwbUTctcEnChh6FTfzN/RhXhylNcenI+u8dwol4UAxSh9zYZcJ16YbMHsyrYfOeONufZCcDhoroX3X21m5jNB8t577z35qZ/6Kdm4caPcunVL9uzZIw8fPqTd+6FDh5bmXdZQFeLDw9J4oLQbmLs+I9csAexTBJdPh0FS4vx3kFEIizeEXkquqR08UvT3voOHJfLhexK/dEH8Byu3u2VmXMoUd1fxhTrm4uzwbNjEoHJ0EM1oVJyNuZVdGJ2gwwm3ShifhCNJGceMYiJFh1GMxqHrBkmrr7VFAlMD4mvyKwIZ7hdnZ6eIb+ncpyy55nyCvVcKMMuG7gIWkwCkZZD22Eke5rzK5d+AgFFm2H03x8xhPvM0IDqJzTs5jxI78Jyy0k/ExChQYYQTJeZXFtPg4XEAZiLeO1e5XYMnf8LjCd0bdL8w90WXxEhpkofIAWbVofvj1FRQ8ZpN4ghP0+0ydhRymvLFD7ieWlbupYD3g2KAlyY7ae5vSItAstHNosunRQ4wrwGp2fmTEj1Sl3Xjw3ICC0eavGzdU9a0Ib+Th8UlDHyqBrqIOI5A2vI7TBn3TjvYfUOuH7oltuMec1LoUCV27FfSyLoGFkbySR5NTyCxBHEGkQrW83qh/qZRde1sXTaQmGyXBOdhSycdGPk8k+Oqi14hVNxDizouim2OWEQRNswXokDm1CSBWU6PV1xjw+wo52+/2NFXxXfpNDuUIPDRE28U7i5msh2rNfkoBV4/isylYVs6jGRFRaQnCuxLdDibFzhf59Qkvu+EBL74kPuaZkdFznNc37mQn4fMlXEhBYoLy347Lzdgnr3vIefX0lD9YOwB98LMPoORFe+56MCD7LV2UXK9aDN56A5OYazkaK4TL+T19n0MEyAYNQVLFFOcmqQ9yzd3djFRddvgN37jN+RXf/VX5erVq8zG+4u/+Avp6emRV155RX72Z392ad5lDRUDxMyYnhFXfenqN+SaeFyhYHH8DLlpD/un5W7PhPSPqsVIS0aWudSAKQqNVYLFF07o3gVefJkmKdHPTore18suXdnnDs9IGh2drspnYTADmE4kOafnbGmTcBwB7xG52zNJieXpq4PyxY0hhoVfuz8mPcMznFNEWHhni5JZQmLp87hEmlsk7XZz4cOZEWQmlZGXLRSl5Jra+JCYCPpd4TMi7NrZ5oOM5g5WcLPh5oYhnkd3qeMvBywMueDPC0anexyq/lUY/hirN9LMAwsZVEFBivLlmlj4QTJbbWV8OcB75woNNtD5irzwZUlu3qlurqj0JmIkCCXlhXqSOYDslnBWrEnJuZrbSRIhIXZ335v7d6mU+C6czJHkYn6rknMJHSssRNLiIPmAXCd6+GXKQ+G8mTWTyABkBp0surAhfqNfFRRAWJihl++mWQjW+ZV5v5ZJCDthVSIbwWADzu/gxz+cE4AOGXa+HJVwuSR+8PksOUXVndLOPND0hF3ZTLA7zaNUxRznGyVa+TORdlIGI562Veya5scn4PjgNigBLOjmEDXLvv/qFyRp2H9YUII8Yd+CvEGuxcD7AkQEpC129BWV59j3UJyRqRKxHg2L2ulhfEKBmV6EtKOzihmjokHdywQoiPCYKFNMqQQoBmC+DkQ9+NEPaGiEIoIduB6AxM/XtKXaDnINhUEzE83Fawq75ihW2oojOAfRQcN5DVMeyHlxrtHYqhJJbiVkHdexvPuv5cRrXwvgelabs5wnybtx44b84i/+Iv+NgORYLCahUEj+2T/7Z/J//V//V7VPV8MiIxWLidPrEWeZ+SrM62l+H2MU7MDcGGbF7vZOUmJohZiD3D2u7DRzfFy0pmaGnhtjxVv9iDUIvflV0VpaJHHzukz/5f8k2SuFFLLgksmK5upAdrE9sB1mmrtk8NQZuRfzyNkbQ3Ll3qg86J+SmahOQxS4WbY1+bOzc3VBj3hcBW6CTk1i+5+nvM177VxmYbrENyAQE+y7QjNqQ30r3s4b8jFKR+ydDRduRm1c8FnhyEZzW0XSPyx8ccMi0VsEEwZ06NAtAEnEQje/M+ABUdq0Y8XNRGIxCrKhr88YGrg9tNVPbtvLbCxIIOEEiYV3MWCeDd2+bFUdcQNw6ZQ0rdLRlfE8vKXcSnNee5idGm/GWIQ/mxwpfy6hyosikpWHhWJSPMpIgtjhl1WVukBmIWY/jLZO8V37QtwDPSQIIPBKqlTZddHKEyMYCO6f17wKZkJBQLJIp8V3/RyPd+/1cyoo2vq4DS3cLjnGMvmgA16cXbh8IE8P83hZIPA68/wkeXBPtc22USKZR6z09VtpU0+JaUYeRXn1mQ/Fc/dqyc+qOgZmrjnR7cuUZoMsRV74CjvuDKsH8e5cR6KJgkpi447iRAQdv52HWHDRhgdKmCwtprOmSXKaQ7gxa3b3Gg2KIOuO7TsuvitnVMbhMgWKBovZBcM1NXbsNYk8/xaJAT9/JrqCr4eiSiaSpfonNzMOqU9P7tmTgpq1LUG0M87KnrvXWRTivRaSchDBvILp/F5/OBuHYgeVGEN9PL+wrzELPycL7xlG1SQvGAxKIqFuuKtWrZJ792arrKOji6e9rWF+gATTXV+Z5h9h6ZZkE4QGksz7fZOcu4OhSlO9V9yFiMoSw4DpSkOjRE59LMlbpfOcnD4fM+tCr78lgRdekvil8wW7kxZAAtGZw+ydfX4Oc3ND49GsIQq6dOduDssX1wfZrbuVqpfITFSioUYSOpC59uaANNZ5aYICk5SKjWc0EL0TnF3CIuZxZKDR5Sq/mwdXzdFB5pOtZFAaQhOA3O2vOg39amGILl4Vlvzo+GEwOztIbrlAlnPWzMD98BZd4NhZSaW4jxO7j0jkpa/lkAjcuCCl1VevMAvvtEl3OgZUF1lIo5qa9vjmdJcsYCEL90oGltv2HUgC/gb7VJueVAHmeVEMyGlD6Dhu7qj08wYPR7UyJA8yR8odYYgD04XVG9T553QqeWCJEPbk1j2cw8McynwytZRpynQu6ZvHYj6/k4cuCACCqrIeZ4kvqt5p7J8ShXRsN8+9azTTyKm4W9EJtm1CgofHQMbo9fMzZPcvlQlzu6k4DtD1RCUeHTYaJJ37hD9zjWSkpEXgvXlRQh/9gHOyUD8ET71DZ8/I82+Kjs53XmGEncuBbuXuihzEUkDnuLld3EWMlpyL7awZjSg5tm2Wkh3O8WHOmlEC29jCrjYk3oWKck8ckPfj3CxxnswbHq8i6qs3zh7DpqmKKpV0y4vsQ1rpP+XmGo8DiCMpJZ0G2B3nWMSshBsFtUodNvMVCjmvn4lxKebEi3s9ZNgYPVnM83alo+oj/8SJE3Ly5EnZtWuXfP3rX6d088qVK/K///f/5u9qeLIwpqfLSjVzYxSmxNPZSYIzOBYVl8vBzt2ThDkxLqauqsWpyYzzXgWgW6bjghj9fXPiFVIpkzNzMKWRdZtlcCxCSSU6daNTMQaKG1zQzy42vR6NuX91AY84mwLikGMS7Cyc11Q1NE3i+4/nVCyXErzQQipoq2hCasWL5lMg1UTnKB+Q0nlvXWKVHDcd3uwrBKqQIBuB0+9y4YiOiDPPhrsYQJwRBaCqmte4sEfXAHOCOTN3sN6/fYUzXYtyTFV4E4XTIudfFgDI3HBjhQV/KYBEoTKfY9ABx8qHt9gpNX1B0dflyvFQrUXuUmLLHu675Lot7PjhuQgzxQ4tDXRcLrUNN+9U+7eEiQY/PzopCEDuvst9iWMEkuWK4HDSjc3qZFULEhAUJFZvzHHYrFawiUKDIrZpEmU6DtL4xUGpafD0ezz2rBkVEG1a9xcBJeNtXZQKOhCXkDGuAfnDdcNehLKktCRz9U0kU8EPvsvMPBzb+W6WFhxmSuLb91Pyitk5dHxJ8ob7c+dp8t9beEp9Nhi6TI1LbO2mkgs4vF9kaEWPf6mic4rXCDspzv7CEDfIYjzKYxCdgaKBzuwgTBV185v9LHOdNXE8oNNp32YgOuxIoBu5zDoSNNRAbuASqg5gcR/87B3RZyaVvL2uft5B5ThmalLNRUCBOJJCwDmAL3unNzuXV4Z44ToA+XXkxa/M3d+GrnJGi5hq4X7muwIDrZ0FY12eZVR9psI9MwzzChH57d/+bf77f/yP/yFbtmypODS9hqWDPl15J0+rq5PxgVEZDHbKVDQhdRlb/icNfWRI0tGY1H31GxL+yQ8lbZoVOWiik+bbs1cS166Q5JnptEzNJGR4Iiqjk3FJz0xKR++g9K47KvpdVX0GpfN5NM7PeVylyW01pgEVAcO/87x5VQtU19H9ANGw4O7vLjjvsqIAE4BiZgZuD+cNEU2Ayn+1QOcNi2gsaGEG4rt1kR1RLhqKEeNUisQSM2rWe2K2D+bHMiYf9io+ZvzsxhVLCQzFwySF8hl0sRbQwYVtNSWmZYDXcJxLKAt/dEFNU/znPuZxDydROlfmLfCxEFbFDxjdqO4pFmu0+He76bDGhabHyw6ou+c+5+kqWcwhFBmSORjgcMHicmVJV2Uf3KUkhPOAVXTANsA+gPmKMzYPWZ7LTYkrOmIwpkFHE+QGBQV97WaJ79gvvuvnJfrc64roaFrJjiG2SXLVOroXIgA+mjlXsh3y7AcwKHU11q/jOcUw+I41ksax1Noh4vGJXkQWiteI7ztOMolZy6xBUlsXCXuy0L6DvDEKA4U6JeOtQHIHEkLjlwoLVzgf2TXLK0IEP/1xZlF7iCQZhQp097F98xUDkLGhg4AYh1Lkh8ey3VnTRFdsRGUt5gHd7QBMfeBSuozcH7PxGEsJl0sZI926xOujvoCgcly7awYrCwezMosUcHLgcEj02Gu5uaI0fouVLWah8Ad1AAsfecVUnCec5y2iGtGRR5tKiV7BzP2zhqrvVv/8n/9z+YVf+AVK4gKBgPzxH//x0ryzGuZN8gJrSlfpU2ZapsIJGYg7JNI9KL5126WpzluxfT+MUTAPtxSA4YrR0yP+48+L1tgkzro6FXnQ2FQ24iEc1SXqaRZzbFoGL96WaV8D5+nweeEK2jDcLVowIM1ru1bc/NNCgcUwwqZZNW9o5gKRAclYnK1gzJoAFD52YbcM+/t5kWnMTaGjh6/2VRzuRiEheOptFYOAWaE0tu161Y1yOHijwva1k04s7KKHXpTA+U+VdNPt4awTSHfs4AtLsoiDLM5//lOSeyzKIFPEAg0ZU450mnI5zCMV66CUBKqqsIKv4G9B2PB50d0EWfM8uMltB0MNEGhkZc2ZgYMD4v7nxH/lDFWGDN1G3hry2pCLhMiP9sw1zuGUxPZ9Evzw+xJ97o2S7xl5cJgBgxwMsQGP/RqAokOwThmQNLVxTq7iLmIesC29mGdzaSqeYGJE3N13uY+Rr5j23CEZZhcOZMlIKvlf/iItnRbXwCN2w1ONsEAfzGzf1VxsIdssn6RQRjs+TMdYVu1Xb8i4j75ceObVnlOWFx/AGSxEWMA5saC8MVCVwQc7v5t3qcV9hkiW3o6NrPSBhFrzRpT4OZ0kdVYBIFrXoExBpidov27vEoIMolOJaxEiI4oBxR677BDvkaSvwMIZXWmYXBQKen6SQJc0/hi6i+hmotOPbRTfe3x+T4Lu7+TYokZgPKvAdQUd/IqQdz+j+UoZuSbODUjz4zsPZFUWdmD+upSUngW/MjEczyqq1giNjY1RprlmzRpKNS9evLg076yGeUE5axbu5IGYT0eSNAy53zclui8kPiMu9cHKCR6MUGb+6i9IxpYCcMs0Ewnx7VM3EpA7ZObZkdRTMjYV4/zcw4FpuXpvVD6/NiTnbg7JtYfj8jC0RqYuXpSpSJKulpifC7lFAkOP1I18ORI8w1ASrKWCpqnq6B2EAKcpRYIr1eOSCS4VVLehuDwDi9VCC8hqAaMLECbk1sFEJb77iMT2neAiGM6YIE1Y+GJhku/OCGDxC6IH90ZPz33OMkWPvlpRd2Je73d6Qjk/rlqn5sDSaTX3E6zjwhLv23/583kZPGChR4JXyeIbXR4Q5IFHJJ6QeTL7z+FQN+4iCweQT1SEsVyAxA9W/MjmolQT+9wmybEINUhzsfku7CPMcmHhn9h77IktnNG11TCHtoCZPP6tx0+ZZnyH2paeh3e4XUEcETBtdKxljAXIV3zPMUk1d3B75wMERS2kj4k2Psr5Pco2IzOUpdrlhYxYCDUoo4wjr2SPXbqPbt0j/suneR0rKJkrkFOW7dqiQ1hgOyh5YxWZcog4mZ6QxNotJL0VweWiEYpmcwlFMcHTe5/dahjZgPTSkfPIy+wmw9jBfpyhe4jOGzrzOUgm6DJYrJOnjQ6VDHlnlzNvFvWJAhmJsKZHF32pAenxzoNKyl4uD7LYU4BY0OBo8SIwnlXwmj/PyAzIuMvN5KE4ClURiBxdfvPmUR9LB/kpRdUrPISgDw4Oym/91m/JuXPn5PDhw5zP+93f/V3m5dWwDOSaDXNvjCBGvcNhudc7SZORxpBHGtqbJI1IgSrsbfVHDxlVoPfk3dAWAemUIZGPPxTf9p3i9HoptzSC9RIeGpXx6Tjn6G48HJMz12CGMixX747J7e4JGZqIcv3QXO8joavftkUa0glpS83QEAXATA+c0hgXsMwAcgfZGoiC79JnS+ashiozFlSwrMeCxOgqX+l+bDB0cfc+oHNeoYViQaQMZQLwGLqRymlPLXjRHUEnhYQJC95DL5J0QPaF2cBiRjokes+9KbFDL7BSWc2MYNXvl6HgLSS5XCyBWNnm1XDDTGzZpRbmVdr4Y3GaaumsqirvGupXc3bIZcuoAIq5pVnAdoR7IiSC6ADCGAQECdvcHmKLRTbILB5nmZDMfc8DorevoptlSafJJQYW9a6xQdviJ5brHlkhXONDDNWmiQzmLKMzlB5i9hMdMOf4MGcV0XWCRBKLpzldQ7hyXj5N4otrA0g1HGTxfCCQ+WZGPKaKGA/hOejuiSJSgYJD0WIGigBFyAzD2asgeYxjgAS+oYkGOZU6l7JLPNKfs+DEvCHn8BqaVdxN5rlJhqfGKQ8mzBQXpSig4ef2SAg8DwspkB5npK52RQHIZKnuBDumGXfg5QCX5a74mAqDOM+rklLngd3ZWnTCwpFKsXgy321ZLiuPIxEIOYcs16nx3oSinAVcj1hwqsAZu4a5mNfZ2tjYKH/v7/09+fDDD+XRo0fyne98R/7sz/6Mc3k1PDmYsJFPJEXzz5o7WK6ZIHcgSYhEaKrziaY56TDp8HolHavsZojn0rsfiW//QUk+KLyYmi8YvP7ZKUlGojK9aY/cfDhOMndtNCWPbnXT4fLy3RHpGw6L0+mQ1ka/dLQESOpaG/x0vMRnIpxOztv4Lp9R1X3TVN2TUGN1leHHNCcFcoeB4chLX+FCKfDFR8pdbBGyZeZUR7ft5RwP3MYqdYpcUpgmq+VqBmaECyH/hU8rcpaD5JGkvYzZxmKgZHyCw0HSFn7pq2pmZxmAC/IyxzpIPrpJWNBXDMj7YKtfBbHGYtWhx9WizRrIz2REGuWyr9xuEmcGb0+Nie/GhTmzhC5GGmzgzBfkenPcPDPv2QyGlMx2EfK95gt2rhDbAKdbzJl5vIwvqAbsDsEkyqVkfpiPgzkNJcs0dDrB4xXGNY7MeWS0ZBZOtmsKM+JAUDKdbrOxmZIszBwikiG/y8RCR55xiB0gOrze5p271lxlMRQleVV28hQprFOumY2IjqjM7ZsEOOOCi+sPzGBQEMRxBpLBRedYZtGJ2KgDz7Njj04fZpWYC8kYkFX8GaEnlVqiYzUJIbcBFqkZ0oz9j9nEfCMWOxBAD0dk5owtIVyDPRUV1oq5Gy5XUBZdI3mLsh1575unQ6ly1yx+DOOajSxZS2GlrgezRRcUjhCls5xmU1cSFlSS0XVdzp49K59//jm7eB0dK3u+Z6XDCIfprGlZ+WMWrX8kQnlm0kjRNdPqbFlw1tUzILwSpMZGxeFxi3fHLklNTooZWVjHCY6XkI9Cdnnv4y+k+8Z9GRK/XJ3S2HXU9ZRoTU0STEakpQEZdEFm0CGyAESv5HM3t3E2BflMqFZjgedIlb6pPm6A1KBDgVkWuh06NZIFmIRg0eG9oQLTFxO4SUN2uOgmMvMEKoTQ4iNIG1VymJ3g/cF0oFxQMhYnlJwuNTDbgSp2MXc9C+guLZMbERa86PSUJf07DqrF/vRE5YG4VRoGQQ4Hd1K4K2afZ2qCBLMSZ9cUnFHdbs4CYT4vR56bSqkZMhiA+AKUoWLGKyf6Ylq5RDJL8UlXg2lS0pnNb0wHCkSblIuuuH1Z4nuOKGOa6UluE7s5BYgdnhOOsrCf55/5g2rGK5PT58xkyaE7nZ0fRbYVpLw7D3NBn0MuIIUs4A6ZAxCdtlXZ17QAIklyiEy421fEe+NCDtnEecWMPpBDOIai6o8OGQyVQKAqhDMM59T67PW/WHRHPkhANbdo0+Pivn9TdR5sUlX7/uKm8PokfuB5RojYrwtG5zpxDSqFi6fnHo/J5Na9lCp77t3IMQAB4SaJLnW9cDjVa2fkvUsBkFKYysAevxxUBuIKInm1EPTHk49XBjRBQk5eIcVI2mRhKIkYnQyU4/GwMiYaG2InvGwcSg2LS/I++OAD+bt/9++S1CEYva6uTr73ve9JT0/uxb2GJ5CRV6cWMbphSu/QjAyMhhlkjrm7QjluzlCdpGamK5ZqutdtoNOlZ8MGST7IDYuujNQlSOpuoFN3fZDB4jcv3pPo1cuSaGgT57pN0tkKMueX+pBXvHUhcTrSosE4oEqgCoyQVYeZluSGHVXLf5YauMGi8zPHrMDtoSEHSA5sgauV05UDKv3MJlsGoEGCPcbB4WAeGarogVNvi/+Lj8R79Yu58y56Ug2DL2IobzFkDUJWyGyHIseOiggUZI8wLmF3t4LjTBsbrN6sB46Uwbqc6AEl/apswYhMLiyK0ZmCvBNkJUfy1tCclciCsEBqZy22rcfgOXj+P2mSx0p1Z1aGR4fNKuTZWt8jkijf7SuUIyKcHUYy9jljGLCAWKN67pyZyMa0YL9h0UQXzcufkzQ7QOxshB2FAZASnH+49iA2JHsOoIhRZp6Zbqd992dJnE2miIIWumvoCObIajMSLQScBz/6vgQ+f5/qBkcsTBJcKdT+zZC8xtaK5/JwTKTdLnbwILEEUaUZUGYWLGUtOm0FN2ZAwglwqCfbLSJZTcTFkfl8mDGitDtYx0gVdits51GpeTwLqVZINjNdTjNF1UMxSXK1wD7FtRWfoxwhxmOxenhcbtALhqG65cvpfr9SQeXGQsg95iK9/oLyabguM1bIfq+CQVWoQRW671zldX+lewc8SVS95WC48rWvfU1GRkbk3//7fy9DQ0PyH//jf5Q33nhDnBWad9SwdBl5mMdDHlz34DSjA0iU8rp3dtC9sgKSp6SaD8W9XlWMPRu3SPL+3aLzfJgBRJcO7wHh4jBHOX0VpG5YrtwdpewSOXX1QbesnuoR1869UhcZF3PNhiK5UFNVbYvs+/Z4OQMF5zhm7izhHFRVQAVruD/HQCIHkF3tU7mTtL1fTCyTblOpaiskephfw+A95FSYL7LPKrqH+lT0wHxNdDKdAnYNYCJUQhqrhs5XTgVbzU5VvrhBx4HZcQ9vl30sOxDzCEIGebB3Q5T0q7LqMLtQLrc4k3Huc0jlsu9nsJsdFDuwuIaE0dqn7P61dqoYh2VA8lJN7SoSAsHqgaCSblb0h4YEz7zPWUoQNBAaXNNyJMKUG/axkEO3R0ifIKHEfq5v4uyv7+JndMbEVYDnnu16AFdJSmKb2ynfhIkLOnr+S58xC68c4CyLAgPD5m3Ey5I3QuqI6xr2D/aLhcTGnZLYto8dfUYRODV2+OcQjRLdfXv3GvuZstgKQphBiPGeQZ54rHl9LBRkt4nHy+gUdJ/tQI4mpZ3W9Quy7c61dHHFOUJChPgfuIS63bPdZQSK47pXwaBBxKwAAQAASURBVPEPmS0eC6KOmV/Op96/WfncchGgEFD//f8qTj3B/evue1Ty8bgGcqG/jO4dpYDzi3OgNXKwIOB4wzkE6fBCkPYXNl8p5h6L65b35sXHGjH0tKLqFdJv/uZvys/+7M9KU9PSuMLVsDDTFcMbkIf90zITTUhTfXnXTK2uXpIPy3fkUqMj4vD5+Xj+XXOzOFwu/tzZ0ibRuC6RuCGRWFImZxIMF4dE1EhlFlqaQwJ6TNqHH0l69frMBdiR6cgMi961nm39QhbckPpQurOAhTYrRqgKL5NCBKpUWOCV7A7BxnvjdmZePZVgwPEYM+UK/trrU5XwxhYuRLz3b9B631rcIzR3vkDnylqIYjYG+wGzSTRCKJCDVcoafVmSvCpkbgAcQQNn3pckoiCKEWc4wGIuq9wcXbHnP/sxu27sXuF5Giof5McMIEgBZjMws0q5MY4fZJlljgkLOF7SrkxsAwK7UdHHfNPty8tDpgxHx/pGGqGg6OSeKmBiBVOUS5/RFVNfi/fsEP/Zj5X5x77j/J7csouzbHajHw9C40HaQ/UkKsh442zY2s0819AFZUfG4xVtcLYLZcGZ1ElouG2H+vj+0E3H39s7UUUBorNmo3KZxVzc9KQiJo/uUJYOSTPuCLG9x8R/6bToazfx/MKiXO9amzXi0VetJ1n1XjvHcGNkIVJqaqa47fSOtTT0yV7PLTmpdf/A+0BszINbynQoR+56hTOMVleKuXqYb0R2Ie4R6bSYdbnrGyw08/P8sJ1w7bB3IUDyQKRj2Ee6zq4ojkGY38B4CB0NdG5VoHgFs8QuN+eOA6ff5T0SKgfvtbPcvvZMMPeDmzyfzAqII4oK6JTiXIoffklcvQ8k8Pl7qihShMRRqmnFlqwA4LpQm8dbhO2I+Bpc7xdIlk1fsGDBJSvlLkTybl+WKK7tK6Sw8NSQPBiu1LA8MT08LjOtayQV16Wp3ifOCk6OSjt5lGpmunhWpy7avlZGvrgsw+t2SzwBUqfkLB63k93DBq+X5M6SiXruPxQtOiOO6+d4Q9HXbOZNF/IYDNraM4TssFr3C0HZeZLHDNeQmiOqpMrMDlaJG/BKBSVgLk+OU2Ix4FjxnHqb+xHSSWc0UtKZsRRA7rCojDz3ppJkYUE7NsTcsfTD2yoA2er4wiCkSGDxcgW7J1VGBECehiIKFqXFzGOygbTzsDRH8Sa+6yBDo5Obd6vKcBXPA/KNBbu+YTvljp7ue5L2eJTkrYAhgL5BdfNAFlRxyEGSsBw6eYC+aiPzoBLbD8yVayKD9vR74u6+o+ay7nRwe6EzFzv4fNY4BuYg7OpYMFMkQ9HDL826mmbm49wP77D7Pf3NX2D3CRmK2CY6Z/tm4UhGeawnt++Xuh/+ubgHe+geq6/fWvln61wnwXvXJRmLivemmr+LvPTVnIIWiDiOB21iTJKbdvCzQKbJY6ypjfsKnT0sAvGZQAbp9slzdZgFH37G1RtynDXthAs5d8FT71Cabhm/eB7cpjusf3RIYkdfUdceh4OkGKZUkLGabjezNe3AcYbZNZBeK3/PkTLVNRlEL0PYaHiD2VO3h3mamJtEVAp+7+p/KGm3l5+D51GFSK7bTOmtVaCAygEzy9gmOPZRLAiefJvHeuRL3yr7fJjFRMcW3V7OiHasFmcyyW1dMB7BNEnEMb+7IgCjpaHerBKmhvmjWpOtorvEchIuEE2UXD9XIYD7b+T5twrnbtZQFZZhYFgN1QKSydHJuIz2j4p73Q4Gm1cKkrxwuGTAOZ4/8eihOF56XcZHwzIVTsrYVFwS8aC03ftC9K7tNENpdDkLzv1ZwI2CN3TcxGHTffe6eO9dk+iRVxjUHS2SZ8YQ5QXOIVDKU2V3Y8mQkWpGj79W/rGYBaMDX3TlzEMshfuZy8XOnef+dbqkKsMV1QmuZPYsi3Ra6fzh1mWRDLjxtXZKFETi4S3x3rokcYSUZyqNXJxW8xpPGPmhy5UC3Q2YlnBeM/88NnTxYuFexWI/H6m2VWJMTdC5MbF5Z3V/i3mnK2f4PrjIPfOBmF4/OxuFgIU0TJeQS5hcv01JFrEQfwxOrJXA6FglnnvXRDDDFYvOFnFSKfGf/YjSwemv/W1xZULOjWbkR02q+bsMoExAhxsmKpBJonAEIxVrYYTiGSRPCJYPffBdxivQHGX1BoZ3e+5eyy0GwCo9PCOxQy+RPICcoHuo5IxVwO2mDD3w2Ts0gYkcf73gtQvHA74sILLAe/2CRJ97XR3Dqzcw2DwH2ERtXZKE4c5Qb5bkkcDDWdMOl5uuxd47lyV28EVKw9ABix7/knI1Pv+p6i6iWxas47bmcyFmI88NFGYs7CaGpyXS3MZrAoxaLGdOvCcay4C4b9vLwgLOI3TNrIIRChQlgWPANOcUP3Btsk/LYv/ifEDkDGaSA59/qLIo4ZKJudoS7rF4P7iX6qs3zV7TPF6SWlf/A0nWzyVyIN6cQaygGLccQBMpTVse7tErGZniJ7MKFwDTNGUoYki0t0f6ptzi9bjE63VLY31QNkBWm1eQ1HVDxicjMh2OitsVE6/HLR6PSwzDFMNISVrS0twYkoDfm7NGnZqOSt/guPQPjsvUTJR/5/N6xOOZPR8wUub3esTv94jfN/s1E47J/e5hedg9LNF4QuqCfgkFfTT4C0cTMjMTk+lwTDa3e+WtF3fISkKN5K1wwEFzaCxCMxNcwBvaqpPRIkbBs3mLzHz3L8WzfqN4tu8ULROmHonpMhVJyHTvgKRmDBnqjopuzFAC6vdq0tzWKL7mJnHqYUmFyjgPpoxZiRYqp/VNSm40NsTgWZonFLmJcL4CUh0Mvs9TNoAbvH2B9CSBGQtIfSo18sDnxyIm9bSRvCrdz/Q1G7hox6IK3QUshNGtYUeuwtk8SPggsaLUKx8wfdmwTQKn3qHhBLoNK81RjrI1mwFFNQBRgPQPRg/2xTcWjpDWYQap4HarAsnNO9X2r9YwB+YcjS1qNgjvDdX6iRGJ5cvTEEANGa7TybkoEHpPWhgLEMuTdT5ROJyqaIHiFSSUKLJFpuk8iQJQ+NVvSrquQfS6BhZ4cNwrgxV37nNAvnfnSrYbCIfaLDQtu/iHbNM5PZ69hlJ+aZc7FrBKx74GWQHxR/eqWP5jIYCIYz9Bolg0Iy8PIDNm/yPOhpYzycKsGruEGVJTTKKsr17P6wSUIuhgocuGzwGpo0NPiB/ziYdfpPsyzFFMn18ZBNm7w8mEBM5/yo4X/hZkGi7IuA5hn+AeBpJH+XfazMqZ+f6rMP5w43PHwpLYdTjn5zSsSJs5RBnbFySV+9bpYFB76P2/pvTSQJxGEaAbbnp8c9wKoSpx93dLskC3jqqTx2BwtVigPDlvTreG4gBxmpiKyPhkWKLRhCR1g1++2LSsG56RG+fuSyyelHAkLuFwTHQjlUOuOJCDa6z6j/q5+iUfC9LVZETlgDssP07MzuH6JSU/7R+T75+JiCsTf0VzvnBlkSHBgFfq6wJ8zzORmJjmIkdOFUD/8Py8IZ4kaiRvBcNImdI/Epbh8agENVMSPg/n5KqF//Ax8e7eK8k7t2Xm7R+JsWOfjITaZXg8JvFkShp77ou7uZMmKW6Xb469OXKbytnLk9jgZm+rUsK6HVUiysNKRQXQsj2gjBPm6ZalhaclsUzkmkqqWflNE4sXziSuwAFkSjKLmN1QqrV+dq6kLJwaO3CeHuUeCOKhct5uFu3o5ADzOOjibd9XXPqKhfPGHeK9f50dDZAKzqmtENBJEQtUGD3MA5ZpScwieZgNu3aOBRjYwS/8DTrZsZkPEMAOqR0JOApCpin+c5+yYm9i8T02rApJja3c1yBOWLwntu6mS+FykzsbXWspO4T82H/+E5XTpidJ1DCHZoHbHfEqBeYJ0S1i5xWzzbhG5BVNQNTQjYvvO0aZIxb56G4ReV1NZZVuK2g4NcbQhD746+ycmTJWKQ8QEhNtN7xGFccizs3AZ++SWJUsgtGBrz5jYNJeXKKM4w0yzAunVO6ddd2li+9e8Z/5QM1HZwppNJyxS8TMFKWRWXOiWxdJXEmuvD7O6kKCDKCLR3OaSo6zAvJ7PCcKIIm833nu3+DsJbqOVpeWM5fNbSx+4ngHscTxgW57MZKHjjHmHLFbKH21Ibl2s4Q++v7c9wXVyUi/RDetkO6FqSz50a1dLgBxSSR1SSR0iSd0/n97awO7U9UAHa5ILEFSg++RSJyECF0oPGdzU520NNVJfcjPXQhVFX7fNzAmfQPjfA91IT+JUTJpyODwhAyOTMnkVLig79h+V1huOdJyFgqKBSLscErQkeve3Ow0ZNx0yUyFOc35iGA7REvHLC02QHRXGmokb4WCGXjDYRmciEpjyCuOyTHR8+UqFQBOnNNhZZQyE1gt0XV+8X1yUma6Nop383ZpCHkkeHtM4puOiOmaKwNhtRZzNtv2Vpe1kjLUxdiai5LS8zmYp6MT03xIHmYm9KSy6l0uUk3MaVQILC4WOpP4JIAKNOZSIMdFVywHcO0ykkUJYDFAnoUFFxYrIHqxvUcl+Nm7XMyWOzYwn4RjgOYqpV6jay0Xw+jiwVGPpGGFYL5dPAsoJDhuX6EUDB0kdCngcMZZsCdMkjAbErx1kV1/RKNQThSeoqQW7xUEldeYJxh2XhVgnrJ+q5JjtnZxkY8O0xxDIRCSIlL2LNyegoU2HLuYgQQBwHkDwoeOE4hePmHD8Y45uDlkLVgv8a172PVK7Nhf0SwxP164sKlCKcCYBPNv2CbljjfmaSFCI0PyYHpSCDjfQR7zfw9SB/IEIyB0mAFHOi0pm+kK5GoscIDkGLq4GlrENTUuCRxr6G45HOz8ofCgZncP58gGUTCBBTwjGQBdFx86kKmUxA88l30syBqeJ43ns0k82bGGMdm6zewmYt7S+l1892Hxf/Ex50+ZeblpF7t5dPHMn3dNpShbRaEVr5EvP0dRATmyDHe3OSkq1UloxcTHYH9hrWC9X3R30OUBIYrGkhKNJdiVsr4HAz5Z09UsqzubxefzkCyhs4UOlPpuyNR0jF2uyamIxBNJSSQNErYkiFvS4GObGoLS2d4krc11MjQyKQ97RqR/aJyvgbzffEAGiNdcs6qFEsIUnHbTIqGAl0RM05wyPDIlA8OTMj4xQzKD7trjxGotIef1hc/ENdQHZOOaVtmbeiSy5Tj9HOLxpNQNPJB42CcNEX/WpR3EFDLO5qYQv2O7YBvjs7tcGjt+HE8an5GRsWmSLnT0QkG/NNT5paujidu1pbmepDieSPL1HLbGCH4WiyW5b6x/a5omG9a1ycZ17ZSChiMJCaM7mE5LKADZpiZ6WuRvfzlPPr4CUCN5KxC4cCH/bnhSETy3yynJcFi0UGULZhz049NxGZuKycR0QqIJg/11l8shHk9QnM+9Ku2XP5PUQ53Bx+jUIZA1WUB2w6onquYISS4xJIsbFWy/s/8/OsSqc6USIMyaYDEn83A5VKYr9U98kYouC+RYnJuxbvoVAO/dbhu/UgAyi0Wb9/5NxljMmcfDIrOCfYJFj/fmJRUAjcWJplHSZRkcwFIe2xWSpVLPBxkP5s7Kv6CTi2BYoXOuY74xDU8A7PjOo9iT/9nRZQdhim/bp8jAMnClBeGI7zykSEZmnzAXbx5un8sFlgMkpIIwPOFs12ICBDHThUl7NYk+9wadaWkiUtcg8T3HFCHQ9YzrY941ntL6RpXdefgl8V08xfNRRwc+71zDPQDPYX0GmCrMZy6KcQXJ8rmo6M6ywLh17+w1vgjmmAnpSbqX0t20/6Ekdh/MhjbbTVfcyDK17jkuN81KTH9AtFhEUplzArJgzrJ2rZ+9VqRNRlHg/kjX2s27aC6Dx9Gpc7g/5z3DFEdvX83CFzIk9QyRI+HLyDMxgwqiB6daRDLgfkLpc6brTqWHpnHe0IApSwbMG7t+TpKhJq4dzPYuLpRz5uchb25qI7lO7FWyZhAez0Av50erBRbYvQNj8qh3lItxkBq3S+OcFBbha9e0StDvpZTvQc8IiRE6Uug8JeI6H+/UnKI5Mt81J0nXTDhO0gbi5Mr8HF8WAXjBMSYjWlB6v5iUWCxBGSI+RyXAaz4OyR+A1+npH+PXk4TP62ZXEaQK5AYE0+N2idcpsvPBWWnbekLSmpOPA5GqC/rE7VbHuDp8YKynngvHU/aIyvwbP7M6lsGPfyhNBzZkCbjvSood5beWoXN1MOCTjrbZa1cUo0vR6rOalwNWzuqlBgIXZ2TPDY1FpS7gIcEDzPAMTVSKgYOp4aSMTsZkcDwqsbguDqdDAl6XtDX6eYGbhYfdF0iJGH7ciCFvDGtvyO2GpU1lgYssppEB0YuRPD2pgkltN0+E4lYTiQApjufR3XkdBZAxLaS7sRhwDfaI99ZlLjYw91UNGGzMmcSV5bDpGu6V+M6DNH9A98DeOdAmxyuex0M1GaTQf+EkZ/Hw/5C4WZ0Lo2s9F8kuhDivmpuzCNCgAp2FCh05jc7VlIGupHy8rMFQFXOOhYCu6EJn75YKltHGUwPNJfG9R7mAfyxOcljMr9rAbEHIDmEyhAgTbRLOqS0FyXyqvlm06QlJbuyQ2NFXSY5gTsIii3U9MnR2+kA6Ii9+hWQD51ulXb8sDF20qQlFYsoUDjmrjbxLzmDmOmuWBCXIZxU5wiL0Ibr2o3zfwU9+NEtMDYOdIXtxkj/uWMP8QBAvAs/x6A73owUUSdABj+w4JPrUJGNwHDNTcn/NThl3NEiLMyytt6+L8+BxLoS1oV4ZWbtTtGhYGhG8npHmct4vozzAXCR6Ek5k2um6pA1dhro2yUTfKM0hUKnd7G8W7dI5uZ/0ycxMVOp6bot7qFc+CAfk2lif/B/+Yfl/Ym2SlI+z5AjkC4v7N91RWdN7Rv760pQMooM0OS1/yzciPzbaJKF9wce53Zr6G7dL/X/m3/gZ3LxhljE5FZWJqXBZwlSOVG3TovKcZ1om0i4ZM91yy/DLmGmLq8h03LLbXEyp90/Kj2Ju0aV65ctiEzxrZgwECeQWZiP4wlqsp2+UxLZEPGvO88AIJBDw0mwE/6++fBn5JeSZDnb80OFCl1I9b5qvuwodw64WCQa9ikjPxLjvO9sa2WUrZJYHpYzLtVkOHqigKFohOB/c/ygrF6cyyzqHalgy1EjeCsPETIImKwG/Szy2kHOQPFf73JktVLHQtesfjdAREw5F+NvWOcROZiuHGPBGh2T7fs6KoHIIkgKrZvsi2v3oDmcAUF1HNdSe22MHDBJIFLvVADweDxmcbi0AMsHUCMvE3ACztPIWqpRrzuSG41YKJfN8cvN4kEHByQ7drGqt7QlUh90eVm6XheS0ApBUYV6kuZ2LIRCm+IHnc0g+jqtKgGMDEjZHMi7+i6fYdc7pJECqtOMgjQi4cCuw2HMN9avfVSrlczgZGA3L85UEuCMuF4OhGirDEykkwEJ/5yEJfPEhrf3RdSsWzo3inBXFAOUFOnqYcSNBBNFDVR4KhZZ2SrTperlqvSJ5m3Zy4QlpW0tzHRedALoyYxNhyrHgdBfwecU/My7+mxfYGcM1A7l0+YVDLJDR7Rkdm+ZitiXqFPn4U/FrIuG+Uc4loaOTMk3e+7LfUybnkDCXFOy5I57pMbnT2SrBySFZP50QuXaNr5lMmHLzVh8XwyBHwYkpOTPwKWVbSlLmELfmlJcjD+RK73viTkRl23S3hNNukf/83+Q9s1MSui5fNvvkB/EmGfsrqzCZFtBnU9R8k1dM+Zv+EfnhT+5Ik98jx2Ld8t+j3RJwmPLt4KSc+nyUC/jdkw/kjuGTuz+8y88LmWAu1H6xsE2LyLf9Y/Inn0/JfndE2p26/CTRJLrosl5LyLDpliTfiVobcF4soVP29qkWk5/1j8p0zCljKR//Npp2yqSuXIxnJ6fUdlgoipEqdH40MyWHPRH5fqyFl/kNWlwOucPyqbOTHSWQJUtaqT5HSraZkzKchhOyWxyGyedpbghKU2OIRMjvA0mCs6JXAj6P+HxuElLLkRFSPpBWqyto/TsU8ktzY1CaGkIkV+h0eUjaXDyeYUQ3Oj4tA0MTMjYxw7m4DRnZXym3ccgFR0anWGx3ZeS12A/oVOJYbWupl872RpK5xQIkkJUAs8/lxhqqBVQ0yDi15vBxrcDMdA1LixrJW0FA0PjDH78vzjVrxbdhttKOm09qdFQ8W2dnOWIJg127gdGITIUTvJDUBz3MrysKPcm8ncjLX1NOl4bS6EMOBUMA3HQtkgdDDeZQZdzFsBAvZmePeTyQRzdkPqs38kR3zkxwkN/94BYd5hh6DcdJr49SvCgyjGzzWpR1QgIAolOFy5sVmDwfS/nFAo1BtuyZH8HLIEVjgGlJrRCSBzkSh/vROYDBxIObnFFBlRyEi1KlCrcH7dTXbpJUy1Z2BRlO3p4ra4PsCfsYToP57nR8P4M9dHOtBtXOC65kZ80ani7gngBJG2ZZQGxAECyjgmg0zs4PFvcNzqDs/+gjLmb7Nx0QZ6pf3HAijWNWBbPaSUnNzMjWvmty8pGZWeC6xO+ql52Ip3g4xI5a6/SQDO08Llp0VAInT8lJX6/se3RT/u8PhqkasYDFNmaQpqYjOV2Mva6w7HDF5FO9QYZlSDb6UnLEH5dLTQ+4UIZhBd4PZqMwo2NhrTMub/om5boekNOnx8tuFzj6fds/Kn8Ra5W4nJQ6hyFf846L29EtZz+6Je3OpHx8coKPfdM7IZ8ZPnmQyidWIpo7Js0DF2ke8V/jTTKedst+ly6HXA9kwnTJjbRbxnR7V9QhdtFgQpzyMOWTNeakOJMi9x24bzokmtYkbpgy1tMvD9Ka7PWPyCV23iqTij1KqfvDW94JSYsjQ/CUpPFQq0umfWtls6OO5IhfKRwjKZmeicqw6ZWhlEeOusOyy5uQQCgoYWeHrJIGyi/xeKeRlLdkQD5INMigUdhQB0QKZALzZuvXtsnqTkXUlNwyJt19o+xkzUTilG6CEK1d1SqNDQHOP+FYRP4j1gzHdh0iGUxHw1J34aR87eWvzn3BTMA9jIVQmPvaE7huo1uGr2oA6/51a5ah1BxzoDD6WWSzHdxPoTLB2AQk0Rz1WWDIeg3lUSN5KwSoVj366LQk+3slMDMh6fVrs1Uio79PdXsamjhnNzIRk+HJmMRiuvi8qGb5lEUtSFjaWVTyR+txWJBPjtIymVIRmE5AGgc78tuXMy6YDg6PJzfupEU1AmeZMzQywAouiBgW1SRuvgAX3kZjq0RPvK5c0TxeCZ78sQQ/e4fSF0jw7PMUmFnzXz4t0aOv5QyQq7m8SUl5K3eZRDUZncJKbbwXGxjId8Tj85prKBijsMjVtaV0EM3KUmEwsWEbCZrDMFhAiKGrV2EYthaBM6qaqaQzI4wFCtwcUIkPnn5X9Ez8gQUsFpyYoSnjALvSQat1FFlW0AzhSiBLqK6DKKncJTeH9LE4xjUZV1KQHhAXi1hRLiVp5jGh4j85HZGHPcPS3TvKQX/8veZ0ZOaJNH5vrA9wwdvZ0UQiBeD5Mc8E6RykXSBeIG1YcKPSb1mdY5Guo0uV97tKcV6Lywn3jPzPyyAShe4Nafk//KNy4e51idkMsn4opnzb/0DWO+Py/4t1yODpn/Cx3/aNyZA+Jo/cMDfIJUiQiuWjwWHIHndU/irWknl+U+5HRE6Y03JvtCfbebLjsHtGRk239Ka8kkqLTKZdlOu95p2UM8l6/n8hbHPF5J7hl3jmc8ykNXE70nyufa6IXDUC2U4bCN/7qcKFqPuGT3b4ovLjhCJ4wCUjKAG3Q7b6EvK2tlpWeb0qEwxdH49LPF53poPk4T6a7h+QA2N3JGak5XpgtexuaVNjFUMx6TKS4jVdMmm6xHBqEvLPSvUgj4Q0EkocHHsgRuj24P+x3x09uhxPJ+XC9lfkb9WHpL2lXlZ1NEnDqR9L9MQbBYukIFLTMxEJffIjiRx5RVrjE6rYeuw1+bqNNEGRok2tly9FZmTm0EuSdPuyBiWpVFrqQr5st7YYDuwprTSAK667565Ej31JSVk1h0ioTpwg24aRe33TkwywTyPX8ZgKna9hYUAxFoqhpTDbgdEUclLT67c+UXXVs4TaamCFoOfSTZm6cUNavvlTEv/oPRI79+o1vCnErlySmdVb5PqtEZmJJPmzYMAtHS2zemssAAOn3ub8HOakCl0MvdfOcT4BmVLm828p0pYZosfCHCYa6OxxPswwRF+7MWs5j4u/C/laDc2cnaK988ykOMJTlNlZBA9wJhOS2H2YrmOF3ge6hZi7AinA4yyg86OB5FURJQANOGa2ntQsG7t4G7cvuGKF7gxJ+AoASD67jjYZmg751syUGJ1rqjPKgDOqofPYU08OW/Yily2XS+Lb91E6RgvtzHwR5WOwTl/pVUOQW3ymIseycjOsdfHygeuh5axnhd/iZyBQA8MTMjEZUU5r6F5lvtA5QtcBjnr2uZ9iwKwSOlNY8NqBXVXJ3M3s45VBBUjk4zKB6En5pCcFWXKxa6SDJKjVqUuPOUvy0CGaMl1y3QzIl7xTJD59KVzj0/JN35h8kGzizA/mgdDdgWwTXwhHhnSztalePG6n7Bm9LQ/S66VDQsoYxDSpWhmMT8kaLSH3M90pbBc8X0dzSN4wdXF5vNLTuVWaE12y19Ugr/bfFWcUnaCA3Pe1UUZHIp35jvGGFydvyp3OnfKtUD0JEmRynffOiyvlko6pAQm37ZQNza2y3pyWNnON/ONdhxmKjOfAviGRB6nRU2Iahvy/fZ5sWDM6oJyAMFNyyJ6zVwKQmON++iLmGDPntWtwh6R6uyXh8fO5X9++p6TsLx/OiQ3iv/K5vPTS/tmfzUxm7uGFVTAgiI0NIfFu3Ch+I8zg9jnmOnpS3H0PJHr8dc7qh66gEPuqSDXKmgrvmVBl5JiTwQAIhc7oTLZgCzLoP/cxZfgwtllJ8+rLGVz3LVFcE4qvMDiiCizfDKmGJUGN5K0ATA6MyPCHH0n9q6+LOxAQ2btf4lcvida1Ssbudcv44JQ8akTV0JCmem82WNIO792rDHCFTXPg8/eZxZTtbqXTDIr1dN8hqUMgK24+kGja56YwK4GZOnffI3bf6KyUiKvA2Qc3GeaqjY1IYucB5gdxwP3KGYntPZ4j49Smx/lcpapuie0HJPDFBwyytS4GSurXV11mzmAvq5EWmLGFCtJj6HagIqZFZiS+CO5RuMGBsC4laHQAOWiFC5SSUk3kktlNHJBxh+JClVAhx3UV38AR4m32PWLXOQHbeZebDnn55gkrDYgICJ78Cd1usfgxWjrnZAMqt77KqqMsDmXsvUF6QFJAUEB8hkenOLtbXw9b6iAX6JbFdU7wbYF/831k5lnwhc4COmFWnpFyxMOsi+WIpzpZ1hwMZqjw2pjhAiHAe8F3dNJoa46uFQpMGSki5YcZO3Q+j6Ze13p+vG1IA+0SP27PzIJ9sVDIJl1t5+qeB++pWqt0nBrWnBCIDDo9IB38WeYrYDNrsIwbfF4PtyvtxG224tjO6EJyVi4zL9c12SsvaU4Z7djIv0EnSmamZF33FbnYtVem40nZOdknR/WYSOs26YwMys+8+GUJtpXunmMe0N2Xkj1HXplzjmu9DyU11Ccz2w/xGMX+5N8M9YlroJ7FxQ0MMz8h27vviKzayWLS/vs3JIbny3+t4T5x95iy6vDsbDDgCcVko8sj7oGQdJ14hdc//7lPJLlunzSXef8FUcX1M7F5lyqa2j47FAeBW5fEZ+qSWLe3KoIHUMXgcM7O1xfKQCwCKGvQwWP2Yt7rYqwCCh/cI4zVG/m+cX/nrPUiESxm+Q32SOT5t+Z+LpA8XOPqm2YJXufaWQOcGhYF9E3YfWTJtqaObt6l01XHq9QwP9RI3grA8KenRHbslUCnkuq5Vq+VxNlz0nv2hsSuX5VY+wZpafQXJHcAZtJAbuLPHaJETmtu4+B8DHbQDc1cEMNFDCsSkD84h0VOfEmcsWhO5Q9SQd/l08oYpa6Btv6e21ck+vxbHMAPfvJDZlmR4GWMWWDAkYT1vQ1wQ0tZgbzFoGkS2/8cySYqo/q6rexSoGNYKVBthAw0WxHEjQEW1DsOUFa6IJjmXCc6zESh24YFJGYL0cWDZHER7OeVXHN6SR020QHDTRyZTAvKAcT8G7qXiwA6o1Z5MwCZxHECUoRFCxz/Kg1xLgjYoV89y+qy1UVm/lI4RotuLMwt5zksoiGrskyN8Dgs2vF7kJCiL5FOc2i/t3+MxIafPUOAIO1rCw9LMO6WO6HVkhqZkT3XT8qdIVPaOpppCAAZYN3dhzLhb5Ck2cMFPA4TZDxNzURkajrKL0gH8R2yOTuZwOtw9mURic/jBs01pDKCVMnnhMQO27apMUjTBpAbFWacyhJUGHIwLwuGGA4HyRP2P/Z+DOHH8STJ0oY17ZxPgixTmYGojhU6diC1I2NTDCyGeQMMICx7+MZM/lZXeyONOHCMedzoHGW+Z9wOKyICKYPkJbbnxaplbdqohyqP+nXt2esPFvjp3Tvla7FJiR8+LOnArLuk585Vqbt7QdI96tqbamzOXAttEu1kQrx3rtHIpdA1DVb/wbtXRILenC48O/Ptq3lOw9ESsQIYK4gdOKYMThC1AwVA3md099znbO+c16lrEtfYoERf+HKWaDCKpEIn3oUAnyH/2gTVArodnK+dz5iBwyFGcxtz+wwbydPXlHemZebgtbO8b2fVE4Chs+BqL5iiyBQ4+Tbn8xfLGZZdJBiyFZjtR9wF7q/OQIhrlPkQPJBDSDsrHRV41oDCAEYqFuIfUA5oJMCpfSlfo4ZZ1EjeMocRi0m4b0A8X8ENTFiR7hkKy0hgtfjPfS4BOFFt2Vxcioah5FsXGQZrXdjQ7Yjvdor/yhkxQJwGukVvW0USR4J244JyW8ub/8KNGk6R0FXjYuC5d4OPx8wdyGH49Z+hNTXNTuAoyZvCq3Nu7OgQWhXGUsBjkH3mP39S3I/ucrGOOUEXKor2TKIicPd35xiuQCIgHi9J70JIHm6Agc/epSTQ/jkgMfVd/pwGMpCoMlcJ73MxgJueU1M3qUWWx+SEA8PRLo/kQTYL+UZJmSz2+eigeO9cFTMQLOrUV/17mqbpjEWUrMUsFvRwIYsyJBbyKTWHZH1P6tgvXdJyvVum0y65/1cn2fHBwrmjtZEZOFg0Y6GNRbeZSmed+CLRuEyCEE1FuIDfGRuQViMik+nL8qHZRgMIOPwVy18CwYPlNRbwkAlahAKzOOhIgP9ZhEq59kk2WLcYXvdMyP0UTCBG1Ob2zsj5R5/JoM1S/Gd8ozREmEzfqHo7V5oltdwAIgZije2KfWmFGIMo4d/Yxg11ATrd4XEgwzgO8HnbWhroXtfeWs9OF7pX1j7C7J3VOXo8qDJqYB6A1B6Sb14Xqyzk8HyGmzI65Nv2sbNM4tAOx1onzbTsQekIF0+1dqiiFIogfQ8kcOZDie85yusDVAlQfoB0Fcu3g7wQ8THaxNjs9QSxBshc3Xko877aKRnEc1rFNBA+dq7w3qzPDvfmaDg7fpDz2eobxfNg9pzBe0VHsGIn3iUAzc4S8XkXCNENpEEVjNJMkxE0zBktBxhlta1itxTKn5wuXltX7n3b4Swfn1QlSEwLKF+Q1YsiJPZhEkZcqzdVfQzzvn3mQ0mu3VzUCfxZR1aquZTSV4ezoDlaDUuDGslb5pi8fU/M1k4GhyIK4W7PpEzMxKW+c7UEh+7zglVq1sjd+0DSHj+DY+2g3fXMlATuXuVNWxG14yrLDsPOtnyeLKybnsMpvmvnWEVDRSZw+l0SJ9xYUCXz3Lok7uF+umfmVAMp1ZxQIdiGrghLmZsD/j56+EVp+Ms/lfiB59SCoee+en4EbBf77HqSNwxLdoAbJmIe0B1EIO1CgNfH64I00gjE+jn+f+vuolltC4UZypivLAXJgykPoiymx3OG23FjRGcOEkzMchYLawa5hcwXxYRC5jCQd0Vicf7b6XCKmTbZTQKRCketnzu4OGfHaSrCLtmBqXtyOuyRPt1N8mR1qGazgCrFUNHfOLKCw7k3tt2uiCRcMfmTeDMtz8diDjrjlQII3NRMdM7P0dXBV7XA++vUkvJpcrajOWx6aAxhkTynpKXOkZKpIoYTdoDAYLapoT5IKV88I9XD9kUwLggwLMqt7h86WFbobX7Y7Zx/49oCspWRVUI6qIwhVHcT+9ciYZb9ufX/6JDBwMSyL4f1Od5PG0iYD4YTqmuF92zlc5XqjtaQC8jkcY1WEvi5cryScEJZcYJKCCgUKNNfu5kSSIS447l8l8/wvpHcvJuFKPvsLciYa6CbnUQ8FveA+L7jZTtVeJ+eu1cldhRyTic7bsyBdM926fID13H9oWmYjeRBdcJokQL3C7j+kVDhuofQ7v5HdHd+kkhmRivmC2x7770bkoC6BK7GME+pMEcQc9MY37BIHgg94o8KbRMQQj52MUgTCoWTo8ozIO/niGrCvRvdZB4L1cIwmOMIy36cB3P9UmuwSB6K+DU8PaiRvGWOydt3JdW5WvpGZuTRwIykzLS0NwW4YJrTJcsDblywIo4eeXnODT3w8Q85xJyAbNJMqxY9q6Fw0mznLFty277c54tHJe3xiaf7LuWD1kKBJPH6eUk1t6pO29iQRJ9/k7MBIFogP6jgorOFSrIZaqRclJJMSHXKAF1FzHihA4gwZNywsJjw3L0+Zy7JgnugR+WiZcgKbg76qg2UxjhMU32W+bhHYeHa94AziZzb2LSTFWfMs4HAxvepjutSAOYrytCkui4ZFtIgTOhyYRGNYwdfNBNAIG3KlPT4iCRNTXTRZOLSVZkINLFD1jDcLa6YQ/oDzbL5Jz+W23VrZFhTnTWrc+ZLRORgtFdO1m+VYP9t8XsfcFYK3S5KA8OxeZEb4Lg/IqO6P0ueQO4WGwfcYfFKWk7ruR2FtVpcdrui8v1EM4ldd8onWzwJeeRukvqQnySkqSFIoqHMGNT8GcgruowwYsBME930dINEynr/ILTY9uhMWk55HW2NtB2HEx6e08r58s5MSPvQffnptfv5fOhIhWZGxdF9T1rdq/h6ra6UdMQ88lPrDlA6CFkhOlsI41WELkBbc3S1QOBqWD7wXjsrqZZOLq7LwjRpbIWFcLURHwgZ19dsFnf3HXbDqnbqdbkldvAFCZz9iPeWyLHX+FwkWQ4nlQ24zqLoB4KQXLd1VhaHAg7CvDEXljIqlvehq4MiE5QcIBLKRKn0dsLIQKD7DgkOCxDxKP8OzpIFAVMP5LCGp5jVChfnSpQmSwlsn4UIp/n+rc+OrmYVZlcoBDijEXWP9PrEd+ULSm0LHW8g3BwjSCaoklkIsP2xvsh/HqwhAEbx3LqkDKiqkVvCG+DaF5Kqb+IYAbp5NRSAnlTu5YukwqlheaBG8pYxjGhMIgNDMt65R3p6piTod0vzg8ti6KtUR6WMnAQXRBCx/Buqq/+RePoeytTPfEd15TBn19wu7sEe0TftEL1jrXivn2Nnx/4aWBhA/giHS6t6C0DG57j6BWVA4de+RcMU0x9SFbhblxgojSpa7ODzlDSig8eK2viIimQo43qIm7y+ZqO4JoYluXYLiSJIY+DMB2I2NHE+ww5Ic1BtxusBvNFhmPu5N1UVGVIWSDYLZeeZpgTOvM+bPqqp+ZVmVHmxraAnx9+7u+9KcstudvXYVa1C4mNJA0FerO4H/l/lWcUlHIGpRJyPw2J9fWpamvQB6ZvWVAckZSq5YcqkYQIIBr4sq3XadM9gDitadgZpmxaVdk3PuOjdzHaNfso3Jp8n62TIHJFmhy5f9p2Xq/EmmcjYhgPH3NNyVpxyvRpTnArgkxTJTnN7swQCPjUThdyulCl1dX4SLTjfzTGdQKfHMp3waHSow5wUZqMSCYN29PgCEQLR3Tl2RxqTM7JmzV4xvAjN9UhTyCvbHpyT6N5vyV4M+jscEghPSPDRTYnZ5lLKAnOgF09JctOBqlxh7eAcasduads8Wy13JBolMPFQOl4+wmMaYdWusZCs3lO46FHD8gTmmdAlx3WRMStlroXoSOHahb+ptnuCIlQCRbE1m9jNm1cci9vDAheusS5I+kEerPfscvFaiI4ZrvsMS8d9wgYaO1UDFBF3HqRZGIqPuGfEy0i9QHAY152ZFUOGGkhnKZk7FCza9KS4hnuzYc0rHUZTG7eXhvvmhipmpB1OuhGDGDt0nfOBdulmDpxO1Tllhu7CRhOoBipAMFAkZncJplOYy4PDZhXzXJ4Ht8SRSkl8x/7ssTrvIu9TDHa/sf2foEy5hqeM5H388cfy+7//+3Lu3DkZGBiQv/zLv5Sf/umfzv4eC9Pf+Z3fkf/wH/6DTExMyPHjx+Xf/tt/K7t3784+JpFIyK/92q/Jf/tv/01isZi8/vrr8sd//MeyZs3SzzgsNabv3Zdkc4cMTye5gAXJc40Nq25bEdmcXcOOG/EcHX5m0QnnQataCZlm4IsPedFz9z1U8QdpNcNhzxvDa4PYxEGe7BcCGI34/OKYmhCHmWL1lRVeDJBrbi4KMNeF1+Xwe+daRdI+f1+ckXDRmQwCJgcjAxI58bqalfCHxPnojkSNtEQ375f6S6dldLtI2IEFfFJciZise3BRhjfulHjYlPTMqDQ+uiEzwTaJh5Pi9ZgS0QKSvv9AeiPKURCkwSJadZOD0jgxJUMuU5rO/aXMpJxyK7Rakt6AuJxOOTJ6XS4H18rUrY8l6DDk2PQ9uXx9VPYM35T3fBsl8sH97DZBhwZdE8jjQDrQYQFRAYlDqC+6XNUYXSAHCkG+753sJhlbTDQ4DYb4dqe8ctAdpoFBvSMlfkdKhjKvhTyoS3pQ9rsj8mGyMSsl3OSKy/fjhY1NQLQQgAySWhdUgfbWZwZJQ4cJs3GW2yG+W12nVjMqdf335PCh8t3eagA5osjsgiT4yZAYbdulM5WSRObaAkMhx7oNIqts+YaBDnHeupDjWmcHTAFyZFFmSvyXPuPxDXkz5MvmPLL6cPONb5+1QwewYIU0mosVf7AWn7BCgVknLKAxMwWiXnKhnExwwYrZNygtqiF5lCOig+YLsGOI2dlix3E5QEKf8vp5TS/UIYLRFcgkCnuLAbxnWORDOcHc1nKmMSjkUbI5LGYixu5EuXm0VF0jCTTifThL+CTA6yK6j4sjQUYxE7FGuI9T4loF0C2FsQ5IXARxNCWkvZjVg2x3wSSPM4S5hVesYWC+Fc90b3EtxchCxSTPytw7/np2u0IqjK62USN5s9se8/S3Llc2t1nDisITJXmRSET2798v3/nOd+Rv/I2/Mef3v/d7vyd/8Ad/IH/6p38q27Ztk3/xL/6FvPnmm3Lr1i2pq1PdqX/8j/+xfO9735P//t//u7S0tMiv/uqvyje+8Q0SR7jTrWRM3rwjA74WicZ1lXmXiPMLVd+SMHTx3bwosT1HOZMHmQLIGhauTgSpe3yir52tzGGBgZsA7Jz9X3wkowdfFo+hy/SDBzLamiI5gdnE+ms35ExjSqbP9ZO4YM4GXRJXIipbevvlYf1aWfXXfyU3GjfIholHsmG6TwbdIem50S/XfF2yd/qB7IgNyv8Md0ny/oeyOzYoE7felj5PbreM1ujoSkUT0pmckg2OiLzz4bh8yz0sH344KN/wTcif/aiHj/0F35C4v7gmnyYbOY/0smdKfqwH5c7nM5lnS8vP+0fk+/EWCaev8yf1DkPe8k7I/4pnCFkWaflp35j8dbJO+s1pzhlt1qJy1HNezushiaedUueKyHuJ2Ztw2hOWjv7P5dOUVy7qi9vJygdyqc4k6+R5z5R8L94i6aK5VrOAvK+5qU5aGkOUDcLoA/JMfs9kUqFbtj/WI0OhDmkPNcnq8Vvys62bpc2YEY9jnfzCqi2cgYItu1dzyPobn8kLe58XZ12dBMPjUtd7T44cfYWdMauTiM5ZQ52fJLdaC3AL7u5RSlSXFLrOxS8yG4On3pYk7Mwp9+2WyHN58i6HU/TONfwdZLo5v0Lx5MJJFRWxfb8Y7avEd/08F9GJnYfE6BplaC/mSEsuULA/RgdVEQdyKxgVxSLsWM95KBYrk+N00MNiqJBzYA3LGDCSGuyhjBCRMigEQJJWbJHvvX+DSgqYgrC7iznaCuesnNYsNAcrNc5So+CW3DYbkVMtqOyA1X4BcM5NT1T1HksB3UFtZFAZolQAbE9P7wNx9N3n+ViuOwHFBhQi7HSWIlnVSgUrBM5fqGdQLI3vO7EozwkCjuBpEuMqY3GwHjBJ1veW3X/Y1iD8+cqfqpA21TyeLRc328WDwUrGgMaKUagUjINA5p6tg4zzgC7YyE591oH4rAe3lF/BgecW5kJdw7LEEyV5X/3qV/lVCKjq/+t//a/ln/7Tfyrf/va3+bP/9J/+k3R0dMif//mfy9//+39fpqam5E/+5E/kz/7sz+SNN9SC7L/8l/8ia9eulXfffVe+/GVlibzScL9nVO7d7pHEuety1r1a1s70y82WtZTfNY7r4u97IJ8NfSIRQznz2Wd7cA/fGu4XM2VI+M670hKfFLcjLQ5NkzXGtOhpkdG0R354WyRmnuMif58xIuOGU67Eb8tXtEE59UG/fN07JlO335e/iqtKLaR6r3qn5EfxuS58+11hue8w5XPdpHSv3jkqH6Q80ur0yidJv3zD1ytD+qQkHIbccfjl7HA//y7hikqrc0yuJC1CNhdHvTNy2/Cx2zauuaTBmZKZtCbrnHH5um9cWp2GuB2m3EklZZczKteMgNxJzcowGh0p0dMOCadnbz7TaY3bxC8picnsz1c51dxYf8bMAiTqbsovQ3G3vOadkkaHIR/azC+Ay3pIfto3KteM6iriIMggX+hmwUwC+w7ZXirHyiehoMq0ghzRboWfiOuydfSO/L9CzTLevCob9AuXQXTEQkE/iV02nDdj5V8OgZM/YccVpMRzNyib8H4Gw5wx3JBHSjyyR9ZEhiSxbpX4+m5Kau1GfoYQvtCtWyTQxnyJbzq0KUcnGVKz9Vs54A/SR2vuAjMm6GTDYIK/t5FXd2bmE10SLNbh1scu9uEXMxLhNonvOshZVCxUUWVndd2++NKTDDEGcUOuHyrj2tiwmmMqsPBMNbSwQANioIWnJFELQl9RgLwdHSd2ZfHl87NDV0hGTnnkcL+SnEPJgS4NlBXF5vjyyBVnoetnzyXI36GkSG7dPa/OkSMeo5SPGZYFH+Dg6+F15ytTzn8+KkgqBNQo2uXPy7sCZ4DxAUrwS5hmodgCV2VcJ+0KlwXBNElEIJ+FugZEHp3HxbCXR0fV9AXmN2PlcEgMQeeVwO3hXCYdTee5rxF7QfmkdcxmDGPQiYzAGTyDVLCeRbaKPgLM1gYezZnFBJHx3Ks8iulpBsZaUKxBNAa8BWp4+rBsZ/IePHggg4OD8tZbs6GYXq9XXnnlFTl16hRJHrp1uq7nPGbVqlWyZ88ePqYYyYPEE18Wpqcrrww9Dnzvvcvi+x//twQkLdOpR/KJ6RIj3Scxuvo5pM2ZlLv3r0mvOfek1CQtm/3Dcj4Zkp3uqPyXeIskxSHf9I6JqSUFFC1mGnI/Ppn9m6BvWs4nGiSZNmREPHz+e4ZPDnnC0uVMyIDpldVaUvpShSp6adniismHCXVT+kKvk696x+WQe0b+d7xVDHHK6WS9vOSZ5Lt/JzHbkRhOuWWXa64LIQBy0hRwywa3yHVXh6xxucSd1mSnU6Qhrcn/Rx+R+94W+X5os/zNqYuydc0q6WnfIqjNtdPZT9mnr42Nij/VJM8FuziPldR1kp+Q7pdv1bfKdH27yiLTnLJ16KZM1u+Wv9XQQRIWCnhl3cNLMrlpj8Q1j3jHBuXnWrrERVc/B/Ot+DqJhPzDYIDdK5/PDR9C+jWiU6bmyHSacmBuDL+HVTtI2HzhiG6hZXn0wMaFxSlkrLVTfuUuZ80oQH4DaS1mMQpl1CXXblJdr/Vb2XXKlxIuFtidWr00TqU5OXyZbiG6EjAVSrtcEl+7seDjuT3gPjsxGy6MXC12/hDg6/ZI9PhrnN3UMStqq2wjuiTmC4hrZJALO3ZXmtoob8Zi2Xflc86XJrbtV9I0zCCNDdJIohBSDU1cxHBBDyme5qaDITr3NElYShvsGhYGugTfZfSMBXSHYWBF4pZHvJA7x+5xxlUS5EUbHShI8nAuBz/9Md0QrZliOAraZ81wriPqxTk1MS/Coo7/tpLHGCN3FovkVQuXW5KYLy80d10IMI458XrJh+CzoAiGjnz08EuLkg2HuUXk0eK1cS13pE3x3L8h8f3PyWIAx0y1Us35gLELUCDMc19b7tyQV/qY2zssaY9HdVZt8txsXmwFsMzW8u+RkOYyS7ECP4CnGmaK3XwY+NUI3tOLZUvyQPAAdO7swP8/evQo+xiPxyNNTU1zHmP9fSH8y3/5Lznrt1yB8OPVTl0CTlP+TbROUuKUV7xTEk075ZoehBkmLdULkbzVWlw2OOPi9pryP2NtdAUE5Qg6TfmvsQ457p6WXe6ouJ1Q7bikS0tKwO2UtLdBOr0e0Vxe2etMyJSvUVKpcfk7zrSM1bVKsxmUmc4NcqhrNclMDHlT0SRlD+uG70jdlqOiMfBZE69pSCA+I7/c0pkN9e288ol4YhHZ+cpPkQQR6bS0fv4TOXj0DS6sreUCukIwv8AN0DXeJrszhimYM8QiCC6bye2vy7a1WwTLFt+FoDw/1Cuxg2vmmAn4Lpzk4v1A3gwjKqcgEYmdiqCgeuo3eyTy/AtZaQgqXH4jKsHwkDIQWF19RRSfQxZZcYgFPMJufZc+K2kCgsozbmL5Zge4wWGBiVlHLiwSyN7zZxds1sKQeUWFFnEYxO9aRyLIhV6enAfPz+7YQkgGKrnosi1S/lIpR7fsTKimMXIjjYV0sZs/jCB2HOC8CizgsTDBoozyZ2s7OLWiMjZU6JOs0u+gUysq1a7Bbi5yKPPMyNFgT++9cZELy+SWwmYqqJ7DXIIW6aF6ZphhXsWjJ2m/DiKAWaZqCwHoJjmMZNHPUMM8AdlFyqC7L64tzIGzGTsxgsbnE9dQfw55w3lMs6v9szI+uHF6717LOkjagdky0+ujDIt/w67IJMm/HViQY2GerIAE0Mzq/nVJbD/A44nzU2U6RHg9T/c9eVLIl1QvFDgXUXTCfoM0G1b++RFBVT3fSD+3I1xJLRdoSnFRAFqkbp59To6xSDgGFkE+O+d1WjslcO4TJY2dx3UfRQNsW1wPIaFnBm0Bk550IMgucjlpKB6TNVvLh8uVmWOezm5jSGVRYHsiBYknBGQholD4pJ1ka3hGSZ6F/Hkey5yhFMo95jd+4zfkV37lV3I6eZB4LhecOLhJrne/IXVfvCO/uioiV7Y8J9tGbpMwbdj5vITSurQP35c39r3AjpKarVLhyp3n3pOGngmp2/+i/H/3HOHMnHO4jxbU67cdka7rn4nm88m/bmkVffMuCZx+TxLbvi7HMhc3R3ha/JdOM9oAch5UGLdeOyvahCHh5w7NCSD33pwUc/UBadxQekHof1QnrmREmjwwaZmVU7paWqUpnZBUXS4TQocIg/sgE3gfkLK4RvrZuZj+qf8zx86Z7pq6TpkcK6KW7MBMKSdOW7XcAjoozLvL0f5vyQmfxU0CCwUQomR8Z/XOcNUgbeZ008oBs1qYy4kdeaVgYC4iHWB1jpsY5EVZmCm6qWLGJXpiF+czMYcJJzUsQCkfhDRq56GSFX593Vbx9NxXlVY7UoaSgW3asaBFFgxFuBgpZ7KwQIBI2i3ZK5E24TEwUUFFH7JNGKOwi1ctXC4SaZLpvMU6LMvrfvjfWIUvetw5NZI7ZEZiVgjHOheMmsZFDgOoT7+r8iwZd+KsbEbj3nWSjRrJW1yAhMPZGMd02ulUC+I8oNuG2Bs7yYMZFjtStvMcxwQIBgm+XdKM6nzfQxXxcuEkF7JpmBq5vXMW95wDvHGBjpjlwGw8XaejMY591/gwz/FS4OzT9Bdzju2VChQ0YRrGGJ5EjNmgFUsa84Dz03fjIuegcu6pTo3nvufeDZULu0gAocT1Su9aP8fxdDGA+zGOM5r5VBntoebxxlhgg1wVCoai1zwULUHQopGShm3MRYT0togEUZmvjPP+iIILCA/OlRiKxE/BsVoJOJNb5hyuYeVj2faqOzsV6cjvyA0PD2e7e3hMMpmk82axxxQCZJ/19fU5X8sJR/aul7/z698RF99nUF6ZuStdq9qkoykoO/dukbV7tkmzlpLOljppa0F4caN0dTTJGo8hrX23RQuFpCU8wkwsGKTUjw+Ke9NW6WrwSsCREuPgc+Lp7xbvjUuUn1nVK3QFAuc+FkcsLGlNU46ZsQizkehOlUfwIPdjbhEWqSXAbkM8RhkaB7RtjpKcKxobZJWRsjOsfYf7SPAgG4y+9FU1q3D3KmUt+Fm+tICytniUVVBGP6ByDZvxob7MvNVcooAOEYwBUDHFF7KEeFPIPiClbMpXbyAh8jy6LUt9wfWf/Thn2xSFobOyjxsYyGkO0qY4x4Yk+Nm7onesYWeH29Z6nd6HlCfCmc/q3mH74WaHRQv2KYAg4VISDhwbkIPlVz7VjbOBskG4Bc4XlHmVCUpetLm/ecyywSUTER3obtCePP/cqBZ5CwsUNDAn5DB0VbUuAixWuK1HByS+92jWFAL7B1286NFXKX0KfvxDkm/fxVMsXhQDFjwIhEbnp9Sx6Op/KIGTb/N5gx98t+Rz1jBLEkC+Ii99VaIvfLlg1wBKBCgVaAzBHWkq6W8B2bKR6cTNqc7XN/K6Bzk1Z25wLhUy7kE0SCKmOiMlwE7i+DDDqJHPR9dMOiqXKUi5PSSXuP6veJipjKtjQ7bIhaJcNSYgWaRN5rYViujhc2MWN3NfWgzgWPJdvyDRwy/zeJnXe64ADLzH9aNKwFCH3SS3Rykr8sLt81GJZBMqFcRAFH2vDU2UKjPm6fYVukpC/QDzl2cBOLaw/plXjEoNKwrLluRt3LiRJO6dd97J/gyE7qOPPpLnn1cD2IcPHxa3253zGEQxXL16NfuYlQrN4xHf6lXiF50zdZC9OCfHxHP7sjiSSTXoPDWWcyHHUDgq+rzYonvXfZcad0gh0O3CYh/VW/H6OFflu/q5cs+yAkNvXuKN2wkHz+kJ5SSYWbyh8pwPVpH9obI3e1VVW8+qESIWMF9iIRWqE9/Vc5S8BU/+hAtHWPmCzOkwt0AOT2snh6fRXTTrmubcpPD6kHuiI4MFEl7P++AW5YzW3FRB2R1svs+flND731VzT7aFujY6ROMASJNgcY5FLLpnSwJsk0d3KOPKLu5KADNd6EQm1m+jZMsO75UvpP6dv2BOlIZA22iYHU4u2A2dcwr5lXssXmDLjiwhENuK3zYWKHndIeZ9tXRI7MDzSj5Yzgm2CPA8dIVbQnB/4v3PU76Ezx954cuL7mqJLqz35iWJHXmVxyD2d9H30NDCcxREs5C8C1V1GFZEn3uT2ZZG5zouaooROHf3PT4XOrylFufuvkc07UDnPLl1b04hoYYCQPEF8rACM645cDh4vcFsqKVoANkv1B2ZQ/LQhe2+S6kvAGIIGbB7sLewax4Cyls6cvcdrp8oaGFeKQM+J6S7Lpe6FsNJd8eBijoe1lzeSgfjhDC7a0kEse0yQe0lUeA8g+M1Zut1ZL8VfLFMN+/hwguLOIehzAGJgTKDc584/5cANLtB9m0VwP0B3WQeTyjOInuxzDmCAgZIHO+VcEcuoAJhFm8JuasyBUJsSbekXW5JtXaxi+5ehG2+EsAoqzUbn5mu5bOMJ0rywuGwXLx4kV+W2Qr+3d3dTbkl4hF+93d/l/l5IG6/9Eu/JIFAQP723/7bfHxDQ4P88i//MmMT3nvvPblw4YL8wi/8guzduzfrtrmS0fLccfFMT8jkkdfEwCCe5mJHDBl0kHFBZscbMuSQ5z9lxRfZdJibwOIVcj5YKMPOHTdo+2C0a2JMUs0dqnLP7lm/OJJxDpSjouUa7CNpQqcOXTF0hmgfbQMrxI1l3A9TBk0peEFxOiW294S4xgZJxLCQgOQP5i2QHUZe+QblK3gPDGINT3EGj0Qhc7NUOTlzq3ioSOGzIH8Pcifn9Lho01MkfXZk9fycV9gg8Z0HmG0GpzprWwAMhs9IptDRUtl/d5XJxqM7ma5XgQ5L3jaqBOh4YRg8uX5LRc5h7J52rKH9OTo+zMDKVNx9ty+xYht542do148BftzIQKxBJLEwzJe5MFw21ECiu9DqsTVAD1MCyGQhEaIMtNrnmcTztDyGebwFzvyBIFZ7o8RiBgUbFDvyj6G0Kf7LpyWxbS8LOSiOoGtWDEZTKw008PiSL4n5L7gHdq7hvwsVE7AgRAcBFXBENhRdnKdS3HZGS6c6N7C4m3g2KuDzBfJA4XZYif0+pJm4FmGxis47lASFgH1EWS4IIfYJ9lemyEdoLkmu3UylRLGuOOfyxmaJIopGmOVDoY3XXD1JImMRRwCEs9LZJcthc6UDRc/8GBPeE0DyihRMsD/8X3yYe0/Qk7w/lSPJeue6HHVLWeA9FHgfvmtnJbF5Z7Zjo69eL45ElMWDxQau+ygoV6RGyagocH+AAkFJYONqXKBM0Y2RCA4nj9HgyR+rOBEbWMzG8Vli+1LJk0youVZcOzOkHUZci9VBXbbAOT3cl6tcquGpxRMleWfPnpWDBw/yC8CcHP79m7/5m/z/X//1XyfR+wf/4B/IkSNHpK+vT95+++1sRh7wh3/4hwxQ/7mf+zl54YUXSAKRm7fSM/KAlkMHxOnSpGu8W5L1TWLAqXHTDlZm4b4CsgOS5L30GYmHwzBk5qt/S5Kbd3LhlfZ62RHByYxZEAaAtnTw5uOIRyR64ku8yIEcoOsCKQ6kjYlNO5UFPBzYgvU0QMGFGzc6O/JtuefMjN2/KcGTb5OQZLt9bje7PPid/4uP6eCIBQNnsDLPGfzkx2oB3tCkgqRvXpDAqXdEG+4nGcGFOB8Gupdw5MoseF3IB2xsIsGxntsiw5gr4YU8bYr37nWSIdwISTz5ZDoXWSTHGUD6hMUUQuOdsQifE92+nM8cmZHgxz+gBKoYOGt47pMsMSPRRXdtwzYxOtby4ltKnocLNAgQyRryD4N14r5/g78CITZ9Qe7/7HbpXCPR517nfB5uhpyTM4zZSj1mATHfEAiq+YSF3ODQibTyuDKVXZBXzKxVBRiHxGMlZy4WT6pZprOyyABBp2zyxnlWo/M7YDhfEXJu2eijo43zrpikDnJOZuph5qpCoKtfqGPLYG7IAmHRj8V53vmes+CFpMqShvqD7EKXk/09y6jKSMPl4jUb12a4YsK5sCAcTqobcMxABYFsVB2ZYrbFLbrMMGkpJoFjJw/dl8y5i0Jg9Lk32LmCugL3Fxwv83Xfe1o6eUrymlt0wvUJXaBi6gv8jTYzlWPXDwKNIkpZZ063m9d4S0lTFHCLfniLsmmr4GsBhSSofnJiIRxOSWzbJ9476ObPjUNaCCD/hzwXypCSgGJkoIczoyC7VlEC9/xKrse8x+8+LLFjr/JYxeyxvZDoGh0qL0NkxEcj10lZyazTqSTO+SMQTxMgT71/Q61tlsCAp4blhydK8l599VWaheR/IfwcQDfvt3/7tynBjMfjlGoiHsEOn88nf/RHfyRjY2MSjUZJ8JaTicpC4F+1WtyNjeK7e0XqG0PiSKdkqndQ0rAZvnJGHPE4L27+S2e42ENVP5WZj4MMEyQktu8EF+64YaNDhhMbFzFIRbDowKIOpAc3e0vSAzc/LOTQwUO1HkGZ6MTl69XxmDmzHsj9uX9DAp+9k+0Mwo0w/yKdpIvYLYnvPqrmioZ6xX/hU+UMF6qX6PNfFn39NkqXINVESCoWMXhO3AzyAfmiE/JVM6XmWpwaJWcgT9YcIGWeZopyNMg0fRdPswuCxxvta9iNAPnCApg3HtssH0hq9Pk3JfLSV/h5UNl2D+Z23dBJxY1izpyctb3GRyidxOfD/B2IIpz0MD8FKQ2/Q4ZbghShc8cbGKSlDockN24X770b7MJ4712TGNz0MpIidB1B4DE/Qvt0VFjNFM04fFfP8v+xMKf7Ihb2tJYuPYtVCvhbysps283oWMU5oeoXVAhuXtrLE+VzSx22nn+DvX5eEtv38Zhm4SBv22Bb2Y1gQKTYUc871nKkdJXaxGeAGzyLCfb9nAnmZsedzoiNai6vWLfWLqXFXGejyuyroTAoQysza2QHyBkz83A9L1GwxLUELq+Y9UOhCx2gHLjcLGIVdSJETEqwjkQRsjlI2LFYx7wpJf8oQBWTFVYAvD+6/M6jm7+cUGyuUe/KdPMKAPcpbE8YI+Haj+0A47BKDakMBNbjnlIEIErBUz+hCQnkmCQnts6fp1vd5/M7Wrh/4LqHgudiF2ZKSjYNQ7w3Lkjw0x/xfo97Oma/LVQyj5cPHKvMjIRxmGW2hvGUliJjGjbEdxycY36E4gruv7h3PnXg/OFlziJmx3RqeOqxbGfyahBxBfwS2rRRZGxE3OmUhBymrLn2qSQiMUnFYqzyYyGW1pzihPEIOjiZCzov5A3NlH4hLiB25GUuwDlEPzFKly0AznsgeHaXRJANLBgwHO7uRdTAjDhA+mwXb3aiUqmceTws8lWXbIqLWJChYhVLRzqtpJfRGS4QvXCUa+1SA9AgrO25kQcgQVjIgAQiSmEOEXG5lHnIlS/Ed/5TSaNDpeu8yWHGEH8H8oaqNBZOtGj2eGdlbpjpa18tnrtX1Qxh19xCAT9rhnigus6bmXVTNZVBArqUkFxmO3XWtsEi6soZkjDcWFBxB9EDIaZxRwZYpJWSbKKLa8lIuf/WbRVnLCz+z94V0+NTA+yZ94QKMuY0IffD7BS6dQg9xzbhgiMTI5GNKUC13qmV7ESWlVjmOXJyO4G0ViFjxfG5aGHDJaBmpB4fyQOpxzGHggTlQe2rlGzK2jZpGBn1KbdYGyDZxAxc/jHPgsRQr3KFrQL8zE4tp1iCLp4K5laudjiXKIvOGPHYoU2OqEwrG0jy5jl/+VTBMk3Jm9/VwtVZ4uNag2tTpfuWRiu4ls3DAIhxDDfOsyuVLTC4PRI7+CKvVVW7JdrhcKrCVZGCwUpAoXudBWukoeB5AmLf1MpZWIxNQBEDwlxpV5SFFEMvLB/Egh1FvYMv0C2ThcrOtewU8j3zPj9WtAAEKT0KuLhfw3lzsYBrmzZR2HwF5mUw++B88IHn5szLY3vN53oMObOac8yoi3A/q6BLxeM6/3FYB0A9MQ8DmWUNqJZunOfajEWfWhfvmUGN5C1zNO7eJVogKKmxUfEfOiT1LQ3SuKqd7n7d209Iz3PfEKO+SeJb9khsz2xUABcU6TTJG+bdrBsUu3gwirCqw5omiT1H59prr1qvFnozk7xRoFpIh8mMxAMzb5TiDfawU+Y/8wG7Y+icobJcLpsLC8zkmk3KHnz1RmVgsW6LuPu7FcEqUHnGDQDyJM7PFZCbYVFktHWqAfMtu5U7aDTCbiY+NwiVJcHE7yD5sEgoBq7xXjDDgBsmFj4lkTEhsDoxNEioqyexxvayd/PQEeXw+95jWfKCTiCMaEC87Dc7GuTYyWOeBJZySJsUBa+HmULf/euUzsJNz39FuWS6JkaZkwXJle/qFzRiwcwDXhv7CIP9WKDYu1noNjAodh7AoiJ/8c9OQb2yqV5OpiuzOXyPj+Sp/MCGvG3TnO3c4nOjs8zZLRt4Ljudc2RhOMZYrJnHDZvdvMyxC5kxDFcS9jw+uOvCqjx//hUGIlOzklwLzzrJw3Uw+NH3lWzy6he5hkiWoUSVuWe4Hj6ODCtcM1EkzJ8TY7GvgKtntaD0F7NaKxS811FZMHfGyxppmHN9Q5ERbpyBOl7fdRR0wtMqT7NSoBDEAs/cmVxcM1DUs7tzomCLTiG6c8osZ2PxLjAMfjZs4ww8DNcqMfyqBNn53DwpKGdHe+/TYbsYyVUz0tXL57F96Ug7M0n1Utl7dxlwbKGIVH2lAtd3nOMoCix1LFENyws1krfMEVi7RgKbN4qja424V60Rc2RIGtoapePALtne6JTGyUGJOz0ysm63JF22i6emsaKFi2b2ooqBW1T+Kwg5ZsZRLEIik9h3TNltp4ysMyYupiq3roc3MpLJfceKB2jnAVU7ZoxlZGMkhZRznGdUQjEwTHrdFnYoOVtm/x3mFldtENMXUDdWkC24Z/kDXMDgtWhJnw+40j28JZHn3mBXjUQl4yZKqVER+SIykywZHcnqKiV1g8yUN9tkQsVBYLh837E5GWw0fjnwfO72Anls6VDbJT9X6eoXqtKeQ4DTnOHCNom89k12bPm6/Q+ZW4TnghseCfL+E9zO6CpicRLffYgSrbTt+eY9l4eZzanC5AxS2PzPU1QSmjH1WOr4BHbVmMP3+KJCtQJyJL1ztbgzUiPK8wrZfmNBhsWeXboFF8SBR8Ud+soAr8O5PNq5n2NMSX42FfZB/jwVlQN5ktxsMDvOlULn19OOzPUDhZPIq9+UGKzq0R3JLHTZGcc5tkyr5zjnUWQrOyc2T5Co9D9Ss90rULapTWZIXhFQspmnvkBxkYXVDMlKbt0j0WOvVWS8k/vc6zMZprlKCN5v1uQScNzn4YLKPMY8s5xiwD0TTttlZ/8qBWIz4NCdVyhEgRXvrWj+nZmiRBJOslUD10dIW3sfcKaUTtkLAK57T5v5Cs6/xObdVR9/Nax81EjeMoevq0sC7W1S/82fksitW6K1tot3yw4JbNkm9aM9sj7SLy3ruqSuvUVmoroMjkVkJpoUE3JILtImciRZJAkVyEVI8mameGGGjBIGALhIeO9c5YC3/+oZduAwuwHpoTNj1VzJPBet60Hs6hpU3ENmkeu5c4WD4pDRlZKQ4DOgigm5SiGgYgViR5KXcT9DRwJEDwtaVFhz3k80zIFxLHIs6QeIEAxSYBxTzN0Q+XLocOKxNEjISExBpEB2fRc/owMYZKaU6FUI3qjvXKWDJzqWrr6HNOtAlRRRB3aAdOOmBmdSkkUO1u/nzF3WKACW3Jt30cwDJBaSHs7w3blG8uDpuctuLGRJ2bm8KoFhe5CmQlVado0gsc0YyqAKDaOAQq/DWA5Idpb4ZvS4pZrqNefKkbJyVsPIyokLAfsOCxgvjt9omJV6GhqV6ZgXAyvmZopRDeLSuBAvHGadZ7YE05/8bq0ly0PH9ikw2agWPI5NM1vg4Gwt5twy8vb5dPEeN8pm3i0AOOYjJ14nwQucfn/FLaBxbTdLuEjT2TnvuNfyz3WEeM/DvAbHkjWznn2qREydh/bZ3QwgB+VMWlvpnNO5ktO8Gd2FdvNw7KfTfF7cRzEaUEp6zPsHigzznMNGwZSO3GWiEyq9Nqr5/cU1pnmy16dUwUzGGp5+1EjeMoe7oV40h8iqrhbRDh4TU9MkevZzMSYnxJyeEs/qNRLU0rJj7wY5vKNdNq/GBc4hIxMxGdNCkhodoZkN5tjcD27SUrsS4EIJ2QNvJJl5NStXCSQoFWpUhAPEApVsRCHAoa2AKUoppzlW4CBHSSbEf+kzyjExl+e7cbGofXSqDdbtPi6KC1rBx6Ps5uGmAeKBmwykifrG7axuguzagc4VFqgAPi8W3ZBtggBFnn+TDpwFFyYOJzsi/itnVAfT1hGDbBU32eiRV6oeJkcnMfLCV+iu5ul9wCoxtouO2T3bTRDkE05i8f3P5yzS2DHkTOLc7Wd0rRUtMk2XUBwL0effUlV8l5uGLNiH6LLObx6vtbh9P1xRx0dUMO/Vs4yLQKgyuz82uEo8z2KC+/wxO2s6Zwq4ecJdtqFZvHevFpRq2rchjkVUugOITum+q8x05gvOBK6m+2x856GC3XeStnySV0iSmwH220qQbGIhyLneRQLDyuFgaNuG7F5bUm5e7x7vsbbsANfmXYcpo+d5v1KcWDETNz0pqbriC2QUWlTh0syVZi/SPme23Z2r2YILjjfelxE3kA+XW+L7TuQ4LJcDJMGmz79o5y4KmrhnoTCJuCfeo51O3nMYQ1Rg31eSj1cKnHVublddvIVmv0GqHiggVV+hgEEP3ZprmXjPJGokb5kDDqP+VV1i3rslq/dul/S6zeLesk3MyUkx4zFxrd8g6XhctGBIGkJe2bK2UY7u6pB9W1qlYc0qcUyOSeTKVTEvnJEU3DgrdY3SNDWgm+mogIzhZudMJpi9xtmyjGSLNx/T5PyGNlZ+9gqEyXKaw4XZmYxL8NTbap5twzb1fdV6SgkLAfM/zmRMEhu3c6A9P3IAck+L9EBS6bl3XWUENrbwc7AqapN6cs7NqnK5XDQpgZMmLoyQpiV2HlC5eAWkaOgWglTmZ87AwALD5cUW7WUBC+1V67kPIMGck203OabiHw48x8fmvjjkr341B5NP9JyaxPYcpaSM8za48Ls9XHwhX9HTe099zkrzmXLm6IqbpYAMw+3NCuYFYU1s2kEbbftrOcs8z6KAFeZeDtg/NqQMOsMWmrHCgg2V7oJSTTtcbm43kHJ0h+d9bGWADrw6RoMlnBGjsxI7FItKkHA1l7e88/LQxUd2GaTP9miVeQN28DiW8gwuVPdaSTZXQifvcQHHODo6RXNGlxMMnXPmzCksJetGFxtEz0ZeFjOeBR0YOFGSHEdmqOwolp0IMKc0Y6BUKVjwsXULFwK8PopRkIAndhwkAQu/8TMS332E2wXkDzP8llnKfJ018wGlDpzBFwOUbC5jsyDcJ1XWcJnua9rMZP4+HY7zNVSPGslbAWh96UWJ9Q+I+cVJadi0XiIjo+J/4SXxHzgk+r074qyrJxm04HVr0tkSlN1710lXo1c2OqbFs32npDx+id+7K0PjUZkMJyShp9jls4AbSLHFD1v9bk/W0c8+o0A3yjUbVVZdMfvkfBct64Ke0dMzn+nQS9nHwJQEEo45s1wZtzOjqU0cZprzgPmD6VYnj+8bGV6GLvyUcEjz+lSl0TaDAOc3u5SBi15bx4yOnG1d4rt+fs5FFR1JkrDHWanHHNXNi+ykgoSCzNk7E9x/mM/g7OJccgqCXkiqiLgDRyLBrl41c3nsjIwNF+3wZGWJE6M0x7GMY4zVG0lwrAw/zJHQqnyJSR67zab5WOUrOJbp+lagmopjSzkbliF5difZEjNClQKFkHyHuzmLVzjgZjq7lFRBHlpktgwGG1wYLVOZE46/wLlPJLlpF88NdPcXCkaaNLTMkc3aJZtKufCMd/LyDWUCwWy0zbIA403OqYUzik66InjI+cM8Xdk/9wc4JpAr11y8fc7Z+K172A2DimWxjyf7jO6Ckekm4j3j3oTvvJY0NNMJNPLyV5nV5+65y+JWQXnrfODxzlu+ng+zrml5mq/oScbw+K/C+fwBR1xKnUN0qoaqaSEOuTWsaNRI3gqAKxCQ1d/6prhDIXHdvS6ukUGZmIyJtn6jJO/fFWd94YsjiF/jW1+Wrm99Q1anpmT9l78k6yQsmzv84ve4JBrTZWg8JsMTUYk/eijeMx/RrINh3TYZnTIQ6VduXWaKF+YsyYOZy8gAO1pY5FtZdaWQX9k2GlsZHWDPzEHHCZVAN2SgecAcEW6orpE+diMgOcsCN2iYpmQqr7hQw02URDLjyIn/z1bB6Hw321ksBnS6OCuY36mAuYk1b5d5rsUEOilZAmcYrBa7+rspQ2VIsmFQ/kgCmumIYWEJEoz3jBsnFraUE5V9MSdz97CYr/hzmCnxXT5NGWwpJ0AsesMvf13JWvOkSMzs0TQeR/xMS2xQ8STkK3OcNe1weyTy8teWdC5qvlBS0mtckGNBUVJKC/IZrFuWM1eUCcMACdbxXWvZTeIMT17UiQW4JWIxxXMuGi46I8wM0dVz5xktySYKUCwyLcN9+8TgcFAijGKftcgv+lCY1lQRvzLvtxSLUPpuBcsHP3+PxaYkrk0VXCdMX3D2nqnrLKwVNRmZJ3DNYqxGFVLM6iSbgUWXW2P0ASMeOQDhQyTSwRdYaMHc+ULlmosN5oQus+sYjq/gZ+9K2uWSyIk3JHrkZa5vkD1YjJyre12ti/cs4/FZy9WwIDg0TdpeeoFEQh87K+lkRGZ8dSJm6Uqo1tQsqalJSceiEti0SWSgTxpjI9I+0CtJw5Sk2y/RWFKSQ4MysuOoRL11EpBH0nDqAzo/BiaHxNd7TxyplIRPvC6+ZBwjf9lOAhY5nNfLLMxBKmD2UrSrA9laIsZKrgXX+LAYq+Yuumlscu0sCUp2iNzQlQwRRA7dmEAdc/BonhIIKdMV24IKizVEKCCfh8Yzq9arhaqZokwTHRRW/8rZCsO8ZMNW2tYXNFExdM7xgahEnn+LNzK89kItyL13LpPUx3cepJsi5rpwQUf3ENsLkQm8iWqaeO/fYK4Vq6fo0DgcrJjC+c//xUcSO/xi2cUmSdjlzxWpLzXzlU4zTBc3HIbOb5zN+iuKQuQNJLm1U1V7HwcgXxnqlejh2a7x40DZyn6h+Zpl0nUxx4fZNZdAXVnnOsaKwMltETqNi4aUKoQgryx7XXJ76FzI2ca8Tg2uTzQuamyRwNkPlRlRIiYxFCDsj4tF1JxlEct2SDa9ty+p601tHmbuOMD+ExI4+zGv7fnFH56nPffZ4cc1G5mrSwkslplphzgh3GNAOqAoqHC/sZOXGYVgNAu6Ukuwz2EwslRgN2+wtyqTsJJAMPnUOAsrhYB7EcYNcB+hcmYexjRLhRzzlYzT9pMGihBQfSS37cv+DETZf+lz5gNDDptjWAYp+Ui/RCroRNfw9GJ5HL01VIzQ5k3i1BzSnJyUUMAjuscvBhb+JZC8d1c8m7aws+feuEki778jrpZWaThwQJrXrZLVGzpl29/5OTl4dLsc2NYmq4/tF8f23VL3xfsSHx2XR1uOy4SvUaKDIxJpWSXpWEx0kAXYuPc+UB2+DEAuSmWiZcO3bXJIksJC0jmnRvMRexUdJIpB7/4Q8+GQT6djpikjv7SkmqwAJ2J0/TQbmvg3qBby55CIIqOv9wFv7pXK9rAQgbSzkDsnZTS+AM0wEHIeOPO+eO9dV2HXlSCVmmNCAhKKbRM9+qoETr8njnicCyJuQ3+I37mgxuzDpp28QeNnyMfLLmYdDhV439aVMwNRFE4nnTjZ6SwBVHxRhUdId3zX4RWziIVxCOZnHrd8hfus7vG6eS4GcExjYYlzHF3zctstuXYLCwT5x/KTBEwrcD6k8mYw9fXI5XyYO38KKTSMgTCvuO+46Gs2i+/KGdHGR+fK++5coUKg2CKQzoiNLewK1FBo+wQkevAF8d6+MnudxAwjnI3Pfiyu0QGJHX2FBja4xi4lKBO3ChMut7p+VnFNs3fyVKj38ulKVSfZhMvm4sitVTB5fckCKsYd4nuPq/NoOYHmK6FlZb5ScB5ac0nswAlJa5pSYFnKBMg6b19Wj19G5LmGx48ayVth8HWiku4Qo7dH1nXWiSfgk9jQiESuXZXE7VuSTs4uWMxkQpLdjyR557a4NylXTb23h4TCvXmLuDq7xLt1m/j27BPN55Og3y3tTQHZuKpB9rx2TDb+3V+Wzd/6iuzcs06aN62XxsSkxNfvYPfP9eGPxPHeD2TCUy+j4pdITBfdMMuSPDWPZ1v0QOJohc0WufG4h2wkDyYHHWtUqLnDwZsS5AiQJfC5kLXjD4jv0mk6dKqbtVMRRgyXD6rhcshLUeVi16vS2SynRnII+/rZjWyyGp1ct5X21Xh/6BTFDrzAYXksYOwzSiCIVlxB9mdwFj3/CYmcXSICYxWSt1HIGLvolomOJJw70TX03rpIQsaLOMxTNu8U39Uz3C750kk4klZqipHcBMlmuGDgvAXPncus1FLm+hiz5hYK5BoanbkmGUsOHJfI/luBC7+q4XbTORVmR8sBkIuBQEDmlg+YU+BaYj+fOafnUFmXADqXjHkZ6BYt45bJx92/wbgXzA6XQnLz7jmmLDXY9kGwTgVyXzsnvkufSfCjH4jv1iUaWcUOvqhyUTdso/vsUsJZrNBYIUzbTN58Q72XA+lmjio6WIsAzmm3lJj5tblB8z62zLDczFeKmpthtGXXYa4RYCqFWCUY2aH4hBnIGp5trJzVWQ1Z2Wbd1q0yc/OGrHKmpc6VFkdzs0yfPCnBnTslfvmCOEMhSSd1um86XC7R+3ok9vlnotU3iDk2KoEXXhb9wX1x7T9Ycqv6fB7x+USa6kRSe7dI5JOPJPjSPoltahXdG5C45pF4IiWeSFLCMV2mIwkZT3mlc2xcJsZmxB3wic+jiUubrSXkmxCg+slQ6mJmDs0d4rx2bnbebHKM8g8QI8+j2yIwtPAGSGzw3Mo0xkEy6Yj10wnUAhymfLcvqwBpWNc3tohreEBiXYVnagoBwbGY2cBNCV01Sk81jWQUBiKQt6FjAALJmbhASBnTrNvCeTq4SWIeAYQOUkpU+f0XT4nRtZ4ZdnBQQ3g5yCorw04nF5ix41/i/8OhEfIw/8WTJOv2CqgKhL3PebyCphgw0MC8ZE6YegFobhU4f/EUuxn5UirnxKh4b1+Vma/8HB1QLanssgfkK8P9El0kBzYA3VHkxpUKkuY8JAoNyzQMe7GB7hcWGfoTyCLMQTLBWVW4yRbrJoBAwO0P5w6KKJAIRo9/KdvFgUQKXUyc6zClSmQKNjAJYrh1mXOplBlRDQpQUsBFGJmjVAXknSecoe65x7nKxTAcygLRP3eu8rpLieUCHFBz5JrhKRYRVyI4/hCNVL8tDF08j+6ookdGpYNiL+5xKxVWhIw1coG1BV1Ln4BqhS7HmeOs8AMcjIhKB0PshOMaVpsDrgGokbwViNCmDSR54bv3xBPwS7M/IBPhKUnuOSwudPnu3RKH1ysOj0eRwm99W5xwWxzok+Arr0k6lZLIe2+Lb+9+cVSoN3c2NLJLiA5UaN3cEFZ08WIJXWJxQ+I9reK7d1ai7oCkkrpEPQGJrNsmXq9bWiYnaLJioaxcUtMotaQduTjUrJnmkrTfpSqPaVN1ujrXcuHFG61pSHLNJgmc+1hStkUmyBUWclYQNqQkIIpV3dBgeNK5lmQKpiFOkFSfX/yXT9NdVJG5KBeOWECigwApJ2SniA/A7/EFQooFKAhSYufBLJFKxqPiv3BK0ppL9K617AQiIw+LTHu4bWz/86pDaL/hOJycPyy2HfF5UZlk/EUZIMoCHRB0ZBgRgc6Gw6GGv0+/yxk8zF/RKn6ojzeY5Q50fdExnk8ocSFgW0AS4w7WUVJbTLbHLt6z5K7ocvHY99y/Tpe9J4K0Kf6rZxjFYpYgWkpivVW8ty7zHMGC3+7Qp40NsvMLoo7ZV++9aywWwPTgWSHtjwO4Bhe9DmMeestuymMR/TKfRTYJIp7fdo7y+oZCoWmoIpV9nqlKYC6ZuaQpI3N/ecznO14b779cAa8MUJScjzQW6hSMUqShdsG9APfZzKjESgVmcV39j/hvxFb4bl7g/TVf9v04UC6iyAL9EfBVQw0Z1OSaKxCBdWvFNFIyefmKuBsbJB0JS8CrSd2FT8WIJ8TxrZ+Xhp//Ban78tfFt++AeDZsEvfadRI49pw4gyF29JyNTZK8c6vi18Q8n6ujU4yhwiHCbpdT6oNeafOmpW5mVDr8ItsO7ZCNR/bIulBaNvVcEq/DlPT0lIykPDI0FpXh8ajEB4cl6q8XwzBz4hzmzAoM9fHL6JwlmCAYME5hGHHXWnFjJg1OeLggYgYHM3t2hyzMp8E6/f4N/i/+ltkKVcoN8RwwbPBeOyfuezdEGx3ic9GJzZpjQmh83wMuHiA9Cp5+j0QPBA/AgiN67FWGkds7ZXCpNFraxWEkxdP3kGYRBbshuKHn5+PhtQOhol01bBNYKlcCVKJBwHFTcw/1KfkdFs6XPhPT48tWaDkPWSDmYrkBpBqEGcR8sQBpHwiC6fXT3Kb4a69M+dZCgO4X5kkhl8wH5dxV5jBWC889nOPOiva3vm4r519dPfdFhwGU3ThiYpTFGy5+02mJHnlFZRSW6NzWsPiArBbmXxXPONuRTFBSnz+TrDpPO8V7/+bCcwwdDl4HnJPjqoj0OCXshiGBMx8uSiQIjnNnrDqSByIEYynky0L6zLnw8ZHZUYkVCiiO4DTtvXWJGa+Yt7dHLz1OlMonraGGUli5Z+AzDFcoJL72Non1YnHtEKfXK/U7d0jHrq2y7W98Q/xBn4xNx8XA4nPjZnbz8uE/ckziVy+LGa08ENjV2SnGYPGbrBmJSPi9t/ncIE8ut0uad2yVVV/9snRtXSfbH3wuXWta5dDuVbJncwtn/4KJaUkEG2UqkpDhiRgz/EAARyZjMjETl5loUmZCreKYGBNtakwMm5Od3r5aNDjcwQglnabhiguW3Drc0SbpymbFJmT/ZvVGkhcs6hz5nbAKARKF7hojIxJxGsnEdx/NeS6EVnu673GhCOMTkCJUo9UTZGb0HM6CuT7I8Uq73Xyvi1k1VGHVY5V3LDvWZPILX+R3PyI2UCmGMUHGFhvyKXR3l5PRRj7QiYQMNr7r4KLJvRhlMdxHwo9OLGR+xSy31YzOyjNdWRCcmsT2HuOsVdbEJ5ViLAGMTbAozTcwWiwgcgRd/dje3HOy+Ht1Uk3gpKx29vE4V0jO3W7OuKKAxPy7Z6kru1xAA6ntJGbVAkVAXKvhRGwVF3j9T8RmZypTxoLfIo4RkIClXIxz5jsvdgJqgrTfr6KEFpg7SJJXppOH6x7n9tJpdv1gbBTbe5yfP7F1N89xFHdKZnCuBDiV+YojGqFSAwU9Fq0W4VhZqk5eDTXko0byVijqdmwXM5GQ5MSE1O/aKZ1vvC5Nhw5KQ8grG1c3SkdTgGYomJMr1CHT6urFu32nxM6dqfg1YdSCTp71fPhujAxL8sE9iV+7IuH3fiLenbvEv/+ghF5/U+KXzkvs4jllt3/gkPj3H5LAtm3SXO+TVW0h2by6Xro8hhw8tk2O7OyQQ9vbSf62r2+SNe0hqQ94xImbuykyE2yWCW+jDE0l2AEcn47LlHglKZok6ptZpY3vOKDmnzSXuAe6s7MWOVlYmiaJjTtoagITkzTzIKrPYUInDiTSRFguyFsekbbkoOg+oqpLB1LMDYanJfjJj+fmc5kmzWOQORf89EdiBhsqiyWoluQhx7DChQC6ih4s0F0uie49pswmEnF2TbOyJ4cjK9lcljB0yl9RhU3lWeAvBJB5gYSDDIOoI7oCZjiFjiU4sj5rnTwAsmDMl4LkIWg6cOYDHnuIGMHiGt2VQp2+hQDnF2TQmCWtWE6ZsXqHoyYWrNb5ga6RFRnBTt4yLmQ8C6D5FByTqwmpTqepqECQOFQgJHqZLh5VFchs8/rpSLxgguQPKGMwe97rIgMEz3P/pnhuX+b7xXUXXZ4YZs+dzuq2TQHQUTJa/DjH+YqcWhibBT/9sQTOf0p3Z6uzjdly3C8YrZSfj7cCgQJn/MAJNdNrzfHPp5u8EMBwLRF79gqFNSwKaiRvhSK4fr1oPq8YMzNSt21rzu+8bk3WdtTJxtUN4nFpMj4Vl3hybvXJu3M3M/T0PuU4mUbuXLxwODCghRB94BAzPEOCF79wTqKnT4oxOCBpwxD/oaPi3aYqo1pjk4S+8nVJjY9L9OTHfLxn02bx7Zo1vTAnJ8VZVyden5dSz9ZGv6xqDcn6rnrZsb5ZDu3okBN7u+TY7k7Z8uVXZP2br8iujc3sADbV+SgRjazeIjI8IMatazIyERVDnDJ04FUZPfqGJJo72flT83yzQCafA0G8vffZ2WEncJ7Awg8dxEKgyyCc+2yLB3Z7UgYXvdmfp9Piu36OixGjbbVEXviycsVa7AFvEhI/O0uVgEQ1EOSshbf3PhfOGPzOz2pix2+ZSjZB/mFuA/v/xQKILjpFlvQWwAKSnWS4vNqgFl3pZ/YGjep+9MirVBygEp7YfZiLQDhOIicNXb3FcvNTHdtTzFSrNBYFYDZYfSMz0kgSL37GhRVMV6wMvEo6HDUsMRxOnnM53by0yXmwYuQG0mAYe6EoB6di90APf4Yv5CQyuNzplLTHM+sKbZrz6jKbbrfK21uqDpahU/odfe4NGov5rnzOz84cOsxwd65bsJyQJmjoVBkFulXY1rcvc4Qg+sJbnEtFfiuKnflB9xhByHd4XpHA9rBJTnmvyzh0Py5k4z1WSExRDcsLNeOVFRyl4Kqrk+DGDYw/KDRDByIU9LllZCImI1MxicbjUh/0ZN0uIePEnB5cM51+n5gzirzVf/NnxBksfIHGXJ7e18cbDrp6mPuDwUshwOwl+OrrEn77R6LfvyeezbOLYsAYGxGtpbS0BZ08n8clvra5Mjt2Ene0S2xbl4T/159L48MzYtTXixYKSVJPSTSuS9jXIqEHD2Qs0ClOp0NcmkM8elx8kbCknQ5Jtbermal5zmTA6KXYzYw3e/NydkaQj4/M0N0T9tJ03US37M5VVurgMLfQwfly4FweZGgVfl6+P3Y9kxJ5/s2C3ZFUY7NyA80Lol8OQJV7sTOYMI+JBWK+gQvMabDo4qInc0NmjiQ6fs/yDdrlKmjljU4fusLomFVMglOpwgYZelL850+yY4sZ3oqB4lPPPXYjcGzHDr/IeT7M0PI9Zt4XZJqQZlNqvYLnjFY6cN5RUcDrbkA8d67xWoprWvT4a3P2DQpn2RxXxMxs3J7p7G/lcQlih4IArhGYOzYHe2nkheMMkvxq5N1O5JjCfXEBBi6lwLifxhZed2IHn+e1BjOF1rUcxArdcsaFzPcYRfxOZi4v/x7h7nsoaW+AhlsA7nupAvc+7BeGylf6krEIi4RWbMlyhtG2ijN6LA4UmIlfCvB+XZNq1jBP1O5WKxQgaI0H9kvj/tIWxR63JqvbQ7JlTaM013llJpLkrJuRyliBt3fQcTPw/EtS/zd/np24xN3iA9wOn19m/vL/kcSdWxJ87c2iBC/7eKdTAs+9KLGL58WM5FbCU2Oj4mqZa/dfKRju7tKkfuN6af7Gt8Q3MiCt27fI8d2dcnxPlxzZ2Sm7j++SLndSttenZF0wLY1GRBovn5Kprfsl7gvJTCIlM4MjMjoZk+lwQsLRJMlhImlwGxUzgyEgzTP04m6NNHrZKu7u2cqzMzrDhWN8z5Gs7MY1PiSxfc8tOcHLSjYrNF/JSqQwV7h5Z3H5m8NJUxl0PpYV0mlFsBsWd5bBNTbEbMZ8cGbC4eBijNB1tXixV7pryN1myKArkauZ++CUBD/+gXjuXssJbMYiEQt3kDvY7Vd0XIwPc1HvP/8pnyvbfXE4Kb9GhyKBY94i55T1+TiLWcMTBKTvqzeKu+euuPofqpnhY69x32Au1g7sKxrndMyadYHw4ZpmHSdWlwTHDiTVcIOMnnhDEjsPsWtVjYST8UAFZqwXC6qz3JGTjWY/3kGuTBirjVV4PhWByfnTPMmmrvN+VShvcqFAd5WmMQuUyz4WuFzqmpWnDlpK4Biuma7UMF/USN4KRvPRI+Jtq4wkhfxuWd/VIJvXNEpDUJG9yZmEpDAL1tpGeSWIo2frNkneu8OYhUJITYyLIxgiOXMW6CAWgtbQIL5duyV6+lQOaUqNjorWsjgLcO+27eLeuk1cGzeR/EGyiq5la3NImg8dkOaH16Xr/kXZNHhdNr3+ghx48zlZu32DbFjdKGt8KelsCYjHo/FvU6m0xBIGtw/m/2gGgznAqbhMRxQJRKcwHUV8gsroKwZUV7XJ8awttTM8I6lgvRpS376PUkgEpz+uqmDWfKXSG6rDKZHjX1LdqFLP29pJu/nlBHRNYW3OsPgycy4VzxSmUkqiGyzQecq4t7phuIPnHexWVe+a1X7xzdnYSnt7dugq6MrC5AhyY/+5T3lOgfChe4E5qEqdUzkjeOMi/42oB8pJ885hxoPkyZJrks3lAagLYKaCuWoEqePaCYku5u1UTqoCHkPnYs0mWHJqSnaOjltGTp2Czb9DBUfrazZxxtZyccaMXUEYupJmW9dRwxAN1/glLNRhHq7cnJuxCJLNQjEKcA8GOV4K2TkIk8MwVszMK44NHhfI1Xx0W/xnPsg57uaFlJGNa8j9eUpF8FQhP6+hBjtqcs1nCJArwpilLuCha+XwRFQmZ5LicTsl6HdTGglDFq2pWfTebvGs3zjHPTM1PibNf/cfSPjH3yfhw2MrgWfHLtF7eyR++SINX0AozWhEnPWLY0hB6emhI6I1zn0/vt17+ZUPs7VZvDMzYpgxWbNJySlNMy0pM80uHohc0lDfMdMII5to3GDcQ0LXxTk6IUbaJeNjUYwciQY5qMvJWUGvS+O/Id1BtRWzeSB1lDRmJC5YgLDK/BilfCSlTufs+6hEflYBSYHNvPfG+crC1nP+0KD8BSHvjpTBKjIq1BVtEzPFan2xyAhlO126iIDZOquKjOiKYqHZOU6ZMBkokomHBYD37jUukmj4sP1A+c/xLMPlolslOyrNpQtW6FCAzEHWhcVV8LN3GfWBzkshl9pCwEIScj+45RU7boqBDpuMw6jPkoQaHj+wr+FYzMzLzD7EvsG8HmfGMtmMrqGewrJBQ2d3BB13fAdBnAOHg47I/sufi9HalRuJgPnzCyc5owllRnLzLtXdb2kXbXykuKR4AcD1BFfEcses3rFaPHevqpm6ecY4gOTh2plFKsVrGeJ+FhuQ3WLGGec1rgFGlefkkwDm/OEgGjz5E+bSYXuhi5zcOus3UC1QaITpUzQQypFmogjM+eIlkgDX8PSjRvKeYbIXCnhkciau3Cqn4uL1aCR73q3bJXHz2hySl7h9UzybtojT7xfvvgMSO39Wgl96k92vcsBjAs+/KLEL52Xm+38lTn9AtObmisPYKwHeWzVAXqAxPKQMZxIJRlFg2+ALRM3vdRWeAyQBNCV+Z0oMT6es2txCMogOH0hgPGHIVCIhRoreneIIdEr77Q9lIghJmFtGZxKigVBrThJDTXOIy+nk90q25UIBQhY89Q4NQUBuYICxYOdJt5tyJ8ojqzAegAsbXE6xQEMAPJwRGVxcwQyCu/chpbDRF75ckBTiveCzFoNzYpQh2DAQAAn3PLiVe6MGYcXz2kgwojlKzjMiDHj1BhrpCAoGtVmKssDxAvlkOZIHWXN89xHuE0SUYLbV2jcgb2WNHmBwdOM8CUK1BA/AYg4kAo6z6G7XOrRPDtz3ecCcXeD0e0reVt9EAjGn8wTJ72fvSipUz+MtduSVooQd83i4fqCLBSLHawEI3sVTvAbEDjwvgbMf0ayEBKV9NWWOLKAtco4iSSSuZeXuD3CAbGrlXKE9f7UakLT0P8ztoNc1FB9LWGAXz+oQcht22XIqlyu0/3977wHd6Hmd+V/0xt77cAqn995HmpFGXbLlxLK9u4mdTdabxHvikz0pe7yJ7Y3LxufEx3uy3s1JWccbO4n/m1iWZNWRNEXS9N4rp3A4LEMOCzrwAfif5374QAAECJAESZC8vyNoZggQRPnw8X3ee+/zGDgrExuSeL1xnNmPf6h2EoxFjKEVFpmN85fyBiFmQfl9DoVYsI9mvlEQkhGRN4uBwCgvtrGz5YDLz+6UfQM+MhRVUGTQSaH+Pm7jBJFgkAKtN6nw6ef538jfC1y7QsqDdjLVD808jASC2B3bd3IrKMSVbpJaFNM+nqJiCjsH+TmG+/tIXz2UwZdpDhAXfdhPuqoyslYmLhiDSojbPb0+hYKhMFcGqa+BHJ4uoupyMpU7WCT6gwoFlQgF/SEKhdUZwBjIIILgROwCC0KYxkSFIOkowgINa1xcrxrUQJxmIxL9S1aTf/EqXiCjxQlObbmIF4DdPEw0shZ5kTCLK4hMTTihXYorYFmII7TMwBCGK3allSlFnh+LsxTARdF24Tj5Vm7kBRl+wdqP7OOZHU0swCocC55g85AhgH6wX3U6GwE8BywM/S0rZp3hCioZeM1gVJIOE1q/4L4XNenhKjDmnyiaI5kCGBNhxz8hikITeH4f76ojqHykzQFuh1JC2c3tpXwMPtIP9JFvzkKyXThB3jVbxIQln8AGC2ag225RpHmhKrSSuhRwzkDwuQ9tnlkQWLCUbAhSR9ZeNBMUggeRKfhse9dsJ9vJgxzdgwgftH3rvW4Koa24v5fFGQvEUYKWb/3gI9WwSG9QWzWznO1lB8iu9jGLvOQYBa6gT1AcAnJs0TINAYnXeLoQ796LWUgW1p1tpIzB5AtB69gggCmQveu+uuFVXs1zp9hoENMVYTyIyBO4aoX4gpJCVez1Of3U3zSfes5eJPvGTVzdC95pZWdNzXUTFTjr2g3kPfIxRVavJdOcuVlX5dBaaaqduCyhbNEXFFDE4yF9TS2F+vr4+Y2GsMtNpqbh7qCaCIR41giuW0nuD/eRZcUqss0dWohC2AUV7RLi6h/aQSEOh/6EaFRbRyEIWd2x1CMKQ+6FUTRS20yBLmq4g/cVF7XPR73CZNCTDgufqPYI2wtzNkQOu3nYegcWjmwGFP8LHruh8ZUxuC06Wi+TH6HFI7SJok1TbZVaRsb2O4kiL2o1Div0YW6fisLCFgsKtGlp34dFBioBaJnhjLVwiFtlInp9tGKkvmCwSM8kEtBO5l25OaXwnG6YYFcfCXPlbEQQC3LjIi90sCvvXbczdXUV1VNk0emNsVgLmF1wPMEI7zkWmrx5kOI+sRjG7KX1yhnyoMKW6jwUDMTtko+he0BR1MiV0gp14X/mCBtRjGUBL0wcwZpGMt+6RIbCEgqlCK3Hezia9wxVPnQKsInL4CM+jnhWM3ocYjMDVR2cKziixmYnvc9DYZ+XWz1xHuG809G0sAcDfI5CrIf1/DGu5GDDyrcsu4oO2kvhvInzYKb284wxCkYjGXs7J6aaFAyo82ao4Ot06hzgONpMp5Jg4wKyXEt0Vs4GbA7gmPRs2qPOhS5YxoZQvoJiru7BVEgQxsP0+zQJEwYqRajsIazc5VhHbf/0/5Hvw3fpIUK9H3VT8S7kXQ1hqqkl3fZd5Dt3hsPQkZNnqhuFffkUA1GK6qLObKHQwOhDZMNuZ9qoiWSMtXWcSZjczoHXHBdbFp0wIbSJKnGugshxD0dic4SYHfRjftAfokG3nzz+ELm8QfW2anGQRSPg+UGDniwGKxUPDlBACXELKSqDLAij34QKYdavR0ER6aLGJNm0zkFADDPLMJl5PgNh9rEcOlQ1/b6E6hBcK7H7OSQKhyytzfdu8S9Oze1SAyYLtnNHeVcaWVPDIhAaF5DpyD624tcP9Kq790WlvCOPxf2IpitJhCpradoDQXznmppPFtcamUAkTIa+Xl5UIkrDvf1pfo1RTYnPEdRui/lL1YyoZyi7UKePub6G0gRJ4/yTrppgGFCFN6ps2P1OZcXOx0t51ZiNI8ytl9k+3RTNg/Su2ED2Yx9ydUezlBfyAGQwQujdv8VxGvHg84/K/1hy7HDuUaz1aStfWvsoAtFh+GR9cJcrVDjuYLyVqRU5HnQ34ByIbgvLpVNkP7ZfnQPOtvMFDpCllew8OqZqXlyMQsRkIV0gkFhBzxHYXGS30KgAxmafwdk3LTfHtLb8dF0lI3U1YK5Y+90Wwvzn7WtkO/Mxm53NiKxBYUoRkScMAy1/haVFtPg/fJE83b3Uc7ed+kpKaMBYQGZPgOxWIxmiu+XGyioqeOIpCnZ2kOfjg1T00mcS2jBDLicpHQ94zi8f0RcX884/KnmjBZU8PQLisxSUcCWFWc1YwQyfLZpxmA2oDrILKEQSQuwRYB+ACFTI5cH8YJB8ugKKuJzkcgcwQsa30WQRtJ5mHodWULvFyHOK+HvqJ6njhTgWF3DAGwnM3cFRLWYJHv+46+eS9dJJ9T50Om6zwxyMa/dLsV96CKTlHXm9gRfZpq42bpPk16mng9ubkhd5EG2BxvkJ7ZcJGFTnPWT+hRauYNdG7MojH8pfWpHRdCX5+fHCaBoPzKNtFu+nHlUM2LfHCVfYxUOkox0N1Vi8F1o+HcKS7ccP8PfGzybhfUSVAIYWmJ3igysqwrUohZQiD5EHvd3kX5Da2AAVFrzXqOKoTpsNXFFJeC5dDyjQOLJDbDr4uXbeJzdCqPt7SIc8NJudjZSwoeAVkZdX4DNuPXeUfDBwigPmGHyOmMAW6ojVQaabl9QNqMb5bCaFc1HWIi+5srNsPZlvXKCIfXQzfvgsojV1zHN5WoyC0q8aUk3Aa2bs7kiIosGGiR6CeBqKPHZWxibhvVtZP3425+q4R+4tTybez4JlZL14kjcJBGG8iMgT0qI3GqmgrpovNQGF4wMeDXhpwBXg661mAweVs1FJTS23Owbu3SHL/KGheP+lCxS43UqmOc0cjp5vsLsnjFcGB2JiKJ5g+3013w8Cpuch2TZuIb3BQBG0lYSUjDmBGnzfZlPM6CWXhjPpiLVrxlGY1L2ICI2B1jJqmltEIYuNK4L8Cuiilb+owyje+75BH+cJRjs/YwU/9Qmq2sdsLiH73Xv0yF7LhR+8nrHbRhf1qA6WXrtMfVVzyO8J8kwhC0pUJfl+7FQW1lG4p5vnWywnDxL5PGQ+tp98O5/lUGq098E9D60t+KWIAXUs4HhuyutWZ2ccibOSGOzHrmmmWQtcOCS5pIwiXqs6I7h4FRkGM5iuxO4kzO57WORN23a+cEidl1yzjSsgXNXURF44xG1keL09LSuGOVtyPAgWKpdOsrEFHxjBIFluXeHWNtw+YjTxIkcTgRB5yTlnGhDXEbM59ZxfJKLOSRaVcaUDYs9y/Rz5Vm1JbAsbRDVFdV0cLRC4fNygHc/uUGeuMIdTVkX6S6embYvZTEXbCEqwtQ8GuHLknoCct3jQIopFvm/J2timF0yYsnVehNNvfGWHF/1Ztr+PtWWT8/1Q4Y6r1GsxCjCaQgU75yByor8noQ0UnQCm9ruk9p5MPyCoNWfljMZOMIG6dEr9/ZC0LsJ5BR0R2WwmCkIm5DeTkBUQc7ignROB4YhgwPxev8vPC3mH1UjmlkXkO3s6JvIwlxBsu0em2noK3m7l6IR8w1BUxJVGnc1G4YH+mNEMCPt85Dn8Ebt2otUSfzeWlJFlyVIWftzqmeUOZ8TrJQMWiHD0fHCfTA354SKGiqy5tIQsAQ+ZykcWMKgKDrgDHBSP7+OKXiTCLaC4DvmChmoHGbuuUHmdg0J6I4VDYVW4waQyWk0M9faSye+kgcbNZCIdt5viVTSaDWQ06CiInMKaOdw2qdgLyV9UQZ5Vi6n0+D7ynj9LYYOJdH6FIr19ZFD85FKIijxe8uF+Bx+Rt6CM7Lp2CgYCpNOEO1otPS42TcgGtFxhXjFcVk2WGxe4kqNHK1EWwepYvGCWBzvyvGCbBrbgyZge3GODCiz+0IIGYxTMj7BBQlsrVylTORxqYDYFlTnHgTf4+WO2ES1oWrskGxX0PaRgVOShQoqqR6pW35GMH3hBhUVStHsALaKmI7fVFttoCxXmpbhFb4xVVV7oRquUYRtMKVxqZcZg5OqDoa87J8ZFQm7ABg+y7xCyrUTzPeEWieNvoh1RcaxjI0MDm0J4PDAOyhS7gduYOu6SJ76yM1a0ls2HnQnVsmE367jHmzH+xWvYdCohRgERE4/UCrr14gk2kso2qiQTyPLjz1Dc5ki4uJwMl08nVPinFYhLaprPQp0Nc0YA7ewQcWnzZ0XgCTlCRJ4wKgzR+AVcasodnB33aNDHgi9oLSEdFtsPH5K1spICN65xDIOpeS55jx8l88LFkxIRMBr0xSUUunaVzPNbyH/1Mtk3b4tdxwK1voFs6zaQa//7LMwgMljkuSDysu+XDw0OsMAzz19A/iuXeUZPFx/SO4UgrB6VTMpghgMzl8qSzPlgnsUtpB+4T9YVw/OnUMV0XT5Mlid2knmO+vO0FlHt2EDVMzC/lHxzK0lXUUnKB+9QwZ6N5Ck1kf3AexwmH7TYaWDDdgrduUXWznvkrW4ix5UzZOrrJsVRRIGiMlJ6HlG/tYLXDCZnH0UMNnJ6Q2TQh6NupKo7qSGFMymLPEdhdEf8HBkR9RAOZWxD1QxlgnVNvDPOuV2rt9K0IhLmHLrYLrvBqDr2ddxji3PM6SFnbkR0OvKt2KiG+Xqc3PrFbV9RVJv3zliLLcci1M3hxU9y5QLRCYGm1IKS8/Xi3U71eo5IsLSqVUP+/u4HIy50M4FKR2jBspRB0WgVxkJaRF7+gMovqk+ouDoOvaVmgYZC5N2wa/IfjE7HLem8UZHBHdN887Iaxp6jjpdYy2aaYx+fZ1SeYOiCmdNgPUxD9AlxIdhwMd+5ypsmlitnyLdq8/gFWPL5Rfuy2cIxOrzRMw03xrRWYTj9BryLh7WMa+D8AWMdNlXJs/WQMPOQerAwZmDcAbHXXFtELY2lVFtRQOYFLeS+epV6Hrlo8OJlCs1tISorp0gkTKHeuIDVPAHh7xA45gULh1ozowTv3SFT0xwKPXpE4YEBsm/dRqGebo6W4EpelvN4gKuExSVkamrm73O9/aZqxJIH6NFiBJGXI6wrVpH/+jXOHkwmcP0a6SwWfh1iPx8tnXG/7PB3i9VCxYsXktXVT5b6OjKbjFS8cT05WlrI6hmkOV/4HK1Z0Ugr92yg5kg/rXxiM83bvp4qCs3UvHMTNe7ZRfMKQrR2cRUtn19Oc20BstVUc/uqZkCDmAuXO8hust19Xup65OELciN9j/rIZbCSz2ChQEk56R/1kA7zIplmYyIRrhxhkclmIF43GeC8Nx1ASPTDDrJcPsOupLDv1mAB1n6H22ODdc3ZL8IMBq5mcCUsrm0MIg9GK7GhTza+mcczKmjtjJ9t1LucfPuUdz/Yl2Bnzk+jtpEXiliY8nPq7+EA4zGhBNUKZrS6CCMeOB3GrobI6+lMeB7C1MKzs0Wl5Nn8BM9Rurc+Re5dz2fXaj0BICIEs6sjwXELfQ9zOoeFDSr+jKFlMwljRxsLPM+6Hfx5wefd2DXksozPN0xqIgYDb6R4Nu8hvd/LonHUxH2e+Wd3d3BVM/78ooHqO37etMVoUiN07l7P2KYppirCZJAfpQRhWsPtmjYTX6p3rKc7P/1nsrk6ydlYR+HCIhp0BylU20yhy1eoaHt5zLQlH4BJDBvFKAqbw/guXyT7hs0sUGCSgoqb5/DHZFm6jGcOdTBcuH6VdEbTqCt5aAXFLJ59yzYK3L5Frvff4yqhuXlshhC5bFkNtiVmFKFVFfOGY5kdRBsrKrj+yxfJtmbI/CDsdpPv0nkqeOrZrCu6SucDMlars2D4nqIXPk2hwUEyRF97tMCaKqso0vWATDYrhecvIAcqrT4fha5coKIiK3+f+5qPTMvm0aLmWp5DRGsp5g/hWIo/lbjICm49veglj72Igv4ghUtqqfBRL5n7HlJ/2wMKFMW1bPLifijH0OQZJENYRwMRM5E7SJ7GpVRw8Qz1rysmvd7AjwVVQyPmJQ3Rltdo9VJ7jlMBcrUsl09xy5RSXjlslpCFlE6tirm37R33z8OiEi2c8bv2WPhhYWp6cFt1yMTs3+VTaqZjmqo3z1kmV0h0WjVPFaRshDDGmTlkSHKLb/R9wZwmKpMwyMAsDRZqEZNJnWtKEpt4/KgCo1VUqckuS1QYP3jPELDN7bl5YHyESh5avrmimMah1nr1LPlQwc5ldwdaNjHr+gBOxUMdCNi0wNwqciW1zx7EJR4jb8bAXTPa1mrofUgefN6NJjZUQgs9Pk/Ztm0iNxVuu6iq80ZNJKJmiKaZVUZVHhs9MOXCbREjMd1y4gJNC8hx+D0KNC8aNkcMEx6OpEnXpikIOSZ/VtvCjMDksFNhcxP5z56i5u0baHFTKS1sKqX6tcsp/OA+9fe5qXfAxzN9yIXLB9BGCRFmXrSEgnfvUNjroeD9exwHgbbMUM9DbrPE7fR2Bws0CMBRVfIgTGDyEgVh8gW7nyDfmZOxBf6UhsLHVfIQVu96503yHj8y5sdmWbaCArdu8Gup3qdC3pPHeC7TkOXrhp+NeUlj7ZCrIwS5sTzxlz5abfGzgu1tsVlHvdXKt4241VDfUO9DMlRUDM0hmgzsFFpgN3M1GrOm1eUOaqgqpLlVDqoqMtPGtXNow9IaWr59DVWW2qlk/Xpa0nOVVlmctGJ+BS2bW06L55ZTS1MJ1VY4qNBuIkdfJztE4v7NZiMbEhlKy6ms+y5ZzEYWg5hRhLNp78BQ9fBhvzdWTUT7Mz4faIX2+pSYQ+pEYmpv5fYp79ptLLCGmZzodOSfv4x8S9eMKXsrFVgscqUhDsz5wbES5jXm29c4rgJOhanvIDpnmSIWQa3mecjcekWdxRojLN7iK0CIe+DKTPfQz6qoVat5CQ8gyI6weH65yqEURlHJmwDL/7HCRkMWGxsEpcLUdpvCFuuExK4gooZjUOKqaTHb/jjnWzWDUq+KEM3syGgk/9I1sc8XPgeotrOhSzaEw9zyiZxAGDXhs4Q5P4icVK7KAOIcraLYSNH53CwQOfM0j8HMJQt4DZOZN5dSVfOQfRefuzoVoGqMWUthdiCVPCHnlKxczpUwa22tGsdgN1OhvZz0y1uIfA9JN28hO3RioRsKBclhN5HFNHU7roaoyMGCHCYrmJlDSyZaCn3nz7L40+bnTDU1FPZ4SOnsINua9Vn/DLRmspNn/M8tLVN3Td1u0hVM3QwCTGciwSBfIIwwS2koLeWKmf/CObKuHD5blwm9zcYtsDDiQWXPf+M6GauqyLJEnW3KhrDTyTNWmUShsa6evCeOclutdfmq2NcNZeWkPOolo1F1Q8XjGM38pNEAIxgDkdVE1FDPFVdDRRW5D7xP1lJUK4daTjWcrcfItmkrGcuH2gvDTY+T851fUmHzWn4MqBr6AyG1YgjTmagpDSIvPN4g9bv9LO4gBlF19AXCfF0k2tpqMenJYjZwpIba6qr+HE0HaveXLexI6hygUNzMXMrXZRxiKeX9oWUT+Xf1Q68jG704ish0+xqZ7t/mNjEsFlMZEbAAQ2ZhqqBpVPPmLeFKIETYuObxkizRuUWzd8jQAv/mSk008xGvJwQesrMCS9aS/dRHY/75wigJI8vSM8xZd6pRj5kuCiS1KCJkneezMC84AQt/fJ4gnCD04PAJx9Fhtv38QHQsxrApEu5qZ9Mk/8JVw+YIUZ2yH9nHjrvDKtdJ4PMLwYbPIUycbGcOU9hqo8CcRWmfKyr8/sVDv2/QWQCByPNr2eYE5hADNmiM5mGZqxrIQUSWIc5BqNZDPON1gZDD6xTAjGVUTONcgs6F8Ww65QIzTF9CypjyIoXph4g8IedYa2qo7oXnhn29dPVK6nz7XdLfvE7lTU1knDufnHob9Tr95PYEyWoZimSYbPMVzMwBiJDB1/6VgndukwK3xtrahIw/Q2UV0aNHXMnTFWTXrhkJBNSqg3V4i4uxqppjFcxTKfLQQohqpnOQZxTRslqw5yl+vK733uYWVUvL6GdF0OKK7zeZzVSw92m+79EAIW2syfwLES2lpnkLSGm/n9BCayiv4DlQXA/Rla3wwfxlfNUVOHY8xq1farvtdvIc+ZhMjU0J7azIhIz4/Swu48FjwnHlPX2S7wdtnUabntub04HKnRZyD4HnCygccg9n2z6Xn+cJlVCEbxeXXa8+/rhsQ5jK4IqhGAv8F+FqptZian/QxovQlGJpAoHIs9y6TP5ImIPMLTcvUthsIZ3XRfYbF8i94xneIXccepMNX1BJjG+75Hm84uEtkmg9xe2x6PKgHW0ci0PEZgSTTF+S2+/UFk4XuyNiHtN86woF5rQMuY8iMsXnTR0BIeQUGCbxojpVW+QUgjZI2+mP+fcAxBTaMrUZV7QWT6TRCFqvITjgnglByeZRKcxdMEdsun+HDUNYBKZqcY46QkLA+ZeOIPKCAVW8rt/J/0SV0o+v3b1BSk2aynya100/0MubNcgwRYUcnzEOvU9X4c8RMIfB68GvQzDA55R4t2RktaJS6d3wGM8u4t8Qst5Vm7nFFK6ktgvHybPxMT63chUP55KpPDaROYoqHnYE07UPCzMKEXnCpGGtqqI5v/ZvKdD7iFy3Wmlg/wfU9PlXqKzYRn1OXyySAQtRE6oVaHmbhApf/EwaKlBoKYT4cjz+xDBhgPB35UE7B8Bnm/uHahIqY6lEBkSj8rCbzPMyuzZO9GuAllIIJbSpwnETFDz+BLn2vU36AgdHYYwGvD5Fz38q9u/R5gMqXR1sfJMN1qXLKZxUWYPYQk4jVwMrsg/Y5Upe9Plr8Nxm3DEAE53AzRtkWTi0AaDcbyNjfWPK9xnHlPOt1ynY8YBMGVxM+efp1Dk/FBLxGYgXhBxqzyIvzEUu/Ju/J/o/VRSGuCqIGUNVMKrFMBb0OuJKoi9aTbTev0td9S3k7/UMKcUINK2O3XRRMYw9I8zlRQUiLpyCGBWRo50n1IwH4IAYMVvJ2NnGUQnqTr6OxZoRuVkIlfb7yH7sA/IvXRczYcE8Hs9exYGFM9rJXBCtJnNKc4eswcaM103hpEoyt99Z7SxMeV4ILZzl1WQ/+iFXTrzrdiS0kCL/ix+rdWIXpQKqJQMUyqNWTQ20OiIaAaLOcXhfLCsy+ViZCOBaCXdaGH5ABKedqdXpuF07E5iBxcyZH5mDaVq3IfAg0OJbQlEVhFAabcUSFUjbqY/IfvR9NR6jvJr07kE+P/gWr5mQCp/p7g3O7POu38nvld41yG6k9pOHOFIGUTqmrvtsXKM9x+BcdQYPhjZ4X7UYGcv1CzzziFZYfrxTiJo5amVRivbhcZ0fhWmBiDxhUmHnxIpyvvg6O1jsFbYs4EUsIhk8PoXc3gCbtaB6gdkkfE988HquwYJem0mDYQdEl3XJstSirLycq3ijEg09D8lQmXoGAWLSf/UKTTVcyevtocCdVip8eqgKqy8oIPuOx8h9cP+YqnEa4YCfnK+/SoXPvkh6+5C1NAuQpHxC/no4TEpXJ9k2ZBdgDRGWfB/GsnLyPFKdEC1Ll2f/WAcGyDx/ZNFtXbWG3PvfJ/Pcefyz0cLrv3GNbOs3pX58BgPZ1m0k78njZHzuxTEZ2migRdOOFtJxAnHo7x8k1zWi+g1LKMhGNGp1EBVETQj6ozOB6kV1JkULqYKSYbSSmDAyGMHmN+Ye9TE3U+17tfZSRFfgz/CKzRx0bnIOkBlGP6EQkc9Lgxt3k/3MxxyF0N/4IgUbF6omDudPkCkSpkhZJen7HlIo3sAhpJCp9RoFrQ6ewdRV1/HPGytYSCaHRA9rv4uaQmARp6+qV1uxkn4mzCRQjaBo1p76Gsku+oTN42WZhTnZQGz5l6xRNy28bo7fmKzZLORHmu+3qlXE8c7Umi0stJAdGp+tpwFBhIxC99YUeX9jeb46PXlXbSGDa0CNS0HHQThE5puXyHHsA/Ku3JSxdTT+saFbIFUlUwP5pqhUetfviBnM4DwAsRlYsJQMjx5yNZEFXlIFFoHomMWDoMP761uyhuzHPiQrOgJgtjJGA6hcgcfFUTY6PQvQ5PZhYeYhIk+YMkrXraWejw9TwYL5LKjQOlbkMPOlplxdZKItzRkXvq4Fa6O6gbkkfM940VnVmTTP0U8oeL+NnSEx55XytgYji8IQZr3QupkFEI2WhYvTiquI38fB8foct3Mp/X3cYmjMQpjpEOvw/rtkWbxk2OyasaKSrKvXkOfQASrY+0xCVStbQt3d3Lbqv3YlwXETlUP3of3c+mjbsIlfA5jdeE+d4JlFGKiMFXYHtVq5Hda+I/uMrNAg5idHtluHoITbKp4PO7Ae2k+WJcvJWJN+/gsVUmQxBu/dnXJHVcCCq6ONHPPnkr3MkbUwZBfS6EVrF2VhGFJdSvH1QXxm3QHy+0Nc5mOxhcywcJgi4QhhwpDFn85GkUCECtvaaNBeTr6Saqq8+DF5yEyWgUHqXvM4FVw8Sd4V2ylYXEX+DXtI53GTru8h6UJGGvTqiHyquU9B+00KWospAmH2sJv6zaVDc4y8SaSeL7LdKEKrZjoDD1QcrdfOxVxIUalJZ9GPXX/s5sdjvXCCQiUVCa6HwvhBC29gHDOYkwFvHExw9W4YBgO5Md+aI9MkiDsc08NEXjishqu3LM9t8LzJnDgbqzdwnmbEXshVQ9+qLSN/P0Rh61Uy37vJGzTc+pkKdv+8Tt7VW1OH10er9umMYzSDKrShe3AbuJKu3MStur7GND9zEmFhxy2jOn6eNHdxQvUSTsFK3fBZc2H6IiJPmDJs9fWkN5nIc+cuOeYmnli4emcx8qW0yBqbS0J7GUwpXF7VfRBzSSajjkxGLOBUoTjatjHcHg6NEASFL3ya9JaR2zCNFVWk9DzMSuTx4heVvK070v5s3E/oYTfpG7NrTcwW5+s/5+oc5tpQSbKuXptSbEKEqsYlvRRyOilw9zYL3Xgs81so1NtLnmOHyb51R0IlClU3/BxUN9O99qjKoWUx0HqTrMtXqs6XaDs8f4YcOx/nn+186w0WexBBmGHD7ceLOh+ny7q1Fs8l4vFwBTMT1pVr2FDFf+0q2TdvJVN9Zpt8y5Kl5Dt7hkxzmqcsLiEeuMkixiNbIJjMWbZRQ9AFgmEWVREcHyUlHHYcjs4bQuTh74ixCNx2k2HjZtI5CihSqaeaE4fJtG45mVeuJeWSjRrDA2Rf3sL3hdZTf6CZK4wQnNj4QaVYd7OTrHv2klkJUODCWYosruI5RmwQoR3c5YXRk3r75OcE4tte0Ypa8vAhKQXFFHCjm0CdaYxo/zM6qNzpJFf/IG9M8JxlGgEZLirhqiAWmlicwvwCO+oQJMHGuTIXkyM4G9Hj5vZYIQU5FF3YoEDsULIBCwRX2GpX2zIngWB1PZlvXox9ttJV76wXjvGGjXvrXrKfPMj5m6laFTnSBZ/ZuDbT0YIKXuTONXU2uKaBN3/cO58bqmLC6XQKTGTQfo4Wc63dHe8dvsYzAYqiurDy46/lqrMwMxCRJ0wZWOSWrltDfSdPk715zoiLXrR9mYzRX1LF2DCMcJUP7Z0Dbj/5Awp5/GEKIfhVR7xrzw6EWbbFjWahC5fIwO1WoiycItEGCvfKkYSjgc1XusmUQ5EHN0+EuyOqoehXPsfCBbN1prqGBAED91AWXitWUfDmDSp48hny7N/HS91k90i8Rp6PD5LztX8l09z5ZG5qpmDnAwpcv0phr5cKn3qODGWpF1hKdyfZNm5Rq3k3r3M7LAQGqm3G+gYyNTRy/EHg3h0qfOaFhJbO8YAqJARutoSdg1zJzKadEq8j2jMhXpJbRdM+HhjJhE6yqEer7lQC99SIz6uaCU0A+OxZQj7ynjhBwbZ7ZF2+gqwrhju1wsnWbTNTUW3UkXTpIgoUWMlUX8+V8/DixeR89y0yrlvP7wuK9zgXxMtw3/kbFG6ZR/baSnZSDR0ZoKICM+mK1fce5wu4+WKzKJxUdcS5A6DFVK/XR91PFTJecZGrcR4LPug6nJ30Bsw06nghiN18R9tNCpksRB43uYoryFuaOnS9Um8lV3sXz/A5UE0oryejx8lznFTbqP4M3DDONEcYHWix48zESTYQmpXADbgh0YAFwgkzseyIO1kbWJi7LSjimddkF9xY+PjFE2yEpFWo/AuWcRUS83bJjxPRKFylG8/jh1PpguWcO4rKWKiskjdy4K6JFlPjo4ds4JLzamcGDP09qiCPOoWjrZlft7JKMj24y88bAh2P0b907aQ9LmFiEZEnTCn2OXPo0bET5L3fTvbG7AODsQjSAtgrS21qNUBRg6y5tdON9s4At4ZhYQYTFwjFsVT6koGNvnL8KFeiMt0XxBuExkhgse89fpRyief4ETI3zlGrhF2dXGWCOQmy6uy7dvPjRqwBqmYFz75Ayv37pC8qIr3RSI7dT5L7w32kQ7h3NHcOQCw5du1mYxIIQ/fHB7hl0fHYHjYhgZBLJfJQZUELJtovUcnCLBsHz58/w06V2muI77WlEYljxQxnVJR+siScwnRlxPsfZdslV42XLOWYjqkWeYFrV9iVdKIqisiThKMoKrLYRHC9/x63tOqS5lKCD9p58yEec5zhDsQ0jIt40yLu6ziukKMIIyRch80BgPtPbqnG+QKZiNmCiu7gCYWKNraoQjMu6kJDqdxI/uvX2IghbK4g/8XzpF82h0KOIj4faZVKruYP1pMt4iaPrYocD9vIuWobBTwurnz0FFVxR0K8rtMOWaMRs4tsbxMzzcE8I56PaYTqYcKcYsc9dR4oDyrH2QCDC1QS0rbEpSIUItODO+TZuHsiH5qQwoDF8EkPmygh2863ZPWkV4FwnBh6ulKKPGP3AzYZUWqHzhtKdQM7XRoedgyLhTE86iKlZvxVSFTLEDeBz7f+4gmuIsKBF06qyCPF1x1H3qfAvMU8Zxi2OnJX3eMdqRR+Ar3dpMTFJiBCAU6bodJyMt27wW2lYXshOeDEmkVEhjA9EJEnTClYuJSsXUP9Z86OSuRpBPr7SW80kbHAQTaDngOuEW5dGwpzpQ8tXWjTQmunxx9SK31om8OMjiX7Sl88cOBEKDpaEJGtNxJo1cy0mIf4QQVJy6kbL+FwmPwXL7B4Q+UDQg4iD3l/qECiqoIKmv/iOa7coZURLZsQbN7TJ7ilE0LQvf8Dcpgtwx4/4gVsq9fxRcNYXU2B1layLF46/DXo7uLFNldhSkpJX1LKc3j6wqKs5xrHClfkMrzHqGZC4Jqb51FoQM3Im0ggDNGyiUoaXE2ngrDbpVZN49xPcwkqthB4BU88HXNqVY+Rm8NahpUH98mybOWI94fMxcDN6zGRp/T28GYBjk3Mz1pXrU2o/horq9VjOsPxFfZ6OC4lOb+RxT4qutFd71RCin923GcjUGAj/4XjVPDUs7Hvi10XmEtBtEEXKuSfW0MFG1u4nXWw/zY1VRlUcxY2pUHVMMKGN15/kDsVVCMc9XMNMciuqqEIuYPBWLtqLCIjCovCYIDKrh4ng7OfegwlFIKJRHQByNfDORViMbr5BaOcsZwPc0oopM4v6nRcEcpWMCCvDLOPsP8XJgmTmWNOOGfT6+Zja1TCPEfwfOzlU2yMkkAkzG6m7NYbL3qwwblwBVkunyEPR8dEj/lwmAx9PeRbui43j6u+mS+ITzEMPiKlvEZtjcT5YOFKbmnFXByqn2gTRbUP84KhyhpSyjDPN8rleSRMlisIqo+wC3EyxkddCc9NKa0ky42LZCwqYbdgTdRxpfPaOfKun5jsRmFyEZEnTDkF8+dR7+EjFHjUR+ay7HePwsEgdbzxJu+61zz9FFmrhxZ0sH3Hzj1ausqLbewWiCofLmjb6nf6udI31rgGLFTRpphJ5Ck93WRZNrKzI4uf8gqe88vGXj8Twdab6gA5qlh+P/nOnYnFF9g2bWGTEFQ4HTsfi7VuKg+71CpdXx95z55mMxh2kPzoIBXseTJjOyLcQ5VjR1JWN5WuLjJUD7WxoVXT9cF7scrLVALjFLSPQrRD5MF4B897IoEAsCxcyJU0mM1MBb6LFzj+IdP8aTpgFITFUbpZRziNYmNBE3jAunQFuT/az4JNa4eFGMQxl2kjBPflPXGMK8JQKDiG7dt2pI31gLiDoMwEBB7EKIR9/Ewl5izTmS+lA63RqCzi/uxJrrCo5qOKDhGD5w8gqOzLlpNy9waVIIcxS/AZg9jj85kS4r/j8x7u6aII6SmCucagQnTkKNHiBUR9PVRaqqNwVZk6VxhWhSIuXr8qJnFfyGBkYys4n/JMZeLwYrKQ5H/ERW6g6ojqIlcaoy6qWh4jxKRW2URVFNfzz0mqjpo67nIVBCHaiMJIa5CRwhERVRJhktEbuIoXisahTAWYedP5/cOyKJFbB3dMbpdMAlU/zN2Z2m+ruYHRGTVUsnLdQonHlCo+BY/bt2Jj7N94/MaeTjI+uEvmGxc5ciPdBiVaP5HVGZizUA01h+HNheOkCyscI+JPquZBiOMSbw6FjFHMCptvXyP/vCUJDqH4PBm77vPfhemNiDxhysGCr3jFcuo/f4GqHlPDU7Ph0fGTbN5SsGAedbz1NlXu2kEF8+alvC0WGajyaZW+qlI7efwKL2y0uAY4AiIDzGY1sugbCVNzM3nPnqKw250QwB0P4hgQjo2KVTZzfqh45ULkofXTsmwFzxeRzcaZbpw5V1vPoeAQf/rCwliVI+z3U8TnY+MUfA2LfyzSg/fukH3TZq6YFDzz/Ijun3DBZGfM/j6uTGqL0WDrLQp2dpB9y1D+EgQfBF66+b3JInj/HvmvXOJZQtd7b1Hgzm2KBAMjOmTmCrwHzl++xju7nEtYUclV32B7G4X6+9WMxgmqqiC0HWK26IVPjesYw9xnwe69w45/zMRBPKOKFw/eb31BYYK7KI4NCLxMc5O4HnEVuF+ls4MrfyPlNhoqK0k5djhjSzVaRfFZgYDkx2Ey8XGB1uPCp5+n0YLID+fbb3D7aLxoRJVRpzdwBRKRJBrmefO4koz3xJCUx5cOPB/VbEpPDlIr/9ioQCYk3guetQwGWGia5y/g85TOECBrder7h/DSDK1QOUS7O76mtbirYkwVZEh+QAUSApDbR1nQwUsiTG5fkNw+ZcjcBjEcQZjdqJEcuJ12Xyz4wkkmOJEIVV29TH0taynoKKKqex/R4I1b5Csf+jxqbapcfcRjg3GVeyCWnybMQnQ6jgXgeID65qEqXusV8i1bn/bbMBNnP/URBdGeaTJxJEoI8QJTBMQg5hxxsZ3+iFtNYdySCpi6RPQGDmOPYCY4EmbR6luyiSMb1CiRIUGH2AduZ40/F+oNFC4uH4rziK90LlhO1sunOetQAtOnN3kv8pxOJ/3Jn/wJvfrqq9Td3U1r1qyh//E//gdt2KAaZeCX+De/+U3667/+a+rr66NNmzbRD3/4Q1q2LLMphpA/FC1dQvd++k8U2rKJDFlUF3zd3eS6eZMaP/dZvn3dC89T59vvUCQU5ty9TKDSV2g38wVxDRwe7QvSoMtPg54giz8OZDaoiymIRIgm/JsXPAYj79yjhSxd5YdbNUdwnEw25ICpibGhkYXYWEH7Gdwxy599MfY1BIoH792LLYrhbpnwONHWlvQ44TYKu3+dycyPLdjWRpYWtQKRDkM1DGS6YiIP9+v+5JB6XVwlkIO+p1jgYbHtOXaUCnY/wQtj05y57DCK2bHJcL2EIHY88TQp9+/xIh+PB+873v9Id5fa4jpBYtN/4RxHZaBldyygrZjdUpetINf77/AMZ3x+IuYzjVU1KVtR8T2+0yc4dgIto8Hbt8iYNI+XDhyTzl/+gh97fAh9utdXZ7FwGzTai9M9D8ztOR7bTZGAWvHG4/McP8pV7bG0TuN7MHcIsZ7stgrhqbfZEwQtn0fmzefNEMPK4aY02QBBifeUW6+jlXmtcs8/t6SMW2LTAaGEDFJcSnPQPcxziIjJwCaPolYM8ScEGlf24JAaNb/RZhb5+9rbKNxQQQ3rF6oVx5rdVH3kENHqFgobLZzPCEMcpycYO19jdrHg5nXylNSRq8+bttrIAjWS+rmjAmk2qoJWM9nRHhPXNOOqlQgMweMORp8TvqZVJPH9ozH7EnIHRAqLoqjIM927xdVgGB2lAyHmSmUt59oFFizjmTU2Q8kDEHOAyId0Ig+iDS2nqMYZO9u5NTvYOE8VvJi16+1OEHloZ1aqhm+KBaLfk9yWGS6toIjNRsaONg51F6YveS/yfvM3f5MuXrxI//AP/0B1dXX0k5/8hJ544gm6fPky1dfX0/e+9z36/ve/T3//939PCxcupG9961v05JNP0rVr16iwcOw2uMLkYrBayTFvLg1evkKla0Ze7GAB8/DAISrfuiUmCBGuXvPM0/TgjV+Srb6OjKNwZ8SiXqvyobXT51fIG1A4pw8LCSwogkqEQhGFZ2Bibp/N88l/YB9ZEAmQohKBVs1sQ9NRQYP7pPvgh9zOaF68dESxgaoTQtnjM+cA8uV4gV0ydIKHa6fv4nmKbNiUsjqE2SU4fCa8Jno9zzmhddO8aDEpiDXIIPLwc1H906IPUA3kNRJs4/No4YMFHCpRaJXUBKmhto4C+96hwhc/PWmPwXPwA46jgOhOqDhhcfygfUJEHqo8wY4HVJQU2o7PFFbMyaYoqUClC48NRj6o4Lrff5ePXQg3PHb/lYtciUwF5j51FitXMSGuUdnDJkQ2oPUT95vt64LPFB/baUSe0vlAnRU1GHm+1Pnma9y2jFbs8Wy04LMH05lk4MSa6vVFVdP90QGyjGGDgTcrjh6mgsefSHDNjf+84fGgyjdZcCQFWjT5PJldCzyOf9fRG2Rdu4ZMJdGOgZIG8vYtJOpoHeZ+DOGI6qPiD5Dv+gAZtm/ngGu0gobiL3EGOHhlVSGnvjY4l6ODQ5vXdnmCFIpoBjhqy6qWAclfib43Rj2EoZ5KC63s9Ip2WY74CYbIPeCLVTnxzfwtMZ2ozlsmz04mi1Kt7RU/A4KYZyexMZYiE1aNIhm6T779LJyjwlwe2ntR0ULeG+bcvGu3Z/w+5Fzaj+xjMxYYx6SKVZgKUJXWXTtP+v7e4UI1GFQfK2boMMdXm9hSifZN870bFGyO/r5WgmToe0i+ZcPn9EKV6c+l/nlLOfOQWzbz6Pe3MINEntfrpX/913+l1157jXbuVNv4vvGNb9AvfvEL+t//+3/Tn/3Zn9EPfvAD+trXvkYvv/wyX//jH/+Yqqur6R//8R/py1/+8hQ/A2E0FK9cQZ1vvUMlq1amFQX4hdZ75BgZHQ4OUY8HQg9tnw8PHuIZvbH+stPy+TS0hYO2sIAIRGunhxzksxRQ+MYtcsyfzwuI+J8ZeviQrKPYncfOP1oHPZ8c4t15zPJxxl2K5xG4BTfLLnapjO3ew7Dh4nmyrU1sUUGrGFpGcftUM4RYCCeLRX48jU3kv3wR/Vn8vRAIWLzCITPVLBaMNTB3xIsOv4/nk1A9DI5QRZgKYIABUY7npxF+1MuOjBG3hwgGFRMMjD3Qyus58jG3rsZXjXAcYNGf/D7mAqWjnV1Xk6tUqEijAqeahgwtzJMXuADRF4jQAGa4c1qsfJwgQxGiGdmE6WY4cT8Fe/aO+fGjtTVbEA0R6u4mmt+S8np8xhDfwY/LbGahitfAsnR8u/l47mhbTm4VTTf/CCMiLKLYDTROXPqvX+XHGPF6KOz1sdutzmzhx4pZRlTtMeOH9s+RKuOYNwy7nBQJhUYVJzKZoOqPymryHCQMcSC+LStWJpxztFxC/71bZGxuIntl6iD6bFHnE5Woi6pamdMYJvIM6gx3shkPTL5YMPrUFtVYHiTfiSo00QmCSh+3uUaNbyCKtd8vuOA+0PYajM6QY/8lFL0vKFaIPlRKSXNbjf+dg8ev5TnGd+Zxq6xaqVTjiPQ8mqA9P86a5N9zqijWZiXVdlz1cWptvci+VJ+L2tmiCUuew9RrLrCTLDQRpWAvIOvZI6QP+Nk0BPN4mYCxT7BxAdnOHVGz//JFzCCiomkBh7f7kkSeof8hhfC1NI8Vz0N/4XgsA88IF1HMJRpH15kAcYnXFG63sTZYYdqR1yJPQd5RKERWa+KH1Waz0ccff0y3b9+mzs5O2rt3aNFgsVho165ddPjw4bQiz+/380VjcHBwAp+FkC2W8nIyFhWS+86dlLN1Ib+fut//kMKKQjVPPZlS/KAKeP9fXyXXzVtZtW1mg/YLGb8YtZm+6nIHi73ezWuo5+RZ8jU2804wfrHidmaDjpRHvWyoMqqf5XCQ44mnWIjAgRGZata16xMWt5j1QxUPi1HfpQtk37SFv44qGmbrUAVMBt/PoitJ5EVCijpHx6HhNDyQfvVanlfCzxt87V+5moIZv1TRAbEWOWT0wRa/aQ4LSLagR8B4jrLvEh+/unDFbJfSdi+jkQluj9cVph3a8YMqFkSObdVqCtxpzXmMQypCPT1qLqJBT94zJ8m+UX0Pgb64hH9Bh9BqmMU856h+7qNHKU1O0FoMYe67cI5sq9fGXhdsOOiMpthMJccWPOwm+/ZdCaIUF571u3M7QTxPJajSIdYgFTwv+qA9IbdPex7jBQIan4OI2026uOraiLEac+C+eScm8hBT4rtwnl93nueDa2Q4xBsDEHgQejp83WTOuJmFDTPM4HLrapZ5jpMNNqdQGU5+LnAyxnkkcP3asDZzvIf4OsykxgsEC1r3xwOEHy5lRePfJOLjUwnHqpH4Ex0l/miHicWizo3DEVUzuYE400QYDHliYjX6dVQa8b34veXyB1nQAfV2EGuqUQ5/E7fbqmJOLfKH+Tb4mYgtwuuFKiiqoSxEoy2u2p9aa2ysUpkkOjVH15jgTFGlHC1oR0TFyrNux6gETWDOAjYamcp5vFQgS8/cepl0Xk+Cayxn+cVFIQzDaORWTcNAL9+ODVTGGE4fmLeEbBeOq9VCyZ+cluS1yEO75ZYtW7hit2TJEq7Q/dM//RMdO3aMWlpaWOABfD0e/Pvu3btp7/e73/0uz/EJ+Ufp2rX08NBHZG9q4sy2+KiEjjffJsfcZirfnLrtEGDBX7X7cbVts7aGjFksssYKqn11yxaS//gRqq6yUVBv4l1czIx4OjrJb3HQI7dCRkMo9gsNu56p2m4SnoNez/N+puZ53FLmPXqYjC++HNuF53kf2MYvWUaDb7xK4WUrOHAdrZrY0U8lDDiL7+zpYV8P9fZy9SXdDj9EYbBGHcrGz8RiEaYU6fLh8HMwrxW4cY2sa9aRAgOKufPUymSGds/RgkoHIiEKn3qWAtdQ9WjjmIh0sQRc6bx2hQzYTIiz1kfOGmalLMtWkfuDd/lxT3TLE5xU0cqL19H51hvRrDhVyONnY05NaW8nw+IcizxsDixeMvzxwBBk+y6eC9WMYFBlxKoNjxVGLQisV9rayFhbn7LtEMYhhqSF+FTChkcRVTAlt2yiasZmQWlMk8YLxFSo/1FCC+VIYCY0/tjznT9H1qXLhlcuxxgRoD6evrwUeTi+wk4nn+9SYVm8jFz73uEOgvhoClT/UKLKtiV+OsHCP8n8K1fBLkElxIIRAk0TgtjExO8o/v0UrdoltL4ix1FPPLepPS7V5VWN9dBuq8Z9qH/i/rXID9Z40bZV5Nni9yQuEIioXMacXNEui4p10mPWhGKsNVUrVWrjZLiuqIGosIFoMAjb3uggpnozdaZex622uH+uzkZ/JKqbyuptpLPauWtF+1l4TGolVqtaqhVYfA2vD9AqndrroT539Udr4nvMv0uMRgrWNZOp7RYFFq5ImMcLNqT+rGhAsGIuLwSx19+b4OI5GtC+GiooZgdOtLYK04+8FnkAs3i/8Ru/wfN3BoOB1q5dS1/4whfo9OmhBWvyhyiTo9p/+S//hX7/938/oZLX2ChWsfkAsvKs1dXUd+IklW9RbcgVj5ejEko3rKeixSMbLmhtm2UbN9D9f/k5Ve7aycJwouBYgtpaivQ+pJJ5c6mk0MLH38PuVvIubCZbdSG7eOIXmj8QHnKm07L6MKifRvSpx7COQk4nV+kg/ADcCS2woTeb2YDCd/kCGYpKuJXLVFeU8thHRZFbyEJKwkKJjVIyZInBpQ/VG4hI+/wFLK5GmsvznTlJumiOIDt5FhWzwUYuRR4s/FF14ioeArExS7VsBbtlapXNZDwfHyLvqeNU8qX/kFTZO80VS4hDndWmGtFMcFB5qPchv3eo+ti3bmdxpd+zNyZGUFGCk2QqQTZW8FxRzeFKYfzXg8GYu6pt81YWd3gfUTVSYzUekeejA3ycwNQHLcLTAXahrIdYvj9M5EHYZ2v4MnZR1U+mhux20PnYs9nZcAef69DDLrJv2Zqzx4OWUERVUB52XeFzjCpduo07vDZwHw7cupVguIMgepxfZuMM2njAnGSmWUlD1i6vuK+xPQ6uxKLCyBVKzL2HY+Y60SJj9OcMCURDVGxxy2qcEAWaqFLbRtWZStXgB9VGzF+GuKUWopMjPKJPUlEi5NRZKORR8K/Yc9NHK5u4H7+WSRlWuyQ1cayJutjrFnOPVYVuzD02QmQw6obEYvSQVR9l8gsbzbnU6yhUP5cKT+wnf8NcClvsRIiuCfgpZC+Ie35qm60WS4LHECitJOuVs6Sz2NUA9KTcztGAWT77iQPc/ppJXAr5R96LvPnz59PBgwfJ7XazGKutraVXXnmF5s6dSzXRCgMqevi6Blw4k6t78aClExchP6nYvpXafvb/yDFvHgu2znfeZffNbASeRvGypWSprKCufR+Q++49vs/4ymAusdbWkK+jgwrmqdUtnOQxj1e+eBEVlKk77+G4mQv8MoPoG4CTpzsY3Z2MWpJHf7mo7TM6Cly+xBEH3vNneacb1ujs2rlTtWFH5cr5xi8oSPfI1DyXdBiKSAHEECp2aBOE+YUGuyQmBUGngkWiy8nzalj8Q2SlilTAXB5aM21wUTx/lgqff4lFpRd29kkCczzACRH28KjAeY9+wot57PQ7X3+VwitXc5tXPIG7d8h34azattbzkCia3wZRiCB0VKmAZdFirngWPPn0hC0eIaqQ96aJLVQVITJd779Lju271JDt6hpulcRtx+LymIowB70XDVtMh3p71GouFm219aQ0NLGwczy2h48bOK/i2MPrzHNjdeOP+ZgsMHOH9zj5GMecqG1toplHLtGXlvHGzGgwz2lmER3xeqNmTsacik5sGuRlFW9wkKv9I4HWdGyEmBe08PEb9nq5yyHdho6Q/3D7p9nIl8lEnbtUf75WjYQIxJ/q4+L/87whC0pEhChhFoic8Rh1iQXa73QIPc2JW7tfrQKqClmFZz6RS4n7wddBog2PSsxtlkWsnnwVc8l6+jj1LdtMtp4HFLaXUt+Aj+cydXGOtZrzq7qJbKWawUEKB2/Sw/oF5H3kGXKa5fSEIXMfzGZCJGrtwVrXETuJ8zcYyblyKxWcOkRBg5lC1XXiIDuNyHuRp+HArJLDwTEJ7777LrtqakJv3759HK0AAoEAi8I///M/n+qHLIzDabNix3bq3n+ARZ6pqJBK1o4+oNpaVUWNn/0V6njrHXLduElFSyYmLNdWV0c9H32c2BbY0UGVjw3NLeEXg1k/FLjOWX1lDj7xo30Gu4n4EzuZ2HmEm2fQ4yEFxhELl5H36iVSWu+RSfGTvqomtgCEGQEWQFgYwojBWJ++Io1KDCpymsjTZvviRV86sLDC7UJdnaprYXc3mVO4IkJ0wQkP4sRU3xgTgpj5Uzo7RzXzhPmvVAYvaC1EJabweTXnbfCNX/DcIm4LIxD/tcvslhi7H4+HXO/8kkx1DWTf9Ti5P3yfHRpR2cLCNz6UHYYiaAFF0L3mEpprWFSVlSeILVRp9Y4Ccn98kE1w8G9ufe3syNmMm9KntuYO+zpmR+NmMvFagniRa125ipxv4TWsz6n4mGiw6cBiOTrHBiAQ0B442nnZ0QBHS9/5vlF9D1o2fW+8ytXk+Cy93Dwe1QxmulXxNDCriPZb1ztvcks6n1/mNOdsA2SmgnMcdg2l2jlEvGHOUDUyQy6u0UB2a26ONQgq1RU1zXvG/9PM3iIUXFBBoYP7qNnhoYg7QLqmhURN1Xw9xJ020xi/kYznFR6cS5GHnWTYvJxCER2vLXiGEoI1oJDHq7Dg9MDwSI/4D8SoGHhNAmHq8cU/QCO5WtZR6cWj1BcyUsCOjiHi6BHeoo62qMbmLNH6O845y1SmX8Loyfvf1hB0eLMXLVpEN2/epD/4gz/gv3/pS1/iN/+rX/0qfec73+EZPVzwd7vdzi2dwvQFVTGYpwQHBqnupRfG/EHXm0xcAUQ1bySR1/HmWzwHCHfO0QIhipnBMKyNTSYKPOojAzJm7OnDwwFO0AU2/OJI/cuj72wb+VYtoYIVK+jOvRuku3+LT9CRuiZ6hJ08iAOTnmwLl/DJFfN5yHlLB0QDsu80tNm+bB334PQJ8xa08kHsURrre/PCxVxdtO+IN+do5OpJtiIPFUbPkU+o8KWXE957jj84eVydXTKZ2PETxhT4k3/2oiU08A8/omBHBxs5IHcO7nyYbyh4+lkWnZaly9gJEr+ZYLwRbwjDcRqbtpDr3bdUkToBM53aPF6q9wcB4ghmx2uNdkJ+zXIk8lCFS2Wwg6+zCUyUVJ81CDvY9OeN+1yW4HFztEfHA66UAXwGEKw+kbEeiIaA8cpoqtc4DrH5wBX5HD82zOxyu26ajZN8ruJpcOtwfx+F+/spNNBP5gxZibMdjqV4/13uarBv2S6COE9QK2+j+Q4zhXbtJNcH+1gCFm7cQHpHZnOfQMsCCpUUka0k/dwxbywr4agBztCsJRv4ROOi1K/h/9Wk2HxUG/FSAOszT4Bc3mC0NVatXkIg+v0hciqBWGtrvIBmIYortBHMuMfCLaoQp9G4E+067e+p4kZQkYz/bcWi16RmXqZzeNXF/hd1vVUDMPlr3EkVbXudKeS9yBsYGOAZuvv371NZWRl95jOfoW9/+9tkiu7g/eEf/iFHLfzO7/xOLAz9vffek4y8GUD1nsf5z/HaftsaG6nn8JG0s5pw7fR2dJHiclOgr48qtm8b1SILj89SWUm+zi6eKfR1dpC1bvwZZ67r17nN1FZdQWWL5pP7zl2y2R1UveFpUnRG3o3rdwV4cD3g9VN40E0hspDRE4hruRh6HqjAcVUjGpQMN0q0PGaLqa6O/BfPcW4eIhzSgSobFpXGOEFhrK8n33sXKFBdS6bauowLDrStISybKy5xRioID8eWIaoeIHinlcPog7dukHXZCgpAxOr1FPG4yfn2G9wWifen5Eu/FasqokIXbLvHv7nMKeYEYSCC+/IcP8K5bLneSUQlL93rjucKIcxGJ/X15L90PuOMcdY/t6+PzCnMLfB40C6aiYkQvJOBsQFzeW0s8lDtDrTepMLnXpzQn8mOloguGBgcMd4gGfvOxydk55rbv0tKWCDpJ3jeNBt4QXjqBFeIsz3Xcuswqq8TWIGdSaALAC69MBdyvvsmOXY+njYzUshvUIm3LGihwL07WZtFwdQrEt3YSkeqKuZQG+3w2ytzGngMo6Iy9e8CiDh/tD3V41NYCGJ9op3TYHJjt5jIDC+CqKCDvmIzm7BahUTUh40NfoaMbrTZR7511BRHi7TiNJHoz/dGK5QYh4mf6xypSqjNfEbgIhs3S4mbQOuhpTUUJ3inG3kv8j772c/yJR14k5Cdh4sws8hVphMqasjV8yO3rmq4yQjEmbWmmmMZut7/gDp++RbVPvfMqH6+ra6WvA86VJGHP5vGV33x9z7iWTxrdNYUGYLe+w/4/guKtJO8hSpL7bx71n/nPg3WVZG9xBa10Y66lqH3n+21VaEQNtvI2/2QLKWlw6zwM4F2Qramxv0gu8vvT5n9BWMWzLbFA8dPtHHCgAXzeWj9TLe7DBEKEYbMLFT04kWe78rlmNU67zjevsU7/FgwwoEPXy/+/L9TDSzsdooE/GwCY4xzFcSiEt+Ds3q6BTWqkZjjw+NI1ZY6VngGpOfhiLbvqKrxa9iykHRWqzqDmcEcJ+PPRYtQCndFvIdofeP3doaCajXMdfAa+C9eIPP8lpTzpBPlsDkakTeRrUmGkjK1PTsPRB7yCJH7l85RUxg/iA+xrljJ5xND6y1y7XuXCvY8mZcOq0JmLCtWkXnB6MzLcn0+MVRU8DkkXYcCRBlipnApLaQpAeMuPPcYc70ZDgvG6JpINcuJCk0IyKhvAu4DhkBuX5DFqjavON3Ie5EnCLkA4sjbdj+1yHvQwXELaLVEiHr7q6+Rt719VEINYqzv5Cn+u7ejk8o2jZzXlk0Vr3DRwthJ2tHcTMbCAipdv3bYSRW5RUH/IFma66miRhVE2i6XNkyNExhaM3pqayjQ2UWe3j4KF5fHIh60aIehYfPUvxyMlZWq+QYHTXeRPqmVEMKR7dDj2v/idxZ5d1FRyHfmFLkP7Y+ZeyTcBwu7Yr5tsL095sqpRLPctABriB/khPEu59LlnHVn37yNhaOhcOTfMOmCqeOFIJwtIUqTRR4qQVo1MVUVgvPl4FBZU6uGhce9lqhOIn5gJJFhrK3l70drHV5HuKmOV+TBNAfujcmiOvQoOh84g+ceMCeqtzso2HaXd8ILX1BnOScaVM7gsJkv6PF4enun+mGozrjnz1LBE0/N6ONuKoFrchjnyqi7q3nefN7w8l++SPatO6b64QljAL9rdBMU+ZL1YzAYUxq45RNYx9hzLMb8qBD6Q+yIPt2YfrJUEMaArbGBPG33U17n7Rhqr8SiA5EL6W6bDmt1FVcKA/0DvE0EQTZWIBKc12+wyIs/wRctW0rOq9dTfo+/p4dbRjUg1DAoDoMXhPNWlNiotqKA5q5ooaqIi6q8vdSwaik1VBVw7ANaO7GzhRNZ36Cfegd87P6JaiCH3obUYFyYVbBxCExBuruGt2CdPknW1WtGbMGCyLGu38iB0ey6mTSBDlGDAGRjdS0LPu16zFJZFi+NLQzhXgjzBYAWUMeOx3I6dwKjFjxHVLpizzGk8HMMtN4i5y9/Qf5bN/j9igcCFkYfgVs3yf3hPg4J14AwzZTrxXNktXWk3L/PrwOqicmv0VhC0FOZrnBO4ixof0MLLPImLQsXT9pMGhw2seudL/DiLA/MV3ynT7JLpiHqcCtMoKFNnIhGBRuzqTCiEoSxAnGH38uzCV204jcdEZEnzAqQZefv6aVwIJDw9bCiUKC3N0Eg2RsbRy3yUAU0l5XRwPnzXNUb7Q41TBHcd+5Q77Hj9OD1X5KpuJgv8RS2LCB3a2vKBb8fLZhVlVnFPQS6urgKV7N0AdWUO6i5tpgWN5fTojlltKiplBY0llBTTSGVFFq5sgfnT4i9fqefXOZCct7vJLejhDztD7g6qD0ebc7N1JQ5jAuvD9o1seBgURS9D27VvH+PK1gwotCZTRQeHODZOs7di2YF4vb4eakqhrkCghHVODwejSDCwCsqqWDPXt4RD965Te4P3ostnLjF88F9jkJADAOiGWDignw+PAdEa+D7MwGzFTw/VDTZYOZRbw5MV1KIPFRlU5ixzDQ4r85o4k2CySLfKnk4lviz5HZP6ZyYgrb55UPhzkKOX2N0U7hdw87DOI/g/Om/nn9RGsL0gV2fu2eXyJvOiMgTZgVoCcTcnffBg4Sv+xEFUF6ekKFnLi+jEHKYXKNbDGEub/DyVf5zNEB4drz5NvWdPsuPs3TdWp4JTAaiD22GgZ7eYd+veDzDRGEqjHY73wdeC0NSyyKqf+il53iHUjs11xax8FvcXKYKwDlltGBZMxXp/FReV0XkcZGro4seofLX56H+48eJlq7i1tBswHOFGQDEm//COf6a0tXBi1HN8VLbNcSMGnaiUQXUxAm3asbN600EXEm7dzf2bxjO4HHwY6uoJMfuJ1nIOd/5JflvXGPnT62iyAPsi5ZQ4bMvks5oYvMDzBBmquQNVRE7Vav4pqaExzDS4s714T5uh00GFaXUlTy1XXOmA4Fb9KnPTKrDoNaSi/bEVO8H5iEnEzx35O+hFTi5+jwZ4FiGq61t4+ZpFcMx3QjcuEaWJctTdlPgfIQOg/juBEEYDQYe2VDn8oT8R0SeMKvm8jz3Eit0vo7OYaKMq0zc3tk2qvtHlSwSDsXMUrIBQhIzgBCW9Z9+icrWr+OfjcpgKhzNc8h9+/bwVs3y7OeqCubPyzozUM0RUsUf4h5KimxUVFtNNSaFFr7wFBVfPknl109ReccNKqgsJ0N1DTk9QW731Fo+B90B/hOVQCcGmKOhs3z/ZjMLJVTLfBfPU/DuUAumJvKCD9p5Dg6tdhqYr8pVtEC2LZtcievvj80Eaq8PgrYhVtFOalu7bpixASzM4QBa9OLLZN+yLaXYSllFrKzi9iqey2u7m1DtxC/Z5EowFtB4nN4TRxOqvRzMC5GXJOaQF8eBunEREjOZXBk5jbeahxZf5ztvqk6wkwxXMvV6ns2abLxnTrKZElqrhYmDs1DT/A6CMyO6E7DZBHDeRc7obGu/E3IzlyfkPyLyhFmDvalxmHCDSUoqUTaWlk3cj6Wigsxlmd3LsPBGJEL7q7+gwiWLqWLb1qxEmmPuXP6+hOfwoIMsKQxl0lG+eRMLvbHC84ddXVTUMp/m/doXqLS5gfRd7dTy1GO0uKmUFjaV0vz6YmrkeT8rG8PwbGCxlcxGPYu+R4M+FnxwrfKG9WTYvpt8rbc4iDy+BRMiD9UzzABq1T1u1YzO7U00LLYgNNvbWGimy1dDVa/ohU/H2knT3ReeW7aW8Zq40xeXYCiT3TFRpfN8dECtCt4ZEvv+yxdYXBY8sZfnrmBCoxHxeFSzl6TKbaj3IRlHsTkgjB4Idd/J4+S/eoVFNeY5/VcukX3bTrW9eZLR2qRR7YGR0WSBjRqlo4Nsq9dN2s+cjYR9voxuuRD6OAbdHx0g79nTHOfiOX5UDU5P97vqowMpK9LC7ERaNqcP0jMhzBpMJSW8SEZwuRl/D4c5PqH6yT0pjVpGytZLBdofGz/7KxlvB/H46Nhxvu/Kx3eRvSG7gHCAubuQx0PBQSeZigo542/gwkVqeHlyHAPVx1BF7tt3+O+cQbdqJV8AFgqwq4cLqL5oeGArnD4x3zfo9pPXp7BlcTgSprDOQrRtDy98+zA2GfBx9g2cshS3h6wlQ1Wo8IBaGWHxMwmgsojZO+TMOR4ffqxMFAhy9549RUEWvk3cDsoGKmVlVPjsC2zqAuGI/Do/3FiffYF3We07HiPXu29z5Q7RFajsGWvrh91/4M4dMtY3Ttrzma2257xRcaeVfL88yyY3hXufJTKZ2IAkNDiQkF3GrbYGw4QKb64sr15HzrfeoJJ/+8UJDYUHcImFwRKE7WS2y85GuP26vGLE4wdZg9h8QI4j5opxDsf3QfjBrCUZzHFiUw3Vm1TXC/kNRw1dvUw6s2VUubgjgY4dZOaSjNbmPSLyhFkDfvGVrl/H829ojQy5PWQqLBg2m6bNruESiHOt1ELExwq+v+fjw+S9f5/KNm9iF8/RLuZ4Jx4tm3fuUMnKFTRw7jzfTzbzeLkClbxHR4+lvK7v1GnqP3eODA47P75k4FBV5DDzJR4tmyY4v1p18kQAaihCzgEn+bCT7A+SdwA7yTqKXLtJhqp6vj1aSSe6EoWWTc+RT3iRY5gkYQlQeYPNvOfwR1zJQ9uobe16Mi9eQhRUyLp2I7kPvE86m4MdTbF418Lc7Zu38nwenCQhEJPDzjEPpnQ+IPsIeX3C+MGxCZGHS2TDZo7e0I7XmLnOshWxxZjrvbfZ6h7RG7jkOoCeF3ytN8l39gyFHnaT/9IFsq5YRRNJsLWV3WLzIZ9vpoNIFA6Mz0ByjIJt3UZyvv0Gb2hhYygetHLyRgVm/ZBROsGbAkLu52A1V91ciTx0rniic3kyX5vfyKdVmFUUL1vKlwevvcGzbZijGzF24Z7a3um8dp1u/+2Phhm3ZAsqbhCXistFDb/6GSqYN3fM4gQtm547dynk89HApcs8xzeZGAsL2ZVU8SS27/i6umnwylWqffZpFp+jMXdAxc5qMVKh3UylhVYqL7ZRVZmdKv39VLV8MZW7e6jBHOQWUGtvB1nmzCFfIER9TjXuoW/Qx62fyAYcb9xAMuxKN6d5WMD7ZABRWbD3WZ6jwfECa3Tnz/8fud75JfkunCGdxcZiLTlUGrM3Svt9rvpBGCYvzJD/B8MYqaxMcs5V3GdeE3kayv02ruI5Hn+CK3ow80mOKRkr7Frbdpdc+96h4K2bHIpt27yN3NhAmGDCzkEylM38mI58QOlRK3mjBa3w1mUryXvi2LDzJ7satyziyt9UtBjPZvBeZHLDDbbfJ8+Jo8O+ju9z7Xub594Ln3kBO6kUGhjIyeNC+z/GA/Ihd1MYGankCbOOktWreNHTe/QYVT/5RNrbYS7v0YmT7LSJFsvybVuo+4P91PDZX0lZ/UsHxNCD19/g+yvfsmncO6G2+jrq2vcB9R49zrEKxhzv9mcCC1VrVSXnAhrnqOYn4WCQut7/gCp37eAQeWNRIblbb1PBgvQzatngunmLKjdtoODgILlPHmVhW2gialrTQsFQhPyBEAs7n18htw+ZfiGe9eMKSly4O0LjsbzW6XWkH4O4xoJ4qmbX0E5lW72WrCtWEumHt/K5PniPQg/aE4LpYQJj37aDhQN2c+PFHFdzbt4gm1TxphS4rGIhhgvMbzjbbPVaFvYQ5hDqqLqNpwKG99p/8Ty3+uJ+MY8FcYljCC6Xnk8OcewHNjEmCuREGkfRki6MDTZY4nbNsbnlmhcu4lleVHi1Yw73CZFn27CJz0No6ZzIY0VIRLl/jzzHjlDRp34l5iwdD8Yj4Ogc8fsovGR5QuXfd+40t+PbVq1JmPE2FOem5RbHGUfwSIU+rxGRJ8xKSteu4RZHmLGkA1U+CBmD1UoNn/k06c1mCvYP0MODH/EcXzaLfpyEu959j41Oyjasz8ljR9wDhJ7r+nVq+rdfoKkAc3kwX3HMaeKFQO/ho5xF6GhWFwAlq1dT34mT5Jg/b8ziCNVPzEzWPPUku42iYol5SkRG6PV6suiJLCZD4oI2ECJvQOF5P49fIT8EYECt7mGDGm2g2Ke2mg3sGArxlw35YE6Sri3GvGAhL+I1t1G0Ygbvt1Hhi58mnxIi/7UrCbM0CGQH2UQ5CBMHNntQTYXDIRtl6PXc1qiBnDMIP5xDxuoMivca84DIbExuw8NnyrpyNZtqmOd8kSaKsMtJ+oLCCbv/2YrS28NVGu19RTYez+hGozvGcjyaWxZyxIK2cIfZk95m5/vU1dVz3qc2FyxMPNiA4T9vt5KlZeHw669f5fdCX1jEMUPYHOL3ze1iZ2Y4Omvg9wMqtbmaqzSUlJHyMDedBsLEIe2awqwFwitdVIEmphpf+SzVPPMUCzxQvnkjBfv7yXX9RlY/o+eTI6S3WnkWMJcUr1hG5Vs289zgVIC5PLRnom2z+8P9LIYrtm+NXQ/xjOt8DzrG/DNg7mJvSoyTgGGONY2TKFcYLUZu96yrLKAFDSW0OCHnr5QWziml+koHV/T6XWj19HK0A8xgPD714vUrCSHv+Q6EAmYuUDEBcCPlMHmzhawrV7HIi3fG46y/BS15IVxnO2rL5l2OD8FsXPx7ghlLzIEiQHysoDKD6JFkgadh27KNlAftFMxRW2gy6JiAu+tIbo8zCWyw+M6fJeVR74SfP7wnj7F5T7LpynhA2zechLUcPRx7xpqaIRG4cLGEqU8SeA+Ujgc8P+m/dnnY8cTH2uVLXP23tCziWAztfUMnBzb/4js4sKmHip/2e2K86EtL2YxMyG9E5AnCCJhLihMWXthRRxUPzpvejpEXX5hPwwxf9Z7Hc76ghiNn8YrlNFVwJa+7m2cbURqr+9SLMSEM8HzRFtt/Vg05H2urZsH88bV7wugF1T5U7exWE8/81VYU0KKmUmppLKU5NcVUWmQlg0HHphi44Jep1x9SQ977vdTn9LH4i8/3yydwTMKkAwt6LKoD16+RZeEivg6La8Q6eD75iKt9wc4OrvKZRoh6ECYPGFrwQikS5gy5ZFDNC95NzMXMFq7ott8n09z0cSnG4hIyNc8lz0cHaSLg+A6rddaYdaAqiws+b85fvsZVsWyB0Ec7ZDbAlRWvLdrl8Hf+Wm9vVqYrmcyeODKm7W6C6YoGjDtw/hhpToznPzvGNrsuDAGxzaZNtXWkM5qGbfb4L5wjc/Nc3sDh/MOoiy8+91z5S5oh58zbhiZ2Ss0FhuJidl4dzey9MPnMjjOvIOQQc2kp1ex9kjrfeS9tlh5CzjHzV/P0UBVwJoEWVkRSOObNpao9j3PVMxnMC/q6uoYZtIymVXOkdtrxgDk9OHxWltqoubaIK35LopfF0aofLnPri9kEBri8QyHvuGD2DxW/fMCyAG1Wt3gBhl/48YHrqBChYoRqH2yv2bVxFDOlwsQKdMuiJRxpkGojiCt9D9rZxW60aOY6qOiOBJxYg/fuUNjlSnsbGMBgk2C0hGZZqyYqL3CgLHz+JbJv3U6+82e4vTETvgtn2b0ycE9tz8sEx6o0z2NDFP+Vyzmr5AHeMGq9yYt3BKsjo1QDxxLmOhFsnw48Dvf+9yns9Yz7scxmENtjmqMatEGwoToX/3lEK6cFc9pRcB5B10bg+hXeHErVtstxQFEBnwzeL8SdjGZ8QOdwUNiZm8rgZBIJBnNW0cx3ROQJwhjATFzts89Q9wcfxjLj4nHdvMkumKgEzlQwp1i6ZnXaKiUWsAiI942h3QzzfmgJHamdNpfgOWA+Dxd2+jSrTp8QeHNqirjdc2Fc0HtDVQFZTEby+BRu+XwUFX5w+Rxw+TnwHW6fqAD6A2j/DFN4Atu3MHCP2QzkkaGlKuG5mUzcsmffsJkKnng6Nrch5AeYizPFzeLFg1Zv5B0qGVx9sSBn58z2+0PzqTeusQjIBHb3MduVSsRh0QcLdsR4oA1xtC2IWADqC2eHyGNRxO2NqhMuqmrWNevJc+yTEasdEHhwrSx46jkKDwykDSVPME6600rmuarIw6IdC/RQ36OEzZ2xgsoR3jdUfCDQkzcJLEuWcQURVb5UwO0XHRHixDl2OOKm5yGZ6lXDIog2zEeGBvr5eIFhEoy14t8btGNCePkvXyLL0mXpzZ5cqtlTMt7jx8h7fLhLZ6a5vFB/5k2MfMN79hR5jx6m2YCIPEEYIxAhtS88R937D5CSdNJEq2Hhwtxk0kxnbHW1Y5rL8z9EPmH+2K6j8od2T1T/0N5ZU+6ghU0lquhrKKE5tUXUWF1IVWUOngkssJvIHDWFCSoRNoHpR/vnoCYI1VlApyYEg6Fxt4NiLgMObJoBizAzMM+ZGzNgSAfERdjvI+/xIzybE+rq5BavbNr3eNZqfgu3C8aLuJBzkFxvvs73U/jcS7yAjGSwc5/OpiuZ2s7YvXJwMO31EFl6uyOhgoLKic5q4wpLKiDQgnfvkmPPUxxjoC8uYQE1EnC/1JnMbGGPijyODxhqYKMnF5EobAjUPI9NViBYU7r9rt2gujqmeM2Ujg6uZgYzHLNCetDya6qrjzlqckt+yyJyvfMmt+UWPPM8mZLeG55JX7GSK7HISk373jY2cuU+eTNH6e7kY0vL1MsGQx7O5eEz5UTeaJrPc9jt5g0MfM5mQ6upuGsKwjiwlJdT4aKFNHDxEpVv2shfC/QPsOhDFWu2A5HXvf/6mEReQUt+i2QOprea+JKOUDhCYezwhxDurv6JsHe0eQaUMMc/8J+BMHnDYQpFf+ngvjFPyNXFaKUUf3ClkS9qLEQ8aM3DXMZsmX+aLRgbG8l7+sSwKIx40F6HCouxsprc+/fx1yzRkPVsgEkD2jvRaoegY+A7c4rMi5eSdYlaFTBUVJDS+5DMo4hsgcgzV+TPZk3aytj1q+Q9c4qdJNHyCLMRCN/4LoXgnVbyHD1MRS9+OqWRDFo1491RY+eIjZvJ+c6bvPmSvPhGtQvvm9Y+bayoUN+DytTmUgDzVqjiaZiXLCXn66/mLOia73PefPJfvpgwj5d8rsEMMAekL1qS2AY30E+O3U/S4M3rFIbpzhSZg033Vs341xXg38gq1CJQ0lXlcRkJiEDPkU/4s63dj9LWxscujjs4+jp2PJbV48RGA46BiSRw7y6PGXDHQ4bnhuPNc/wYm1ZhHtaSwpHUd+k8b4ii6wEbM/EbYWGvlyLBABmKZk4HlqwGBGGcFK9YQYOXr7CbpNaqWbBggbgX4hdKeTkFB508Yzca4NaZT5W8sQJBZjKqxi8FdjOVFFqoosTG5i9oA4X5C88BNqszgAsaVDOY6mhFEN+HrD8WdDodKYpqCjPgCqhtotEQeDiCQjAqOgOLSe0ykS2iwuSAlixUVNCil7a1q7ODzE3NZCgqooK9z5CxqoYrPNmCtrCIwcBiUZv5wQ492nw1sBgK9fSMvl0zjyt5eO08h/ZT4M5tKnr+U+TY/YQqYDAfd/1qwu0gAnEd2uHStSmmqnxBEFqXrWDRnCwuYYQULwzRToc2vXRgNhOVClTaYt9TUMjmSql+9ljhrMb1G8lYnT6j0bp2g1r9DQRiX1O6OlgocEcBTD7SzH+N27F1DDOq0wV2Sob4SN4wMJnI3DRn3OsKbOLgvuKNXAJ3b/P5Aq7LiF3JtpqH9mA81okAnw/fuTPkO3uKN6zw+fMc/STmIJrq9rjesmQpz8NCGEaSbou5Y2ysmBcvIWNVFYWSXIX9ly+Qa9+7LPZmClLJE4RxYioq5Iw4xCoULllMrhs32YxEUNtDrDXVbKKCTL1sgCAM+QNknCWzPBBwZr2B2zvt1tS30XL+UBlEtS8UirCoc/sUcnn8aoVQiVAYVqfxLXehCEV0+BolBMSbjPhTLxsR0wS0v3k+PqhWl5IqtXDfhEjTqnyoRtm3bBvV/WPnG3OB/mtXybZuI3lPnyTrmnUJ+XyocAXbTmd9nzhm87ldk+cY332T553sO1bHXle4FUKoON99k0UXxK3v7GkyN8/jxabzjVfJsnwlv2YJFaxHj3jhmApuh0XmYVw1NjzQz7OQ8ZUuY3kl+c6kf42D7e38PsT/bGDbvDXnn+XkSlIy2FDAcee/eZ2sS1WnZ7hqamLT1DSH/BfPZ7yf0YAWYs/Hh7g1Ndtq03QCVVCY1tg3bRlzNmY2YPMmcO0Kf+YRr8Oh5mgPhRHU0uVZV/N0NhvPkGITBMc1BB+3jrtdFPH71ZB2n4//TopCBc+9mLaVNPY5GhygcH+/akIUDvOsKirdpvpGNvxxvvU62TZuGTbHzBXFUIiNgfBZxvPxXb4YC4MHvksX2HkaG2eGymquzpsXLWHxBxOZYFsbmRoayPPJQXLs3jsjumJE5AlCDihetYJD0hEtgMWDpVKCphPn8h5kLfICPb1cxZMctyHwWmjtmqZoA4bDZqLSIiymHdG2UAjASOIiOxyhINpEFVUUIige83+oBgYVdZcTS0NExKsiUBOCejIa9bFWUWFqgdCAWMIsDcRGPGhLyoWZDs929Z/hVi4EsyfPdsIABjv8OL9ls/jhhR0c+CbJPGm0sBNtQSHZVq8ddh0cau2btnK0BCpaaMUsfO5FtZoybwFHHcS/5qh8asYXqcD3Qfxg1gpVN/6ejgfDFqpYaPKi2ethsT7sMbfejH1/wvdN0efUsmQ5uT98j4UcBAIW+Fr1F62eMOzJVcsmqq3eUyfIunwFi5B0r9F0JTQwQO4P93GFFDmnEwlmRVEZg8MkH4f1jTFRiWoeWnXxWUc75kjwWEFJKXmPfsJOrBzlUFOnbkRYLKSzWNWL1cKbF8jkNCQZg8XPBaLtGMHuiGfABgJn/UXPNagOwzwM1W/vsSMUrKoiy4pVLAhhAoTjo/CpZ2O3t65cw4IQztNkNPLzxOev6IVP8/XYyPGeOEqeE8dI4VgQJ3cq6NF91NlJrnffIvu2nbyZMZ0RkScIOcBaU8MnyYcHD3F0gAiUuNemtpZ6Dx+Zda2ak4UmzijLjV8IwWB0FhCzgfg3in9wAPUHFfIFQnwJeoJcFURwvDYfqLafShVwKkDLH8wuNFt1fi8hugL+BJv7sWJENfDCOa4MFmCxlCQcsMiCKMKiCk6umeAqXh5X43meLJonmQosMrFwdR/8kBy7Ho+JVbSDOd98nat62iwdt2qmcUiN3V/THJ610kQaql7JVS7+LFdUqovNJJENsaT09pB9xy7KF7AYN5RVcCuxqbqWqzWYG4uZfERz2SyLx1fNg5mQ7+IFKtjzJIuK8OAgb25Ylw9FCExnIHAglq1r15N5TvOE/zyu2C1oocC1q1zFi5/fxUYFHJoDN2/wBkdGjEa+bfEXfm1YhTkeU12dOieXRuTxXGBNLTm2j3x8G6uqecMFQh9CDJtP2FAoePIprvBq4LFYFi0h5zu/5N9j+D77lu1cPdeuJyXEs8jFn3mFfFcuce4fKvuYycMGhfKwix+vfvEKIlOaNps8R0SeIOQADv9etZK63v+AqnbPvDaS8WCtqqTAo0cUDgazikTw9/SQvUkcIicKNm6xGMlqGUEE4pdf1BjGHxgShPhafBUQNUAd6bgSCFD5QwQF58prragmA1cGhXG+bzDBwCwNAs4b1PxILJpQWcrFphIWS2S2kG3lqpj5yrDblMN8pSdrkWcYhUnLRMKVtvKKWLWCW8IGB8hYP3IOJ8weYIYSb/iA6hEqIf4rF7laADEDwebYtnPE+zLVNbBFPc8J6XU8+2RM0RJnqFTn8pIrqRA6mLtMVy2cKmDXj1koCke4ihN/LOJ14lbXRYvHfIxyHMjlS9zCqFWWUG1yH9rP4mQmbKiiyoTjczRztOMFbp2odOFknTzPicfhfO8tFp0jVe3Zcbari4/ZkQQeMFbXsnERKtWpWlGxUZAc4D7sNq03uQJZ+NRzZFuzji8jYYEZVV09HzfJz0N1yx1Qq9A4rz5oJ9u6DarpUdMc3uRxH/iAuxG8+94iWraaqHn8ESWTTX6dLQRhGlMwfx5FFIXD0oWkXcOqKg5GtzeouT+ZnDVL1w1voRImUQSajWRVNzwTQPsnCz+YvITCsTZSCL34SmAE7aMRIgVzT54gm8BA8EWiv1w191Bc0BYKISitoSPDFukw8Lh4nudoYFGPGR4seHJ1/2Z8PkeYBYLDJmzWKYVrXb5m5OE1cr3/LplbFnK7FwjcuJ5yvjEZrRqVDGbQ3Ps/oMFrV0lnd7D5iD5Ta1tcy6YOLp5lZSlbWXkG8MLZ4Q6gt26QY3v+bSBiUYzYCHZAXJPYNozqCeDZpxRtptnABiF6PRniHEdh+KGzWvm6dBmTUwHeW2Ndw6hnuSAwIEYmE7TQsig3m4c9XlTEYBgEY5KRDH0gTrFhQaYUvyySwM+B2ML5I/k+YXSims2kfw3QjomsTrjIuj85RAVPPp3582swkBGbVynAManDJpTJyFVyzBFCaGtgo8u6fBWb0tj2Pse/86YjIvIEIUfghFK0NHdD5jMxLy+TyEO1T3G5yFQ8cyyMZxIQalauAmb3qwNVQZ9fYfdPX0CJVfrYOS2AKiFcQcPk8fq5Gqi1hHI4vU6nGs5EK4RoE8VlJuzcj6ul8vpVXnzBYAAtgrk0B8ACFYsfnmNJdX15BQWuXE5YeCkP7pN9646UlbxcOj6OFbhaWlevpWDrLa4EoLqEqkHhMy+M+T6xAC58/iWeT8TzBNkcl2gFQ3sY8vCwwE4Fzz4+epQw+8jZeEZTVhXUqQBmHah6IHoiHjx+VOBc+9/nYzU+QzBbYBCCds/k1xfzWohxyBeRh/k298H93EqYaZZtmMvqgwdcFZ5s8N5kyudM9xnmc/jZU2z64z3ySVazujgG4s15NLBhxdEQaTaYEKPgO3OSHHv28sye56MD7LyZqZKXDlTTUWF2bNvBfwbb27jSnvz4UYFGy2bw2mXSLZv89ycXSA+NIAiTMpfnzSIU3d/Ty7ELM8HVSlCrgjCIQWxEQ1Uh1VUWUHWZncPkm2uLaFE0PmLhnFJqqimiimIbFdotZDOr0RFmkzH2d4jCRwiUHxgKlEeExKDbz4HyaCeND/OeiWChW7D7SRZV7KiZ488JAphDcLZLE9eAeauwx82LJDjSwQgj2NGR0nI9H+ITYNIQcjpZENt3Pk7eM6d5UWesqMqJGQhef8zvZJurhdcXc36IFkg3w4fqnjb7qMFtuQtaKF9BFaoAC/AUIg5VN3YXPX1y1PeLdjpUeCCOk0HrKgw3UNXOByDe0daAsPLRgJk4vc3KpiWTDY61dMZIpjlz+DhFe2XaSpjZzJVund3On/dM4JhXOtqHfV2LcEgFKmwwSHE8/gR/znAOhLCEMAw+GH5f2eC/eZ03WWBwg3MZZgoREp8M51xu3sYbWaH2ezQdkUqeIAgTjrW6ioIDA9T6139HOpORjAUFVL1nN5nLSoebruR5eLKQW5AjiEuhPX3LD7eJKup8oGoUA+dQmMXAJEYhb0B1DFWUYMwpFPv+XBHUUyw8HqITqEUB1VcUpAqXn41gwed4fA+5P3iPdFYbmZJ23FnUlJXxXJ7/wjluH42EQ+S/epkXQ8mVjeR2TQQQY9GIvK+JBpUFCAvs9qtirIgrF2yksvsJmgq0lk2IE56BHKEtNtB2l6zR1w+VBuu68TuoThRsGDNC1RaOmM633uBFOYRutvivXuHZsVQVHryWMLPBAn2qDVi4nba1lcUsRBuNIpgeLY8jtSlOFZg9RUWSW2LrG4YZxWCzBBsn/N5X1bAQghHPSOCYR2tmvDMqzhMRjzuteRTeX7T6xldHEYFg37aD3B8doIInnh6VA2YElVM4EkdjR9BujOMyXWUdx5l12y7yK9NzA1FEniAIEw4MV5p//d+p9uCKQp679+jB629Q9d4nyFY3dHIN9PRw1U8QhrWJ8pygcUTHUL82L6ioQfAQh5gd5KD4UIRzA7WAePxfk3VKKKjOChLaSaMxEmgNjfsZ+NpsEILYLbdv30XuQweoYPcTXIlJuL68knfW9XYHtzNRMEiDr/+cAu1tFEaAOjLToMAVhe3TNbBj7j2uuuxyFTJNaxZmDtlCfZw28sHbqDSYuMVVA4Y1aNPUT+HcNNoM8dqN1N7Jla+zp2nw1f9HOrOFF6BY2E5XYBbDkRSHPyZjNIoiE8heQ2RI4fOfSnsby+Jl5Nr3Djsgaq6JUwFMdLB5aZ43n41BRgMERnyWWz4BJ18thzMeX9TlF+3bwNw8l7M1YXQyEjjm0V4LYQvDKADHWVRqU3UlYKMGLdaOPU8Nuw7izLZ6HbkPvE8Fe5/JuhU49KiXz/Xa/B3PjeLcb0wvhyAw9QGZyRMEQRj5JG8wkMFgoMKFLWQscFDnu/uoYusWKkDshF7P7ZrFK2eGLbYwFY6hqX9Rs5NaNEtQM3+JV3kQgOweivlAniEMUiCAaPmh3duYEIyaxmiiTwuYj1+0a7ebrmABZd+4iXfKschOWIAZ9Nw+WPqbv60+R7RsNc8l11tvcMsVV52WrWTzhvjXAMHEaOuLeL1qlECKagfaPv2XLpC+pGRcIg8tXr7zZ8i+a/ew92Gq59qwyM00R4bFM9ofschFuyJE4XQHi2m06+F9sa3LbM0PoxkcA3qrdeRA9sYmNfQ6Rd7hZMHOp9FqU3gUWZIQsnB5jTeVySf4tT13miIhJebqinxJ5VEvFT7zfOx2ePwRr4erciOFnQNsWHBFd+58Flx4n7GplAqlvY1bxNNV6iCq0T7uPvAhFTzxFJtG4bMP0WYoTP09EI0492jnBRgy4bHMVKSSJwjClIAKXv1LL1L3gYPU8/EnZG9q5JZOc2mJvCPCFGQJmhJbeqLVQHwv/o3qIKIkYCTDFcOAwuLQoygUUlRBqMZJDAXMQwBazAY1Z5BbRxPFYD6DBbb/xnVVkM1TF0H8OnR28rwOApPtG1W3yog/QBGfj+yP7eGdf88nhxJmzjC/B5OMwmdfpLBzkDzHj5Bp3vxhwth78hhZ128k/4XzWYUxpwIOxzDAgL1+Ome96QKEglYtmQlY16wl15uvq1WgNDEdsQrOtSvk2P1k5vtcsUoNvUaemd2utk5evcyxGaiC6iwWMi9clFF8jBV0pyAL0BqtUKrzaYNkKC7JrlWzuiZvZ9AhsPE+eT75iKvLaLdEFZ/bNOOiPLhCB1OhO7fJkKF1FucF76nj5Hrnl/xZRfUvXesyzj+ofI8EPudht5ucb77GLrd4zyNKkGf4jEn3C7GKWT6ch2KP3WDMu1iSXDJzn5kgCHkPZvIaXv4UKR4vee7dI0tlZdo2LkGYLDhOwJR0HJpTVwch/oKhMLeFakIOwFF00B0gr0+hQCSs3j6ktYpqFUI1gkKr/ME9FD/XnCcuolhAIwMNlTosRDFLRoEAFT3/aXLte5v8N66ptujdnWRZuZqCrTdV10/s7McZNnjPnuLFHLK0YH2vM5l5lz4+ngCLL57Xm7eAIh4P7/BnU/GJB68jHi9njrWkDzoXpga0nCJg23vsCBU8/Vzacz1EExxIsxFKOKYgBHwXz5Ft/SZuCUZsBqrJkWCAgu3tFLh+jWxrJ2amkWfRyspjM2YQFqhQZSXyHtyf9OiE0WLbup0/m/4rl9g0iD/fKTYecI7wHj2ccT6S36+WRVzNxhxeuvMcqoKhvr5YJmg62Ihlwya14l1YyMcYZn89Bz+kgqeeSzBYQhUS54ZcmC5NF0TkCYIw5RjtNipaLIsyYfpVB1GtS9VQBiOZqlI7t4FqraK44O/avzljMFophHGM2xtkcxlkCwK1DVS9jRYujwgKY/TPSclAszt40c0B4OfPsphjg5aduzkwmRdZG7ewIyTy6JAXF2/YggUXHAftW7bHXjfrsuXku3wpJvLY0vzMyZiRA4Se851fknX1ulFt+mAhisyrgj3b80IkC6krxIHbrVwJxiZCyvDzq5c5iD5bkFs4+Pqr7AwLEYHWPa06g7gLtIhOBKgg+a9fS2g91iIwKEULIDYxUOVmB1VHAccJ4BjPZyCaLC2L+IL20nTzlHjeqKBlqsDj84nPKdpbR/qMovJvnj8/q89/csUbM4ThJWqsB+fpRR9zoPVWbBZwtiAiTxAEQRAm0j00y9tyq1m0JVTLFtSEIXuZoDUsGCZvKEwhzP1E5wN5xDCi5gyiEogW0WzD5TVjGtyXBaH0SeYy2Jn3njjGbW8QY5jTAZi5c2zfxW1xmjGDY9fj3Crp2PkYGSoqeb4ucOMa2Xc8lrBYM9Y3UuTMafKePU0Rv18NSK5riC3UYCePRSPCpdNZqw97Hr093OJX8PTz0g2Q56Dy4nr3LTKkmL2EiQnEQrqIiVRgEQ8XVbRJ8iZE3LEP46Dk3MHxAhEKG360B5rqG2OfCf550eM2FXCqhLukpbGJBSJXt6dRVUlvSW/+w50I0Ww92wgiD9V/nLCQP5fOjZPbdW/dooKnnhnzY4UpVGhwgFzvvc3nIpxTUGE17XycZhMi8gRBEAQhD9CEFi7FBcMXVKrYC/O8ICp+EIKIllDbRPVcNUQVsG/Qn3i/WHxy7qAqtPD9EHcQhqhEmkwGFpP9Lj9/DbdDviGH16OlymzmCgQqdvELaDbTiLM+x/yOY/tOdubkBRxy/Z55fpjzHVf/1m8gpb2d4wKQAZfs4mlZ0KJWSbIQeZjt8Rz+iGwbNnMlR8hv0NroeGwPuT7cRw64h1bXJAgoy6Klo67EanOjyWhVMzY4GcOMZzLI5YMDLOcClpQON/aBqOzrS2m+xJsWc+dxFWsmApdN96H9XNkMdXVxVc+yFG3a9thMnBZ5gY0dSjNvh1lFnD/GM0epnmM28qYBxB5y/ODGOdvGQUTkCYIgCMJ0cRHVq7mCdquJUi1ZIfQ8PpjChGNfw9yg2xdkUQgg4BxWNZICVT+L2cgiD2YycBft7fdS34CPzFHTGKVlGQUunadQSRUZ3X4Wf2oGodpOajYOVQCxYEeGFXbNMb+Trnpiqq3nSzpQ7fMcP8bmCxG/j130LIuWpJx18p05xa2lmeZ3hPwBgsux4zHeELBv2cbVHVTilO5usm/dntufVV7OLcO5EHmYT8NxlrwpocHmK1Yri4p4V0hUp4L375N1RfZtqNMNvL46o4nc779Hxupq3uRBbAYMdHC+CNy5TcbKSjI1z+Pog3Sgndc0d964H48ObZxJG1GzDRF5giAIgjBDgAAsLki9Wx2KCr9U83wQcyajmQrsZiorstKAy08P+zwEr5iCpgayLWhWXUc5bkJtF1XNZIg8PrVyaDUbWDhiJi85SH0sCzTMZYUednElBjvwcPZDZlZ8hQRzTcEH9xMc84RpFNWxaQvb9GP2Ey3A2CDItdshzDaU3ocpYztGS+BOK1lXjRzXYCgtp1Bfb4LIQ0UJLYPTqT1zLKByr30+cX5wf7iPZ/DQmhq4cplsGzezQUokqHDranLlHS3hSkd7zLlXGB8i8gRBEARhFpCtWQtaOMuLbVRSaI0JwGSwgAtHkHseIY8vSE5PkAZcvmjLpyr4sNZjw5mIaiID91Dcd3IbG+4LfqPJc4SWloVEuEQrIbBdRyuXKepIiNY579FPyL5tZ1YB20L+garYRFdgIfJg5DFeQoOD7PyaqTKELEY2X4lrNQ623UuY3ZupxH+28XcYLsFEiTHoOVOPTasqKynU0036pJnMYNtdMtbUTmm4/UxCRJ4gCIIgCMNIJe402PQF0YN6Hc8P4lJTbudWUZcnQANuVexhvg8dmzCU8fhDFAoFOYhelXWqayj/LRolod03zwpGRSG7jKKyt2ot+c6e4kUgt4Id+YTn+WZzO5aQGQ4pdzoTQr3HQvBuK5uLZDJwgfkKTIc0uAJ+/x5nt802ULlEVQ6GTBB8mghEFRetucnGO2jVtCxaPEWPduYhIk8QBEEQhPEvKAx6KnKY+VJT7ojFP2imMcFgiDMF1eoeKz2e5ePZPnb6jPAsoY9NZYLs+okZQS1sXl9YQWEykPvmTdIF/BSG2+eSFXxfXA2Muoxqf7JYNOqzdhoVZiYQZcjdQ3UN4mIssPPtndvk2LYr4225ktf3iA2BdEYjhfv7iPQGMhSldpOc6SAqhcPJ41q4YegUOHFsWLwCzFqMI8zqCqNDRJ4gCIIgCDklOYqBTWMsxpSZgunADCEqgBB7gSAqgUFyr11P7oMfUoR0ZNr9FA16FFUA6uIzBdW/hyNhCkYzB9Xr1JZQFpbRP9VZxPwInxcmDsRzwHxlNCIv3iETRkI4gPSlpVlly8EV1vn2G2TfuoOU9vuzolVzJLQWaw0Y18DxFDN4Wqt18O5truzNNgfMiUREniAIgiAIeTlDaMPFoi1VbEQ1RdTlfkjm2jqyNNSrVTue54sKvKiQU0UeRQWielENYyLsNqqE1YqiLxAmlzeo5gyiBTXaHgpQVUR1kR9LtOKo3ffQ3/F17WeKUMxX4LCJeU4NzHOGenrY0ROZdRzjEee+iSqc6723OMDcWF1LEY+bzM3zsn6PkQVobLvHGxIIuZyNrZqZqqtc8ezt4QofZ+PdvsUxKELuEJEnCIIgCMK0oXrHtqxvi8zBVGjGMYicQJUQuYP+IALoVUEI4DRqNRlZ1EEgIpMQwg/fFwpFKACHUTaFUQPr1fnCCBn0EKYGnkcU4ZcfGMorOd8OoCXQvf99np3TFxYRmUw8M1bwzHNchdNiOXC9ZclyUro6SAkEyDxvdO6cqN4VVlRQ4NZNvi8h6T2prCal5yG3bsJASWezc3C5kDtE5AmCIAiCMKuIGceYkReI8MCh61DhS9VymhhKr84Ban/Hn4iVUJQIuTx+cnkVcnv9LAI1Z1HEW+BPYfJh236fjyM3PEc+JvuGTYmmH6EQeY8cJvvOx0jp7IjFcqCVEMHcloVjMwNBEDjCv4XhoHUW8QqYX+Rq587HZVMkx4jIEwRBEARBiJJO3CWG0qe/TWWpjfwwjfEFuUKIEHpc8G+0f2rzgarLKOeAR91F8d0RNrBBFRA/A3/P9HiELEV9WTl5Du0n+47Hhs2IWVevJff775L//FluG8QsncRyTCzGikpyd3WSqWkOvyeZXEuFGSbyFEWhb3zjG/TTn/6UOjs7qba2lr74xS/Sf/2v/5X00YMBO2nf/OY36a//+q+pr6+PNm3aRD/84Q9p2bJlU/3wBUEQBEGYhaBNNL5VVJ3/U9tBETOhKTtEF6pzgHoWdRCHbm+QBSHmBUMhJZYjqEk9/Ct+/lCbD0TFUBOFXFkMqTOI2jwhvkerQMaET9x1Mx3L0uXsdpnKfAUCw759F5uloC1TYjkmHmThFezZq2bnicCbfSLvz//8z+mv/uqv6Mc//jGLtpMnT9KXvvQlKi4upt/7vd/j23zve9+j73//+/T3f//3tHDhQvrWt75FTz75JF27do0KCwun+ikIgiAIgjDLgZCyW018KSvKcONS1VkUcROY/VPiYifwb7SFQqfh62wkwwYxYQoqEJJBvo3BAAGnJ71Bx1+H06jmFqlFSuBrWqspxbWVxgs/LZKCq5dQpNMYU21dxkw3btG0qHN5wsRjrK6Rl3kC0UW0LZ085Pnnn6fq6mr6u7/7u9jXPvOZz5Ddbqd/+Id/4JNPXV0dffWrX6U/+qM/4uv9fj9/DwTil7/85ax+zuDgIAvHgYEBKirKdPYVBEEQBEHIH7AeUoWeKvzUEHm1qqcKOVUYalU/gK/hOnwPKoguT5C8AYXCIVVQxkcIcKC3Eo62kuoTnEa1XEJ8DXOHgjCTCARDXIFf3FyW1sgpX8nrSt727du5knf9+nWu0p07d44+/vhj+sEPfsDX3759m9s49+7dG/sei8VCu3btosOHD2ct8gRBEARBEKYrEGOqscvwRShnAdLwKlzy16pKKcFERpsVxH0jdgLtpoPuAM8XxruL6qO5hKFImBR3gPQ6PRmNUbMZg5pBiBZTvj9ujRwSmoIgzFKRh+ocqmuLFy8mg8FAoVCIvv3tb9PnP/95vh4CD6ByFw/+fffu3bT3i2ofLvGVPEEQBEEQhNkMV+hSiEJUMBw2E5UX22KuoqgcQuVprZ34t8+vkNunkMcb5MgJfyAI48Qhs5moO6lmMgPZB18ZLZ8QghTdo1r1EaLVapYoCkGYcSLvZz/7Gf3kJz+hf/zHf+SZvLNnz3JrJlo0f/3Xfz12u+SB4fgWg1R897vfZbMWQRAEQRAEIXt0cSYv8aByh+D60ujUC9o7kUOoVQQ1IOK09lJETsTnE2J+EB4cuH+rxcyi8dGgn78f96+1oao5h2p8BX/NqE+oDmpCVDOfgVtp8uMVhJlOXs/kNTY20h//8R/T7/7u78a+BmMVCL+rV69Sa2srzZ8/n06fPk1r1qyJ3eall16ikpISNmzJtpKHnyUzeYIgCIIgCJOPVr1TRaQqyCAS3V6FnG4/efwQgSFSosHzqPqh9ROtpBCUqAzyBn/UilSNusD8oCo4EV8RuzLOr1QznGFTGtwnC9mhx6XOHqrXCbOPgMzkTQwejycWlaCBts0wav9ENHfuXKqpqaF9+/bFRF4gEKCDBw+y8Uo6MLeHiyAIgiAIgpAvraKJM4Vo1ywpxEVds6nVvzCLOxZ5OriHQvypQg8yTGsfVcWbgf/tj8ZX4E8tsgK31fIMcR2bywTVCmE8XDUMq26kEI12q5Erg4KQ7+R1u+YLL7zAM3hNTU3crnnmzBmOS/iN3/gNvh4fYrRvfuc736GWlha+4O9w3/zCF74w1Q9fEARBEARByBFau2Y8BrORrOaRv0+Lr0iHFlGhtoEmCzx1hhAVnX6njwY9QXJ6AlwJVGuCiSmGEIJqhIUuZSupIEwWeS3y/vIv/5L+5E/+hH7nd36Huru7eRYPjpl/+qd/GrvNH/7hH5LX6+XbaGHo7733nmTkCYIgCIIgCNlVEWNCLTUwniktsrK7qD8Q4tZSzURG+058LaCEKYAZQ0W13le8wajRjNoaquUQJgtMrU01llOIK6PupiIShRk3kzdZSE6eIAiCIAiCkCsgAHmGMNpKqraGKvw1VRyqjjRGvRo5gXxCCEPMDmpLc/yhVhj5X1w7VOMpYCSjikFNAMYMS2PVxaF5QmHsyEyeIAiCIAiCIAgMG7+YjUQpWkm1tlBthjBZGGoB8yA+UgIVQhdmCP0Kef1qmD2+B3BMRVw1Ev/G9fzzKBLLL0S1ULs/fK/WAgvRqIFvC7LbqZqXiO/DHCIuUlWcPuR1u6YgCIIgCIIgzCRQXYvTVMOF4QhUawY0oSGRp7Z2qncY7w6qXY/bosXU7QtyZQozgzarmcPqUWHE1yAaNSDk4DiK9lSz0UCDngB5kX/o87P4xNwhrtdyFTWdqvUGaj+fn2fUCEcD34/biVvpxCMiTxAEQRAEQRCmsQFNtiA7UHMgjf9aEOH2SSJPu01VmT3WtqiJRa8fQfcRCgRQJ0w9+aXmIWo9pHEZ1ki6iFYy8b2oECL0Hm2omgGOzCKOHxF5giAIgiAIgjALQIREqq+l+no8WrsmOYgqyRZzHkWlUIudSK7YxSqJSphvoxnLoNSnXY9Yi35ngKMslFCQq4AQmZrY0+5XcytFFRLX4098v5qBCIE6ZGrDBjcGcTUVkScIgiAIgiAIQtaw8GIxNd4XzUKVpXZ2LeXWU3YTVVtNUQWEgItVEYMKhWBQE1ZbVZGXiIpjod3MtUTMEeI+0HqqhOBqiuFG9bHieohAs0lPFrOBW06TgbCcSTOHIvIEQRAEQRAEQZgSIMJGyjHUgGjTZgwh8rit1ATBpht2vQJX0+jtAL4Od1O3N0BOd3BoljHOjZQrkaoujFqVQhxGyGqannJpej5qQRAEQRAEQRBmDWqWoNq2mfF6c+r70ExotBgLrXqnVRBjIjGkikCtYmk2jm0GcioRkScIgiAIgiAIwuwwrbFNP8E2FmbHsxQEQRAEQRAEQZgliMgTBEEQBEEQBEGYQYjIEwRBEARBEARBmEGIyBMEQRAEQRAEQZhBiMgTBEEQBEEQBEGYQYjIEwRBEARBEARBmEGIyBMEQRAEQRAEQZhBiMgTBEEQBEEQBEGYQYjIEwRBEARBEARBmEGIyBMEQRAEQRAEQZhBiMgTBEEQBEEQBEGYQYjIEwRBEARBEARBmEGIyBMEQRAEQRAEQZhBGKf6AeQDkUiE/xwcHJzqhyIIgiAIgiAIgjAihYWFpNPp0l4vIo+InE4nvxiNjY0jv5qCIAiCIAiCIAhTzMDAABUVFaW9XhfRylizmHA4TA8ePMioiCcTVBUhOtva2kZ8AwVBjidhspHzkyDHk5CvyPlJmC3HU6FU8jKj1+upoaGB8hEcUPl2UAnTFzmeBDmehHxFzk+CHE9CvlI0DdfjYrwiCIIgCIIgCIIwgxCRJwiCIAiCIAiCMIMQkZenWCwW+vrXv85/CoIcT0I+IecnQY4nIV+R85Mgx5OKGK8IgiAIgiAIgiDMIKSSJwiCIAiCIAiCMIMQkScIgiAIgiAIgjCDEJEnCIIgCIIgCIIwgxCRl4f8r//1v2ju3LlktVpp3bp19NFHH031QxKmAd/4xjdIp9MlXGpqamLXRyIRvk1dXR3ZbDZ67LHH6NKlS1P6mIX84dChQ/TCCy/w8YFj5xe/+EXC9dkcP36/n/7Tf/pPVFFRQQ6Hg1588UW6f//+JD8TYTocT1/84heHna82b96ccBs5ngSN7373u7RhwwYOf66qqqJPfepTdO3atYQXSM5RQi6Ppy/OgHOUiLw842c/+xl99atfpa997Wt05swZ2rFjBz3zzDN07969qX5owjRg2bJl1NHREbtcuHAhdt33vvc9+v73v0//83/+Tzpx4gQLwCeffJKcTueUPmYhP3C73bRq1So+PlKRzfGDc9err75K//zP/0wff/wxuVwuev755ykUCk3iMxGmw/EEnn766YTz1VtvvZVwvRxPgsbBgwfpd3/3d+no0aO0b98+UhSF9u7dy8eZhpyjhFweTzPiHBUR8oqNGzdG/uN//I8JX1u8eHHkj//4j6fsMQnTg69//euRVatWpbwuHA5HampqIv/9v//32NefMHvAAAAIOElEQVR8Pl+kuLg48ld/9VeT+CiF6QB+Nbz66qujOn76+/sjJpMp8s///M+x27S3t0f0en3knXfemeRnIOTz8QR+/dd/PfLSSy+l/R45noSR6O7u5uPq4MGD/G85Rwm5PJ5myjlKKnl5RCAQoFOnTvFuQjz49+HDh6fscQnThxs3bnB7FNp9P/e5z1Frayt//fbt29TZ2ZlwbCFLaNeuXXJsCRnJ5vjBuSsYDCbcBsfi8uXL5RgTUnLgwAFulVq4cCH91m/9FnV3d8euk+NJGImBgQH+s6ysTM5RQs6Pp5lyjhKRl0f09PRwibe6ujrh6/g3FliCMBKbNm2i//t//y+9++679Dd/8zd8zGzdupV6e3tjx48cW8JYyOb4wZ9ms5lKS0vl/CVkBGMIP/3pT+nDDz+kv/iLv+AW4N27d/OMixxPwkigOPz7v//7tH37dl5QyzlKyPXxNFPOUcapfgDCcDDcmXwAJn9NEJLBCUljxYoVtGXLFpo/fz79+Mc/jg0Ly7EljIexHD9y/hJS8corr8T+joXV+vXrac6cOfTmm2/Syy+/LMeTkJavfOUrdP78eZ6BknOUMFHH0ysz4Bwllbw8Au48BoNh2A4AysPJO+iCkAk4PUHsoYVTc9mUY0sYC9kcP7gNWs77+vrk/CWMmtraWl5A4Xwlx5OQDjgZvv7667R//35qaGiQc5QwIcfTTDlHicjLI1D2RWQCnH7iwb/RdicIowEtBVeuXOETE2b0cEKKP7ZwcoLDlBxbQiayOX5w7jKZTAm3gRvZxYsX5RgTMoK28ra2Nj5fyfEkpKqOoOLy85//nNvncE6Sc5QwUcfTjDlHTbXzi5AIXHrg1vN3f/d3kcuXL0e++tWvRhwOR+TOnTvyUgkj8p//83+OHDhwINLa2ho5evRo5Pnnn48UFhbGjh04I8IN8ec//3nkwoULkc9//vOR2trayODgoLyyQsTpdEbOnDnDF/xq+P73v89/v3v3btbHD5yBGxoaIu+//37k9OnTkd27d7Pjq6Io8grPMkY6nnAdzleHDx+O3L59O7J///7Ili1bIvX19XI8CSn57d/+bT7/4HdcR0dH7OLxeGK3kXOUkKvjyTlDzlEi8vKQH/7wh5E5c+ZEzGZzZO3atQmWroKQjldeeYUX3dgkqKuri7z88suRS5cuxa6HxTRiFmCFb7FYIjt37uTFuiAA/BLDYjz5AhvpbI8fr9cb+cpXvhIpKyuL2Gw23mi4d++evMCzkJGOJyyk9u7dG6msrOTzVVNTE389+ViR40nQSHUs4fKjH/0odhs5Rwm5Op48M+QcpcP/prqaKAiCIAiCIAiCIOQGmckTBEEQBEEQBEGYQYjIEwRBEARBEARBmEGIyBMEQRAEQRAEQZhBiMgTBEEQBEEQBEGYQYjIEwRBEARBEARBmEGIyBMEQRAEQRAEQZhBiMgTBEEQBEEQBEGYQYjIEwRBEARBEARBmEGIyBMEQRAEQRAEQZhBiMgTBEEQhHHwxS9+kT71qU/JaygIgiDkDSLyBEEQBEEQBEEQZhAi8gRBEAQhC/7lX/6FVqxYQTabjcrLy+mJJ56gP/iDP6Af//jH9Nprr5FOp+PLgQMH+Pbt7e30yiuvUGlpKd/+pZdeojt37gyrAH7zm9+kqqoqKioqoi9/+csUCATk/RAEQRDGhXF83y4IgiAIM5+Ojg76/Oc/T9/73vfo05/+NDmdTvroo4/o137t1+jevXs0ODhIP/rRj/i2ZWVl5PF46PHHH6cdO3bQoUOHyGg00re+9S16+umn6fz582Q2m/m2H3zwAVmtVtq/fz8LwC996UtUUVFB3/72t6f4GQuCIAjTGRF5giAIgpCFyFMUhV5++WWaM2cOfw1VPYDKnt/vp5qamtjtf/KTn5Ber6e//du/5eoegAgsKSnhSt/evXv5axB7/+f//B+y2+20bNky+m//7b9xdfDP/uzP+PsFQRAEYSzIbxBBEARByMCqVatoz549LOx+9Vd/lf7mb/6G+vr60t7+1KlTdPPmTSosLKSCggK+oMLn8/no1q1bCfcLgaexZcsWcrlc1NbWJu+JIAiCMGakkicIgiAIGTAYDLRv3z46fPgwvffee/SXf/mX9LWvfY2OHTuW8vbhcJjWrVtHP/3pT4ddV1lZmfH11qp/giAIgjAWROQJgiAIQhZAeG3bto0vf/qnf8ptm6+++iq3XIZCoYTbrl27ln72s5/FDFXSce7cOfJ6vdzyCY4ePcpVv4aGBnlPBEEQhDEj7ZqCIAiCkAFU7L7zne/QyZMn2Wjl5z//OT18+JCWLFlCzc3NbKZy7do16unpoWAwSP/m3/wbNlCBoyYMWm7fvk0HDx6k3/u936P79+/H7hdOmv/+3/97unz5Mr399tv09a9/nb7yla/IPJ4gCIIwLqSSJwiCIAgZQDUOLpk/+MEP2EkTVby/+Iu/oGeeeYbWr1/PZir4E/N0cMp87LHH+PZ/9Ed/xGYtcOOsr6/nub74yh7+3dLSQjt37mTzls997nP0jW98Q94PQRAEYVzoIpFIZHx3IQiCIAjCaEFOXn9/P/3iF7+QF08QBEHIKdKuKQiCIAiCIAiCMIMQkScIgiAIgiAIgjCDkHZNQRAEQRAEQRCEGYRU8gRBEARBEARBEGYQIvIEQRAEQRAEQRBmECLyBEEQBEEQBEEQZhAi8gRBEARBEARBEGYQIvIEQRAEQRAEQRBmECLyBEEQBEEQBEEQZhAi8gRBEARBEARBEGYQIvIEQRAEQRAEQRBmECLyBEEQBEEQBEEQaObw/wOAxWbqU/QAEQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "steps = np.arange(N_STEPS + 1)\n", + "low, q25, median, q75, high = np.percentile(paths, [10, 25, 50, 75, 90], axis=0)\n", + "\n", + "fig, ax = plt.subplots(figsize=(9, 4.5))\n", + "ax.fill_between(steps, low, high, color=\"#4c72b0\", alpha=0.18, label=\"10-90%\")\n", + "ax.fill_between(steps, q25, q75, color=\"#4c72b0\", alpha=0.32, label=\"25-75%\")\n", + "ax.plot(steps, median, color=\"#1f3d7a\", lw=2, label=\"median\")\n", + "for path in paths[:8]:\n", + " ax.plot(steps, path, color=\"#c44e52\", lw=0.7, alpha=0.6)\n", + "\n", + "ax.set_xlabel(\"step\")\n", + "ax.set_ylabel(\"value\")\n", + "ax.set_title(\"20,000 simulated paths\")\n", + "ax.legend(loc=\"upper left\", frameon=False)\n", + "ax.spines[[\"top\", \"right\"]].set_visible(False)\n", + "fig.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-06", + "metadata": {}, + "source": [ + "## Widgets survive the cache too\n", + "\n", + "The render cache stores the state of every `ipywidgets` model alongside the\n", + "outputs, so widgets are rebuilt looking exactly as they did when the cache was\n", + "saved. That is what makes static rendering usable for dashboards rather than\n", + "only for notebooks full of text and images.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-07", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-18T20:23:16.019709Z", + "iopub.status.busy": "2026-08-18T20:23:16.019472Z", + "iopub.status.idle": "2026-08-18T20:23:16.029441Z", + "shell.execute_reply": "2026-08-18T20:23:16.029019Z" + }, + "specta": { + "showOutput": "Yes", + "showSource": "Yes" + } + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ff99a72b55384a758fbf488d6fb4b64c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(HTML(value=\"
\"\n", + " f\"
{label}
\"\n", + " f\"
{value}
\"\n", + " )\n", + "\n", + "HBox(\n", + " [\n", + " card(\"median\", f\"{np.median(final):.1f}\"),\n", + " card(\"mean\", f\"{final.mean():.1f}\"),\n", + " card(\"5th pct\", f\"{np.percentile(final, 5):.1f}\"),\n", + " card(\"95th pct\", f\"{np.percentile(final, 95):.1f}\"),\n", + " card(\"above start\", f\"{(final > START).mean():.0%}\"),\n", + " ],\n", + " layout=Layout(flex_flow=\"row wrap\", grid_gap=\"10px\", margin=\"4px 0 12px 0\"),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-08", + "metadata": {}, + "source": [ + "### What a cache cannot do\n", + "\n", + "The slider below is real — it was rendered from stored widget state, and you can\n", + "drag it. But the readout next to it only changes when Python is there to answer,\n", + "and in a statically rendered page there is no kernel listening.\n", + "\n", + "Open the settings menu in the top bar and press **Render with kernel** to start\n", + "one; the slider becomes live, at the cost of the startup you just skipped.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cell-09", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-18T20:23:16.030696Z", + "iopub.status.busy": "2026-08-18T20:23:16.030475Z", + "iopub.status.idle": "2026-08-18T20:23:16.039946Z", + "shell.execute_reply": "2026-08-18T20:23:16.039535Z" + }, + "specta": { + "showOutput": "Yes", + "showSource": "Yes" + } + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "469727b497364583af4feaa9d42564d5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(IntSlider(value=50, description='percentile:', max=99, min=1), Output()))" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "slider = IntSlider(value=50, min=1, max=99, description=\"percentile:\")\n", + "readout = Output()\n", + "\n", + "def show_percentile(change=None):\n", + " readout.clear_output()\n", + " with readout:\n", + " print(f\"percentile {slider.value}: {np.percentile(final, slider.value):.2f}\")\n", + "\n", + "slider.observe(show_percentile, names=\"value\")\n", + "show_percentile()\n", + "\n", + "VBox([slider, readout])" + ] + }, + { + "cell_type": "markdown", + "id": "cell-10", + "metadata": {}, + "source": [ + "## Distribution of outcomes" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cell-11", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-18T20:23:16.043305Z", + "iopub.status.busy": "2026-08-18T20:23:16.043186Z", + "iopub.status.idle": "2026-08-18T20:23:16.162404Z", + "shell.execute_reply": "2026-08-18T20:23:16.161946Z" + }, + "specta": { + "outputSize": "Big", + "showOutput": "Yes", + "showSource": "No" + } + }, + "outputs": [ + { + "data": { + "image/png": "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iVx++eXmua+//nrJzs4u1feHIA8AAACABAIBOZqXH5ZDX/tszZkzRypXrizr1683Ad+DDz4ot99+u7Rp00Y2bdoknTp1MoHc0aNHTVCVmpoqTZs2lU8//VQWL14sP/74o3Tr1s16vkcffVRWrFghCxYskKVLl8rKlStl48aNpw+m3G75xz/+IVu3bjXjWb58uQwbNiykj77+3/72N5k7d66sXr1a9uzZI0OHDi3VK45yTQBnpd/4TN4xAAAcKO9YgVzRaXRYXnv7kqekXHyZs3pMkyZN5PHHHzftESNGyPjx403Q17dvX3PuySeflKlTp8rmzZtl0aJF0qxZMxk7dqz1+Jdfftlk33bu3Ck1a9aUmTNnyiuvvCIdO3Y092vQVqtWrdOOofCiLPXr15e//OUvJth84YUXrPMFBQUybdo0k1lUAwcOlDFjxkhpIsgDAAAAYDtXXnml1fZ4PFKpUiVTOhmkJZkqJyfHZOQ0S6flkkV9++23kpeXJ/n5+dK6dWvrfHJyslx66aVyOvqcGjhqKWhubq54vV45duyYKf0MLtBSrlw5K8BTWgqqYypNBHkAAAAAJL5srMmoheu1z1ZsbOhjXC5XyDnX/22t5Pf7zXHzzTfLM888c8LzaND19ddfn/Xr796928zx69evn8ngaVC4Zs0a6d27t8nenW6c51KeejYI8gAAAACY4ONsSybtolmzZvL222+bhVNiYk4MgS655BITjK1bt07q1Kljzh04cMCUcupcvpPRuX2audMFWnRunnrzzTclEhDkAcBZzjmcNjyN9wwAABsZMGCAzJgxQ3r06GEWWNG5e998843Mnz/fnNcyTs3A6X1a9qmlniNHjrSCt5PREkwN8p577jmTJfz444/N3LtIwOqaAGzN646RN1v0Moe2AQAAitKFVTQI8/l8ZtXNlJQUefjhh6VChQpWIPfss89K27ZtpUuXLpKWlibXXXedNG/eXE5FV+rULRS0BFSf77XXXpNx48ZJJHAFSrsg1AZ0kqT+Bz548KAkJiaGezhARHPC6pper08yl6427bT0thITc3YbqJPJAwAAkYxMHgAAAAA4CLVNAGzN7fdJi+8+Nu1P610rfvfZZeUAAACchkweAFtzB/xy6Y/bzKFtAACAaEcmD4Cj5tsBAABEu7Bm8nTJ0ccff1zq168v8fHxctFFF8mYMWPM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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(figsize=(9, 3.6))\n", + "ax.hist(final, bins=80, color=\"#4c72b0\", alpha=0.85)\n", + "ax.axvline(START, color=\"#c44e52\", lw=1.5, ls=\"--\", label=\"start\")\n", + "ax.axvline(np.median(final), color=\"#1f3d7a\", lw=1.5, label=\"median\")\n", + "ax.set_xlabel(\"value at final step\")\n", + "ax.set_ylabel(\"paths\")\n", + "ax.legend(frameon=False)\n", + "ax.spines[[\"top\", \"right\"]].set_visible(False)\n", + "fig.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-12", + "metadata": {}, + "source": [ + "## Refreshing the cache\n", + "\n", + "The cache lives in this notebook's metadata, so it travels with the file — no\n", + "sidecar to keep in sync.\n", + "\n", + "To rebuild it, open the notebook in JupyterLab with **Open With ▸ Specta**, wait\n", + "for it to finish executing, then press **Save cache** in the settings menu.\n", + "Specta stores a hash of the code alongside the cache: edit any cell and the\n", + "settings menu will report the cache as out of sync, and offer to re-run the\n", + "notebook with a kernel instead of showing you a stale page.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.13 (XPython)", + "language": "python", + "name": "xpython" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.5" + }, + "specta": { + "defaultLayout": "article", + "hideTopbar": "No" + } + }, + "nbformat": 4, + "nbformat_minor": 5 + }, + "widgetStates": { + "state": { + "31a75b329de24cedbdeb6f442769cb13": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "IntSliderModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "IntSliderModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "2.0.0", + "_view_name": "IntSliderView", + "behavior": "drag-tap", + "continuous_update": true, + "description": "percentile:", + "description_allow_html": false, + "disabled": false, + "layout": "IPY_MODEL_b59475eac908417192df8ae5bc3d42fa", + "max": 99, + "min": 1, + "orientation": "horizontal", + "readout": true, + "readout_format": "d", + "step": 1, + "style": "IPY_MODEL_5aecd0017b1e4672acd8a2548e968d56", + "tabbable": null, + "tooltip": null, + "value": 50 + } + }, + "32f127e31f564f08a4a66fbbd887a135": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "2.0.0", + "_view_name": "HTMLView", + "description": "", + "description_allow_html": false, + "layout": "IPY_MODEL_85d20f2cb201451394e404065ac7e57a", + "placeholder": "​", + "style": "IPY_MODEL_d4d02ffea2554040b4c75a7a6f24bd3c", + "tabbable": null, + "tooltip": null, + "value": "
mean
106.0
" + } + }, + "33eaf1ea790b4c25bd6809827057fd53": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "2.0.0", + "_view_name": "HTMLView", + "description": "", + "description_allow_html": false, + "layout": "IPY_MODEL_5b4929a91fdc402ab171f3d0ab8de872", + "placeholder": "​", + "style": "IPY_MODEL_841593bde6634b3887a58011bf81b6bb", + "tabbable": null, + "tooltip": null, + "value": "
above start
56%
" + } + }, + "438550c9abdf4dbea79e6844e1fe6415": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "469727b497364583af4feaa9d42564d5": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "VBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "VBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "2.0.0", + "_view_name": "VBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_31a75b329de24cedbdeb6f442769cb13", + "IPY_MODEL_4ec16114ac1c43dc980aafa207ba8285" + ], + "layout": "IPY_MODEL_743b5555229a4c3aba6c9f905956f793", + "tabbable": null, + "tooltip": null + } + }, + "4ec16114ac1c43dc980aafa207ba8285": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_c23cbbab78fa44e09222b3d2334ec05e", + "msg_id": "", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": "percentile 50: 103.39\n" + } + ], + "tabbable": null, + "tooltip": null + } + }, + "5aecd0017b1e4672acd8a2548e968d56": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "SliderStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "SliderStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "StyleView", + "description_width": "", + "handle_color": null + } + }, + "5b4929a91fdc402ab171f3d0ab8de872": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "70c2afee162f486d92f1a3bcf8202241": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "2.0.0", + "_view_name": "HTMLView", + "description": "", + "description_allow_html": false, + "layout": "IPY_MODEL_ff5b9b72b02643a7b63917090a7673a8", + "placeholder": "​", + "style": "IPY_MODEL_c290c17824d849549444ed23df0c3ea8", + "tabbable": null, + "tooltip": null, + "value": "
median
103.4
" + } + }, + "743b5555229a4c3aba6c9f905956f793": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "841593bde6634b3887a58011bf81b6bb": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "HTMLStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "HTMLStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "StyleView", + "background": null, + "description_width": "", + "font_size": null, + "text_color": null + } + }, + "849ef3dc2f784f9b901fa44c7c4e36b1": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": "row wrap", + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": "10px", + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": "4px 0 12px 0", + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "85d20f2cb201451394e404065ac7e57a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b241fb55c6cd4811b0f87dadbda59b06": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "HTMLStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "HTMLStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "StyleView", + "background": null, + "description_width": "", + "font_size": null, + "text_color": null + } + }, + "b59475eac908417192df8ae5bc3d42fa": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "c23cbbab78fa44e09222b3d2334ec05e": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "c290c17824d849549444ed23df0c3ea8": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "HTMLStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "HTMLStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "StyleView", + "background": null, + "description_width": "", + "font_size": null, + "text_color": null + } + }, + "c7c6d943de2d4153815fc027442732a6": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "2.0.0", + "_view_name": "HTMLView", + "description": "", + "description_allow_html": false, + "layout": "IPY_MODEL_438550c9abdf4dbea79e6844e1fe6415", + "placeholder": "​", + "style": "IPY_MODEL_b241fb55c6cd4811b0f87dadbda59b06", + "tabbable": null, + "tooltip": null, + "value": "
5th pct
72.0
" + } + }, + "c8b7e7ba344447518d0acec8a6855663": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "c945cd25bb214f0fb2732589d9a3dc1d": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "2.0.0", + "_view_name": "HTMLView", + "description": "", + "description_allow_html": false, + "layout": "IPY_MODEL_c8b7e7ba344447518d0acec8a6855663", + "placeholder": "​", + "style": "IPY_MODEL_e83bc44303de48e39b3df05afe222dad", + "tabbable": null, + "tooltip": null, + "value": "
95th pct
148.3
" + } + }, + "d4d02ffea2554040b4c75a7a6f24bd3c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "HTMLStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "HTMLStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "StyleView", + "background": null, + "description_width": "", + "font_size": null, + "text_color": null + } + }, + "e83bc44303de48e39b3df05afe222dad": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "HTMLStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "HTMLStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "StyleView", + "background": null, + "description_width": "", + "font_size": null, + "text_color": null + } + }, + "ff5b9b72b02643a7b63917090a7673a8": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "ff99a72b55384a758fbf488d6fb4b64c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "2.0.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_70c2afee162f486d92f1a3bcf8202241", + "IPY_MODEL_32f127e31f564f08a4a66fbbd887a135", + "IPY_MODEL_c7c6d943de2d4153815fc027442732a6", + "IPY_MODEL_c945cd25bb214f0fb2732589d9a3dc1d", + "IPY_MODEL_33eaf1ea790b4c25bd6809827057fd53" + ], + "layout": "IPY_MODEL_849ef3dc2f784f9b901fa44c7c4e36b1", + "tabbable": null, + "tooltip": null + } + } + }, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/demo/jupyter-lite.json b/demo/jupyter-lite.json index ca0f44a..f489a6b 100644 --- a/demo/jupyter-lite.json +++ b/demo/jupyter-lite.json @@ -29,6 +29,13 @@ "slides.ipynb": { "hideTopbar": true, "slidesTheme": "sky" + }, + "static-rendering.ipynb": { + "hideTopbar": false, + "defaultLayout": "article", + "topBar": { + "title": "Static rendering" + } } }, "labConfig": {