{{ eventType }}
506 | 507 |{{ this.event.data.object.description }}
513 |520 | 521 | HideInspect JSON 522 |
523 |527 | 528 | HideShow metadata 529 |
530 |{{ json | prettify }}
577 | `
578 | });
579 |
580 | Vue.component("logs", {
581 | data() {
582 | return { store };
583 | },
584 | template: `
585 |
597 | `
598 | });
599 |
600 | Vue.component("charts", {
601 | data() {
602 | return {
603 | store,
604 | graph: null,
605 | data: [],
606 | month: new Date(2014, 0, 1),
607 | ticking: false,
608 | filtering: false,
609 | transitioning: false,
610 | plotted: false,
611 | animationSpeed: 150,
612 | easing: d3.easeCubicIn,
613 | listeners: {},
614 | // Selections to create and remove before and after transitions
615 | removeQueue: null,
616 | createQueue: null,
617 | // Current translation offset of elements that are drawn on the page
618 | translateOffset: 0,
619 | plot: {
620 | x: null,
621 | y: null,
622 | svg: null,
623 | area: null,
624 | xAxisGroup: null,
625 | yAxisGroup: null,
626 | xAxis: null,
627 | yAxis: null,
628 | stack: null,
629 | layer: null,
630 | path: null,
631 | data: null
632 | }
633 | };
634 | },
635 | methods: {
636 | xDomain(data) {
637 | let lastDate = new Date(data[data.length - 2].timestamp);
638 | let firstDate = new Date(data[0].timestamp);
639 | return [firstDate, lastDate];
640 | },
641 | yDomain(data) {
642 | // Get the maximum of the total number of event types
643 | let max = d3.max(data, d => {
644 | // Sum the values for each event type
645 | return d3.sum(this.keys(), key => d[key]) * 1.2;
646 | });
647 | return [0, max];
648 | },
649 | keys() {
650 | return store.eventTypes.filter(
651 | key => store.filteredHidden.indexOf(key) < 0
652 | );
653 | },
654 | // Visually update the areas of the stacked graph
655 | updateAreas(selection) {
656 | selection.attr("d", this.plot.area);
657 | if (this.createQueue) {
658 | this.createQueue.attr("d", this.plot.area);
659 | this.createQueue = null;
660 | }
661 | if (this.removeQueue) {
662 | this.removeQueue.remove();
663 | this.removeQueue = null;
664 | }
665 | },
666 | // Animate the areas of the stacked graph
667 | animateAreas(selection) {
668 | // Calculate how far we'll want to slide each area:
669 | // in this case, one time interval on the plot
670 | const previousStatement = new Date(
671 | this.plot.data[0].timestamp - store.stats.interval
672 | );
673 | const translateOffset = this.plot.x(previousStatement);
674 | // Set the actual offset to zero at the start of the transition
675 | this.translateOffset = 0;
676 | // Set the `transitioning` state if there are elements to transition
677 | if (!selection.empty()) {
678 | this.transitioning = true;
679 | }
680 |
681 | selection
682 | // Set the original translate offset to zero
683 | .attr("transform", "translate(0)")
684 | .transition()
685 | // Slide each area across the plot
686 | .duration(this.animationSpeed)
687 | .ease(this.easing)
688 | .attr("transform", `translate(${translateOffset})`)
689 | .on("end", () => {
690 | this.translateOffset = translateOffset;
691 | this.transitioning = false;
692 | this.filtering = false;
693 | });
694 | },
695 | updateGraph() {
696 | // If the event stream is paused, don't update the graph
697 | if (store.eventsPaused || document.hidden) {
698 | return;
699 | }
700 | // Define the animation behavior (speed and easing)
701 | const plot = this.plot;
702 | const data = store.stats.numEvents;
703 | plot.data = data;
704 |
705 | // Use a stack layout to create our stacked area graph
706 | plot.stack = d3
707 | .stack()
708 | .keys(this.keys())
709 | .order(d3.stackOrderNone)
710 | .offset(d3.stackOffsetNone);
711 |
712 | // Bind the data to the layer
713 | const layer = plot.svg
714 | .selectAll(".layer")
715 | .data(plot.stack(data), d => d.key);
716 |
717 | // Recalculate the domains of each axis
718 | plot.x.domain(this.xDomain(data));
719 | plot.y.domain(this.yDomain(data));
720 |
721 | // Update the axes
722 | plot.xAxisGroup
723 | .transition()
724 | .duration(this.animationSpeed)
725 | .ease(this.easing)
726 | .call(plot.xAxis);
727 | plot.yAxisGroup
728 | .transition()
729 | .duration(this.animationSpeed)
730 | .ease(this.easing)
731 | .call(plot.yAxis);
732 |
733 | // Enter new elements
734 | layer
735 | .enter()
736 | .append("g")
737 | .attr("class", d => `layer ${d.key}`)
738 | .attr("clip-path", "url(#clip)")
739 | .append("path")
740 | // Set up paths for new elements (but don't actually plot them yet)
741 | .attr("class", d => `area ${d.key}`)
742 | .style("fill", (d, i) => store.eventColors[d.key])
743 | .call(selection => {
744 | // If we're not in the middle of a transition, visually update the plot
745 | if (!this.transitioning) {
746 | selection
747 | .attr("d", plot.area)
748 | .attr("transform", `translate(${this.translateOffset})`);
749 | } else {
750 | // Otherwise, add it to a creation queue
751 | this.createQueue = selection;
752 | }
753 | });
754 |
755 | // Remove exited elements
756 | layer.exit().call(selection => {
757 | this.removeQueue = selection;
758 | });
759 |
760 | // Visually update the plot (when we're not filtering or transitioning)
761 | if (!this.transitioning && !this.filtering) {
762 | layer
763 | .select(".area")
764 | .call(this.updateAreas)
765 | .call(this.animateAreas);
766 | } else {
767 | // If we've just filtered our data:
768 | if (this.filtering) {
769 | // ...and we're not in the middle of a transition, visually update the plot
770 | if (!this.transitioning) {
771 | layer.select(".area").call(this.updateAreas);
772 | }
773 | // Otherwise, skip this update cycle (but make sure we update +
774 | // animate the plot on the next cycle)
775 | this.filtering = false;
776 | }
777 | }
778 | },
779 | drawGraph() {
780 | const plot = this.plot;
781 | const data = store.recalculateStats();
782 | plot.data = data;
783 |
784 | let $recentEvents = this.$refs.recentEvents;
785 | // Build the SVG plot
786 | const padding = { horizontal: 20, vertical: 20 },
787 | width = $recentEvents.offsetWidth - padding.horizontal,
788 | height = $recentEvents.offsetHeight - padding.vertical;
789 |
790 | plot.x = d3
791 | .scaleTime()
792 | // Find the largest and smallest dates for the domain
793 | .domain(this.xDomain(data))
794 | .range([0, width]);
795 | plot.y = d3
796 | .scaleLinear()
797 | // Set the domain to the highest event count
798 | .domain(this.yDomain(data))
799 | .range([height, 0]);
800 |
801 | plot.svg = d3
802 | .select("svg")
803 | .attr("width", width)
804 | .attr("height", height)
805 | .append("g")
806 | .attr("width", width)
807 | .attr("height", height)
808 | .attr(
809 | "transform",
810 | `translate(${padding.vertical},-${padding.horizontal})`
811 | );
812 |
813 | plot.svg
814 | .append("defs")
815 | .append("clipPath")
816 | .attr("id", "clip")
817 | .append("rect")
818 | .attr("width", width)
819 | .attr("height", height);
820 |
821 | // Build our area graph
822 | plot.area = d3
823 | .area()
824 | // The x axis is based on time
825 | .x((d, i) => plot.x(new Date(d.data.timestamp)))
826 | // y0 and y1 represent the bottom and top values for each section of the area graph
827 | .y0(d => plot.y(d[0]))
828 | .y1(d => plot.y(d[1]))
829 | .curve(d3.curveBasis);
830 |
831 | // Define the visual axes on the plot
832 | plot.xAxis = d3
833 | .axisBottom()
834 | .scale(plot.x)
835 | .tickFormat(d3.timeFormat("%H:%M:%S"));
836 | plot.yAxis = d3.axisLeft().scale(plot.y);
837 |
838 | // Add our axes to the plot
839 | plot.xAxisGroup = plot.svg
840 | .append("g")
841 | .attr("class", "x axis")
842 | .attr("transform", `translate(0, ${height})`)
843 | .attr("clip-path", "url(#clip)")
844 | .call(plot.xAxis);
845 | plot.yAxisGroup = plot.svg
846 | .append("g")
847 | .attr("class", "y axis")
848 | .call(plot.yAxis);
849 |
850 | // Update the graph to use our data set: we've now fully plotted the graph
851 | this.updateGraph(data);
852 | this.plotted = true;
853 | }
854 | },
855 | destroyed() {
856 | // Remove all of the event listeners on `window`
857 | window.removeEventListener("statsReady", this.drawGraph);
858 | window.removeEventListener("newStats", this.listeners.newStats);
859 | window.removeEventListener("filteredType", this.listeners.filteredType);
860 | window.removeEventListener("resize", this.listeners.resize);
861 | },
862 |
863 | mounted() {
864 | // Wait for stats to be ready before drawing the graph
865 | if (store.stats.ready) {
866 | this.drawGraph();
867 | } else {
868 | // Draw the graph once statistics are set up
869 | window.addEventListener("statsReady", this.drawGraph);
870 | }
871 |
872 | // Tick the graph whenever new stats are available
873 | this.listeners.newStats = () => {
874 | if (this.plotted) {
875 | this.updateGraph();
876 | }
877 | };
878 | window.addEventListener("newStats", this.listeners.newStats);
879 |
880 | // Visually update the graph whenever data is filtered (by event type)
881 | this.listeners.filteredType = () => {
882 | if (this.plotted) {
883 | this.filtering = true;
884 | this.updateGraph();
885 | }
886 | };
887 | window.addEventListener("filteredType", this.listeners.filteredType);
888 |
889 | // Redraw the graph after the window has been resized
890 | let onResize;
891 | this.listeners.resize = () => {
892 | clearTimeout(onResize);
893 | onResize = setTimeout(() => {
894 | const svg = d3.select("svg");
895 | svg
896 | .attr("width", null)
897 | .attr("height", null)
898 | .selectAll("*")
899 | .remove();
900 |
901 | this.drawGraph();
902 | }, 200);
903 | };
904 | window.addEventListener("resize", this.listeners.resize);
905 | },
906 | template: `
907 |
916 | `
917 | });
918 |
919 | Vue.component("monitor-options", {
920 | data() {
921 | return {
922 | store
923 | };
924 | },
925 | mounted() {
926 | const waypoint = new Waypoint({
927 | element: this.$refs.monitorOptions,
928 | handler: direction => {
929 | if (direction == "down") {
930 | store.optionsSticky = true;
931 | } else {
932 | store.optionsSticky = false;
933 | }
934 | }
935 | });
936 | },
937 | methods: {
938 | toggleType(type) {
939 | let index = store.filteredHidden.indexOf(type);
940 | if (index > -1) {
941 | store.filteredHidden.splice(index, 1);
942 | } else {
943 | store.filteredHidden.push(type);
944 | }
945 | window.dispatchEvent(new Event("filteredType"));
946 | },
947 | colorize(type) {
948 | return store.eventColors[type];
949 | }
950 | },
951 | template: `
952 |
971 | `
972 | });
973 |
974 | const router = new VueRouter({
975 | //base: window.location.href,
976 | routes: [
977 | { name: "logs", path: "/", component: Vue.component("logs") },
978 | { name: "charts", path: "/charts", component: Vue.component("charts") }
979 | ]
980 | });
981 |
982 | const app = new Vue({
983 | el: "#app",
984 | router: router,
985 | data() {
986 | return { store };
987 | },
988 | created() {
989 | // Gather info on our Stripe account and start our subscription
990 | getStripeInfo();
991 | subscribeEvents();
992 | },
993 | template: `
994 |