Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions backend/wordmodels/neighbor_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ def _graph_vega_doc(timeframes, nodes, links):
"height": 500,
"padding": 0,
"autosize": "none",

"signals": [
{ "name": "cx", "update": "width / 2" },
{ "name": "cy", "update": "height / 2" },
Expand Down Expand Up @@ -154,7 +153,14 @@ def _graph_vega_doc(timeframes, nodes, links):
"on": [
{"events": {"signal": "fix"}, "update": "fix && fix.length"}
]
}
},
{
'name': 'theme',
'description': 'Current site theme (light/dark)',
'bind': {
'element': '#current-theme',
}
},
],

"data": [
Expand All @@ -181,13 +187,13 @@ def _graph_vega_doc(timeframes, nodes, links):
]
}
],

"scales": [
{
'name': 'link-color',
'type': 'linear',
'domain': {"data": "link-data", "field": "value"},
'range': {'scheme': 'greys'},
'reverse': { 'signal': 'theme === "dark"' },
},
{
'name': 'text-weight',
Expand All @@ -196,13 +202,11 @@ def _graph_vega_doc(timeframes, nodes, links):
'range': ['bold', 'normal'],
}
],

"marks": [
{
"name": "nodes",
"type": "text",
"zindex": 1,

"from": {"data": "node-data"},
"on": [
{
Expand All @@ -218,7 +222,6 @@ def _graph_vega_doc(timeframes, nodes, links):
"encode": {
"enter": {
"fontSize": {"value": 15},
"fill": {"value": "black"},
"text": {"field": "term"},
"baseline": {"value": "middle"},
"align": {"value": "center"},
Expand All @@ -227,7 +230,8 @@ def _graph_vega_doc(timeframes, nodes, links):
},
},
"update": {
"cursor": {"value": "pointer"}
"cursor": {"value": "pointer"},
"fill": {'signal': 'theme === "dark" ? "white" : "black"'},
},
},
"transform": [
Expand Down
67 changes: 67 additions & 0 deletions documentation/Styling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Styling
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond impressed by this documentation.


This document covers CSS styling for the frontend.

Textcavator uses the CSS framework [bulma](https://bulma.io/documentation/).

Initial and derived variables from bulma are customised in [_utilities.css](/frontend/src/_utilities.scss). This file only defines variables and mixins and can be imported in component stylesheets. [styles.csss](/frontend/src/styles.scss) includes site-wide selectors.

## Dark mode

Dark mode is managed by the [ThemeService](/frontend/src/app/services/theme.service.ts).

The service sets `data-theme="dark"` / `data-theme="light"` on the HTML root node, which is used by CSS selectors. If you need to observe the theme in a component, you can also use `ThemeService.theme$`.

## Other libraries

Several other libraries are used to provide components, visualisations, etc. These are often customised to fit the site theme and/or adapt to dark mode.

## PrimeNG components

We use several components from [primeNG](https://v19.primeng.org/). [primeng-theme.ts](/frontend/src/app/primeng-theme.ts) defines the preset to customise primeNG styles, mostly using bulma CSS variables.

## Chart.js

[select-color.ts](/frontend/src/app/utils/select-color.ts) defines the colour palettes for data visualisations and a utility function to select the nth colour. There are several palettes; the default is chosen to fit the site theme. The [palette selector](/frontend/src/app/visualization/visualization-footer/palette-select/palette-select.component.ts) lets users choose a preferred palette; the `VisualizationComponent` and `WordModelsComponent` provide the chosen palette as input to visualisation components.

Chart.js has no built-in "dark mode". The `ThemeService` adjusts several defaults to get readable charts in dark mode, and updates all chart instances when the theme changes.

## Vega

To let a Vega visualisation adapt to dark/light mode, relevant colours should depend on a signal.

Add the following signal in your vega document:

```json
{
"name": "theme",
"description": "Current site theme (light/dark)",
"bind": { "element": "#current-theme" }
}
```

The value of the `theme` signal will be `"dark"` or `"light"`. When you define colours in your document, let theme depend on this signal, e.g. with a mark like this:

```json
{
"marks": [
{
"type": "text",
// ...
"encode": {
"update": {
"fill": {"signal": "theme === \"dark\" ? \"white\" : \"black\""}
},
}
},
]
}
```

When you display the visualisation, include a [theme indicator](/frontend/src/app/visualization/theme-indicator.directive.ts) on your page like this:

```html
<input iaThemeIndicator >
```

The Vega graph will bind the `theme` signal to the value of this element. The element will not be visible to users.
1 change: 1 addition & 0 deletions frontend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { MatomoConfig, matomoImports } from './routing/matomo';
import { stylePreset } from './primeng-theme';
import { CoreModule } from './core/core.module';


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Load-bearing whitespace?

export const appRoutes: Routes = [
{
path: 'search/:corpus',
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/app/core/footer/footer.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
<strong>Textcavator is developed by </strong>
</p>
<a href="https://cdh.uu.nl/centre-for-digital-humanities/research-software-lab/" target="_blank">
<img src="assets/UU-CDH_logo_EN_def_UU_CDH_logo_EN_yellowwhite.jpg" alt="Centre for Digital Humanities, Utrecht University" width="400">
<img [src]="
(theme$ | async) == 'dark' ?
'assets/UU-CDH_logo_EN_def_UU_CDH_logo_EN_white.png' :
'assets/UU-CDH_logo_EN_def_UU_CDH_logo_EN_yellowwhite.jpg'"
alt="Centre for Digital Humanities, Utrecht University"
width="400">
</a>
</div>
<div *ngIf="environment.logos?.length" class="column">
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/app/core/footer/footer.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component } from '@angular/core';

import { environment } from '@environments/environment';
import { ThemeService } from '@services/theme.service';

@Component({
selector: 'ia-footer',
Expand All @@ -10,7 +11,9 @@ import { environment } from '@environments/environment';
})
export class FooterComponent {
environment = environment as any;
theme$ = this.themeService.theme$;

constructor() { }

constructor(
private themeService: ThemeService
) { }
}
14 changes: 14 additions & 0 deletions frontend/src/app/core/menu/menu.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@
</a>
</div>
<div class="navbar-end">
<!-- theme dropdown -->
<ia-menu-dropdown>
<span iaMenuDropdownLabel>
<fa-icon [icon]="currentThemeOption.icon"/>
</span>
@for (option of themeOptions; track option.value) {
<a class="navbar-item" role="button" (click)="setTheme(option.value)">
<span class="icon" aria-hidden><fa-icon [icon]="option.icon"/></span>
<span>{{option.label}}</span>
</a>
}
</ia-menu-dropdown>

<!-- user dropdown -->
<ia-menu-dropdown *ngIf="user$ | async as user; else notLoggedIn">
<div iaMenuDropdownLabel>
<span class="icon" aria-hidden="true">
Expand Down
24 changes: 22 additions & 2 deletions frontend/src/app/core/menu/menu.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { User } from '@models/index';
import { environment } from '@environments/environment';
import { AuthService } from '@services/auth.service';
import { filter, map } from 'rxjs/operators';
import { navIcons, userIcons } from '@shared/icons';
import { navIcons, themeIcons, userIcons } from '@shared/icons';
import * as _ from 'lodash';
import { Theme, ThemeService } from '@app/services/theme.service';

@Component({
selector: 'ia-menu',
Expand All @@ -29,14 +31,28 @@ export class MenuComponent implements OnDestroy, OnInit {
navIcons = navIcons;
userIcons = userIcons;

themeOptions = [
{ label: 'auto', icon: themeIcons.system, value: undefined },
{ label: 'light', icon: themeIcons.light, value: Theme.LIGHT },
{ label: 'dark', icon: themeIcons.dark, value: Theme.DARK },
];


private destroy$ = new Subject<void>();

constructor(
private authService: AuthService,
private router: Router,
private route: ActivatedRoute
private route: ActivatedRoute,
private themeService: ThemeService,
) {}

get currentThemeOption() {
return this.themeOptions.find(
option => option.value == this.themeService.selection$.value
);
}

ngOnDestroy() {
this.destroy$.next(undefined);
}
Expand All @@ -50,6 +66,10 @@ export class MenuComponent implements OnDestroy, OnInit {
this.menuOpen$.next(!this.menuOpen$.value);
}

setTheme(value: Theme | undefined) {
this.themeService.selection$.next(value);
}

public async logout() {
this.authService.logout(true).subscribe();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
.title-divider {
margin-top: 0px;
margin-bottom: 1rem;
background-color: var(--bulma-primary-soft);
}

.columns .align-bottom {
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/app/services/theme.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { ThemeService } from './theme.service';

describe('ThemeService', () => {
let service: ThemeService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ThemeService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});
});
83 changes: 83 additions & 0 deletions frontend/src/app/services/theme.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, combineLatest, fromEvent, map, Observable, startWith } from 'rxjs';
import { Chart } from 'chart.js';
import _ from 'lodash';
import { environment } from '@environments/environment';

export enum Theme {
DARK = 'dark',
LIGHT = 'light',
}

@Injectable({
providedIn: 'root'
})
export class ThemeService {
/** Theme selection from the user.
* `null` means no explicit preference, i.e. use the system theme.
*/
selection$ = new BehaviorSubject<Theme | null>(null);

/** Theme used by the site */
theme$: Observable<Theme>;

/** Default dark/light preference from the browser/OS */
private systemTheme$: Observable<Theme>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're called System Team

private storageKey = 'theme';

constructor() {
const initialSelection = environment.runInIFrame ? Theme.LIGHT : this.readStoredSelection();
this.selection$.next(initialSelection);
this.selection$.subscribe((theme) => this.storeSelection(theme));

this.systemTheme$ = this.watchSystemTheme();
this.theme$ = combineLatest([this.selection$, this.systemTheme$]).pipe(
map(([selection, system]) => selection || system)
);
this.theme$.subscribe((theme) => this.applyTheme(theme));
}

private watchSystemTheme(): Observable<Theme> {
const query = window.matchMedia('(prefers-color-scheme: dark)');
return fromEvent<MediaQueryListEvent>(query, 'change').pipe(
startWith(query),
map(list => list.matches ? Theme.DARK : Theme.LIGHT)
);
}

/** set theme in the site layout */
private applyTheme(theme: Theme) {
const root = (document.getRootNode() as Document).documentElement;
root.setAttribute('data-theme', theme);
this.applyChartJSTheme();
}

/** set chartjs defaults based on current style, and update active charts */
private applyChartJSTheme() {
const style = window.getComputedStyle(document.body);
Chart.defaults.color = () => style.getPropertyValue('--bulma-text-strong');
Chart.defaults.borderColor = () => style.getPropertyValue('--bulma-border');

const active = _.values(Chart.instances);
for (let chart of active) {
chart.update();
}
}

private readStoredSelection(): Theme | null {
const value = localStorage.getItem(this.storageKey);
if ([Theme.DARK, Theme.LIGHT].map(String).includes(value)) {
return value as Theme;
} else {
return null;
}
}

private storeSelection(theme: Theme | null): void {
if (theme) {
localStorage.setItem(this.storageKey, theme);
} else {
localStorage.removeItem(this.storageKey);
}
}
}
9 changes: 9 additions & 0 deletions frontend/src/app/shared/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ import {
faArrowUp,
faArrowDown,
faRedo,
faCircleHalfStroke,
faMoon,
faSun,
} from '@fortawesome/free-solid-svg-icons';

type IconDefinition = SolidIconDefinition | RegularIconDefinition;
Expand Down Expand Up @@ -179,3 +182,9 @@ export const entityIcons: Icons = {
organization: faBuilding,
miscellaneous: faBookmark,
}

export const themeIcons: Icons = {
system: faCircleHalfStroke,
light: faSun,
dark: faMoon,
}
3 changes: 3 additions & 0 deletions frontend/src/app/visualization/map/map.component.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
<div class="map-container">
<div #vegaMap></div>
</div>

<input iaThemeIndicator >

Loading