├── client
├── src
│ ├── assets
│ │ ├── .gitkeep
│ │ ├── bokeh_angular.png
│ │ └── favicon.svg
│ ├── app
│ │ ├── app.component.css
│ │ ├── shared
│ │ │ ├── components
│ │ │ │ └── bokeh-chart
│ │ │ │ │ ├── bokeh-chart.component.css
│ │ │ │ │ ├── bokeh-chart.component.html
│ │ │ │ │ └── bokeh-chart.component.ts
│ │ │ ├── shared.module.ts
│ │ │ └── services
│ │ │ │ ├── bokeh.service.ts
│ │ │ │ └── bokeh.service.spec.ts
│ │ ├── core
│ │ │ ├── types
│ │ │ │ ├── message.ts
│ │ │ │ └── dictionary.ts
│ │ │ ├── services
│ │ │ │ ├── debug.ts
│ │ │ │ ├── websocket.service.ts
│ │ │ │ └── message.service.ts
│ │ │ └── core.module.ts
│ │ ├── app.component.ts
│ │ ├── app.module.ts
│ │ ├── app.component.spec.ts
│ │ └── app.component.html
│ ├── styles
│ │ ├── variables.scss
│ │ ├── main.scss
│ │ └── mixins.scss
│ ├── favicon.ico
│ ├── environments
│ │ ├── environment.prod.ts
│ │ └── environment.ts
│ ├── styles.css
│ ├── typings.d.ts
│ ├── tsconfig.app.json
│ ├── main.ts
│ ├── tsconfig.spec.json
│ ├── index.html
│ ├── test.ts
│ └── polyfills.ts
├── e2e
│ ├── app.po.ts
│ ├── tsconfig.e2e.json
│ ├── app.e2e-spec.ts
│ └── websocket.e2e-spec.ts
├── .editorconfig
├── tsconfig.json
├── protractor.conf.js
├── README.md
├── karma.conf.js
├── .angular-cli.json
├── package.json
├── tslint.json
└── angular.json
├── requirements.txt
├── AGENT.md
├── python
├── services
│ ├── interaction.py
│ ├── __init__.py
│ ├── chartProvider.py
│ ├── pythonToView.py
│ ├── junction.py
│ ├── aiohttpServer.py
│ └── backbonelogger.py
├── tests
│ └── test_chart_provider.py
└── app.py
├── setup.sh
├── IMPROVEMENTS.md
├── LICENSE
├── .gitignore
└── Readme.md
/client/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/app/app.component.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/app/shared/components/bokeh-chart/bokeh-chart.component.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | nucosCR>=0.2.7
2 | nucosObs>=0.3.2
3 | bokeh>=3.0
4 |
--------------------------------------------------------------------------------
/client/src/styles/variables.scss:
--------------------------------------------------------------------------------
1 | $mainBgColor: black;
2 | $mainTextColor: #ccc;
--------------------------------------------------------------------------------
/client/src/app/shared/components/bokeh-chart/bokeh-chart.component.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NuCOS/angular-bokeh/HEAD/client/src/favicon.ico
--------------------------------------------------------------------------------
/client/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/client/src/styles.css:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/client/src/assets/bokeh_angular.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NuCOS/angular-bokeh/HEAD/client/src/assets/bokeh_angular.png
--------------------------------------------------------------------------------
/client/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | /* SystemJS module definition */
2 | declare var module: NodeModule;
3 | interface NodeModule {
4 | id: string;
5 | }
6 |
--------------------------------------------------------------------------------
/client/src/app/core/types/message.ts:
--------------------------------------------------------------------------------
1 |
2 | export interface Message {
3 |
4 | name: string;
5 | args: any;
6 | action?: string;
7 | directive?: string;
8 | user?: string;
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/client/src/app/core/services/debug.ts:
--------------------------------------------------------------------------------
1 | /* Debug indicator
2 | contains the Servcies/Components where the console.log output can be switched on/off
3 | */
4 |
5 | export const Debug = {
6 | messageService: true,
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/app/core/core.module.ts:
--------------------------------------------------------------------------------
1 | // ng modules
2 | import { NgModule } from '@angular/core';
3 |
4 | // shared modules
5 | // import { SharedModule } from './../shared/shared.module';
6 |
7 | @NgModule({
8 | imports: []
9 | })
10 | export class CoreModule { }
11 |
--------------------------------------------------------------------------------
/client/e2e/app.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, by, element } from 'protractor';
2 |
3 | export class ClientPage {
4 | navigateTo() {
5 | return browser.get('/');
6 | }
7 |
8 | getParagraphText() {
9 | return element(by.css('app-root h1')).getText();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/AGENT.md:
--------------------------------------------------------------------------------
1 | # Guidelines for Codex
2 |
3 | - Run `npm test --silent` and `npm run e2e --silent` from the `client` directory after making changes.
4 | - If these commands fail, include logs and mention the failures in the PR summary.
5 | - When Python tests are added, run `pytest` from the `python` directory as well.
6 |
--------------------------------------------------------------------------------
/client/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/client/e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/e2e",
5 | "baseUrl": "./",
6 | "module": "commonjs",
7 | "target": "es2020",
8 | "types": [
9 | "jasmine",
10 | "jasminewd2",
11 | "node"
12 | ]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/client/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "baseUrl": "./",
6 | "module": "es2020",
7 | "types": []
8 | },
9 | "exclude": [
10 | "test.ts",
11 | "**/*.spec.ts"
12 | ],
13 | "files": [
14 | "main.ts",
15 | "polyfills.ts"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/client/e2e/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { ClientPage } from './app.po';
2 |
3 | describe('client App', () => {
4 | let page: ClientPage;
5 |
6 | beforeEach(() => {
7 | page = new ClientPage();
8 | });
9 |
10 | it('should display welcome message', () => {
11 | page.navigateTo();
12 | expect(page.getParagraphText()).toEqual('Welcome to app!');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/client/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(AppModule)
12 | .catch(err => console.error(err));
13 |
--------------------------------------------------------------------------------
/client/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // The file contents for the current environment will overwrite these during build.
2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do
3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead.
4 | // The list of which env maps to which file can be found in `.angular-cli.json`.
5 |
6 | export const environment = {
7 | production: false
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "baseUrl": "./",
6 | "module": "commonjs",
7 | "target": "es2020",
8 | "types": [
9 | "jasmine",
10 | "node"
11 | ]
12 | },
13 | "files": [
14 | "test.ts",
15 | "polyfills.ts"
16 | ],
17 | "include": [
18 | "**/*.spec.ts",
19 | "**/*.d.ts"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/client/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { BokehChartComponent } from './shared/components/bokeh-chart/bokeh-chart.component';
3 |
4 | @Component({
5 | selector: 'app-root',
6 | standalone: true,
7 | imports: [
8 | BokehChartComponent
9 | ],
10 | templateUrl: './app.component.html',
11 | styleUrls: ['./app.component.css']
12 | })
13 | export class AppComponent {
14 | title = 'bokeh-app';
15 | }
16 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "outDir": "./dist/out-tsc",
5 | "sourceMap": true,
6 | "declaration": false,
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "target": "es2020",
11 | "typeRoots": [
12 | "node_modules/@types"
13 | ],
14 | "lib": [
15 | "es2020",
16 | "dom"
17 | ]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/python/services/interaction.py:
--------------------------------------------------------------------------------
1 | from nucosObs.observer import Observer
2 | import asyncio as aio
3 |
4 |
5 | class Interaction(Observer):
6 | def __init__(self, name, ptv, observable):
7 | super(Interaction, self).__init__(name, observable)
8 | self.cn = self.__class__.__name__
9 | self.ptv = ptv
10 |
11 | async def addChart(self):
12 | await self.ptv.addChart()
13 |
14 | def scheduleOnceSync(self, method, t, *args):
15 | aio.ensure_future(self.scheduleOnce(method, t, *args))
16 |
17 |
--------------------------------------------------------------------------------
/python/tests/test_chart_provider.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | # Ensure services package can be imported
5 | sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
6 |
7 | from services.chartProvider import ChartProvider
8 |
9 |
10 | def test_chart_provider_returns_bokeh_item():
11 | cp = ChartProvider()
12 | item = cp.chartExample()
13 | assert isinstance(item, dict)
14 | # Basic keys expected in a Bokeh JSON item
15 | for key in ["target_id", "root_id", "doc", "version"]:
16 | assert key in item
17 |
--------------------------------------------------------------------------------
/python/services/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | services
3 | --------
4 |
5 | Created 2016/2017
6 |
7 | @author: oliver, johannes
8 |
9 | general app services
10 |
11 | """
12 |
13 | DEBUG = False
14 | if DEBUG:
15 | # initiate logger with the lowest log-level
16 | loglvl = "DEBUG"
17 | else:
18 | """
19 | ======== ===========
20 | ERROR 'ERROR'
21 | WARNING 'WARNING'
22 | INFO 'INFO'
23 | DEBUG 'DEBUG'
24 | ======== ===========
25 | """
26 | loglvl = "INFO" # WARNING ERROR
27 |
28 | from services.backbonelogger import Logger
29 | logger = Logger('Logger')
30 | logger.level(loglvl)
31 |
--------------------------------------------------------------------------------
/client/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { BrowserModule } from '@angular/platform-browser';
2 | import { NgModule } from '@angular/core';
3 |
4 | import { AppComponent } from './app.component';
5 | import { CoreModule } from './core/core.module';
6 | import { SharedModule } from './shared/shared.module';
7 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
8 |
9 | @NgModule({
10 | declarations: [
11 | ],
12 | imports: [
13 | BrowserModule,
14 | CoreModule,
15 | SharedModule,
16 | BrowserAnimationsModule,
17 | AppComponent
18 | ],
19 | providers: [],
20 | bootstrap: [AppComponent]
21 | })
22 | export class AppModule { }
23 |
--------------------------------------------------------------------------------
/python/services/chartProvider.py:
--------------------------------------------------------------------------------
1 | import time
2 | import numpy as np
3 | from bokeh.plotting import figure
4 | from bokeh.embed import json_item
5 |
6 | # NOTE: updated for Bokeh 3.x
7 |
8 | class ChartProvider():
9 | def __init__(self):
10 | self.phi = 0
11 |
12 | def chartExample(self):
13 | t0 = time.time()
14 | # prepare some data
15 | self.phi += 0.02
16 | x = np.arange(0., 10., 0.1)
17 | y = np.sin(x + self.phi)
18 | # create a new plot
19 | p = figure()
20 | p.line(x, y, legend_label="SIN")
21 | chart_item = json_item(p)
22 | print(time.time()-t0)
23 | return chart_item
24 |
--------------------------------------------------------------------------------
/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | # Ensure Node 22 is available without using nvm
5 | if ! command -v node >/dev/null 2>&1; then
6 | echo "Node.js is required but not installed. Please install Node.js 22.x and rerun this script." >&2
7 |
8 | exit 1
9 | fi
10 |
11 | NODE_MAJOR=$(node -v | cut -d. -f1 | tr -d 'v')
12 |
13 | if [ "$NODE_MAJOR" -ne 22 ]; then
14 | echo "Node.js 22.x is required. Current version: $(node -v)" >&2
15 |
16 | exit 1
17 | fi
18 |
19 | # Install client dependencies
20 | cd client
21 | npm install --legacy-peer-deps
22 | cd ..
23 |
24 | # Install python dependencies
25 | python -m pip install --upgrade pip
26 | pip install -r requirements.txt
27 |
28 | echo "Setup complete."
29 |
--------------------------------------------------------------------------------
/client/src/app/shared/components/bokeh-chart/bokeh-chart.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { BokehService } from './../../services/bokeh.service';
3 |
4 | @Component({
5 | selector: 'bokeh-chart',
6 | standalone: true,
7 | templateUrl: './bokeh-chart.component.html',
8 | styleUrls: ['./bokeh-chart.component.css']
9 | })
10 | export class BokehChartComponent implements OnInit {
11 | public id: string;
12 |
13 | constructor(
14 | private bokehService: BokehService) { }
15 |
16 |
17 | ngOnInit() {
18 | this.id = String(Math.floor(Math.random() * Math.floor(9000)) + 1000);
19 | console.log('do bokeh plot');
20 | this.bokehService.getChart(this.id);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/client/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Client
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/client/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // Protractor configuration file, see link for more information
2 | // https://github.com/angular/protractor/blob/master/lib/config.ts
3 |
4 | const { SpecReporter } = require('jasmine-spec-reporter');
5 |
6 | exports.config = {
7 | allScriptsTimeout: 11000,
8 | specs: [
9 | './e2e/**/*.e2e-spec.ts'
10 | ],
11 | capabilities: {
12 | browserName: 'chrome',
13 | chromeOptions: {
14 | args: ['--headless', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage']
15 | }
16 | },
17 | directConnect: true,
18 | baseUrl: 'http://localhost:4200/',
19 | framework: 'jasmine',
20 | jasmineNodeOpts: {
21 | showColors: true,
22 | defaultTimeoutInterval: 30000,
23 | print: function() {}
24 | },
25 | onPrepare() {
26 | require('ts-node').register({
27 | project: 'e2e/tsconfig.e2e.json'
28 | });
29 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/client/src/app/shared/shared.module.ts:
--------------------------------------------------------------------------------
1 | // ng lib modules
2 | import { NgModule } from '@angular/core';
3 | import { CommonModule } from '@angular/common';
4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms';
5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
6 |
7 | import { BokehChartComponent } from './components/bokeh-chart/bokeh-chart.component';
8 |
9 | @NgModule({
10 | imports: [
11 | // ng modules
12 | CommonModule,
13 | FormsModule,
14 | ReactiveFormsModule,
15 | BrowserAnimationsModule,
16 | BokehChartComponent,
17 | ],
18 | declarations: [
19 | // shared directives
20 | ],
21 | exports: [
22 | // shared components
23 | BokehChartComponent,
24 | // ng lib modules
25 | CommonModule,
26 | FormsModule,
27 | BrowserAnimationsModule
28 | ],
29 | providers: [
30 |
31 | ],
32 | // bootstrap: [SandboxBottomSheetComponent]
33 | })
34 | export class SharedModule { }
35 |
36 |
--------------------------------------------------------------------------------
/client/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
11 | declare const __karma__: any;
12 | declare const require: any;
13 |
14 | // Prevent Karma from running prematurely.
15 | __karma__.loaded = function () {};
16 |
17 | // First, initialize the Angular testing environment.
18 | getTestBed().initTestEnvironment(
19 | BrowserDynamicTestingModule,
20 | platformBrowserDynamicTesting()
21 | );
22 | // Then we find all the tests.
23 | const context = require.context('./', true, /\.spec\.ts$/);
24 | // And load the modules.
25 | context.keys().map(context);
26 | // Finally, start Karma to run the tests.
27 | __karma__.start();
28 |
--------------------------------------------------------------------------------
/IMPROVEMENTS.md:
--------------------------------------------------------------------------------
1 | # Suggested Improvements
2 |
3 | These are prioritized ideas for enhancing the project and making it more suitable for building dashboards and web applications.
4 |
5 | 1. **Automated Testing Across Python and Angular**
6 | - Introduce Python unit tests using `pytest`.
7 | - Configure GitHub Actions (or another CI) to run Angular and Python tests automatically.
8 | 2. **Containerized Development Environment**
9 | - Provide a `Dockerfile` and `docker-compose` setup for running both Angular and Python services.
10 | - Simplifies local development and deployment.
11 | 3. **User Authentication**
12 | - Add a simple auth layer (e.g., JWT) for the websocket and API endpoints.
13 | - Helps secure dashboards when hosted.
14 | 4. **Real-time Data Integration**
15 | - Expand `BokehService` to consume real-time data feeds and update plots dynamically.
16 | 5. **Responsive Layouts**
17 | - Improve Angular components to support responsive design and theming for use in complex dashboards.
18 |
19 |
--------------------------------------------------------------------------------
/client/e2e/websocket.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { browser, by, element, protractor } from 'protractor';
2 | import { spawn } from 'child_process';
3 | import * as path from 'path';
4 |
5 | describe('Backend websocket', () => {
6 | let server: any;
7 |
8 | beforeAll(() => {
9 | const python = 'python';
10 | const appPath = path.join(__dirname, '..', '..', 'python', 'app.py');
11 | server = spawn(python, [appPath], {
12 | cwd: path.join(__dirname, '..', '..', 'python'),
13 | stdio: 'inherit'
14 | });
15 | browser.waitForAngularEnabled(false);
16 | return browser.sleep(5000);
17 | });
18 |
19 | afterAll(() => {
20 | if (server) {
21 | server.kill();
22 | }
23 | });
24 |
25 | it('should render chart via websocket', async () => {
26 | await browser.get('http://localhost:9000/');
27 | const chart = element(by.css('bokeh-chart .bk-root'));
28 | await browser.wait(protractor.ExpectedConditions.presenceOf(chart), 10000);
29 | expect(await chart.isPresent()).toBe(true);
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/client/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, waitForAsync } from '@angular/core/testing';
2 |
3 | import { AppComponent } from './app.component';
4 |
5 | describe('AppComponent', () => {
6 | beforeEach(waitForAsync(() => {
7 | TestBed.configureTestingModule({
8 | declarations: [
9 | AppComponent
10 | ],
11 | }).compileComponents();
12 | }));
13 |
14 | it('should create the app', waitForAsync(() => {
15 | const fixture = TestBed.createComponent(AppComponent);
16 | const app = fixture.debugElement.componentInstance;
17 | expect(app).toBeTruthy();
18 | }));
19 |
20 | it(`should have as title 'app'`, waitForAsync(() => {
21 | const fixture = TestBed.createComponent(AppComponent);
22 | const app = fixture.debugElement.componentInstance;
23 | expect(app.title).toEqual('app');
24 | }));
25 |
26 | it('should render title in a h1 tag', waitForAsync(() => {
27 | const fixture = TestBed.createComponent(AppComponent);
28 | fixture.detectChanges();
29 | const compiled = fixture.debugElement.nativeElement;
30 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
31 | }));
32 | });
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 NuCOS GmbH, Germany Stuttgart, Apfelblütenweg 9
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # Client
2 |
3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.2.2.
4 |
5 | ## Development server
6 |
7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
8 |
9 | ## Code scaffolding
10 |
11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|module`.
12 |
13 | ## Build
14 |
15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.
16 |
17 | ## Running unit tests
18 |
19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
20 |
21 | ## Running end-to-end tests
22 |
23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
24 | Before running the tests make sure you are serving the app via `ng serve`.
25 |
26 | ## Further help
27 |
28 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
29 |
--------------------------------------------------------------------------------
/client/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/0.13/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client:{
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | reports: [ 'html', 'lcovonly' ],
20 | fixWebpackSourcePaths: true
21 | },
22 | reporters: ['progress', 'kjhtml'],
23 | port: 9876,
24 | colors: true,
25 | logLevel: config.LOG_INFO,
26 | autoWatch: true,
27 | browsers: ['ChromeHeadless'],
28 | singleRun: true,
29 | customLaunchers: {
30 | ChromeHeadlessCI: {
31 | base: 'ChromeHeadless',
32 | flags: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage']
33 | }
34 | }
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/client/src/styles/main.scss:
--------------------------------------------------------------------------------
1 | /*****************************************************
2 | * node_modules imports
3 | *****************************************************/
4 | @import 'bootstrap/scss/bootstrap';
5 |
6 | /*****************************************************
7 | * project imports
8 | *****************************************************/
9 | @import 'variables';
10 | @import 'mixins';
11 |
12 | /*****************************************************
13 | * styles
14 | *****************************************************/
15 | html, body, app-root, app-game {
16 | overflow: hidden;
17 | width : 100%;
18 | height : 100%;
19 | margin : 0;
20 | padding : 0;
21 | }
22 |
23 | body {
24 | background-color: $mainBgColor;
25 | color: $mainTextColor;
26 | }
27 |
28 | .engine-wrapper{
29 | position: absolute;
30 | top: 0;
31 | right: 0;
32 | bottom: 0;
33 | left: 0;
34 | z-index: 0;
35 |
36 | #renderCanvas {
37 | width : 100%;
38 | height : 100%;
39 | touch-action: none;
40 | }
41 |
42 | #renderCanvas:focus {
43 | outline: none;
44 | }
45 | }
46 |
47 | .ui-wrapper {
48 | position: absolute;
49 |
50 | * {
51 | z-index: 10;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/python/services/pythonToView.py:
--------------------------------------------------------------------------------
1 | """
2 | pythonToView
3 | ------------
4 |
5 | .. module:: pythonToView
6 | :platform: Unix, Windows
7 | :synopsis: python to view/gui base class
8 |
9 | Created 2016/2017
10 |
11 | @author: oliver, johannes
12 | """
13 |
14 | import simplejson as json
15 | from services.junction import Junction
16 | from services.chartProvider import ChartProvider
17 |
18 | class PythonToView(Junction):
19 | """
20 | This is the connection layer between angular and python. It keeps also information of the state of the gui.
21 |
22 | """
23 | def __init__(self):
24 | self.servers = []
25 | super(PythonToView, self).__init__(self.servers)
26 | self.chartProvider = ChartProvider()
27 |
28 | def setServer(self, server):
29 | self.servers.append(server)
30 |
31 | async def connect(self, *args, user=None):
32 | print("TEST", user, args)
33 |
34 | async def addChart(self, id_, callbackId, user):
35 | """
36 | Example for adding a bokeh chart from backend
37 |
38 | """
39 | chartItem = self.chartProvider.chartExample()
40 | print("try to add chart for dom-id %s" % id_)
41 | context = {"name": callbackId,
42 | "args": {"item": chartItem, "id": id_}}
43 | await self.send_event(json.dumps(context), user=user)
44 |
--------------------------------------------------------------------------------
/client/src/app/shared/services/bokeh.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { MessageService } from './../../core/services/message.service';
3 | import { filter } from 'rxjs/operators';
4 |
5 | // this is the global hook to the bokehjs lib (without types)
6 | declare var Bokeh: any;
7 |
8 |
9 | @Injectable({
10 | providedIn: 'root'
11 | })
12 | export class BokehService {
13 |
14 | constructor(private msgService: MessageService) { }
15 |
16 | public plot(id: string, item: any) {
17 | const el = document.getElementById(id);
18 | // first remove the previous charts as child
19 | // this necessary, since bokeh do not let us update a chart
20 | while (el.hasChildNodes()) {
21 | el.removeChild(el.lastChild);
22 | }
23 | // be sure to include the correct dom-id as second argument
24 | Bokeh.embed.embed_item(item, id);
25 | }
26 |
27 | public getChart(id: string) {
28 | const callbackId = 'plot';
29 | const msg = {
30 | name: 'addChart',
31 | args: [id, callbackId],
32 | action: 'default'
33 | };
34 | this.msgService.sendMsg(msg);
35 | this.msgService.awaitMessage()
36 | .pipe(filter(msg=> msg.name == callbackId))
37 | .subscribe(
38 | msg => {
39 | this.plot(msg.args.id, msg.args.item);
40 | }
41 | )
42 | }
43 | }
44 |
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | *.com
4 | *.class
5 | *.dll
6 | *.exe
7 | *.o
8 | *.so
9 |
10 | # python
11 | *.pyc
12 |
13 | # Byte-compiled / optimized / DLL files
14 | __pycache__/
15 | *.py[cod]
16 | *.bak
17 | *$py.class
18 |
19 | # angular / compiled output
20 | node_modules/
21 | dist/
22 | doc/generated
23 | tmp/
24 | client/package-lock.json
25 |
26 | # it's better to unpack these files and commit the raw source
27 | # git has its own built in compression methods
28 | *.7z
29 | *.dmg
30 | *.gz
31 | *.iso
32 | *.jar
33 | *.rar
34 | *.tar
35 | *.zip
36 |
37 | # Logs and databases #
38 | ######################
39 | *.log
40 | *.sql
41 | *.sqlite
42 | data/ldm_db
43 |
44 | # IDEs and editors
45 | /.idea
46 | .project
47 | .classpath
48 | .c9/
49 | *.launch
50 | .settings/
51 |
52 | # IDE - VSCode
53 | # We use workspace settings for the entire team
54 | !.vscode/
55 | !.vscode/settings.json
56 | .vscode/tasks.json
57 | .vscode/extensions.json
58 |
59 | # OS generated files #
60 | ######################
61 | .DS_Store
62 | .DS_Store?
63 | ._*
64 | .Spotlight-V100
65 | .Trashes
66 | ehthumbs.db
67 | Thumbs.db
68 |
69 | # misc
70 | /.sass-cache
71 | /connect.lock
72 | /coverage/*
73 | /libpeerconnection.log
74 | npm-debug.log
75 | debug.log
76 | testem.log
77 | /typings
78 |
79 | # e2e
80 | /e2e/*.js
81 | /e2e/*.map
82 |
83 | # mergetool meld #
84 | ##################
85 |
86 | *.py.orig
87 | *.orig
88 |
--------------------------------------------------------------------------------
/client/src/app/shared/services/bokeh.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 | import { Subject } from 'rxjs';
3 | import { BokehService } from './bokeh.service';
4 | import { MessageService } from '../../core/services/message.service';
5 |
6 | class MockMessageService {
7 | public sent: any = null;
8 | public stream = new Subject();
9 | sendMsg(msg: any) { this.sent = msg; }
10 | awaitMessage() { return this.stream; }
11 | }
12 |
13 | declare var Bokeh: any;
14 |
15 | describe('BokehService', () => {
16 | let service: BokehService;
17 | let mock: MockMessageService;
18 |
19 | beforeEach(() => {
20 | mock = new MockMessageService();
21 | (global as any).Bokeh = { embed: { embed_item: jasmine.createSpy('embed_item') } };
22 | TestBed.configureTestingModule({
23 | providers: [
24 | BokehService,
25 | { provide: MessageService, useValue: mock }
26 | ]
27 | });
28 | service = TestBed.inject(BokehService);
29 | });
30 |
31 | it('should embed chart when data arrives', () => {
32 | const div = document.createElement('div');
33 | div.id = 'test-div';
34 | document.body.appendChild(div);
35 |
36 | service.getChart('test-div');
37 |
38 | mock.stream.next({ name: 'plot', args: { id: 'test-div', item: { foo: 'bar' } } });
39 |
40 | expect(Bokeh.embed.embed_item).toHaveBeenCalledWith({ foo: 'bar' }, 'test-div');
41 |
42 | document.body.removeChild(div);
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/client/.angular-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "project": {
4 | "name": "client"
5 | },
6 | "apps": [
7 | {
8 | "root": "src",
9 | "outDir": "dist",
10 | "assets": [
11 | "assets",
12 | "favicon.ico"
13 | ],
14 | "index": "index.html",
15 | "main": "main.ts",
16 | "polyfills": "polyfills.ts",
17 | "test": "test.ts",
18 | "tsconfig": "tsconfig.app.json",
19 | "testTsconfig": "tsconfig.spec.json",
20 | "prefix": "app",
21 | "styles": [
22 | "styles.css"
23 | ],
24 | "scripts": [],
25 | "environmentSource": "environments/environment.ts",
26 | "environments": {
27 | "dev": "environments/environment.ts",
28 | "prod": "environments/environment.prod.ts"
29 | }
30 | }
31 | ],
32 | "e2e": {
33 | "protractor": {
34 | "config": "./protractor.conf.js"
35 | }
36 | },
37 | "lint": [
38 | {
39 | "project": "src/tsconfig.app.json",
40 | "exclude": "**/node_modules/**"
41 | },
42 | {
43 | "project": "src/tsconfig.spec.json",
44 | "exclude": "**/node_modules/**"
45 | },
46 | {
47 | "project": "e2e/tsconfig.e2e.json",
48 | "exclude": "**/node_modules/**"
49 | }
50 | ],
51 | "test": {
52 | "karma": {
53 | "config": "./karma.conf.js"
54 | }
55 | },
56 | "defaults": {
57 | "styleExt": "css",
58 | "component": {}
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/client/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Welcome to {{title}}!
5 |
6 |

7 |
8 | This is the embedded bokeh chart component:
9 |
10 |
11 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "agrotech",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "build": "ng build",
8 | "test": "ng test",
9 | "lint": "ng lint",
10 | "e2e": "ng e2e"
11 | },
12 | "private": true,
13 | "dependencies": {
14 | "@angular/animations": "~20.0.3",
15 | "@angular/cdk": "^20.0.3",
16 | "@angular/common": "~20.0.3",
17 | "@angular/compiler": "~20.0.3",
18 | "@angular/core": "~20.0.3",
19 | "@angular/forms": "~20.0.3",
20 | "@angular/material": "^20.0.3",
21 | "@angular/platform-browser": "~20.0.3",
22 | "@angular/platform-browser-dynamic": "~20.0.3",
23 | "@angular/router": "~20.0.3",
24 | "bootstrap": "^5.3.7",
25 | "core-js": "^3.43.0",
26 | "js-sha256": "latest",
27 | "rxjs": "~7.8.2",
28 | "tslib": "^2.6.0",
29 | "zone.js": "~0.15.1"
30 |
31 | },
32 | "devDependencies": {
33 | "@angular-devkit/build-angular": "~20.0.3",
34 | "@angular/cli": "~20.0.3",
35 | "@angular/compiler-cli": "~20.0.4",
36 | "@types/node": "^24.0.3",
37 | "@types/jasmine": "~5.1.0",
38 | "@types/jasminewd2": "~2.0.3",
39 | "codelyzer": "^6.0.0",
40 | "jasmine-core": "~5.8.0",
41 | "jasmine-spec-reporter": "~7.0.0",
42 | "karma": "~6.4.0",
43 | "karma-chrome-launcher": "~3.2.0",
44 | "karma-coverage-istanbul-reporter": "~3.0.2",
45 | "karma-jasmine": "~5.1.0",
46 | "karma-jasmine-html-reporter": "^2.1.0",
47 | "protractor": "~7.0.0",
48 | "ts-node": "~10.9.1",
49 | "tslint": "~6.1.0",
50 | "typescript": "~5.8.3"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/client/src/app/core/services/websocket.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Observable, Observer, Subject } from 'rxjs';
3 |
4 | @Injectable({
5 | providedIn: 'root',
6 | })
7 | export class WebsocketService {
8 | private subject: Subject;
9 |
10 | public connect(url: string): Subject {
11 | if (!this.subject) {
12 | this.subject = this.create(url);
13 | }
14 | return this.subject;
15 | }
16 |
17 | private create(url: string): Subject {
18 | const ws = new WebSocket(url);
19 | ws.onerror = (event) => {
20 | console.log('WebSocket Connection Failed with message', event.type);
21 | return false;
22 | };
23 |
24 |
25 | const observable = new Observable(
26 | (obs: Observer) => {
27 | ws.onmessage = obs.next.bind(obs);
28 | ws.onerror = obs.error.bind(obs);
29 | ws.onclose = obs.complete.bind(obs);
30 |
31 | return ws.close.bind(ws);
32 | })
33 |
34 | const subject = new Subject();
35 | const originalNext = subject.next.bind(subject);
36 |
37 | observable.subscribe({
38 | next: (msg) => originalNext(msg),
39 | error: (err) => subject.error(err),
40 | complete: () => subject.complete(),
41 | });
42 |
43 | subject.next = (data: any) => {
44 | if (ws.readyState === WebSocket.OPEN) {
45 | // console.log(JSON.stringify(data));
46 | ws.send(JSON.stringify(data));
47 | } else {
48 | console.log('not ready yet');
49 | // try again after 500 ms
50 | setTimeout(() => { subject.next(data); }, 500);
51 | }
52 | };
53 |
54 | return subject;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/python/services/junction.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import asyncio as aio
3 | import traceback
4 | import simplejson as json
5 | import inspect
6 | from services import logger
7 |
8 | from nucosObs.observer import broadcast
9 |
10 | modalFct = ["wakeUp"]
11 |
12 |
13 | class Junction():
14 | def __init__(self, servers):
15 | self.cn = self.__class__.__name__
16 | self.servers = servers
17 | self.sleep = False
18 |
19 | async def shutdown(self):
20 | """
21 | not used at the moment
22 | """
23 | await broadcast.put("stop_observer")
24 |
25 | def do_it(self, a):
26 | try:
27 | inp = json.loads(a)
28 | is_json = True
29 | except:
30 | is_json = False
31 | logger.log(lvl="INFO", msg="from angular: %s" % a[0:100], orig=self.cn)
32 | if is_json:
33 | fct = inp["name"]
34 | args = inp["args"]
35 | user = inp["user"]
36 | if self.sleep and fct not in modalFct:
37 | return
38 | try:
39 | method = getattr(self, fct)
40 | if inspect.iscoroutinefunction(method):
41 | aio.ensure_future(method(*args, user=user))
42 | else:
43 | return method(*args, user=user)
44 | except:
45 | exc_type, exc_value, exc_traceback = sys.exc_info()
46 | formatted_lines = traceback.format_exc().splitlines()
47 | return ("ERROR no valid json-function call from junction via .. %s \n %s \n %s \n %s" % (fct, exc_type, exc_value, formatted_lines))
48 |
49 | async def send_event(self, jtxt, user=None):
50 | await self.servers[0].sendEventWait(jtxt, user)
51 |
52 |
53 |
--------------------------------------------------------------------------------
/python/app.py:
--------------------------------------------------------------------------------
1 |
2 | import os
3 | import getpass
4 | import argparse
5 | from aiohttp import web
6 |
7 | from services.aiohttpServer import AiohttpServer
8 | from services.pythonToView import PythonToView
9 | from services.interaction import Interaction
10 | from services import logger
11 | from nucosObs import main_loop, loop, debug
12 | from nucosObs.observable import Observable
13 |
14 | import logging
15 |
16 | # NOTE to switch on logging of the aiohttp server
17 | # logging.basicConfig(level=logging.DEBUG)
18 |
19 | _root_ = os.path.realpath(os.path.dirname(__file__))
20 |
21 | debug.append(False)
22 |
23 | parser = argparse.ArgumentParser(description="Angular-Bokeh server")
24 | parser.add_argument(
25 | "--port",
26 | type=int,
27 | default=int(os.getenv("PORT", 9000)),
28 | help="Port for the web server (env: PORT)",
29 | )
30 | parser.add_argument(
31 | "--angular-path",
32 | default=os.getenv("ANGULAR_DIST_PATH", os.path.join(_root_, "../client/dist/dev")),
33 | help="Path to built Angular files (env: ANGULAR_DIST_PATH)",
34 | )
35 |
36 | args = parser.parse_args()
37 | port = args.port
38 |
39 | if __name__ == '__main__':
40 | ptv = PythonToView()
41 | server = AiohttpServer(ptv)
42 | ptv.setServer(server)
43 |
44 | path = os.path.abspath(args.angular_path)
45 | if not os.path.exists(path):
46 | logger.log(lvl="ERROR", msg="build the angular app first")
47 | exit()
48 |
49 | async def handle(request):
50 | return web.FileResponse(os.path.join(path, 'index.html'))
51 |
52 | # NOTE the app is already started in the server ....
53 | app = server.app
54 | app.add_routes([web.get('/', handle)])
55 | # NOTE for angular projects this is necessary ...
56 | app.add_routes([web.static('/', os.path.join(path, './'))])
57 | logger.log(lvl="INFO", msg="start server on localhost port %s" % port)
58 | logger.log(lvl="INFO", msg=f"path: {path}")
59 | runner = server.startService('0.0.0.0', port)
60 | interactionObservable = Observable()
61 | interaction = Interaction("ia", ptv, interactionObservable)
62 | # NOTE to test an update later on
63 | # interaction.scheduleOnceSync(interaction.addChart, 7.0)
64 | # interaction.scheduleRegular(interaction.addChart, 2.0)
65 | main_loop([runner, ])
66 |
--------------------------------------------------------------------------------
/client/src/app/core/types/dictionary.ts:
--------------------------------------------------------------------------------
1 | export interface IDictionary {
2 | add(key: string, value: T): void;
3 | containsKey(key: string): boolean;
4 | count(): number;
5 | item(key: string): T;
6 | keys(): string[];
7 | remove(key: string): T;
8 | values(): T[];
9 | pos(key: string): number;
10 | replace(newkey: string, value: T, oldkey: string): boolean;
11 | removeAll(): void;
12 | }
13 |
14 | export class Dictionary implements IDictionary {
15 | public items: { [index: string]: T } = {};
16 |
17 | public _count = 0;
18 |
19 | constructor(private maxCount: number = -1) { }
20 |
21 | public containsKey(key: string): boolean {
22 | return this.items.hasOwnProperty(key);
23 | }
24 |
25 | public count(): number {
26 | return this._count;
27 | }
28 |
29 | public add(key: string, value: T): void {
30 | if (((this._count < this.maxCount) && (this.maxCount > -1)) || (this.maxCount === -1)) {
31 | this.items[key] = value;
32 | this._count++;
33 | }
34 | }
35 |
36 | public remove(key: string): T {
37 | if (this.containsKey(key)) {
38 | const val = this.items[key];
39 | delete this.items[key];
40 | this._count--;
41 | return val;
42 | } else {
43 | return null;
44 | }
45 | }
46 |
47 | public removeAll(): void {
48 | for (const k of this.keys()) { this.remove(k); }
49 | }
50 |
51 | public item(key: string): T {
52 | return this.items[key];
53 | }
54 |
55 | public keys(): string[] {
56 | return Object.keys(this.items);
57 | }
58 |
59 | public values(): T[] {
60 | const values: T[] = [];
61 | for (const prop in this.items) {
62 | if (this.items.hasOwnProperty(prop)) {
63 | values.push(this.items[prop]);
64 | }
65 | }
66 | return values;
67 | }
68 |
69 | public pos(key: string): number {
70 | return this.keys().indexOf(key);
71 | }
72 |
73 | public replace(newkey: string, value: T, oldkey: string): boolean {
74 | // should insert a value of type T on the same place as given by oldkey. (Key,value) of oldkey is deleted after operation.
75 | if (this.containsKey(oldkey)) {
76 | // since the order of attributes can not be changed ( at least not known ) ->
77 | // create a new storage with changed Items
78 | const changedItems: { [index: string]: T } = {};
79 | for (const key in this.items) {
80 | if (key === oldkey) {
81 | changedItems[newkey] = value;
82 | } else { changedItems[key] = this.item(key); }
83 | }
84 | this.items = changedItems;
85 | return true;
86 | } else {
87 | return false;
88 | }
89 | }
90 | }
91 |
92 |
93 |
--------------------------------------------------------------------------------
/client/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/
22 | // import 'core-js/es6/symbol';
23 | // import 'core-js/es6/object';
24 | // import 'core-js/es6/function';
25 | // import 'core-js/es6/parse-int';
26 | // import 'core-js/es6/parse-float';
27 | // import 'core-js/es6/number';
28 | // import 'core-js/es6/math';
29 | // import 'core-js/es6/string';
30 | // import 'core-js/es6/date';
31 | // import 'core-js/es6/array';
32 | // import 'core-js/es6/regexp';
33 | // import 'core-js/es6/map';
34 | // import 'core-js/es6/weak-map';
35 | // import 'core-js/es6/set';
36 |
37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
38 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
39 |
40 | /** Evergreen browsers require these. **/
41 |
42 |
43 | /**
44 | * Required to support Web Animations `@angular/animation`.
45 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
46 | **/
47 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
48 |
49 |
50 |
51 | /***************************************************************************************************
52 | * Zone JS is required by Angular itself.
53 | */
54 | import 'zone.js'; // Included with Angular CLI.
55 |
56 |
57 |
58 | /***************************************************************************************************
59 | * APPLICATION IMPORTS
60 | */
61 |
62 | /**
63 | * Date, currency, decimal and percent pipes.
64 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
65 | */
66 | // import 'intl'; // Run `npm install --save intl`.
67 | /**
68 | * Need to import at least one locale-data with intl.
69 | */
70 | // import 'intl/locale-data/jsonp/en';
71 |
--------------------------------------------------------------------------------
/client/src/app/core/services/message.service.ts:
--------------------------------------------------------------------------------
1 | import { Debug } from './debug';
2 | import { Injectable } from '@angular/core';
3 | import { Subject } from 'rxjs';
4 | import { WebsocketService } from './websocket.service';
5 | import { Message } from '../types/message';
6 | import { share, map } from 'rxjs/operators';
7 | import { sha256 } from 'js-sha256';
8 |
9 | const URL = 'ws://127.0.0.1:9000/ws';
10 |
11 | @Injectable({
12 | providedIn: 'root',
13 | })
14 | export class MessageService {
15 | public message: Subject;
16 | public log = '';
17 | public user = '';
18 | public password = '';
19 | public isAuthenticated = false;
20 | public dataStream = new Subject();
21 |
22 | constructor(private wsService: WebsocketService) {
23 | console.log('create message service');
24 | // think of a nice and individual user name generated here
25 | const user = this.getRandomInt(9000) + 1000;
26 | this.connect(String(user), '');
27 | }
28 |
29 | public connect(user: string, password: string): void {
30 | console.log('try to connect');
31 | this.message = >this.wsService
32 | .connect(URL).pipe(
33 | map((response: MessageEvent): Message => {
34 | const data = JSON.parse(response.data);
35 | return {
36 | name: data.name,
37 | args: data.args,
38 | action: data.action,
39 | directive: data.directive
40 | };
41 | }), share());
42 | this.message.subscribe(msg => {
43 | this.manageMsg(msg);
44 | });
45 | this.user = user;
46 | this.password = password;
47 | }
48 |
49 | public getRandomInt(max) {
50 | return Math.floor(Math.random() * Math.floor(max));
51 | }
52 |
53 | public getLog(): string {
54 | return this.log;
55 | }
56 |
57 | public sendMsg(msg: Message) {
58 | // send a message via websocket connection to python backend (here)
59 | if (Debug.messageService) {
60 | console.log('new message from angular to python: ', msg);
61 | }
62 | msg.user = this.user;
63 | if (msg.name === '') {
64 | return;
65 | } else { this.message.next(msg); }
66 | }
67 |
68 | public hexdigest_n(input: string, n: number) {
69 | let i = 0;
70 | let pre_digest = sha256(input);
71 | while (i < n - 1) {
72 | i += 1;
73 | pre_digest = sha256(pre_digest);
74 | }
75 | return pre_digest;
76 | }
77 |
78 | public awaitMessage() {
79 | return this.dataStream;
80 | }
81 |
82 | private manageMsg(msg: Message): void {
83 | if (msg.name === 'Log') {
84 | if (this.log.length > 1500) { this.log = this.log.slice(msg.args.length); }
85 | this.log += msg.args;
86 | }
87 | else if (msg.name === 'doAuth') {
88 | if (msg.action === 'authenticate') {
89 | const id = msg.args['id'];
90 | const nonce = msg.args['nonce'];
91 | const pre_digest = this.hexdigest_n(this.password, 100);
92 | const challenge = this.hexdigest_n(pre_digest + nonce, 100);
93 | const authMsg = {
94 | name: 'doAuth',
95 | args: { 'user': this.user, 'challenge': challenge, 'id': id },
96 | action: 'default'
97 | };
98 | this.sendMsg(authMsg);
99 | } else {
100 | this.isAuthenticated = msg.args['authenticated'];
101 | if (!msg.args['authenticated']) {
102 | console.log('auth failed');
103 | } else {
104 | console.log('auth success');
105 | }
106 | }
107 | } else {
108 | this.dataStream.next(msg);
109 | }
110 | }
111 | } // end class MessageService
112 |
--------------------------------------------------------------------------------
/client/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "arrow-return-shorthand": true,
7 | "callable-types": true,
8 | "class-name": true,
9 | "comment-format": [
10 | true,
11 | "check-space"
12 | ],
13 | "curly": true,
14 | "eofline": true,
15 | "forin": true,
16 | "import-blacklist": [
17 | true,
18 | "rxjs"
19 | ],
20 | "import-spacing": true,
21 | "indent": [
22 | true,
23 | "spaces"
24 | ],
25 | "interface-over-type-literal": true,
26 | "label-position": true,
27 | "max-line-length": [
28 | true,
29 | 140
30 | ],
31 | "member-access": false,
32 | "member-ordering": [
33 | true,
34 | {
35 | "order": [
36 | "static-field",
37 | "instance-field",
38 | "static-method",
39 | "instance-method"
40 | ]
41 | }
42 | ],
43 | "no-arg": true,
44 | "no-bitwise": true,
45 | "no-console": [
46 | true,
47 | "debug",
48 | "info",
49 | "time",
50 | "timeEnd",
51 | "trace"
52 | ],
53 | "no-construct": true,
54 | "no-debugger": true,
55 | "no-duplicate-super": true,
56 | "no-empty": false,
57 | "no-empty-interface": true,
58 | "no-eval": true,
59 | "no-inferrable-types": [
60 | true,
61 | "ignore-params"
62 | ],
63 | "no-misused-new": true,
64 | "no-non-null-assertion": true,
65 | "no-shadowed-variable": true,
66 | "no-string-literal": false,
67 | "no-string-throw": true,
68 | "no-switch-case-fall-through": true,
69 | "no-trailing-whitespace": true,
70 | "no-unnecessary-initializer": true,
71 | "no-unused-expression": true,
72 | "no-use-before-declare": true,
73 | "no-var-keyword": true,
74 | "object-literal-sort-keys": false,
75 | "one-line": [
76 | true,
77 | "check-open-brace",
78 | "check-catch",
79 | "check-else",
80 | "check-whitespace"
81 | ],
82 | "prefer-const": true,
83 | "quotemark": [
84 | true,
85 | "single"
86 | ],
87 | "radix": true,
88 | "semicolon": [
89 | true,
90 | "always"
91 | ],
92 | "triple-equals": [
93 | true,
94 | "allow-null-check"
95 | ],
96 | "typedef-whitespace": [
97 | true,
98 | {
99 | "call-signature": "nospace",
100 | "index-signature": "nospace",
101 | "parameter": "nospace",
102 | "property-declaration": "nospace",
103 | "variable-declaration": "nospace"
104 | }
105 | ],
106 | "typeof-compare": true,
107 | "unified-signatures": true,
108 | "variable-name": false,
109 | "whitespace": [
110 | true,
111 | "check-branch",
112 | "check-decl",
113 | "check-operator",
114 | "check-separator",
115 | "check-type"
116 | ],
117 | "directive-selector": [
118 | true,
119 | "attribute",
120 | "app",
121 | "camelCase"
122 | ],
123 | "component-selector": [
124 | true,
125 | "element",
126 | "app",
127 | "kebab-case"
128 | ],
129 | "use-input-property-decorator": true,
130 | "use-output-property-decorator": true,
131 | "use-host-property-decorator": true,
132 | "no-input-rename": true,
133 | "no-output-rename": true,
134 | "use-life-cycle-interface": true,
135 | "use-pipe-transform-interface": true,
136 | "component-class-suffix": true,
137 | "directive-class-suffix": true,
138 | "no-access-missing-member": true,
139 | "templates-use-public": true,
140 | "invoke-injectable": true
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/python/services/aiohttpServer.py:
--------------------------------------------------------------------------------
1 | from aiohttp import web
2 | import asyncio as aio
3 | import simplejson as json
4 |
5 | from nucosObs import debug
6 | from nucosObs.observer import Observer
7 | from nucosObs.observable import Observable
8 | from nucosObs.aiohttpWebsocketInterface import AiohttpWebsocketInterface
9 | from services import logger
10 |
11 |
12 | # debug.append(True)
13 |
14 | messageBroker = Observable()
15 |
16 |
17 | def authenticate(id_, user, nonce, challenge):
18 | return True
19 |
20 |
21 | class Authenticator():
22 | def __init__(self):
23 | self.approved = False
24 |
25 | async def startAuth(self, msg, wsi, nonce):
26 | inp = json.loads(msg)
27 | args = inp["args"]
28 | try:
29 | user, challenge, id_ = args["user"], args["challenge"], args["id"]
30 | except:
31 | return None, user
32 | if debug[-1]:
33 | print("start auth", msg)
34 | if authenticate(id_, user, nonce, challenge):
35 | context = {"name": "doAuth",
36 | "args": {"authenticated": True, "id": id_},
37 | "action": "finalizeAuth"}
38 | await wsi.send_str(json.dumps(context))
39 | if debug[-1]:
40 | print("Authenticate accepted of user %s" % user)
41 | return id_, user
42 | else:
43 | context = {"name": "doAuth",
44 | "args": {"authenticated": False, "id": id_},
45 | "action": "finalizeAuth"}
46 | await wsi.send_str(json.dumps(context))
47 | if debug[-1]:
48 | print("Authenticate refused")
49 | return None, user
50 |
51 |
52 | class WebsocketObserver(Observer):
53 | def __init__(self, name, observable, wsi, ptv, concurrent=[]):
54 | super(WebsocketObserver, self).__init__(name, observable, concurrent)
55 | self.wsi = wsi
56 | self.ptv = ptv
57 | self.cn = self.__class__.__name__
58 |
59 | def parse(self, item):
60 | if debug[-1]:
61 | logger.log(lvl="INFO", msg="message received: %s" % item, orig=self.cn)
62 | if item.startswith("send") or item.startswith("client"):
63 | return super(WebsocketObserver, self).parse(item)
64 | else:
65 | out = self.ptv.do_it(item)
66 | # in case of error
67 | if type(out) is str:
68 | if "ERROR" in out:
69 | print(out)
70 | return False, None, None
71 |
72 | async def client(self, msg):
73 | """
74 | do some extra shutdown work if necessary
75 | """
76 | pass
77 |
78 | async def send(self, user, *msg):
79 | n = 0
80 | while True and n < 20:
81 | n += 1
82 | try:
83 | await self.wsi.broadcast(' '.join(msg))
84 | break
85 | except:
86 | pass
87 | await aio.sleep(0.5)
88 |
89 |
90 |
91 | class AiohttpServer():
92 | def __init__(self, ptv):
93 | self.app = web.Application(debug=False)
94 | self.messageBroker = Observable()
95 | self.wsi = AiohttpWebsocketInterface(
96 | self.app, self.messageBroker, doAuth=True, authenticator=Authenticator(), closeOnClientQuit=False)
97 | self.ptv = ptv
98 | self.wso = WebsocketObserver(
99 | "WSO", self.messageBroker, self.wsi, self.ptv)
100 |
101 | def close(self):
102 | aio.ensure_future(self.wsi.shutdown())
103 |
104 | async def startService(self, ip, port):
105 | """
106 | Handles a coroutine to be put into the loop elsewhere
107 | """
108 | runner = web.AppRunner(self.app)
109 | await runner.setup()
110 | site = web.TCPSite(runner, ip, port, ssl_context=None)
111 | await site.start()
112 |
113 | def sendEvent(self, jtxt, user):
114 | """
115 | Non blocking in this aio process:
116 | used if it is not critical to be processed at the moment
117 | """
118 | aio.ensure_future(self.messageBroker.put(f"send {user} " + jtxt))
119 |
120 | async def sendEventWait(self, jtxt, user):
121 | """
122 | blocking in this aio process:
123 | used if events must be put into certain order
124 | """
125 | await self.messageBroker.put(f"send {user} " + jtxt)
126 |
127 |
128 |
129 |
130 |
--------------------------------------------------------------------------------
/client/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "ng-three-template": {
7 | "root": "",
8 | "sourceRoot": "src",
9 | "projectType": "application",
10 | "prefix": "app",
11 | "schematics": {
12 | "@schematics/angular:component": {
13 | "styleext": "scss"
14 | }
15 | },
16 | "architect": {
17 | "build": {
18 | "builder": "@angular-devkit/build-angular:browser",
19 | "options": {
20 | "outputPath": "dist/dev",
21 | "index": "src/index.html",
22 | "main": "src/main.ts",
23 | "polyfills": "src/polyfills.ts",
24 | "tsConfig": "src/tsconfig.app.json",
25 | "assets": [
26 | "src/assets",
27 | "src/favicon.ico"
28 | ],
29 | "styles": [
30 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
31 | "src/styles/main.scss"
32 | ],
33 | "scripts": []
34 | },
35 | "configurations": {
36 | "production": {
37 | "outputPath": "dist/prod",
38 | "fileReplacements": [
39 | {
40 | "replace": "src/environments/environment.ts",
41 | "with": "src/environments/environment.prod.ts"
42 | }
43 | ],
44 | "optimization": true,
45 | "outputHashing": "all",
46 | "sourceMap": false,
47 | "extractCss": true,
48 | "namedChunks": false,
49 | "aot": true,
50 | "extractLicenses": true,
51 | "vendorChunk": false,
52 | "buildOptimizer": true,
53 | "budgets": [
54 | {
55 | "type": "initial",
56 | "maximumWarning": "2mb",
57 | "maximumError": "5mb"
58 | }
59 | ]
60 | }
61 | }
62 | },
63 | "serve": {
64 | "builder": "@angular-devkit/build-angular:dev-server",
65 | "options": {
66 | "browserTarget": "ng-three-template:build"
67 | },
68 | "configurations": {
69 | "production": {
70 | "browserTarget": "ng-three-template:build:production"
71 | }
72 | }
73 | },
74 | "extract-i18n": {
75 | "builder": "@angular-devkit/build-angular:extract-i18n",
76 | "options": {
77 | "browserTarget": "ng-three-template:build"
78 | }
79 | },
80 | "test": {
81 | "builder": "@angular-devkit/build-angular:karma",
82 | "options": {
83 | "main": "src/test.ts",
84 | "karmaConfig": "./karma.conf.js",
85 | "polyfills": "src/polyfills.ts",
86 | "tsConfig": "src/tsconfig.spec.json",
87 | "scripts": [],
88 | "styles": [
89 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
90 | "src/styles/main.scss"
91 | ],
92 | "assets": [
93 | "src/assets",
94 | "src/favicon.ico"
95 | ]
96 | }
97 | },
98 | "lint": {
99 | "builder": "@angular-devkit/build-angular:tslint",
100 | "options": {
101 | "tsConfig": [
102 | "src/tsconfig.app.json",
103 | "src/tsconfig.spec.json"
104 | ],
105 | "exclude": [
106 | "**/node_modules/**"
107 | ]
108 | }
109 | }
110 | }
111 | },
112 | "ng-three-template-e2e": {
113 | "root": "e2e/",
114 | "projectType": "application",
115 | "prefix": "",
116 | "architect": {
117 | "e2e": {
118 | "builder": "@angular-devkit/build-angular:protractor",
119 | "options": {
120 | "protractorConfig": "./protractor.conf.js",
121 | "devServerTarget": "ng-three-template:serve"
122 | },
123 | "configurations": {
124 | "production": {
125 | "devServerTarget": "ng-dummy:serve:production"
126 | }
127 | }
128 | },
129 | "lint": {
130 | "builder": "@angular-devkit/build-angular:tslint",
131 | "options": {
132 | "tsConfig": "e2e/tsconfig.e2e.json",
133 | "exclude": [
134 | "**/node_modules/**"
135 | ]
136 | }
137 | }
138 | }
139 | }
140 | },
141 | "defaultProject": "ng-three-template"
142 | }
--------------------------------------------------------------------------------
/python/services/backbonelogger.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | backbonelogger
4 | --------------
5 |
6 | .. module:: backbonelogger
7 | :platform: Unix, Windows
8 | :synopsis: provides logging
9 |
10 | log levels:
11 |
12 | ======== ===========
13 | ERROR 'ERROR'
14 | WARNING 'WARNING'
15 | INFO 'INFO'
16 | DEBUG 'DEBUG'
17 | ======== ===========
18 |
19 | Created 2016/2017
20 |
21 | @author: oliver, johannes
22 | """
23 |
24 | import logging
25 | import os
26 |
27 |
28 | class Logger():
29 | """
30 | This brings advanced logging from the python batteries:
31 |
32 | logger = Logger('clientLogger')
33 |
34 | logger.format([], '[%(asctime)-15s] %(name)-8s %(levelname)-7s -- %(message)s')
35 |
36 | logger.level("DEBUG")
37 |
38 | Das format gibt ein Ausgabeformat vor, das bestimmte items vorhält. Bei FDM Tool braucht zunächst kein item in die Liste rein, also eher sowas::
39 |
40 | logger.format(["clientip","user"], '[%(asctime)-15s] %(name)-8s %(levelname)-7s %(clientip)s %(user)s -- %(message)s')
41 |
42 | a basic logger format would be::
43 |
44 | "%(levelname)s:%(name)s:%(message)s"
45 |
46 | """
47 |
48 | def __init__(self, name, path=os.path.dirname(__file__)):
49 | self.logger = logging.getLogger(name=name)
50 | self.items = []
51 | self.path = path
52 | self.format(
53 | ["orig"],
54 | '[%(asctime)-15s] %(orig)-12s %(levelname)-7s -- %(message)s')
55 | self.level("WARNING")
56 |
57 | def getLogger(self, name):
58 | return self
59 | # Logger(name,path=self.path)
60 | # self.logger.getChild(name)
61 |
62 | def format(self, items, FORMAT):
63 | """
64 | :param items: [] list type item argument(s)
65 | :param FORMAT: '[%(asctime)-15s] %(name)-8s %(levelname)-7s -- %(message)s' format string e.g. with four variables
66 | """
67 | if self.logger.handlers:
68 | return
69 | self.items = items
70 | handler = logging.StreamHandler()
71 | self.formatter = logging.Formatter(FORMAT)
72 | handler.setFormatter(self.formatter)
73 | self.logger.addHandler(handler)
74 |
75 | def level(self, loglevel):
76 | numeric_level = getattr(logging, loglevel.upper(), None)
77 | if isinstance(numeric_level, int):
78 | self.logger.setLevel(numeric_level)
79 | else:
80 | raise ValueError('Invalid log level: %s' % loglevel)
81 |
82 | def log(self, *msgs, **logdict):
83 | """
84 | more robust version, since missing arguments in the logdict are handled correctly
85 | TODO: add the loglevel logic
86 |
87 | Most commonly use like this::
88 |
89 | self.cn = self.__class__.__name__
90 | message = f"my message from {self.cn}"
91 |
92 | log(lvl="INFO", msg=message, orig=self.cn)
93 |
94 | """
95 | # if logger.isEnabledFor(self.DEBUG):
96 | # this is to put a special output level in the Logger class
97 |
98 | if not msgs:
99 | msg = logdict.pop('msg')
100 | else:
101 | msg = msgs[0]
102 | for i in self.items:
103 | if i not in logdict.keys():
104 | logdict.update({i: ""})
105 | if 'lvl' in logdict.keys():
106 | lvl = logdict['lvl']
107 | numeric_level = getattr(logging, lvl.upper(), None)
108 | if not isinstance(numeric_level, int):
109 | raise ValueError('Invalid log level: %s' % lvl)
110 | else:
111 | numeric_level = logging.INFO
112 | self.logger.log(numeric_level, msg, extra=logdict)
113 |
114 |
115 | if __name__ == "__main__":
116 |
117 | logger = Logger("TestLogger")
118 | logger.level("DEBUG")
119 |
120 | class ExampleClass():
121 | """
122 |
123 | """
124 |
125 | def __init__(self):
126 | self.logger = logger.getLogger(name=self.__class__.__name__)
127 | self.logger.level("DEBUG")
128 |
129 | def print_fun(self, msg):
130 | self.logger.log(lvl="DEBUG", msg=msg)
131 |
132 | class OtherExampleClass():
133 | """
134 |
135 | """
136 |
137 | def __init__(self):
138 | self.logger = logger.getLogger(name=self.__class__.__name__)
139 | self.logger.level("DEBUG")
140 |
141 | def print_fun(self, msg):
142 | self.logger.log(lvl="DEBUG", msg=msg)
143 |
144 | logger.log(lvl="INFO", msg="Initial message")
145 |
146 | new_instance1 = ExampleClass()
147 | new_instance1.print_fun("message1")
148 | new_instance2 = OtherExampleClass()
149 | new_instance2.print_fun("message2")
150 |
--------------------------------------------------------------------------------
/client/src/styles/mixins.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Source: https://engageinteractive.co.uk/blog/top-10-scss-mixins
3 | */
4 |
5 | // To quickly centre a block element
6 | @mixin push--auto {
7 | margin: {
8 | left: auto;
9 | right: auto;
10 | }
11 | }
12 |
13 | /*
14 | * Usage Example:
15 | *
16 | * div::after {
17 | * @include pseudo;
18 | * top: -1rem; left: -1rem;
19 | * width: 1rem; height: 1rem;
20 | * }
21 | */
22 | @mixin pseudo($display: block, $pos: absolute, $content: ''){
23 | content: $content;
24 | display: $display;
25 | position: $pos;
26 | }
27 |
28 | /*
29 | * Usage Example:
30 | *
31 | * div {
32 | * @include responsive-ratio(16,9);
33 | * }
34 | */
35 | @mixin responsive-ratio($x,$y, $pseudo: false) {
36 | $padding: unquote( ( $y / $x ) * 100 + '%' );
37 | @if $pseudo {
38 | &:before {
39 | @include pseudo($pos: relative);
40 | width: 100%;
41 | padding-top: $padding;
42 | }
43 | } @else {
44 | padding-top: $padding;
45 | }
46 | }
47 |
48 | /*
49 | * This mixin takes all the hassle out of creating that triangle you'll see coming out of most traditional tooltips,
50 | * all without images, you just specify it's colour, how big you want it, the direction it's going to come out of your element and you're done!
51 | */
52 | @mixin css-triangle($color, $direction, $size: 6px, $position: absolute, $round: false){
53 | @include pseudo($pos: $position);
54 | width: 0;
55 | height: 0;
56 | @if $round {
57 | border-radius: 3px;
58 | }
59 | @if $direction == down {
60 | border-left: $size solid transparent;
61 | border-right: $size solid transparent;
62 | border-top: $size solid $color;
63 | margin-top: 0 - round( $size / 2.5 );
64 | } @else if $direction == up {
65 | border-left: $size solid transparent;
66 | border-right: $size solid transparent;
67 | border-bottom: $size solid $color;
68 | margin-bottom: 0 - round( $size / 2.5 );
69 | } @else if $direction == right {
70 | border-top: $size solid transparent;
71 | border-bottom: $size solid transparent;
72 | border-left: $size solid $color;
73 | margin-right: -$size;
74 | } @else if $direction == left {
75 | border-top: $size solid transparent;
76 | border-bottom: $size solid transparent;
77 | border-right: $size solid $color;
78 | margin-left: -$size;
79 | }
80 | }
81 |
82 | @mixin font-source-sans($size: false, $colour: false, $weight: false, $lh: false) {
83 | font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif;
84 | @if $size { font-size: $size; }
85 | @if $colour { color: $colour; }
86 | @if $weight { font-weight: $weight; }
87 | @if $lh { line-height: $lh; }
88 | }
89 |
90 |
91 | @mixin input-placeholder {
92 | &.placeholder { @content; }
93 | &:-moz-placeholder { @content; }
94 | &::-moz-placeholder { @content; }
95 | &:-ms-input-placeholder { @content; }
96 | &::-webkit-input-placeholder { @content; }
97 | }
98 |
99 | /*
100 | * Usage Example:
101 | *
102 | * .site-header {
103 | * padding: 2rem;
104 | * font-size: 1.8rem;
105 | * @include mq('tablet-wide') {
106 | * padding-top: 4rem;
107 | * font-size: 2.4rem;
108 | * }
109 | * }
110 | */
111 | $breakpoints: (
112 | "phone": 400px,
113 | "phone-wide": 480px,
114 | "phablet": 560px,
115 | "tablet-small": 640px,
116 | "tablet": 768px,
117 | "tablet-wide": 1024px,
118 | "desktop": 1248px,
119 | "desktop-wide": 1440px
120 | );
121 | @mixin mq($width, $type: min) {
122 | @if map_has_key($breakpoints, $width) {
123 | $width: map_get($breakpoints, $width);
124 | @if $type == max {
125 | $width: $width - 1px;
126 | }
127 | @media only screen and (#{$type}-width: $width) {
128 | @content;
129 | }
130 | }
131 | }
132 |
133 | /*
134 | * .site-header {
135 | * z-index: z('site-header');
136 | * }
137 | */
138 | @function z($name) {
139 | @if index($z-indexes, $name) {
140 | @return (length($z-indexes) - index($z-indexes, $name)) + 1;
141 | } @else {
142 | @warn 'There is no item "#{$name}" in this list; choose one of: #{$z-indexes}';
143 | @return null;
144 | }
145 | }
146 | $z-indexes: (
147 | "outdated-browser",
148 | "modal",
149 | "site-header",
150 | "page-wrapper",
151 | "site-footer"
152 | );
153 |
154 | // Simple and effective for when you need to trigger hardware acceleration for some animation,
155 | // keeping everything fast, slick and flicker-free.
156 | @mixin hardware($backface: true, $perspective: 1000) {
157 | @if $backface {
158 | backface-visibility: hidden;
159 | }
160 | perspective: $perspective;
161 | }
162 |
163 | @mixin truncate($truncation-boundary) {
164 | max-width: $truncation-boundary;
165 | white-space: nowrap;
166 | overflow: hidden;
167 | text-overflow: ellipsis;
168 | }
169 |
170 | /*
171 | * Source: http://thesassway.com/intermediate/mixins-for-semi-transparent-colors
172 | *
173 | * Usage Example:
174 | *
175 | * .button { @include alpha-attribute('background-color', rgba(black, 0.5), white); }
176 | */
177 | @mixin alpha-attribute($attribute, $color, $background) {
178 | $percent: alpha($color) * 100%;
179 | $opaque: opacify($color, 1);
180 | $solid-color: mix($opaque, $background, $percent);
181 | #{$attribute}: $solid-color;
182 | #{$attribute}: $color;
183 | }
184 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # Angular & Bokeh
2 | > A small example on connecting bokeh with Angular and send data from a python backend.
3 |
4 | objectives that are solved here:
5 |
6 | * display a chart figure in an app or website,
7 | * be able to send update events from the python back-end.
8 |
9 | A bokeh-chart component might not always be the optimal solution, but we found this is a nice minimal example and a demonstrator:
10 |
11 |
12 |
13 | [see in app.component](client/src/app/app.component.html)
14 |
15 | The interesting part of the problem is not the integration of bokeh as a bokeh-chart component to angular,
16 |
17 | [see in bokeh-chart.component](client/src/app/shared/components/bokeh-chart/bokeh-chart.component.ts)
18 |
19 | but the service, that provides the data for the chart and the functionality to the component, e.g. getChart():
20 |
21 | [see in bokeh.service](client/src/app/shared/services/bokeh.service.ts)
22 |
23 | and a possible back-end service written in python addChart() sends the chartItem as a json item over the websocket:
24 |
25 | [see in pythonToView](python/services/pythonToView.py)
26 |
27 | the minimal example, even written as a member function, looks very simple (chartProvider.py):
28 |
29 | [see in chartProvider](python/services/chartProvider.py)
30 |
31 | ## Installation
32 |
33 | OS X & Linux & Windows:
34 |
35 | Install Anaconda and open a conda enabled shell:
36 |
37 | ```bash
38 |
39 | After installing dependencies you can run Angular unit and end-to-end tests:
40 |
41 | ```bash
42 | cd client
43 | npm test --silent
44 | npm run e2e --silent
45 | ```
46 |
47 | Python tests can be added under `python/tests` and executed with `pytest`.
48 |
49 | ```bash
50 | conda create -n angular-bokeh python=3.8 simplejson "bokeh>=3.0" aiohttp
51 | conda activate angular-bokeh
52 | pip install -r requirements.txt
53 | ```
54 |
55 |
56 |
57 | Before running the setup script make sure Node.js **22.x** is installed
58 | system-wide. On Debian/Ubuntu based systems you can use the NodeSource
59 | packages:
60 |
61 | ```bash
62 | curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
63 | sudo apt-get install -y nodejs
64 | ```
65 |
66 | Angular 19 requires Node.js **22.x**. Ensure your Node version matches this
67 | requirement before continuing.
68 |
69 | After creating the conda environment you can install all Node and Python
70 | dependencies in one step by running the provided setup script:
71 |
72 | ```bash
73 | ./setup.sh
74 | ```
75 |
76 |
77 | If you install the Node dependencies manually, run `npm install --legacy-peer-deps`
78 | to avoid Angular peer dependency conflicts. The provided `./setup.sh` script
79 | already uses this flag.
80 |
81 | This repository now targets **Bokeh 3.x** and **Angular 19**. Make sure the Bokeh-JS version referenced in `client/src/index.html` matches the installed
82 | Python package.
83 |
84 | ## Usage
85 |
86 | open shell in root folder of the repository
87 |
88 | ```
89 | $ cd client/
90 | $ npm install
91 | $ ng build
92 | ```
93 |
94 | change to python folder
95 |
96 | ```
97 | $ cd ../python/
98 | $ python app.py
99 | ```
100 |
101 | The server listens on port `9000` by default and serves the Angular build from
102 | `../client/dist/dev`. You can override these defaults using environment
103 | variables or command line options:
104 |
105 | ```
106 | $ PORT=8080 ANGULAR_DIST_PATH=/path/to/build python app.py --port 8080 --angular-path /path/to/build
107 | ```
108 |
109 | in your browser you should see the app being served to:
110 |
111 | ```
112 | http://localhost:9000/
113 | ```
114 |
115 | ## Wiki
116 |
117 | [Angular & Bokeh Wiki](../../wiki)
118 |
119 | ## Meta
120 |
121 | contact@nucos.de
122 |
123 | [https://github.com/NuCOS](https://github.com/NuCOS)
124 |
125 | ## Contributing
126 |
127 | 1. Fork it ()
128 | 2. Create your feature branch (`git checkout -b feature/fooBar`)
129 | 3. Commit your changes (`git commit -am 'Add some fooBar'`)
130 | 4. Push to the branch (`git push origin feature/fooBar`)
131 | 5. Create a new Pull Request
132 |
133 | ### Keep your fork up-to-date
134 |
135 | In your local working copy of your forked repository, you should add the original GitHub repository to the "remote" branches. ("remotes" are like nicknames for the URLs of repositories - origin is the default one, for example.) Then you can fetch all the branches from that upstream repository, and rebase your work to continue working on the upstream version. This can be done with the following sequence of commands:
136 |
137 | 1. Add the remote, call it e.g.: "upstream":
138 |
139 | ```
140 | git remote add upstream git@github.com:NuCOS/angular-bokeh.git
141 | ```
142 |
143 | 2. Fetch all the branches of that remote into remote-tracking branches, such as upstream/master:
144 |
145 | ```
146 | git fetch upstream
147 | ```
148 |
149 | 3. Make sure that you're on your master branch:
150 |
151 | ```
152 | git checkout master
153 | ```
154 | 4. Rewrite your master branch so that any commits of yours that aren't already in upstream/master are replayed on top of that other branch:
155 |
156 | ```
157 | git rebase upstream/master
158 | ```
159 |
160 | If you don't want to rewrite the history of your master branch, (for example because other people may have cloned it) then you should replace the last command with git merge upstream/master. However, for making further pull requests that are as clean as possible, it's probably better to rebase.
161 |
162 | 5. If you've rebased your branch onto upstream/master you may need to force the push in order to push it to your own forked repository on GitHub. You'd do that with:
163 | ```
164 | git push -f origin master
165 | ```
166 | You only need to use the -f the first time after you've rebased
167 |
--------------------------------------------------------------------------------
/client/src/assets/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
204 |
--------------------------------------------------------------------------------