diff --git a/app/services/work_packages/set_attributes_service.rb b/app/services/work_packages/set_attributes_service.rb
index b0c40bef6df6..51b596476205 100644
--- a/app/services/work_packages/set_attributes_service.rb
+++ b/app/services/work_packages/set_attributes_service.rb
@@ -69,7 +69,7 @@ def set_custom_values_to_validate(attributes, validate_custom_fields = nil)
def set_static_attributes(attributes)
assignable_attributes = attributes.select do |key, _|
- !CustomField.custom_field_attribute?(key) && work_package.respond_to?(key)
+ !CustomField.custom_field_attribute?(key) && work_package.respond_to?("#{key}=")
end
work_package.attributes = assignable_attributes
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 027f316bc786..4c7afcb9a696 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -4130,6 +4130,7 @@ en:
notice_successful_delete: "Successful deletion."
notice_successful_cancel: "Successful cancellation."
notice_successful_update: "Successful update."
+ notice_successful_move: "Successful move from %{from} to %{to}."
notice_unsuccessful_create: "Creation failed."
notice_unsuccessful_create_with_reason: "Creation failed: %{reason}"
notice_unsuccessful_update: "Update failed."
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index ff9f10abdb08..2d7c355b75d5 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -92,7 +92,6 @@
"jquery": "^3.7.1",
"jquery.caret": "^0.3.1",
"jquery.cookie": "^1.4.1",
- "jquery.flot": "^0.8.3",
"json5": "^2.2.2",
"lit-html": "^3.3.2",
"lodash": "^4.17.23",
@@ -16955,11 +16954,6 @@
"integrity": "sha512-c/hZOOL+8VSw/FkTVH637gS1/6YzMSCROpTZ2qBYwJ7s7sHajU7uBkSSiE5+GXWwrfCCyO+jsYjUQ7Hs2rIxAA==",
"license": "MIT"
},
- "node_modules/jquery.flot": {
- "version": "0.8.3",
- "resolved": "https://registry.npmjs.org/jquery.flot/-/jquery.flot-0.8.3.tgz",
- "integrity": "sha512-/tEE8J5NjwvStHDaCHkvTJpD7wDS4hE1OEL8xEmhgQfUe0gLUem923PIceNez1mz4yBNx6Hjv7pJcowLNd+nbg=="
- },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -36583,11 +36577,6 @@
"resolved": "https://registry.npmjs.org/jquery.cookie/-/jquery.cookie-1.4.1.tgz",
"integrity": "sha512-c/hZOOL+8VSw/FkTVH637gS1/6YzMSCROpTZ2qBYwJ7s7sHajU7uBkSSiE5+GXWwrfCCyO+jsYjUQ7Hs2rIxAA=="
},
- "jquery.flot": {
- "version": "0.8.3",
- "resolved": "https://registry.npmjs.org/jquery.flot/-/jquery.flot-0.8.3.tgz",
- "integrity": "sha512-/tEE8J5NjwvStHDaCHkvTJpD7wDS4hE1OEL8xEmhgQfUe0gLUem923PIceNez1mz4yBNx6Hjv7pJcowLNd+nbg=="
- },
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 791f4b6386db..50478a0686b0 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -147,7 +147,6 @@
"jquery": "^3.7.1",
"jquery.caret": "^0.3.1",
"jquery.cookie": "^1.4.1",
- "jquery.flot": "^0.8.3",
"json5": "^2.2.2",
"lit-html": "^3.3.2",
"lodash": "^4.17.23",
diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts
index 028cdc1ed2c2..802e6b3ec6e4 100644
--- a/frontend/src/app/app.module.ts
+++ b/frontend/src/app/app.module.ts
@@ -206,6 +206,7 @@ import { WorkPackageFullCreateEntryComponent } from 'core-app/features/work-pack
import { WorkPackageFullViewEntryComponent } from 'core-app/features/work-packages/routing/wp-full-view/wp-full-view-entry.component';
import { MyPageComponent } from './features/my-page/my-page.component';
import { DashboardComponent } from './features/overview/dashboard.component';
+import { BurndownChartComponent } from './features/backlogs/burndown-chart.component';
export function initializeServices(injector:Injector) {
return () => {
@@ -419,5 +420,6 @@ export class OpenProjectModule implements DoBootstrap {
registerCustomElement('opce-my-page', MyPageComponent, { injector });
registerCustomElement('opce-dashboard', DashboardComponent, { injector });
+ registerCustomElement('opce-burndown-chart', BurndownChartComponent, { injector });
}
}
diff --git a/frontend/src/app/features/backlogs/burndown-chart.component.html b/frontend/src/app/features/backlogs/burndown-chart.component.html
new file mode 100644
index 000000000000..fb72c70a9ac1
--- /dev/null
+++ b/frontend/src/app/features/backlogs/burndown-chart.component.html
@@ -0,0 +1,18 @@
+
+
+
+
+@if (isDevMode) {
+
+
+
+ Debug
+
+ {{maxValue() }}
+ {{lineChartData() | json}}
+
+
+}
diff --git a/frontend/src/app/features/backlogs/burndown-chart.component.ts b/frontend/src/app/features/backlogs/burndown-chart.component.ts
new file mode 100644
index 000000000000..7989f667c4cf
--- /dev/null
+++ b/frontend/src/app/features/backlogs/burndown-chart.component.ts
@@ -0,0 +1,86 @@
+//-- copyright
+// OpenProject is an open source project management software.
+// Copyright (C) the OpenProject GmbH
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+// Copyright (C) 2006-2013 Jean-Philippe Lang
+// Copyright (C) 2010-2013 the ChiliProject Team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See COPYRIGHT and LICENSE files for more details.
+//++
+
+import { JsonPipe } from '@angular/common';
+import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
+import { ChartData, ChartOptions } from 'chart.js';
+import { I18nService } from 'core-app/core/i18n/i18n.service';
+import PrimerColorsPlugin from 'core-app/shared/components/work-package-graphs/plugin.primer-colors';
+import { BaseChartDirective, provideCharts, withDefaultRegisterables } from 'ng2-charts';
+import { environment } from '../../../environments/environment';
+
+const BURNDOWN_Y_SCALE_MIN = 25;
+
+@Component({
+ selector: 'op-burndown-chart',
+ templateUrl: './burndown-chart.component.html',
+ imports: [BaseChartDirective, JsonPipe],
+ providers: [provideCharts(withDefaultRegisterables(PrimerColorsPlugin))],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class BurndownChartComponent {
+ readonly isDevMode = !environment.production;
+ readonly i18n = inject(I18nService);
+ readonly chartData = input.required();
+
+ readonly lineChartData = computed>(() => {
+ const data = JSON.parse(this.chartData()) as ChartData<'line'>;
+ return data;
+ });
+
+ readonly maxValue = computed(() => {
+ return this.lineChartData().datasets
+ .flatMap((dataset) => dataset.data)
+ .filter((item):item is number => typeof item === 'number')
+ .reduce((a, b) => Math.max(a, b), 0);
+ });
+
+ readonly lineChartOptions = computed>(() => ({
+ scales: {
+ x: {
+ title: {
+ display: true,
+ text: this.i18n.t('js.burndown.day')
+ }
+ },
+ y: {
+ title: {
+ display: true,
+ text: this.i18n.t('js.burndown.points')
+ },
+ suggestedMin: 0,
+ max: this.maxValue() + BURNDOWN_Y_SCALE_MIN
+ }
+ },
+ plugins: {
+ legend: {
+ position: 'top'
+ }
+ }
+ }));
+}
diff --git a/frontend/src/app/features/plugins/plugin-context.ts b/frontend/src/app/features/plugins/plugin-context.ts
index 2dbd58f1d8f0..c51d0a50d798 100644
--- a/frontend/src/app/features/plugins/plugin-context.ts
+++ b/frontend/src/app/features/plugins/plugin-context.ts
@@ -31,6 +31,7 @@ import { HttpClient } from '@angular/common/http';
import { TimezoneService } from 'core-app/core/datetime/timezone.service';
import { TurboRequestsService } from 'core-app/core/turbo/turbo-requests.service';
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
+import { HalEventsService } from '../hal/services/hal-events.service';
/**
* Plugin context bridge for plugins outside the CLI compiler context
* in order to access services and parts of the core application
@@ -48,6 +49,7 @@ export class OpenProjectPluginContext {
confirmDialog: this.injector.get(ConfirmDialogService),
externalQueryConfiguration: this.injector.get(ExternalQueryConfigurationService),
externalRelationQueryConfiguration: this.injector.get(ExternalRelationQueryConfigurationService),
+ halEvents: this.injector.get(HalEventsService),
halResource: this.injector.get(HalResourceService),
hooks: this.injector.get(HookService),
i18n: this.injector.get(I18nService),
diff --git a/frontend/src/assets/sass/backlogs/_index.sass b/frontend/src/assets/sass/backlogs/_index.sass
index bb1eb5d44a78..0f2b73435f43 100644
--- a/frontend/src/assets/sass/backlogs/_index.sass
+++ b/frontend/src/assets/sass/backlogs/_index.sass
@@ -33,6 +33,11 @@
* See COPYRIGHT and LICENSE files for more details.
*/
+// Variables
+@import "../../../global_styles/openproject/_variable_defaults.scss"
+
+@import "../../../global_styles/openproject/_variables.sass"
+
@import global
@import global_print
@import jqplot
diff --git a/frontend/src/assets/sass/backlogs/_master_backlog.sass b/frontend/src/assets/sass/backlogs/_master_backlog.sass
index a6f0a9e3cd13..159b6dd9ae24 100644
--- a/frontend/src/assets/sass/backlogs/_master_backlog.sass
+++ b/frontend/src/assets/sass/backlogs/_master_backlog.sass
@@ -26,299 +26,116 @@
* See COPYRIGHT and LICENSE files for more details. ++
*/
-#rb
- #backlogs_container
- width: 100%
+$op-backlogs-header--points-min-width: 5rem
+$op-backlogs-header--points-min-width-narrow: 2rem
+
+.op-backlogs-header
+ display: grid
+ grid-template-columns: 1fr minmax($op-backlogs-header--points-min-width, max-content) auto
+ grid-template-areas: "collapsible points menu"
+ align-items: center
+
+.op-backlogs-header--collapsible
+ margin-left: calc(var(--stack-padding-normal) / 2)
+
+.op-backlogs-header--points
+ margin-left: var(--stack-gap-normal)
+ font-variant-numeric: tabular-nums
+
+.op-backlogs-header--menu
+ margin-left: var(--stack-gap-normal)
+
+.op-backlogs-collapsible
+ display: flex
+ flex-wrap: wrap
+ align-items: center
+ column-gap: var(--stack-gap-normal)
+ row-gap: var(--base-size-4)
+ flex: 1
+
+ &--title-line
display: flex
- flex-wrap: wrap
- justify-content: space-between
- #owner_backlogs_container
- min-width: 420px
- order: 2
- width: 49%
- flex: 0 0 49%
- #sprint_backlogs_container
- min-width: 420px
- width: 49%
- flex: 0 0 49%
- min-height: 230px
- #owner_backlogs_container .backlog .header > .add_new_story
- height: 28px
- line-height: 31px
- padding: 0
- position: absolute
- right: 10px
- text-align: right
- top: 1px
- width: 100px
- #backlogs_container .backlog
- border: 1px solid var(--borderColor-default)
- display: block
- margin: 0 0 10px 0
- width: 100%
+ align-items: center
+ gap: var(--stack-gap-condensed)
+ flex: 1
+ min-width: fit-content
-#rb
- #backlogs_container .backlog .header
- background-color: var(--bgColor-muted)
- height: 30px
- position: relative
- width: 100%
- .backlog .header .backlog-menu
- border-right: 1px solid var(--borderColor-default)
- cursor: pointer
- height: 30px
- overflow: visible
- position: absolute
- top: 0
- right: 0
- width: 30px
- .icon-context
- position: absolute
- top: 7px
- left: 12px
- // Firefox wrongly positions icon
- &:before
- padding: 0
- &.open
- &+ .items
- display: block
- .items
- display: none
- background-color: var(--overlay-bgColor)
- border: 1px solid var(--borderColor-default)
- position: absolute
- top: 30px
- right: -2px
- list-style: none
- margin: 0
- padding: 0
- z-index: 1000
- .item
- display: block
- width: 160px
- height: 2rem
- font-size: 0.9rem
- text-align: left
- text-decoration: none
- vertical-align: middle
- overflow: hidden
- white-space: nowrap
- &.hover, &:hover
- background-color: #999
- a
- display: block
- height: 100%
- padding: 6px
- width: 100%
- &.hover a, &:hover a
- color: #FFFFFF
- text-decoration: none
- #backlogs_container
- .backlog
- .header
- .velocity
- height: 28px
- line-height: 31px
- padding: 0 3px 0 9px
- position: absolute
- right: 25px
- text-align: right
- top: 0px
- width: 32px
- .toggler
- font-family: "openproject-icon-font"
- height: 30px
- line-height: 31px
- padding: 0
- position: absolute
- left: 0
- top: 0
- width: 23px
- cursor: pointer
- &:before
- position: absolute
- left: 6px
- top: 10px
- &.closed:before
- position: absolute
- left: 6px
- top: 10px
- &:hover
- cursor: pointer
- background-color: #D8D8D8
- .sprint
- background-color: transparent
- cursor: pointer
- display: block
- height: 29px
- width: auto
- margin-left: 30px
- margin-right: 50px
- &.error.icon-bug
- background: none
- text-align: center
- &:before
- position: absolute
- color: red
- .id, .status
- display: none
+ &--description
+ display: inline
+ white-space: nowrap
- .name
- line-height: 2rem
- font-weight: var(--base-text-weight-bold)
- overflow: hidden
- white-space: nowrap
- margin-left: 0.5em
+.op-backlogs-story
+ display: grid
+ grid-template-columns: var(--control-xsmall-size) 1fr minmax($op-backlogs-header--points-min-width, max-content) auto
+ grid-template-rows: auto auto
+ grid-template-areas: "drag_handle info_line points menu" ". subject subject subject"
+ align-items: center
+ margin-top: calc(-1 * var(--base-size-4))
+ margin-bottom: var(--base-size-4)
- .start_date, .effective_date
- float: right
- height: 28px
- line-height: 2rem
- width: 6.5em
- margin-left: 0.5em
- .stories
- list-style: none
- min-height: 2rem
- margin: 0
- padding: 0 0 0px 0
- z-index: 500
- overflow-y: auto
- overflow-x: hidden
- &.closed
- display: none
+.op-backlogs-story--drag_handle_button
+ padding: var(--base-size-4)
- .error.icon.icon-bug
- text-align: left
- .stories:not(.prevent_drag) .story
- cursor: move
- .stories .story
- display: block
- font-size: 0.9rem
- margin: 0
- overflow: hidden
- position: relative
- width: 100%
- &.odd
- background-color: var(--bgColor-neutral-muted)
- &.even
- background-color: var(--body-background)
- &.error.icon-bug
- background: none
- text-align: center
- &:before
- position: absolute
- color: red
- pointer-events: none
- &.hover, &:hover
- background-color: var(--highlight-neutral-bgColor)
- &.closed
- text-decoration: line-through
- .id
- float: left
- margin-left: 1em
- margin-right: 1em
- padding: 5px 2px 4px 2px
- width: 4em
- text-align: right
- white-space: nowrap
- .type_id .t
- float: left
- padding: 5px 2px 4px 2px
- text-align: right
- white-space: nowrap
- .subject
- overflow: hidden
- margin-left: 4em
- padding: 5px 2px 4px 2px
- white-space: nowrap
- min-height: 1em
- .status_id
- float: right
- padding: 5px 2px 4px 2px
- margin-left: 1em
- width: 68px
- .story_points
- float: right
- padding: 5px 1rem 4px 2px
- width: 3.5rem
- min-height: 14px
- height: 2rem
- text-align: center
- .type_id .v, .id .v, .status_id .v, .version_id, .higher_item_id
- display: none
+.op-backlogs-story--points
+ margin-left: var(--stack-gap-normal)
+ font-variant-numeric: tabular-nums
-.rb_dialog
- .burndown_chart
- margin-top: 20px
- margin-bottom: 20px
- margin-left: 20px
- #charts
- h3
- border: 0px
- overflow: hidden
- fieldset.burndown_control
- padding-left: 10px
- border: none
- .axislabel
- font-weight: var(--base-text-weight-bold)
+.op-backlogs-story--menu
+ margin-left: var(--stack-gap-normal)
-/* In-place Sprint Editor */
+.op-backlogs-story--subject
+ align-self: start // Align to top of second row
+ word-wrap: break-word
+ overflow-wrap: break-word
-#rb #backlogs_container
- .sprint.editing
- .editors, > .editor
- display: block
- label, > *
- display: none
- +
- .velocity, .add_new_story
- display: none
- .backlog .sprint.editing
- .editors
- display: flex
- align-items: center
- flex-direction: row-reverse
+.op-backlogs-page
+ display: block
+ container-name: backlogsListsContainer
+ container-type: inline-size
+
+.op-backlogs-container
+ display: flex
+ flex-direction: row
+ gap: var(--stack-gap-normal)
+
+.op-backlogs-lists
+ display: flex
+ flex-direction: column
+ gap: var(--stack-gap-normal)
+ flex: 1 1 100%
+ overflow: hidden
+
+// Note: Using hardcoded values because Sass doesn't interpolate variables in
+// @container query conditions.
+// Note: 655px is between $breakpoint-sm and $breakpoint-md. This was found to
+// be a sensible value after initial testing with different viewports.
+@container backlogsListsContainer (min-width: 655px)
+ .op-backlogs-header-form
+ .FormControl-spacingWrapper
+ flex-direction: row
+ column-gap: 0.5rem
- .editor
- font-size: 0.9rem
- line-height: 1.5rem
- height: 30px
- margin: 0
- padding: 0
+ & > :first-child
+ flex: 1 1 auto
+ min-width: 33%
- &.name
- flex-basis: 15em
- &.start_date,
- &.effective_date
- margin-left: 0.5em
- flex-basis: 12.5em
+@container backlogsListsContainer (max-width: 654px)
+ .op-backlogs-header
+ grid-template-columns: 1fr minmax($op-backlogs-header--points-min-width-narrow, max-content) auto
- .stories .story.editing
- >
- *, .editors label
+ .op-backlogs-collapsible
+ flex-direction: column
+ align-items: flex-start
+
+ &--description
+ [data-collapsed] &
display: none
- .editors
- display: block
- select, input
- display: inline-block
- float: none
- margin: 5px 3px 4px 2px
- font-size: 0.8rem
- // reset the line-height (foundation sets it to "normal" but that does not work here)
- line-height: inherit
- .type_id.editor
- width: 15%
- /* sets max-width for IE */
- max-width: 140px
- /* for the cool guys */
- .subject.editor
- width: 55%
- .status_id.editor
- width: 15%
- float: right
- .story_points.editor
- float: right
- width: 10%
-.backlog
- font-size: 0.9rem
+ .op-backlogs-points-label
+ display: none
+
+ .op-backlogs-story
+ grid-template-columns: var(--control-xsmall-size) 1fr minmax($op-backlogs-header--points-min-width-narrow, max-content) auto
+
+ .op-backlogs-container
+ flex-direction: column
diff --git a/frontend/src/global_styles/primer/_overrides.sass b/frontend/src/global_styles/primer/_overrides.sass
index 5dec0007aaa4..0ca650eb6261 100644
--- a/frontend/src/global_styles/primer/_overrides.sass
+++ b/frontend/src/global_styles/primer/_overrides.sass
@@ -152,3 +152,17 @@ ul.SegmentedControl,
.ActionListContent[aria-disabled="true"]
.ActionListItem-label[class^="__hl_"], .ActionListItem-label[class*=" __hl_"]
color: var(--control-fgColor-disabled) !important
+
+.Box-row--focus-gray
+ &:focus-visible
+ background-color: var(--bgColor-muted)
+
+.Box-row--focus-blue
+ &:focus-visible
+ background-color: var(--bgColor-accent-muted)
+
+.Box-row--clickable
+ cursor: pointer
+
+.Box-row:is(.Box-row--draggable)
+ padding-left: 0
diff --git a/frontend/src/stimulus/controllers/dynamic/backlogs.controller.ts b/frontend/src/stimulus/controllers/dynamic/backlogs.controller.ts
index a551ee1768e3..239409703ef3 100644
--- a/frontend/src/stimulus/controllers/dynamic/backlogs.controller.ts
+++ b/frontend/src/stimulus/controllers/dynamic/backlogs.controller.ts
@@ -1,24 +1,105 @@
+//-- copyright
+// OpenProject is an open source project management software.
+// Copyright (C) the OpenProject GmbH
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+// Copyright (C) 2006-2013 Jean-Philippe Lang
+// Copyright (C) 2010-2013 the ChiliProject Team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See COPYRIGHT and LICENSE files for more details.
+//++
+
import { Controller } from '@hotwired/stimulus';
+import { FrameElement, TurboVisitEvent } from '@hotwired/turbo';
+import { HalEventsService } from 'core-app/features/hal/services/hal-events.service';
+import { filter, Subscription } from 'rxjs';
+import StoryController from './backlogs/story.controller';
+
+export default class BacklogsController extends Controller {
+ static outlets = ['backlogs--story'];
+ declare backlogsStoryOutlets:StoryController[];
+
+ static values = {
+ listUrl: String,
+ };
+
+ declare listUrlValue:string;
+
+ private abortController:AbortController|null = null;
+ private service:HalEventsService|null = null;
+ private subscription:Subscription|null = null;
+
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ async connect() {
+ this.abortController = new AbortController();
+ const { signal } = this.abortController;
+
+ document.addEventListener('turbo:visit', this.updateSelection, { signal });
+
+ const { services: { halEvents } } = await window.OpenProject.getPluginContext();
+
+ this.service = halEvents;
+ this.subscription = this.service.aggregated$('WorkPackage')
+ .pipe(filter((events) => events.some((event) => event.eventType === 'updated')))
+ .subscribe(() => { this.refreshList(); });
+ }
+
+ disconnect() {
+ this.subscription?.unsubscribe();
+ this.subscription = null;
+ this.service = null;
+
+ this.abortController?.abort();
+ this.abortController = null;
+ }
+
+ backlogsStoryOutletConnected(outlet:StoryController) {
+ const selectedId = this.getSelectedIdFromPathname(window.location.pathname);
+ if (selectedId !== null && outlet.idValue === selectedId) {
+ outlet.markAsSelected();
+ }
+ }
+
+ private updateSelection = (event:TurboVisitEvent) => {
+ const url = new URL(event.detail.url, window.location.origin);
+ const selectedId = this.getSelectedIdFromPathname(url.pathname);
+
+ this.backlogsStoryOutlets.forEach((story) => {
+ if (selectedId !== null && story.idValue === selectedId) {
+ story.markAsSelected(event);
+ } else {
+ story.unmarkAsSelected(event);
+ }
+ });
+ };
+
+ private getSelectedIdFromPathname(pathname:string):number|null {
+ const match = /\/details\/(\d+)/.exec(pathname);
+ return match ? Number(match[1]) : null;
+ }
+
+ private refreshList() {
+ this.listElement.src = this.listUrlValue;
+ }
-import 'jquery.flot';
-import 'jquery.flot/excanvas';
-
-import 'core-vendor/jquery.jeditable.mini';
-import 'core-vendor/jquery.colorcontrast';
-
-import './backlogs/common';
-import './backlogs/master_backlog';
-import './backlogs/backlog';
-import './backlogs/burndown';
-import './backlogs/model';
-import './backlogs/editable_inplace';
-import './backlogs/sprint';
-import './backlogs/work_package';
-import './backlogs/story';
-import './backlogs/task';
-import './backlogs/impediment';
-import './backlogs/taskboard';
-import './backlogs/show_main';
-
-export default class BacklogsController extends Controller {
+ private get listElement() {
+ return this.element.querySelector('#backlogs_container')!;
+ }
}
diff --git a/frontend/src/stimulus/controllers/dynamic/backlogs/backlog.ts b/frontend/src/stimulus/controllers/dynamic/backlogs/backlog.ts
deleted file mode 100644
index 91cb0937b8c4..000000000000
--- a/frontend/src/stimulus/controllers/dynamic/backlogs/backlog.ts
+++ /dev/null
@@ -1,182 +0,0 @@
-//-- copyright
-// OpenProject is an open source project management software.
-// Copyright (C) the OpenProject GmbH
-//
-// This program is free software; you can redistribute it and/or
-// modify it under the terms of the GNU General Public License version 3.
-//
-// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
-// Copyright (C) 2006-2013 Jean-Philippe Lang
-// Copyright (C) 2010-2013 the ChiliProject Team
-//
-// This program is free software; you can redistribute it and/or
-// modify it under the terms of the GNU General Public License
-// as published by the Free Software Foundation; either version 2
-// of the License, or (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-//
-// See COPYRIGHT and LICENSE files for more details.
-//++
-
-/******************************************
- BACKLOG
- A backlog is a visual representation of
- a sprint and its stories. It is not a
- sprint. Imagine it this way: A sprint is
- a start and end date, and a set of
- objectives. A backlog is something you
- would draw up on the board or a spread-
- sheet (or in Redmine Backlogs!) to
- visualize the sprint.
-******************************************/
-
-// @ts-expect-error TS(2304): Cannot find name 'RB'.
-RB.Backlog = (function ($) {
- // @ts-expect-error TS(2304): Cannot find name 'RB'.
- return RB.Object.create({
-
- initialize(el:any) {
- this.$ = $(el);
- this.el = el;
-
- // Associate this object with the element for later retrieval
- this.$.data('this', this);
-
- // Make the list sortable
- this.getList().sortable({
- connectWith: '.stories',
- dropOnEmpty: true,
- start: this.dragStart,
- stop: this.dragStop,
- update: this.dragComplete,
- receive: this.dragChanged,
- remove: this.dragChanged,
- containment: $('#backlogs_container'),
- cancel: 'input, textarea, button, select, option, .prevent_drag',
- scroll: true,
- helper(event:any, ui:any) {
- const $clone = $(ui).clone();
- $clone.css('position', 'absolute');
- return $clone.get(0);
- },
- });
-
- // Observe menu items
- this.$.find('.add_new_story').click(this.handleNewStoryClick);
-
- if (this.isSprintBacklog()) {
- // @ts-expect-error TS(2304): Cannot find name 'RB'.
- RB.Factory.initialize(RB.Sprint, this.getSprint());
- // @ts-expect-error TS(2304): Cannot find name 'RB'.
- this.burndown = RB.Factory.initialize(RB.Burndown, this.$.find('.show_burndown_chart'));
- this.burndown.setSprintId(this.getSprint().data('this').getID());
- }
-
- // Initialize each item in the backlog
- this.getStories().each(function (this:any, index:any) {
- // 'this' refers to an element with class="story"
- // @ts-expect-error TS(2304): Cannot find name 'RB'.
- RB.Factory.initialize(RB.Story, this);
- });
-
- if (this.isSprintBacklog()) {
- this.refresh();
- }
- },
-
- dragChanged(e:any, ui:any) {
- $(this).parents('.backlog').data('this').refresh();
- },
-
- dragComplete(e:any, ui:any) {
- const isDropTarget = (ui.sender === null || ui.sender === undefined);
-
- // jQuery triggers dragComplete of source and target.
- // Thus we have to check here. Otherwise, the story
- // would be saved twice.
- if (isDropTarget) {
- ui.item.data('this').saveDragResult();
- }
- },
-
- dragStart(e:any, ui:any) {
- ui.item.addClass('dragging');
- },
-
- dragStop(e:any, ui:any) {
- ui.item.removeClass('dragging');
- },
-
- getSprint() {
- return $(this.el).find('.model.sprint').first();
- },
-
- getStories() {
- return this.getList().children('.story');
- },
-
- getList() {
- return this.$.children('.stories').first();
- },
-
- handleNewStoryClick(e:any) {
- const toggler = $(this).parents('.header').find('.toggler');
- if (toggler.hasClass('closed')) {
- toggler.click();
- }
- e.preventDefault();
- $(this).parents('.backlog').data('this').newStory();
- },
-
- // return true if backlog has an element with class="sprint"
- isSprintBacklog() {
- return $(this.el).find('.sprint').length === 1;
- },
-
- newStory() {
- let story;
- let o;
-
- story = $('#story_template').children().first().clone();
- this.getList().prepend(story);
-
- // @ts-expect-error TS(2304): Cannot find name 'RB'.
- o = RB.Factory.initialize(RB.Story, story[0]);
- o.edit();
-
- story.find('.editor').first().focus();
- },
-
- refresh() {
- this.recalcVelocity();
- this.recalcOddity();
- },
-
- recalcVelocity() {
- let total:any;
-
- if (!this.isSprintBacklog()) {
- return;
- }
-
- total = 0;
- this.getStories().each(function (this:any, index:any) {
- total += $(this).data('this').getPoints();
- });
- this.$.children('.header').children('.velocity').text(total);
- },
-
- recalcOddity() {
- this.$.find('.story:even').removeClass('odd').addClass('even');
- this.$.find('.story:odd').removeClass('even').addClass('odd');
- },
- });
-}(jQuery));
diff --git a/frontend/src/stimulus/controllers/dynamic/backlogs/burndown.ts b/frontend/src/stimulus/controllers/dynamic/backlogs/burndown.ts
deleted file mode 100644
index b6942b866767..000000000000
--- a/frontend/src/stimulus/controllers/dynamic/backlogs/burndown.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-//-- copyright
-// OpenProject is an open source project management software.
-// Copyright (C) the OpenProject GmbH
-//
-// This program is free software; you can redistribute it and/or
-// modify it under the terms of the GNU General Public License version 3.
-//
-// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
-// Copyright (C) 2006-2013 Jean-Philippe Lang
-// Copyright (C) 2010-2013 the ChiliProject Team
-//
-// This program is free software; you can redistribute it and/or
-// modify it under the terms of the GNU General Public License
-// as published by the Free Software Foundation; either version 2
-// of the License, or (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-//
-// See COPYRIGHT and LICENSE files for more details.
-//++
-
-// @ts-expect-error TS(2304): Cannot find name 'RB'.
-RB.Burndown = (function ($) {
- // @ts-expect-error TS(2304): Cannot find name 'RB'.
- return RB.Object.create({
-
- initialize(el:any) {
- this.$ = $(el);
- this.el = el;
-
- // Associate this object with the element for later retrieval
- this.$.data('this', this);
-
- // Observe menu items
- this.$.click(this.show);
- },
-
- setSprintId(sprintId:any) {
- this.sprintId = sprintId;
- },
-
- getSprintId() {
- return this.sprintId;
- },
-
- show(e:any) {
- e.preventDefault();
-
- if ($('#charts').length === 0) {
- $('
').appendTo('body');
- }
- // @ts-expect-error TS(2304): Cannot find name 'RB'.
- $('#charts').html(`${RB.i18n.generating_graph}
`);
-
- // @ts-expect-error TS(2304): Cannot find name 'RB'.
- const url = RB.urlFor('show_burndown_chart', { sprint_id: $(this).data('this').sprintId, project_id: RB.constants.project_id });
- window.open(url);
- },
- });
-}(jQuery));
diff --git a/frontend/src/stimulus/controllers/dynamic/backlogs/master_backlog.ts b/frontend/src/stimulus/controllers/dynamic/backlogs/master_backlog.ts
deleted file mode 100644
index 9f055c6bcfcf..000000000000
--- a/frontend/src/stimulus/controllers/dynamic/backlogs/master_backlog.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-//-- copyright
-// OpenProject is an open source project management software.
-// Copyright (C) the OpenProject GmbH
-//
-// This program is free software; you can redistribute it and/or
-// modify it under the terms of the GNU General Public License version 3.
-//
-// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
-// Copyright (C) 2006-2013 Jean-Philippe Lang
-// Copyright (C) 2010-2013 the ChiliProject Team
-//
-// This program is free software; you can redistribute it and/or
-// modify it under the terms of the GNU General Public License
-// as published by the Free Software Foundation; either version 2
-// of the License, or (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-//
-// See COPYRIGHT and LICENSE files for more details.
-//++
-
-// Initialize the backlogs after DOM is loaded
-jQuery(($) => {
- // Initialize each backlog
- $('.backlog').each(function (index) {
- // 'this' refers to an element with class="backlog"
- // @ts-expect-error TS(2304): Cannot find name 'RB'.
- RB.Factory.initialize(RB.Backlog, this);
- });
-
- $('.backlog .toggler').on('click', function () {
- $(this).toggleClass('closed icon-arrow-up1 icon-arrow-down1');
- $(this).parents('.backlog').find('ul.stories').toggleClass('closed');
- });
-});
diff --git a/frontend/src/stimulus/controllers/dynamic/backlogs/story.controller.ts b/frontend/src/stimulus/controllers/dynamic/backlogs/story.controller.ts
new file mode 100644
index 000000000000..4a6f76bfcb3d
--- /dev/null
+++ b/frontend/src/stimulus/controllers/dynamic/backlogs/story.controller.ts
@@ -0,0 +1,164 @@
+//-- copyright
+// OpenProject is an open source project management software.
+// Copyright (C) the OpenProject GmbH
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+// Copyright (C) 2006-2013 Jean-Philippe Lang
+// Copyright (C) 2010-2013 the ChiliProject Team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See COPYRIGHT and LICENSE files for more details.
+//++
+
+import { Controller } from '@hotwired/stimulus';
+import * as Turbo from '@hotwired/turbo';
+
+export default class StoryController extends Controller implements EventListenerObject {
+ static values = {
+ id: Number,
+ splitUrl: String,
+ fullUrl: String,
+ };
+
+ declare idValue:number;
+ declare splitUrlValue:string;
+ declare fullUrlValue:string;
+
+ static classes = ['selected'];
+ declare readonly selectedClass:string;
+
+ private abortController:AbortController|null = null;
+ private clickTimeout:number|null = null;
+
+ connect():void {
+ this.abortController = new AbortController();
+ const { signal } = this.abortController;
+
+ this.element.addEventListener('click', this, { signal });
+ this.element.addEventListener('dblclick', this, { signal });
+ this.element.addEventListener('keydown', this, { signal });
+ }
+
+ disconnect():void {
+ this.abortController?.abort();
+ this.abortController = null;
+
+ if (this.clickTimeout !== null) {
+ clearTimeout(this.clickTimeout);
+ this.clickTimeout = null;
+ }
+ }
+
+ markAsSelected(_event?:Event) {
+ this.element.classList.add(this.selectedClass);
+ this.element.setAttribute('aria-current', 'true');
+ }
+
+ unmarkAsSelected(_event?:Event) {
+ this.element.classList.remove(this.selectedClass);
+ this.element.removeAttribute('aria-current');
+ }
+
+ handleEvent(event:Event):void {
+ switch (event.type) {
+ case 'click':
+ this.onClick(event as MouseEvent);
+ break;
+ case 'dblclick':
+ this.onDblClick(event as MouseEvent);
+ break;
+ case 'keydown':
+ this.onKeydown(event as KeyboardEvent);
+ break;
+ }
+ }
+
+ private onClick(event:MouseEvent):void {
+ const target = event.target;
+ if (!(target instanceof HTMLElement)) return;
+
+ if (
+ target.closest('a') ||
+ target.closest('button') ||
+ target.closest('[data-drag-handle]')
+ ) {
+ return;
+ }
+
+ if (this.clickTimeout !== null) return;
+
+ this.clickTimeout = window.setTimeout(() => {
+ this.clickTimeout = null;
+ this.openSplitPane();
+ }, 250);
+ }
+
+ private onDblClick(event:MouseEvent):void {
+ const target = event.target;
+ if (!(target instanceof HTMLElement)) return;
+
+ if (
+ target.closest('a') ||
+ target.closest('button') ||
+ target.closest('[data-drag-handle]')
+ ) {
+ return;
+ }
+
+ if (this.clickTimeout !== null) {
+ clearTimeout(this.clickTimeout);
+ this.clickTimeout = null;
+ }
+
+ this.openFullPane();
+ }
+
+ private onKeydown(event:KeyboardEvent):void {
+ if (event.key !== 'Enter') return;
+
+ const target = event.target;
+ if (!(target instanceof HTMLElement)) return;
+
+ if (
+ target.closest('a') ||
+ target.closest('button') ||
+ target.closest('input') ||
+ target.closest('textarea') ||
+ target.closest('select') ||
+ target.closest("[contenteditable='true']")
+ ) {
+ return;
+ }
+
+ event.preventDefault();
+ if (event.shiftKey) {
+ this.openFullPane();
+ } else {
+ this.openSplitPane();
+ }
+ }
+
+ private openSplitPane():void {
+ Turbo.visit(this.splitUrlValue, { frame: 'content-bodyRight', action: 'advance' });
+ }
+
+ private openFullPane():void {
+ Turbo.visit(this.fullUrlValue, { frame: '_top' });
+ }
+}
diff --git a/frontend/src/stimulus/controllers/dynamic/backlogs/story.ts b/frontend/src/stimulus/controllers/dynamic/backlogs/story.ts
deleted file mode 100644
index a9f945adacf6..000000000000
--- a/frontend/src/stimulus/controllers/dynamic/backlogs/story.ts
+++ /dev/null
@@ -1,145 +0,0 @@
-//-- copyright
-// OpenProject is an open source project management software.
-// Copyright (C) the OpenProject GmbH
-//
-// This program is free software; you can redistribute it and/or
-// modify it under the terms of the GNU General Public License version 3.
-//
-// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
-// Copyright (C) 2006-2013 Jean-Philippe Lang
-// Copyright (C) 2010-2013 the ChiliProject Team
-//
-// This program is free software; you can redistribute it and/or
-// modify it under the terms of the GNU General Public License
-// as published by the Free Software Foundation; either version 2
-// of the License, or (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-//
-// See COPYRIGHT and LICENSE files for more details.
-//++
-
-import { FetchResponse } from '@rails/request.js';
-
-/**************************************
- STORY
-***************************************/
-// @ts-expect-error TS(2304): Cannot find name 'RB'.
-RB.Story = (function ($) {
- // @ts-expect-error TS(2304): Cannot find name 'RB'.
- return RB.Object.create(RB.WorkPackage, RB.EditableInplace, {
- initialize(el:any) {
- this.$ = $(el);
- this.el = el;
-
- // Associate this object with the element for later retrieval
- this.$.data('this', this);
- this.$.on('click', '.editable', this.handleClick);
- },
-
- /**
- * Callbacks from model.js
- **/
- beforeSave() {
- this.refreshStory();
- },
-
- afterCreate(data:string, response:FetchResponse) {
- this.refreshStory();
- },
-
- afterUpdate(data:string, response:FetchResponse) {
- this.refreshStory();
- },
-
- refreshed() {
- this.refreshStory();
- },
- /**/
-
- editDialogTitle() {
- return `Story #${this.getID()}`;
- },
-
- editorDisplayed(editor:any) { },
-
- getPoints() {
- const points = parseInt(this.$.find('.story_points').first().text(), 10);
- return isNaN(points) ? 0 : points;
- },
-
- getType() {
- return 'Story';
- },
-
- markIfClosed() {
- // Do nothing
- },
-
- newDialogTitle() {
- return 'New Story';
- },
-
- refreshStory() {
- this.recalcVelocity();
- },
-
- recalcVelocity() {
- this.$.parents('.backlog').first().data('this').refresh();
- },
-
- saveDirectives() {
- let url;
- let prev;
- let sprintId;
-
- let data;
- let method;
-
- prev = this.$.prev();
- sprintId = this.$.parents('.backlog').data('this').isSprintBacklog()
- ? this.$.parents('.backlog').data('this').getSprint().data('this')
-.getID()
- : '';
-
- data = `prev=${
- prev.length === 1 ? prev.data('this').getID() : ''
- }&version_id=${sprintId}`;
-
- if (this.$.find('.editor').length > 0) {
- data += `&${this.$.find('.editor').serialize()}`;
- }
-
- //TODO: this might be unsave in case the parent of this story is not the
- // sprint backlog, then we dont have a sprintId an cannot generate a
- // valid url - one option might be to take RB.constants.sprint_id
- // hoping it exists
- if (this.isNew()) {
- // @ts-expect-error TS(2304): Cannot find name 'RB'.
- url = RB.urlFor('create_story', { sprint_id: sprintId });
- method = 'post';
- } else {
- // @ts-expect-error TS(2304): Cannot find name 'RB'.
- url = RB.urlFor('update_story', { id: this.getID(), sprint_id: sprintId });
- method = 'put';
- }
-
- return {
- url,
- method,
- data,
- };
- },
-
- beforeSaveDragResult() {
- // Do nothing
- },
- });
-}(jQuery));
diff --git a/frontend/src/stimulus/controllers/dynamic/backlogs/taskboard-legacy.controller.ts b/frontend/src/stimulus/controllers/dynamic/backlogs/taskboard-legacy.controller.ts
new file mode 100644
index 000000000000..3e3beebea8ed
--- /dev/null
+++ b/frontend/src/stimulus/controllers/dynamic/backlogs/taskboard-legacy.controller.ts
@@ -0,0 +1,17 @@
+import { Controller } from '@hotwired/stimulus';
+
+import 'core-vendor/jquery.jeditable.mini';
+import 'core-vendor/jquery.colorcontrast';
+
+import './common';
+import './model';
+import './editable_inplace';
+import './sprint';
+import './work_package';
+import './task';
+import './impediment';
+import './taskboard';
+import './show_main';
+
+export default class TaskboardLegacyController extends Controller {
+}
diff --git a/frontend/src/vendor/jquery.flot/LICENSE b/frontend/src/vendor/jquery.flot/LICENSE
deleted file mode 100644
index 07d5b2094d15..000000000000
--- a/frontend/src/vendor/jquery.flot/LICENSE
+++ /dev/null
@@ -1,22 +0,0 @@
-Copyright (c) 2007-2009 IOLA and Ole Laursen
-
-Permission is hereby granted, free of charge, to any person
-obtaining a copy of this software and associated documentation
-files (the "Software"), to deal in the Software without
-restriction, including without limitation the rights to use,
-copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the
-Software is furnished to do so, subject to the following
-conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
-OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-OTHER DEALINGS IN THE SOFTWARE.
diff --git a/frontend/src/vendor/jquery.flot/excanvas.js b/frontend/src/vendor/jquery.flot/excanvas.js
deleted file mode 100644
index c40d6f7014d8..000000000000
--- a/frontend/src/vendor/jquery.flot/excanvas.js
+++ /dev/null
@@ -1,1427 +0,0 @@
-// Copyright 2006 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-
-// Known Issues:
-//
-// * Patterns only support repeat.
-// * Radial gradient are not implemented. The VML version of these look very
-// different from the canvas one.
-// * Clipping paths are not implemented.
-// * Coordsize. The width and height attribute have higher priority than the
-// width and height style values which isn't correct.
-// * Painting mode isn't implemented.
-// * Canvas width/height should is using content-box by default. IE in
-// Quirks mode will draw the canvas using border-box. Either change your
-// doctype to HTML5
-// (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype)
-// or use Box Sizing Behavior from WebFX
-// (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html)
-// * Non uniform scaling does not correctly scale strokes.
-// * Filling very large shapes (above 5000 points) is buggy.
-// * Optimize. There is always room for speed improvements.
-
-// Only add this code if we do not already have a canvas implementation
-if (!document.createElement('canvas').getContext) {
-
-(function() {
-
- // alias some functions to make (compiled) code shorter
- var m = Math;
- var mr = m.round;
- var ms = m.sin;
- var mc = m.cos;
- var abs = m.abs;
- var sqrt = m.sqrt;
-
- // this is used for sub pixel precision
- var Z = 10;
- var Z2 = Z / 2;
-
- /**
- * This funtion is assigned to the elements as element.getContext().
- * @this {HTMLElement}
- * @return {CanvasRenderingContext2D_}
- */
- function getContext() {
- return this.context_ ||
- (this.context_ = new CanvasRenderingContext2D_(this));
- }
-
- var slice = Array.prototype.slice;
-
- /**
- * Binds a function to an object. The returned function will always use the
- * passed in {@code obj} as {@code this}.
- *
- * Example:
- *
- * g = bind(f, obj, a, b)
- * g(c, d) // will do f.call(obj, a, b, c, d)
- *
- * @param {Function} f The function to bind the object to
- * @param {Object} obj The object that should act as this when the function
- * is called
- * @param {*} var_args Rest arguments that will be used as the initial
- * arguments when the function is called
- * @return {Function} A new function that has bound this
- */
- function bind(f, obj, var_args) {
- var a = slice.call(arguments, 2);
- return function() {
- return f.apply(obj, a.concat(slice.call(arguments)));
- };
- }
-
- function encodeHtmlAttribute(s) {
- return String(s).replace(/&/g, '&').replace(/"/g, '"');
- }
-
- function addNamespacesAndStylesheet(doc) {
- // create xmlns
- if (!doc.namespaces['g_vml_']) {
- doc.namespaces.add('g_vml_', 'urn:schemas-microsoft-com:vml',
- '#default#VML');
-
- }
- if (!doc.namespaces['g_o_']) {
- doc.namespaces.add('g_o_', 'urn:schemas-microsoft-com:office:office',
- '#default#VML');
- }
-
- // Setup default CSS. Only add one style sheet per document
- if (!doc.styleSheets['ex_canvas_']) {
- var ss = doc.createStyleSheet();
- ss.owningElement.id = 'ex_canvas_';
- ss.cssText = 'canvas{display:inline-block;overflow:hidden;' +
- // default size is 300x150 in Gecko and Opera
- 'text-align:left;width:300px;height:150px}';
- }
- }
-
- // Add namespaces and stylesheet at startup.
- addNamespacesAndStylesheet(document);
-
- var G_vmlCanvasManager_ = {
- init: function(opt_doc) {
- if (/MSIE/.test(navigator.userAgent) && !window.opera) {
- var doc = opt_doc || document;
- // Create a dummy element so that IE will allow canvas elements to be
- // recognized.
- doc.createElement('canvas');
- doc.attachEvent('onreadystatechange', bind(this.init_, this, doc));
- }
- },
-
- init_: function(doc) {
- // find all canvas elements
- var els = doc.getElementsByTagName('canvas');
- for (var i = 0; i < els.length; i++) {
- this.initElement(els[i]);
- }
- },
-
- /**
- * Public initializes a canvas element so that it can be used as canvas
- * element from now on. This is called automatically before the page is
- * loaded but if you are creating elements using createElement you need to
- * make sure this is called on the element.
- * @param {HTMLElement} el The canvas element to initialize.
- * @return {HTMLElement} the element that was created.
- */
- initElement: function(el) {
- if (!el.getContext) {
- el.getContext = getContext;
-
- // Add namespaces and stylesheet to document of the element.
- addNamespacesAndStylesheet(el.ownerDocument);
-
- // Remove fallback content. There is no way to hide text nodes so we
- // just remove all childNodes. We could hide all elements and remove
- // text nodes but who really cares about the fallback content.
- el.innerHTML = '';
-
- // do not use inline function because that will leak memory
- el.attachEvent('onpropertychange', onPropertyChange);
- el.attachEvent('onresize', onResize);
-
- var attrs = el.attributes;
- if (attrs.width && attrs.width.specified) {
- // TODO: use runtimeStyle and coordsize
- // el.getContext().setWidth_(attrs.width.nodeValue);
- el.style.width = attrs.width.nodeValue + 'px';
- } else {
- el.width = el.clientWidth;
- }
- if (attrs.height && attrs.height.specified) {
- // TODO: use runtimeStyle and coordsize
- // el.getContext().setHeight_(attrs.height.nodeValue);
- el.style.height = attrs.height.nodeValue + 'px';
- } else {
- el.height = el.clientHeight;
- }
- //el.getContext().setCoordsize_()
- }
- return el;
- }
- };
-
- function onPropertyChange(e) {
- var el = e.srcElement;
-
- switch (e.propertyName) {
- case 'width':
- el.getContext().clearRect();
- el.style.width = el.attributes.width.nodeValue + 'px';
- // In IE8 this does not trigger onresize.
- el.firstChild.style.width = el.clientWidth + 'px';
- break;
- case 'height':
- el.getContext().clearRect();
- el.style.height = el.attributes.height.nodeValue + 'px';
- el.firstChild.style.height = el.clientHeight + 'px';
- break;
- }
- }
-
- function onResize(e) {
- var el = e.srcElement;
- if (el.firstChild) {
- el.firstChild.style.width = el.clientWidth + 'px';
- el.firstChild.style.height = el.clientHeight + 'px';
- }
- }
-
- G_vmlCanvasManager_.init();
-
- // precompute "00" to "FF"
- var decToHex = [];
- for (var i = 0; i < 16; i++) {
- for (var j = 0; j < 16; j++) {
- decToHex[i * 16 + j] = i.toString(16) + j.toString(16);
- }
- }
-
- function createMatrixIdentity() {
- return [
- [1, 0, 0],
- [0, 1, 0],
- [0, 0, 1]
- ];
- }
-
- function matrixMultiply(m1, m2) {
- var result = createMatrixIdentity();
-
- for (var x = 0; x < 3; x++) {
- for (var y = 0; y < 3; y++) {
- var sum = 0;
-
- for (var z = 0; z < 3; z++) {
- sum += m1[x][z] * m2[z][y];
- }
-
- result[x][y] = sum;
- }
- }
- return result;
- }
-
- function copyState(o1, o2) {
- o2.fillStyle = o1.fillStyle;
- o2.lineCap = o1.lineCap;
- o2.lineJoin = o1.lineJoin;
- o2.lineWidth = o1.lineWidth;
- o2.miterLimit = o1.miterLimit;
- o2.shadowBlur = o1.shadowBlur;
- o2.shadowColor = o1.shadowColor;
- o2.shadowOffsetX = o1.shadowOffsetX;
- o2.shadowOffsetY = o1.shadowOffsetY;
- o2.strokeStyle = o1.strokeStyle;
- o2.globalAlpha = o1.globalAlpha;
- o2.font = o1.font;
- o2.textAlign = o1.textAlign;
- o2.textBaseline = o1.textBaseline;
- o2.arcScaleX_ = o1.arcScaleX_;
- o2.arcScaleY_ = o1.arcScaleY_;
- o2.lineScale_ = o1.lineScale_;
- }
-
- var colorData = {
- aliceblue: '#F0F8FF',
- antiquewhite: '#FAEBD7',
- aquamarine: '#7FFFD4',
- azure: '#F0FFFF',
- beige: '#F5F5DC',
- bisque: '#FFE4C4',
- black: '#000000',
- blanchedalmond: '#FFEBCD',
- blueviolet: '#8A2BE2',
- brown: '#A52A2A',
- burlywood: '#DEB887',
- cadetblue: '#5F9EA0',
- chartreuse: '#7FFF00',
- chocolate: '#D2691E',
- coral: '#FF7F50',
- cornflowerblue: '#6495ED',
- cornsilk: '#FFF8DC',
- crimson: '#DC143C',
- cyan: '#00FFFF',
- darkblue: '#00008B',
- darkcyan: '#008B8B',
- darkgoldenrod: '#B8860B',
- darkgray: '#A9A9A9',
- darkgreen: '#006400',
- darkgrey: '#A9A9A9',
- darkkhaki: '#BDB76B',
- darkmagenta: '#8B008B',
- darkolivegreen: '#556B2F',
- darkorange: '#FF8C00',
- darkorchid: '#9932CC',
- darkred: '#8B0000',
- darksalmon: '#E9967A',
- darkseagreen: '#8FBC8F',
- darkslateblue: '#483D8B',
- darkslategray: '#2F4F4F',
- darkslategrey: '#2F4F4F',
- darkturquoise: '#00CED1',
- darkviolet: '#9400D3',
- deeppink: '#FF1493',
- deepskyblue: '#00BFFF',
- dimgray: '#696969',
- dimgrey: '#696969',
- dodgerblue: '#1E90FF',
- firebrick: '#B22222',
- floralwhite: '#FFFAF0',
- forestgreen: '#228B22',
- gainsboro: '#DCDCDC',
- ghostwhite: '#F8F8FF',
- gold: '#FFD700',
- goldenrod: '#DAA520',
- grey: '#808080',
- greenyellow: '#ADFF2F',
- honeydew: '#F0FFF0',
- hotpink: '#FF69B4',
- indianred: '#CD5C5C',
- indigo: '#4B0082',
- ivory: '#FFFFF0',
- khaki: '#F0E68C',
- lavender: '#E6E6FA',
- lavenderblush: '#FFF0F5',
- lawngreen: '#7CFC00',
- lemonchiffon: '#FFFACD',
- lightblue: '#ADD8E6',
- lightcoral: '#F08080',
- lightcyan: '#E0FFFF',
- lightgoldenrodyellow: '#FAFAD2',
- lightgreen: '#90EE90',
- lightgrey: '#D3D3D3',
- lightpink: '#FFB6C1',
- lightsalmon: '#FFA07A',
- lightseagreen: '#20B2AA',
- lightskyblue: '#87CEFA',
- lightslategray: '#778899',
- lightslategrey: '#778899',
- lightsteelblue: '#B0C4DE',
- lightyellow: '#FFFFE0',
- limegreen: '#32CD32',
- linen: '#FAF0E6',
- magenta: '#FF00FF',
- mediumaquamarine: '#66CDAA',
- mediumblue: '#0000CD',
- mediumorchid: '#BA55D3',
- mediumpurple: '#9370DB',
- mediumseagreen: '#3CB371',
- mediumslateblue: '#7B68EE',
- mediumspringgreen: '#00FA9A',
- mediumturquoise: '#48D1CC',
- mediumvioletred: '#C71585',
- midnightblue: '#191970',
- mintcream: '#F5FFFA',
- mistyrose: '#FFE4E1',
- moccasin: '#FFE4B5',
- navajowhite: '#FFDEAD',
- oldlace: '#FDF5E6',
- olivedrab: '#6B8E23',
- orange: '#FFA500',
- orangered: '#FF4500',
- orchid: '#DA70D6',
- palegoldenrod: '#EEE8AA',
- palegreen: '#98FB98',
- paleturquoise: '#AFEEEE',
- palevioletred: '#DB7093',
- papayawhip: '#FFEFD5',
- peachpuff: '#FFDAB9',
- peru: '#CD853F',
- pink: '#FFC0CB',
- plum: '#DDA0DD',
- powderblue: '#B0E0E6',
- rosybrown: '#BC8F8F',
- royalblue: '#4169E1',
- saddlebrown: '#8B4513',
- salmon: '#FA8072',
- sandybrown: '#F4A460',
- seagreen: '#2E8B57',
- seashell: '#FFF5EE',
- sienna: '#A0522D',
- skyblue: '#87CEEB',
- slateblue: '#6A5ACD',
- slategray: '#708090',
- slategrey: '#708090',
- snow: '#FFFAFA',
- springgreen: '#00FF7F',
- steelblue: '#4682B4',
- tan: '#D2B48C',
- thistle: '#D8BFD8',
- tomato: '#FF6347',
- turquoise: '#40E0D0',
- violet: '#EE82EE',
- wheat: '#F5DEB3',
- whitesmoke: '#F5F5F5',
- yellowgreen: '#9ACD32'
- };
-
-
- function getRgbHslContent(styleString) {
- var start = styleString.indexOf('(', 3);
- var end = styleString.indexOf(')', start + 1);
- var parts = styleString.substring(start + 1, end).split(',');
- // add alpha if needed
- if (parts.length == 4 && styleString.substr(3, 1) == 'a') {
- alpha = Number(parts[3]);
- } else {
- parts[3] = 1;
- }
- return parts;
- }
-
- function percent(s) {
- return parseFloat(s) / 100;
- }
-
- function clamp(v, min, max) {
- return Math.min(max, Math.max(min, v));
- }
-
- function hslToRgb(parts){
- var r, g, b;
- h = parseFloat(parts[0]) / 360 % 360;
- if (h < 0)
- h++;
- s = clamp(percent(parts[1]), 0, 1);
- l = clamp(percent(parts[2]), 0, 1);
- if (s == 0) {
- r = g = b = l; // achromatic
- } else {
- var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
- var p = 2 * l - q;
- r = hueToRgb(p, q, h + 1 / 3);
- g = hueToRgb(p, q, h);
- b = hueToRgb(p, q, h - 1 / 3);
- }
-
- return '#' + decToHex[Math.floor(r * 255)] +
- decToHex[Math.floor(g * 255)] +
- decToHex[Math.floor(b * 255)];
- }
-
- function hueToRgb(m1, m2, h) {
- if (h < 0)
- h++;
- if (h > 1)
- h--;
-
- if (6 * h < 1)
- return m1 + (m2 - m1) * 6 * h;
- else if (2 * h < 1)
- return m2;
- else if (3 * h < 2)
- return m1 + (m2 - m1) * (2 / 3 - h) * 6;
- else
- return m1;
- }
-
- function processStyle(styleString) {
- var str, alpha = 1;
-
- styleString = String(styleString);
- if (styleString.charAt(0) == '#') {
- str = styleString;
- } else if (/^rgb/.test(styleString)) {
- var parts = getRgbHslContent(styleString);
- var str = '#', n;
- for (var i = 0; i < 3; i++) {
- if (parts[i].indexOf('%') != -1) {
- n = Math.floor(percent(parts[i]) * 255);
- } else {
- n = Number(parts[i]);
- }
- str += decToHex[clamp(n, 0, 255)];
- }
- alpha = parts[3];
- } else if (/^hsl/.test(styleString)) {
- var parts = getRgbHslContent(styleString);
- str = hslToRgb(parts);
- alpha = parts[3];
- } else {
- str = colorData[styleString] || styleString;
- }
- return {color: str, alpha: alpha};
- }
-
- var DEFAULT_STYLE = {
- style: 'normal',
- variant: 'normal',
- weight: 'normal',
- size: 10,
- family: 'sans-serif'
- };
-
- // Internal text style cache
- var fontStyleCache = {};
-
- function processFontStyle(styleString) {
- if (fontStyleCache[styleString]) {
- return fontStyleCache[styleString];
- }
-
- var el = document.createElement('div');
- var style = el.style;
- try {
- style.font = styleString;
- } catch (ex) {
- // Ignore failures to set to invalid font.
- }
-
- return fontStyleCache[styleString] = {
- style: style.fontStyle || DEFAULT_STYLE.style,
- variant: style.fontVariant || DEFAULT_STYLE.variant,
- weight: style.fontWeight || DEFAULT_STYLE.weight,
- size: style.fontSize || DEFAULT_STYLE.size,
- family: style.fontFamily || DEFAULT_STYLE.family
- };
- }
-
- function getComputedStyle(style, element) {
- var computedStyle = {};
-
- for (var p in style) {
- computedStyle[p] = style[p];
- }
-
- // Compute the size
- var canvasFontSize = parseFloat(element.currentStyle.fontSize),
- fontSize = parseFloat(style.size);
-
- if (typeof style.size == 'number') {
- computedStyle.size = style.size;
- } else if (style.size.indexOf('px') != -1) {
- computedStyle.size = fontSize;
- } else if (style.size.indexOf('em') != -1) {
- computedStyle.size = canvasFontSize * fontSize;
- } else if(style.size.indexOf('%') != -1) {
- computedStyle.size = (canvasFontSize / 100) * fontSize;
- } else if (style.size.indexOf('pt') != -1) {
- computedStyle.size = fontSize / .75;
- } else {
- computedStyle.size = canvasFontSize;
- }
-
- // Different scaling between normal text and VML text. This was found using
- // trial and error to get the same size as non VML text.
- computedStyle.size *= 0.981;
-
- return computedStyle;
- }
-
- function buildStyle(style) {
- return style.style + ' ' + style.variant + ' ' + style.weight + ' ' +
- style.size + 'px ' + style.family;
- }
-
- function processLineCap(lineCap) {
- switch (lineCap) {
- case 'butt':
- return 'flat';
- case 'round':
- return 'round';
- case 'square':
- default:
- return 'square';
- }
- }
-
- /**
- * This class implements CanvasRenderingContext2D interface as described by
- * the WHATWG.
- * @param {HTMLElement} surfaceElement The element that the 2D context should
- * be associated with
- */
- function CanvasRenderingContext2D_(surfaceElement) {
- this.m_ = createMatrixIdentity();
-
- this.mStack_ = [];
- this.aStack_ = [];
- this.currentPath_ = [];
-
- // Canvas context properties
- this.strokeStyle = '#000';
- this.fillStyle = '#000';
-
- this.lineWidth = 1;
- this.lineJoin = 'miter';
- this.lineCap = 'butt';
- this.miterLimit = Z * 1;
- this.globalAlpha = 1;
- this.font = '10px sans-serif';
- this.textAlign = 'left';
- this.textBaseline = 'alphabetic';
- this.canvas = surfaceElement;
-
- var el = surfaceElement.ownerDocument.createElement('div');
- el.style.width = surfaceElement.clientWidth + 'px';
- el.style.height = surfaceElement.clientHeight + 'px';
- el.style.overflow = 'hidden';
- el.style.position = 'absolute';
- surfaceElement.appendChild(el);
-
- this.element_ = el;
- this.arcScaleX_ = 1;
- this.arcScaleY_ = 1;
- this.lineScale_ = 1;
- }
-
- var contextPrototype = CanvasRenderingContext2D_.prototype;
- contextPrototype.clearRect = function() {
- if (this.textMeasureEl_) {
- this.textMeasureEl_.removeNode(true);
- this.textMeasureEl_ = null;
- }
- this.element_.innerHTML = '';
- };
-
- contextPrototype.beginPath = function() {
- // TODO: Branch current matrix so that save/restore has no effect
- // as per safari docs.
- this.currentPath_ = [];
- };
-
- contextPrototype.moveTo = function(aX, aY) {
- var p = this.getCoords_(aX, aY);
- this.currentPath_.push({type: 'moveTo', x: p.x, y: p.y});
- this.currentX_ = p.x;
- this.currentY_ = p.y;
- };
-
- contextPrototype.lineTo = function(aX, aY) {
- var p = this.getCoords_(aX, aY);
- this.currentPath_.push({type: 'lineTo', x: p.x, y: p.y});
-
- this.currentX_ = p.x;
- this.currentY_ = p.y;
- };
-
- contextPrototype.bezierCurveTo = function(aCP1x, aCP1y,
- aCP2x, aCP2y,
- aX, aY) {
- var p = this.getCoords_(aX, aY);
- var cp1 = this.getCoords_(aCP1x, aCP1y);
- var cp2 = this.getCoords_(aCP2x, aCP2y);
- bezierCurveTo(this, cp1, cp2, p);
- };
-
- // Helper function that takes the already fixed cordinates.
- function bezierCurveTo(self, cp1, cp2, p) {
- self.currentPath_.push({
- type: 'bezierCurveTo',
- cp1x: cp1.x,
- cp1y: cp1.y,
- cp2x: cp2.x,
- cp2y: cp2.y,
- x: p.x,
- y: p.y
- });
- self.currentX_ = p.x;
- self.currentY_ = p.y;
- }
-
- contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) {
- // the following is lifted almost directly from
- // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes
-
- var cp = this.getCoords_(aCPx, aCPy);
- var p = this.getCoords_(aX, aY);
-
- var cp1 = {
- x: this.currentX_ + 2.0 / 3.0 * (cp.x - this.currentX_),
- y: this.currentY_ + 2.0 / 3.0 * (cp.y - this.currentY_)
- };
- var cp2 = {
- x: cp1.x + (p.x - this.currentX_) / 3.0,
- y: cp1.y + (p.y - this.currentY_) / 3.0
- };
-
- bezierCurveTo(this, cp1, cp2, p);
- };
-
- contextPrototype.arc = function(aX, aY, aRadius,
- aStartAngle, aEndAngle, aClockwise) {
- aRadius *= Z;
- var arcType = aClockwise ? 'at' : 'wa';
-
- var xStart = aX + mc(aStartAngle) * aRadius - Z2;
- var yStart = aY + ms(aStartAngle) * aRadius - Z2;
-
- var xEnd = aX + mc(aEndAngle) * aRadius - Z2;
- var yEnd = aY + ms(aEndAngle) * aRadius - Z2;
-
- // IE won't render arches drawn counter clockwise if xStart == xEnd.
- if (xStart == xEnd && !aClockwise) {
- xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something
- // that can be represented in binary
- }
-
- var p = this.getCoords_(aX, aY);
- var pStart = this.getCoords_(xStart, yStart);
- var pEnd = this.getCoords_(xEnd, yEnd);
-
- this.currentPath_.push({type: arcType,
- x: p.x,
- y: p.y,
- radius: aRadius,
- xStart: pStart.x,
- yStart: pStart.y,
- xEnd: pEnd.x,
- yEnd: pEnd.y});
-
- };
-
- contextPrototype.rect = function(aX, aY, aWidth, aHeight) {
- this.moveTo(aX, aY);
- this.lineTo(aX + aWidth, aY);
- this.lineTo(aX + aWidth, aY + aHeight);
- this.lineTo(aX, aY + aHeight);
- this.closePath();
- };
-
- contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) {
- var oldPath = this.currentPath_;
- this.beginPath();
-
- this.moveTo(aX, aY);
- this.lineTo(aX + aWidth, aY);
- this.lineTo(aX + aWidth, aY + aHeight);
- this.lineTo(aX, aY + aHeight);
- this.closePath();
- this.stroke();
-
- this.currentPath_ = oldPath;
- };
-
- contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) {
- var oldPath = this.currentPath_;
- this.beginPath();
-
- this.moveTo(aX, aY);
- this.lineTo(aX + aWidth, aY);
- this.lineTo(aX + aWidth, aY + aHeight);
- this.lineTo(aX, aY + aHeight);
- this.closePath();
- this.fill();
-
- this.currentPath_ = oldPath;
- };
-
- contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) {
- var gradient = new CanvasGradient_('gradient');
- gradient.x0_ = aX0;
- gradient.y0_ = aY0;
- gradient.x1_ = aX1;
- gradient.y1_ = aY1;
- return gradient;
- };
-
- contextPrototype.createRadialGradient = function(aX0, aY0, aR0,
- aX1, aY1, aR1) {
- var gradient = new CanvasGradient_('gradientradial');
- gradient.x0_ = aX0;
- gradient.y0_ = aY0;
- gradient.r0_ = aR0;
- gradient.x1_ = aX1;
- gradient.y1_ = aY1;
- gradient.r1_ = aR1;
- return gradient;
- };
-
- contextPrototype.drawImage = function(image, var_args) {
- var dx, dy, dw, dh, sx, sy, sw, sh;
-
- // to find the original width we overide the width and height
- var oldRuntimeWidth = image.runtimeStyle.width;
- var oldRuntimeHeight = image.runtimeStyle.height;
- image.runtimeStyle.width = 'auto';
- image.runtimeStyle.height = 'auto';
-
- // get the original size
- var w = image.width;
- var h = image.height;
-
- // and remove overides
- image.runtimeStyle.width = oldRuntimeWidth;
- image.runtimeStyle.height = oldRuntimeHeight;
-
- if (arguments.length == 3) {
- dx = arguments[1];
- dy = arguments[2];
- sx = sy = 0;
- sw = dw = w;
- sh = dh = h;
- } else if (arguments.length == 5) {
- dx = arguments[1];
- dy = arguments[2];
- dw = arguments[3];
- dh = arguments[4];
- sx = sy = 0;
- sw = w;
- sh = h;
- } else if (arguments.length == 9) {
- sx = arguments[1];
- sy = arguments[2];
- sw = arguments[3];
- sh = arguments[4];
- dx = arguments[5];
- dy = arguments[6];
- dw = arguments[7];
- dh = arguments[8];
- } else {
- throw Error('Invalid number of arguments');
- }
-
- var d = this.getCoords_(dx, dy);
-
- var w2 = sw / 2;
- var h2 = sh / 2;
-
- var vmlStr = [];
-
- var W = 10;
- var H = 10;
-
- // For some reason that I've now forgotten, using divs didn't work
- vmlStr.push(' ' ,
- ' ',
- ' ');
-
- this.element_.insertAdjacentHTML('BeforeEnd', vmlStr.join(''));
- };
-
- contextPrototype.stroke = function(aFill) {
- var W = 10;
- var H = 10;
- // Divide the shape into chunks if it's too long because IE has a limit
- // somewhere for how long a VML shape can be. This simple division does
- // not work with fills, only strokes, unfortunately.
- var chunkSize = 5000;
-
- var min = {x: null, y: null};
- var max = {x: null, y: null};
-
- for (var j = 0; j < this.currentPath_.length; j += chunkSize) {
- var lineStr = [];
- var lineOpen = false;
-
- lineStr.push('');
-
- if (!aFill) {
- appendStroke(this, lineStr);
- } else {
- appendFill(this, lineStr, min, max);
- }
-
- lineStr.push(' ');
-
- this.element_.insertAdjacentHTML('beforeEnd', lineStr.join(''));
- }
- };
-
- function appendStroke(ctx, lineStr) {
- var a = processStyle(ctx.strokeStyle);
- var color = a.color;
- var opacity = a.alpha * ctx.globalAlpha;
- var lineWidth = ctx.lineScale_ * ctx.lineWidth;
-
- // VML cannot correctly render a line if the width is less than 1px.
- // In that case, we dilute the color to make the line look thinner.
- if (lineWidth < 1) {
- opacity *= lineWidth;
- }
-
- lineStr.push(
- ' '
- );
- }
-
- function appendFill(ctx, lineStr, min, max) {
- var fillStyle = ctx.fillStyle;
- var arcScaleX = ctx.arcScaleX_;
- var arcScaleY = ctx.arcScaleY_;
- var width = max.x - min.x;
- var height = max.y - min.y;
- if (fillStyle instanceof CanvasGradient_) {
- // TODO: Gradients transformed with the transformation matrix.
- var angle = 0;
- var focus = {x: 0, y: 0};
-
- // additional offset
- var shift = 0;
- // scale factor for offset
- var expansion = 1;
-
- if (fillStyle.type_ == 'gradient') {
- var x0 = fillStyle.x0_ / arcScaleX;
- var y0 = fillStyle.y0_ / arcScaleY;
- var x1 = fillStyle.x1_ / arcScaleX;
- var y1 = fillStyle.y1_ / arcScaleY;
- var p0 = ctx.getCoords_(x0, y0);
- var p1 = ctx.getCoords_(x1, y1);
- var dx = p1.x - p0.x;
- var dy = p1.y - p0.y;
- angle = Math.atan2(dx, dy) * 180 / Math.PI;
-
- // The angle should be a non-negative number.
- if (angle < 0) {
- angle += 360;
- }
-
- // Very small angles produce an unexpected result because they are
- // converted to a scientific notation string.
- if (angle < 1e-6) {
- angle = 0;
- }
- } else {
- var p0 = ctx.getCoords_(fillStyle.x0_, fillStyle.y0_);
- focus = {
- x: (p0.x - min.x) / width,
- y: (p0.y - min.y) / height
- };
-
- width /= arcScaleX * Z;
- height /= arcScaleY * Z;
- var dimension = m.max(width, height);
- shift = 2 * fillStyle.r0_ / dimension;
- expansion = 2 * fillStyle.r1_ / dimension - shift;
- }
-
- // We need to sort the color stops in ascending order by offset,
- // otherwise IE won't interpret it correctly.
- var stops = fillStyle.colors_;
- stops.sort(function(cs1, cs2) {
- return cs1.offset - cs2.offset;
- });
-
- var length = stops.length;
- var color1 = stops[0].color;
- var color2 = stops[length - 1].color;
- var opacity1 = stops[0].alpha * ctx.globalAlpha;
- var opacity2 = stops[length - 1].alpha * ctx.globalAlpha;
-
- var colors = [];
- for (var i = 0; i < length; i++) {
- var stop = stops[i];
- colors.push(stop.offset * expansion + shift + ' ' + stop.color);
- }
-
- // When colors attribute is used, the meanings of opacity and o:opacity2
- // are reversed.
- lineStr.push(' ');
- } else if (fillStyle instanceof CanvasPattern_) {
- if (width && height) {
- var deltaLeft = -min.x;
- var deltaTop = -min.y;
- lineStr.push(' ');
- }
- } else {
- var a = processStyle(ctx.fillStyle);
- var color = a.color;
- var opacity = a.alpha * ctx.globalAlpha;
- lineStr.push(' ');
- }
- }
-
- contextPrototype.fill = function() {
- this.stroke(true);
- };
-
- contextPrototype.closePath = function() {
- this.currentPath_.push({type: 'close'});
- };
-
- /**
- * @private
- */
- contextPrototype.getCoords_ = function(aX, aY) {
- var m = this.m_;
- return {
- x: Z * (aX * m[0][0] + aY * m[1][0] + m[2][0]) - Z2,
- y: Z * (aX * m[0][1] + aY * m[1][1] + m[2][1]) - Z2
- };
- };
-
- contextPrototype.save = function() {
- var o = {};
- copyState(this, o);
- this.aStack_.push(o);
- this.mStack_.push(this.m_);
- this.m_ = matrixMultiply(createMatrixIdentity(), this.m_);
- };
-
- contextPrototype.restore = function() {
- if (this.aStack_.length) {
- copyState(this.aStack_.pop(), this);
- this.m_ = this.mStack_.pop();
- }
- };
-
- function matrixIsFinite(m) {
- return isFinite(m[0][0]) && isFinite(m[0][1]) &&
- isFinite(m[1][0]) && isFinite(m[1][1]) &&
- isFinite(m[2][0]) && isFinite(m[2][1]);
- }
-
- function setM(ctx, m, updateLineScale) {
- if (!matrixIsFinite(m)) {
- return;
- }
- ctx.m_ = m;
-
- if (updateLineScale) {
- // Get the line scale.
- // Determinant of this.m_ means how much the area is enlarged by the
- // transformation. So its square root can be used as a scale factor
- // for width.
- var det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
- ctx.lineScale_ = sqrt(abs(det));
- }
- }
-
- contextPrototype.translate = function(aX, aY) {
- var m1 = [
- [1, 0, 0],
- [0, 1, 0],
- [aX, aY, 1]
- ];
-
- setM(this, matrixMultiply(m1, this.m_), false);
- };
-
- contextPrototype.rotate = function(aRot) {
- var c = mc(aRot);
- var s = ms(aRot);
-
- var m1 = [
- [c, s, 0],
- [-s, c, 0],
- [0, 0, 1]
- ];
-
- setM(this, matrixMultiply(m1, this.m_), false);
- };
-
- contextPrototype.scale = function(aX, aY) {
- this.arcScaleX_ *= aX;
- this.arcScaleY_ *= aY;
- var m1 = [
- [aX, 0, 0],
- [0, aY, 0],
- [0, 0, 1]
- ];
-
- setM(this, matrixMultiply(m1, this.m_), true);
- };
-
- contextPrototype.transform = function(m11, m12, m21, m22, dx, dy) {
- var m1 = [
- [m11, m12, 0],
- [m21, m22, 0],
- [dx, dy, 1]
- ];
-
- setM(this, matrixMultiply(m1, this.m_), true);
- };
-
- contextPrototype.setTransform = function(m11, m12, m21, m22, dx, dy) {
- var m = [
- [m11, m12, 0],
- [m21, m22, 0],
- [dx, dy, 1]
- ];
-
- setM(this, m, true);
- };
-
- /**
- * The text drawing function.
- * The maxWidth argument isn't taken in account, since no browser supports
- * it yet.
- */
- contextPrototype.drawText_ = function(text, x, y, maxWidth, stroke) {
- var m = this.m_,
- delta = 1000,
- left = 0,
- right = delta,
- offset = {x: 0, y: 0},
- lineStr = [];
-
- var fontStyle = getComputedStyle(processFontStyle(this.font),
- this.element_);
-
- var fontStyleString = buildStyle(fontStyle);
-
- var elementStyle = this.element_.currentStyle;
- var textAlign = this.textAlign.toLowerCase();
- switch (textAlign) {
- case 'left':
- case 'center':
- case 'right':
- break;
- case 'end':
- textAlign = elementStyle.direction == 'ltr' ? 'right' : 'left';
- break;
- case 'start':
- textAlign = elementStyle.direction == 'rtl' ? 'right' : 'left';
- break;
- default:
- textAlign = 'left';
- }
-
- // 1.75 is an arbitrary number, as there is no info about the text baseline
- switch (this.textBaseline) {
- case 'hanging':
- case 'top':
- offset.y = fontStyle.size / 1.75;
- break;
- case 'middle':
- break;
- default:
- case null:
- case 'alphabetic':
- case 'ideographic':
- case 'bottom':
- offset.y = -fontStyle.size / 2.25;
- break;
- }
-
- switch(textAlign) {
- case 'right':
- left = delta;
- right = 0.05;
- break;
- case 'center':
- left = right = delta / 2;
- break;
- }
-
- var d = this.getCoords_(x + offset.x, y + offset.y);
-
- lineStr.push('');
-
- if (stroke) {
- appendStroke(this, lineStr);
- } else {
- // TODO: Fix the min and max params.
- appendFill(this, lineStr, {x: -left, y: 0},
- {x: right, y: fontStyle.size});
- }
-
- var skewM = m[0][0].toFixed(3) + ',' + m[1][0].toFixed(3) + ',' +
- m[0][1].toFixed(3) + ',' + m[1][1].toFixed(3) + ',0,0';
-
- var skewOffset = mr(d.x / Z) + ',' + mr(d.y / Z);
-
- lineStr.push(' ',
- ' ',
- ' ');
-
- this.element_.insertAdjacentHTML('beforeEnd', lineStr.join(''));
- };
-
- contextPrototype.fillText = function(text, x, y, maxWidth) {
- this.drawText_(text, x, y, maxWidth, false);
- };
-
- contextPrototype.strokeText = function(text, x, y, maxWidth) {
- this.drawText_(text, x, y, maxWidth, true);
- };
-
- contextPrototype.measureText = function(text) {
- if (!this.textMeasureEl_) {
- var s = ' ';
- this.element_.insertAdjacentHTML('beforeEnd', s);
- this.textMeasureEl_ = this.element_.lastChild;
- }
- var doc = this.element_.ownerDocument;
- this.textMeasureEl_.innerHTML = '';
- this.textMeasureEl_.style.font = this.font;
- // Don't use innerHTML or innerText because they allow markup/whitespace.
- this.textMeasureEl_.appendChild(doc.createTextNode(text));
- return {width: this.textMeasureEl_.offsetWidth};
- };
-
- /******** STUBS ********/
- contextPrototype.clip = function() {
- // TODO: Implement
- };
-
- contextPrototype.arcTo = function() {
- // TODO: Implement
- };
-
- contextPrototype.createPattern = function(image, repetition) {
- return new CanvasPattern_(image, repetition);
- };
-
- // Gradient / Pattern Stubs
- function CanvasGradient_(aType) {
- this.type_ = aType;
- this.x0_ = 0;
- this.y0_ = 0;
- this.r0_ = 0;
- this.x1_ = 0;
- this.y1_ = 0;
- this.r1_ = 0;
- this.colors_ = [];
- }
-
- CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) {
- aColor = processStyle(aColor);
- this.colors_.push({offset: aOffset,
- color: aColor.color,
- alpha: aColor.alpha});
- };
-
- function CanvasPattern_(image, repetition) {
- assertImageIsValid(image);
- switch (repetition) {
- case 'repeat':
- case null:
- case '':
- this.repetition_ = 'repeat';
- break
- case 'repeat-x':
- case 'repeat-y':
- case 'no-repeat':
- this.repetition_ = repetition;
- break;
- default:
- throwException('SYNTAX_ERR');
- }
-
- this.src_ = image.src;
- this.width_ = image.width;
- this.height_ = image.height;
- }
-
- function throwException(s) {
- throw new DOMException_(s);
- }
-
- function assertImageIsValid(img) {
- if (!img || img.nodeType != 1 || img.tagName != 'IMG') {
- throwException('TYPE_MISMATCH_ERR');
- }
- if (img.readyState != 'complete') {
- throwException('INVALID_STATE_ERR');
- }
- }
-
- function DOMException_(s) {
- this.code = this[s];
- this.message = s +': DOM Exception ' + this.code;
- }
- var p = DOMException_.prototype = new Error;
- p.INDEX_SIZE_ERR = 1;
- p.DOMSTRING_SIZE_ERR = 2;
- p.HIERARCHY_REQUEST_ERR = 3;
- p.WRONG_DOCUMENT_ERR = 4;
- p.INVALID_CHARACTER_ERR = 5;
- p.NO_DATA_ALLOWED_ERR = 6;
- p.NO_MODIFICATION_ALLOWED_ERR = 7;
- p.NOT_FOUND_ERR = 8;
- p.NOT_SUPPORTED_ERR = 9;
- p.INUSE_ATTRIBUTE_ERR = 10;
- p.INVALID_STATE_ERR = 11;
- p.SYNTAX_ERR = 12;
- p.INVALID_MODIFICATION_ERR = 13;
- p.NAMESPACE_ERR = 14;
- p.INVALID_ACCESS_ERR = 15;
- p.VALIDATION_ERR = 16;
- p.TYPE_MISMATCH_ERR = 17;
-
- // set up externs
- G_vmlCanvasManager = G_vmlCanvasManager_;
- CanvasRenderingContext2D = CanvasRenderingContext2D_;
- CanvasGradient = CanvasGradient_;
- CanvasPattern = CanvasPattern_;
- DOMException = DOMException_;
-})();
-
-} // if
diff --git a/frontend/src/vendor/jquery.flot/excanvas.min.js b/frontend/src/vendor/jquery.flot/excanvas.min.js
deleted file mode 100644
index 12c74f7bea84..000000000000
--- a/frontend/src/vendor/jquery.flot/excanvas.min.js
+++ /dev/null
@@ -1 +0,0 @@
-if(!document.createElement("canvas").getContext){(function(){var z=Math;var K=z.round;var J=z.sin;var U=z.cos;var b=z.abs;var k=z.sqrt;var D=10;var F=D/2;function T(){return this.context_||(this.context_=new W(this))}var O=Array.prototype.slice;function G(i,j,m){var Z=O.call(arguments,2);return function(){return i.apply(j,Z.concat(O.call(arguments)))}}function AD(Z){return String(Z).replace(/&/g,"&").replace(/"/g,""")}function r(i){if(!i.namespaces.g_vml_){i.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml","#default#VML")}if(!i.namespaces.g_o_){i.namespaces.add("g_o_","urn:schemas-microsoft-com:office:office","#default#VML")}if(!i.styleSheets.ex_canvas_){var Z=i.createStyleSheet();Z.owningElement.id="ex_canvas_";Z.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}"}}r(document);var E={init:function(Z){if(/MSIE/.test(navigator.userAgent)&&!window.opera){var i=Z||document;i.createElement("canvas");i.attachEvent("onreadystatechange",G(this.init_,this,i))}},init_:function(m){var j=m.getElementsByTagName("canvas");for(var Z=0;Z1){j--}if(6*j<1){return i+(Z-i)*6*j}else{if(2*j<1){return Z}else{if(3*j<2){return i+(Z-i)*(2/3-j)*6}else{return i}}}}function Y(Z){var AE,p=1;Z=String(Z);if(Z.charAt(0)=="#"){AE=Z}else{if(/^rgb/.test(Z)){var m=g(Z);var AE="#",AF;for(var j=0;j<3;j++){if(m[j].indexOf("%")!=-1){AF=Math.floor(C(m[j])*255)}else{AF=Number(m[j])}AE+=I[N(AF,0,255)]}p=m[3]}else{if(/^hsl/.test(Z)){var m=g(Z);AE=c(m);p=m[3]}else{AE=B[Z]||Z}}}return{color:AE,alpha:p}}var L={style:"normal",variant:"normal",weight:"normal",size:10,family:"sans-serif"};var f={};function X(Z){if(f[Z]){return f[Z]}var m=document.createElement("div");var j=m.style;try{j.font=Z}catch(i){}return f[Z]={style:j.fontStyle||L.style,variant:j.fontVariant||L.variant,weight:j.fontWeight||L.weight,size:j.fontSize||L.size,family:j.fontFamily||L.family}}function P(j,i){var Z={};for(var AF in j){Z[AF]=j[AF]}var AE=parseFloat(i.currentStyle.fontSize),m=parseFloat(j.size);if(typeof j.size=="number"){Z.size=j.size}else{if(j.size.indexOf("px")!=-1){Z.size=m}else{if(j.size.indexOf("em")!=-1){Z.size=AE*m}else{if(j.size.indexOf("%")!=-1){Z.size=(AE/100)*m}else{if(j.size.indexOf("pt")!=-1){Z.size=m/0.75}else{Z.size=AE}}}}}Z.size*=0.981;return Z}function AA(Z){return Z.style+" "+Z.variant+" "+Z.weight+" "+Z.size+"px "+Z.family}function t(Z){switch(Z){case"butt":return"flat";case"round":return"round";case"square":default:return"square"}}function W(i){this.m_=V();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.strokeStyle="#000";this.fillStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=D*1;this.globalAlpha=1;this.font="10px sans-serif";this.textAlign="left";this.textBaseline="alphabetic";this.canvas=i;var Z=i.ownerDocument.createElement("div");Z.style.width=i.clientWidth+"px";Z.style.height=i.clientHeight+"px";Z.style.overflow="hidden";Z.style.position="absolute";i.appendChild(Z);this.element_=Z;this.arcScaleX_=1;this.arcScaleY_=1;this.lineScale_=1}var M=W.prototype;M.clearRect=function(){if(this.textMeasureEl_){this.textMeasureEl_.removeNode(true);this.textMeasureEl_=null}this.element_.innerHTML=""};M.beginPath=function(){this.currentPath_=[]};M.moveTo=function(i,Z){var j=this.getCoords_(i,Z);this.currentPath_.push({type:"moveTo",x:j.x,y:j.y});this.currentX_=j.x;this.currentY_=j.y};M.lineTo=function(i,Z){var j=this.getCoords_(i,Z);this.currentPath_.push({type:"lineTo",x:j.x,y:j.y});this.currentX_=j.x;this.currentY_=j.y};M.bezierCurveTo=function(j,i,AI,AH,AG,AE){var Z=this.getCoords_(AG,AE);var AF=this.getCoords_(j,i);var m=this.getCoords_(AI,AH);e(this,AF,m,Z)};function e(Z,m,j,i){Z.currentPath_.push({type:"bezierCurveTo",cp1x:m.x,cp1y:m.y,cp2x:j.x,cp2y:j.y,x:i.x,y:i.y});Z.currentX_=i.x;Z.currentY_=i.y}M.quadraticCurveTo=function(AG,j,i,Z){var AF=this.getCoords_(AG,j);var AE=this.getCoords_(i,Z);var AH={x:this.currentX_+2/3*(AF.x-this.currentX_),y:this.currentY_+2/3*(AF.y-this.currentY_)};var m={x:AH.x+(AE.x-this.currentX_)/3,y:AH.y+(AE.y-this.currentY_)/3};e(this,AH,m,AE)};M.arc=function(AJ,AH,AI,AE,i,j){AI*=D;var AN=j?"at":"wa";var AK=AJ+U(AE)*AI-F;var AM=AH+J(AE)*AI-F;var Z=AJ+U(i)*AI-F;var AL=AH+J(i)*AI-F;if(AK==Z&&!j){AK+=0.125}var m=this.getCoords_(AJ,AH);var AG=this.getCoords_(AK,AM);var AF=this.getCoords_(Z,AL);this.currentPath_.push({type:AN,x:m.x,y:m.y,radius:AI,xStart:AG.x,yStart:AG.y,xEnd:AF.x,yEnd:AF.y})};M.rect=function(j,i,Z,m){this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath()};M.strokeRect=function(j,i,Z,m){var p=this.currentPath_;this.beginPath();this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath();this.stroke();this.currentPath_=p};M.fillRect=function(j,i,Z,m){var p=this.currentPath_;this.beginPath();this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath();this.fill();this.currentPath_=p};M.createLinearGradient=function(i,m,Z,j){var p=new v("gradient");p.x0_=i;p.y0_=m;p.x1_=Z;p.y1_=j;return p};M.createRadialGradient=function(m,AE,j,i,p,Z){var AF=new v("gradientradial");AF.x0_=m;AF.y0_=AE;AF.r0_=j;AF.x1_=i;AF.y1_=p;AF.r1_=Z;return AF};M.drawImage=function(AO,j){var AH,AF,AJ,AV,AM,AK,AQ,AX;var AI=AO.runtimeStyle.width;var AN=AO.runtimeStyle.height;AO.runtimeStyle.width="auto";AO.runtimeStyle.height="auto";var AG=AO.width;var AT=AO.height;AO.runtimeStyle.width=AI;AO.runtimeStyle.height=AN;if(arguments.length==3){AH=arguments[1];AF=arguments[2];AM=AK=0;AQ=AJ=AG;AX=AV=AT}else{if(arguments.length==5){AH=arguments[1];AF=arguments[2];AJ=arguments[3];AV=arguments[4];AM=AK=0;AQ=AG;AX=AT}else{if(arguments.length==9){AM=arguments[1];AK=arguments[2];AQ=arguments[3];AX=arguments[4];AH=arguments[5];AF=arguments[6];AJ=arguments[7];AV=arguments[8]}else{throw Error("Invalid number of arguments")}}}var AW=this.getCoords_(AH,AF);var m=AQ/2;var i=AX/2;var AU=[];var Z=10;var AE=10;AU.push(" ',' "," ");this.element_.insertAdjacentHTML("BeforeEnd",AU.join(""))};M.stroke=function(AM){var m=10;var AN=10;var AE=5000;var AG={x:null,y:null};var AL={x:null,y:null};for(var AH=0;AHAL.x){AL.x=Z.x}if(AG.y==null||Z.yAL.y){AL.y=Z.y}}}AK.push(' ">');if(!AM){R(this,AK)}else{a(this,AK,AG,AL)}AK.push("");this.element_.insertAdjacentHTML("beforeEnd",AK.join(""))}};function R(j,AE){var i=Y(j.strokeStyle);var m=i.color;var p=i.alpha*j.globalAlpha;var Z=j.lineScale_*j.lineWidth;if(Z<1){p*=Z}AE.push(" ')}function a(AO,AG,Ah,AP){var AH=AO.fillStyle;var AY=AO.arcScaleX_;var AX=AO.arcScaleY_;var Z=AP.x-Ah.x;var m=AP.y-Ah.y;if(AH instanceof v){var AL=0;var Ac={x:0,y:0};var AU=0;var AK=1;if(AH.type_=="gradient"){var AJ=AH.x0_/AY;var j=AH.y0_/AX;var AI=AH.x1_/AY;var Aj=AH.y1_/AX;var Ag=AO.getCoords_(AJ,j);var Af=AO.getCoords_(AI,Aj);var AE=Af.x-Ag.x;var p=Af.y-Ag.y;AL=Math.atan2(AE,p)*180/Math.PI;if(AL<0){AL+=360}if(AL<0.000001){AL=0}}else{var Ag=AO.getCoords_(AH.x0_,AH.y0_);Ac={x:(Ag.x-Ah.x)/Z,y:(Ag.y-Ah.y)/m};Z/=AY*D;m/=AX*D;var Aa=z.max(Z,m);AU=2*AH.r0_/Aa;AK=2*AH.r1_/Aa-AU}var AS=AH.colors_;AS.sort(function(Ak,i){return Ak.offset-i.offset});var AN=AS.length;var AR=AS[0].color;var AQ=AS[AN-1].color;var AW=AS[0].alpha*AO.globalAlpha;var AV=AS[AN-1].alpha*AO.globalAlpha;var Ab=[];for(var Ae=0;Ae ')}else{if(AH instanceof u){if(Z&&m){var AF=-Ah.x;var AZ=-Ah.y;AG.push(" ')}}else{var Ai=Y(AO.fillStyle);var AT=Ai.color;var Ad=Ai.alpha*AO.globalAlpha;AG.push(' ')}}}M.fill=function(){this.stroke(true)};M.closePath=function(){this.currentPath_.push({type:"close"})};M.getCoords_=function(j,i){var Z=this.m_;return{x:D*(j*Z[0][0]+i*Z[1][0]+Z[2][0])-F,y:D*(j*Z[0][1]+i*Z[1][1]+Z[2][1])-F}};M.save=function(){var Z={};Q(this,Z);this.aStack_.push(Z);this.mStack_.push(this.m_);this.m_=d(V(),this.m_)};M.restore=function(){if(this.aStack_.length){Q(this.aStack_.pop(),this);this.m_=this.mStack_.pop()}};function H(Z){return isFinite(Z[0][0])&&isFinite(Z[0][1])&&isFinite(Z[1][0])&&isFinite(Z[1][1])&&isFinite(Z[2][0])&&isFinite(Z[2][1])}function y(i,Z,j){if(!H(Z)){return }i.m_=Z;if(j){var p=Z[0][0]*Z[1][1]-Z[0][1]*Z[1][0];i.lineScale_=k(b(p))}}M.translate=function(j,i){var Z=[[1,0,0],[0,1,0],[j,i,1]];y(this,d(Z,this.m_),false)};M.rotate=function(i){var m=U(i);var j=J(i);var Z=[[m,j,0],[-j,m,0],[0,0,1]];y(this,d(Z,this.m_),false)};M.scale=function(j,i){this.arcScaleX_*=j;this.arcScaleY_*=i;var Z=[[j,0,0],[0,i,0],[0,0,1]];y(this,d(Z,this.m_),true)};M.transform=function(p,m,AF,AE,i,Z){var j=[[p,m,0],[AF,AE,0],[i,Z,1]];y(this,d(j,this.m_),true)};M.setTransform=function(AE,p,AG,AF,j,i){var Z=[[AE,p,0],[AG,AF,0],[j,i,1]];y(this,Z,true)};M.drawText_=function(AK,AI,AH,AN,AG){var AM=this.m_,AQ=1000,i=0,AP=AQ,AF={x:0,y:0},AE=[];var Z=P(X(this.font),this.element_);var j=AA(Z);var AR=this.element_.currentStyle;var p=this.textAlign.toLowerCase();switch(p){case"left":case"center":case"right":break;case"end":p=AR.direction=="ltr"?"right":"left";break;case"start":p=AR.direction=="rtl"?"right":"left";break;default:p="left"}switch(this.textBaseline){case"hanging":case"top":AF.y=Z.size/1.75;break;case"middle":break;default:case null:case"alphabetic":case"ideographic":case"bottom":AF.y=-Z.size/2.25;break}switch(p){case"right":i=AQ;AP=0.05;break;case"center":i=AP=AQ/2;break}var AO=this.getCoords_(AI+AF.x,AH+AF.y);AE.push('');if(AG){R(this,AE)}else{a(this,AE,{x:-i,y:0},{x:AP,y:Z.size})}var AL=AM[0][0].toFixed(3)+","+AM[1][0].toFixed(3)+","+AM[0][1].toFixed(3)+","+AM[1][1].toFixed(3)+",0,0";var AJ=K(AO.x/D)+","+K(AO.y/D);AE.push(' ',' ',' ');this.element_.insertAdjacentHTML("beforeEnd",AE.join(""))};M.fillText=function(j,Z,m,i){this.drawText_(j,Z,m,i,false)};M.strokeText=function(j,Z,m,i){this.drawText_(j,Z,m,i,true)};M.measureText=function(j){if(!this.textMeasureEl_){var Z=' ';this.element_.insertAdjacentHTML("beforeEnd",Z);this.textMeasureEl_=this.element_.lastChild}var i=this.element_.ownerDocument;this.textMeasureEl_.innerHTML="";this.textMeasureEl_.style.font=this.font;this.textMeasureEl_.appendChild(i.createTextNode(j));return{width:this.textMeasureEl_.offsetWidth}};M.clip=function(){};M.arcTo=function(){};M.createPattern=function(i,Z){return new u(i,Z)};function v(Z){this.type_=Z;this.x0_=0;this.y0_=0;this.r0_=0;this.x1_=0;this.y1_=0;this.r1_=0;this.colors_=[]}v.prototype.addColorStop=function(i,Z){Z=Y(Z);this.colors_.push({offset:i,color:Z.color,alpha:Z.alpha})};function u(i,Z){q(i);switch(Z){case"repeat":case null:case"":this.repetition_="repeat";break;case"repeat-x":case"repeat-y":case"no-repeat":this.repetition_=Z;break;default:n("SYNTAX_ERR")}this.src_=i.src;this.width_=i.width;this.height_=i.height}function n(Z){throw new o(Z)}function q(Z){if(!Z||Z.nodeType!=1||Z.tagName!="IMG"){n("TYPE_MISMATCH_ERR")}if(Z.readyState!="complete"){n("INVALID_STATE_ERR")}}function o(Z){this.code=this[Z];this.message=Z+": DOM Exception "+this.code}var x=o.prototype=new Error;x.INDEX_SIZE_ERR=1;x.DOMSTRING_SIZE_ERR=2;x.HIERARCHY_REQUEST_ERR=3;x.WRONG_DOCUMENT_ERR=4;x.INVALID_CHARACTER_ERR=5;x.NO_DATA_ALLOWED_ERR=6;x.NO_MODIFICATION_ALLOWED_ERR=7;x.NOT_FOUND_ERR=8;x.NOT_SUPPORTED_ERR=9;x.INUSE_ATTRIBUTE_ERR=10;x.INVALID_STATE_ERR=11;x.SYNTAX_ERR=12;x.INVALID_MODIFICATION_ERR=13;x.NAMESPACE_ERR=14;x.INVALID_ACCESS_ERR=15;x.VALIDATION_ERR=16;x.TYPE_MISMATCH_ERR=17;G_vmlCanvasManager=E;CanvasRenderingContext2D=W;CanvasGradient=v;CanvasPattern=u;DOMException=o})()};
\ No newline at end of file
diff --git a/frontend/src/vendor/jquery.flot/jquery.colorhelpers.js b/frontend/src/vendor/jquery.flot/jquery.colorhelpers.js
deleted file mode 100644
index d3524d786f0a..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.colorhelpers.js
+++ /dev/null
@@ -1,179 +0,0 @@
-/* Plugin for jQuery for working with colors.
- *
- * Version 1.1.
- *
- * Inspiration from jQuery color animation plugin by John Resig.
- *
- * Released under the MIT license by Ole Laursen, October 2009.
- *
- * Examples:
- *
- * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
- * var c = $.color.extract($("#mydiv"), 'background-color');
- * console.log(c.r, c.g, c.b, c.a);
- * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
- *
- * Note that .scale() and .add() return the same modified object
- * instead of making a new one.
- *
- * V. 1.1: Fix error handling so e.g. parsing an empty string does
- * produce a color rather than just crashing.
- */
-
-(function($) {
- $.color = {};
-
- // construct color object with some convenient chainable helpers
- $.color.make = function (r, g, b, a) {
- var o = {};
- o.r = r || 0;
- o.g = g || 0;
- o.b = b || 0;
- o.a = a != null ? a : 1;
-
- o.add = function (c, d) {
- for (var i = 0; i < c.length; ++i)
- o[c.charAt(i)] += d;
- return o.normalize();
- };
-
- o.scale = function (c, f) {
- for (var i = 0; i < c.length; ++i)
- o[c.charAt(i)] *= f;
- return o.normalize();
- };
-
- o.toString = function () {
- if (o.a >= 1.0) {
- return "rgb("+[o.r, o.g, o.b].join(",")+")";
- } else {
- return "rgba("+[o.r, o.g, o.b, o.a].join(",")+")";
- }
- };
-
- o.normalize = function () {
- function clamp(min, value, max) {
- return value < min ? min: (value > max ? max: value);
- }
-
- o.r = clamp(0, parseInt(o.r), 255);
- o.g = clamp(0, parseInt(o.g), 255);
- o.b = clamp(0, parseInt(o.b), 255);
- o.a = clamp(0, o.a, 1);
- return o;
- };
-
- o.clone = function () {
- return $.color.make(o.r, o.b, o.g, o.a);
- };
-
- return o.normalize();
- }
-
- // extract CSS color property from element, going up in the DOM
- // if it's "transparent"
- $.color.extract = function (elem, css) {
- var c;
- do {
- c = elem.css(css).toLowerCase();
- // keep going until we find an element that has color, or
- // we hit the body
- if (c != '' && c != 'transparent')
- break;
- elem = elem.parent();
- } while (!$.nodeName(elem.get(0), "body"));
-
- // catch Safari's way of signalling transparent
- if (c == "rgba(0, 0, 0, 0)")
- c = "transparent";
-
- return $.color.parse(c);
- }
-
- // parse CSS color string (like "rgb(10, 32, 43)" or "#fff"),
- // returns color object, if parsing failed, you get black (0, 0,
- // 0) out
- $.color.parse = function (str) {
- var res, m = $.color.make;
-
- // Look for rgb(num,num,num)
- if (res = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))
- return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10));
-
- // Look for rgba(num,num,num,num)
- if (res = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))
- return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10), parseFloat(res[4]));
-
- // Look for rgb(num%,num%,num%)
- if (res = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))
- return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55);
-
- // Look for rgba(num%,num%,num%,num)
- if (res = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))
- return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55, parseFloat(res[4]));
-
- // Look for #a0b1c2
- if (res = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))
- return m(parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16));
-
- // Look for #fff
- if (res = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))
- return m(parseInt(res[1]+res[1], 16), parseInt(res[2]+res[2], 16), parseInt(res[3]+res[3], 16));
-
- // Otherwise, we're most likely dealing with a named color
- var name = $.trim(str).toLowerCase();
- if (name == "transparent")
- return m(255, 255, 255, 0);
- else {
- // default to black
- res = lookupColors[name] || [0, 0, 0];
- return m(res[0], res[1], res[2]);
- }
- }
-
- var lookupColors = {
- aqua:[0,255,255],
- azure:[240,255,255],
- beige:[245,245,220],
- black:[0,0,0],
- blue:[0,0,255],
- brown:[165,42,42],
- cyan:[0,255,255],
- darkblue:[0,0,139],
- darkcyan:[0,139,139],
- darkgrey:[169,169,169],
- darkgreen:[0,100,0],
- darkkhaki:[189,183,107],
- darkmagenta:[139,0,139],
- darkolivegreen:[85,107,47],
- darkorange:[255,140,0],
- darkorchid:[153,50,204],
- darkred:[139,0,0],
- darksalmon:[233,150,122],
- darkviolet:[148,0,211],
- fuchsia:[255,0,255],
- gold:[255,215,0],
- green:[0,128,0],
- indigo:[75,0,130],
- khaki:[240,230,140],
- lightblue:[173,216,230],
- lightcyan:[224,255,255],
- lightgreen:[144,238,144],
- lightgrey:[211,211,211],
- lightpink:[255,182,193],
- lightyellow:[255,255,224],
- lime:[0,255,0],
- magenta:[255,0,255],
- maroon:[128,0,0],
- navy:[0,0,128],
- olive:[128,128,0],
- orange:[255,165,0],
- pink:[255,192,203],
- purple:[128,0,128],
- violet:[128,0,128],
- red:[255,0,0],
- silver:[192,192,192],
- white:[255,255,255],
- yellow:[255,255,0]
- };
-})(jQuery);
diff --git a/frontend/src/vendor/jquery.flot/jquery.colorhelpers.min.js b/frontend/src/vendor/jquery.flot/jquery.colorhelpers.min.js
deleted file mode 100644
index 7f44c57b560c..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.colorhelpers.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(function(b){b.color={};b.color.make=function(f,e,c,d){var h={};h.r=f||0;h.g=e||0;h.b=c||0;h.a=d!=null?d:1;h.add=function(k,j){for(var g=0;g=1){return"rgb("+[h.r,h.g,h.b].join(",")+")"}else{return"rgba("+[h.r,h.g,h.b,h.a].join(",")+")"}};h.normalize=function(){function g(j,k,i){return ki?i:k)}h.r=g(0,parseInt(h.r),255);h.g=g(0,parseInt(h.g),255);h.b=g(0,parseInt(h.b),255);h.a=g(0,h.a,1);return h};h.clone=function(){return b.color.make(h.r,h.b,h.g,h.a)};return h.normalize()};b.color.extract=function(e,d){var f;do{f=e.css(d).toLowerCase();if(f!=""&&f!="transparent"){break}e=e.parent()}while(!b.nodeName(e.get(0),"body"));if(f=="rgba(0, 0, 0, 0)"){f="transparent"}return b.color.parse(f)};b.color.parse=function(f){var e,c=b.color.make;if(e=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(f)){return c(parseInt(e[1],10),parseInt(e[2],10),parseInt(e[3],10))}if(e=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(f)){return c(parseInt(e[1],10),parseInt(e[2],10),parseInt(e[3],10),parseFloat(e[4]))}if(e=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(f)){return c(parseFloat(e[1])*2.55,parseFloat(e[2])*2.55,parseFloat(e[3])*2.55)}if(e=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(f)){return c(parseFloat(e[1])*2.55,parseFloat(e[2])*2.55,parseFloat(e[3])*2.55,parseFloat(e[4]))}if(e=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(f)){return c(parseInt(e[1],16),parseInt(e[2],16),parseInt(e[3],16))}if(e=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(f)){return c(parseInt(e[1]+e[1],16),parseInt(e[2]+e[2],16),parseInt(e[3]+e[3],16))}var d=b.trim(f).toLowerCase();if(d=="transparent"){return c(255,255,255,0)}else{e=a[d]||[0,0,0];return c(e[0],e[1],e[2])}};var a={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);
\ No newline at end of file
diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.crosshair.js b/frontend/src/vendor/jquery.flot/jquery.flot.crosshair.js
deleted file mode 100644
index 1d433f0074d1..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.flot.crosshair.js
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
-Flot plugin for showing crosshairs, thin lines, when the mouse hovers
-over the plot.
-
- crosshair: {
- mode: null or "x" or "y" or "xy"
- color: color
- lineWidth: number
- }
-
-Set the mode to one of "x", "y" or "xy". The "x" mode enables a
-vertical crosshair that lets you trace the values on the x axis, "y"
-enables a horizontal crosshair and "xy" enables them both. "color" is
-the color of the crosshair (default is "rgba(170, 0, 0, 0.80)"),
-"lineWidth" is the width of the drawn lines (default is 1).
-
-The plugin also adds four public methods:
-
- - setCrosshair(pos)
-
- Set the position of the crosshair. Note that this is cleared if
- the user moves the mouse. "pos" is in coordinates of the plot and
- should be on the form { x: xpos, y: ypos } (you can use x2/x3/...
- if you're using multiple axes), which is coincidentally the same
- format as what you get from a "plothover" event. If "pos" is null,
- the crosshair is cleared.
-
- - clearCrosshair()
-
- Clear the crosshair.
-
- - lockCrosshair(pos)
-
- Cause the crosshair to lock to the current location, no longer
- updating if the user moves the mouse. Optionally supply a position
- (passed on to setCrosshair()) to move it to.
-
- Example usage:
- var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } };
- $("#graph").bind("plothover", function (evt, position, item) {
- if (item) {
- // Lock the crosshair to the data point being hovered
- myFlot.lockCrosshair({ x: item.datapoint[0], y: item.datapoint[1] });
- }
- else {
- // Return normal crosshair operation
- myFlot.unlockCrosshair();
- }
- });
-
- - unlockCrosshair()
-
- Free the crosshair to move again after locking it.
-*/
-
-(function ($) {
- var options = {
- crosshair: {
- mode: null, // one of null, "x", "y" or "xy",
- color: "rgba(170, 0, 0, 0.80)",
- lineWidth: 1
- }
- };
-
- function init(plot) {
- // position of crosshair in pixels
- var crosshair = { x: -1, y: -1, locked: false };
-
- plot.setCrosshair = function setCrosshair(pos) {
- if (!pos)
- crosshair.x = -1;
- else {
- var o = plot.p2c(pos);
- crosshair.x = Math.max(0, Math.min(o.left, plot.width()));
- crosshair.y = Math.max(0, Math.min(o.top, plot.height()));
- }
-
- plot.triggerRedrawOverlay();
- };
-
- plot.clearCrosshair = plot.setCrosshair; // passes null for pos
-
- plot.lockCrosshair = function lockCrosshair(pos) {
- if (pos)
- plot.setCrosshair(pos);
- crosshair.locked = true;
- }
-
- plot.unlockCrosshair = function unlockCrosshair() {
- crosshair.locked = false;
- }
-
- function onMouseOut(e) {
- if (crosshair.locked)
- return;
-
- if (crosshair.x != -1) {
- crosshair.x = -1;
- plot.triggerRedrawOverlay();
- }
- }
-
- function onMouseMove(e) {
- if (crosshair.locked)
- return;
-
- if (plot.getSelection && plot.getSelection()) {
- crosshair.x = -1; // hide the crosshair while selecting
- return;
- }
-
- var offset = plot.offset();
- crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width()));
- crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height()));
- plot.triggerRedrawOverlay();
- }
-
- plot.hooks.bindEvents.push(function (plot, eventHolder) {
- if (!plot.getOptions().crosshair.mode)
- return;
-
- eventHolder.mouseout(onMouseOut);
- eventHolder.mousemove(onMouseMove);
- });
-
- plot.hooks.drawOverlay.push(function (plot, ctx) {
- var c = plot.getOptions().crosshair;
- if (!c.mode)
- return;
-
- var plotOffset = plot.getPlotOffset();
-
- ctx.save();
- ctx.translate(plotOffset.left, plotOffset.top);
-
- if (crosshair.x != -1) {
- ctx.strokeStyle = c.color;
- ctx.lineWidth = c.lineWidth;
- ctx.lineJoin = "round";
-
- ctx.beginPath();
- if (c.mode.indexOf("x") != -1) {
- ctx.moveTo(crosshair.x, 0);
- ctx.lineTo(crosshair.x, plot.height());
- }
- if (c.mode.indexOf("y") != -1) {
- ctx.moveTo(0, crosshair.y);
- ctx.lineTo(plot.width(), crosshair.y);
- }
- ctx.stroke();
- }
- ctx.restore();
- });
-
- plot.hooks.shutdown.push(function (plot, eventHolder) {
- eventHolder.unbind("mouseout", onMouseOut);
- eventHolder.unbind("mousemove", onMouseMove);
- });
- }
-
- $.plot.plugins.push({
- init: init,
- options: options,
- name: 'crosshair',
- version: '1.0'
- });
-})(jQuery);
diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.crosshair.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.crosshair.min.js
deleted file mode 100644
index ccaf240366ac..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.flot.crosshair.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(function(b){var a={crosshair:{mode:null,color:"rgba(170, 0, 0, 0.80)",lineWidth:1}};function c(h){var j={x:-1,y:-1,locked:false};h.setCrosshair=function e(l){if(!l){j.x=-1}else{var k=h.p2c(l);j.x=Math.max(0,Math.min(k.left,h.width()));j.y=Math.max(0,Math.min(k.top,h.height()))}h.triggerRedrawOverlay()};h.clearCrosshair=h.setCrosshair;h.lockCrosshair=function f(k){if(k){h.setCrosshair(k)}j.locked=true};h.unlockCrosshair=function g(){j.locked=false};function d(k){if(j.locked){return}if(j.x!=-1){j.x=-1;h.triggerRedrawOverlay()}}function i(k){if(j.locked){return}if(h.getSelection&&h.getSelection()){j.x=-1;return}var l=h.offset();j.x=Math.max(0,Math.min(k.pageX-l.left,h.width()));j.y=Math.max(0,Math.min(k.pageY-l.top,h.height()));h.triggerRedrawOverlay()}h.hooks.bindEvents.push(function(l,k){if(!l.getOptions().crosshair.mode){return}k.mouseout(d);k.mousemove(i)});h.hooks.drawOverlay.push(function(m,k){var n=m.getOptions().crosshair;if(!n.mode){return}var l=m.getPlotOffset();k.save();k.translate(l.left,l.top);if(j.x!=-1){k.strokeStyle=n.color;k.lineWidth=n.lineWidth;k.lineJoin="round";k.beginPath();if(n.mode.indexOf("x")!=-1){k.moveTo(j.x,0);k.lineTo(j.x,m.height())}if(n.mode.indexOf("y")!=-1){k.moveTo(0,j.y);k.lineTo(m.width(),j.y)}k.stroke()}k.restore()});h.hooks.shutdown.push(function(l,k){k.unbind("mouseout",d);k.unbind("mousemove",i)})}b.plot.plugins.push({init:c,options:a,name:"crosshair",version:"1.0"})})(jQuery);
\ No newline at end of file
diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.fillbetween.js b/frontend/src/vendor/jquery.flot/jquery.flot.fillbetween.js
deleted file mode 100644
index 69700e79ce66..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.flot.fillbetween.js
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
-Flot plugin for computing bottoms for filled line and bar charts.
-
-The case: you've got two series that you want to fill the area
-between. In Flot terms, you need to use one as the fill bottom of the
-other. You can specify the bottom of each data point as the third
-coordinate manually, or you can use this plugin to compute it for you.
-
-In order to name the other series, you need to give it an id, like this
-
- var dataset = [
- { data: [ ... ], id: "foo" } , // use default bottom
- { data: [ ... ], fillBetween: "foo" }, // use first dataset as bottom
- ];
-
- $.plot($("#placeholder"), dataset, { line: { show: true, fill: true }});
-
-As a convenience, if the id given is a number that doesn't appear as
-an id in the series, it is interpreted as the index in the array
-instead (so fillBetween: 0 can also mean the first series).
-
-Internally, the plugin modifies the datapoints in each series. For
-line series, extra data points might be inserted through
-interpolation. Note that at points where the bottom line is not
-defined (due to a null point or start/end of line), the current line
-will show a gap too. The algorithm comes from the jquery.flot.stack.js
-plugin, possibly some code could be shared.
-*/
-
-(function ($) {
- var options = {
- series: { fillBetween: null } // or number
- };
-
- function init(plot) {
- function findBottomSeries(s, allseries) {
- var i;
- for (i = 0; i < allseries.length; ++i) {
- if (allseries[i].id == s.fillBetween)
- return allseries[i];
- }
-
- if (typeof s.fillBetween == "number") {
- i = s.fillBetween;
-
- if (i < 0 || i >= allseries.length)
- return null;
-
- return allseries[i];
- }
-
- return null;
- }
-
- function computeFillBottoms(plot, s, datapoints) {
- if (s.fillBetween == null)
- return;
-
- var other = findBottomSeries(s, plot.getData());
- if (!other)
- return;
-
- var ps = datapoints.pointsize,
- points = datapoints.points,
- otherps = other.datapoints.pointsize,
- otherpoints = other.datapoints.points,
- newpoints = [],
- px, py, intery, qx, qy, bottom,
- withlines = s.lines.show,
- withbottom = ps > 2 && datapoints.format[2].y,
- withsteps = withlines && s.lines.steps,
- fromgap = true,
- i = 0, j = 0, l;
-
- while (true) {
- if (i >= points.length)
- break;
-
- l = newpoints.length;
-
- if (points[i] == null) {
- // copy gaps
- for (m = 0; m < ps; ++m)
- newpoints.push(points[i + m]);
- i += ps;
- }
- else if (j >= otherpoints.length) {
- // for lines, we can't use the rest of the points
- if (!withlines) {
- for (m = 0; m < ps; ++m)
- newpoints.push(points[i + m]);
- }
- i += ps;
- }
- else if (otherpoints[j] == null) {
- // oops, got a gap
- for (m = 0; m < ps; ++m)
- newpoints.push(null);
- fromgap = true;
- j += otherps;
- }
- else {
- // cases where we actually got two points
- px = points[i];
- py = points[i + 1];
- qx = otherpoints[j];
- qy = otherpoints[j + 1];
- bottom = 0;
-
- if (px == qx) {
- for (m = 0; m < ps; ++m)
- newpoints.push(points[i + m]);
-
- //newpoints[l + 1] += qy;
- bottom = qy;
-
- i += ps;
- j += otherps;
- }
- else if (px > qx) {
- // we got past point below, might need to
- // insert interpolated extra point
- if (withlines && i > 0 && points[i - ps] != null) {
- intery = py + (points[i - ps + 1] - py) * (qx - px) / (points[i - ps] - px);
- newpoints.push(qx);
- newpoints.push(intery)
- for (m = 2; m < ps; ++m)
- newpoints.push(points[i + m]);
- bottom = qy;
- }
-
- j += otherps;
- }
- else { // px < qx
- if (fromgap && withlines) {
- // if we come from a gap, we just skip this point
- i += ps;
- continue;
- }
-
- for (m = 0; m < ps; ++m)
- newpoints.push(points[i + m]);
-
- // we might be able to interpolate a point below,
- // this can give us a better y
- if (withlines && j > 0 && otherpoints[j - otherps] != null)
- bottom = qy + (otherpoints[j - otherps + 1] - qy) * (px - qx) / (otherpoints[j - otherps] - qx);
-
- //newpoints[l + 1] += bottom;
-
- i += ps;
- }
-
- fromgap = false;
-
- if (l != newpoints.length && withbottom)
- newpoints[l + 2] = bottom;
- }
-
- // maintain the line steps invariant
- if (withsteps && l != newpoints.length && l > 0
- && newpoints[l] != null
- && newpoints[l] != newpoints[l - ps]
- && newpoints[l + 1] != newpoints[l - ps + 1]) {
- for (m = 0; m < ps; ++m)
- newpoints[l + ps + m] = newpoints[l + m];
- newpoints[l + 1] = newpoints[l - ps + 1];
- }
- }
-
- datapoints.points = newpoints;
- }
-
- plot.hooks.processDatapoints.push(computeFillBottoms);
- }
-
- $.plot.plugins.push({
- init: init,
- options: options,
- name: 'fillbetween',
- version: '1.0'
- });
-})(jQuery);
diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.fillbetween.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.fillbetween.min.js
deleted file mode 100644
index 47f3dfb6de04..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.flot.fillbetween.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(function(b){var a={series:{fillBetween:null}};function c(f){function d(j,h){var g;for(g=0;g=h.length){return null}return h[g]}return null}function e(B,u,g){if(u.fillBetween==null){return}var p=d(u,B.getData());if(!p){return}var y=g.pointsize,E=g.points,h=p.datapoints.pointsize,x=p.datapoints.points,r=[],w,v,k,G,F,q,t=u.lines.show,o=y>2&&g.format[2].y,n=t&&u.lines.steps,D=true,C=0,A=0,z;while(true){if(C>=E.length){break}z=r.length;if(E[C]==null){for(m=0;m=x.length){if(!t){for(m=0;mG){if(t&&C>0&&E[C-y]!=null){k=v+(E[C-y+1]-v)*(G-w)/(E[C-y]-w);r.push(G);r.push(k);for(m=2;m0&&x[A-h]!=null){q=F+(x[A-h+1]-F)*(w-G)/(x[A-h]-G)}C+=y}}D=false;if(z!=r.length&&o){r[z+2]=q}}}}if(n&&z!=r.length&&z>0&&r[z]!=null&&r[z]!=r[z-y]&&r[z+1]!=r[z-y+1]){for(m=0;m ').load(handler).error(handler).attr('src', url);
- });
- }
-
- function drawSeries(plot, ctx, series) {
- var plotOffset = plot.getPlotOffset();
-
- if (!series.images || !series.images.show)
- return;
-
- var points = series.datapoints.points,
- ps = series.datapoints.pointsize;
-
- for (var i = 0; i < points.length; i += ps) {
- var img = points[i],
- x1 = points[i + 1], y1 = points[i + 2],
- x2 = points[i + 3], y2 = points[i + 4],
- xaxis = series.xaxis, yaxis = series.yaxis,
- tmp;
-
- // actually we should check img.complete, but it
- // appears to be a somewhat unreliable indicator in
- // IE6 (false even after load event)
- if (!img || img.width <= 0 || img.height <= 0)
- continue;
-
- if (x1 > x2) {
- tmp = x2;
- x2 = x1;
- x1 = tmp;
- }
- if (y1 > y2) {
- tmp = y2;
- y2 = y1;
- y1 = tmp;
- }
-
- // if the anchor is at the center of the pixel, expand the
- // image by 1/2 pixel in each direction
- if (series.images.anchor == "center") {
- tmp = 0.5 * (x2-x1) / (img.width - 1);
- x1 -= tmp;
- x2 += tmp;
- tmp = 0.5 * (y2-y1) / (img.height - 1);
- y1 -= tmp;
- y2 += tmp;
- }
-
- // clip
- if (x1 == x2 || y1 == y2 ||
- x1 >= xaxis.max || x2 <= xaxis.min ||
- y1 >= yaxis.max || y2 <= yaxis.min)
- continue;
-
- var sx1 = 0, sy1 = 0, sx2 = img.width, sy2 = img.height;
- if (x1 < xaxis.min) {
- sx1 += (sx2 - sx1) * (xaxis.min - x1) / (x2 - x1);
- x1 = xaxis.min;
- }
-
- if (x2 > xaxis.max) {
- sx2 += (sx2 - sx1) * (xaxis.max - x2) / (x2 - x1);
- x2 = xaxis.max;
- }
-
- if (y1 < yaxis.min) {
- sy2 += (sy1 - sy2) * (yaxis.min - y1) / (y2 - y1);
- y1 = yaxis.min;
- }
-
- if (y2 > yaxis.max) {
- sy1 += (sy1 - sy2) * (yaxis.max - y2) / (y2 - y1);
- y2 = yaxis.max;
- }
-
- x1 = xaxis.p2c(x1);
- x2 = xaxis.p2c(x2);
- y1 = yaxis.p2c(y1);
- y2 = yaxis.p2c(y2);
-
- // the transformation may have swapped us
- if (x1 > x2) {
- tmp = x2;
- x2 = x1;
- x1 = tmp;
- }
- if (y1 > y2) {
- tmp = y2;
- y2 = y1;
- y1 = tmp;
- }
-
- tmp = ctx.globalAlpha;
- ctx.globalAlpha *= series.images.alpha;
- ctx.drawImage(img,
- sx1, sy1, sx2 - sx1, sy2 - sy1,
- x1 + plotOffset.left, y1 + plotOffset.top,
- x2 - x1, y2 - y1);
- ctx.globalAlpha = tmp;
- }
- }
-
- function processRawData(plot, series, data, datapoints) {
- if (!series.images.show)
- return;
-
- // format is Image, x1, y1, x2, y2 (opposite corners)
- datapoints.format = [
- { required: true },
- { x: true, number: true, required: true },
- { y: true, number: true, required: true },
- { x: true, number: true, required: true },
- { y: true, number: true, required: true }
- ];
- }
-
- function init(plot) {
- plot.hooks.processRawData.push(processRawData);
- plot.hooks.drawSeries.push(drawSeries);
- }
-
- $.plot.plugins.push({
- init: init,
- options: options,
- name: 'image',
- version: '1.1'
- });
-})(jQuery);
diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.image.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.image.min.js
deleted file mode 100644
index 9480c1e7a319..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.flot.image.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(function(c){var a={series:{images:{show:false,alpha:1,anchor:"corner"}}};c.plot.image={};c.plot.image.loadDataImages=function(g,f,k){var j=[],h=[];var i=f.series.images.show;c.each(g,function(l,m){if(!(i||m.images.show)){return}if(m.data){m=m.data}c.each(m,function(n,o){if(typeof o[0]=="string"){j.push(o[0]);h.push(o)}})});c.plot.image.load(j,function(l){c.each(h,function(n,o){var m=o[0];if(l[m]){o[0]=l[m]}});k()})};c.plot.image.load=function(h,i){var g=h.length,f={};if(g==0){i({})}c.each(h,function(k,j){var l=function(){--g;f[j]=this;if(g==0){i(f)}};c(" ").load(l).error(l).attr("src",j)})};function d(q,o,l){var m=q.getPlotOffset();if(!l.images||!l.images.show){return}var r=l.datapoints.points,n=l.datapoints.pointsize;for(var t=0;tv){x=v;v=w;w=x}if(g>f){x=f;f=g;g=x}if(l.images.anchor=="center"){x=0.5*(v-w)/(y.width-1);w-=x;v+=x;x=0.5*(f-g)/(y.height-1);g-=x;f+=x}if(w==v||g==f||w>=h.max||v<=h.min||g>=u.max||f<=u.min){continue}var k=0,s=0,j=y.width,p=y.height;if(wh.max){j+=(j-k)*(h.max-v)/(v-w);v=h.max}if(gu.max){s+=(s-p)*(u.max-f)/(f-g);f=u.max}w=h.p2c(w);v=h.p2c(v);g=u.p2c(g);f=u.p2c(f);if(w>v){x=v;v=w;w=x}if(g>f){x=f;f=g;g=x}x=o.globalAlpha;o.globalAlpha*=l.images.alpha;o.drawImage(y,k,s,j-k,p-s,w+m.left,g+m.top,v-w,f-g);o.globalAlpha=x}}function b(i,f,g,h){if(!f.images.show){return}h.format=[{required:true},{x:true,number:true,required:true},{y:true,number:true,required:true},{x:true,number:true,required:true},{y:true,number:true,required:true}]}function e(f){f.hooks.processRawData.push(b);f.hooks.drawSeries.push(d)}c.plot.plugins.push({init:e,options:a,name:"image",version:"1.1"})})(jQuery);
\ No newline at end of file
diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.js b/frontend/src/vendor/jquery.flot/jquery.flot.js
deleted file mode 100644
index 28abf7f5c8a2..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.flot.js
+++ /dev/null
@@ -1,2599 +0,0 @@
-/*! Javascript plotting library for jQuery, v. 0.7.
- *
- * Released under the MIT license by IOLA, December 2007.
- *
- */
-
-// first an inline dependency, jquery.colorhelpers.js, we inline it here
-// for convenience
-
-/* Plugin for jQuery for working with colors.
- *
- * Version 1.1.
- *
- * Inspiration from jQuery color animation plugin by John Resig.
- *
- * Released under the MIT license by Ole Laursen, October 2009.
- *
- * Examples:
- *
- * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
- * var c = $.color.extract($("#mydiv"), 'background-color');
- * console.log(c.r, c.g, c.b, c.a);
- * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
- *
- * Note that .scale() and .add() return the same modified object
- * instead of making a new one.
- *
- * V. 1.1: Fix error handling so e.g. parsing an empty string does
- * produce a color rather than just crashing.
- */
-(function(B){B.color={};B.color.make=function(F,E,C,D){var G={};G.r=F||0;G.g=E||0;G.b=C||0;G.a=D!=null?D:1;G.add=function(J,I){for(var H=0;H=1){return"rgb("+[G.r,G.g,G.b].join(",")+")"}else{return"rgba("+[G.r,G.g,G.b,G.a].join(",")+")"}};G.normalize=function(){function H(J,K,I){return KI?I:K)}G.r=H(0,parseInt(G.r),255);G.g=H(0,parseInt(G.g),255);G.b=H(0,parseInt(G.b),255);G.a=H(0,G.a,1);return G};G.clone=function(){return B.color.make(G.r,G.b,G.g,G.a)};return G.normalize()};B.color.extract=function(D,C){var E;do{E=D.css(C).toLowerCase();if(E!=""&&E!="transparent"){break}D=D.parent()}while(!B.nodeName(D.get(0),"body"));if(E=="rgba(0, 0, 0, 0)"){E="transparent"}return B.color.parse(E)};B.color.parse=function(F){var E,C=B.color.make;if(E=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10))}if(E=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10),parseFloat(E[4]))}if(E=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55)}if(E=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55,parseFloat(E[4]))}if(E=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(F)){return C(parseInt(E[1],16),parseInt(E[2],16),parseInt(E[3],16))}if(E=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(F)){return C(parseInt(E[1]+E[1],16),parseInt(E[2]+E[2],16),parseInt(E[3]+E[3],16))}var D=B.trim(F).toLowerCase();if(D=="transparent"){return C(255,255,255,0)}else{E=A[D]||[0,0,0];return C(E[0],E[1],E[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);
-
-// the actual Flot code
-(function($) {
- function Plot(placeholder, data_, options_, plugins) {
- // data is on the form:
- // [ series1, series2 ... ]
- // where series is either just the data as [ [x1, y1], [x2, y2], ... ]
- // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }
-
- var series = [],
- options = {
- // the color theme used for graphs
- colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],
- legend: {
- show: true,
- noColumns: 1, // number of colums in legend table
- labelFormatter: null, // fn: string -> string
- labelBoxBorderColor: "#ccc", // border color for the little label boxes
- container: null, // container (as jQuery object) to put legend in, null means default on top of graph
- position: "ne", // position of default legend container within plot
- margin: 5, // distance from grid edge to default legend container within plot
- backgroundColor: null, // null means auto-detect
- backgroundOpacity: 0.85 // set to 0 to avoid background
- },
- xaxis: {
- show: null, // null = auto-detect, true = always, false = never
- position: "bottom", // or "top"
- mode: null, // null or "time"
- color: null, // base color, labels, ticks
- tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)"
- transform: null, // null or f: number -> number to transform axis
- inverseTransform: null, // if transform is set, this should be the inverse function
- min: null, // min. value to show, null means set automatically
- max: null, // max. value to show, null means set automatically
- autoscaleMargin: null, // margin in % to add if auto-setting min/max
- ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks
- tickFormatter: null, // fn: number -> string
- labelWidth: null, // size of tick labels in pixels
- labelHeight: null,
- reserveSpace: null, // whether to reserve space even if axis isn't shown
- tickLength: null, // size in pixels of ticks, or "full" for whole line
- alignTicksWithAxis: null, // axis number or null for no sync
-
- // mode specific options
- tickDecimals: null, // no. of decimals, null means auto
- tickSize: null, // number or [number, "unit"]
- minTickSize: null, // number or [number, "unit"]
- monthNames: null, // list of names of months
- timeformat: null, // format string to use
- twelveHourClock: false // 12 or 24 time in time mode
- },
- yaxis: {
- autoscaleMargin: 0.02,
- position: "left" // or "right"
- },
- xaxes: [],
- yaxes: [],
- series: {
- points: {
- show: false,
- radius: 3,
- lineWidth: 2, // in pixels
- fill: true,
- fillColor: "#ffffff",
- symbol: "circle" // or callback
- },
- lines: {
- // we don't put in show: false so we can see
- // whether lines were actively disabled
- lineWidth: 2, // in pixels
- fill: false,
- fillColor: null,
- steps: false
- },
- bars: {
- show: false,
- lineWidth: 2, // in pixels
- barWidth: 1, // in units of the x axis
- fill: true,
- fillColor: null,
- align: "left", // or "center"
- horizontal: false
- },
- shadowSize: 3
- },
- grid: {
- show: true,
- aboveData: false,
- color: "#545454", // primary color used for outline and labels
- backgroundColor: null, // null for transparent, else color
- borderColor: null, // set if different from the grid color
- tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)"
- labelMargin: 5, // in pixels
- axisMargin: 8, // in pixels
- borderWidth: 2, // in pixels
- minBorderMargin: null, // in pixels, null means taken from points radius
- markings: null, // array of ranges or fn: axes -> array of ranges
- markingsColor: "#f4f4f4",
- markingsLineWidth: 2,
- // interactive stuff
- clickable: false,
- hoverable: false,
- autoHighlight: true, // highlight in case mouse is near
- mouseActiveRadius: 10 // how far the mouse can be away to activate an item
- },
- hooks: {}
- },
- canvas = null, // the canvas for the plot itself
- overlay = null, // canvas for interactive stuff on top of plot
- eventHolder = null, // jQuery object that events should be bound to
- ctx = null, octx = null,
- xaxes = [], yaxes = [],
- plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
- canvasWidth = 0, canvasHeight = 0,
- plotWidth = 0, plotHeight = 0,
- hooks = {
- processOptions: [],
- processRawData: [],
- processDatapoints: [],
- drawSeries: [],
- draw: [],
- bindEvents: [],
- drawOverlay: [],
- shutdown: []
- },
- plot = this;
-
- // public functions
- plot.setData = setData;
- plot.setupGrid = setupGrid;
- plot.draw = draw;
- plot.getPlaceholder = function() { return placeholder; };
- plot.getCanvas = function() { return canvas; };
- plot.getPlotOffset = function() { return plotOffset; };
- plot.width = function () { return plotWidth; };
- plot.height = function () { return plotHeight; };
- plot.offset = function () {
- var o = eventHolder.offset();
- o.left += plotOffset.left;
- o.top += plotOffset.top;
- return o;
- };
- plot.getData = function () { return series; };
- plot.getAxes = function () {
- var res = {}, i;
- $.each(xaxes.concat(yaxes), function (_, axis) {
- if (axis)
- res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis;
- });
- return res;
- };
- plot.getXAxes = function () { return xaxes; };
- plot.getYAxes = function () { return yaxes; };
- plot.c2p = canvasToAxisCoords;
- plot.p2c = axisToCanvasCoords;
- plot.getOptions = function () { return options; };
- plot.highlight = highlight;
- plot.unhighlight = unhighlight;
- plot.triggerRedrawOverlay = triggerRedrawOverlay;
- plot.pointOffset = function(point) {
- return {
- left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left),
- top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top)
- };
- };
- plot.shutdown = shutdown;
- plot.resize = function () {
- getCanvasDimensions();
- resizeCanvas(canvas);
- resizeCanvas(overlay);
- };
-
- // public attributes
- plot.hooks = hooks;
-
- // initialize
- initPlugins(plot);
- parseOptions(options_);
- setupCanvases();
- setData(data_);
- setupGrid();
- draw();
- bindEvents();
-
-
- function executeHooks(hook, args) {
- args = [plot].concat(args);
- for (var i = 0; i < hook.length; ++i)
- hook[i].apply(this, args);
- }
-
- function initPlugins() {
- for (var i = 0; i < plugins.length; ++i) {
- var p = plugins[i];
- p.init(plot);
- if (p.options)
- $.extend(true, options, p.options);
- }
- }
-
- function parseOptions(opts) {
- var i;
-
- $.extend(true, options, opts);
-
- if (options.xaxis.color == null)
- options.xaxis.color = options.grid.color;
- if (options.yaxis.color == null)
- options.yaxis.color = options.grid.color;
-
- if (options.xaxis.tickColor == null) // backwards-compatibility
- options.xaxis.tickColor = options.grid.tickColor;
- if (options.yaxis.tickColor == null) // backwards-compatibility
- options.yaxis.tickColor = options.grid.tickColor;
-
- if (options.grid.borderColor == null)
- options.grid.borderColor = options.grid.color;
- if (options.grid.tickColor == null)
- options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString();
-
- // fill in defaults in axes, copy at least always the
- // first as the rest of the code assumes it'll be there
- for (i = 0; i < Math.max(1, options.xaxes.length); ++i)
- options.xaxes[i] = $.extend(true, {}, options.xaxis, options.xaxes[i]);
- for (i = 0; i < Math.max(1, options.yaxes.length); ++i)
- options.yaxes[i] = $.extend(true, {}, options.yaxis, options.yaxes[i]);
-
- // backwards compatibility, to be removed in future
- if (options.xaxis.noTicks && options.xaxis.ticks == null)
- options.xaxis.ticks = options.xaxis.noTicks;
- if (options.yaxis.noTicks && options.yaxis.ticks == null)
- options.yaxis.ticks = options.yaxis.noTicks;
- if (options.x2axis) {
- options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis);
- options.xaxes[1].position = "top";
- }
- if (options.y2axis) {
- options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis);
- options.yaxes[1].position = "right";
- }
- if (options.grid.coloredAreas)
- options.grid.markings = options.grid.coloredAreas;
- if (options.grid.coloredAreasColor)
- options.grid.markingsColor = options.grid.coloredAreasColor;
- if (options.lines)
- $.extend(true, options.series.lines, options.lines);
- if (options.points)
- $.extend(true, options.series.points, options.points);
- if (options.bars)
- $.extend(true, options.series.bars, options.bars);
- if (options.shadowSize != null)
- options.series.shadowSize = options.shadowSize;
-
- // save options on axes for future reference
- for (i = 0; i < options.xaxes.length; ++i)
- getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i];
- for (i = 0; i < options.yaxes.length; ++i)
- getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i];
-
- // add hooks from options
- for (var n in hooks)
- if (options.hooks[n] && options.hooks[n].length)
- hooks[n] = hooks[n].concat(options.hooks[n]);
-
- executeHooks(hooks.processOptions, [options]);
- }
-
- function setData(d) {
- series = parseData(d);
- fillInSeriesOptions();
- processData();
- }
-
- function parseData(d) {
- var res = [];
- for (var i = 0; i < d.length; ++i) {
- var s = $.extend(true, {}, options.series);
-
- if (d[i].data != null) {
- s.data = d[i].data; // move the data instead of deep-copy
- delete d[i].data;
-
- $.extend(true, s, d[i]);
-
- d[i].data = s.data;
- }
- else
- s.data = d[i];
- res.push(s);
- }
-
- return res;
- }
-
- function axisNumber(obj, coord) {
- var a = obj[coord + "axis"];
- if (typeof a == "object") // if we got a real axis, extract number
- a = a.n;
- if (typeof a != "number")
- a = 1; // default to first axis
- return a;
- }
-
- function allAxes() {
- // return flat array without annoying null entries
- return $.grep(xaxes.concat(yaxes), function (a) { return a; });
- }
-
- function canvasToAxisCoords(pos) {
- // return an object with x/y corresponding to all used axes
- var res = {}, i, axis;
- for (i = 0; i < xaxes.length; ++i) {
- axis = xaxes[i];
- if (axis && axis.used)
- res["x" + axis.n] = axis.c2p(pos.left);
- }
-
- for (i = 0; i < yaxes.length; ++i) {
- axis = yaxes[i];
- if (axis && axis.used)
- res["y" + axis.n] = axis.c2p(pos.top);
- }
-
- if (res.x1 !== undefined)
- res.x = res.x1;
- if (res.y1 !== undefined)
- res.y = res.y1;
-
- return res;
- }
-
- function axisToCanvasCoords(pos) {
- // get canvas coords from the first pair of x/y found in pos
- var res = {}, i, axis, key;
-
- for (i = 0; i < xaxes.length; ++i) {
- axis = xaxes[i];
- if (axis && axis.used) {
- key = "x" + axis.n;
- if (pos[key] == null && axis.n == 1)
- key = "x";
-
- if (pos[key] != null) {
- res.left = axis.p2c(pos[key]);
- break;
- }
- }
- }
-
- for (i = 0; i < yaxes.length; ++i) {
- axis = yaxes[i];
- if (axis && axis.used) {
- key = "y" + axis.n;
- if (pos[key] == null && axis.n == 1)
- key = "y";
-
- if (pos[key] != null) {
- res.top = axis.p2c(pos[key]);
- break;
- }
- }
- }
-
- return res;
- }
-
- function getOrCreateAxis(axes, number) {
- if (!axes[number - 1])
- axes[number - 1] = {
- n: number, // save the number for future reference
- direction: axes == xaxes ? "x" : "y",
- options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis)
- };
-
- return axes[number - 1];
- }
-
- function fillInSeriesOptions() {
- var i;
-
- // collect what we already got of colors
- var neededColors = series.length,
- usedColors = [],
- assignedColors = [];
- for (i = 0; i < series.length; ++i) {
- var sc = series[i].color;
- if (sc != null) {
- --neededColors;
- if (typeof sc == "number")
- assignedColors.push(sc);
- else
- usedColors.push($.color.parse(series[i].color));
- }
- }
-
- // we might need to generate more colors if higher indices
- // are assigned
- for (i = 0; i < assignedColors.length; ++i) {
- neededColors = Math.max(neededColors, assignedColors[i] + 1);
- }
-
- // produce colors as needed
- var colors = [], variation = 0;
- i = 0;
- while (colors.length < neededColors) {
- var c;
- if (options.colors.length == i) // check degenerate case
- c = $.color.make(100, 100, 100);
- else
- c = $.color.parse(options.colors[i]);
-
- // vary color if needed
- var sign = variation % 2 == 1 ? -1 : 1;
- c.scale('rgb', 1 + sign * Math.ceil(variation / 2) * 0.2)
-
- // FIXME: if we're getting to close to something else,
- // we should probably skip this one
- colors.push(c);
-
- ++i;
- if (i >= options.colors.length) {
- i = 0;
- ++variation;
- }
- }
-
- // fill in the options
- var colori = 0, s;
- for (i = 0; i < series.length; ++i) {
- s = series[i];
-
- // assign colors
- if (s.color == null) {
- s.color = colors[colori].toString();
- ++colori;
- }
- else if (typeof s.color == "number")
- s.color = colors[s.color].toString();
-
- // turn on lines automatically in case nothing is set
- if (s.lines.show == null) {
- var v, show = true;
- for (v in s)
- if (s[v] && s[v].show) {
- show = false;
- break;
- }
- if (show)
- s.lines.show = true;
- }
-
- // setup axes
- s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x"));
- s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y"));
- }
- }
-
- function processData() {
- var topSentry = Number.POSITIVE_INFINITY,
- bottomSentry = Number.NEGATIVE_INFINITY,
- fakeInfinity = Number.MAX_VALUE,
- i, j, k, m, length,
- s, points, ps, x, y, axis, val, f, p;
-
- function updateAxis(axis, min, max) {
- if (min < axis.datamin && min != -fakeInfinity)
- axis.datamin = min;
- if (max > axis.datamax && max != fakeInfinity)
- axis.datamax = max;
- }
-
- $.each(allAxes(), function (_, axis) {
- // init axis
- axis.datamin = topSentry;
- axis.datamax = bottomSentry;
- axis.used = false;
- });
-
- for (i = 0; i < series.length; ++i) {
- s = series[i];
- s.datapoints = { points: [] };
-
- executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]);
- }
-
- // first pass: clean and copy data
- for (i = 0; i < series.length; ++i) {
- s = series[i];
-
- var data = s.data, format = s.datapoints.format;
-
- if (!format) {
- format = [];
- // find out how to copy
- format.push({ x: true, number: true, required: true });
- format.push({ y: true, number: true, required: true });
-
- if (s.bars.show || (s.lines.show && s.lines.fill)) {
- format.push({ y: true, number: true, required: false, defaultValue: 0 });
- if (s.bars.horizontal) {
- delete format[format.length - 1].y;
- format[format.length - 1].x = true;
- }
- }
-
- s.datapoints.format = format;
- }
-
- if (s.datapoints.pointsize != null)
- continue; // already filled in
-
- s.datapoints.pointsize = format.length;
-
- ps = s.datapoints.pointsize;
- points = s.datapoints.points;
-
- let insertSteps = s.lines.show && s.lines.steps;
- s.xaxis.used = s.yaxis.used = true;
-
- for (j = k = 0; j < data.length; ++j, k += ps) {
- p = data[j];
-
- var nullify = p == null;
- if (!nullify) {
- for (m = 0; m < ps; ++m) {
- val = p[m];
- f = format[m];
-
- if (f) {
- if (f.number && val != null) {
- val = +val; // convert to number
- if (isNaN(val))
- val = null;
- else if (val == Infinity)
- val = fakeInfinity;
- else if (val == -Infinity)
- val = -fakeInfinity;
- }
-
- if (val == null) {
- if (f.required)
- nullify = true;
-
- if (f.defaultValue != null)
- val = f.defaultValue;
- }
- }
-
- points[k + m] = val;
- }
- }
-
- if (nullify) {
- for (m = 0; m < ps; ++m) {
- val = points[k + m];
- if (val != null) {
- f = format[m];
- // extract min/max info
- if (f.x)
- updateAxis(s.xaxis, val, val);
- if (f.y)
- updateAxis(s.yaxis, val, val);
- }
- points[k + m] = null;
- }
- }
- else {
- // a little bit of line specific stuff that
- // perhaps shouldn't be here, but lacking
- // better means...
- if (insertSteps && k > 0
- && points[k - ps] != null
- && points[k - ps] != points[k]
- && points[k - ps + 1] != points[k + 1]) {
- // copy the point to make room for a middle point
- for (m = 0; m < ps; ++m)
- points[k + ps + m] = points[k + m];
-
- // middle point has same y
- points[k + 1] = points[k - ps + 1];
-
- // we've added a point, better reflect that
- k += ps;
- }
- }
- }
- }
-
- // give the hooks a chance to run
- for (i = 0; i < series.length; ++i) {
- s = series[i];
-
- executeHooks(hooks.processDatapoints, [ s, s.datapoints]);
- }
-
- // second pass: find datamax/datamin for auto-scaling
- for (i = 0; i < series.length; ++i) {
- s = series[i];
- points = s.datapoints.points,
- ps = s.datapoints.pointsize;
-
- var xmin = topSentry, ymin = topSentry,
- xmax = bottomSentry, ymax = bottomSentry;
-
- for (j = 0; j < points.length; j += ps) {
- if (points[j] == null)
- continue;
-
- for (m = 0; m < ps; ++m) {
- val = points[j + m];
- f = format[m];
- if (!f || val == fakeInfinity || val == -fakeInfinity)
- continue;
-
- if (f.x) {
- if (val < xmin)
- xmin = val;
- if (val > xmax)
- xmax = val;
- }
- if (f.y) {
- if (val < ymin)
- ymin = val;
- if (val > ymax)
- ymax = val;
- }
- }
- }
-
- if (s.bars.show) {
- // make sure we got room for the bar on the dancing floor
- var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2;
- if (s.bars.horizontal) {
- ymin += delta;
- ymax += delta + s.bars.barWidth;
- }
- else {
- xmin += delta;
- xmax += delta + s.bars.barWidth;
- }
- }
-
- updateAxis(s.xaxis, xmin, xmax);
- updateAxis(s.yaxis, ymin, ymax);
- }
-
- $.each(allAxes(), function (_, axis) {
- if (axis.datamin == topSentry)
- axis.datamin = null;
- if (axis.datamax == bottomSentry)
- axis.datamax = null;
- });
- }
-
- function makeCanvas(skipPositioning, cls) {
- var c = document.createElement('canvas');
- c.className = cls;
- c.width = canvasWidth;
- c.height = canvasHeight;
-
- if (!skipPositioning)
- $(c).css({ position: 'absolute', left: 0, top: 0 });
-
- $(c).appendTo(placeholder);
-
- if (!c.getContext) // excanvas hack
- c = window.G_vmlCanvasManager.initElement(c);
-
- // used for resetting in case we get replotted
- c.getContext("2d").save();
-
- return c;
- }
-
- function getCanvasDimensions() {
- canvasWidth = placeholder.width();
- canvasHeight = placeholder.height();
-
- if (canvasWidth <= 0 || canvasHeight <= 0)
- throw new Error("Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight);
- }
-
- function resizeCanvas(c) {
- // resizing should reset the state (excanvas seems to be
- // buggy though)
- if (c.width != canvasWidth)
- c.width = canvasWidth;
-
- if (c.height != canvasHeight)
- c.height = canvasHeight;
-
- // so try to get back to the initial state (even if it's
- // gone now, this should be safe according to the spec)
- var cctx = c.getContext("2d");
- cctx.restore();
-
- // and save again
- cctx.save();
- }
-
- function setupCanvases() {
- var reused,
- existingCanvas = placeholder.children("canvas.base"),
- existingOverlay = placeholder.children("canvas.overlay");
-
- if (existingCanvas.length == 0 || existingOverlay == 0) {
- // init everything
-
- placeholder.html(""); // make sure placeholder is clear
-
- placeholder.css({ padding: 0 }); // padding messes up the positioning
-
- if (placeholder.css("position") == 'static')
- placeholder.css("position", "relative"); // for positioning labels and overlay
-
- getCanvasDimensions();
-
- canvas = makeCanvas(true, "base");
- overlay = makeCanvas(false, "overlay"); // overlay canvas for interactive features
-
- reused = false;
- }
- else {
- // reuse existing elements
-
- canvas = existingCanvas.get(0);
- overlay = existingOverlay.get(0);
-
- reused = true;
- }
-
- ctx = canvas.getContext("2d");
- octx = overlay.getContext("2d");
-
- // we include the canvas in the event holder too, because IE 7
- // sometimes has trouble with the stacking order
- eventHolder = $([overlay, canvas]);
-
- if (reused) {
- // run shutdown in the old plot object
- placeholder.data("plot").shutdown();
-
- // reset reused canvases
- plot.resize();
-
- // make sure overlay pixels are cleared (canvas is cleared when we redraw)
- octx.clearRect(0, 0, canvasWidth, canvasHeight);
-
- // then whack any remaining obvious garbage left
- eventHolder.unbind();
- placeholder.children().not([canvas, overlay]).remove();
- }
-
- // save in case we get replotted
- placeholder.data("plot", plot);
- }
-
- function bindEvents() {
- // bind events
- if (options.grid.hoverable) {
- eventHolder.mousemove(onMouseMove);
- eventHolder.mouseleave(onMouseLeave);
- }
-
- if (options.grid.clickable)
- eventHolder.click(onClick);
-
- executeHooks(hooks.bindEvents, [eventHolder]);
- }
-
- function shutdown() {
- if (redrawTimeout)
- clearTimeout(redrawTimeout);
-
- eventHolder.unbind("mousemove", onMouseMove);
- eventHolder.unbind("mouseleave", onMouseLeave);
- eventHolder.unbind("click", onClick);
-
- executeHooks(hooks.shutdown, [eventHolder]);
- }
-
- function setTransformationHelpers(axis) {
- // set helper functions on the axis, assumes plot area
- // has been computed already
-
- function identity(x) { return x; }
-
- var s, m, t = axis.options.transform || identity,
- it = axis.options.inverseTransform;
-
- // precompute how much the axis is scaling a point
- // in canvas space
- if (axis.direction == "x") {
- s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min));
- m = Math.min(t(axis.max), t(axis.min));
- }
- else {
- s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min));
- s = -s;
- m = Math.max(t(axis.max), t(axis.min));
- }
-
- // data point to canvas coordinate
- if (t == identity) // slight optimization
- axis.p2c = function (p) { return (p - m) * s; };
- else
- axis.p2c = function (p) { return (t(p) - m) * s; };
- // canvas coordinate to data point
- if (!it)
- axis.c2p = function (c) { return m + c / s; };
- else
- axis.c2p = function (c) { return it(m + c / s); };
- }
-
- function measureTickLabels(axis) {
- var opts = axis.options, i, ticks = axis.ticks || [], labels = [],
- l, w = opts.labelWidth, h = opts.labelHeight, dummyDiv;
-
- function makeDummyDiv(labels, width) {
- return $('' +
- '
'
- + labels.join("") + '
')
- .appendTo(placeholder);
- }
-
- if (axis.direction == "x") {
- // to avoid measuring the widths of the labels (it's slow), we
- // construct fixed-size boxes and put the labels inside
- // them, we don't need the exact figures and the
- // fixed-size box content is easy to center
- if (w == null)
- w = Math.floor(canvasWidth / (ticks.length > 0 ? ticks.length : 1));
-
- // measure x label heights
- if (h == null) {
- labels = [];
- for (i = 0; i < ticks.length; ++i) {
- l = ticks[i].label;
- if (l)
- labels.push('' + l + '
');
- }
-
- if (labels.length > 0) {
- // stick them all in the same div and measure
- // collective height
- labels.push('
');
- dummyDiv = makeDummyDiv(labels, "width:10000px;");
- h = dummyDiv.height();
- dummyDiv.remove();
- }
- }
- }
- else if (w == null || h == null) {
- // calculate y label dimensions
- for (i = 0; i < ticks.length; ++i) {
- l = ticks[i].label;
- if (l)
- labels.push('' + l + '
');
- }
-
- if (labels.length > 0) {
- dummyDiv = makeDummyDiv(labels, "");
- if (w == null)
- w = dummyDiv.children().width();
- if (h == null)
- h = dummyDiv.find("div.tickLabel").height();
- dummyDiv.remove();
- }
- }
-
- if (w == null)
- w = 0;
- if (h == null)
- h = 0;
-
- axis.labelWidth = w;
- axis.labelHeight = h;
- }
-
- function allocateAxisBoxFirstPhase(axis) {
- // find the bounding box of the axis by looking at label
- // widths/heights and ticks, make room by diminishing the
- // plotOffset
-
- var lw = axis.labelWidth,
- lh = axis.labelHeight,
- pos = axis.options.position,
- tickLength = axis.options.tickLength,
- axismargin = options.grid.axisMargin,
- padding = options.grid.labelMargin,
- all = axis.direction == "x" ? xaxes : yaxes,
- index;
-
- // determine axis margin
- var samePosition = $.grep(all, function (a) {
- return a && a.options.position == pos && a.reserveSpace;
- });
- if ($.inArray(axis, samePosition) == samePosition.length - 1)
- axismargin = 0; // outermost
-
- // determine tick length - if we're innermost, we can use "full"
- if (tickLength == null)
- tickLength = "full";
-
- var sameDirection = $.grep(all, function (a) {
- return a && a.reserveSpace;
- });
-
- var innermost = $.inArray(axis, sameDirection) == 0;
- if (!innermost && tickLength == "full")
- tickLength = 5;
-
- if (!isNaN(+tickLength))
- padding += +tickLength;
-
- // compute box
- if (axis.direction == "x") {
- lh += padding;
-
- if (pos == "bottom") {
- plotOffset.bottom += lh + axismargin;
- axis.box = { top: canvasHeight - plotOffset.bottom, height: lh };
- }
- else {
- axis.box = { top: plotOffset.top + axismargin, height: lh };
- plotOffset.top += lh + axismargin;
- }
- }
- else {
- lw += padding;
-
- if (pos == "left") {
- axis.box = { left: plotOffset.left + axismargin, width: lw };
- plotOffset.left += lw + axismargin;
- }
- else {
- plotOffset.right += lw + axismargin;
- axis.box = { left: canvasWidth - plotOffset.right, width: lw };
- }
- }
-
- // save for future reference
- axis.position = pos;
- axis.tickLength = tickLength;
- axis.box.padding = padding;
- axis.innermost = innermost;
- }
-
- function allocateAxisBoxSecondPhase(axis) {
- // set remaining bounding box coordinates
- if (axis.direction == "x") {
- axis.box.left = plotOffset.left;
- axis.box.width = plotWidth;
- }
- else {
- axis.box.top = plotOffset.top;
- axis.box.height = plotHeight;
- }
- }
-
- function setupGrid() {
- var i, axes = allAxes();
-
- // first calculate the plot and axis box dimensions
-
- $.each(axes, function (_, axis) {
- axis.show = axis.options.show;
- if (axis.show == null)
- axis.show = axis.used; // by default an axis is visible if it's got data
-
- axis.reserveSpace = axis.show || axis.options.reserveSpace;
-
- setRange(axis);
- });
-
- let allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; });
-
- plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0;
- if (options.grid.show) {
- $.each(allocatedAxes, function (_, axis) {
- // make the ticks
- setupTickGeneration(axis);
- setTicks(axis);
- snapRangeToTicks(axis, axis.ticks);
-
- // find labelWidth/Height for axis
- measureTickLabels(axis);
- });
-
- // with all dimensions in house, we can compute the
- // axis boxes, start from the outside (reverse order)
- for (i = allocatedAxes.length - 1; i >= 0; --i)
- allocateAxisBoxFirstPhase(allocatedAxes[i]);
-
- // make sure we've got enough space for things that
- // might stick out
- var minMargin = options.grid.minBorderMargin;
- if (minMargin == null) {
- minMargin = 0;
- for (i = 0; i < series.length; ++i)
- minMargin = Math.max(minMargin, series[i].points.radius + series[i].points.lineWidth/2);
- }
-
- for (var a in plotOffset) {
- plotOffset[a] += options.grid.borderWidth;
- plotOffset[a] = Math.max(minMargin, plotOffset[a]);
- }
- }
-
- plotWidth = canvasWidth - plotOffset.left - plotOffset.right;
- plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top;
-
- // now we got the proper plotWidth/Height, we can compute the scaling
- $.each(axes, function (_, axis) {
- setTransformationHelpers(axis);
- });
-
- if (options.grid.show) {
- $.each(allocatedAxes, function (_, axis) {
- allocateAxisBoxSecondPhase(axis);
- });
-
- insertAxisLabels();
- }
-
- insertLegend();
- }
-
- function setRange(axis) {
- var opts = axis.options,
- min = +(opts.min != null ? opts.min : axis.datamin),
- max = +(opts.max != null ? opts.max : axis.datamax),
- delta = max - min;
-
- if (delta == 0.0) {
- // degenerate case
- var widen = max == 0 ? 1 : 0.01;
-
- if (opts.min == null)
- min -= widen;
- // always widen max if we couldn't widen min to ensure we
- // don't fall into min == max which doesn't work
- if (opts.max == null || opts.min != null)
- max += widen;
- }
- else {
- // consider autoscaling
- var margin = opts.autoscaleMargin;
- if (margin != null) {
- if (opts.min == null) {
- min -= delta * margin;
- // make sure we don't go below zero if all values
- // are positive
- if (min < 0 && axis.datamin != null && axis.datamin >= 0)
- min = 0;
- }
- if (opts.max == null) {
- max += delta * margin;
- if (max > 0 && axis.datamax != null && axis.datamax <= 0)
- max = 0;
- }
- }
- }
- axis.min = min;
- axis.max = max;
- }
-
- function setupTickGeneration(axis) {
- var opts = axis.options;
-
- // estimate number of ticks
- var noTicks;
- if (typeof opts.ticks == "number" && opts.ticks > 0)
- noTicks = opts.ticks;
- else
- // heuristic based on the model a*sqrt(x) fitted to
- // some data points that seemed reasonable
- noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? canvasWidth : canvasHeight);
-
- var delta = (axis.max - axis.min) / noTicks,
- size, generator, unit, formatter, i, magn, norm;
-
- if (opts.mode == "time") {
- // pretty handling of time
-
- // map of app. size of time units in milliseconds
- var timeUnitSize = {
- "second": 1000,
- "minute": 60 * 1000,
- "hour": 60 * 60 * 1000,
- "day": 24 * 60 * 60 * 1000,
- "month": 30 * 24 * 60 * 60 * 1000,
- "year": 365.2425 * 24 * 60 * 60 * 1000
- };
-
-
- // the allowed tick sizes, after 1 year we use
- // an integer algorithm
- var spec = [
- [1, "second"], [2, "second"], [5, "second"], [10, "second"],
- [30, "second"],
- [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
- [30, "minute"],
- [1, "hour"], [2, "hour"], [4, "hour"],
- [8, "hour"], [12, "hour"],
- [1, "day"], [2, "day"], [3, "day"],
- [0.25, "month"], [0.5, "month"], [1, "month"],
- [2, "month"], [3, "month"], [6, "month"],
- [1, "year"]
- ];
-
- var minSize = 0;
- if (opts.minTickSize != null) {
- if (typeof opts.tickSize == "number")
- minSize = opts.tickSize;
- else
- minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];
- }
-
- for (var i = 0; i < spec.length - 1; ++i)
- if (delta < (spec[i][0] * timeUnitSize[spec[i][1]]
- + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
- && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize)
- break;
- size = spec[i][0];
- unit = spec[i][1];
-
- // special-case the possibility of several years
- if (unit == "year") {
- magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10));
- norm = (delta / timeUnitSize.year) / magn;
- if (norm < 1.5)
- size = 1;
- else if (norm < 3)
- size = 2;
- else if (norm < 7.5)
- size = 5;
- else
- size = 10;
-
- size *= magn;
- }
-
- axis.tickSize = opts.tickSize || [size, unit];
-
- generator = function(axis) {
- var ticks = [],
- tickSize = axis.tickSize[0], unit = axis.tickSize[1],
- d = new Date(axis.min);
-
- var step = tickSize * timeUnitSize[unit];
-
- if (unit == "second")
- d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize));
- if (unit == "minute")
- d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize));
- if (unit == "hour")
- d.setUTCHours(floorInBase(d.getUTCHours(), tickSize));
- if (unit == "month")
- d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize));
- if (unit == "year")
- d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize));
-
- // reset smaller components
- d.setUTCMilliseconds(0);
- if (step >= timeUnitSize.minute)
- d.setUTCSeconds(0);
- if (step >= timeUnitSize.hour)
- d.setUTCMinutes(0);
- if (step >= timeUnitSize.day)
- d.setUTCHours(0);
- if (step >= timeUnitSize.day * 4)
- d.setUTCDate(1);
- if (step >= timeUnitSize.year)
- d.setUTCMonth(0);
-
-
- var carry = 0, v = Number.NaN, prev;
- do {
- prev = v;
- v = d.getTime();
- ticks.push(v);
- if (unit == "month") {
- if (tickSize < 1) {
- // a bit complicated - we'll divide the month
- // up but we need to take care of fractions
- // so we don't end up in the middle of a day
- d.setUTCDate(1);
- var start = d.getTime();
- d.setUTCMonth(d.getUTCMonth() + 1);
- var end = d.getTime();
- d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
- carry = d.getUTCHours();
- d.setUTCHours(0);
- }
- else
- d.setUTCMonth(d.getUTCMonth() + tickSize);
- }
- else if (unit == "year") {
- d.setUTCFullYear(d.getUTCFullYear() + tickSize);
- }
- else
- d.setTime(v + step);
- } while (v < axis.max && v != prev);
-
- return ticks;
- };
-
- formatter = function (v, axis) {
- var d = new Date(v);
-
- // first check global format
- if (opts.timeformat != null)
- return $.plot.formatDate(d, opts.timeformat, opts.monthNames);
-
- var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
- var span = axis.max - axis.min;
- var suffix = (opts.twelveHourClock) ? " %p" : "";
-
- if (t < timeUnitSize.minute)
- fmt = "%h:%M:%S" + suffix;
- else if (t < timeUnitSize.day) {
- if (span < 2 * timeUnitSize.day)
- fmt = "%h:%M" + suffix;
- else
- fmt = "%b %d %h:%M" + suffix;
- }
- else if (t < timeUnitSize.month)
- fmt = "%b %d";
- else if (t < timeUnitSize.year) {
- if (span < timeUnitSize.year)
- fmt = "%b";
- else
- fmt = "%b %y";
- }
- else
- fmt = "%y";
-
- return $.plot.formatDate(d, fmt, opts.monthNames);
- };
- }
- else {
- // pretty rounding of base-10 numbers
- var maxDec = opts.tickDecimals;
- var dec = -Math.floor(Math.log(delta) / Math.LN10);
- if (maxDec != null && dec > maxDec)
- dec = maxDec;
-
- magn = Math.pow(10, -dec);
- norm = delta / magn; // norm is between 1.0 and 10.0
-
- if (norm < 1.5)
- size = 1;
- else if (norm < 3) {
- size = 2;
- // special case for 2.5, requires an extra decimal
- if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
- size = 2.5;
- ++dec;
- }
- }
- else if (norm < 7.5)
- size = 5;
- else
- size = 10;
-
- size *= magn;
-
- if (opts.minTickSize != null && size < opts.minTickSize)
- size = opts.minTickSize;
-
- axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
- axis.tickSize = opts.tickSize || size;
-
- generator = function (axis) {
- var ticks = [];
-
- // spew out all possible ticks
- var start = floorInBase(axis.min, axis.tickSize),
- i = 0, v = Number.NaN, prev;
- do {
- prev = v;
- v = start + i * axis.tickSize;
- ticks.push(v);
- ++i;
- } while (v < axis.max && v != prev);
- return ticks;
- };
-
- formatter = function (v, axis) {
- return v.toFixed(axis.tickDecimals);
- };
- }
-
- if (opts.alignTicksWithAxis != null) {
- var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1];
- if (otherAxis && otherAxis.used && otherAxis != axis) {
- // consider snapping min/max to outermost nice ticks
- var niceTicks = generator(axis);
- if (niceTicks.length > 0) {
- if (opts.min == null)
- axis.min = Math.min(axis.min, niceTicks[0]);
- if (opts.max == null && niceTicks.length > 1)
- axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]);
- }
-
- generator = function (axis) {
- // copy ticks, scaled to this axis
- var ticks = [], v, i;
- for (i = 0; i < otherAxis.ticks.length; ++i) {
- v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min);
- v = axis.min + v * (axis.max - axis.min);
- ticks.push(v);
- }
- return ticks;
- };
-
- // we might need an extra decimal since forced
- // ticks don't necessarily fit naturally
- if (axis.mode != "time" && opts.tickDecimals == null) {
- var extraDec = Math.max(0, -Math.floor(Math.log(delta) / Math.LN10) + 1),
- ts = generator(axis);
-
- // only proceed if the tick interval rounded
- // with an extra decimal doesn't give us a
- // zero at end
- if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec))))
- axis.tickDecimals = extraDec;
- }
- }
- }
-
- axis.tickGenerator = generator;
- if ($.isFunction(opts.tickFormatter))
- axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); };
- else
- axis.tickFormatter = formatter;
- }
-
- function setTicks(axis) {
- var oticks = axis.options.ticks, ticks = [];
- if (oticks == null || (typeof oticks == "number" && oticks > 0))
- ticks = axis.tickGenerator(axis);
- else if (oticks) {
- if ($.isFunction(oticks))
- // generate the ticks
- ticks = oticks({ min: axis.min, max: axis.max });
- else
- ticks = oticks;
- }
-
- // clean up/labelify the supplied ticks, copy them over
- var i, v;
- axis.ticks = [];
- for (i = 0; i < ticks.length; ++i) {
- var label = null;
- var t = ticks[i];
- if (typeof t == "object") {
- v = +t[0];
- if (t.length > 1)
- label = t[1];
- }
- else
- v = +t;
- if (label == null)
- label = axis.tickFormatter(v, axis);
- if (!isNaN(v))
- axis.ticks.push({ v: v, label: label });
- }
- }
-
- function snapRangeToTicks(axis, ticks) {
- if (axis.options.autoscaleMargin && ticks.length > 0) {
- // snap to ticks
- if (axis.options.min == null)
- axis.min = Math.min(axis.min, ticks[0].v);
- if (axis.options.max == null && ticks.length > 1)
- axis.max = Math.max(axis.max, ticks[ticks.length - 1].v);
- }
- }
-
- function draw() {
- ctx.clearRect(0, 0, canvasWidth, canvasHeight);
-
- var grid = options.grid;
-
- // draw background, if any
- if (grid.show && grid.backgroundColor)
- drawBackground();
-
- if (grid.show && !grid.aboveData)
- drawGrid();
-
- for (var i = 0; i < series.length; ++i) {
- executeHooks(hooks.drawSeries, [ctx, series[i]]);
- drawSeries(series[i]);
- }
-
- executeHooks(hooks.draw, [ctx]);
-
- if (grid.show && grid.aboveData)
- drawGrid();
- }
-
- function extractRange(ranges, coord) {
- var axis, from, to, key, axes = allAxes();
-
- for (i = 0; i < axes.length; ++i) {
- axis = axes[i];
- if (axis.direction == coord) {
- key = coord + axis.n + "axis";
- if (!ranges[key] && axis.n == 1)
- key = coord + "axis"; // support x1axis as xaxis
- if (ranges[key]) {
- from = ranges[key].from;
- to = ranges[key].to;
- break;
- }
- }
- }
-
- // backwards-compat stuff - to be removed in future
- if (!ranges[key]) {
- axis = coord == "x" ? xaxes[0] : yaxes[0];
- from = ranges[coord + "1"];
- to = ranges[coord + "2"];
- }
-
- // auto-reverse as an added bonus
- if (from != null && to != null && from > to) {
- var tmp = from;
- from = to;
- to = tmp;
- }
-
- return { from: from, to: to, axis: axis };
- }
-
- function drawBackground() {
- ctx.save();
- ctx.translate(plotOffset.left, plotOffset.top);
-
- ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");
- ctx.fillRect(0, 0, plotWidth, plotHeight);
- ctx.restore();
- }
-
- function drawGrid() {
- var i;
-
- ctx.save();
- ctx.translate(plotOffset.left, plotOffset.top);
-
- // draw markings
- var markings = options.grid.markings;
- if (markings) {
- if ($.isFunction(markings)) {
- var axes = plot.getAxes();
- // xmin etc. is backwards compatibility, to be
- // removed in the future
- axes.xmin = axes.xaxis.min;
- axes.xmax = axes.xaxis.max;
- axes.ymin = axes.yaxis.min;
- axes.ymax = axes.yaxis.max;
-
- markings = markings(axes);
- }
-
- for (i = 0; i < markings.length; ++i) {
- var m = markings[i],
- xrange = extractRange(m, "x"),
- yrange = extractRange(m, "y");
-
- // fill in missing
- if (xrange.from == null)
- xrange.from = xrange.axis.min;
- if (xrange.to == null)
- xrange.to = xrange.axis.max;
- if (yrange.from == null)
- yrange.from = yrange.axis.min;
- if (yrange.to == null)
- yrange.to = yrange.axis.max;
-
- // clip
- if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max ||
- yrange.to < yrange.axis.min || yrange.from > yrange.axis.max)
- continue;
-
- xrange.from = Math.max(xrange.from, xrange.axis.min);
- xrange.to = Math.min(xrange.to, xrange.axis.max);
- yrange.from = Math.max(yrange.from, yrange.axis.min);
- yrange.to = Math.min(yrange.to, yrange.axis.max);
-
- if (xrange.from == xrange.to && yrange.from == yrange.to)
- continue;
-
- // then draw
- xrange.from = xrange.axis.p2c(xrange.from);
- xrange.to = xrange.axis.p2c(xrange.to);
- yrange.from = yrange.axis.p2c(yrange.from);
- yrange.to = yrange.axis.p2c(yrange.to);
-
- if (xrange.from == xrange.to || yrange.from == yrange.to) {
- // draw line
- ctx.beginPath();
- ctx.strokeStyle = m.color || options.grid.markingsColor;
- ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth;
- ctx.moveTo(xrange.from, yrange.from);
- ctx.lineTo(xrange.to, yrange.to);
- ctx.stroke();
- }
- else {
- // fill area
- ctx.fillStyle = m.color || options.grid.markingsColor;
- ctx.fillRect(xrange.from, yrange.to,
- xrange.to - xrange.from,
- yrange.from - yrange.to);
- }
- }
- }
-
- // draw the ticks
- var axes = allAxes(), bw = options.grid.borderWidth;
-
- for (var j = 0; j < axes.length; ++j) {
- var axis = axes[j], box = axis.box,
- t = axis.tickLength, x, y, xoff, yoff;
- if (!axis.show || axis.ticks.length == 0)
- continue
-
- ctx.strokeStyle = axis.options.tickColor || $.color.parse(axis.options.color).scale('a', 0.22).toString();
- ctx.lineWidth = 1;
-
- // find the edges
- if (axis.direction == "x") {
- x = 0;
- if (t == "full")
- y = (axis.position == "top" ? 0 : plotHeight);
- else
- y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0);
- }
- else {
- y = 0;
- if (t == "full")
- x = (axis.position == "left" ? 0 : plotWidth);
- else
- x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0);
- }
-
- // draw tick bar
- if (!axis.innermost) {
- ctx.beginPath();
- xoff = yoff = 0;
- if (axis.direction == "x")
- xoff = plotWidth;
- else
- yoff = plotHeight;
-
- if (ctx.lineWidth == 1) {
- x = Math.floor(x) + 0.5;
- y = Math.floor(y) + 0.5;
- }
-
- ctx.moveTo(x, y);
- ctx.lineTo(x + xoff, y + yoff);
- ctx.stroke();
- }
-
- // draw ticks
- ctx.beginPath();
- for (i = 0; i < axis.ticks.length; ++i) {
- var v = axis.ticks[i].v;
-
- xoff = yoff = 0;
-
- if (v < axis.min || v > axis.max
- // skip those lying on the axes if we got a border
- || (t == "full" && bw > 0
- && (v == axis.min || v == axis.max)))
- continue;
-
- if (axis.direction == "x") {
- x = axis.p2c(v);
- yoff = t == "full" ? -plotHeight : t;
-
- if (axis.position == "top")
- yoff = -yoff;
- }
- else {
- y = axis.p2c(v);
- xoff = t == "full" ? -plotWidth : t;
-
- if (axis.position == "left")
- xoff = -xoff;
- }
-
- if (ctx.lineWidth == 1) {
- if (axis.direction == "x")
- x = Math.floor(x) + 0.5;
- else
- y = Math.floor(y) + 0.5;
- }
-
- ctx.moveTo(x, y);
- ctx.lineTo(x + xoff, y + yoff);
- }
-
- ctx.stroke();
- }
-
-
- // draw border
- if (bw) {
- ctx.lineWidth = bw;
- ctx.strokeStyle = options.grid.borderColor;
- ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw);
- }
-
- ctx.restore();
- }
-
- function insertAxisLabels() {
- placeholder.find(".tickLabels").remove();
-
- var html = [''];
-
- var axes = allAxes();
- for (var j = 0; j < axes.length; ++j) {
- var axis = axes[j], box = axis.box;
- if (!axis.show)
- continue;
- //debug: html.push('
')
- html.push('
');
- for (var i = 0; i < axis.ticks.length; ++i) {
- var tick = axis.ticks[i];
- if (!tick.label || tick.v < axis.min || tick.v > axis.max)
- continue;
-
- var pos = {}, align;
-
- if (axis.direction == "x") {
- align = "center";
- pos.left = Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2);
- if (axis.position == "bottom")
- pos.top = box.top + box.padding;
- else
- pos.bottom = canvasHeight - (box.top + box.height - box.padding);
- }
- else {
- pos.top = Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2);
- if (axis.position == "left") {
- pos.right = canvasWidth - (box.left + box.width - box.padding)
- align = "right";
- }
- else {
- pos.left = box.left + box.padding;
- align = "left";
- }
- }
-
- pos.width = axis.labelWidth;
-
- var style = ["position:absolute", "text-align:" + align ];
- for (var a in pos)
- style.push(a + ":" + pos[a] + "px")
-
- html.push('
' + tick.label + '
');
- }
- html.push('
');
- }
-
- html.push('
');
-
- placeholder.append(html.join(""));
- }
-
- function drawSeries(series) {
- if (series.lines.show)
- drawSeriesLines(series);
- if (series.bars.show)
- drawSeriesBars(series);
- if (series.points.show)
- drawSeriesPoints(series);
- }
-
- function drawSeriesLines(series) {
- function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {
- var points = datapoints.points,
- ps = datapoints.pointsize,
- prevx = null, prevy = null;
-
- ctx.beginPath();
- for (var i = ps; i < points.length; i += ps) {
- var x1 = points[i - ps], y1 = points[i - ps + 1],
- x2 = points[i], y2 = points[i + 1];
-
- if (x1 == null || x2 == null)
- continue;
-
- // clip with ymin
- if (y1 <= y2 && y1 < axisy.min) {
- if (y2 < axisy.min)
- continue; // line segment is outside
- // compute new intersection point
- x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
- y1 = axisy.min;
- }
- else if (y2 <= y1 && y2 < axisy.min) {
- if (y1 < axisy.min)
- continue;
- x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
- y2 = axisy.min;
- }
-
- // clip with ymax
- if (y1 >= y2 && y1 > axisy.max) {
- if (y2 > axisy.max)
- continue;
- x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
- y1 = axisy.max;
- }
- else if (y2 >= y1 && y2 > axisy.max) {
- if (y1 > axisy.max)
- continue;
- x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
- y2 = axisy.max;
- }
-
- // clip with xmin
- if (x1 <= x2 && x1 < axisx.min) {
- if (x2 < axisx.min)
- continue;
- y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
- x1 = axisx.min;
- }
- else if (x2 <= x1 && x2 < axisx.min) {
- if (x1 < axisx.min)
- continue;
- y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
- x2 = axisx.min;
- }
-
- // clip with xmax
- if (x1 >= x2 && x1 > axisx.max) {
- if (x2 > axisx.max)
- continue;
- y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
- x1 = axisx.max;
- }
- else if (x2 >= x1 && x2 > axisx.max) {
- if (x1 > axisx.max)
- continue;
- y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
- x2 = axisx.max;
- }
-
- if (x1 != prevx || y1 != prevy)
- ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
-
- prevx = x2;
- prevy = y2;
- ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
- }
- ctx.stroke();
- }
-
- function plotLineArea(datapoints, axisx, axisy) {
- var points = datapoints.points,
- ps = datapoints.pointsize,
- bottom = Math.min(Math.max(0, axisy.min), axisy.max),
- i = 0, top, areaOpen = false,
- ypos = 1, segmentStart = 0, segmentEnd = 0;
-
- // we process each segment in two turns, first forward
- // direction to sketch out top, then once we hit the
- // end we go backwards to sketch the bottom
- while (true) {
- if (ps > 0 && i > points.length + ps)
- break;
-
- i += ps; // ps is negative if going backwards
-
- var x1 = points[i - ps],
- y1 = points[i - ps + ypos],
- x2 = points[i], y2 = points[i + ypos];
-
- if (areaOpen) {
- if (ps > 0 && x1 != null && x2 == null) {
- // at turning point
- segmentEnd = i;
- ps = -ps;
- ypos = 2;
- continue;
- }
-
- if (ps < 0 && i == segmentStart + ps) {
- // done with the reverse sweep
- ctx.fill();
- areaOpen = false;
- ps = -ps;
- ypos = 1;
- i = segmentStart = segmentEnd + ps;
- continue;
- }
- }
-
- if (x1 == null || x2 == null)
- continue;
-
- // clip x values
-
- // clip with xmin
- if (x1 <= x2 && x1 < axisx.min) {
- if (x2 < axisx.min)
- continue;
- y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
- x1 = axisx.min;
- }
- else if (x2 <= x1 && x2 < axisx.min) {
- if (x1 < axisx.min)
- continue;
- y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
- x2 = axisx.min;
- }
-
- // clip with xmax
- if (x1 >= x2 && x1 > axisx.max) {
- if (x2 > axisx.max)
- continue;
- y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
- x1 = axisx.max;
- }
- else if (x2 >= x1 && x2 > axisx.max) {
- if (x1 > axisx.max)
- continue;
- y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
- x2 = axisx.max;
- }
-
- if (!areaOpen) {
- // open area
- ctx.beginPath();
- ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
- areaOpen = true;
- }
-
- // now first check the case where both is outside
- if (y1 >= axisy.max && y2 >= axisy.max) {
- ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
- ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
- continue;
- }
- else if (y1 <= axisy.min && y2 <= axisy.min) {
- ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
- ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
- continue;
- }
-
- // else it's a bit more complicated, there might
- // be a flat maxed out rectangle first, then a
- // triangular cutout or reverse; to find these
- // keep track of the current x values
- var x1old = x1, x2old = x2;
-
- // clip the y values, without shortcutting, we
- // go through all cases in turn
-
- // clip with ymin
- if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
- x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
- y1 = axisy.min;
- }
- else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
- x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
- y2 = axisy.min;
- }
-
- // clip with ymax
- if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
- x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
- y1 = axisy.max;
- }
- else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
- x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
- y2 = axisy.max;
- }
-
- // if the x value was changed we got a rectangle
- // to fill
- if (x1 != x1old) {
- ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1));
- // it goes to (x1, y1), but we fill that below
- }
-
- // fill triangular section, this sometimes result
- // in redundant points if (x1, y1) hasn't changed
- // from previous line to, but we just ignore that
- ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
- ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
-
- // fill the other rectangle if it's there
- if (x2 != x2old) {
- ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
- ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2));
- }
- }
- }
-
- ctx.save();
- ctx.translate(plotOffset.left, plotOffset.top);
- ctx.lineJoin = "round";
-
- var lw = series.lines.lineWidth,
- sw = series.shadowSize;
- // FIXME: consider another form of shadow when filling is turned on
- if (lw > 0 && sw > 0) {
- // draw shadow as a thick and thin line with transparency
- ctx.lineWidth = sw;
- ctx.strokeStyle = "rgba(0,0,0,0.1)";
- // position shadow at angle from the mid of line
- var angle = Math.PI/18;
- plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis);
- ctx.lineWidth = sw/2;
- plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis);
- }
-
- ctx.lineWidth = lw;
- ctx.strokeStyle = series.color;
- var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);
- if (fillStyle) {
- ctx.fillStyle = fillStyle;
- plotLineArea(series.datapoints, series.xaxis, series.yaxis);
- }
-
- if (lw > 0)
- plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);
- ctx.restore();
- }
-
- function drawSeriesPoints(series) {
- function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) {
- var points = datapoints.points, ps = datapoints.pointsize;
-
- for (var i = 0; i < points.length; i += ps) {
- var x = points[i], y = points[i + 1];
- if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
- continue;
-
- ctx.beginPath();
- x = axisx.p2c(x);
- y = axisy.p2c(y) + offset;
- if (symbol == "circle")
- ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);
- else
- symbol(ctx, x, y, radius, shadow);
- ctx.closePath();
-
- if (fillStyle) {
- ctx.fillStyle = fillStyle;
- ctx.fill();
- }
- ctx.stroke();
- }
- }
-
- ctx.save();
- ctx.translate(plotOffset.left, plotOffset.top);
-
- var lw = series.points.lineWidth,
- sw = series.shadowSize,
- radius = series.points.radius,
- symbol = series.points.symbol;
- if (lw > 0 && sw > 0) {
- // draw shadow in two steps
- var w = sw / 2;
- ctx.lineWidth = w;
- ctx.strokeStyle = "rgba(0,0,0,0.1)";
- plotPoints(series.datapoints, radius, null, w + w/2, true,
- series.xaxis, series.yaxis, symbol);
-
- ctx.strokeStyle = "rgba(0,0,0,0.2)";
- plotPoints(series.datapoints, radius, null, w/2, true,
- series.xaxis, series.yaxis, symbol);
- }
-
- ctx.lineWidth = lw;
- ctx.strokeStyle = series.color;
- plotPoints(series.datapoints, radius,
- getFillStyle(series.points, series.color), 0, false,
- series.xaxis, series.yaxis, symbol);
- ctx.restore();
- }
-
- function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {
- var left, right, bottom, top,
- drawLeft, drawRight, drawTop, drawBottom,
- tmp;
-
- // in horizontal mode, we start the bar from the left
- // instead of from the bottom so it appears to be
- // horizontal rather than vertical
- if (horizontal) {
- drawBottom = drawRight = drawTop = true;
- drawLeft = false;
- left = b;
- right = x;
- top = y + barLeft;
- bottom = y + barRight;
-
- // account for negative bars
- if (right < left) {
- tmp = right;
- right = left;
- left = tmp;
- drawLeft = true;
- drawRight = false;
- }
- }
- else {
- drawLeft = drawRight = drawTop = true;
- drawBottom = false;
- left = x + barLeft;
- right = x + barRight;
- bottom = b;
- top = y;
-
- // account for negative bars
- if (top < bottom) {
- tmp = top;
- top = bottom;
- bottom = tmp;
- drawBottom = true;
- drawTop = false;
- }
- }
-
- // clip
- if (right < axisx.min || left > axisx.max ||
- top < axisy.min || bottom > axisy.max)
- return;
-
- if (left < axisx.min) {
- left = axisx.min;
- drawLeft = false;
- }
-
- if (right > axisx.max) {
- right = axisx.max;
- drawRight = false;
- }
-
- if (bottom < axisy.min) {
- bottom = axisy.min;
- drawBottom = false;
- }
-
- if (top > axisy.max) {
- top = axisy.max;
- drawTop = false;
- }
-
- left = axisx.p2c(left);
- bottom = axisy.p2c(bottom);
- right = axisx.p2c(right);
- top = axisy.p2c(top);
-
- // fill the bar
- if (fillStyleCallback) {
- c.beginPath();
- c.moveTo(left, bottom);
- c.lineTo(left, top);
- c.lineTo(right, top);
- c.lineTo(right, bottom);
- c.fillStyle = fillStyleCallback(bottom, top);
- c.fill();
- }
-
- // draw outline
- if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {
- c.beginPath();
-
- // FIXME: inline moveTo is buggy with excanvas
- c.moveTo(left, bottom + offset);
- if (drawLeft)
- c.lineTo(left, top + offset);
- else
- c.moveTo(left, top + offset);
- if (drawTop)
- c.lineTo(right, top + offset);
- else
- c.moveTo(right, top + offset);
- if (drawRight)
- c.lineTo(right, bottom + offset);
- else
- c.moveTo(right, bottom + offset);
- if (drawBottom)
- c.lineTo(left, bottom + offset);
- else
- c.moveTo(left, bottom + offset);
- c.stroke();
- }
- }
-
- function drawSeriesBars(series) {
- function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) {
- var points = datapoints.points, ps = datapoints.pointsize;
-
- for (var i = 0; i < points.length; i += ps) {
- if (points[i] == null)
- continue;
- drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth);
- }
- }
-
- ctx.save();
- ctx.translate(plotOffset.left, plotOffset.top);
-
- // FIXME: figure out a way to add shadows (for instance along the right edge)
- ctx.lineWidth = series.bars.lineWidth;
- ctx.strokeStyle = series.color;
- var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
- var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null;
- plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis);
- ctx.restore();
- }
-
- function getFillStyle(filloptions, seriesColor, bottom, top) {
- var fill = filloptions.fill;
- if (!fill)
- return null;
-
- if (filloptions.fillColor)
- return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
-
- var c = $.color.parse(seriesColor);
- c.a = typeof fill == "number" ? fill : 0.4;
- c.normalize();
- return c.toString();
- }
-
- function insertLegend() {
- placeholder.find(".legend").remove();
-
- if (!options.legend.show)
- return;
-
- var fragments = [], rowStarted = false,
- lf = options.legend.labelFormatter, s, label;
- for (var i = 0; i < series.length; ++i) {
- s = series[i];
- label = s.label;
- if (!label)
- continue;
-
- if (i % options.legend.noColumns == 0) {
- if (rowStarted)
- fragments.push('');
- fragments.push('');
- rowStarted = true;
- }
-
- if (lf)
- label = lf(label, s);
-
- fragments.push(
- ' ' +
- '' + label + ' ');
- }
- if (rowStarted)
- fragments.push(' ');
-
- if (fragments.length == 0)
- return;
-
- var table = '' + fragments.join("") + '
';
- if (options.legend.container != null)
- $(options.legend.container).html(table);
- else {
- var pos = "",
- p = options.legend.position,
- m = options.legend.margin;
- if (m[0] == null)
- m = [m, m];
- if (p.charAt(0) == "n")
- pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
- else if (p.charAt(0) == "s")
- pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
- if (p.charAt(1) == "e")
- pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
- else if (p.charAt(1) == "w")
- pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
- var legend = $('' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(placeholder);
- if (options.legend.backgroundOpacity != 0.0) {
- // put in the transparent background
- // separately to avoid blended labels and
- // label boxes
- var c = options.legend.backgroundColor;
- if (c == null) {
- c = options.grid.backgroundColor;
- if (c && typeof c == "string")
- c = $.color.parse(c);
- else
- c = $.color.extract(legend, 'background-color');
- c.a = 1;
- c = c.toString();
- }
- var div = legend.children();
- $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
- }
- }
- }
-
-
- // interactive features
-
- var highlights = [],
- redrawTimeout = null;
-
- // returns the data item the mouse is over, or null if none is found
- function findNearbyItem(mouseX, mouseY, seriesFilter) {
- var maxDistance = options.grid.mouseActiveRadius,
- smallestDistance = maxDistance * maxDistance + 1,
- item = null, foundPoint = false, i, j;
-
- for (i = series.length - 1; i >= 0; --i) {
- if (!seriesFilter(series[i]))
- continue;
-
- var s = series[i],
- axisx = s.xaxis,
- axisy = s.yaxis,
- points = s.datapoints.points,
- ps = s.datapoints.pointsize,
- mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster
- my = axisy.c2p(mouseY),
- maxx = maxDistance / axisx.scale,
- maxy = maxDistance / axisy.scale;
-
- // with inverse transforms, we can't use the maxx/maxy
- // optimization, sadly
- if (axisx.options.inverseTransform)
- maxx = Number.MAX_VALUE;
- if (axisy.options.inverseTransform)
- maxy = Number.MAX_VALUE;
-
- if (s.lines.show || s.points.show) {
- for (j = 0; j < points.length; j += ps) {
- var x = points[j], y = points[j + 1];
- if (x == null)
- continue;
-
- // For points and lines, the cursor must be within a
- // certain distance to the data point
- if (x - mx > maxx || x - mx < -maxx ||
- y - my > maxy || y - my < -maxy)
- continue;
-
- // We have to calculate distances in pixels, not in
- // data units, because the scales of the axes may be different
- var dx = Math.abs(axisx.p2c(x) - mouseX),
- dy = Math.abs(axisy.p2c(y) - mouseY),
- dist = dx * dx + dy * dy; // we save the sqrt
-
- // use <= to ensure last point takes precedence
- // (last generally means on top of)
- if (dist < smallestDistance) {
- smallestDistance = dist;
- item = [i, j / ps];
- }
- }
- }
-
- if (s.bars.show && !item) { // no other point can be nearby
- var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2,
- barRight = barLeft + s.bars.barWidth;
-
- for (j = 0; j < points.length; j += ps) {
- var x = points[j], y = points[j + 1], b = points[j + 2];
- if (x == null)
- continue;
-
- // for a bar graph, the cursor must be inside the bar
- if (series[i].bars.horizontal ?
- (mx <= Math.max(b, x) && mx >= Math.min(b, x) &&
- my >= y + barLeft && my <= y + barRight) :
- (mx >= x + barLeft && mx <= x + barRight &&
- my >= Math.min(b, y) && my <= Math.max(b, y)))
- item = [i, j / ps];
- }
- }
- }
-
- if (item) {
- i = item[0];
- j = item[1];
- ps = series[i].datapoints.pointsize;
-
- return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),
- dataIndex: j,
- series: series[i],
- seriesIndex: i };
- }
-
- return null;
- }
-
- function onMouseMove(e) {
- if (options.grid.hoverable)
- triggerClickHoverEvent("plothover", e,
- function (s) { return s["hoverable"] != false; });
- }
-
- function onMouseLeave(e) {
- if (options.grid.hoverable)
- triggerClickHoverEvent("plothover", e,
- function (s) { return false; });
- }
-
- function onClick(e) {
- triggerClickHoverEvent("plotclick", e,
- function (s) { return s["clickable"] != false; });
- }
-
- // trigger click or hover event (they send the same parameters
- // so we share their code)
- function triggerClickHoverEvent(eventname, event, seriesFilter) {
- var offset = eventHolder.offset(),
- canvasX = event.pageX - offset.left - plotOffset.left,
- canvasY = event.pageY - offset.top - plotOffset.top,
- pos = canvasToAxisCoords({ left: canvasX, top: canvasY });
-
- pos.pageX = event.pageX;
- pos.pageY = event.pageY;
-
- var item = findNearbyItem(canvasX, canvasY, seriesFilter);
-
- if (item) {
- // fill in mouse pos for any listeners out there
- item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left);
- item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top);
- }
-
- if (options.grid.autoHighlight) {
- // clear auto-highlights
- for (var i = 0; i < highlights.length; ++i) {
- var h = highlights[i];
- if (h.auto == eventname &&
- !(item && h.series == item.series &&
- h.point[0] == item.datapoint[0] &&
- h.point[1] == item.datapoint[1]))
- unhighlight(h.series, h.point);
- }
-
- if (item)
- highlight(item.series, item.datapoint, eventname);
- }
-
- placeholder.trigger(eventname, [ pos, item ]);
- }
-
- function triggerRedrawOverlay() {
- if (!redrawTimeout)
- redrawTimeout = setTimeout(drawOverlay, 30);
- }
-
- function drawOverlay() {
- redrawTimeout = null;
-
- // draw highlights
- octx.save();
- octx.clearRect(0, 0, canvasWidth, canvasHeight);
- octx.translate(plotOffset.left, plotOffset.top);
-
- var i, hi;
- for (i = 0; i < highlights.length; ++i) {
- hi = highlights[i];
-
- if (hi.series.bars.show)
- drawBarHighlight(hi.series, hi.point);
- else
- drawPointHighlight(hi.series, hi.point);
- }
- octx.restore();
-
- executeHooks(hooks.drawOverlay, [octx]);
- }
-
- function highlight(s, point, auto) {
- if (typeof s == "number")
- s = series[s];
-
- if (typeof point == "number") {
- var ps = s.datapoints.pointsize;
- point = s.datapoints.points.slice(ps * point, ps * (point + 1));
- }
-
- var i = indexOfHighlight(s, point);
- if (i == -1) {
- highlights.push({ series: s, point: point, auto: auto });
-
- triggerRedrawOverlay();
- }
- else if (!auto)
- highlights[i].auto = false;
- }
-
- function unhighlight(s, point) {
- if (s == null && point == null) {
- highlights = [];
- triggerRedrawOverlay();
- }
-
- if (typeof s == "number")
- s = series[s];
-
- if (typeof point == "number")
- point = s.data[point];
-
- var i = indexOfHighlight(s, point);
- if (i != -1) {
- highlights.splice(i, 1);
-
- triggerRedrawOverlay();
- }
- }
-
- function indexOfHighlight(s, p) {
- for (var i = 0; i < highlights.length; ++i) {
- var h = highlights[i];
- if (h.series == s && h.point[0] == p[0]
- && h.point[1] == p[1])
- return i;
- }
- return -1;
- }
-
- function drawPointHighlight(series, point) {
- var x = point[0], y = point[1],
- axisx = series.xaxis, axisy = series.yaxis;
-
- if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
- return;
-
- var pointRadius = series.points.radius + series.points.lineWidth / 2;
- octx.lineWidth = pointRadius;
- octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
- var radius = 1.5 * pointRadius,
- x = axisx.p2c(x),
- y = axisy.p2c(y);
-
- octx.beginPath();
- if (series.points.symbol == "circle")
- octx.arc(x, y, radius, 0, 2 * Math.PI, false);
- else
- series.points.symbol(octx, x, y, radius, false);
- octx.closePath();
- octx.stroke();
- }
-
- function drawBarHighlight(series, point) {
- octx.lineWidth = series.bars.lineWidth;
- octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
- var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString();
- var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
- drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,
- 0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth);
- }
-
- function getColorOrGradient(spec, bottom, top, defaultColor) {
- if (typeof spec == "string")
- return spec;
- else {
- // assume this is a gradient spec; IE currently only
- // supports a simple vertical gradient properly, so that's
- // what we support too
- var gradient = ctx.createLinearGradient(0, top, 0, bottom);
-
- for (var i = 0, l = spec.colors.length; i < l; ++i) {
- var c = spec.colors[i];
- if (typeof c != "string") {
- var co = $.color.parse(defaultColor);
- if (c.brightness != null)
- co = co.scale('rgb', c.brightness)
- if (c.opacity != null)
- co.a *= c.opacity;
- c = co.toString();
- }
- gradient.addColorStop(i / (l - 1), c);
- }
-
- return gradient;
- }
- }
- }
-
- $.plot = function(placeholder, data, options) {
- //var t0 = new Date();
- var plot = new Plot($(placeholder), data, options, $.plot.plugins);
- //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime()));
- return plot;
- };
-
- $.plot.version = "0.7";
-
- $.plot.plugins = [];
-
- // returns a string with the date d formatted according to fmt
- $.plot.formatDate = function(d, fmt, monthNames) {
- var leftPad = function(n) {
- n = "" + n;
- return n.length == 1 ? "0" + n : n;
- };
-
- var r = [];
- var escape = false, padNext = false;
- var hours = d.getUTCHours();
- var isAM = hours < 12;
- if (monthNames == null)
- monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
-
- if (fmt.search(/%p|%P/) != -1) {
- if (hours > 12) {
- hours = hours - 12;
- } else if (hours == 0) {
- hours = 12;
- }
- }
- for (var i = 0; i < fmt.length; ++i) {
- var c = fmt.charAt(i);
-
- if (escape) {
- switch (c) {
- case 'h': c = "" + hours; break;
- case 'H': c = leftPad(hours); break;
- case 'M': c = leftPad(d.getUTCMinutes()); break;
- case 'S': c = leftPad(d.getUTCSeconds()); break;
- case 'd': c = "" + d.getUTCDate(); break;
- case 'm': c = "" + (d.getUTCMonth() + 1); break;
- case 'y': c = "" + d.getUTCFullYear(); break;
- case 'b': c = "" + monthNames[d.getUTCMonth()]; break;
- case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
- case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
- case '0': c = ""; padNext = true; break;
- }
- if (c && padNext) {
- c = leftPad(c);
- padNext = false;
- }
- r.push(c);
- if (!padNext)
- escape = false;
- }
- else {
- if (c == "%")
- escape = true;
- else
- r.push(c);
- }
- }
- return r.join("");
- };
-
- // round to nearby lower multiple of base
- function floorInBase(n, base) {
- return base * Math.floor(n / base);
- }
-
-})(jQuery);
diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.min.js
deleted file mode 100644
index 4467fc5d8cd3..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.flot.min.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/* Javascript plotting library for jQuery, v. 0.7.
- *
- * Released under the MIT license by IOLA, December 2007.
- *
- */
-(function(b){b.color={};b.color.make=function(d,e,g,f){var c={};c.r=d||0;c.g=e||0;c.b=g||0;c.a=f!=null?f:1;c.add=function(h,j){for(var k=0;k=1){return"rgb("+[c.r,c.g,c.b].join(",")+")"}else{return"rgba("+[c.r,c.g,c.b,c.a].join(",")+")"}};c.normalize=function(){function h(k,j,l){return jl?l:j)}c.r=h(0,parseInt(c.r),255);c.g=h(0,parseInt(c.g),255);c.b=h(0,parseInt(c.b),255);c.a=h(0,c.a,1);return c};c.clone=function(){return b.color.make(c.r,c.b,c.g,c.a)};return c.normalize()};b.color.extract=function(d,e){var c;do{c=d.css(e).toLowerCase();if(c!=""&&c!="transparent"){break}d=d.parent()}while(!b.nodeName(d.get(0),"body"));if(c=="rgba(0, 0, 0, 0)"){c="transparent"}return b.color.parse(c)};b.color.parse=function(c){var d,f=b.color.make;if(d=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(c)){return f(parseInt(d[1],10),parseInt(d[2],10),parseInt(d[3],10))}if(d=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(c)){return f(parseInt(d[1],10),parseInt(d[2],10),parseInt(d[3],10),parseFloat(d[4]))}if(d=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(c)){return f(parseFloat(d[1])*2.55,parseFloat(d[2])*2.55,parseFloat(d[3])*2.55)}if(d=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(c)){return f(parseFloat(d[1])*2.55,parseFloat(d[2])*2.55,parseFloat(d[3])*2.55,parseFloat(d[4]))}if(d=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(c)){return f(parseInt(d[1],16),parseInt(d[2],16),parseInt(d[3],16))}if(d=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(c)){return f(parseInt(d[1]+d[1],16),parseInt(d[2]+d[2],16),parseInt(d[3]+d[3],16))}var e=b.trim(c).toLowerCase();if(e=="transparent"){return f(255,255,255,0)}else{d=a[e]||[0,0,0];return f(d[0],d[1],d[2])}};var a={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);(function(c){function b(av,ai,J,af){var Q=[],O={colors:["#edc240","#afd8f8","#cb4b4b","#4da74d","#9440ed"],legend:{show:true,noColumns:1,labelFormatter:null,labelBoxBorderColor:"#ccc",container:null,position:"ne",margin:5,backgroundColor:null,backgroundOpacity:0.85},xaxis:{show:null,position:"bottom",mode:null,color:null,tickColor:null,transform:null,inverseTransform:null,min:null,max:null,autoscaleMargin:null,ticks:null,tickFormatter:null,labelWidth:null,labelHeight:null,reserveSpace:null,tickLength:null,alignTicksWithAxis:null,tickDecimals:null,tickSize:null,minTickSize:null,monthNames:null,timeformat:null,twelveHourClock:false},yaxis:{autoscaleMargin:0.02,position:"left"},xaxes:[],yaxes:[],series:{points:{show:false,radius:3,lineWidth:2,fill:true,fillColor:"#ffffff",symbol:"circle"},lines:{lineWidth:2,fill:false,fillColor:null,steps:false},bars:{show:false,lineWidth:2,barWidth:1,fill:true,fillColor:null,align:"left",horizontal:false},shadowSize:3},grid:{show:true,aboveData:false,color:"#545454",backgroundColor:null,borderColor:null,tickColor:null,labelMargin:5,axisMargin:8,borderWidth:2,minBorderMargin:null,markings:null,markingsColor:"#f4f4f4",markingsLineWidth:2,clickable:false,hoverable:false,autoHighlight:true,mouseActiveRadius:10},hooks:{}},az=null,ad=null,y=null,H=null,A=null,p=[],aw=[],q={left:0,right:0,top:0,bottom:0},G=0,I=0,h=0,w=0,ak={processOptions:[],processRawData:[],processDatapoints:[],drawSeries:[],draw:[],bindEvents:[],drawOverlay:[],shutdown:[]},aq=this;aq.setData=aj;aq.setupGrid=t;aq.draw=W;aq.getPlaceholder=function(){return av};aq.getCanvas=function(){return az};aq.getPlotOffset=function(){return q};aq.width=function(){return h};aq.height=function(){return w};aq.offset=function(){var aB=y.offset();aB.left+=q.left;aB.top+=q.top;return aB};aq.getData=function(){return Q};aq.getAxes=function(){var aC={},aB;c.each(p.concat(aw),function(aD,aE){if(aE){aC[aE.direction+(aE.n!=1?aE.n:"")+"axis"]=aE}});return aC};aq.getXAxes=function(){return p};aq.getYAxes=function(){return aw};aq.c2p=C;aq.p2c=ar;aq.getOptions=function(){return O};aq.highlight=x;aq.unhighlight=T;aq.triggerRedrawOverlay=f;aq.pointOffset=function(aB){return{left:parseInt(p[aA(aB,"x")-1].p2c(+aB.x)+q.left),top:parseInt(aw[aA(aB,"y")-1].p2c(+aB.y)+q.top)}};aq.shutdown=ag;aq.resize=function(){B();g(az);g(ad)};aq.hooks=ak;F(aq);Z(J);X();aj(ai);t();W();ah();function an(aD,aB){aB=[aq].concat(aB);for(var aC=0;aC=O.colors.length){aG=0;++aF}}var aH=0,aN;for(aG=0;aGa3.datamax&&a1!=aB){a3.datamax=a1}}c.each(m(),function(a1,a2){a2.datamin=aO;a2.datamax=aI;a2.used=false});for(aU=0;aU0&&aT[aR-aP]!=null&&aT[aR-aP]!=aT[aR]&&aT[aR-aP+1]!=aT[aR+1]){for(aN=0;aNaM){aM=a0}}if(aX.y){if(a0aV){aV=a0}}}}if(aJ.bars.show){var aY=aJ.bars.align=="left"?0:-aJ.bars.barWidth/2;if(aJ.bars.horizontal){aQ+=aY;aV+=aY+aJ.bars.barWidth}else{aK+=aY;aM+=aY+aJ.bars.barWidth}}aF(aJ.xaxis,aK,aM);aF(aJ.yaxis,aQ,aV)}c.each(m(),function(a1,a2){if(a2.datamin==aO){a2.datamin=null}if(a2.datamax==aI){a2.datamax=null}})}function j(aB,aC){var aD=document.createElement("canvas");aD.className=aC;aD.width=G;aD.height=I;if(!aB){c(aD).css({position:"absolute",left:0,top:0})}c(aD).appendTo(av);if(!aD.getContext){aD=window.G_vmlCanvasManager.initElement(aD)}aD.getContext("2d").save();return aD}function B(){G=av.width();I=av.height();if(G<=0||I<=0){throw"Invalid dimensions for plot, width = "+G+", height = "+I}}function g(aC){if(aC.width!=G){aC.width=G}if(aC.height!=I){aC.height=I}var aB=aC.getContext("2d");aB.restore();aB.save()}function X(){var aC,aB=av.children("canvas.base"),aD=av.children("canvas.overlay");if(aB.length==0||aD==0){av.html("");av.css({padding:0});if(av.css("position")=="static"){av.css("position","relative")}B();az=j(true,"base");ad=j(false,"overlay");aC=false}else{az=aB.get(0);ad=aD.get(0);aC=true}H=az.getContext("2d");A=ad.getContext("2d");y=c([ad,az]);if(aC){av.data("plot").shutdown();aq.resize();A.clearRect(0,0,G,I);y.unbind();av.children().not([az,ad]).remove()}av.data("plot",aq)}function ah(){if(O.grid.hoverable){y.mousemove(aa);y.mouseleave(l)}if(O.grid.clickable){y.click(R)}an(ak.bindEvents,[y])}function ag(){if(M){clearTimeout(M)}y.unbind("mousemove",aa);y.unbind("mouseleave",l);y.unbind("click",R);an(ak.shutdown,[y])}function r(aG){function aC(aH){return aH}var aF,aB,aD=aG.options.transform||aC,aE=aG.options.inverseTransform;if(aG.direction=="x"){aF=aG.scale=h/Math.abs(aD(aG.max)-aD(aG.min));aB=Math.min(aD(aG.max),aD(aG.min))}else{aF=aG.scale=w/Math.abs(aD(aG.max)-aD(aG.min));aF=-aF;aB=Math.max(aD(aG.max),aD(aG.min))}if(aD==aC){aG.p2c=function(aH){return(aH-aB)*aF}}else{aG.p2c=function(aH){return(aD(aH)-aB)*aF}}if(!aE){aG.c2p=function(aH){return aB+aH/aF}}else{aG.c2p=function(aH){return aE(aB+aH/aF)}}}function L(aD){var aB=aD.options,aF,aJ=aD.ticks||[],aI=[],aE,aK=aB.labelWidth,aG=aB.labelHeight,aC;function aH(aM,aL){return c('").appendTo(av)}if(aD.direction=="x"){if(aK==null){aK=Math.floor(G/(aJ.length>0?aJ.length:1))}if(aG==null){aI=[];for(aF=0;aF'+aE+"")}}if(aI.length>0){aI.push('
');aC=aH(aI,"width:10000px;");aG=aC.height();aC.remove()}}}else{if(aK==null||aG==null){for(aF=0;aF'+aE+"")}}if(aI.length>0){aC=aH(aI,"");if(aK==null){aK=aC.children().width()}if(aG==null){aG=aC.find("div.tickLabel").height()}aC.remove()}}}if(aK==null){aK=0}if(aG==null){aG=0}aD.labelWidth=aK;aD.labelHeight=aG}function au(aD){var aC=aD.labelWidth,aL=aD.labelHeight,aH=aD.options.position,aF=aD.options.tickLength,aG=O.grid.axisMargin,aJ=O.grid.labelMargin,aK=aD.direction=="x"?p:aw,aE;var aB=c.grep(aK,function(aN){return aN&&aN.options.position==aH&&aN.reserveSpace});if(c.inArray(aD,aB)==aB.length-1){aG=0}if(aF==null){aF="full"}var aI=c.grep(aK,function(aN){return aN&&aN.reserveSpace});var aM=c.inArray(aD,aI)==0;if(!aM&&aF=="full"){aF=5}if(!isNaN(+aF)){aJ+=+aF}if(aD.direction=="x"){aL+=aJ;if(aH=="bottom"){q.bottom+=aL+aG;aD.box={top:I-q.bottom,height:aL}}else{aD.box={top:q.top+aG,height:aL};q.top+=aL+aG}}else{aC+=aJ;if(aH=="left"){aD.box={left:q.left+aG,width:aC};q.left+=aC+aG}else{q.right+=aC+aG;aD.box={left:G-q.right,width:aC}}}aD.position=aH;aD.tickLength=aF;aD.box.padding=aJ;aD.innermost=aM}function U(aB){if(aB.direction=="x"){aB.box.left=q.left;aB.box.width=h}else{aB.box.top=q.top;aB.box.height=w}}function t(){var aC,aE=m();c.each(aE,function(aF,aG){aG.show=aG.options.show;if(aG.show==null){aG.show=aG.used}aG.reserveSpace=aG.show||aG.options.reserveSpace;n(aG)});allocatedAxes=c.grep(aE,function(aF){return aF.reserveSpace});q.left=q.right=q.top=q.bottom=0;if(O.grid.show){c.each(allocatedAxes,function(aF,aG){S(aG);P(aG);ap(aG,aG.ticks);L(aG)});for(aC=allocatedAxes.length-1;aC>=0;--aC){au(allocatedAxes[aC])}var aD=O.grid.minBorderMargin;if(aD==null){aD=0;for(aC=0;aC=0){aD=0}}if(aF.max==null){aB+=aH*aG;if(aB>0&&aE.datamax!=null&&aE.datamax<=0){aB=0}}}}aE.min=aD;aE.max=aB}function S(aG){var aM=aG.options;var aH;if(typeof aM.ticks=="number"&&aM.ticks>0){aH=aM.ticks}else{aH=0.3*Math.sqrt(aG.direction=="x"?G:I)}var aT=(aG.max-aG.min)/aH,aO,aB,aN,aR,aS,aQ,aI;if(aM.mode=="time"){var aJ={second:1000,minute:60*1000,hour:60*60*1000,day:24*60*60*1000,month:30*24*60*60*1000,year:365.2425*24*60*60*1000};var aK=[[1,"second"],[2,"second"],[5,"second"],[10,"second"],[30,"second"],[1,"minute"],[2,"minute"],[5,"minute"],[10,"minute"],[30,"minute"],[1,"hour"],[2,"hour"],[4,"hour"],[8,"hour"],[12,"hour"],[1,"day"],[2,"day"],[3,"day"],[0.25,"month"],[0.5,"month"],[1,"month"],[2,"month"],[3,"month"],[6,"month"],[1,"year"]];var aC=0;if(aM.minTickSize!=null){if(typeof aM.tickSize=="number"){aC=aM.tickSize}else{aC=aM.minTickSize[0]*aJ[aM.minTickSize[1]]}}for(var aS=0;aS=aC){break}}aO=aK[aS][0];aN=aK[aS][1];if(aN=="year"){aQ=Math.pow(10,Math.floor(Math.log(aT/aJ.year)/Math.LN10));aI=(aT/aJ.year)/aQ;if(aI<1.5){aO=1}else{if(aI<3){aO=2}else{if(aI<7.5){aO=5}else{aO=10}}}aO*=aQ}aG.tickSize=aM.tickSize||[aO,aN];aB=function(aX){var a2=[],a0=aX.tickSize[0],a3=aX.tickSize[1],a1=new Date(aX.min);var aW=a0*aJ[a3];if(a3=="second"){a1.setUTCSeconds(a(a1.getUTCSeconds(),a0))}if(a3=="minute"){a1.setUTCMinutes(a(a1.getUTCMinutes(),a0))}if(a3=="hour"){a1.setUTCHours(a(a1.getUTCHours(),a0))}if(a3=="month"){a1.setUTCMonth(a(a1.getUTCMonth(),a0))}if(a3=="year"){a1.setUTCFullYear(a(a1.getUTCFullYear(),a0))}a1.setUTCMilliseconds(0);if(aW>=aJ.minute){a1.setUTCSeconds(0)}if(aW>=aJ.hour){a1.setUTCMinutes(0)}if(aW>=aJ.day){a1.setUTCHours(0)}if(aW>=aJ.day*4){a1.setUTCDate(1)}if(aW>=aJ.year){a1.setUTCMonth(0)}var a5=0,a4=Number.NaN,aY;do{aY=a4;a4=a1.getTime();a2.push(a4);if(a3=="month"){if(a0<1){a1.setUTCDate(1);var aV=a1.getTime();a1.setUTCMonth(a1.getUTCMonth()+1);var aZ=a1.getTime();a1.setTime(a4+a5*aJ.hour+(aZ-aV)*a0);a5=a1.getUTCHours();a1.setUTCHours(0)}else{a1.setUTCMonth(a1.getUTCMonth()+a0)}}else{if(a3=="year"){a1.setUTCFullYear(a1.getUTCFullYear()+a0)}else{a1.setTime(a4+aW)}}}while(a4aU){aP=aU}aQ=Math.pow(10,-aP);aI=aT/aQ;if(aI<1.5){aO=1}else{if(aI<3){aO=2;if(aI>2.25&&(aU==null||aP+1<=aU)){aO=2.5;++aP}}else{if(aI<7.5){aO=5}else{aO=10}}}aO*=aQ;if(aM.minTickSize!=null&&aO0){if(aM.min==null){aG.min=Math.min(aG.min,aL[0])}if(aM.max==null&&aL.length>1){aG.max=Math.max(aG.max,aL[aL.length-1])}}aB=function(aX){var aY=[],aV,aW;for(aW=0;aW1&&/\..*0$/.test((aD[1]-aD[0]).toFixed(aE)))){aG.tickDecimals=aE}}}}aG.tickGenerator=aB;if(c.isFunction(aM.tickFormatter)){aG.tickFormatter=function(aV,aW){return""+aM.tickFormatter(aV,aW)}}else{aG.tickFormatter=aR}}function P(aF){var aH=aF.options.ticks,aG=[];if(aH==null||(typeof aH=="number"&&aH>0)){aG=aF.tickGenerator(aF)}else{if(aH){if(c.isFunction(aH)){aG=aH({min:aF.min,max:aF.max})}else{aG=aH}}}var aE,aB;aF.ticks=[];for(aE=0;aE1){aC=aD[1]}}else{aB=+aD}if(aC==null){aC=aF.tickFormatter(aB,aF)}if(!isNaN(aB)){aF.ticks.push({v:aB,label:aC})}}}function ap(aB,aC){if(aB.options.autoscaleMargin&&aC.length>0){if(aB.options.min==null){aB.min=Math.min(aB.min,aC[0].v)}if(aB.options.max==null&&aC.length>1){aB.max=Math.max(aB.max,aC[aC.length-1].v)}}}function W(){H.clearRect(0,0,G,I);var aC=O.grid;if(aC.show&&aC.backgroundColor){N()}if(aC.show&&!aC.aboveData){ac()}for(var aB=0;aBaG){var aC=aH;aH=aG;aG=aC}return{from:aH,to:aG,axis:aE}}function N(){H.save();H.translate(q.left,q.top);H.fillStyle=am(O.grid.backgroundColor,w,0,"rgba(255, 255, 255, 0)");H.fillRect(0,0,h,w);H.restore()}function ac(){var aF;H.save();H.translate(q.left,q.top);var aH=O.grid.markings;if(aH){if(c.isFunction(aH)){var aK=aq.getAxes();aK.xmin=aK.xaxis.min;aK.xmax=aK.xaxis.max;aK.ymin=aK.yaxis.min;aK.ymax=aK.yaxis.max;aH=aH(aK)}for(aF=0;aFaC.axis.max||aI.toaI.axis.max){continue}aC.from=Math.max(aC.from,aC.axis.min);aC.to=Math.min(aC.to,aC.axis.max);aI.from=Math.max(aI.from,aI.axis.min);aI.to=Math.min(aI.to,aI.axis.max);if(aC.from==aC.to&&aI.from==aI.to){continue}aC.from=aC.axis.p2c(aC.from);aC.to=aC.axis.p2c(aC.to);aI.from=aI.axis.p2c(aI.from);aI.to=aI.axis.p2c(aI.to);if(aC.from==aC.to||aI.from==aI.to){H.beginPath();H.strokeStyle=aD.color||O.grid.markingsColor;H.lineWidth=aD.lineWidth||O.grid.markingsLineWidth;H.moveTo(aC.from,aI.from);H.lineTo(aC.to,aI.to);H.stroke()}else{H.fillStyle=aD.color||O.grid.markingsColor;H.fillRect(aC.from,aI.to,aC.to-aC.from,aI.from-aI.to)}}}var aK=m(),aM=O.grid.borderWidth;for(var aE=0;aEaB.max||(aQ=="full"&&aM>0&&(aO==aB.min||aO==aB.max))){continue}if(aB.direction=="x"){aN=aB.p2c(aO);aJ=aQ=="full"?-w:aQ;if(aB.position=="top"){aJ=-aJ}}else{aL=aB.p2c(aO);aP=aQ=="full"?-h:aQ;if(aB.position=="left"){aP=-aP}}if(H.lineWidth==1){if(aB.direction=="x"){aN=Math.floor(aN)+0.5}else{aL=Math.floor(aL)+0.5}}H.moveTo(aN,aL);H.lineTo(aN+aP,aL+aJ)}H.stroke()}if(aM){H.lineWidth=aM;H.strokeStyle=O.grid.borderColor;H.strokeRect(-aM/2,-aM/2,h+aM,w+aM)}H.restore()}function k(){av.find(".tickLabels").remove();var aG=[''];var aJ=m();for(var aD=0;aD
');for(var aE=0;aEaC.max){continue}var aK={},aI;if(aC.direction=="x"){aI="center";aK.left=Math.round(q.left+aC.p2c(aH.v)-aC.labelWidth/2);if(aC.position=="bottom"){aK.top=aF.top+aF.padding}else{aK.bottom=I-(aF.top+aF.height-aF.padding)}}else{aK.top=Math.round(q.top+aC.p2c(aH.v)-aC.labelHeight/2);if(aC.position=="left"){aK.right=G-(aF.left+aF.width-aF.padding);aI="right"}else{aK.left=aF.left+aF.padding;aI="left"}}aK.width=aC.labelWidth;var aB=["position:absolute","text-align:"+aI];for(var aL in aK){aB.push(aL+":"+aK[aL]+"px")}aG.push(''+aH.label+"
")}aG.push(" ")}aG.push("");av.append(aG.join(""))}function d(aB){if(aB.lines.show){at(aB)}if(aB.bars.show){e(aB)}if(aB.points.show){ao(aB)}}function at(aE){function aD(aP,aQ,aI,aU,aT){var aV=aP.points,aJ=aP.pointsize,aN=null,aM=null;H.beginPath();for(var aO=aJ;aO=aR&&aS>aT.max){if(aR>aT.max){continue}aL=(aT.max-aS)/(aR-aS)*(aK-aL)+aL;aS=aT.max}else{if(aR>=aS&&aR>aT.max){if(aS>aT.max){continue}aK=(aT.max-aS)/(aR-aS)*(aK-aL)+aL;aR=aT.max}}if(aL<=aK&&aL=aK&&aL>aU.max){if(aK>aU.max){continue}aS=(aU.max-aL)/(aK-aL)*(aR-aS)+aS;aL=aU.max}else{if(aK>=aL&&aK>aU.max){if(aL>aU.max){continue}aR=(aU.max-aL)/(aK-aL)*(aR-aS)+aS;aK=aU.max}}if(aL!=aN||aS!=aM){H.moveTo(aU.p2c(aL)+aQ,aT.p2c(aS)+aI)}aN=aK;aM=aR;H.lineTo(aU.p2c(aK)+aQ,aT.p2c(aR)+aI)}H.stroke()}function aF(aI,aQ,aP){var aW=aI.points,aV=aI.pointsize,aN=Math.min(Math.max(0,aP.min),aP.max),aX=0,aU,aT=false,aM=1,aL=0,aR=0;while(true){if(aV>0&&aX>aW.length+aV){break}aX+=aV;var aZ=aW[aX-aV],aK=aW[aX-aV+aM],aY=aW[aX],aJ=aW[aX+aM];if(aT){if(aV>0&&aZ!=null&&aY==null){aR=aX;aV=-aV;aM=2;continue}if(aV<0&&aX==aL+aV){H.fill();aT=false;aV=-aV;aM=1;aX=aL=aR+aV;continue}}if(aZ==null||aY==null){continue}if(aZ<=aY&&aZ=aY&&aZ>aQ.max){if(aY>aQ.max){continue}aK=(aQ.max-aZ)/(aY-aZ)*(aJ-aK)+aK;aZ=aQ.max}else{if(aY>=aZ&&aY>aQ.max){if(aZ>aQ.max){continue}aJ=(aQ.max-aZ)/(aY-aZ)*(aJ-aK)+aK;aY=aQ.max}}if(!aT){H.beginPath();H.moveTo(aQ.p2c(aZ),aP.p2c(aN));aT=true}if(aK>=aP.max&&aJ>=aP.max){H.lineTo(aQ.p2c(aZ),aP.p2c(aP.max));H.lineTo(aQ.p2c(aY),aP.p2c(aP.max));continue}else{if(aK<=aP.min&&aJ<=aP.min){H.lineTo(aQ.p2c(aZ),aP.p2c(aP.min));H.lineTo(aQ.p2c(aY),aP.p2c(aP.min));continue}}var aO=aZ,aS=aY;if(aK<=aJ&&aK=aP.min){aZ=(aP.min-aK)/(aJ-aK)*(aY-aZ)+aZ;aK=aP.min}else{if(aJ<=aK&&aJ=aP.min){aY=(aP.min-aK)/(aJ-aK)*(aY-aZ)+aZ;aJ=aP.min}}if(aK>=aJ&&aK>aP.max&&aJ<=aP.max){aZ=(aP.max-aK)/(aJ-aK)*(aY-aZ)+aZ;aK=aP.max}else{if(aJ>=aK&&aJ>aP.max&&aK<=aP.max){aY=(aP.max-aK)/(aJ-aK)*(aY-aZ)+aZ;aJ=aP.max}}if(aZ!=aO){H.lineTo(aQ.p2c(aO),aP.p2c(aK))}H.lineTo(aQ.p2c(aZ),aP.p2c(aK));H.lineTo(aQ.p2c(aY),aP.p2c(aJ));if(aY!=aS){H.lineTo(aQ.p2c(aY),aP.p2c(aJ));H.lineTo(aQ.p2c(aS),aP.p2c(aJ))}}}H.save();H.translate(q.left,q.top);H.lineJoin="round";var aG=aE.lines.lineWidth,aB=aE.shadowSize;if(aG>0&&aB>0){H.lineWidth=aB;H.strokeStyle="rgba(0,0,0,0.1)";var aH=Math.PI/18;aD(aE.datapoints,Math.sin(aH)*(aG/2+aB/2),Math.cos(aH)*(aG/2+aB/2),aE.xaxis,aE.yaxis);H.lineWidth=aB/2;aD(aE.datapoints,Math.sin(aH)*(aG/2+aB/4),Math.cos(aH)*(aG/2+aB/4),aE.xaxis,aE.yaxis)}H.lineWidth=aG;H.strokeStyle=aE.color;var aC=ae(aE.lines,aE.color,0,w);if(aC){H.fillStyle=aC;aF(aE.datapoints,aE.xaxis,aE.yaxis)}if(aG>0){aD(aE.datapoints,0,0,aE.xaxis,aE.yaxis)}H.restore()}function ao(aE){function aH(aN,aM,aU,aK,aS,aT,aQ,aJ){var aR=aN.points,aI=aN.pointsize;for(var aL=0;aLaT.max||aOaQ.max){continue}H.beginPath();aP=aT.p2c(aP);aO=aQ.p2c(aO)+aK;if(aJ=="circle"){H.arc(aP,aO,aM,0,aS?Math.PI:Math.PI*2,false)}else{aJ(H,aP,aO,aM,aS)}H.closePath();if(aU){H.fillStyle=aU;H.fill()}H.stroke()}}H.save();H.translate(q.left,q.top);var aG=aE.points.lineWidth,aC=aE.shadowSize,aB=aE.points.radius,aF=aE.points.symbol;if(aG>0&&aC>0){var aD=aC/2;H.lineWidth=aD;H.strokeStyle="rgba(0,0,0,0.1)";aH(aE.datapoints,aB,null,aD+aD/2,true,aE.xaxis,aE.yaxis,aF);H.strokeStyle="rgba(0,0,0,0.2)";aH(aE.datapoints,aB,null,aD/2,true,aE.xaxis,aE.yaxis,aF)}H.lineWidth=aG;H.strokeStyle=aE.color;aH(aE.datapoints,aB,ae(aE.points,aE.color),0,false,aE.xaxis,aE.yaxis,aF);H.restore()}function E(aN,aM,aV,aI,aQ,aF,aD,aL,aK,aU,aR,aC){var aE,aT,aJ,aP,aG,aB,aO,aH,aS;if(aR){aH=aB=aO=true;aG=false;aE=aV;aT=aN;aP=aM+aI;aJ=aM+aQ;if(aTaL.max||aPaK.max){return}if(aEaL.max){aT=aL.max;aB=false}if(aJaK.max){aP=aK.max;aO=false}aE=aL.p2c(aE);aJ=aK.p2c(aJ);aT=aL.p2c(aT);aP=aK.p2c(aP);if(aD){aU.beginPath();aU.moveTo(aE,aJ);aU.lineTo(aE,aP);aU.lineTo(aT,aP);aU.lineTo(aT,aJ);aU.fillStyle=aD(aJ,aP);aU.fill()}if(aC>0&&(aG||aB||aO||aH)){aU.beginPath();aU.moveTo(aE,aJ+aF);if(aG){aU.lineTo(aE,aP+aF)}else{aU.moveTo(aE,aP+aF)}if(aO){aU.lineTo(aT,aP+aF)}else{aU.moveTo(aT,aP+aF)}if(aB){aU.lineTo(aT,aJ+aF)}else{aU.moveTo(aT,aJ+aF)}if(aH){aU.lineTo(aE,aJ+aF)}else{aU.moveTo(aE,aJ+aF)}aU.stroke()}}function e(aD){function aC(aJ,aI,aL,aG,aK,aN,aM){var aO=aJ.points,aF=aJ.pointsize;for(var aH=0;aH")}aH.push("");aF=true}if(aN){aJ=aN(aJ,aM)}aH.push(''+aJ+" ")}if(aF){aH.push(" ")}if(aH.length==0){return}var aL='";if(O.legend.container!=null){c(O.legend.container).html(aL)}else{var aI="",aC=O.legend.position,aD=O.legend.margin;if(aD[0]==null){aD=[aD,aD]}if(aC.charAt(0)=="n"){aI+="top:"+(aD[1]+q.top)+"px;"}else{if(aC.charAt(0)=="s"){aI+="bottom:"+(aD[1]+q.bottom)+"px;"}}if(aC.charAt(1)=="e"){aI+="right:"+(aD[0]+q.right)+"px;"}else{if(aC.charAt(1)=="w"){aI+="left:"+(aD[0]+q.left)+"px;"}}var aK=c(''+aL.replace('style="','style="position:absolute;'+aI+";")+"
").appendTo(av);if(O.legend.backgroundOpacity!=0){var aG=O.legend.backgroundColor;if(aG==null){aG=O.grid.backgroundColor;if(aG&&typeof aG=="string"){aG=c.color.parse(aG)}else{aG=c.color.extract(aK,"background-color")}aG.a=1;aG=aG.toString()}var aB=aK.children();c('
').prependTo(aK).css("opacity",O.legend.backgroundOpacity)}}}var ab=[],M=null;function K(aI,aG,aD){var aO=O.grid.mouseActiveRadius,a0=aO*aO+1,aY=null,aR=false,aW,aU;for(aW=Q.length-1;aW>=0;--aW){if(!aD(Q[aW])){continue}var aP=Q[aW],aH=aP.xaxis,aF=aP.yaxis,aV=aP.datapoints.points,aT=aP.datapoints.pointsize,aQ=aH.c2p(aI),aN=aF.c2p(aG),aC=aO/aH.scale,aB=aO/aF.scale;if(aH.options.inverseTransform){aC=Number.MAX_VALUE}if(aF.options.inverseTransform){aB=Number.MAX_VALUE}if(aP.lines.show||aP.points.show){for(aU=0;aUaC||aK-aQ<-aC||aJ-aN>aB||aJ-aN<-aB){continue}var aM=Math.abs(aH.p2c(aK)-aI),aL=Math.abs(aF.p2c(aJ)-aG),aS=aM*aM+aL*aL;if(aS=Math.min(aZ,aK)&&aN>=aJ+aE&&aN<=aJ+aX):(aQ>=aK+aE&&aQ<=aK+aX&&aN>=Math.min(aZ,aJ)&&aN<=Math.max(aZ,aJ))){aY=[aW,aU/aT]}}}}if(aY){aW=aY[0];aU=aY[1];aT=Q[aW].datapoints.pointsize;return{datapoint:Q[aW].datapoints.points.slice(aU*aT,(aU+1)*aT),dataIndex:aU,series:Q[aW],seriesIndex:aW}}return null}function aa(aB){if(O.grid.hoverable){u("plothover",aB,function(aC){return aC.hoverable!=false})}}function l(aB){if(O.grid.hoverable){u("plothover",aB,function(aC){return false})}}function R(aB){u("plotclick",aB,function(aC){return aC.clickable!=false})}function u(aC,aB,aD){var aE=y.offset(),aH=aB.pageX-aE.left-q.left,aF=aB.pageY-aE.top-q.top,aJ=C({left:aH,top:aF});aJ.pageX=aB.pageX;aJ.pageY=aB.pageY;var aK=K(aH,aF,aD);if(aK){aK.pageX=parseInt(aK.series.xaxis.p2c(aK.datapoint[0])+aE.left+q.left);aK.pageY=parseInt(aK.series.yaxis.p2c(aK.datapoint[1])+aE.top+q.top)}if(O.grid.autoHighlight){for(var aG=0;aGaH.max||aIaG.max){return}var aF=aE.points.radius+aE.points.lineWidth/2;A.lineWidth=aF;A.strokeStyle=c.color.parse(aE.color).scale("a",0.5).toString();var aB=1.5*aF,aC=aH.p2c(aC),aI=aG.p2c(aI);A.beginPath();if(aE.points.symbol=="circle"){A.arc(aC,aI,aB,0,2*Math.PI,false)}else{aE.points.symbol(A,aC,aI,aB,false)}A.closePath();A.stroke()}function v(aE,aB){A.lineWidth=aE.bars.lineWidth;A.strokeStyle=c.color.parse(aE.color).scale("a",0.5).toString();var aD=c.color.parse(aE.color).scale("a",0.5).toString();var aC=aE.bars.align=="left"?0:-aE.bars.barWidth/2;E(aB[0],aB[1],aB[2]||0,aC,aC+aE.bars.barWidth,0,function(){return aD},aE.xaxis,aE.yaxis,A,aE.bars.horizontal,aE.bars.lineWidth)}function am(aJ,aB,aH,aC){if(typeof aJ=="string"){return aJ}else{var aI=H.createLinearGradient(0,aH,0,aB);for(var aE=0,aD=aJ.colors.length;aE12){n=n-12}else{if(n==0){n=12}}}for(var g=0;g0&&L.which!=M.which)||E(L.target).is(M.not)){return }}switch(L.type){case"mousedown":E.extend(M,E(K).offset(),{elem:K,target:L.target,pageX:L.pageX,pageY:L.pageY});A.add(document,"mousemove mouseup",H,M);G(K,false);F.dragging=null;return false;case !F.dragging&&"mousemove":if(I(L.pageX-M.pageX)+I(L.pageY-M.pageY) max) {
- // make sure min < max
- var tmp = min;
- min = max;
- max = tmp;
- }
-
- var range = max - min;
- if (zr &&
- ((zr[0] != null && range < zr[0]) ||
- (zr[1] != null && range > zr[1])))
- return;
-
- opts.min = min;
- opts.max = max;
- });
-
- plot.setupGrid();
- plot.draw();
-
- if (!args.preventEvent)
- plot.getPlaceholder().trigger("plotzoom", [ plot ]);
- }
-
- plot.pan = function (args) {
- var delta = {
- x: +args.left,
- y: +args.top
- };
-
- if (isNaN(delta.x))
- delta.x = 0;
- if (isNaN(delta.y))
- delta.y = 0;
-
- $.each(plot.getAxes(), function (_, axis) {
- var opts = axis.options,
- min, max, d = delta[axis.direction];
-
- min = axis.c2p(axis.p2c(axis.min) + d),
- max = axis.c2p(axis.p2c(axis.max) + d);
-
- var pr = opts.panRange;
- if (pr === false) // no panning on this axis
- return;
-
- if (pr) {
- // check whether we hit the wall
- if (pr[0] != null && pr[0] > min) {
- d = pr[0] - min;
- min += d;
- max += d;
- }
-
- if (pr[1] != null && pr[1] < max) {
- d = pr[1] - max;
- min += d;
- max += d;
- }
- }
-
- opts.min = min;
- opts.max = max;
- });
-
- plot.setupGrid();
- plot.draw();
-
- if (!args.preventEvent)
- plot.getPlaceholder().trigger("plotpan", [ plot ]);
- }
-
- function shutdown(plot, eventHolder) {
- eventHolder.unbind(plot.getOptions().zoom.trigger, onZoomClick);
- eventHolder.unbind("mousewheel", onMouseWheel);
- eventHolder.unbind("dragstart", onDragStart);
- eventHolder.unbind("drag", onDrag);
- eventHolder.unbind("dragend", onDragEnd);
- if (panTimeout)
- clearTimeout(panTimeout);
- }
-
- plot.hooks.bindEvents.push(bindEvents);
- plot.hooks.shutdown.push(shutdown);
- }
-
- $.plot.plugins.push({
- init: init,
- options: options,
- name: 'navigate',
- version: '1.3'
- });
-})(jQuery);
diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.navigate.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.navigate.min.js
deleted file mode 100644
index ecf63c93ba5b..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.flot.navigate.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(function(i){i.fn.drag=function(j,k,l){if(k){this.bind("dragstart",j)}if(l){this.bind("dragend",l)}return !j?this.trigger("drag"):this.bind("drag",k?k:j)};var d=i.event,c=d.special,h=c.drag={not:":input",distance:0,which:1,dragging:false,setup:function(j){j=i.extend({distance:h.distance,which:h.which,not:h.not},j||{});j.distance=e(j.distance);d.add(this,"mousedown",f,j);if(this.attachEvent){this.attachEvent("ondragstart",a)}},teardown:function(){d.remove(this,"mousedown",f);if(this===h.dragging){h.dragging=h.proxy=false}g(this,true);if(this.detachEvent){this.detachEvent("ondragstart",a)}}};c.dragstart=c.dragend={setup:function(){},teardown:function(){}};function f(j){var k=this,l,m=j.data||{};if(m.elem){k=j.dragTarget=m.elem;j.dragProxy=h.proxy||k;j.cursorOffsetX=m.pageX-m.left;j.cursorOffsetY=m.pageY-m.top;j.offsetX=j.pageX-j.cursorOffsetX;j.offsetY=j.pageY-j.cursorOffsetY}else{if(h.dragging||(m.which>0&&j.which!=m.which)||i(j.target).is(m.not)){return}}switch(j.type){case"mousedown":i.extend(m,i(k).offset(),{elem:k,target:j.target,pageX:j.pageX,pageY:j.pageY});d.add(document,"mousemove mouseup",f,m);g(k,false);h.dragging=null;return false;case !h.dragging&&"mousemove":if(e(j.pageX-m.pageX)+e(j.pageY-m.pageY)w){var A=B;B=w;w=A}var y=w-B;if(E&&((E[0]!=null&&yE[1]))){return}D.min=B;D.max=w});o.setupGrid();o.draw();if(!q.preventEvent){o.getPlaceholder().trigger("plotzoom",[o])}};o.pan=function(p){var q={x:+p.left,y:+p.top};if(isNaN(q.x)){q.x=0}if(isNaN(q.y)){q.y=0}b.each(o.getAxes(),function(s,u){var v=u.options,t,r,w=q[u.direction];t=u.c2p(u.p2c(u.min)+w),r=u.c2p(u.p2c(u.max)+w);var x=v.panRange;if(x===false){return}if(x){if(x[0]!=null&&x[0]>t){w=x[0]-t;t+=w;r+=w}if(x[1]!=null&&x[1]1)
- options.series.pie.tilt=1;
- if (options.series.pie.tilt<0)
- options.series.pie.tilt=0;
-
- // add processData hook to do transformations on the data
- plot.hooks.processDatapoints.push(processDatapoints);
- plot.hooks.drawOverlay.push(drawOverlay);
-
- // add draw hook
- plot.hooks.draw.push(draw);
- }
- }
-
- // bind hoverable events
- function bindEvents(plot, eventHolder)
- {
- var options = plot.getOptions();
-
- if (options.series.pie.show && options.grid.hoverable)
- eventHolder.unbind('mousemove').mousemove(onMouseMove);
-
- if (options.series.pie.show && options.grid.clickable)
- eventHolder.unbind('click').click(onClick);
- }
-
-
- // debugging function that prints out an object
- function alertObject(obj)
- {
- var msg = '';
- function traverse(obj, depth)
- {
- if (!depth)
- depth = 0;
- for (var i = 0; i < obj.length; ++i)
- {
- for (var j=0; jcanvas.width-maxRadius)
- centerLeft = canvas.width-maxRadius;
- }
-
- function fixData(data)
- {
- for (var i = 0; i < data.length; ++i)
- {
- if (typeof(data[i].data)=='number')
- data[i].data = [[1,data[i].data]];
- else if (typeof(data[i].data)=='undefined' || typeof(data[i].data[0])=='undefined')
- {
- if (typeof(data[i].data)!='undefined' && typeof(data[i].data.label)!='undefined')
- data[i].label = data[i].data.label; // fix weirdness coming from flot
- data[i].data = [[1,0]];
-
- }
- }
- return data;
- }
-
- function combine(data)
- {
- data = fixData(data);
- calcTotal(data);
- var combined = 0;
- var numCombined = 0;
- var color = options.series.pie.combine.color;
-
- var newdata = [];
- for (var i = 0; i < data.length; ++i)
- {
- // make sure its a number
- data[i].data[0][1] = parseFloat(data[i].data[0][1]);
- if (!data[i].data[0][1])
- data[i].data[0][1] = 0;
-
- if (data[i].data[0][1]/total<=options.series.pie.combine.threshold)
- {
- combined += data[i].data[0][1];
- numCombined++;
- if (!color)
- color = data[i].color;
- }
- else
- {
- newdata.push({
- data: [[1,data[i].data[0][1]]],
- color: data[i].color,
- label: data[i].label,
- angle: (data[i].data[0][1]*(Math.PI*2))/total,
- percent: (data[i].data[0][1]/total*100)
- });
- }
- }
- if (numCombined>0)
- newdata.push({
- data: [[1,combined]],
- color: color,
- label: options.series.pie.combine.label,
- angle: (combined*(Math.PI*2))/total,
- percent: (combined/total*100)
- });
- return newdata;
- }
-
- function draw(plot, newCtx)
- {
- if (!target) return; // if no series were passed
- ctx = newCtx;
-
- setupPie();
- var slices = plot.getData();
-
- var attempts = 0;
- while (redraw && attempts0)
- maxRadius *= shrink;
- attempts += 1;
- clear();
- if (options.series.pie.tilt<=0.8)
- drawShadow();
- drawPie();
- }
- if (attempts >= redrawAttempts) {
- clear();
- target.prepend('Could not draw pie with labels contained inside canvas
');
- }
-
- if ( plot.setSeries && plot.insertLegend )
- {
- plot.setSeries(slices);
- plot.insertLegend();
- }
-
- // we're actually done at this point, just defining internal functions at this point
-
- function clear()
- {
- ctx.clearRect(0,0,canvas.width,canvas.height);
- target.children().filter('.pieLabel, .pieLabelBackground').remove();
- }
-
- function drawShadow()
- {
- var shadowLeft = 5;
- var shadowTop = 15;
- var edge = 10;
- var alpha = 0.02;
-
- // set radius
- if (options.series.pie.radius>1)
- var radius = options.series.pie.radius;
- else
- var radius = maxRadius * options.series.pie.radius;
-
- if (radius>=(canvas.width/2)-shadowLeft || radius*options.series.pie.tilt>=(canvas.height/2)-shadowTop || radius<=edge)
- return; // shadow would be outside canvas, so don't draw it
-
- ctx.save();
- ctx.translate(shadowLeft,shadowTop);
- ctx.globalAlpha = alpha;
- ctx.fillStyle = '#000';
-
- // center and rotate to starting position
- ctx.translate(centerLeft,centerTop);
- ctx.scale(1, options.series.pie.tilt);
-
- //radius -= edge;
- for (var i=1; i<=edge; i++)
- {
- ctx.beginPath();
- ctx.arc(0,0,radius,0,Math.PI*2,false);
- ctx.fill();
- radius -= i;
- }
-
- ctx.restore();
- }
-
- function drawPie()
- {
- startAngle = Math.PI*options.series.pie.startAngle;
-
- // set radius
- if (options.series.pie.radius>1)
- var radius = options.series.pie.radius;
- else
- var radius = maxRadius * options.series.pie.radius;
-
- // center and rotate to starting position
- ctx.save();
- ctx.translate(centerLeft,centerTop);
- ctx.scale(1, options.series.pie.tilt);
- //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera
-
- // draw slices
- ctx.save();
- var currentAngle = startAngle;
- for (var i = 0; i < slices.length; ++i)
- {
- slices[i].startAngle = currentAngle;
- drawSlice(slices[i].angle, slices[i].color, true);
- }
- ctx.restore();
-
- // draw slice outlines
- ctx.save();
- ctx.lineWidth = options.series.pie.stroke.width;
- currentAngle = startAngle;
- for (var i = 0; i < slices.length; ++i)
- drawSlice(slices[i].angle, options.series.pie.stroke.color, false);
- ctx.restore();
-
- // draw donut hole
- drawDonutHole(ctx);
-
- // draw labels
- if (options.series.pie.label.show)
- drawLabels();
-
- // restore to original state
- ctx.restore();
-
- function drawSlice(angle, color, fill)
- {
- if (angle<=0)
- return;
-
- if (fill)
- ctx.fillStyle = color;
- else
- {
- ctx.strokeStyle = color;
- ctx.lineJoin = 'round';
- }
-
- ctx.beginPath();
- if (Math.abs(angle - Math.PI*2) > 0.000000001)
- ctx.moveTo(0,0); // Center of the pie
- //ctx.arc(0,0,radius,0,angle,false); // This doesn't work properly in Opera
- ctx.arc(0,0,radius,currentAngle,currentAngle+angle,false);
- ctx.closePath();
- //ctx.rotate(angle); // This doesn't work properly in Opera
- currentAngle += angle;
-
- if (fill)
- ctx.fill();
- else
- ctx.stroke();
- }
-
- function drawLabels()
- {
- var currentAngle = startAngle;
-
- // set radius
- if (options.series.pie.label.radius>1)
- var radius = options.series.pie.label.radius;
- else
- var radius = maxRadius * options.series.pie.label.radius;
-
- for (var i = 0; i < slices.length; ++i)
- {
- if (slices[i].percent >= options.series.pie.label.threshold*100)
- drawLabel(slices[i], currentAngle, i);
- currentAngle += slices[i].angle;
- }
-
- function drawLabel(slice, startAngle, index)
- {
- if (slice.data[0][1]==0)
- return;
-
- // format label text
- var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter;
- if (lf)
- text = lf(slice.label, slice);
- else
- text = slice.label;
- if (plf)
- text = plf(text, slice);
-
- var halfAngle = ((startAngle+slice.angle) + startAngle)/2;
- var x = centerLeft + Math.round(Math.cos(halfAngle) * radius);
- var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt;
-
- var html = '' + text + " ";
- target.append(html);
- var label = target.children('#pieLabel'+index);
- var labelTop = (y - label.height()/2);
- var labelLeft = (x - label.width()/2);
- label.css('top', labelTop);
- label.css('left', labelLeft);
-
- // check to make sure that the label is not outside the canvas
- if (0-labelTop>0 || 0-labelLeft>0 || canvas.height-(labelTop+label.height())<0 || canvas.width-(labelLeft+label.width())<0)
- redraw = true;
-
- if (options.series.pie.label.background.opacity != 0) {
- // put in the transparent background separately to avoid blended labels and label boxes
- var c = options.series.pie.label.background.color;
- if (c == null) {
- c = slice.color;
- }
- var pos = 'top:'+labelTop+'px;left:'+labelLeft+'px;';
- $('
').insertBefore(label).css('opacity', options.series.pie.label.background.opacity);
- }
- } // end individual label function
- } // end drawLabels function
- } // end drawPie function
- } // end draw function
-
- // Placed here because it needs to be accessed from multiple locations
- function drawDonutHole(layer)
- {
- // draw donut hole
- if(options.series.pie.innerRadius > 0)
- {
- // subtract the center
- layer.save();
- innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius;
- layer.globalCompositeOperation = 'destination-out'; // this does not work with excanvas, but it will fall back to using the stroke color
- layer.beginPath();
- layer.fillStyle = options.series.pie.stroke.color;
- layer.arc(0,0,innerRadius,0,Math.PI*2,false);
- layer.fill();
- layer.closePath();
- layer.restore();
-
- // add inner stroke
- layer.save();
- layer.beginPath();
- layer.strokeStyle = options.series.pie.stroke.color;
- layer.arc(0,0,innerRadius,0,Math.PI*2,false);
- layer.stroke();
- layer.closePath();
- layer.restore();
- // TODO: add extra shadow inside hole (with a mask) if the pie is tilted.
- }
- }
-
- //-- Additional Interactive related functions --
-
- function isPointInPoly(poly, pt)
- {
- for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i)
- ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1]))
- && (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0])
- && (c = !c);
- return c;
- }
-
- function findNearbySlice(mouseX, mouseY)
- {
- var slices = plot.getData(),
- options = plot.getOptions(),
- radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
-
- for (var i = 0; i < slices.length; ++i)
- {
- var s = slices[i];
-
- if(s.pie.show)
- {
- ctx.save();
- ctx.beginPath();
- ctx.moveTo(0,0); // Center of the pie
- //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here.
- ctx.arc(0,0,radius,s.startAngle,s.startAngle+s.angle,false);
- ctx.closePath();
- x = mouseX-centerLeft;
- y = mouseY-centerTop;
- if(ctx.isPointInPath)
- {
- if (ctx.isPointInPath(mouseX-centerLeft, mouseY-centerTop))
- {
- //alert('found slice!');
- ctx.restore();
- return {datapoint: [s.percent, s.data], dataIndex: 0, series: s, seriesIndex: i};
- }
- }
- else
- {
- // excanvas for IE doesn;t support isPointInPath, this is a workaround.
- p1X = (radius * Math.cos(s.startAngle));
- p1Y = (radius * Math.sin(s.startAngle));
- p2X = (radius * Math.cos(s.startAngle+(s.angle/4)));
- p2Y = (radius * Math.sin(s.startAngle+(s.angle/4)));
- p3X = (radius * Math.cos(s.startAngle+(s.angle/2)));
- p3Y = (radius * Math.sin(s.startAngle+(s.angle/2)));
- p4X = (radius * Math.cos(s.startAngle+(s.angle/1.5)));
- p4Y = (radius * Math.sin(s.startAngle+(s.angle/1.5)));
- p5X = (radius * Math.cos(s.startAngle+s.angle));
- p5Y = (radius * Math.sin(s.startAngle+s.angle));
- arrPoly = [[0,0],[p1X,p1Y],[p2X,p2Y],[p3X,p3Y],[p4X,p4Y],[p5X,p5Y]];
- arrPoint = [x,y];
- // TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt?
- if(isPointInPoly(arrPoly, arrPoint))
- {
- ctx.restore();
- return {datapoint: [s.percent, s.data], dataIndex: 0, series: s, seriesIndex: i};
- }
- }
- ctx.restore();
- }
- }
-
- return null;
- }
-
- function onMouseMove(e)
- {
- triggerClickHoverEvent('plothover', e);
- }
-
- function onClick(e)
- {
- triggerClickHoverEvent('plotclick', e);
- }
-
- // trigger click or hover event (they send the same parameters so we share their code)
- function triggerClickHoverEvent(eventname, e)
- {
- var offset = plot.offset(),
- canvasX = parseInt(e.pageX - offset.left),
- canvasY = parseInt(e.pageY - offset.top),
- item = findNearbySlice(canvasX, canvasY);
-
- if (options.grid.autoHighlight)
- {
- // clear auto-highlights
- for (var i = 0; i < highlights.length; ++i)
- {
- var h = highlights[i];
- if (h.auto == eventname && !(item && h.series == item.series))
- unhighlight(h.series);
- }
- }
-
- // highlight the slice
- if (item)
- highlight(item.series, eventname);
-
- // trigger any hover bind events
- var pos = { pageX: e.pageX, pageY: e.pageY };
- target.trigger(eventname, [ pos, item ]);
- }
-
- function highlight(s, auto)
- {
- if (typeof s == "number")
- s = series[s];
-
- var i = indexOfHighlight(s);
- if (i == -1)
- {
- highlights.push({ series: s, auto: auto });
- plot.triggerRedrawOverlay();
- }
- else if (!auto)
- highlights[i].auto = false;
- }
-
- function unhighlight(s)
- {
- if (s == null)
- {
- highlights = [];
- plot.triggerRedrawOverlay();
- }
-
- if (typeof s == "number")
- s = series[s];
-
- var i = indexOfHighlight(s);
- if (i != -1)
- {
- highlights.splice(i, 1);
- plot.triggerRedrawOverlay();
- }
- }
-
- function indexOfHighlight(s)
- {
- for (var i = 0; i < highlights.length; ++i)
- {
- var h = highlights[i];
- if (h.series == s)
- return i;
- }
- return -1;
- }
-
- function drawOverlay(plot, octx)
- {
- //alert(options.series.pie.radius);
- var options = plot.getOptions();
- //alert(options.series.pie.radius);
-
- var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
-
- octx.save();
- octx.translate(centerLeft, centerTop);
- octx.scale(1, options.series.pie.tilt);
-
- for (i = 0; i < highlights.length; ++i)
- drawHighlight(highlights[i].series);
-
- drawDonutHole(octx);
-
- octx.restore();
-
- function drawHighlight(series)
- {
- if (series.angle < 0) return;
-
- //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString();
- octx.fillStyle = "rgba(255, 255, 255, "+options.series.pie.highlight.opacity+")"; // this is temporary until we have access to parseColor
-
- octx.beginPath();
- if (Math.abs(series.angle - Math.PI*2) > 0.000000001)
- octx.moveTo(0,0); // Center of the pie
- octx.arc(0,0,radius,series.startAngle,series.startAngle+series.angle,false);
- octx.closePath();
- octx.fill();
- }
-
- }
-
- } // end init (plugin body)
-
- // define pie specific options and their default values
- var options = {
- series: {
- pie: {
- show: false,
- radius: 'auto', // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value)
- innerRadius:0, /* for donut */
- startAngle: 3/2,
- tilt: 1,
- offset: {
- top: 0,
- left: 'auto'
- },
- stroke: {
- color: '#FFF',
- width: 1
- },
- label: {
- show: 'auto',
- formatter: function(label, slice){
- return ''+label+' '+Math.round(slice.percent)+'%
';
- }, // formatter function
- radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value)
- background: {
- color: null,
- opacity: 0
- },
- threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow)
- },
- combine: {
- threshold: -1, // percentage at which to combine little slices into one larger slice
- color: null, // color to give the new slice (auto-generated if null)
- label: 'Other' // label to give the new slice
- },
- highlight: {
- //color: '#FFF', // will add this functionality once parseColor is available
- opacity: 0.5
- }
- }
- }
- };
-
- $.plot.plugins.push({
- init: init,
- options: options,
- name: "pie",
- version: "1.0"
- });
-})(jQuery);
diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.pie.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.pie.min.js
deleted file mode 100644
index b7bf870d759a..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.flot.pie.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(function(b){function c(D){var h=null;var L=null;var n=null;var B=null;var p=null;var M=0;var F=true;var o=10;var w=0.95;var A=0;var d=false;var z=false;var j=[];D.hooks.processOptions.push(g);D.hooks.bindEvents.push(e);function g(O,N){if(N.series.pie.show){N.grid.show=false;if(N.series.pie.label.show=="auto"){if(N.legend.show){N.series.pie.label.show=false}else{N.series.pie.label.show=true}}if(N.series.pie.radius=="auto"){if(N.series.pie.label.show){N.series.pie.radius=3/4}else{N.series.pie.radius=1}}if(N.series.pie.tilt>1){N.series.pie.tilt=1}if(N.series.pie.tilt<0){N.series.pie.tilt=0}O.hooks.processDatapoints.push(E);O.hooks.drawOverlay.push(H);O.hooks.draw.push(r)}}function e(P,N){var O=P.getOptions();if(O.series.pie.show&&O.grid.hoverable){N.unbind("mousemove").mousemove(t)}if(O.series.pie.show&&O.grid.clickable){N.unbind("click").click(l)}}function G(O){var P="";function N(S,T){if(!T){T=0}for(var R=0;Rh.width-n){B=h.width-n}}}function v(O){for(var N=0;N0){R.push({data:[[1,P]],color:N,label:a.series.pie.combine.label,angle:(P*(Math.PI*2))/M,percent:(P/M*100)})}return R}function r(S,Q){if(!L){return}ctx=Q;I();var T=S.getData();var P=0;while(F&&P0){n*=w}P+=1;N();if(a.series.pie.tilt<=0.8){O()}R()}if(P>=o){N();L.prepend('Could not draw pie with labels contained inside canvas
')}if(S.setSeries&&S.insertLegend){S.setSeries(T);S.insertLegend()}function N(){ctx.clearRect(0,0,h.width,h.height);L.children().filter(".pieLabel, .pieLabelBackground").remove()}function O(){var Z=5;var Y=15;var W=10;var X=0.02;if(a.series.pie.radius>1){var U=a.series.pie.radius}else{var U=n*a.series.pie.radius}if(U>=(h.width/2)-Z||U*a.series.pie.tilt>=(h.height/2)-Y||U<=W){return}ctx.save();ctx.translate(Z,Y);ctx.globalAlpha=X;ctx.fillStyle="#000";ctx.translate(B,p);ctx.scale(1,a.series.pie.tilt);for(var V=1;V<=W;V++){ctx.beginPath();ctx.arc(0,0,U,0,Math.PI*2,false);ctx.fill();U-=V}ctx.restore()}function R(){startAngle=Math.PI*a.series.pie.startAngle;if(a.series.pie.radius>1){var U=a.series.pie.radius}else{var U=n*a.series.pie.radius}ctx.save();ctx.translate(B,p);ctx.scale(1,a.series.pie.tilt);ctx.save();var Y=startAngle;for(var W=0;W1e-9){ctx.moveTo(0,0)}else{if(b.browser.msie){ab-=0.0001}}ctx.arc(0,0,U,Y,Y+ab,false);ctx.closePath();Y+=ab;if(aa){ctx.fill()}else{ctx.stroke()}}function V(){var ac=startAngle;if(a.series.pie.label.radius>1){var Z=a.series.pie.label.radius}else{var Z=n*a.series.pie.label.radius}for(var ab=0;ab=a.series.pie.label.threshold*100){aa(T[ab],ac,ab)}ac+=T[ab].angle}function aa(ap,ai,ag){if(ap.data[0][1]==0){return}var ar=a.legend.labelFormatter,aq,ae=a.series.pie.label.formatter;if(ar){aq=ar(ap.label,ap)}else{aq=ap.label}if(ae){aq=ae(aq,ap)}var aj=((ai+ap.angle)+ai)/2;var ao=B+Math.round(Math.cos(aj)*Z);var am=p+Math.round(Math.sin(aj)*Z)*a.series.pie.tilt;var af=''+aq+" ";L.append(af);var an=L.children("#pieLabel"+ag);var ad=(am-an.height()/2);var ah=(ao-an.width()/2);an.css("top",ad);an.css("left",ah);if(0-ad>0||0-ah>0||h.height-(ad+an.height())<0||h.width-(ah+an.width())<0){F=true}if(a.series.pie.label.background.opacity!=0){var ak=a.series.pie.label.background.color;if(ak==null){ak=ap.color}var al="top:"+ad+"px;left:"+ah+"px;";b('
').insertBefore(an).css("opacity",a.series.pie.label.background.opacity)}}}}}function J(N){if(a.series.pie.innerRadius>0){N.save();innerRadius=a.series.pie.innerRadius>1?a.series.pie.innerRadius:n*a.series.pie.innerRadius;N.globalCompositeOperation="destination-out";N.beginPath();N.fillStyle=a.series.pie.stroke.color;N.arc(0,0,innerRadius,0,Math.PI*2,false);N.fill();N.closePath();N.restore();N.save();N.beginPath();N.strokeStyle=a.series.pie.stroke.color;N.arc(0,0,innerRadius,0,Math.PI*2,false);N.stroke();N.closePath();N.restore()}}function s(Q,R){for(var S=false,P=-1,N=Q.length,O=N-1;++P1?O.series.pie.radius:n*O.series.pie.radius;for(var Q=0;Q1?P.series.pie.radius:n*P.series.pie.radius;R.save();R.translate(B,p);R.scale(1,P.series.pie.tilt);for(i=0;i1e-9){R.moveTo(0,0)}R.arc(0,0,N,S.startAngle,S.startAngle+S.angle,false);R.closePath();R.fill()}}}var a={series:{pie:{show:false,radius:"auto",innerRadius:0,startAngle:3/2,tilt:1,offset:{top:0,left:"auto"},stroke:{color:"#FFF",width:1},label:{show:"auto",formatter:function(d,e){return''+d+" "+Math.round(e.percent)+"%
"},radius:1,background:{color:null,opacity:0},threshold:0},combine:{threshold:-1,color:null,label:"Other"},highlight:{opacity:0.5}}}};b.plot.plugins.push({init:c,options:a,name:"pie",version:"1.0"})})(jQuery);
\ No newline at end of file
diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.resize.js b/frontend/src/vendor/jquery.flot/jquery.flot.resize.js
deleted file mode 100644
index 69dfb24f38e2..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.flot.resize.js
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
-Flot plugin for automatically redrawing plots when the placeholder
-size changes, e.g. on window resizes.
-
-It works by listening for changes on the placeholder div (through the
-jQuery resize event plugin) - if the size changes, it will redraw the
-plot.
-
-There are no options. If you need to disable the plugin for some
-plots, you can just fix the size of their placeholders.
-*/
-
-
-/* Inline dependency:
- * jQuery resize event - v1.1 - 3/14/2010
- * http://benalman.com/projects/jquery-resize-plugin/
- *
- * Copyright (c) 2010 "Cowboy" Ben Alman
- * Dual licensed under the MIT and GPL licenses.
- * http://benalman.com/about/license/
- */
-(function($,h,c){var a=$([]),e=$.resize=$.extend($.resize,{}),i,k="setTimeout",j="resize",d=j+"-special-event",b="delay",f="throttleWindow";e[b]=250;e[f]=true;$.event.special[j]={setup:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.add(l);$.data(this,d,{w:l.width(),h:l.height()});if(a.length===1){g()}},teardown:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.not(l);l.removeData(d);if(!a.length){clearTimeout(i)}},add:function(l){if(!e[f]&&this[k]){return false}var n;function m(s,o,p){var q=$(this),r=$.data(this,d);r.w=o!==c?o:q.width();r.h=p!==c?p:q.height();n.apply(this,arguments)}if($.isFunction(l)){n=l;return m}else{n=l.handler;l.handler=m}}};function g(){i=h[k](function(){a.each(function(){var n=$(this),m=n.width(),l=n.height(),o=$.data(this,d);if(m!==o.w||l!==o.h){n.trigger(j,[o.w=m,o.h=l])}});g()},e[b])}})(jQuery,this);
-
-
-(function ($) {
- var options = { }; // no options
-
- function init(plot) {
- function onResize() {
- var placeholder = plot.getPlaceholder();
-
- // somebody might have hidden us and we can't plot
- // when we don't have the dimensions
- if (placeholder.width() == 0 || placeholder.height() == 0)
- return;
-
- plot.resize();
- plot.setupGrid();
- plot.draw();
- }
-
- function bindEvents(plot, eventHolder) {
- plot.getPlaceholder().resize(onResize);
- }
-
- function shutdown(plot, eventHolder) {
- plot.getPlaceholder().unbind("resize", onResize);
- }
-
- plot.hooks.bindEvents.push(bindEvents);
- plot.hooks.shutdown.push(shutdown);
- }
-
- $.plot.plugins.push({
- init: init,
- options: options,
- name: 'resize',
- version: '1.0'
- });
-})(jQuery);
diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.resize.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.resize.min.js
deleted file mode 100644
index 1fa0771f570e..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.flot.resize.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(function(n,p,u){var w=n([]),s=n.resize=n.extend(n.resize,{}),o,l="setTimeout",m="resize",t=m+"-special-event",v="delay",r="throttleWindow";s[v]=250;s[r]=true;n.event.special[m]={setup:function(){if(!s[r]&&this[l]){return false}var a=n(this);w=w.add(a);n.data(this,t,{w:a.width(),h:a.height()});if(w.length===1){q()}},teardown:function(){if(!s[r]&&this[l]){return false}var a=n(this);w=w.not(a);a.removeData(t);if(!w.length){clearTimeout(o)}},add:function(b){if(!s[r]&&this[l]){return false}var c;function a(d,h,g){var f=n(this),e=n.data(this,t);e.w=h!==u?h:f.width();e.h=g!==u?g:f.height();c.apply(this,arguments)}if(n.isFunction(b)){c=b;return a}else{c=b.handler;b.handler=a}}};function q(){o=p[l](function(){w.each(function(){var d=n(this),a=d.width(),b=d.height(),c=n.data(this,t);if(a!==c.w||b!==c.h){d.trigger(m,[c.w=a,c.h=b])}});q()},s[v])}})(jQuery,this);(function(b){var a={};function c(f){function e(){var h=f.getPlaceholder();if(h.width()==0||h.height()==0){return}f.resize();f.setupGrid();f.draw()}function g(i,h){i.getPlaceholder().resize(e)}function d(i,h){i.getPlaceholder().unbind("resize",e)}f.hooks.bindEvents.push(g);f.hooks.shutdown.push(d)}b.plot.plugins.push({init:c,options:a,name:"resize",version:"1.0"})})(jQuery);
\ No newline at end of file
diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.selection.js b/frontend/src/vendor/jquery.flot/jquery.flot.selection.js
deleted file mode 100644
index 7f7b32694bda..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.flot.selection.js
+++ /dev/null
@@ -1,344 +0,0 @@
-/*
-Flot plugin for selecting regions.
-
-The plugin defines the following options:
-
- selection: {
- mode: null or "x" or "y" or "xy",
- color: color
- }
-
-Selection support is enabled by setting the mode to one of "x", "y" or
-"xy". In "x" mode, the user will only be able to specify the x range,
-similarly for "y" mode. For "xy", the selection becomes a rectangle
-where both ranges can be specified. "color" is color of the selection
-(if you need to change the color later on, you can get to it with
-plot.getOptions().selection.color).
-
-When selection support is enabled, a "plotselected" event will be
-emitted on the DOM element you passed into the plot function. The
-event handler gets a parameter with the ranges selected on the axes,
-like this:
-
- placeholder.bind("plotselected", function(event, ranges) {
- alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to)
- // similar for yaxis - with multiple axes, the extra ones are in
- // x2axis, x3axis, ...
- });
-
-The "plotselected" event is only fired when the user has finished
-making the selection. A "plotselecting" event is fired during the
-process with the same parameters as the "plotselected" event, in case
-you want to know what's happening while it's happening,
-
-A "plotunselected" event with no arguments is emitted when the user
-clicks the mouse to remove the selection.
-
-The plugin allso adds the following methods to the plot object:
-
-- setSelection(ranges, preventEvent)
-
- Set the selection rectangle. The passed in ranges is on the same
- form as returned in the "plotselected" event. If the selection mode
- is "x", you should put in either an xaxis range, if the mode is "y"
- you need to put in an yaxis range and both xaxis and yaxis if the
- selection mode is "xy", like this:
-
- setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } });
-
- setSelection will trigger the "plotselected" event when called. If
- you don't want that to happen, e.g. if you're inside a
- "plotselected" handler, pass true as the second parameter. If you
- are using multiple axes, you can specify the ranges on any of those,
- e.g. as x2axis/x3axis/... instead of xaxis, the plugin picks the
- first one it sees.
-
-- clearSelection(preventEvent)
-
- Clear the selection rectangle. Pass in true to avoid getting a
- "plotunselected" event.
-
-- getSelection()
-
- Returns the current selection in the same format as the
- "plotselected" event. If there's currently no selection, the
- function returns null.
-
-*/
-
-(function ($) {
- function init(plot) {
- var selection = {
- first: { x: -1, y: -1}, second: { x: -1, y: -1},
- show: false,
- active: false
- };
-
- // FIXME: The drag handling implemented here should be
- // abstracted out, there's some similar code from a library in
- // the navigation plugin, this should be massaged a bit to fit
- // the Flot cases here better and reused. Doing this would
- // make this plugin much slimmer.
- var savedhandlers = {};
-
- var mouseUpHandler = null;
-
- function onMouseMove(e) {
- if (selection.active) {
- updateSelection(e);
-
- plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]);
- }
- }
-
- function onMouseDown(e) {
- if (e.which != 1) // only accept left-click
- return;
-
- // cancel out any text selections
- document.body.focus();
-
- // prevent text selection and drag in old-school browsers
- if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) {
- savedhandlers.onselectstart = document.onselectstart;
- document.onselectstart = function () { return false; };
- }
- if (document.ondrag !== undefined && savedhandlers.ondrag == null) {
- savedhandlers.ondrag = document.ondrag;
- document.ondrag = function () { return false; };
- }
-
- setSelectionPos(selection.first, e);
-
- selection.active = true;
-
- // this is a bit silly, but we have to use a closure to be
- // able to whack the same handler again
- mouseUpHandler = function (e) { onMouseUp(e); };
-
- $(document).one("mouseup", mouseUpHandler);
- }
-
- function onMouseUp(e) {
- mouseUpHandler = null;
-
- // revert drag stuff for old-school browsers
- if (document.onselectstart !== undefined)
- document.onselectstart = savedhandlers.onselectstart;
- if (document.ondrag !== undefined)
- document.ondrag = savedhandlers.ondrag;
-
- // no more dragging
- selection.active = false;
- updateSelection(e);
-
- if (selectionIsSane())
- triggerSelectedEvent();
- else {
- // this counts as a clear
- plot.getPlaceholder().trigger("plotunselected", [ ]);
- plot.getPlaceholder().trigger("plotselecting", [ null ]);
- }
-
- return false;
- }
-
- function getSelection() {
- if (!selectionIsSane())
- return null;
-
- var r = {}, c1 = selection.first, c2 = selection.second;
- $.each(plot.getAxes(), function (name, axis) {
- if (axis.used) {
- var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]);
- r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) };
- }
- });
- return r;
- }
-
- function triggerSelectedEvent() {
- var r = getSelection();
-
- plot.getPlaceholder().trigger("plotselected", [ r ]);
-
- // backwards-compat stuff, to be removed in future
- if (r.xaxis && r.yaxis)
- plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]);
- }
-
- function clamp(min, value, max) {
- return value < min ? min: (value > max ? max: value);
- }
-
- function setSelectionPos(pos, e) {
- var o = plot.getOptions();
- var offset = plot.getPlaceholder().offset();
- var plotOffset = plot.getPlotOffset();
- pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width());
- pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height());
-
- if (o.selection.mode == "y")
- pos.x = pos == selection.first ? 0 : plot.width();
-
- if (o.selection.mode == "x")
- pos.y = pos == selection.first ? 0 : plot.height();
- }
-
- function updateSelection(pos) {
- if (pos.pageX == null)
- return;
-
- setSelectionPos(selection.second, pos);
- if (selectionIsSane()) {
- selection.show = true;
- plot.triggerRedrawOverlay();
- }
- else
- clearSelection(true);
- }
-
- function clearSelection(preventEvent) {
- if (selection.show) {
- selection.show = false;
- plot.triggerRedrawOverlay();
- if (!preventEvent)
- plot.getPlaceholder().trigger("plotunselected", [ ]);
- }
- }
-
- // function taken from markings support in Flot
- function extractRange(ranges, coord) {
- var axis, from, to, key, axes = plot.getAxes();
-
- for (var k in axes) {
- axis = axes[k];
- if (axis.direction == coord) {
- key = coord + axis.n + "axis";
- if (!ranges[key] && axis.n == 1)
- key = coord + "axis"; // support x1axis as xaxis
- if (ranges[key]) {
- from = ranges[key].from;
- to = ranges[key].to;
- break;
- }
- }
- }
-
- // backwards-compat stuff - to be removed in future
- if (!ranges[key]) {
- axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0];
- from = ranges[coord + "1"];
- to = ranges[coord + "2"];
- }
-
- // auto-reverse as an added bonus
- if (from != null && to != null && from > to) {
- var tmp = from;
- from = to;
- to = tmp;
- }
-
- return { from: from, to: to, axis: axis };
- }
-
- function setSelection(ranges, preventEvent) {
- var axis, range, o = plot.getOptions();
-
- if (o.selection.mode == "y") {
- selection.first.x = 0;
- selection.second.x = plot.width();
- }
- else {
- range = extractRange(ranges, "x");
-
- selection.first.x = range.axis.p2c(range.from);
- selection.second.x = range.axis.p2c(range.to);
- }
-
- if (o.selection.mode == "x") {
- selection.first.y = 0;
- selection.second.y = plot.height();
- }
- else {
- range = extractRange(ranges, "y");
-
- selection.first.y = range.axis.p2c(range.from);
- selection.second.y = range.axis.p2c(range.to);
- }
-
- selection.show = true;
- plot.triggerRedrawOverlay();
- if (!preventEvent && selectionIsSane())
- triggerSelectedEvent();
- }
-
- function selectionIsSane() {
- var minSize = 5;
- return Math.abs(selection.second.x - selection.first.x) >= minSize &&
- Math.abs(selection.second.y - selection.first.y) >= minSize;
- }
-
- plot.clearSelection = clearSelection;
- plot.setSelection = setSelection;
- plot.getSelection = getSelection;
-
- plot.hooks.bindEvents.push(function(plot, eventHolder) {
- var o = plot.getOptions();
- if (o.selection.mode != null) {
- eventHolder.mousemove(onMouseMove);
- eventHolder.mousedown(onMouseDown);
- }
- });
-
-
- plot.hooks.drawOverlay.push(function (plot, ctx) {
- // draw selection
- if (selection.show && selectionIsSane()) {
- var plotOffset = plot.getPlotOffset();
- var o = plot.getOptions();
-
- ctx.save();
- ctx.translate(plotOffset.left, plotOffset.top);
-
- var c = $.color.parse(o.selection.color);
-
- ctx.strokeStyle = c.scale('a', 0.8).toString();
- ctx.lineWidth = 1;
- ctx.lineJoin = "round";
- ctx.fillStyle = c.scale('a', 0.4).toString();
-
- var x = Math.min(selection.first.x, selection.second.x),
- y = Math.min(selection.first.y, selection.second.y),
- w = Math.abs(selection.second.x - selection.first.x),
- h = Math.abs(selection.second.y - selection.first.y);
-
- ctx.fillRect(x, y, w, h);
- ctx.strokeRect(x, y, w, h);
-
- ctx.restore();
- }
- });
-
- plot.hooks.shutdown.push(function (plot, eventHolder) {
- eventHolder.unbind("mousemove", onMouseMove);
- eventHolder.unbind("mousedown", onMouseDown);
-
- if (mouseUpHandler)
- $(document).unbind("mouseup", mouseUpHandler);
- });
-
- }
-
- $.plot.plugins.push({
- init: init,
- options: {
- selection: {
- mode: null, // one of null, "x", "y" or "xy"
- color: "#e8cfac"
- }
- },
- name: 'selection',
- version: '1.1'
- });
-})(jQuery);
diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.selection.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.selection.min.js
deleted file mode 100644
index badc0052dbe0..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.flot.selection.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(function(a){function b(k){var p={first:{x:-1,y:-1},second:{x:-1,y:-1},show:false,active:false};var m={};var r=null;function e(s){if(p.active){l(s);k.getPlaceholder().trigger("plotselecting",[g()])}}function n(s){if(s.which!=1){return}document.body.focus();if(document.onselectstart!==undefined&&m.onselectstart==null){m.onselectstart=document.onselectstart;document.onselectstart=function(){return false}}if(document.ondrag!==undefined&&m.ondrag==null){m.ondrag=document.ondrag;document.ondrag=function(){return false}}d(p.first,s);p.active=true;r=function(t){j(t)};a(document).one("mouseup",r)}function j(s){r=null;if(document.onselectstart!==undefined){document.onselectstart=m.onselectstart}if(document.ondrag!==undefined){document.ondrag=m.ondrag}p.active=false;l(s);if(f()){i()}else{k.getPlaceholder().trigger("plotunselected",[]);k.getPlaceholder().trigger("plotselecting",[null])}return false}function g(){if(!f()){return null}var u={},t=p.first,s=p.second;a.each(k.getAxes(),function(v,w){if(w.used){var y=w.c2p(t[w.direction]),x=w.c2p(s[w.direction]);u[v]={from:Math.min(y,x),to:Math.max(y,x)}}});return u}function i(){var s=g();k.getPlaceholder().trigger("plotselected",[s]);if(s.xaxis&&s.yaxis){k.getPlaceholder().trigger("selected",[{x1:s.xaxis.from,y1:s.yaxis.from,x2:s.xaxis.to,y2:s.yaxis.to}])}}function h(t,u,s){return us?s:u)}function d(w,t){var v=k.getOptions();var u=k.getPlaceholder().offset();var s=k.getPlotOffset();w.x=h(0,t.pageX-u.left-s.left,k.width());w.y=h(0,t.pageY-u.top-s.top,k.height());if(v.selection.mode=="y"){w.x=w==p.first?0:k.width()}if(v.selection.mode=="x"){w.y=w==p.first?0:k.height()}}function l(s){if(s.pageX==null){return}d(p.second,s);if(f()){p.show=true;k.triggerRedrawOverlay()}else{q(true)}}function q(s){if(p.show){p.show=false;k.triggerRedrawOverlay();if(!s){k.getPlaceholder().trigger("plotunselected",[])}}}function c(s,w){var t,y,z,A,x=k.getAxes();for(var u in x){t=x[u];if(t.direction==w){A=w+t.n+"axis";if(!s[A]&&t.n==1){A=w+"axis"}if(s[A]){y=s[A].from;z=s[A].to;break}}}if(!s[A]){t=w=="x"?k.getXAxes()[0]:k.getYAxes()[0];y=s[w+"1"];z=s[w+"2"]}if(y!=null&&z!=null&&y>z){var v=y;y=z;z=v}return{from:y,to:z,axis:t}}function o(t,s){var v,u,w=k.getOptions();if(w.selection.mode=="y"){p.first.x=0;p.second.x=k.width()}else{u=c(t,"x");p.first.x=u.axis.p2c(u.from);p.second.x=u.axis.p2c(u.to)}if(w.selection.mode=="x"){p.first.y=0;p.second.y=k.height()}else{u=c(t,"y");p.first.y=u.axis.p2c(u.from);p.second.y=u.axis.p2c(u.to)}p.show=true;k.triggerRedrawOverlay();if(!s&&f()){i()}}function f(){var s=5;return Math.abs(p.second.x-p.first.x)>=s&&Math.abs(p.second.y-p.first.y)>=s}k.clearSelection=q;k.setSelection=o;k.getSelection=g;k.hooks.bindEvents.push(function(t,s){var u=t.getOptions();if(u.selection.mode!=null){s.mousemove(e);s.mousedown(n)}});k.hooks.drawOverlay.push(function(v,D){if(p.show&&f()){var t=v.getPlotOffset();var s=v.getOptions();D.save();D.translate(t.left,t.top);var z=a.color.parse(s.selection.color);D.strokeStyle=z.scale("a",0.8).toString();D.lineWidth=1;D.lineJoin="round";D.fillStyle=z.scale("a",0.4).toString();var B=Math.min(p.first.x,p.second.x),A=Math.min(p.first.y,p.second.y),C=Math.abs(p.second.x-p.first.x),u=Math.abs(p.second.y-p.first.y);D.fillRect(B,A,C,u);D.strokeRect(B,A,C,u);D.restore()}});k.hooks.shutdown.push(function(t,s){s.unbind("mousemove",e);s.unbind("mousedown",n);if(r){a(document).unbind("mouseup",r)}})}a.plot.plugins.push({init:b,options:{selection:{mode:null,color:"#e8cfac"}},name:"selection",version:"1.1"})})(jQuery);
\ No newline at end of file
diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.stack.js b/frontend/src/vendor/jquery.flot/jquery.flot.stack.js
deleted file mode 100644
index a31d5dc9b586..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.flot.stack.js
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
-Flot plugin for stacking data sets, i.e. putting them on top of each
-other, for accumulative graphs.
-
-The plugin assumes the data is sorted on x (or y if stacking
-horizontally). For line charts, it is assumed that if a line has an
-undefined gap (from a null point), then the line above it should have
-the same gap - insert zeros instead of "null" if you want another
-behaviour. This also holds for the start and end of the chart. Note
-that stacking a mix of positive and negative values in most instances
-doesn't make sense (so it looks weird).
-
-Two or more series are stacked when their "stack" attribute is set to
-the same key (which can be any number or string or just "true"). To
-specify the default stack, you can set
-
- series: {
- stack: null or true or key (number/string)
- }
-
-or specify it for a specific series
-
- $.plot($("#placeholder"), [{ data: [ ... ], stack: true }])
-
-The stacking order is determined by the order of the data series in
-the array (later series end up on top of the previous).
-
-Internally, the plugin modifies the datapoints in each series, adding
-an offset to the y value. For line series, extra data points are
-inserted through interpolation. If there's a second y value, it's also
-adjusted (e.g for bar charts or filled areas).
-*/
-
-(function ($) {
- var options = {
- series: { stack: null } // or number/string
- };
-
- function init(plot) {
- function findMatchingSeries(s, allseries) {
- var res = null
- for (var i = 0; i < allseries.length; ++i) {
- if (s == allseries[i])
- break;
-
- if (allseries[i].stack == s.stack)
- res = allseries[i];
- }
-
- return res;
- }
-
- function stackData(plot, s, datapoints) {
- if (s.stack == null)
- return;
-
- var other = findMatchingSeries(s, plot.getData());
- if (!other)
- return;
-
- var ps = datapoints.pointsize,
- points = datapoints.points,
- otherps = other.datapoints.pointsize,
- otherpoints = other.datapoints.points,
- newpoints = [],
- px, py, intery, qx, qy, bottom,
- withlines = s.lines.show,
- horizontal = s.bars.horizontal,
- withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y),
- withsteps = withlines && s.lines.steps,
- fromgap = true,
- keyOffset = horizontal ? 1 : 0,
- accumulateOffset = horizontal ? 0 : 1,
- i = 0, j = 0, l;
-
- while (true) {
- if (i >= points.length)
- break;
-
- l = newpoints.length;
-
- if (points[i] == null) {
- // copy gaps
- for (m = 0; m < ps; ++m)
- newpoints.push(points[i + m]);
- i += ps;
- }
- else if (j >= otherpoints.length) {
- // for lines, we can't use the rest of the points
- if (!withlines) {
- for (m = 0; m < ps; ++m)
- newpoints.push(points[i + m]);
- }
- i += ps;
- }
- else if (otherpoints[j] == null) {
- // oops, got a gap
- for (m = 0; m < ps; ++m)
- newpoints.push(null);
- fromgap = true;
- j += otherps;
- }
- else {
- // cases where we actually got two points
- px = points[i + keyOffset];
- py = points[i + accumulateOffset];
- qx = otherpoints[j + keyOffset];
- qy = otherpoints[j + accumulateOffset];
- bottom = 0;
-
- if (px == qx) {
- for (m = 0; m < ps; ++m)
- newpoints.push(points[i + m]);
-
- newpoints[l + accumulateOffset] += qy;
- bottom = qy;
-
- i += ps;
- j += otherps;
- }
- else if (px > qx) {
- // we got past point below, might need to
- // insert interpolated extra point
- if (withlines && i > 0 && points[i - ps] != null) {
- intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px);
- newpoints.push(qx);
- newpoints.push(intery + qy);
- for (m = 2; m < ps; ++m)
- newpoints.push(points[i + m]);
- bottom = qy;
- }
-
- j += otherps;
- }
- else { // px < qx
- if (fromgap && withlines) {
- // if we come from a gap, we just skip this point
- i += ps;
- continue;
- }
-
- for (m = 0; m < ps; ++m)
- newpoints.push(points[i + m]);
-
- // we might be able to interpolate a point below,
- // this can give us a better y
- if (withlines && j > 0 && otherpoints[j - otherps] != null)
- bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx);
-
- newpoints[l + accumulateOffset] += bottom;
-
- i += ps;
- }
-
- fromgap = false;
-
- if (l != newpoints.length && withbottom)
- newpoints[l + 2] += bottom;
- }
-
- // maintain the line steps invariant
- if (withsteps && l != newpoints.length && l > 0
- && newpoints[l] != null
- && newpoints[l] != newpoints[l - ps]
- && newpoints[l + 1] != newpoints[l - ps + 1]) {
- for (m = 0; m < ps; ++m)
- newpoints[l + ps + m] = newpoints[l + m];
- newpoints[l + 1] = newpoints[l - ps + 1];
- }
- }
-
- datapoints.points = newpoints;
- }
-
- plot.hooks.processDatapoints.push(stackData);
- }
-
- $.plot.plugins.push({
- init: init,
- options: options,
- name: 'stack',
- version: '1.2'
- });
-})(jQuery);
diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.stack.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.stack.min.js
deleted file mode 100644
index bba2a0e5ff7e..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.flot.stack.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(function(b){var a={series:{stack:null}};function c(f){function d(k,j){var h=null;for(var g=0;g2&&(G?g.format[2].x:g.format[2].y),n=u&&v.lines.steps,E=true,q=G?1:0,H=G?0:1,D=0,B=0,A;while(true){if(D>=F.length){break}A=t.length;if(F[D]==null){for(m=0;m=y.length){if(!u){for(m=0;mJ){if(u&&D>0&&F[D-z]!=null){k=w+(F[D-z+H]-w)*(J-x)/(F[D-z+q]-x);t.push(J);t.push(k+I);for(m=2;m0&&y[B-h]!=null){r=I+(y[B-h+H]-I)*(x-J)/(y[B-h+q]-J)}t[A+H]+=r;D+=z}}E=false;if(A!=t.length&&o){t[A+2]+=r}}}}if(n&&A!=t.length&&A>0&&t[A]!=null&&t[A]!=t[A-z]&&t[A+1]!=t[A-z+1]){for(m=0;m s = r * sqrt(pi)/2
- var size = radius * Math.sqrt(Math.PI) / 2;
- ctx.rect(x - size, y - size, size + size, size + size);
- },
- diamond: function (ctx, x, y, radius, shadow) {
- // pi * r^2 = 2s^2 => s = r * sqrt(pi/2)
- var size = radius * Math.sqrt(Math.PI / 2);
- ctx.moveTo(x - size, y);
- ctx.lineTo(x, y - size);
- ctx.lineTo(x + size, y);
- ctx.lineTo(x, y + size);
- ctx.lineTo(x - size, y);
- },
- triangle: function (ctx, x, y, radius, shadow) {
- // pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3))
- var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3));
- var height = size * Math.sin(Math.PI / 3);
- ctx.moveTo(x - size/2, y + height/2);
- ctx.lineTo(x + size/2, y + height/2);
- if (!shadow) {
- ctx.lineTo(x, y - height/2);
- ctx.lineTo(x - size/2, y + height/2);
- }
- },
- cross: function (ctx, x, y, radius, shadow) {
- // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2
- var size = radius * Math.sqrt(Math.PI) / 2;
- ctx.moveTo(x - size, y - size);
- ctx.lineTo(x + size, y + size);
- ctx.moveTo(x - size, y + size);
- ctx.lineTo(x + size, y - size);
- }
- }
-
- var s = series.points.symbol;
- if (handlers[s])
- series.points.symbol = handlers[s];
- }
-
- function init(plot) {
- plot.hooks.processDatapoints.push(processRawData);
- }
-
- $.plot.plugins.push({
- init: init,
- name: 'symbols',
- version: '1.0'
- });
-})(jQuery);
diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.symbol.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.symbol.min.js
deleted file mode 100644
index 272e003ab49c..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.flot.symbol.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(function(b){function a(h,e,g){var d={square:function(k,j,n,i,m){var l=i*Math.sqrt(Math.PI)/2;k.rect(j-l,n-l,l+l,l+l)},diamond:function(k,j,n,i,m){var l=i*Math.sqrt(Math.PI/2);k.moveTo(j-l,n);k.lineTo(j,n-l);k.lineTo(j+l,n);k.lineTo(j,n+l);k.lineTo(j-l,n)},triangle:function(l,k,o,j,n){var m=j*Math.sqrt(2*Math.PI/Math.sin(Math.PI/3));var i=m*Math.sin(Math.PI/3);l.moveTo(k-m/2,o+i/2);l.lineTo(k+m/2,o+i/2);if(!n){l.lineTo(k,o-i/2);l.lineTo(k-m/2,o+i/2)}},cross:function(k,j,n,i,m){var l=i*Math.sqrt(Math.PI)/2;k.moveTo(j-l,n-l);k.lineTo(j+l,n+l);k.moveTo(j-l,n+l);k.lineTo(j+l,n-l)}};var f=e.points.symbol;if(d[f]){e.points.symbol=d[f]}}function c(d){d.hooks.processDatapoints.push(a)}b.plot.plugins.push({init:c,name:"symbols",version:"1.0"})})(jQuery);
\ No newline at end of file
diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.threshold.js b/frontend/src/vendor/jquery.flot/jquery.flot.threshold.js
deleted file mode 100644
index 0b2e7ac82a73..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.flot.threshold.js
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
-Flot plugin for thresholding data. Controlled through the option
-"threshold" in either the global series options
-
- series: {
- threshold: {
- below: number
- color: colorspec
- }
- }
-
-or in a specific series
-
- $.plot($("#placeholder"), [{ data: [ ... ], threshold: { ... }}])
-
-The data points below "below" are drawn with the specified color. This
-makes it easy to mark points below 0, e.g. for budget data.
-
-Internally, the plugin works by splitting the data into two series,
-above and below the threshold. The extra series below the threshold
-will have its label cleared and the special "originSeries" attribute
-set to the original series. You may need to check for this in hover
-events.
-*/
-
-(function ($) {
- var options = {
- series: { threshold: null } // or { below: number, color: color spec}
- };
-
- function init(plot) {
- function thresholdData(plot, s, datapoints) {
- if (!s.threshold)
- return;
-
- var ps = datapoints.pointsize, i, x, y, p, prevp,
- thresholded = $.extend({}, s); // note: shallow copy
-
- thresholded.datapoints = { points: [], pointsize: ps };
- thresholded.label = null;
- thresholded.color = s.threshold.color;
- thresholded.threshold = null;
- thresholded.originSeries = s;
- thresholded.data = [];
-
- var below = s.threshold.below,
- origpoints = datapoints.points,
- addCrossingPoints = s.lines.show;
-
- threspoints = [];
- newpoints = [];
-
- for (i = 0; i < origpoints.length; i += ps) {
- x = origpoints[i]
- y = origpoints[i + 1];
-
- prevp = p;
- if (y < below)
- p = threspoints;
- else
- p = newpoints;
-
- if (addCrossingPoints && prevp != p && x != null
- && i > 0 && origpoints[i - ps] != null) {
- var interx = (x - origpoints[i - ps]) / (y - origpoints[i - ps + 1]) * (below - y) + x;
- prevp.push(interx);
- prevp.push(below);
- for (m = 2; m < ps; ++m)
- prevp.push(origpoints[i + m]);
-
- p.push(null); // start new segment
- p.push(null);
- for (m = 2; m < ps; ++m)
- p.push(origpoints[i + m]);
- p.push(interx);
- p.push(below);
- for (m = 2; m < ps; ++m)
- p.push(origpoints[i + m]);
- }
-
- p.push(x);
- p.push(y);
- }
-
- datapoints.points = newpoints;
- thresholded.datapoints.points = threspoints;
-
- if (thresholded.datapoints.points.length > 0)
- plot.getData().push(thresholded);
-
- // FIXME: there are probably some edge cases left in bars
- }
-
- plot.hooks.processDatapoints.push(thresholdData);
- }
-
- $.plot.plugins.push({
- init: init,
- options: options,
- name: 'threshold',
- version: '1.0'
- });
-})(jQuery);
diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.threshold.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.threshold.min.js
deleted file mode 100644
index d8b79dfc93c9..000000000000
--- a/frontend/src/vendor/jquery.flot/jquery.flot.threshold.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(function(B){var A={series:{threshold:null}};function C(D){function E(L,S,M){if(!S.threshold){return }var F=M.pointsize,I,O,N,G,K,H=B.extend({},S);H.datapoints={points:[],pointsize:F};H.label=null;H.color=S.threshold.color;H.threshold=null;H.originSeries=S;H.data=[];var P=S.threshold.below,Q=M.points,R=S.lines.show;threspoints=[];newpoints=[];for(I=0;I0&&Q[I-F]!=null){var J=(O-Q[I-F])/(N-Q[I-F+1])*(P-N)+O;K.push(J);K.push(P);for(m=2;m0){L.getData().push(H)}}D.hooks.processDatapoints.push(E)}B.plot.plugins.push({init:C,options:A,name:"threshold",version:"1.0"})})(jQuery);
\ No newline at end of file
diff --git a/modules/backlogs/app/components/backlogs/backlog_component.html.erb b/modules/backlogs/app/components/backlogs/backlog_component.html.erb
new file mode 100644
index 000000000000..cbbe2339740c
--- /dev/null
+++ b/modules/backlogs/app/components/backlogs/backlog_component.html.erb
@@ -0,0 +1,63 @@
+<%# -- copyright
+OpenProject is an open source project management software.
+Copyright (C) the OpenProject GmbH
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See COPYRIGHT and LICENSE files for more details.
+
+++# %>
+
+<%= component_wrapper(tag: :section) do %>
+ <%= render(Primer::Beta::BorderBox.new(**@system_arguments)) do |border_box| %>
+ <% border_box.with_header do %>
+ <%= render(Backlogs::BacklogHeaderComponent.new(backlog:, project: @project, folded: folded?)) %>
+ <% end %>
+ <% if backlog.stories.empty? %>
+ <% border_box.with_row(data: { empty_list_item: true }) do %>
+ <%=
+ render Primer::Beta::Blankslate.new(role: "status", aria: { live: "polite" }) do |blankslate|
+ blankslate.with_heading(tag: :h4).with_content(t(".blankslate_title", name: sprint.name))
+ blankslate.with_description_content(t(".blankslate_description"))
+ end
+ %>
+ <% end %>
+ <% end %>
+ <% backlog.stories.each do |story| %>
+ <% border_box.with_row(
+ id: dom_id(story),
+ classes: "Box-row--hover-blue Box-row--focus-gray Box-row--clickable Box-row--draggable",
+ data: draggable_item_config(story).merge(
+ story: true,
+ controller: "backlogs--story",
+ backlogs__story_id_value: story.id,
+ backlogs__story_split_url_value: details_backlogs_project_backlogs_path(project, story),
+ backlogs__story_full_url_value: work_package_path(story),
+ backlogs__story_selected_class: "Box-row--blue"
+ ),
+ tabindex: 0
+ ) do %>
+ <%= render(Backlogs::StoryComponent.new(story:, sprint:, max_position:)) %>
+ <% end %>
+ <% end %>
+ <% end %>
+<% end %>
diff --git a/modules/backlogs/app/components/backlogs/backlog_component.rb b/modules/backlogs/app/components/backlogs/backlog_component.rb
new file mode 100644
index 000000000000..a25366262a33
--- /dev/null
+++ b/modules/backlogs/app/components/backlogs/backlog_component.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module Backlogs
+ class BacklogComponent < ApplicationComponent
+ include Primer::AttributesHelper
+ include OpTurbo::Streamable
+ include RbCommonHelper
+
+ attr_reader :backlog, :project, :current_user
+
+ delegate :sprint, :stories, to: :backlog
+
+ def initialize(backlog:, project:, current_user: User.current, **system_arguments)
+ super()
+
+ @backlog = backlog
+ @project = project
+ @current_user = current_user
+
+ @system_arguments = system_arguments
+ @system_arguments[:id] = dom_id(backlog)
+ @system_arguments[:list_id] = "#{@system_arguments[:id]}-list"
+ @system_arguments[:padding] = :condensed
+ @system_arguments[:data] = merge_data(
+ @system_arguments,
+ { data: drop_target_config }
+ )
+ end
+
+ def wrapper_uniq_by
+ backlog.sprint_id
+ end
+
+ private
+
+ def folded?
+ current_user.backlogs_preference(:versions_default_fold_state) == "closed"
+ end
+
+ def max_position
+ stories.filter_map(&:position).max
+ end
+
+ def drop_target_config
+ {
+ generic_drag_and_drop_target: "container",
+ target_container_accessor: ":scope > ul",
+ target_id: backlog.sprint_id,
+ target_allowed_drag_type: "story"
+ }
+ end
+
+ def draggable_item_config(story)
+ {
+ draggable_id: story.id,
+ draggable_type: "story",
+ drop_url: move_backlogs_project_sprint_story_path(project, sprint, story)
+ }
+ end
+ end
+end
diff --git a/modules/backlogs/app/components/backlogs/backlog_header_component.html.erb b/modules/backlogs/app/components/backlogs/backlog_header_component.html.erb
new file mode 100644
index 000000000000..9d6f47b15188
--- /dev/null
+++ b/modules/backlogs/app/components/backlogs/backlog_header_component.html.erb
@@ -0,0 +1,90 @@
+<%# -- copyright
+OpenProject is an open source project management software.
+Copyright (C) the OpenProject GmbH
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See COPYRIGHT and LICENSE files for more details.
+
+++# %>
+
+<%= component_wrapper(tag: :header) do %>
+ <% if show? %>
+ <%= grid_layout("op-backlogs-header", tag: :div) do |grid| %>
+ <% grid.with_area(:collapsible) do %>
+ <%=
+ render(
+ Backlogs::CollapsibleComponent.new(
+ collapsible_id: "#{dom_id(backlog)}-list",
+ toggle_label: t(".label_toggle_backlog", name: sprint.name),
+ collapsed:
+ )
+ ) do |collapsible|
+ collapsible.with_title { sprint.name }
+ collapsible.with_count(
+ scheme: :default,
+ count: story_count,
+ round: true,
+ aria: {
+ label: t(".label_story_count", count: story_count),
+ live: "polite"
+ }
+ )
+ collapsible.with_description(role: "group") do
+ format_date_range(date_range)
+ end
+ end
+ %>
+ <% end %>
+
+ <% grid.with_area(:points) do %>
+ <%=
+ render(
+ Primer::Beta::Text.new(
+ color: :subtle,
+ classes: "velocity",
+ aria: { live: "polite" }
+ )
+ ) do
+ %>
+ <%= story_points %>
+ <%= t(:"backlogs.points_label", count: story_points) %>
+ <% end %>
+ <% end %>
+
+ <% grid.with_area(:menu) do %>
+ <%= render(Backlogs::BacklogMenuComponent.new(backlog:, project: @project)) %>
+ <% end %>
+ <% end %>
+ <% else %>
+ <%=
+ primer_form_with(
+ url: backlogs_project_sprint_path(project, sprint),
+ model: sprint,
+ method: :patch,
+ class: "op-backlogs-header-form"
+ ) do |f|
+ render(Backlogs::BacklogHeaderForm.new(f, cancel_path: show_name_backlogs_project_sprint_path(project, sprint)))
+ end
+ %>
+ <% end %>
+<% end %>
diff --git a/modules/backlogs/app/components/backlogs/backlog_header_component.rb b/modules/backlogs/app/components/backlogs/backlog_header_component.rb
new file mode 100644
index 000000000000..d8c621ca19c6
--- /dev/null
+++ b/modules/backlogs/app/components/backlogs/backlog_header_component.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module Backlogs
+ class BacklogHeaderComponent < ApplicationComponent
+ include OpPrimer::ComponentHelpers
+ include OpTurbo::Streamable
+ include Primer::FetchOrFallbackHelper
+ include Redmine::I18n
+ include RbCommonHelper
+
+ STATE_DEFAULT = :show
+ STATE_OPTIONS = [STATE_DEFAULT, :edit].freeze
+
+ attr_reader :backlog, :project, :state, :collapsed, :current_user
+
+ delegate :sprint, :stories, to: :backlog
+ delegate :name, to: :sprint, prefix: :sprint
+ delegate :edit?, :show?, to: :state
+
+ def initialize(
+ backlog:,
+ project:,
+ state: STATE_DEFAULT,
+ folded: false,
+ current_user: User.current
+ )
+ super()
+
+ @backlog = backlog
+ @project = project
+ @state = ActiveSupport::StringInquirer.new(fetch_or_fallback(STATE_OPTIONS, state, STATE_DEFAULT).to_s)
+ @collapsed = folded
+ @current_user = current_user
+ end
+
+ def wrapper_uniq_by
+ backlog.sprint_id
+ end
+
+ private
+
+ def story_points
+ @story_points ||= stories.sum { |story| story.story_points || 0 }
+ end
+
+ def story_count
+ @story_count ||= stories.size
+ end
+
+ def date_range
+ [sprint.start_date, sprint.effective_date]
+ end
+ end
+end
diff --git a/modules/backlogs/app/components/backlogs/backlog_menu_component.html.erb b/modules/backlogs/app/components/backlogs/backlog_menu_component.html.erb
new file mode 100644
index 000000000000..78f56b0dba4c
--- /dev/null
+++ b/modules/backlogs/app/components/backlogs/backlog_menu_component.html.erb
@@ -0,0 +1,116 @@
+<%# -- copyright
+OpenProject is an open source project management software.
+Copyright (C) the OpenProject GmbH
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See COPYRIGHT and LICENSE files for more details.
+
+++# %>
+
+<%=
+ render(Primer::Alpha::ActionMenu.new(anchor_align: :end, classes: "hide-when-print")) do |menu|
+ menu.with_show_button(
+ scheme: :invisible,
+ icon: :"kebab-horizontal",
+ "aria-label": t(".label_actions"),
+ tooltip_direction: :se
+ )
+
+ if user_allowed?(:update_sprints)
+ menu.with_item(
+ label: t(".action_menu.edit_sprint"),
+ href: edit_name_backlogs_project_sprint_path(project, sprint),
+ content_arguments: { data: { turbo_stream: true } }
+ ) do |item|
+ item.with_leading_visual_icon(icon: :pencil)
+ end
+ end
+
+ if user_allowed?(:add_work_packages)
+ menu.with_item(
+ label: t(".action_menu.new_story"),
+ href: new_project_work_packages_dialog_path(
+ project,
+ version_id: sprint.id,
+ type_id: available_story_types.first
+ ),
+ content_arguments: { data: { turbo_stream: true } }
+ ) do |item|
+ item.with_leading_visual_icon(icon: :compose)
+ end
+ end
+
+ if user_allowed?(:update_sprints) || user_allowed?(:add_work_packages)
+ menu.with_divider
+ end
+
+ menu.with_item(
+ label: t(".action_menu.stories_tasks"),
+ tag: :a,
+ href: backlogs_project_sprint_query_path(project, sprint)
+ ) do |item|
+ item.with_leading_visual_icon(icon: :"op-view-list")
+ end
+
+ if backlog.sprint_backlog?
+ if user_allowed?(:view_taskboards)
+ menu.with_item(
+ label: t(".action_menu.task_board"),
+ tag: :a,
+ href: backlogs_project_sprint_taskboard_path(project, sprint)
+ ) do |item|
+ item.with_leading_visual_icon(icon: :"op-view-cards")
+ end
+ end
+
+ menu.with_item(
+ label: t(".action_menu.burndown_chart"),
+ tag: :a,
+ href: backlogs_project_sprint_burndown_chart_path(project, sprint),
+ disabled: !sprint.has_burndown?
+ ) do |item|
+ item.with_leading_visual_icon(icon: :graph)
+ end
+
+ if project.module_enabled? "wiki"
+ menu.with_item(
+ label: t(".action_menu.wiki"),
+ tag: :a,
+ href: edit_backlogs_project_sprint_wiki_path(project, sprint)
+ ) do |item|
+ item.with_leading_visual_icon(icon: :book)
+ end
+ end
+ end
+
+ if user_allowed?(:manage_versions)
+ menu.with_item(
+ label: t(".action_menu.properties"),
+ tag: :a,
+ href: edit_version_path(sprint, back_url: backlogs_project_backlogs_path(project), project_id: project)
+ ) do |item|
+ item.with_leading_visual_icon(icon: :gear)
+ end
+ end
+ end
+%>
diff --git a/modules/backlogs/app/components/backlogs/backlog_menu_component.rb b/modules/backlogs/app/components/backlogs/backlog_menu_component.rb
new file mode 100644
index 000000000000..07f5ad79bad5
--- /dev/null
+++ b/modules/backlogs/app/components/backlogs/backlog_menu_component.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module Backlogs
+ class BacklogMenuComponent < ApplicationComponent
+ include RbCommonHelper
+
+ attr_reader :backlog, :project, :current_user
+
+ delegate :sprint, :stories, to: :backlog
+
+ def initialize(backlog:, project:, current_user: User.current)
+ super()
+
+ @backlog = backlog
+ @project = project
+ @current_user = current_user
+ end
+
+ private
+
+ def user_allowed?(permission)
+ current_user.allowed_in_project?(permission, project)
+ end
+
+ def available_story_types
+ @available_story_types ||= story_types & project.types
+ end
+ end
+end
diff --git a/modules/backlogs/app/components/backlogs/collapsible_component.html.erb b/modules/backlogs/app/components/backlogs/collapsible_component.html.erb
new file mode 100644
index 000000000000..efe6818b35a8
--- /dev/null
+++ b/modules/backlogs/app/components/backlogs/collapsible_component.html.erb
@@ -0,0 +1,61 @@
+<%# -- copyright
+OpenProject is an open source project management software.
+Copyright (C) the OpenProject GmbH
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See COPYRIGHT and LICENSE files for more details.
+
+++# %>
+
+<%= render(Primer::BaseComponent.new(**@system_arguments)) do %>
+ <%=
+ render(
+ Primer::BaseComponent.new(
+ tag: :div,
+ role: "button",
+ tabindex: 0,
+ classes: "op-backlogs-collapsible",
+ aria: {
+ label: @toggle_label,
+ controls: @collapsible_id,
+ expanded: !@collapsed
+ },
+ data: {
+ target: "collapsible-header.triggerElement",
+ action: "click:collapsible-header#toggle keydown:collapsible-header#toggleViaKeyboard"
+ }
+ )
+ ) do
+ %>
+ <%= render(Primer::BaseComponent.new(tag: :div, classes: "op-backlogs-collapsible--title-line")) do %>
+ <%= title %>
+ <%= count %>
+ <%= render(Primer::BaseComponent.new(tag: :div, classes: "op-backlogs-collapsible--toggle")) do %>
+ <%= render(Primer::Beta::Octicon.new(icon: "chevron-up", hidden: @collapsed, data: { target: "collapsible-header.arrowUp" })) %>
+ <%= render(Primer::Beta::Octicon.new(icon: "chevron-down", hidden: !@collapsed, data: { target: "collapsible-header.arrowDown" })) %>
+ <% end %>
+ <% end %>
+
+ <%= description %>
+ <% end %>
+<% end %>
diff --git a/modules/backlogs/app/components/backlogs/collapsible_component.rb b/modules/backlogs/app/components/backlogs/collapsible_component.rb
new file mode 100644
index 000000000000..3ab049402716
--- /dev/null
+++ b/modules/backlogs/app/components/backlogs/collapsible_component.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module Backlogs
+ class CollapsibleComponent < Primer::Component
+ include OpPrimer::ComponentHelpers
+
+ renders_one :title, ->(**system_arguments) {
+ system_arguments[:classes] = class_names(
+ system_arguments[:classes],
+ "op-backlogs-collapsible--title",
+ "Box-title"
+ )
+
+ Primer::Beta::Truncate.new(tag: :h3, **system_arguments)
+ }
+
+ renders_one :count, ->(**system_arguments) {
+ system_arguments[:mr] ||= 2
+ system_arguments[:scheme] ||= :primary
+ system_arguments[:classes] = class_names(
+ system_arguments[:classes],
+ "op-backlogs-collapsible--count"
+ )
+
+ Primer::Beta::Counter.new(**system_arguments)
+ }
+
+ renders_one :description, ->(**system_arguments) {
+ system_arguments[:color] ||= :subtle
+ system_arguments[:hidden] = @collapsed
+ system_arguments[:classes] = class_names(
+ system_arguments[:classes],
+ "op-backlogs-collapsible--description"
+ )
+
+ Primer::Beta::Text.new(**system_arguments)
+ }
+
+ def initialize(collapsible_id:, toggle_label:, collapsed: false, **system_arguments)
+ super()
+
+ @collapsible_id = collapsible_id
+ @toggle_label = toggle_label
+ @collapsed = collapsed
+
+ @system_arguments = deny_tag_argument(**system_arguments)
+ @system_arguments[:tag] = :"collapsible-header"
+ @system_arguments[:classes] = class_names(
+ system_arguments[:classes],
+ "CollapsibleHeader",
+ "CollapsibleHeader--collapsed" => @collapsed
+ )
+ if @collapsed
+ @system_arguments[:data] = merge_data(
+ @system_arguments, {
+ data: { collapsed: @collapsed }
+ }
+ )
+ end
+ end
+ end
+end
diff --git a/modules/backlogs/app/views/shared/_validation_errors.html.erb b/modules/backlogs/app/components/backlogs/sprint_page_header_component.html.erb
similarity index 81%
rename from modules/backlogs/app/views/shared/_validation_errors.html.erb
rename to modules/backlogs/app/components/backlogs/sprint_page_header_component.html.erb
index 8837b4d6ba77..7b8ba671461a 100644
--- a/modules/backlogs/app/views/shared/_validation_errors.html.erb
+++ b/modules/backlogs/app/components/backlogs/sprint_page_header_component.html.erb
@@ -1,4 +1,4 @@
-<%#-- copyright
+<%# -- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH
@@ -25,12 +25,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
-++#%>
+++# %>
-<%= validation_errors.length > 1 ? t(:error_intro_plural) : t(:error_intro_singular) %>
-
- <%- validation_errors.each_full do |msg| %>
- <%= msg %>
- <%- end %>
-
-<%= t(:error_outro) %>
+<%= render(@page_header) do |header| %>
+ <% header.with_title_content(@sprint.name) %>
+ <% header.with_description { format_date_range(date_range) } %>
+ <% header.with_breadcrumbs(breadcrumb_items) %>
+ <%= content %>
+<% end %>
diff --git a/modules/backlogs/app/components/backlogs/sprint_page_header_component.rb b/modules/backlogs/app/components/backlogs/sprint_page_header_component.rb
new file mode 100644
index 000000000000..60158874b0c2
--- /dev/null
+++ b/modules/backlogs/app/components/backlogs/sprint_page_header_component.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+# -- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+# ++
+
+module Backlogs
+ class SprintPageHeaderComponent < ApplicationComponent
+ include ApplicationHelper
+ include RbCommonHelper
+
+ delegate :with_action_button, to: :@page_header
+
+ def initialize(sprint:, project:)
+ super
+
+ @sprint = sprint
+ @project = project
+
+ @page_header = Primer::OpenProject::PageHeader.new
+ end
+
+ def breadcrumb_items
+ [{ href: project_overview_path(@project), text: @project.name },
+ { href: backlogs_project_backlogs_path(@project), text: t(:label_backlogs) },
+ @sprint.name]
+ end
+
+ private
+
+ def date_range
+ [@sprint.start_date, @sprint.effective_date]
+ end
+ end
+end
diff --git a/modules/backlogs/app/views/rb_master_backlogs/_backlog.html.erb b/modules/backlogs/app/components/backlogs/story_component.html.erb
similarity index 50%
rename from modules/backlogs/app/views/rb_master_backlogs/_backlog.html.erb
rename to modules/backlogs/app/components/backlogs/story_component.html.erb
index 77d2f4f29703..d18cd643c735 100644
--- a/modules/backlogs/app/views/rb_master_backlogs/_backlog.html.erb
+++ b/modules/backlogs/app/components/backlogs/story_component.html.erb
@@ -1,4 +1,4 @@
-<%#-- copyright
+<%# -- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH
@@ -25,27 +25,42 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
-++#%>
-
-<%
- folded = current_user.backlogs_preference(:versions_default_fold_state) == "closed"
- editable = User.current.allowed_in_project?(:edit_work_packages, @project)
-%>
-
-
-
<%= " prevent_drag" unless editable %>">
- <% reset_cycle "stories" %>
- <% backlog.stories.each_with_index do |story, index| %>
- <% higher_item = index == 0 ? nil : backlog.stories[index - 1] %>
-
- <%= render partial: "rb_stories/story",
- locals: { story:, higher_item: } %>
+++# %>
+
+<%= grid_layout("op-backlogs-story", tag: :article) do |grid| %>
+ <% grid.with_area(:drag_handle, classes: "hide-when-print") do %>
+ <%=
+ render(
+ Primer::OpenProject::DragHandle.new(
+ role: "button",
+ classes: "op-backlogs-story--drag_handle_button",
+ tabindex: 0,
+ aria: {
+ label: t(".label_drag_story", name: story.subject)
+ }
+ )
+ )
+ %>
+ <% end %>
+
+ <% grid.with_area(:info_line) do %>
+ <%= render(WorkPackages::InfoLineComponent.new(work_package: story)) %>
+ <% end %>
+
+ <% grid.with_area(:points) do %>
+ <%= render(Primer::Beta::Text.new(color: :subtle)) do %>
+ <%= story_points %>
+ <%= t(:"backlogs.points_label", count: story_points) %>
+ <% end %>
+ <% end %>
+
+ <% grid.with_area(:menu) do %>
+ <%= render(Backlogs::StoryMenuComponent.new(story:, sprint:, max_position:)) %>
+ <% end %>
+
+ <% grid.with_area(:subject) do %>
+ <%= render(Primer::Beta::Text.new(font_weight: :semibold)) do %>
+ <%= story.subject %>
<% end %>
-
-
+ <% end %>
+<% end %>
diff --git a/modules/backlogs/app/components/backlogs/story_component.rb b/modules/backlogs/app/components/backlogs/story_component.rb
new file mode 100644
index 000000000000..39c37f48a7f9
--- /dev/null
+++ b/modules/backlogs/app/components/backlogs/story_component.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module Backlogs
+ class StoryComponent < ApplicationComponent
+ include OpPrimer::ComponentHelpers
+
+ attr_reader :story, :sprint, :max_position, :current_user
+
+ def initialize(story:, sprint:, max_position:, current_user: User.current)
+ super()
+
+ @story = story
+ @sprint = sprint
+ @max_position = max_position
+ @current_user = current_user
+ end
+
+ private
+
+ def story_points
+ story.story_points || 0
+ end
+ end
+end
diff --git a/modules/backlogs/app/views/layouts/backlogs.html.erb b/modules/backlogs/app/components/backlogs/story_menu_component.html.erb
similarity index 54%
rename from modules/backlogs/app/views/layouts/backlogs.html.erb
rename to modules/backlogs/app/components/backlogs/story_menu_component.html.erb
index fb3aeec57ad3..617e70857335 100644
--- a/modules/backlogs/app/views/layouts/backlogs.html.erb
+++ b/modules/backlogs/app/components/backlogs/story_menu_component.html.erb
@@ -1,4 +1,4 @@
-<%#-- copyright
+<%# -- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH
@@ -25,16 +25,38 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
-++#%>
-
-<% content_for :header_tags do %>
- <%= frontend_stylesheet_link_tag "backlogs.css" %>
-<% end %>
-
-<% content_for :additional_js_dom_ready do %>
- <%= render(partial: "shared/server_variables", formats: [:js]) %>
-<% end %>
-
-<% content_controller "backlogs" %>
-
-<%= render template: "layouts/base", locals: local_assigns.merge(turbo_opt_out: true) %>
+++# %>
+
+<%=
+ render(Primer::Alpha::ActionMenu.new(anchor_align: :end, classes: "hide-when-print")) do |menu|
+ menu.with_show_button(
+ scheme: :invisible,
+ icon: :"kebab-horizontal",
+ "aria-label": t(".label_actions"),
+ tooltip_direction: :se
+ )
+
+ menu.with_item(
+ tag: :a,
+ label: t(:"js.button_open_details"),
+ href: details_backlogs_project_backlogs_path(project, story),
+ content_arguments: { turbo_frame: "content-bodyRight", turbo_action: "advance" }
+ ) do |item|
+ item.with_leading_visual_icon(icon: :"op-view-split")
+ end
+
+ menu.with_item(
+ tag: :a,
+ label: t(:"js.button_open_fullscreen"),
+ href: work_package_path(story),
+ content_arguments: { turbo_frame: "_top" }
+ ) do |item|
+ item.with_leading_visual_icon(icon: :"screen-full")
+ end
+
+ if show_move_items?
+ menu.with_divider
+ build_move_menu(menu)
+ end
+ end
+%>
diff --git a/modules/backlogs/app/components/backlogs/story_menu_component.rb b/modules/backlogs/app/components/backlogs/story_menu_component.rb
new file mode 100644
index 000000000000..99581a19047f
--- /dev/null
+++ b/modules/backlogs/app/components/backlogs/story_menu_component.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module Backlogs
+ class StoryMenuComponent < ApplicationComponent
+ attr_reader :story, :sprint, :project, :max_position, :current_user
+
+ def initialize(story:, sprint:, max_position:, current_user: User.current)
+ super()
+
+ @story = story
+ @sprint = sprint
+ @project = sprint.project
+ @max_position = max_position
+ @current_user = current_user
+ end
+
+ private
+
+ def show_move_items?
+ !(first_item? && last_item?)
+ end
+
+ def build_move_menu(menu)
+ unless first_item?
+ build_move_item(menu, label: I18n.t(:label_sort_highest), direction: "highest", icon: :"move-to-top")
+ build_move_item(menu, label: I18n.t(:label_sort_higher), direction: "higher", icon: :"chevron-up")
+ end
+ unless last_item?
+ build_move_item(menu, label: I18n.t(:label_sort_lower), direction: "lower", icon: :"chevron-down")
+ build_move_item(menu, label: I18n.t(:label_sort_lowest), direction: "lowest", icon: :"move-to-bottom")
+ end
+ end
+
+ def build_move_item(menu, label:, direction:, icon:)
+ menu.with_item(
+ label:,
+ tag: :button,
+ href: reorder_backlogs_project_sprint_story_path(project, sprint, story),
+ form_arguments: { method: :post, inputs: [{ name: "direction", value: direction }] }
+ ) do |item|
+ item.with_leading_visual_icon(icon:)
+ end
+ end
+
+ def first_item?
+ story.position == 1
+ end
+
+ def last_item?
+ story.position == max_position
+ end
+ end
+end
diff --git a/modules/backlogs/app/controllers/backlogs_settings_controller.rb b/modules/backlogs/app/controllers/backlogs_settings_controller.rb
index 09318d09c684..845c60b6b7f9 100644
--- a/modules/backlogs/app/controllers/backlogs_settings_controller.rb
+++ b/modules/backlogs/app/controllers/backlogs_settings_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
diff --git a/modules/backlogs/app/controllers/projects/settings/backlogs_controller.rb b/modules/backlogs/app/controllers/projects/settings/backlogs_controller.rb
index 6fa3587d0fc8..b53f8be524b8 100644
--- a/modules/backlogs/app/controllers/projects/settings/backlogs_controller.rb
+++ b/modules/backlogs/app/controllers/projects/settings/backlogs_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -30,7 +32,7 @@ class Projects::Settings::BacklogsController < Projects::SettingsController
menu_item :settings_backlogs
def show
- @statuses_done_for_project = @project.done_statuses.select(:id).map(&:id)
+ @statuses_done_for_project = @project.done_statuses.pluck(:id)
end
def update
diff --git a/modules/backlogs/app/controllers/rb_application_controller.rb b/modules/backlogs/app/controllers/rb_application_controller.rb
index 37e0502db725..944ce7c2452b 100644
--- a/modules/backlogs/app/controllers/rb_application_controller.rb
+++ b/modules/backlogs/app/controllers/rb_application_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -32,10 +34,6 @@ class RbApplicationController < ApplicationController
before_action :load_sprint_and_project, :check_if_plugin_is_configured, :authorize
- # Use special backlogs layout to initialize stimulus side-loading legacy backlogs scripts
- # and CSS from frontend
- layout "backlogs"
-
private
# Loads the project to be used by the authorize filter to determine if
diff --git a/modules/backlogs/app/controllers/rb_burndown_charts_controller.rb b/modules/backlogs/app/controllers/rb_burndown_charts_controller.rb
index de32109c5d48..914d1f0a2148 100644
--- a/modules/backlogs/app/controllers/rb_burndown_charts_controller.rb
+++ b/modules/backlogs/app/controllers/rb_burndown_charts_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
diff --git a/modules/backlogs/app/controllers/rb_impediments_controller.rb b/modules/backlogs/app/controllers/rb_impediments_controller.rb
index d67c86b22dc7..1a95316f107c 100644
--- a/modules/backlogs/app/controllers/rb_impediments_controller.rb
+++ b/modules/backlogs/app/controllers/rb_impediments_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
diff --git a/modules/backlogs/app/controllers/rb_master_backlogs_controller.rb b/modules/backlogs/app/controllers/rb_master_backlogs_controller.rb
index 0602221ec5d5..dd6838e553ba 100644
--- a/modules/backlogs/app/controllers/rb_master_backlogs_controller.rb
+++ b/modules/backlogs/app/controllers/rb_master_backlogs_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -27,12 +29,35 @@
#++
class RbMasterBacklogsController < RbApplicationController
+ include WorkPackages::WithSplitView
+
menu_item :backlogs
+ before_action :load_backlogs, only: :index
+
def index
+ if turbo_frame_request?
+ render partial: "list", layout: false
+ else
+ render :index
+ end
+ end
+
+ def details
+ if turbo_frame_request?
+ render "work_packages/split_view", layout: false
+ else
+ load_backlogs
+ render :index
+ end
+ end
+
+ def split_view_base_route = backlogs_project_backlogs_path(request.query_parameters)
+
+ private
+
+ def load_backlogs
@owner_backlogs = Backlog.owner_backlogs(@project)
@sprint_backlogs = Backlog.sprint_backlogs(@project)
-
- @last_update = (@sprint_backlogs + @owner_backlogs).filter_map(&:updated_at).max
end
end
diff --git a/modules/backlogs/app/controllers/rb_queries_controller.rb b/modules/backlogs/app/controllers/rb_queries_controller.rb
index da11a8155037..eaf990c0fab1 100644
--- a/modules/backlogs/app/controllers/rb_queries_controller.rb
+++ b/modules/backlogs/app/controllers/rb_queries_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
diff --git a/modules/backlogs/app/controllers/rb_sprints_controller.rb b/modules/backlogs/app/controllers/rb_sprints_controller.rb
index 3603a756b444..60abfe686dc5 100644
--- a/modules/backlogs/app/controllers/rb_sprints_controller.rb
+++ b/modules/backlogs/app/controllers/rb_sprints_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -26,30 +28,64 @@
# See COPYRIGHT and LICENSE files for more details.
#++
-# Responsible for exposing sprint CRUD. It SHOULD NOT be used for displaying the
-# taskboard since the taskboard is a management interface used for managing
-# objects within a sprint. For info about the taskboard, see
-# RbTaskboardsController
class RbSprintsController < RbApplicationController
+ include OpTurbo::ComponentStream
+
+ def edit_name
+ update_header_component_via_turbo_stream(state: :edit)
+ respond_with_turbo_streams
+ end
+
+ def show_name
+ update_header_component_via_turbo_stream(state: :show)
+ respond_with_turbo_streams
+ end
+
def update
- result = @sprint.update(params.permit(:name,
- :start_date,
- :effective_date))
- status = (result ? 200 : 400)
+ call = Versions::UpdateService
+ .new(user: current_user, model: @sprint)
+ .call(attributes: sprint_params)
- respond_to do |format|
- format.html { render partial: "sprint", status:, object: @sprint }
+ if call.success?
+ status = 200
+ state = :show
+ @sprint = call.result
+ render_success_flash_message_via_turbo_stream(message: I18n.t(:notice_successful_update))
+ else
+ status = 422
+ state = :edit
+ render_error_flash_message_via_turbo_stream(
+ message: I18n.t(:notice_unsuccessful_update_with_reason, reason: call.message)
+ )
end
+
+ update_header_component_via_turbo_stream(state:)
+ respond_with_turbo_streams(status:)
end
- # Overwrite load_sprint_and_project to load the sprint from the :id instead of
- # :sprint_id
+ private
+
+ def update_header_component_via_turbo_stream(state: :show)
+ @backlog = Backlog.for(sprint: @sprint, project: @project)
+
+ update_via_turbo_stream(
+ component: Backlogs::BacklogHeaderComponent.new(
+ backlog: @backlog,
+ project: @project,
+ state:
+ )
+ )
+ end
+
+ # Overrides load_sprint_and_project to load the sprint from :id instead of :sprint_id
def load_sprint_and_project
- if params[:id]
- @sprint = Sprint.find(params[:id])
- @project = @sprint.project
- end
+ @sprint = Sprint.visible.find(params[:id])
+ @project = @sprint.project
# This overrides sprint's project if we set another project, say a subproject
- @project = Project.find(params[:project_id]) if params[:project_id]
+ @project = Project.visible.find(params[:project_id])
+ end
+
+ def sprint_params
+ params.expect(sprint: %i[name start_date effective_date])
end
end
diff --git a/modules/backlogs/app/controllers/rb_stories_controller.rb b/modules/backlogs/app/controllers/rb_stories_controller.rb
index 3b6dbc3402a3..0b1048a2349c 100644
--- a/modules/backlogs/app/controllers/rb_stories_controller.rb
+++ b/modules/backlogs/app/controllers/rb_stories_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -27,50 +29,75 @@
#++
class RbStoriesController < RbApplicationController
- # This is a constant here because we will recruit it elsewhere to whitelist
- # attributes. This is necessary for now as we still directly use `attributes=`
- # in non-controller code.
- PERMITTED_PARAMS = %i[id status_id version_id
- story_points type_id subject author_id
- sprint_id]
-
- def create
- call = Stories::CreateService
- .new(user: current_user)
- .call(attributes: story_params,
- prev: params[:prev])
-
- respond_with_story(call)
- end
+ include OpTurbo::ComponentStream
+
+ before_action :load_story
- def update
- story = Story.find(params[:id])
+ def move # rubocop:disable Metrics/AbcSize
+ # The update service reloads the story internally (via #move_after),
+ # so we memoize the previous version_id before the call.
+ version_id_was = @story.version_id
call = Stories::UpdateService
- .new(user: current_user, story:)
- .call(attributes: story_params,
- prev: params[:prev])
+ .new(user: current_user, story: @story)
+ .call(
+ attributes: { version_id: move_params[:target_id] },
+ position: move_params[:position].to_i
+ )
unless call.success?
- # reload the story to be able to display it correctly
- call.result.reload
+ render_error_flash_message_via_turbo_stream(
+ message: I18n.t(:notice_unsuccessful_update_with_reason, reason: call.message)
+ )
+ end
+
+ replace_backlog_component_via_turbo_stream(sprint: @sprint)
+
+ if @story.version_id != version_id_was
+ new_sprint = @story.version.becomes(Sprint)
+
+ render_success_flash_message_via_turbo_stream(
+ message: I18n.t(:notice_successful_move, from: @sprint.name, to: new_sprint.name)
+ )
+ replace_backlog_component_via_turbo_stream(sprint: new_sprint)
end
- respond_with_story(call)
+ respond_with_turbo_streams
+ end
+
+ def reorder
+ call = Stories::UpdateService
+ .new(user: current_user, story: @story)
+ .call(attributes: { move_to: reorder_param })
+
+ unless call.success?
+ render_error_flash_message_via_turbo_stream(
+ message: I18n.t(:notice_unsuccessful_update_with_reason, reason: call.message)
+ )
+ end
+
+ replace_backlog_component_via_turbo_stream(sprint: @sprint)
+
+ respond_with_turbo_streams
end
private
- def respond_with_story(call)
- status = call.success? ? 200 : 400
- story = call.result
+ def replace_backlog_component_via_turbo_stream(sprint:)
+ @backlog = Backlog.for(sprint:, project: @project)
+ replace_via_turbo_stream(component: Backlogs::BacklogComponent.new(backlog: @backlog, project: @project))
+ end
- respond_to do |format|
- format.html { render partial: "story", object: story, status:, locals: { errors: call.errors } }
- end
+ def load_story
+ @story = Story.visible.find(params[:id])
+ end
+
+ def move_params
+ params.require(%i[position target_id])
+ params.permit(:position, :target_id)
end
- def story_params
- params.permit(PERMITTED_PARAMS).merge(project: @project).to_h
+ def reorder_param
+ params.expect(:direction)
end
end
diff --git a/modules/backlogs/app/controllers/rb_taskboards_controller.rb b/modules/backlogs/app/controllers/rb_taskboards_controller.rb
index 73cda7f7b903..05196ec200e1 100644
--- a/modules/backlogs/app/controllers/rb_taskboards_controller.rb
+++ b/modules/backlogs/app/controllers/rb_taskboards_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
diff --git a/modules/backlogs/app/controllers/rb_tasks_controller.rb b/modules/backlogs/app/controllers/rb_tasks_controller.rb
index d1bd84f1559b..5182715d05ff 100644
--- a/modules/backlogs/app/controllers/rb_tasks_controller.rb
+++ b/modules/backlogs/app/controllers/rb_tasks_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -31,12 +33,12 @@ class RbTasksController < RbApplicationController
# attributes. This is necessary for now as we still directly use `attributes=`
# in non-controller code.
PERMITTED_PARAMS = ["id", "subject", "assigned_to_id", "remaining_hours", "parent_id",
- "estimated_hours", "status_id", "sprint_id"]
+ "estimated_hours", "status_id", "sprint_id"].freeze
def create
call = ::Tasks::CreateService
.new(user: current_user)
- .call(attributes: task_params.merge(project: @project), prev: params[:prev])
+ .call(attributes: task_params.merge(project: @project), prev_id: params[:prev])
respond_with_task call
end
@@ -46,7 +48,7 @@ def update
call = ::Tasks::UpdateService
.new(user: current_user, task:)
- .call(attributes: task_params, prev: params[:prev])
+ .call(attributes: task_params, prev_id: params[:prev])
respond_with_task call
end
diff --git a/modules/backlogs/app/controllers/rb_wikis_controller.rb b/modules/backlogs/app/controllers/rb_wikis_controller.rb
index 4de3f05da697..7684363cffac 100644
--- a/modules/backlogs/app/controllers/rb_wikis_controller.rb
+++ b/modules/backlogs/app/controllers/rb_wikis_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -32,10 +34,10 @@ class RbWikisController < RbApplicationController
#
# NOTE: The methods #show and #edit create a template page when called.
def show
- redirect_to controller: "/wiki", action: "index", project_id: @project.id, id: @sprint.wiki_page
+ redirect_to controller: "/wiki", action: "index", project_id: @project, id: @sprint.wiki_page
end
def edit
- redirect_to controller: "/wiki", action: "edit", project_id: @project.id, id: @sprint.wiki_page
+ redirect_to controller: "/wiki", action: "edit", project_id: @project, id: @sprint.wiki_page
end
end
diff --git a/modules/backlogs/app/forms/backlogs/backlog_header_form.rb b/modules/backlogs/app/forms/backlogs/backlog_header_form.rb
new file mode 100644
index 000000000000..f8b298d9411e
--- /dev/null
+++ b/modules/backlogs/app/forms/backlogs/backlog_header_form.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module Backlogs
+ class BacklogHeaderForm < ApplicationForm
+ attr_reader :cancel_path
+
+ form do |f|
+ f.text_field(
+ name: :name,
+ label: attribute_name(:name),
+ placeholder: attribute_name(:name),
+ visually_hide_label: true,
+ autofocus: true,
+ autocomplete: "off"
+ )
+
+ f.group(layout: :horizontal) do |dates|
+ dates.single_date_picker(
+ name: :start_date,
+ label: attribute_name(:start_date),
+ placeholder: attribute_name(:start_date),
+ visually_hide_label: true,
+ leading_visual: { icon: :calendar },
+ datepicker_options: {}
+ )
+ dates.single_date_picker(
+ name: :effective_date,
+ label: attribute_name(:effective_date),
+ placeholder: attribute_name(:effective_date),
+ visually_hide_label: true,
+ leading_visual: { icon: :calendar },
+ datepicker_options: {}
+ )
+ end
+
+ f.group(layout: :horizontal) do |buttons|
+ buttons.submit(scheme: :primary, name: :submit, label: I18n.t(:button_save))
+ buttons.button(
+ scheme: :secondary,
+ name: :cancel,
+ label: I18n.t(:button_cancel),
+ tag: :a,
+ data: { turbo_stream: true },
+ href: cancel_path
+ )
+ end
+ end
+
+ def initialize(cancel_path:)
+ super()
+
+ @cancel_path = cancel_path
+ end
+ end
+end
diff --git a/modules/backlogs/app/helpers/burndown_charts_helper.rb b/modules/backlogs/app/helpers/burndown_charts_helper.rb
index 03c031eab721..1c087f111259 100644
--- a/modules/backlogs/app/helpers/burndown_charts_helper.rb
+++ b/modules/backlogs/app/helpers/burndown_charts_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -27,52 +29,23 @@
#++
module BurndownChartsHelper
- def yaxis_labels(burndown)
- max = burndown.max[:points]
-
- mvalue = (max / 25) + 1
-
- labels = (0..mvalue).map { |i| "[#{i * 25}, #{i * 25}]" }
-
- mvalue = mvalue + 1 if mvalue == 1 || ((max % 25) == 0)
-
- labels << "[#{mvalue * 25}, '#{I18n.t('backlogs.points')} ']"
-
- result = labels.join(", ")
-
- result.html_safe
- end
-
def xaxis_labels(burndown)
# 14 entries (plus the axis label) have come along as the best value for a good optical result.
# Thus it is enough space between the entries.
entries_displayed = (burndown.days.length / 14.0).ceil
- result = burndown.days.enum_for(:each_with_index).map do |d, i|
+ burndown.days.enum_for(:each_with_index).map do |d, i|
if (i % entries_displayed) == 0
- "[#{i + 1}, '#{escape_javascript(::I18n.t('date.abbr_day_names')[d.wday % 7])} #{d.strftime('%d/%m')}']"
+ ["#{escape_javascript(::I18n.t('date.abbr_day_names')[d.wday % 7])} #{d.strftime('%d/%m')}"]
end
- end.join(",").html_safe +
- ", [#{burndown.days.length + 1},
- '#{I18n.t('backlogs.date')} ']".html_safe
+ end
end
def dataseries(burndown)
- dataset = {}
- burndown.series.each do |s|
- dataset[s.first] = {
- label: I18n.t("backlogs." + s.first.to_s),
- data: s.last.enum_for(:each_with_index).map { |val, i| [i + 1, val] }
+ burndown.series.map do |s|
+ {
+ label: I18n.t("burndown.#{s.first}"),
+ data: s.last.enum_for(:each)
}
end
-
- dataset
- end
-
- def burndown_series_checkboxes(burndown)
- boxes = ""
- burndown.series(:all).map { |s| s.first.to_s }.sort.each do |series|
- boxes += " #{I18n.t('backlogs.' + series.to_s)} "
- end
- boxes.html_safe
end
end
diff --git a/modules/backlogs/app/helpers/rb_common_helper.rb b/modules/backlogs/app/helpers/rb_common_helper.rb
index d25776d4fe73..b28a5891b700 100644
--- a/modules/backlogs/app/helpers/rb_common_helper.rb
+++ b/modules/backlogs/app/helpers/rb_common_helper.rb
@@ -27,6 +27,13 @@
#++
module RbCommonHelper
+ def format_date_range(dates)
+ return nil if dates.all?(&:nil?)
+
+ from, to = dates.map { |date| tag.time(datetime: date.iso8601) { format_date(date) } if date }
+ safe_join([from, "–", to], " ") # – and
+ end
+
def assignee_id_or_empty(story)
story.assigned_to_id.to_s
end
@@ -57,14 +64,6 @@ def color_contrast_class(task)
end
end
- # Return true if the difference between two colors
- # matches the W3C recommendations for readability
- # See http://www.wat-c.org/tools/CCA/1.1/
- def colors_diff_ok?(color_1, color_2)
- cont, bright = find_color_diff color_1, color_2
- (cont > 500) && (bright > 125) # Acceptable diff according to w3c
- end
-
def color_contrast(color)
_, bright = find_color_diff 0x000000, color
(bright > 128)
@@ -95,18 +94,13 @@ def is_assigned_task?(task)
def background_color_hex(task)
background_color = get_backlogs_preference(task.assigned_to, :task_color)
- background_color_hex = background_color.sub("#", "0x").hex
+ background_color.sub("#", "0x").hex
end
def id_or_empty(item)
item.id.to_s
end
- def shortened_id(record)
- id = record.id.to_s
- (id.length > 8 ? "#{id[0..1]}...#{id[-4..-1]}" : id)
- end
-
def work_package_link_or_empty(work_package)
modal_link_to_work_package(work_package.id, work_package, class: "prevent_edit") unless work_package.new_record?
end
@@ -120,53 +114,14 @@ def modal_link_to(title, path, options = {})
link_to(title, path, options.merge(id: html_id, target: "_blank"))
end
- def sprint_link_or_empty(item)
- item_id = item.id.to_s
- text = (item_id.length > 8 ? "#{item_id[0..1]}...#{item_id[-4..-1]}" : item_id)
- if item.new_record?
- ""
- else
- link_to(text, backlogs_project_sprint_path(id: item.id, project_id: item.project.identifier), class: "prevent_edit")
- end
- end
-
def mark_if_closed(story)
!story.new_record? && work_package_status_for_id(story.status_id).is_closed? ? "closed" : ""
end
- def story_points_or_empty(story)
- story.story_points.to_s
- end
-
- def status_id_or_default(story)
- story.new_record? ? new_record_status.id : story.status_id
- end
-
- def status_label_or_default(story)
- story.new_record? ? new_record_status.name : h(work_package_status_for_id(story.status_id).name)
- end
-
- def sprint_html_id_or_empty(sprint)
- sprint.id.nil? ? "" : "sprint_#{sprint.id}"
- end
-
def story_html_id_or_empty(story)
story.id.nil? ? "" : "story_#{story.id}"
end
- def type_id_or_empty(story)
- story.type_id.to_s
- end
-
- def type_name_or_empty(story)
- return "" if story.type_id.nil?
-
- type = backlogs_types_by_id[story.type_id]
- return "" if type.nil?
-
- h(type.name)
- end
-
def date_string_with_milliseconds(d, add = 0)
return "" if d.blank?
@@ -177,49 +132,8 @@ def remaining_hours(item)
item.remaining_hours.blank? || item.remaining_hours == 0 ? "" : item.remaining_hours
end
- def available_story_types
- @available_story_types ||= begin
- types = story_types & @project.types if @project
-
- types
- end
- end
-
- def available_statuses_by_type
- @available_statuses_by_type ||= begin
- available_statuses_by_type = Hash.new do |type_hash, type|
- type_hash[type] = Hash.new do |status_hash, status|
- status_hash[status] = [status]
- end
- end
-
- all_workflows.each do |w|
- type_status = available_statuses_by_type[story_types_by_id[w.type_id]][w.old_status]
-
- type_status << w.new_status unless type_status.include?(w.new_status)
- end
-
- available_statuses_by_type
- end
- end
-
- def show_burndown_link(project, sprint)
- link_to(I18n.t("backlogs.show_burndown_chart"),
- backlogs_project_sprint_burndown_chart_path(project.identifier, sprint),
- class: "show_burndown_chart button",
- target: :_blank, rel: :noopener)
- end
-
private
- def new_record_status
- @new_record_status ||= all_work_package_status.first
- end
-
- def default_work_package_status
- @default_work_package_status ||= all_work_package_status.detect(&:is_default)
- end
-
def work_package_status_for_id(id)
@all_work_package_status_by_id ||= all_work_package_status.inject({}) do |mem, status|
mem[status.id] = status
@@ -229,18 +143,6 @@ def work_package_status_for_id(id)
@all_work_package_status_by_id[id]
end
- # Returns all distinct virtual workflows for the roles the current user has in the project and the story types.
- # Virtual workflow because not every instance of a workflow in the database will be returned but a representation
- # distinct by type_id, old_status_id and new_status_id. This helps in case a lot of workflows are configured.
- def all_workflows
- Workflow
- .includes(%i[new_status old_status])
- .where(role_id: User.current.roles_for_project(@project).map(&:id),
- type_id: story_types.map(&:id))
- .group(:type_id, :old_status_id, :new_status_id)
- .reselect(:type_id, :old_status_id, :new_status_id)
- end
-
def all_work_package_status
@all_work_package_status ||= Status.order(Arel.sql("position ASC"))
end
@@ -254,13 +156,6 @@ def backlogs_types
end
end
- def backlogs_types_by_id
- @backlogs_types_by_id ||= backlogs_types.inject({}) do |mem, type|
- mem[type.id] = type
- mem
- end
- end
-
def story_types
@story_types ||= begin
backlogs_type_ids = Setting.plugin_openproject_backlogs["story_types"].map(&:to_i)
@@ -269,20 +164,7 @@ def story_types
end
end
- def story_types_by_id
- @story_types_by_id ||= story_types.inject({}) do |mem, type|
- mem[type.id] = type
- mem
- end
- end
-
def get_backlogs_preference(assignee, attr)
assignee.is_a?(User) ? assignee.backlogs_preference(attr) : "#24B3E7"
end
-
- def template_story
- Story.new.tap do |s|
- s.type = available_story_types.first
- end
- end
end
diff --git a/modules/backlogs/app/helpers/rb_master_backlogs_helper.rb b/modules/backlogs/app/helpers/rb_master_backlogs_helper.rb
deleted file mode 100644
index 89866fd1cff1..000000000000
--- a/modules/backlogs/app/helpers/rb_master_backlogs_helper.rb
+++ /dev/null
@@ -1,121 +0,0 @@
-#-- copyright
-# OpenProject is an open source project management software.
-# Copyright (C) the OpenProject GmbH
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License version 3.
-#
-# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
-# Copyright (C) 2006-2013 Jean-Philippe Lang
-# Copyright (C) 2010-2013 the ChiliProject Team
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-# See COPYRIGHT and LICENSE files for more details.
-#++
-
-module RbMasterBacklogsHelper
- include Redmine::I18n
-
- def render_backlog_menu(backlog)
- # associated javascript defined in taskboard.js
- content_tag(:div, class: "backlog-menu") do
- [
- content_tag(:div, "", class: "menu-trigger icon-context icon-pulldown icon-small"),
- content_tag(:ul, class: "items") do
- backlog_menu_items_for(backlog).map do |item|
- content_tag(:li, item, class: "item")
- end.join.html_safe
- end
- ].join.html_safe
- end
- end
-
- def backlog_menu_items_for(backlog)
- items = common_backlog_menu_items_for(backlog)
-
- if backlog.sprint_backlog?
- items.merge!(sprint_backlog_menu_items_for(backlog))
- end
-
- menu = []
- %i[new_story stories_tasks task_board burndown cards wiki configs properties].each do |key|
- menu << items[key] if items.keys.include?(key)
- end
-
- menu
- end
-
- def common_backlog_menu_items_for(backlog)
- items = {}
-
- if current_user.allowed_in_project?(:add_work_packages, @project)
- items[:new_story] = content_tag(:a,
- I18n.t("backlogs.add_new_story"),
- href: "#",
- class: "add_new_story")
- end
-
- items[:stories_tasks] = link_to(I18n.t(:label_stories_tasks),
- controller: "/rb_queries",
- action: "show",
- project_id: @project,
- sprint_id: backlog.sprint)
-
- if current_user.allowed_in_project?(:manage_versions, @project)
- items[:properties] = properties_link(backlog)
- end
-
- items
- end
-
- def properties_link(backlog)
- back_path = backlogs_project_backlogs_path(@project)
-
- version_path = edit_version_path(backlog.sprint, back_url: back_path, project_id: @project.id)
-
- link_to(I18n.t(:"backlogs.properties"), version_path)
- end
-
- def sprint_backlog_menu_items_for(backlog)
- items = {}
-
- if current_user.allowed_in_project?(:view_taskboards, @project)
- items[:task_board] = link_to(I18n.t(:label_task_board),
- { controller: "/rb_taskboards",
- action: "show",
- project_id: @project,
- sprint_id: backlog.sprint },
- class: "show_task_board")
- end
-
- if backlog.sprint.has_burndown?
- items[:burndown] = content_tag(:a,
- I18n.t("backlogs.show_burndown_chart"),
- href: "#",
- class: "show_burndown_chart")
- end
-
- if @project.module_enabled? "wiki"
- items[:wiki] = link_to(I18n.t(:label_wiki),
- controller: "/rb_wikis",
- action: "edit",
- project_id: @project,
- sprint_id: backlog.sprint)
- end
-
- items
- end
-end
diff --git a/modules/backlogs/app/models/backlog.rb b/modules/backlogs/app/models/backlog.rb
index 9dd2cbbaa898..482106b803cc 100644
--- a/modules/backlogs/app/models/backlog.rb
+++ b/modules/backlogs/app/models/backlog.rb
@@ -27,11 +27,18 @@
#++
class Backlog
+ extend ActiveModel::Naming
+
attr_accessor :sprint, :stories
- def self.owner_backlogs(project, options = {})
- options.reverse_merge!(limit: nil)
+ delegate :id, to: :sprint, prefix: true
+
+ def self.for(sprint:, project:)
+ owner_backlog = sprint.settings(project)&.display == VersionSetting::DISPLAY_RIGHT
+ new(sprint:, stories: sprint.stories(project), owner_backlog:)
+ end
+ def self.owner_backlogs(project)
backlogs = Sprint.apply_to(project).with_status_open.displayed_right(project).order(:name)
stories_by_sprints = Story.backlogs(project.id, backlogs.map(&:id))
@@ -47,11 +54,10 @@ def self.sprint_backlogs(project)
sprints.map { |sprint| new(stories: stories_by_sprints[sprint.id], sprint:) }
end
- def initialize(options = {})
- options = options.with_indifferent_access
- @sprint = options["sprint"]
- @stories = options["stories"]
- @owner_backlog = options["owner_backlog"]
+ def initialize(sprint:, stories:, owner_backlog: false)
+ @sprint = sprint
+ @stories = stories
+ @owner_backlog = owner_backlog
end
def updated_at
@@ -65,4 +71,8 @@ def owner_backlog?
def sprint_backlog?
!owner_backlog?
end
+
+ def to_key
+ [sprint_id]
+ end
end
diff --git a/modules/backlogs/app/models/sprint.rb b/modules/backlogs/app/models/sprint.rb
index 5cdb1f0f2454..cdb61d41a8d0 100644
--- a/modules/backlogs/app/models/sprint.rb
+++ b/modules/backlogs/app/models/sprint.rb
@@ -156,6 +156,10 @@ def impediments(project)
Impediment.default_scope.where(version_id: self, project_id: project)
end
+ def settings(project)
+ version_settings.find { it.project_id == project.id || it.project_id.nil? }
+ end
+
private
def create_wiki_page(page_title, author: User.current)
diff --git a/modules/backlogs/app/models/story.rb b/modules/backlogs/app/models/story.rb
index d72ae19edf9a..6948b8dfdbca 100644
--- a/modules/backlogs/app/models/story.rb
+++ b/modules/backlogs/app/models/story.rb
@@ -29,11 +29,13 @@
class Story < WorkPackage
extend OpenProject::Backlogs::Mixins::PreventIssueSti
- def self.backlogs(project_id, sprint_ids, options = {})
+ def self.backlogs(project_id, sprint_ids, options = {}) # rubocop:disable Metrics/AbcSize
options.reverse_merge!(order: Story::ORDER,
conditions: Story.condition(project_id, sprint_ids))
- candidates = Story.where(options[:conditions]).order(Arel.sql(options[:order]))
+ candidates = Story.where(options[:conditions])
+ .includes(:status, :type)
+ .order(Arel.sql(options[:order]))
stories_by_version = Hash.new do |hash, sprint_id|
hash[sprint_id] = []
diff --git a/modules/backlogs/app/services/stories/create_service.rb b/modules/backlogs/app/services/stories/create_service.rb
index f15a00de3f2d..d8c523914b93 100644
--- a/modules/backlogs/app/services/stories/create_service.rb
+++ b/modules/backlogs/app/services/stories/create_service.rb
@@ -33,13 +33,13 @@ def initialize(user:)
self.user = user
end
- def call(attributes: {}, prev: nil)
+ def call(attributes: {}, position: nil)
create_call = WorkPackages::CreateService
.new(user:)
.call(**attributes.symbolize_keys)
if create_call.success?
- create_call.result.move_after prev
+ create_call.result.move_after position:
end
create_call
diff --git a/modules/backlogs/app/services/stories/update_service.rb b/modules/backlogs/app/services/stories/update_service.rb
index 02f4862078f0..fd5f94f50d6e 100644
--- a/modules/backlogs/app/services/stories/update_service.rb
+++ b/modules/backlogs/app/services/stories/update_service.rb
@@ -34,14 +34,14 @@ def initialize(user:, story:)
self.story = story
end
- def call(attributes: {}, prev: nil)
+ def call(attributes: {}, position: nil)
create_call = WorkPackages::UpdateService
.new(user:,
model: story)
- .call(**attributes.symbolize_keys)
+ .call(**attributes.to_h.symbolize_keys)
- if create_call.success? && prev
- create_call.result.move_after prev
+ if create_call.success? && position
+ create_call.result.move_after(position:)
end
create_call
diff --git a/modules/backlogs/app/services/tasks/create_service.rb b/modules/backlogs/app/services/tasks/create_service.rb
index 47591c790f0a..fd34ec2bb97f 100644
--- a/modules/backlogs/app/services/tasks/create_service.rb
+++ b/modules/backlogs/app/services/tasks/create_service.rb
@@ -33,7 +33,7 @@ def initialize(user:)
self.user = user
end
- def call(attributes: {}, prev: "")
+ def call(attributes: {}, prev_id: "")
attributes[:type_id] = Task.type
create_call = WorkPackages::CreateService
@@ -41,7 +41,7 @@ def call(attributes: {}, prev: "")
.call(**attributes)
if create_call.success?
- create_call.result.move_after prev
+ create_call.result.move_after prev_id:
end
create_call
diff --git a/modules/backlogs/app/services/tasks/update_service.rb b/modules/backlogs/app/services/tasks/update_service.rb
index b936db0344f4..a249626d024f 100644
--- a/modules/backlogs/app/services/tasks/update_service.rb
+++ b/modules/backlogs/app/services/tasks/update_service.rb
@@ -34,14 +34,14 @@ def initialize(user:, task:)
self.task = task
end
- def call(attributes: {}, prev: "")
+ def call(attributes: {}, prev_id: "")
create_call = WorkPackages::UpdateService
.new(user:,
model: task)
.call(**attributes)
if create_call.success?
- create_call.result.move_after prev
+ create_call.result.move_after prev_id:
end
create_call
diff --git a/modules/backlogs/app/views/projects/settings/backlogs/show.html.erb b/modules/backlogs/app/views/projects/settings/backlogs/show.html.erb
index 49424558d343..bb32c332dbf0 100644
--- a/modules/backlogs/app/views/projects/settings/backlogs/show.html.erb
+++ b/modules/backlogs/app/views/projects/settings/backlogs/show.html.erb
@@ -31,8 +31,8 @@ See COPYRIGHT and LICENSE files for more details.
render Primer::OpenProject::PageHeader.new do |header|
header.with_title { t("backlogs.definition_of_done") }
header.with_breadcrumbs(
- [{ href: project_overview_path(@project.id), text: @project.name },
- { href: project_settings_general_path(@project.id), text: I18n.t(:label_project_settings) },
+ [{ href: project_overview_path(@project), text: @project.name },
+ { href: project_settings_general_path(@project), text: I18n.t(:label_project_settings) },
t("backlogs.definition_of_done")]
)
end
diff --git a/modules/backlogs/app/views/rb_burndown_charts/_burndown.html.erb b/modules/backlogs/app/views/rb_burndown_charts/_burndown.html.erb
index 5dc27ca0548f..487f17ba88a5 100644
--- a/modules/backlogs/app/views/rb_burndown_charts/_burndown.html.erb
+++ b/modules/backlogs/app/views/rb_burndown_charts/_burndown.html.erb
@@ -27,112 +27,7 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
-<%= nonced_javascript_tag do %>
- jQuery(function () {
- var Burndown = {
- datasets: <%= dataseries(burndown).to_json.html_safe %> ,
- previousPoint: null,
-
- setDatasetColor: function () {
- var i = 0;
-
- jQuery.each(Burndown.datasets, function (key, val) {
- val.color = i;
- val.points = {show: false, radius: 2};
- val.lines = {show: true};
- ++i;
- });
- },
-
- plotAccordingToChoices: function () {
- var data = [];
-
- jQuery('.burndown_control').find("input:checked").each(function () {
- var key = jQuery(this).attr('value');
-
- if (key && Burndown.datasets[key]) {
- data.push(Burndown.datasets[key]);
- }
- });
-
- if (data.length === 0) { //in order to render an empty graph if no data is selected
- data.push({data : []});
- }
-
- Burndown.plot(data);
- },
-
- plot: function (data) {
- if (data.length > 0) {
- jQuery.plot(jQuery(".burndown_chart"), data, {
- yaxis: { min: 0,
- ticks: [ <%= yaxis_labels(burndown) %> ] },
- xaxis: {
- ticks: [ <%= xaxis_labels(burndown) %> ],
- tickDecimals: 0,
- max: <%= burndown.days.length + 1 %>,
- min: 1
- },
- grid: { hoverable: true, clickable: true }
- });
- }
- },
-
- showTooltip: function(x, y, contents) {
- jQuery('' + contents + '
').css( {
- position: 'absolute',
- display: 'none',
- top: y + 5,
- left: x + 5,
- border: '1px solid #fdd',
- padding: '2px',
- 'background-color': '#fee',
- opacity: 0.80
- }).appendTo("body").css('z-index', 2000).fadeIn(200);
- },
-
- showTooltipOnHover: function (event, pos, item) {
-
- if (item) {
- if (Burndown.previousPoint != item.dataIndex) {
- Burndown.previousPoint = item.dataIndex;
-
- jQuery("#tooltip").remove();
- var x = item.datapoint[0].toFixed(0),
- y = item.datapoint[1].toFixed(0);
-
- Burndown.showTooltip(item.pageX, item.pageY,
- item.series.label + ": " + y);
- }
- }
- else {
- jQuery("#tooltip").remove();
- Burndown.previousPoint = null;
- }
- },
-
- init: function () {
- Burndown.setDatasetColor();
-
- jQuery('.burndown_control input').click(Burndown.plotAccordingToChoices);
- jQuery(".burndown_chart").bind("plothover", Burndown.showTooltipOnHover);
-
- Burndown.plotAccordingToChoices();
- },
-
- saveInit: function() {
- // Ensure jQuery.plot is defined before progressing.
- // This static page might already be ready but the webpack required
- // jquery.flot.js file might not be loaded yet.
-
- if (jQuery.plot) {
- this.init();
- } else {
- setTimeout(() => { this.saveInit()}, 50);
- }
- }
- };
-
- Burndown.saveInit();
- });
-<% end %>
+<%= angular_component_tag "opce-burndown-chart", "chart-data": {
+ labels: xaxis_labels(@burndown),
+ datasets: dataseries(@burndown)
+ }.to_json %>
diff --git a/modules/backlogs/app/views/rb_burndown_charts/show.html.erb b/modules/backlogs/app/views/rb_burndown_charts/show.html.erb
index 3282a40457d7..5d0524637451 100644
--- a/modules/backlogs/app/views/rb_burndown_charts/show.html.erb
+++ b/modules/backlogs/app/views/rb_burndown_charts/show.html.erb
@@ -27,19 +27,35 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
-
- <%= "#{@sprint.name}: #{@sprint.start_date.present? ? I18n.l(@sprint.start_date) : ''} - #{@sprint.effective_date.present? ? I18n.l(@sprint.effective_date) : ''}" %>
-
+<% html_title @sprint.name %>
+
+<%= content_for :header_tags do %>
+
+<% end -%>
+
+<%=
+ render(Backlogs::SprintPageHeaderComponent.new(sprint: @sprint, project: @project)) do |header|
+ header.with_action_button(
+ tag: :a,
+ href: backlogs_project_sprint_taskboard_path(@project, @sprint),
+ mobile_icon: :"op-view-cards",
+ mobile_label: t(:label_task_board),
+ aria: { label: t(:label_task_board) }
+ ) do |button|
+ button.with_leading_visual_icon(icon: :"op-view-cards")
+ t(:label_task_board)
+ end
+ end
+%>
<% if @burndown %>
<%= render partial: "burndown", locals: { div: "burndown_", burndown: @burndown } %>
-
-
-
-
- <%= t("backlogs.chart_options") %>
- <%= burndown_series_checkboxes(@burndown) %>
-
<% else %>
- <%= t("backlogs.no_burndown_data") %>
+ <%=
+ render(Primer::Beta::Blankslate.new(border: true, spacious: true)) do |blankslate|
+ blankslate.with_visual_icon(icon: :graph)
+ blankslate.with_heading(tag: :h2).with_content(t(".blankslate_title"))
+ blankslate.with_description_content(t(".blankslate_description"))
+ end
+ %>
<% end %>
diff --git a/modules/backlogs/app/views/rb_master_backlogs/_list.html.erb b/modules/backlogs/app/views/rb_master_backlogs/_list.html.erb
new file mode 100644
index 000000000000..4295b3a50ac1
--- /dev/null
+++ b/modules/backlogs/app/views/rb_master_backlogs/_list.html.erb
@@ -0,0 +1,23 @@
+
+ <% if @owner_backlogs.empty? && @sprint_backlogs.empty? %>
+ <%=
+ render(Primer::Beta::Blankslate.new(border: true, spacious: true)) do |blankslate|
+ blankslate.with_visual_icon(icon: :versions)
+ blankslate.with_heading(tag: :h2).with_content(t(:backlogs_empty_title))
+
+ if current_user.allowed_in_project?(:manage_versions, @project)
+ blankslate.with_description_content(t(:backlogs_empty_action_text))
+ end
+ end
+ %>
+ <% else %>
+
+
+ <%= render(Backlogs::BacklogComponent.with_collection(@sprint_backlogs, project: @project)) %>
+
+
+ <%= render(Backlogs::BacklogComponent.with_collection(@owner_backlogs, project: @project)) %>
+
+
+ <% end %>
+
diff --git a/modules/backlogs/app/views/rb_master_backlogs/index.html.erb b/modules/backlogs/app/views/rb_master_backlogs/index.html.erb
index 0ecc2ab079ad..ee5f07a80e8b 100644
--- a/modules/backlogs/app/views/rb_master_backlogs/index.html.erb
+++ b/modules/backlogs/app/views/rb_master_backlogs/index.html.erb
@@ -27,50 +27,40 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
-
<% html_title t(:label_backlogs) %>
-<%=
- render Primer::OpenProject::PageHeader.new do |header|
- header.with_title { t(:label_backlogs) }
- header.with_breadcrumbs(
- [{ href: project_overview_path(@project.id), text: @project.name },
- t(:label_backlogs)]
- )
- end
-%>
+<% content_controller "backlogs",
+ "backlogs-list-url-value": backlogs_project_backlogs_path(@project),
+ "backlogs-backlogs--story-outlet": "li[data-story]" %>
-<%= render(Primer::OpenProject::SubHeader.new) do |subheader|
- subheader.with_action_button(
- scheme: :primary,
- leading_icon: :plus,
- label: I18n.t(:label_version_new),
- tag: :a,
- href: url_for({ controller: "/versions", action: "new", project_id: @project })
- ) do
- t("activerecord.models.version")
- end
- end %>
+<% content_for :content_header do %>
+ <%=
+ render Primer::OpenProject::PageHeader.new do |header|
+ header.with_title { t(:label_backlogs) }
+ header.with_breadcrumbs(
+ [{ href: project_overview_path(@project), text: @project.name },
+ t(:label_backlogs)]
+ )
+ end
+ %>
-<% if (@owner_backlogs.empty? && @sprint_backlogs.empty?) %>
- <%= no_results_box action_url: new_project_version_path(@project),
- display_action: authorize_for("versions", "new"),
- custom_title: t(:backlogs_empty_title),
- custom_action_text: t(:backlogs_empty_action_text) %>
+ <%= render(Primer::OpenProject::SubHeader.new) do |subheader|
+ subheader.with_action_button(
+ scheme: :primary,
+ leading_icon: :plus,
+ label: I18n.t(:label_version_new),
+ tag: :a,
+ href: url_for({ controller: "/versions", action: "new", project_id: @project })
+ ) do
+ t("activerecord.models.version")
+ end
+ end %>
<% end %>
-
-
-
- <%= render partial: "backlog", collection: @owner_backlogs %>
-
-
- <%= render partial: "backlog", collection: @sprint_backlogs %>
-
-
+<% content_for :content_body do %>
+ <%= render partial: "list" %>
+<% end %>
-
- <%= render partial: "rb_stories/helpers" %>
-
<%= date_string_with_milliseconds(@last_update, 0.001) %>
-
-
+<% content_for :content_body_right do %>
+ <%= render(split_view_instance) if render_work_package_split_view? %>
+<% end %>
diff --git a/modules/backlogs/app/views/rb_sprints/_sprint.html.erb b/modules/backlogs/app/views/rb_sprints/_sprint.html.erb
deleted file mode 100644
index af87f44bda95..000000000000
--- a/modules/backlogs/app/views/rb_sprints/_sprint.html.erb
+++ /dev/null
@@ -1,78 +0,0 @@
-<%#-- copyright
-OpenProject is an open source project management software.
-Copyright (C) the OpenProject GmbH
-
-This program is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License version 3.
-
-OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
-Copyright (C) 2006-2013 Jean-Philippe Lang
-Copyright (C) 2010-2013 the ChiliProject Team
-
-This program is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-See COPYRIGHT and LICENSE files for more details.
-
-++#%>
-
-
-
-
<%= sprint_link_or_empty(sprint) %>
-
<%= id_or_empty(sprint) %>
-
-
- <% editable = User.current.allowed_in_project?(:update_sprints, @project) ? "editable" : "" %>
-
-
-
<%= sprint.effective_date %>
-
<%= sprint.start_date %>
-
<%= sprint.name %>
-
-
- <% if User.current.allowed_in_project?(:update_sprints, @project) %>
-
- <%= angular_component_tag "opce-basic-single-date-picker",
- inputs: {
- value: sprint.effective_date,
- inputClassNames: "effective_date editor",
- id: "effective_date_#{sprint.id}",
- name: :effective_date
- } %>
- <%= angular_component_tag "opce-basic-single-date-picker",
- inputs: {
- value: sprint.start_date,
- inputClassNames: "start_date editor",
- id: "start_date_#{sprint.id}",
- name: :start_date
- } %>
- <%= text_field_tag :name,
- sprint.name,
- class: "name editor" %>
-
- <% end %>
-
-
- <%= render partial: "shared/model_errors", object: sprint.errors %>
-
-
diff --git a/modules/backlogs/app/views/rb_stories/_helpers.html.erb b/modules/backlogs/app/views/rb_stories/_helpers.html.erb
deleted file mode 100644
index 34816d40855f..000000000000
--- a/modules/backlogs/app/views/rb_stories/_helpers.html.erb
+++ /dev/null
@@ -1,75 +0,0 @@
-<%#-- copyright
-OpenProject is an open source project management software.
-Copyright (C) the OpenProject GmbH
-
-This program is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License version 3.
-
-OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
-Copyright (C) 2006-2013 Jean-Philippe Lang
-Copyright (C) 2010-2013 the ChiliProject Team
-
-This program is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-See COPYRIGHT and LICENSE files for more details.
-
-++#%>
-
-
-<% available_statuses_by_type.each do |type, statuses| %>
- <% statuses.each do |old_status, allowed_statuses| %>
-
- <% allowed_statuses.sort_by(&:position).each do |status| %>
- ">
- <%= status.name %>
-
- <% end %>
-
- <% end %>
-<% end %>
-
-<% all_work_package_status.each do |old_status| %>
-
- <% if old_status != default_work_package_status %>
- ">
- <%= old_status.name %>
-
- <% else %>
- ">
- <%= default_work_package_status.name %>
-
- <% end %>
-
-<% end %>
-
-
- <% all_work_package_status.each do |status| %>
- ">
- <%= status.name %>
-
- <% end %>
-
-
-
- <% available_story_types.each do |type| %>
-
- <%= type.name %>
-
- <% end %>
-
-
-
- <%= render partial: "rb_stories/story", object: template_story, locals: { project: @project, permission: :add_work_packages } %>
-
diff --git a/modules/backlogs/app/views/rb_stories/_story.html.erb b/modules/backlogs/app/views/rb_stories/_story.html.erb
deleted file mode 100644
index 0f791ab2aacd..000000000000
--- a/modules/backlogs/app/views/rb_stories/_story.html.erb
+++ /dev/null
@@ -1,79 +0,0 @@
-<%#-- copyright
-OpenProject is an open source project management software.
-Copyright (C) the OpenProject GmbH
-
-This program is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License version 3.
-
-OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
-Copyright (C) 2006-2013 Jean-Philippe Lang
-Copyright (C) 2010-2013 the ChiliProject Team
-
-This program is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-See COPYRIGHT and LICENSE files for more details.
-
-++#%>
-
-<%
- project ||= story.project
- permission ||= :edit_work_packages
- other_fields_editable = User.current.allowed_in_project?(permission, project)
- status_field_editable = other_fields_editable || User.current.allowed_in_project?(:change_work_package_status, project)
-%>
-" id="<%= story_html_id_or_empty(story) %>">
-
-
<%= work_package_link_or_empty(story) %>
-
<%= id_or_empty(story) %>
-
- <%= story_points_or_empty(story) %>
-
-
<%= status_label_or_default(story) %>
-
<%= status_id_or_default(story) %>
-
-
-
<%= type_name_or_empty(story) %>:
-
<%= type_id_or_empty(story) %>
-
- <%= story.subject %>
- <%= story.version_id %>
- <%= !defined?(higher_item) || higher_item.nil? ? "" : higher_item.id %>
-
- <%= render(partial: "shared/model_errors", object: errors) if defined?(errors) && !errors.empty? %>
-
-
diff --git a/modules/backlogs/app/views/rb_taskboards/show.html.erb b/modules/backlogs/app/views/rb_taskboards/show.html.erb
index 7416de928d27..23dc8eb7a2d9 100644
--- a/modules/backlogs/app/views/rb_taskboards/show.html.erb
+++ b/modules/backlogs/app/views/rb_taskboards/show.html.erb
@@ -27,33 +27,50 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
+<% content_for :additional_js_dom_ready do %>
+ <%= render(partial: "shared/server_variables", formats: [:js]) %>
+<% end %>
+
+<% content_controller "backlogs--taskboard-legacy" %>
+
<% html_title @sprint.name %>
+
<%=
- render Primer::OpenProject::PageHeader.new do |header|
- header.with_title { @sprint.name }
- header.with_breadcrumbs(
- [{ href: project_overview_path(@project.id), text: @project.name },
- { href: backlogs_project_backlogs_path(@project), text: t(:label_backlogs) },
- @sprint.name]
- )
+ render(Backlogs::SprintPageHeaderComponent.new(sprint: @sprint, project: @project)) do |header|
+ header.with_action_button(
+ tag: :a,
+ href: backlogs_project_sprint_burndown_chart_path(@project, @sprint),
+ mobile_icon: :graph,
+ mobile_label: t(:"backlogs.show_burndown_chart"),
+ aria: { label: t(:"backlogs.show_burndown_chart") },
+ disabled: !@sprint.has_burndown?
+ ) do |button|
+ button.with_leading_visual_icon(icon: :graph)
+ t(:"backlogs.show_burndown_chart")
+ end
end
%>
-<%# we decided to keep current toolbar design for taskboard %>
-
+<% end %>
+