aboutsummaryrefslogblamecommitdiff
path: root/src/Chart.tsx
blob: 0c8f9582d606a17ce9abb72cf22ed86d28090d66 (plain) (tree)
1
2
3
4
5
6
7
8
9
                          
                                                             
                                          
                                                 

                                                            

                               
                                                   
                                           











                                  
 
                                   




                         
                                


                                    







                             

  














































































































                                                                                                                
                                                                            
              
                                                 
                                                                           
                                                                              
                
                                 




                                                                        
                                                                                
                                                                             
                                                                                    



                                             







                                                            

                                



                                                                                                
                                                            


                                                                                         



                             

                                                                                                         
                                                                                                     
                                                




















                                                                                          
     

 
















































                                                                                             
 
                            




                                         



                                                     
            


                                                                  
             

                                                                   
             


             
                                                                  
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 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, 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: {
        margin: '0 auto',
    }
});

interface PatternPieChartProps {
    classes: {
        patternTableWrapper: string,
        pieChart: string
    };
    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> {
    render() {
        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 (
            <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.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: { index: number, datasetIndex: number },
                                data: { labels: string[], datasets: { data: number[] }[] }) => {
                            const v = data.datasets[item.datasetIndex].data[item.index];
                            return (
                            `${data.labels[item.index]}: ` +
                            `${v.toFixed(2)} hr (${(v / totalValue * 100).toFixed(2)} %)`
                            );
                        }
                    }
                },
                plugins: {
                    labels: {
                        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 = 150;
                            var luminance = 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b;
                            return luminance > threshold ? 'black' : 'white';
                        },
                        arc: false,
                        overlap: false
                    }
                },
                legend: {
                    position: 'bottom'
                },
                layout: {
                    padding: {
                        left: this.props.paddingLeft,
                        right: this.props.paddingRight,
                        top: this.props.paddingTop,
                        bottom: this.props.paddingBottom
                    }
                },
                maintainAspectRatio: false,
                responsive: true}} />
        );
    }
}

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: {
        patternTableWrapper: string,
        pieChart: string
    },
    patternGraphData: PatternGraphData[],
    calendarGraphData: PatternGraphData[]
};

function DoublePieChart(props: DoublePieChartProps) {
    return (
    <Grid container spacing={16}>
      <Grid item md={12} sm={6} xs={12}>
      <DoughnutChart height={300} data={props.patternGraphData} />
      </Grid>
      <Grid item md={12} sm={6} xs={12}>
      <DoughnutChart height={300} data={props.calendarGraphData} />
      </Grid>
    </Grid>);
}

export const AnalyzePieChart = withStyles(styles)(DoublePieChart);