aboutsummaryrefslogtreecommitdiff
path: root/src/Chart.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/Chart.tsx')
-rw-r--r--src/Chart.tsx254
1 files changed, 212 insertions, 42 deletions
diff --git a/src/Chart.tsx b/src/Chart.tsx
index 170b5fa..0c8f958 100644
--- a/src/Chart.tsx
+++ b/src/Chart.tsx
@@ -2,11 +2,24 @@ import React from 'react';
import { Theme, withStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
import cyan from '@material-ui/core/colors/cyan';
-import { Doughnut } from 'react-chartjs-2';
+import Typography from '@material-ui/core/Typography';
+import { Doughnut as DChart, Chart } from 'react-chartjs-2';
import 'chartjs-plugin-labels';
import Color from 'color';
-import { defaultChartColor } from './theme';
+import { defaultChartColor, theme } from './theme';
import { PatternGraphData } from './graph';
+import Doughnut from './Doughnut';
+
+declare module 'react-chartjs-2' {
+ export var Chart: {
+ controllers: {
+ doughnut: any,
+ pie: any
+ },
+ elements: { Arc: any },
+ pluginService: any
+ };
+}
const styles = (theme: Theme) => ({
pieChart: {
@@ -14,67 +27,176 @@ const styles = (theme: Theme) => ({
}
});
-type PatternPieChartProps = {
+interface PatternPieChartProps {
classes: {
patternTableWrapper: string,
pieChart: string
- },
- height?: number | string,
- data: PatternGraphData[],
- borderWidth: number,
- labelFontSize : number,
- paddingTop: number,
- paddingBottom: number,
- paddingLeft: number,
- paddingRight: number,
+ };
+ data: PatternGraphData[];
+ borderWidth: number;
+ labelFontSize : number;
+ paddingTop: number;
+ paddingBottom: number;
+ paddingLeft: number;
+ paddingRight: number;
};
+Chart.elements.Arc.prototype.draw = function() {
+ let ctx = this._chart.ctx;
+ const vm = this._view;
+ const sA = vm.startAngle;
+ const eA = vm.endAngle;
+ const pixelMargin = (vm.borderAlign === 'inner') ? 0.33 : 0;
+ let angleMargin;
+
+ ctx.save();
+
+ const delta = 3;
+ const deltaOuter = Math.asin(delta / vm.outerRadius);
+ const deltaInner = Math.asin(delta / vm.innerRadius);
+
+ let sA1 = sA;
+ let sA2 = sA;
+ let eA1 = eA;
+ let eA2 = eA;
+
+ if ((eA - sA) > 2 * deltaInner + 0.05) {
+ sA1 += deltaOuter;
+ eA1 -= deltaOuter;
+ sA2 += deltaInner;
+ eA2 -= deltaInner;
+ }
+
+ ctx.beginPath();
+ ctx.arc(vm.x, vm.y, Math.max(vm.outerRadius - pixelMargin, 0), sA1, eA1);
+ ctx.arc(vm.x, vm.y, vm.innerRadius, eA2, sA2, true);
+ ctx.closePath();
+
+ ctx.fillStyle = vm.backgroundColor;
+ ctx.fill();
+
+ if (vm.borderWidth) {
+ if (vm.borderAlign === 'inner') {
+ // Draw an inner border by cliping the arc and drawing a double-width border
+ // Enlarge the clipping arc by 0.33 pixels to eliminate glitches between borders
+ ctx.beginPath();
+ angleMargin = pixelMargin / vm.outerRadius;
+ ctx.arc(vm.x, vm.y, vm.outerRadius, sA - angleMargin, eA + angleMargin);
+ if (vm.innerRadius > pixelMargin) {
+ angleMargin = pixelMargin / vm.innerRadius;
+ ctx.arc(vm.x, vm.y, vm.innerRadius - pixelMargin, eA + angleMargin, sA - angleMargin, true);
+ } else {
+ ctx.arc(vm.x, vm.y, pixelMargin, eA + Math.PI / 2, sA - Math.PI / 2);
+ }
+ ctx.closePath();
+ ctx.clip();
+
+ ctx.beginPath();
+ ctx.arc(vm.x, vm.y, vm.outerRadius, sA, eA);
+ ctx.arc(vm.x, vm.y, vm.innerRadius, eA, sA, true);
+ ctx.closePath();
+
+ ctx.lineWidth = vm.borderWidth * 2;
+ ctx.lineJoin = 'round';
+ } else {
+ ctx.lineWidth = vm.borderWidth;
+ ctx.lineJoin = 'bevel';
+ }
+
+ ctx.strokeStyle = vm.borderColor;
+ ctx.stroke();
+ }
+
+ ctx.restore();
+};
+
+// Code adapted from https://stackoverflow.com/a/43026361/544806
+Chart.pluginService.register({
+ beforeDraw: function (chart: any) {
+ if (chart.config.options.elements.center) {
+ //Get ctx from string
+ let ctx = chart.chart.ctx;
+ //Get options from the center object in options
+ const centerConfig = chart.config.options.elements.center;
+ const fontStyle = centerConfig.fontStyle || 'Noto Sans';
+ const txt = centerConfig.text;
+ const color = centerConfig.color || '#000';
+ const sidePadding = centerConfig.sidePadding || 20;
+ const sidePaddingCalculated = (sidePadding/100) * (chart.innerRadius * 2)
+ //Start with a base font of 30px
+ ctx.font = "12px " + fontStyle;
+
+ //Get the width of the string and also the width of the element minus 10 to give it 5px side padding
+ const stringWidth = ctx.measureText(txt).width;
+ const elementWidth = (chart.innerRadius * 2) - sidePaddingCalculated;
+
+ // Find out how much the font can grow in width.
+ const widthRatio = elementWidth / stringWidth;
+ const newFontSize = Math.floor(30 * widthRatio);
+ const elementHeight = (chart.innerRadius * 2);
+
+ // Pick a new font size so it will not be larger than the height of label.
+ const fontSizeToUse = Math.min(newFontSize, elementHeight, centerConfig.maxFontSize);
+
+ // Set font settings to draw it correctly.
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ const centerX = ((chart.chartArea.left + chart.chartArea.right) / 2);
+ const centerY = ((chart.chartArea.top + chart.chartArea.bottom) / 2);
+ ctx.font = fontSizeToUse+"px " + fontStyle;
+ ctx.fillStyle = color;
+
+ // Draw text in center
+ ctx.fillText(txt, centerX, centerY);
+ }
+ }
+});
+
export class PatternPieChart extends React.Component<PatternPieChartProps> {
- public static defaultProps = {
- borderWidth: 1,
- labelFontSize: 12,
- paddingTop: 0,
- paddingBottom: 0,
- paddingLeft: 0,
- paddingRight: 0,
- };
render() {
- let { height, data, labelFontSize } = this.props;
- const theme = {
- labels: {
- text: {
- fontSize: labelFontSize
- }
- }
- };
+ let { data, labelFontSize } = this.props;
const colors = data.map(p => p.color ? p.color: defaultChartColor);
+ const totalValue = data.map(p => p.value).reduce((ans, v) => ans + v);
return (
- <Doughnut data={(canvas: any) => {
+ <DChart data={() => {
return {
datasets: [{
data: data.map(p => p.value),
backgroundColor: colors,
borderWidth: data.map(() => this.props.borderWidth),
+ borderColor: colors.map(c => Color(c).darken(0.1).string()),
hoverBorderWidth: data.map(() => this.props.borderWidth),
- hoverBorderColor: colors.map(c => Color(c).darken(0.2).string())
+ hoverBorderColor: colors.map(c => Color(c).darken(0.3).string())
}],
labels: data.map(p => p.name)
};
}} options={{
+ elements: {
+ center: {
+ text: `${totalValue.toFixed(2)} hr`,
+ color: theme.palette.text.secondary,
+ maxFontSize: 20,
+ sidePadding: 50
+ }
+ },
tooltips: {
callbacks: {
- label: (item: any, data: any) => (
+ label: (item: { index: number, datasetIndex: number },
+ data: { labels: string[], datasets: { data: number[] }[] }) => {
+ const v = data.datasets[item.datasetIndex].data[item.index];
+ return (
`${data.labels[item.index]}: ` +
- `${data.datasets[item.datasetIndex].data[item.index].toFixed(2)} hr`
- )
+ `${v.toFixed(2)} hr (${(v / totalValue * 100).toFixed(2)} %)`
+ );
+ }
}
},
plugins: {
labels: {
- render: (args: any) => `${args.value.toFixed(2)} hr`,
- fontColor: (data: any) => {
+ render: (args: { value: number }) => `${args.value.toFixed(2)} hr`,
+ fontColor: (data: { index: number, dataset: { backgroundColor: string[] } }) => {
var rgb = Color(data.dataset.backgroundColor[data.index]).rgb().object();
- var threshold = 140;
+ var threshold = 150;
var luminance = 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b;
return luminance > threshold ? 'black' : 'white';
},
@@ -99,7 +221,55 @@ export class PatternPieChart extends React.Component<PatternPieChartProps> {
}
}
-export const StyledPatternPieChart = withStyles(styles)(PatternPieChart);
+interface DoughnutChartProps extends PatternPieChartProps {
+ height: number
+}
+
+class _DoughnutChart extends React.Component<DoughnutChartProps> {
+ public static defaultProps = {
+ height: 300,
+ borderWidth: 1,
+ labelFontSize: 12,
+ paddingTop: 0,
+ paddingBottom: 0,
+ paddingLeft: 0,
+ paddingRight: 0,
+ };
+ render() {
+ const h = this.props.height;
+ const ih = h * (1 - 0.05 - 0.20);
+ return ((this.props.data.some(dd => dd.value > 1e-3) &&
+ <div style={{height: h}}>
+ <PatternPieChart {...this.props} />
+ </div>) ||
+ <div style={{
+ marginTop: 0.05 * h,
+ marginBottom: 0.20 * h,
+ textAlign: 'center'
+ }}>
+ <div style={{
+ position: 'relative',
+ height: ih,
+ display: 'inline-block'
+ }}>
+ <Doughnut style={{
+ height: '100%'
+ }} />
+ <div style={{
+ position: 'absolute',
+ bottom: -ih * 0.15,
+ left: ih * 0.5 - 73,
+ }}>
+ <Typography variant="subtitle1" align="center" color="textSecondary">
+ No matching events.
+ </Typography>
+ </div>
+ </div>
+ </div>);
+ }
+}
+
+export const DoughnutChart = withStyles(styles)(_DoughnutChart);
type DoublePieChartProps = {
classes: {
@@ -112,12 +282,12 @@ type DoublePieChartProps = {
function DoublePieChart(props: DoublePieChartProps) {
return (
- <Grid container spacing={0}>
- <Grid item md={12} lg={12} style={{height: 300}}>
- <StyledPatternPieChart data={props.patternGraphData} />
+ <Grid container spacing={16}>
+ <Grid item md={12} sm={6} xs={12}>
+ <DoughnutChart height={300} data={props.patternGraphData} />
</Grid>
- <Grid item md={12} lg={12} style={{height: 300}}>
- <StyledPatternPieChart data={props.calendarGraphData} />
+ <Grid item md={12} sm={6} xs={12}>
+ <DoughnutChart height={300} data={props.calendarGraphData} />
</Grid>
</Grid>);
}