diff --git a/package-lock.json b/package-lock.json index 9665e50..c988d09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8060,8 +8060,7 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", @@ -17998,9 +17997,9 @@ } }, "topola": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/topola/-/topola-2.2.9.tgz", - "integrity": "sha512-bMS3RxmB4P9JmwaiycnbgguIRaXUUAnASyUqGc1GNmloRUBM9yZgGWQMZ4xkFHE7Otr4IiyMp8wOwrXahYLaYQ==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/topola/-/topola-2.3.4.tgz", + "integrity": "sha512-eJpy19T6xFkrLgf1NGAMkgmLoswB00hY206LQZWiQRURr0+p0fdGAmNQKkWLba/e/MT3H/Qe/SSnAvRmHhFobA==", "requires": { "d3": "^5.4.0", "d3-flextree": "^2.1.1", diff --git a/package.json b/package.json index 2a507ae..249b903 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "react-router-dom": "^4.3.1", "semantic-ui-css": "^2.4.1", "semantic-ui-react": "^0.84.0", - "topola": "^2.2.9" + "topola": "^2.3.4" }, "devDependencies": { "@types/array.prototype.flatmap": "^1.2.0", diff --git a/src/app.tsx b/src/app.tsx index 82c8c1f..0ae5fae 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,7 +1,7 @@ import * as queryString from 'query-string'; import * as React from 'react'; import {analyticsEvent} from './analytics'; -import {Chart} from './chart'; +import {Chart, ChartType} from './chart'; import {Details} from './details'; import {getSelection, loadFromUrl, loadGedcom} from './load_data'; import {IndiInfo} from 'topola'; @@ -53,20 +53,27 @@ interface State { hash?: string; /** Error to display. */ error?: string; - /** True if currently loading. */ + /** True if data is currently being loaded. */ loading: boolean; /** URL of the data that is loaded or is being loaded. */ url?: string; - /** Whether the side panel is shoen. */ + /** Whether the side panel is shown. */ showSidePanel?: boolean; /** Whether the app is in embedded mode, i.e. embedded in an iframe. */ embedded: boolean; - /** Whether the app is in standalone mode, i.e. showing 'open file' menus */ + /** Whether the app is in standalone mode, i.e. showing 'open file' menus. */ standalone: boolean; + /** Type of displayed chart. */ + chartType: ChartType; } export class App extends React.Component { - state: State = {loading: false, embedded: false, standalone: true}; + state: State = { + loading: false, + embedded: false, + standalone: true, + chartType: ChartType.Hourglass, + }; chartRef: Chart | null = null; private isNewData( @@ -81,16 +88,18 @@ export class App extends React.Component { ); } - /** Sets the state with a new individual selection. */ - private updateSelection(selection: IndiInfo) { + /** Sets the state with a new individual selection and chart type. */ + private updateDisplay(selection: IndiInfo, chartType?: ChartType) { if ( !this.state.selection || this.state.selection.id !== selection.id || - this.state.selection!.generation !== selection.generation + this.state.selection!.generation !== selection.generation || + (chartType !== undefined && chartType !== this.state.chartType) ) { this.setState( Object.assign({}, this.state, { selection, + chartType: chartType !== undefined ? chartType : this.state.chartType, }), ); } @@ -179,6 +188,10 @@ export class App extends React.Component { const hash = getParam('file'); const handleCors = getParam('handleCors') !== 'false'; // True by default. const standalone = getParam('standalone') !== 'false'; // True by default. + const view = getParam('view'); + // Hourglass is the default view. + const chartType = + view === 'relatives' ? ChartType.Relatives : ChartType.Hourglass; const gedcom = this.props.location.state && this.props.location.state.data; const images = @@ -198,6 +211,7 @@ export class App extends React.Component { loading: true, url, standalone, + chartType, }), ); const data = hash @@ -221,6 +235,7 @@ export class App extends React.Component { url, showSidePanel, standalone, + chartType, }), ); } catch (error) { @@ -234,7 +249,7 @@ export class App extends React.Component { indi, generation, ); - this.updateSelection(selection); + this.updateDisplay(selection, chartType); } } @@ -246,7 +261,7 @@ export class App extends React.Component { analyticsEvent('selection_changed'); if (this.state.embedded) { // In embedded mode the URL doesn't change. - this.updateSelection(selection); + this.updateDisplay(selection); return; } const location = this.props.location; @@ -263,8 +278,9 @@ export class App extends React.Component {
(this.chartRef = ref)} /> {this.state.showSidePanel ? ( diff --git a/src/chart.tsx b/src/chart.tsx index 5c315cb..9a45ee8 100644 --- a/src/chart.tsx +++ b/src/chart.tsx @@ -10,6 +10,7 @@ import { createChart, DetailedRenderer, HourglassChart, + RelativesChart, } from 'topola'; /** Called when the view is dragged with the mouse. */ @@ -101,9 +102,16 @@ function canvasToBlob(canvas: HTMLCanvasElement, type: string) { }); } +/** Supported chart types. */ +export enum ChartType { + Hourglass, + Relatives, +} + export interface ChartProps { data: JsonGedcomData; selection: IndiInfo; + chartType: ChartType; onSelection: (indiInfo: IndiInfo) => void; } @@ -111,6 +119,18 @@ export interface ChartProps { export class Chart extends React.PureComponent { private chart?: ChartHandle; + private getChartType() { + switch (this.props.chartType) { + case ChartType.Hourglass: + return HourglassChart; + case ChartType.Relatives: + return RelativesChart; + default: + // Fall back to hourglass chart. + return HourglassChart; + } + } + /** * Renders the chart or performs a transition animation to a new state. * If indiInfo is not given, it means that it is the initial render and no @@ -121,7 +141,7 @@ export class Chart extends React.PureComponent { (d3.select('#chart').node() as HTMLElement).innerHTML = ''; this.chart = createChart({ json: this.props.data, - chartType: HourglassChart, + chartType: this.getChartType(), renderer: DetailedRenderer, svgSelector: '#chart', indiCallback: (info) => this.props.onSelection(info), @@ -192,7 +212,10 @@ export class Chart extends React.PureComponent { } componentDidUpdate(prevProps: ChartProps) { - this.renderChart({initialRender: this.props.data !== prevProps.data}); + const initialRender = + this.props.data !== prevProps.data || + this.props.chartType !== prevProps.chartType; + this.renderChart({initialRender}); } /** Make intl appear in this.context. */ diff --git a/src/top_bar.tsx b/src/top_bar.tsx index 8801a4e..484cc49 100644 --- a/src/top_bar.tsx +++ b/src/top_bar.tsx @@ -184,6 +184,16 @@ export class TopBar extends React.Component< } } + changeView(view: string) { + const location = this.props.location; + const search = queryString.parse(location.search); + if (search.view !== view) { + search.view = view; + location.search = queryString.stringify(search); + this.props.history.push(location); + } + } + componentDidMount() { this.initializeSearchIndex(); } @@ -259,6 +269,7 @@ export class TopBar extends React.Component< + @@ -280,6 +291,34 @@ export class TopBar extends React.Component< + + + + +
+ } + className="item" + > + + this.changeView('hourglass')}> + + + + this.changeView('relatives')}> + + + + + + , data: SearchProps) => diff --git a/src/translations/pl.json b/src/translations/pl.json index fbf63c5..4aa8996 100644 --- a/src/translations/pl.json +++ b/src/translations/pl.json @@ -6,6 +6,9 @@ "menu.pdf_file": "Plik PDF", "menu.png_file": "Plik PNG", "menu.svg_file": "Plik SVG", + "menu.view": "Widok", + "menu.hourglass": "Wykres klepsydrowy", + "menu.relatives": "Wszyscy krewni", "menu.github": "Źródła na GitHub", "menu.powered_by": "Topola Genealogy", "menu.search.placeholder": "Szukaj osoby",