├── 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 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 35 | 37 | 61 | 92 | 203 | 204 | --------------------------------------------------------------------------------