Added menu option to display the relatives chart

This commit is contained in:
Przemek Wiech
2019-05-20 22:13:18 +02:00
parent d091d3e3ce
commit 9bd1720122
6 changed files with 99 additions and 19 deletions

9
package-lock.json generated
View File

@@ -8060,8 +8060,7 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.2.4", "version": "2.2.4",
@@ -17998,9 +17997,9 @@
} }
}, },
"topola": { "topola": {
"version": "2.2.9", "version": "2.3.4",
"resolved": "https://registry.npmjs.org/topola/-/topola-2.2.9.tgz", "resolved": "https://registry.npmjs.org/topola/-/topola-2.3.4.tgz",
"integrity": "sha512-bMS3RxmB4P9JmwaiycnbgguIRaXUUAnASyUqGc1GNmloRUBM9yZgGWQMZ4xkFHE7Otr4IiyMp8wOwrXahYLaYQ==", "integrity": "sha512-eJpy19T6xFkrLgf1NGAMkgmLoswB00hY206LQZWiQRURr0+p0fdGAmNQKkWLba/e/MT3H/Qe/SSnAvRmHhFobA==",
"requires": { "requires": {
"d3": "^5.4.0", "d3": "^5.4.0",
"d3-flextree": "^2.1.1", "d3-flextree": "^2.1.1",

View File

@@ -23,7 +23,7 @@
"react-router-dom": "^4.3.1", "react-router-dom": "^4.3.1",
"semantic-ui-css": "^2.4.1", "semantic-ui-css": "^2.4.1",
"semantic-ui-react": "^0.84.0", "semantic-ui-react": "^0.84.0",
"topola": "^2.2.9" "topola": "^2.3.4"
}, },
"devDependencies": { "devDependencies": {
"@types/array.prototype.flatmap": "^1.2.0", "@types/array.prototype.flatmap": "^1.2.0",

View File

@@ -1,7 +1,7 @@
import * as queryString from 'query-string'; import * as queryString from 'query-string';
import * as React from 'react'; import * as React from 'react';
import {analyticsEvent} from './analytics'; import {analyticsEvent} from './analytics';
import {Chart} from './chart'; import {Chart, ChartType} from './chart';
import {Details} from './details'; import {Details} from './details';
import {getSelection, loadFromUrl, loadGedcom} from './load_data'; import {getSelection, loadFromUrl, loadGedcom} from './load_data';
import {IndiInfo} from 'topola'; import {IndiInfo} from 'topola';
@@ -53,20 +53,27 @@ interface State {
hash?: string; hash?: string;
/** Error to display. */ /** Error to display. */
error?: string; error?: string;
/** True if currently loading. */ /** True if data is currently being loaded. */
loading: boolean; loading: boolean;
/** URL of the data that is loaded or is being loaded. */ /** URL of the data that is loaded or is being loaded. */
url?: string; url?: string;
/** Whether the side panel is shoen. */ /** Whether the side panel is shown. */
showSidePanel?: boolean; showSidePanel?: boolean;
/** Whether the app is in embedded mode, i.e. embedded in an iframe. */ /** Whether the app is in embedded mode, i.e. embedded in an iframe. */
embedded: boolean; 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; standalone: boolean;
/** Type of displayed chart. */
chartType: ChartType;
} }
export class App extends React.Component<RouteComponentProps, {}> { export class App extends React.Component<RouteComponentProps, {}> {
state: State = {loading: false, embedded: false, standalone: true}; state: State = {
loading: false,
embedded: false,
standalone: true,
chartType: ChartType.Hourglass,
};
chartRef: Chart | null = null; chartRef: Chart | null = null;
private isNewData( private isNewData(
@@ -81,16 +88,18 @@ export class App extends React.Component<RouteComponentProps, {}> {
); );
} }
/** Sets the state with a new individual selection. */ /** Sets the state with a new individual selection and chart type. */
private updateSelection(selection: IndiInfo) { private updateDisplay(selection: IndiInfo, chartType?: ChartType) {
if ( if (
!this.state.selection || !this.state.selection ||
this.state.selection.id !== selection.id || 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( this.setState(
Object.assign({}, this.state, { Object.assign({}, this.state, {
selection, selection,
chartType: chartType !== undefined ? chartType : this.state.chartType,
}), }),
); );
} }
@@ -179,6 +188,10 @@ export class App extends React.Component<RouteComponentProps, {}> {
const hash = getParam('file'); const hash = getParam('file');
const handleCors = getParam('handleCors') !== 'false'; // True by default. const handleCors = getParam('handleCors') !== 'false'; // True by default.
const standalone = getParam('standalone') !== '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 gedcom = this.props.location.state && this.props.location.state.data;
const images = const images =
@@ -198,6 +211,7 @@ export class App extends React.Component<RouteComponentProps, {}> {
loading: true, loading: true,
url, url,
standalone, standalone,
chartType,
}), }),
); );
const data = hash const data = hash
@@ -221,6 +235,7 @@ export class App extends React.Component<RouteComponentProps, {}> {
url, url,
showSidePanel, showSidePanel,
standalone, standalone,
chartType,
}), }),
); );
} catch (error) { } catch (error) {
@@ -234,7 +249,7 @@ export class App extends React.Component<RouteComponentProps, {}> {
indi, indi,
generation, generation,
); );
this.updateSelection(selection); this.updateDisplay(selection, chartType);
} }
} }
@@ -246,7 +261,7 @@ export class App extends React.Component<RouteComponentProps, {}> {
analyticsEvent('selection_changed'); analyticsEvent('selection_changed');
if (this.state.embedded) { if (this.state.embedded) {
// In embedded mode the URL doesn't change. // In embedded mode the URL doesn't change.
this.updateSelection(selection); this.updateDisplay(selection);
return; return;
} }
const location = this.props.location; const location = this.props.location;
@@ -263,8 +278,9 @@ export class App extends React.Component<RouteComponentProps, {}> {
<div id="content"> <div id="content">
<Chart <Chart
data={this.state.data.chartData} data={this.state.data.chartData}
onSelection={this.onSelection}
selection={this.state.selection} selection={this.state.selection}
chartType={this.state.chartType}
onSelection={this.onSelection}
ref={(ref) => (this.chartRef = ref)} ref={(ref) => (this.chartRef = ref)}
/> />
{this.state.showSidePanel ? ( {this.state.showSidePanel ? (

View File

@@ -10,6 +10,7 @@ import {
createChart, createChart,
DetailedRenderer, DetailedRenderer,
HourglassChart, HourglassChart,
RelativesChart,
} from 'topola'; } from 'topola';
/** Called when the view is dragged with the mouse. */ /** 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 { export interface ChartProps {
data: JsonGedcomData; data: JsonGedcomData;
selection: IndiInfo; selection: IndiInfo;
chartType: ChartType;
onSelection: (indiInfo: IndiInfo) => void; onSelection: (indiInfo: IndiInfo) => void;
} }
@@ -111,6 +119,18 @@ export interface ChartProps {
export class Chart extends React.PureComponent<ChartProps, {}> { export class Chart extends React.PureComponent<ChartProps, {}> {
private chart?: ChartHandle; 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. * 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 * 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<ChartProps, {}> {
(d3.select('#chart').node() as HTMLElement).innerHTML = ''; (d3.select('#chart').node() as HTMLElement).innerHTML = '';
this.chart = createChart({ this.chart = createChart({
json: this.props.data, json: this.props.data,
chartType: HourglassChart, chartType: this.getChartType(),
renderer: DetailedRenderer, renderer: DetailedRenderer,
svgSelector: '#chart', svgSelector: '#chart',
indiCallback: (info) => this.props.onSelection(info), indiCallback: (info) => this.props.onSelection(info),
@@ -192,7 +212,10 @@ export class Chart extends React.PureComponent<ChartProps, {}> {
} }
componentDidUpdate(prevProps: ChartProps) { 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. */ /** Make intl appear in this.context. */

View File

@@ -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() { componentDidMount() {
this.initializeSearchIndex(); this.initializeSearchIndex();
} }
@@ -259,6 +269,7 @@ export class TopBar extends React.Component<
<Icon name="print" /> <Icon name="print" />
<FormattedMessage id="menu.print" defaultMessage="Print" /> <FormattedMessage id="menu.print" defaultMessage="Print" />
</Menu.Item> </Menu.Item>
<Dropdown <Dropdown
trigger={ trigger={
<div> <div>
@@ -280,6 +291,34 @@ export class TopBar extends React.Component<
</Dropdown.Item> </Dropdown.Item>
</Dropdown.Menu> </Dropdown.Menu>
</Dropdown> </Dropdown>
<Dropdown
trigger={
<div>
<Icon name="eye" />
<FormattedMessage id="menu.view" defaultMessage="View" />
</div>
}
className="item"
>
<Dropdown.Menu>
<Dropdown.Item onClick={() => this.changeView('hourglass')}>
<Icon name="hourglass" />
<FormattedMessage
id="menu.hourglass"
defaultMessage="Hourglass chart"
/>
</Dropdown.Item>
<Dropdown.Item onClick={() => this.changeView('relatives')}>
<Icon name="users" />
<FormattedMessage
id="menu.relatives"
defaultMessage="All relatives"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<Search <Search
onSearchChange={debounce( onSearchChange={debounce(
(_: React.MouseEvent<HTMLElement>, data: SearchProps) => (_: React.MouseEvent<HTMLElement>, data: SearchProps) =>

View File

@@ -6,6 +6,9 @@
"menu.pdf_file": "Plik PDF", "menu.pdf_file": "Plik PDF",
"menu.png_file": "Plik PNG", "menu.png_file": "Plik PNG",
"menu.svg_file": "Plik SVG", "menu.svg_file": "Plik SVG",
"menu.view": "Widok",
"menu.hourglass": "Wykres klepsydrowy",
"menu.relatives": "Wszyscy krewni",
"menu.github": "Źródła na GitHub", "menu.github": "Źródła na GitHub",
"menu.powered_by": "Topola Genealogy", "menu.powered_by": "Topola Genealogy",
"menu.search.placeholder": "Szukaj osoby", "menu.search.placeholder": "Szukaj osoby",