mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-08-02 09:01:48 +00:00
Refactored Chart component from class-based to functional
This commit is contained in:
19
src/app.tsx
19
src/app.tsx
@@ -3,7 +3,6 @@ import * as queryString from 'query-string';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {analyticsEvent} from './util/analytics';
|
import {analyticsEvent} from './util/analytics';
|
||||||
import {Changelog} from './changelog';
|
import {Changelog} from './changelog';
|
||||||
import {Chart, ChartComponent, ChartType} from './chart';
|
|
||||||
import {DataSourceEnum, SourceSelection} from './datasource/data_source';
|
import {DataSourceEnum, SourceSelection} from './datasource/data_source';
|
||||||
import {Details} from './details/details';
|
import {Details} from './details/details';
|
||||||
import {EmbeddedDataSource, EmbeddedSourceSpec} from './datasource/embedded';
|
import {EmbeddedDataSource, EmbeddedSourceSpec} from './datasource/embedded';
|
||||||
@@ -16,6 +15,14 @@ import {Media} from './util/media';
|
|||||||
import {Redirect, Route, RouteComponentProps, Switch} from 'react-router-dom';
|
import {Redirect, Route, RouteComponentProps, Switch} from 'react-router-dom';
|
||||||
import {TopBar} from './menu/top_bar';
|
import {TopBar} from './menu/top_bar';
|
||||||
import {TopolaData} from './util/gedcom_util';
|
import {TopolaData} from './util/gedcom_util';
|
||||||
|
import {
|
||||||
|
Chart,
|
||||||
|
ChartType,
|
||||||
|
downloadPdf,
|
||||||
|
downloadPng,
|
||||||
|
downloadSvg,
|
||||||
|
printChart,
|
||||||
|
} from './chart';
|
||||||
import {
|
import {
|
||||||
argsToConfig,
|
argsToConfig,
|
||||||
Config,
|
Config,
|
||||||
@@ -212,7 +219,6 @@ class AppComponent extends React.Component<
|
|||||||
showErrorPopup: false,
|
showErrorPopup: false,
|
||||||
config: DEFALUT_CONFIG,
|
config: DEFALUT_CONFIG,
|
||||||
};
|
};
|
||||||
chartRef: ChartComponent | null = null;
|
|
||||||
|
|
||||||
/** Sets the state with a new individual selection and chart type. */
|
/** Sets the state with a new individual selection and chart type. */
|
||||||
private updateDisplay(
|
private updateDisplay(
|
||||||
@@ -418,7 +424,7 @@ class AppComponent extends React.Component<
|
|||||||
|
|
||||||
private onPrint = () => {
|
private onPrint = () => {
|
||||||
analyticsEvent('print');
|
analyticsEvent('print');
|
||||||
this.chartRef && this.chartRef.print();
|
printChart();
|
||||||
};
|
};
|
||||||
|
|
||||||
private showErrorPopup(message: string, otherStateChanges?: Partial<State>) {
|
private showErrorPopup(message: string, otherStateChanges?: Partial<State>) {
|
||||||
@@ -438,7 +444,7 @@ class AppComponent extends React.Component<
|
|||||||
private onDownloadPdf = async () => {
|
private onDownloadPdf = async () => {
|
||||||
analyticsEvent('download_pdf');
|
analyticsEvent('download_pdf');
|
||||||
try {
|
try {
|
||||||
this.chartRef && (await this.chartRef.downloadPdf());
|
await downloadPdf();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.showErrorPopup(
|
this.showErrorPopup(
|
||||||
this.props.intl.formatMessage({
|
this.props.intl.formatMessage({
|
||||||
@@ -454,7 +460,7 @@ class AppComponent extends React.Component<
|
|||||||
private onDownloadPng = async () => {
|
private onDownloadPng = async () => {
|
||||||
analyticsEvent('download_png');
|
analyticsEvent('download_png');
|
||||||
try {
|
try {
|
||||||
this.chartRef && (await this.chartRef.downloadPng());
|
await downloadPng();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.showErrorPopup(
|
this.showErrorPopup(
|
||||||
this.props.intl.formatMessage({
|
this.props.intl.formatMessage({
|
||||||
@@ -469,7 +475,7 @@ class AppComponent extends React.Component<
|
|||||||
|
|
||||||
private onDownloadSvg = () => {
|
private onDownloadSvg = () => {
|
||||||
analyticsEvent('download_svg');
|
analyticsEvent('download_svg');
|
||||||
this.chartRef && this.chartRef.downloadSvg();
|
downloadSvg();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onDismissErrorPopup = () => {
|
private onDismissErrorPopup = () => {
|
||||||
@@ -530,7 +536,6 @@ class AppComponent extends React.Component<
|
|||||||
onSelection={this.onSelection}
|
onSelection={this.onSelection}
|
||||||
freezeAnimation={this.state.freezeAnimation}
|
freezeAnimation={this.state.freezeAnimation}
|
||||||
colors={this.state.config.color}
|
colors={this.state.config.color}
|
||||||
ref={(ref) => (this.chartRef = ref)}
|
|
||||||
/>
|
/>
|
||||||
{this.state.showSidePanel ? (
|
{this.state.showSidePanel ? (
|
||||||
<Media at="large" className="sidePanel">
|
<Media at="large" className="sidePanel">
|
||||||
|
|||||||
340
src/chart.tsx
340
src/chart.tsx
@@ -1,4 +1,3 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import {ChartColors} from './config';
|
import {ChartColors} from './config';
|
||||||
import {injectIntl, WrappedComponentProps} from 'react-intl';
|
import {injectIntl, WrappedComponentProps} from 'react-intl';
|
||||||
import {interpolateNumber} from 'd3-interpolate';
|
import {interpolateNumber} from 'd3-interpolate';
|
||||||
@@ -6,6 +5,7 @@ import {max, min} from 'd3-array';
|
|||||||
import {Media} from './util/media';
|
import {Media} from './util/media';
|
||||||
import {saveAs} from 'file-saver';
|
import {saveAs} from 'file-saver';
|
||||||
import {select, Selection} from 'd3-selection';
|
import {select, Selection} from 'd3-selection';
|
||||||
|
import {useEffect, useRef} from 'react';
|
||||||
import 'd3-transition';
|
import 'd3-transition';
|
||||||
import {
|
import {
|
||||||
D3ZoomEvent,
|
D3ZoomEvent,
|
||||||
@@ -107,7 +107,7 @@ function loadImage(blob: Blob): Promise<HTMLImageElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Draw image on a new canvas and return the canvas. */
|
/** Draw image on a new canvas and return the canvas. */
|
||||||
function drawOnCanvas(image: HTMLImageElement) {
|
function drawImageOnCanvas(image: HTMLImageElement) {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
// Scale image for better quality.
|
// Scale image for better quality.
|
||||||
canvas.width = image.width * 2;
|
canvas.width = image.width * 2;
|
||||||
@@ -135,6 +135,84 @@ function canvasToBlob(canvas: HTMLCanvasElement, type: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Return a copy of the SVG chart but without scaling and positioning. */
|
||||||
|
function getStrippedSvg() {
|
||||||
|
const svg = document.getElementById('chartSvg')!.cloneNode(true) as Element;
|
||||||
|
|
||||||
|
svg.removeAttribute('transform');
|
||||||
|
const parent = select('#svgContainer').node() as Element;
|
||||||
|
const scale = zoomTransform(parent).k;
|
||||||
|
svg.setAttribute('width', String(Number(svg.getAttribute('width')) / scale));
|
||||||
|
svg.setAttribute(
|
||||||
|
'height',
|
||||||
|
String(Number(svg.getAttribute('height')) / scale),
|
||||||
|
);
|
||||||
|
svg.querySelector('#chart')!.removeAttribute('transform');
|
||||||
|
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSvgContents() {
|
||||||
|
return new XMLSerializer().serializeToString(getStrippedSvg());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSvgContentsWithInlinedImages() {
|
||||||
|
const svg = getStrippedSvg();
|
||||||
|
await inlineImages(svg);
|
||||||
|
return new XMLSerializer().serializeToString(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shows the print dialog to print the currently displayed chart. */
|
||||||
|
export function printChart() {
|
||||||
|
const printWindow = document.createElement('iframe');
|
||||||
|
printWindow.style.position = 'absolute';
|
||||||
|
printWindow.style.top = '-1000px';
|
||||||
|
printWindow.style.left = '-1000px';
|
||||||
|
printWindow.onload = () => {
|
||||||
|
printWindow.contentDocument!.open();
|
||||||
|
printWindow.contentDocument!.write(getSvgContents());
|
||||||
|
printWindow.contentDocument!.close();
|
||||||
|
// Doesn't work on Firefox without the setTimeout.
|
||||||
|
setTimeout(() => {
|
||||||
|
printWindow.contentWindow!.focus();
|
||||||
|
printWindow.contentWindow!.print();
|
||||||
|
printWindow.parentNode!.removeChild(printWindow);
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
document.body.appendChild(printWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadSvg() {
|
||||||
|
const contents = await getSvgContentsWithInlinedImages();
|
||||||
|
const blob = new Blob([contents], {type: 'image/svg+xml'});
|
||||||
|
saveAs(blob, 'topola.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function drawOnCanvas(): Promise<HTMLCanvasElement> {
|
||||||
|
const contents = await getSvgContentsWithInlinedImages();
|
||||||
|
const blob = new Blob([contents], {type: 'image/svg+xml'});
|
||||||
|
return drawImageOnCanvas(await loadImage(blob));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadPng() {
|
||||||
|
const canvas = await drawOnCanvas();
|
||||||
|
const blob = await canvasToBlob(canvas, 'image/png');
|
||||||
|
saveAs(blob, 'topola.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadPdf() {
|
||||||
|
// Lazy load jspdf.
|
||||||
|
const {default: jspdf} = await import('jspdf');
|
||||||
|
const canvas = await drawOnCanvas();
|
||||||
|
const doc = new jspdf({
|
||||||
|
orientation: canvas.width > canvas.height ? 'l' : 'p',
|
||||||
|
unit: 'pt',
|
||||||
|
format: [canvas.width, canvas.height],
|
||||||
|
});
|
||||||
|
doc.addImage(canvas, 'PNG', 0, 0, canvas.width, canvas.height, 'NONE');
|
||||||
|
doc.save('topola.pdf');
|
||||||
|
}
|
||||||
|
|
||||||
/** Supported chart types. */
|
/** Supported chart types. */
|
||||||
export enum ChartType {
|
export enum ChartType {
|
||||||
Hourglass,
|
Hourglass,
|
||||||
@@ -148,6 +226,30 @@ const chartColors = new Map<ChartColors, TopolaChartColors>([
|
|||||||
[ChartColors.COLOR_BY_SEX, TopolaChartColors.COLOR_BY_SEX],
|
[ChartColors.COLOR_BY_SEX, TopolaChartColors.COLOR_BY_SEX],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
function getChartType(chartType: ChartType) {
|
||||||
|
switch (chartType) {
|
||||||
|
case ChartType.Hourglass:
|
||||||
|
return HourglassChart;
|
||||||
|
case ChartType.Relatives:
|
||||||
|
return RelativesChart;
|
||||||
|
case ChartType.Fancy:
|
||||||
|
return FancyChart;
|
||||||
|
default:
|
||||||
|
// Fall back to hourglass chart.
|
||||||
|
return HourglassChart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRendererType(chartType: ChartType) {
|
||||||
|
switch (chartType) {
|
||||||
|
case ChartType.Fancy:
|
||||||
|
return CircleRenderer;
|
||||||
|
default:
|
||||||
|
// Use DetailedRenderer by default.
|
||||||
|
return DetailedRenderer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChartProps {
|
export interface ChartProps {
|
||||||
data: JsonGedcomData;
|
data: JsonGedcomData;
|
||||||
selection: IndiInfo;
|
selection: IndiInfo;
|
||||||
@@ -157,11 +259,9 @@ export interface ChartProps {
|
|||||||
colors?: ChartColors;
|
colors?: ChartColors;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Component showing the genealogy chart and handling transition animations. */
|
type ChartComponentProps = ChartProps & WrappedComponentProps;
|
||||||
export class ChartComponent extends React.PureComponent<
|
|
||||||
ChartProps & WrappedComponentProps,
|
class ChartWrapper {
|
||||||
{}
|
|
||||||
> {
|
|
||||||
private chart?: ChartHandle;
|
private chart?: ChartHandle;
|
||||||
/** Animation is in progress. */
|
/** Animation is in progress. */
|
||||||
private animating = false;
|
private animating = false;
|
||||||
@@ -169,32 +269,10 @@ export class ChartComponent extends React.PureComponent<
|
|||||||
private rerenderRequired = false;
|
private rerenderRequired = false;
|
||||||
/** The d3 zoom behavior object. */
|
/** The d3 zoom behavior object. */
|
||||||
private zoomBehavior?: ZoomBehavior<Element, any>;
|
private zoomBehavior?: ZoomBehavior<Element, any>;
|
||||||
|
/** Props that will be used for rerendering. */
|
||||||
|
private rerenderProps?: ChartComponentProps;
|
||||||
|
|
||||||
private getChartType() {
|
zoom(factor: number) {
|
||||||
switch (this.props.chartType) {
|
|
||||||
case ChartType.Hourglass:
|
|
||||||
return HourglassChart;
|
|
||||||
case ChartType.Relatives:
|
|
||||||
return RelativesChart;
|
|
||||||
case ChartType.Fancy:
|
|
||||||
return FancyChart;
|
|
||||||
default:
|
|
||||||
// Fall back to hourglass chart.
|
|
||||||
return HourglassChart;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getRendererType() {
|
|
||||||
switch (this.props.chartType) {
|
|
||||||
case ChartType.Fancy:
|
|
||||||
return CircleRenderer;
|
|
||||||
default:
|
|
||||||
// Use DetailedRenderer by default.
|
|
||||||
return DetailedRenderer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private zoom(factor: number) {
|
|
||||||
const parent = select('#svgContainer') as Selection<Element, any, any, any>;
|
const parent = select('#svgContainer') as Selection<Element, any, any, any>;
|
||||||
this.zoomBehavior!.scaleBy(parent, factor);
|
this.zoomBehavior!.scaleBy(parent, factor);
|
||||||
}
|
}
|
||||||
@@ -204,7 +282,8 @@ export class ChartComponent extends React.PureComponent<
|
|||||||
* 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
|
||||||
* animation is performed.
|
* animation is performed.
|
||||||
*/
|
*/
|
||||||
private renderChart(
|
renderChart(
|
||||||
|
props: ChartComponentProps,
|
||||||
args: {initialRender: boolean; resetPosition: boolean} = {
|
args: {initialRender: boolean; resetPosition: boolean} = {
|
||||||
initialRender: false,
|
initialRender: false,
|
||||||
resetPosition: false,
|
resetPosition: false,
|
||||||
@@ -213,33 +292,34 @@ export class ChartComponent extends React.PureComponent<
|
|||||||
// Wait for animation to finish if animation is in progress.
|
// Wait for animation to finish if animation is in progress.
|
||||||
if (!args.initialRender && this.animating) {
|
if (!args.initialRender && this.animating) {
|
||||||
this.rerenderRequired = true;
|
this.rerenderRequired = true;
|
||||||
|
this.rerenderProps = props;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Freeze changing selection after initial rendering.
|
// Freeze changing selection after initial rendering.
|
||||||
if (!args.initialRender && this.props.freezeAnimation) {
|
if (!args.initialRender && props.freezeAnimation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.initialRender) {
|
if (args.initialRender) {
|
||||||
(select('#chart').node() as HTMLElement).innerHTML = '';
|
(select('#chart').node() as HTMLElement).innerHTML = '';
|
||||||
this.chart = createChart({
|
this.chart = createChart({
|
||||||
json: this.props.data,
|
json: props.data,
|
||||||
chartType: this.getChartType(),
|
chartType: getChartType(props.chartType),
|
||||||
renderer: this.getRendererType(),
|
renderer: getRendererType(props.chartType),
|
||||||
svgSelector: '#chart',
|
svgSelector: '#chart',
|
||||||
indiCallback: (info) => this.props.onSelection(info),
|
indiCallback: (info) => props.onSelection(info),
|
||||||
colors: chartColors.get(this.props.colors!),
|
colors: chartColors.get(props.colors!),
|
||||||
animate: true,
|
animate: true,
|
||||||
updateSvgSize: false,
|
updateSvgSize: false,
|
||||||
locale: this.props.intl.locale,
|
locale: props.intl.locale,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.chart!.setData(this.props.data);
|
this.chart!.setData(props.data);
|
||||||
}
|
}
|
||||||
const chartInfo = this.chart!.render({
|
const chartInfo = this.chart!.render({
|
||||||
startIndi: this.props.selection.id,
|
startIndi: props.selection.id,
|
||||||
baseGeneration: this.props.selection.generation,
|
baseGeneration: props.selection.generation,
|
||||||
});
|
});
|
||||||
const svg = select('#chartSvg');
|
const svg = select('#chartSvg');
|
||||||
const parent = select('#svgContainer').node() as Element;
|
const parent = select('#svgContainer').node() as Element;
|
||||||
@@ -309,123 +389,65 @@ export class ChartComponent extends React.PureComponent<
|
|||||||
this.animating = false;
|
this.animating = false;
|
||||||
if (this.rerenderRequired) {
|
if (this.rerenderRequired) {
|
||||||
this.rerenderRequired = false;
|
this.rerenderRequired = false;
|
||||||
this.renderChart({initialRender: false, resetPosition: false});
|
// Use `this.rerenderProps` instead of the props in scope because
|
||||||
|
// the props may have been updated in the meantime.
|
||||||
|
this.renderChart(this.rerenderProps!, {
|
||||||
|
initialRender: false,
|
||||||
|
resetPosition: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.renderChart({initialRender: true, resetPosition: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: ChartProps) {
|
|
||||||
const initialRender =
|
|
||||||
this.props.chartType !== prevProps.chartType ||
|
|
||||||
this.props.colors !== prevProps.colors;
|
|
||||||
const resetPosition = this.props.chartType !== prevProps.chartType;
|
|
||||||
this.renderChart({initialRender, resetPosition});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div id="svgContainer">
|
|
||||||
<Media at="large" className="zoom">
|
|
||||||
<button className="zoom-in" onClick={() => this.zoom(ZOOM_FACTOR)}>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="zoom-out"
|
|
||||||
onClick={() => this.zoom(1 / ZOOM_FACTOR)}
|
|
||||||
>
|
|
||||||
−
|
|
||||||
</button>
|
|
||||||
</Media>
|
|
||||||
<svg id="chartSvg">
|
|
||||||
<g id="chart" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return a copy of the SVG chart but without scaling and positioning. */
|
|
||||||
private getStrippedSvg() {
|
|
||||||
const svg = document.getElementById('chartSvg')!.cloneNode(true) as Element;
|
|
||||||
|
|
||||||
svg.removeAttribute('transform');
|
|
||||||
const parent = select('#svgContainer').node() as Element;
|
|
||||||
const scale = zoomTransform(parent).k;
|
|
||||||
svg.setAttribute(
|
|
||||||
'width',
|
|
||||||
String(Number(svg.getAttribute('width')) / scale),
|
|
||||||
);
|
|
||||||
svg.setAttribute(
|
|
||||||
'height',
|
|
||||||
String(Number(svg.getAttribute('height')) / scale),
|
|
||||||
);
|
|
||||||
svg.querySelector('#chart')!.removeAttribute('transform');
|
|
||||||
|
|
||||||
return svg;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSvgContents() {
|
|
||||||
return new XMLSerializer().serializeToString(this.getStrippedSvg());
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getSvgContentsWithInlinedImages() {
|
|
||||||
const svg = this.getStrippedSvg();
|
|
||||||
await inlineImages(svg);
|
|
||||||
return new XMLSerializer().serializeToString(svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Shows the print dialog to print the currently displayed chart. */
|
|
||||||
print() {
|
|
||||||
const printWindow = document.createElement('iframe');
|
|
||||||
printWindow.style.position = 'absolute';
|
|
||||||
printWindow.style.top = '-1000px';
|
|
||||||
printWindow.style.left = '-1000px';
|
|
||||||
printWindow.onload = () => {
|
|
||||||
printWindow.contentDocument!.open();
|
|
||||||
printWindow.contentDocument!.write(this.getSvgContents());
|
|
||||||
printWindow.contentDocument!.close();
|
|
||||||
// Doesn't work on Firefox without the setTimeout.
|
|
||||||
setTimeout(() => {
|
|
||||||
printWindow.contentWindow!.focus();
|
|
||||||
printWindow.contentWindow!.print();
|
|
||||||
printWindow.parentNode!.removeChild(printWindow);
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
document.body.appendChild(printWindow);
|
|
||||||
}
|
|
||||||
|
|
||||||
async downloadSvg() {
|
|
||||||
const contents = await this.getSvgContentsWithInlinedImages();
|
|
||||||
const blob = new Blob([contents], {type: 'image/svg+xml'});
|
|
||||||
saveAs(blob, 'topola.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
private async drawOnCanvas(): Promise<HTMLCanvasElement> {
|
|
||||||
const contents = await this.getSvgContentsWithInlinedImages();
|
|
||||||
const blob = new Blob([contents], {type: 'image/svg+xml'});
|
|
||||||
return await drawOnCanvas(await loadImage(blob));
|
|
||||||
}
|
|
||||||
|
|
||||||
async downloadPng() {
|
|
||||||
const canvas = await this.drawOnCanvas();
|
|
||||||
const blob = await canvasToBlob(canvas, 'image/png');
|
|
||||||
saveAs(blob, 'topola.png');
|
|
||||||
}
|
|
||||||
|
|
||||||
async downloadPdf() {
|
|
||||||
// Lazy load jspdf.
|
|
||||||
const {default: jspdf} = await import('jspdf');
|
|
||||||
const canvas = await this.drawOnCanvas();
|
|
||||||
const doc = new jspdf({
|
|
||||||
orientation: canvas.width > canvas.height ? 'l' : 'p',
|
|
||||||
unit: 'pt',
|
|
||||||
format: [canvas.width, canvas.height],
|
|
||||||
});
|
|
||||||
doc.addImage(canvas, 'PNG', 0, 0, canvas.width, canvas.height, 'NONE');
|
|
||||||
doc.save('topola.pdf');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
export const Chart = injectIntl(ChartComponent, {forwardRef: true});
|
|
||||||
|
function usePrevious<T>(value: T): T | undefined {
|
||||||
|
const ref = useRef<T>();
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value;
|
||||||
|
});
|
||||||
|
return ref.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChartComponent(props: ChartComponentProps) {
|
||||||
|
const chartWrapper = useRef(new ChartWrapper());
|
||||||
|
const prevProps = usePrevious(props);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (prevProps) {
|
||||||
|
const initialRender =
|
||||||
|
props.chartType !== prevProps?.chartType ||
|
||||||
|
props.colors !== prevProps?.colors;
|
||||||
|
const resetPosition = props.chartType !== prevProps?.chartType;
|
||||||
|
chartWrapper.current.renderChart(props, {initialRender, resetPosition});
|
||||||
|
} else {
|
||||||
|
chartWrapper.current.renderChart(props, {
|
||||||
|
initialRender: true,
|
||||||
|
resetPosition: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="svgContainer">
|
||||||
|
<Media at="large" className="zoom">
|
||||||
|
<button
|
||||||
|
className="zoom-in"
|
||||||
|
onClick={() => chartWrapper.current.zoom(ZOOM_FACTOR)}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="zoom-out"
|
||||||
|
onClick={() => chartWrapper.current.zoom(1 / ZOOM_FACTOR)}
|
||||||
|
>
|
||||||
|
−
|
||||||
|
</button>
|
||||||
|
</Media>
|
||||||
|
<svg id="chartSvg">
|
||||||
|
<g id="chart" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Chart = injectIntl(ChartComponent);
|
||||||
|
|||||||
Reference in New Issue
Block a user