vargburz
3 years ago
17 changed files with 5757 additions and 2 deletions
@ -0,0 +1,201 @@ |
|||||||
|
Apache License |
||||||
|
Version 2.0, January 2004 |
||||||
|
http://www.apache.org/licenses/ |
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
||||||
|
|
||||||
|
1. Definitions. |
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction, |
||||||
|
and distribution as defined by Sections 1 through 9 of this document. |
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by |
||||||
|
the copyright owner that is granting the License. |
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all |
||||||
|
other entities that control, are controlled by, or are under common |
||||||
|
control with that entity. For the purposes of this definition, |
||||||
|
"control" means (i) the power, direct or indirect, to cause the |
||||||
|
direction or management of such entity, whether by contract or |
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the |
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity. |
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity |
||||||
|
exercising permissions granted by this License. |
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications, |
||||||
|
including but not limited to software source code, documentation |
||||||
|
source, and configuration files. |
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical |
||||||
|
transformation or translation of a Source form, including but |
||||||
|
not limited to compiled object code, generated documentation, |
||||||
|
and conversions to other media types. |
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or |
||||||
|
Object form, made available under the License, as indicated by a |
||||||
|
copyright notice that is included in or attached to the work |
||||||
|
(an example is provided in the Appendix below). |
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object |
||||||
|
form, that is based on (or derived from) the Work and for which the |
||||||
|
editorial revisions, annotations, elaborations, or other modifications |
||||||
|
represent, as a whole, an original work of authorship. For the purposes |
||||||
|
of this License, Derivative Works shall not include works that remain |
||||||
|
separable from, or merely link (or bind by name) to the interfaces of, |
||||||
|
the Work and Derivative Works thereof. |
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including |
||||||
|
the original version of the Work and any modifications or additions |
||||||
|
to that Work or Derivative Works thereof, that is intentionally |
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner |
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of |
||||||
|
the copyright owner. For the purposes of this definition, "submitted" |
||||||
|
means any form of electronic, verbal, or written communication sent |
||||||
|
to the Licensor or its representatives, including but not limited to |
||||||
|
communication on electronic mailing lists, source code control systems, |
||||||
|
and issue tracking systems that are managed by, or on behalf of, the |
||||||
|
Licensor for the purpose of discussing and improving the Work, but |
||||||
|
excluding communication that is conspicuously marked or otherwise |
||||||
|
designated in writing by the copyright owner as "Not a Contribution." |
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity |
||||||
|
on behalf of whom a Contribution has been received by Licensor and |
||||||
|
subsequently incorporated within the Work. |
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of |
||||||
|
this License, each Contributor hereby grants to You a perpetual, |
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||||
|
copyright license to reproduce, prepare Derivative Works of, |
||||||
|
publicly display, publicly perform, sublicense, and distribute the |
||||||
|
Work and such Derivative Works in Source or Object form. |
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of |
||||||
|
this License, each Contributor hereby grants to You a perpetual, |
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||||
|
(except as stated in this section) patent license to make, have made, |
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work, |
||||||
|
where such license applies only to those patent claims licensable |
||||||
|
by such Contributor that are necessarily infringed by their |
||||||
|
Contribution(s) alone or by combination of their Contribution(s) |
||||||
|
with the Work to which such Contribution(s) was submitted. If You |
||||||
|
institute patent litigation against any entity (including a |
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work |
||||||
|
or a Contribution incorporated within the Work constitutes direct |
||||||
|
or contributory patent infringement, then any patent licenses |
||||||
|
granted to You under this License for that Work shall terminate |
||||||
|
as of the date such litigation is filed. |
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the |
||||||
|
Work or Derivative Works thereof in any medium, with or without |
||||||
|
modifications, and in Source or Object form, provided that You |
||||||
|
meet the following conditions: |
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or |
||||||
|
Derivative Works a copy of this License; and |
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices |
||||||
|
stating that You changed the files; and |
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works |
||||||
|
that You distribute, all copyright, patent, trademark, and |
||||||
|
attribution notices from the Source form of the Work, |
||||||
|
excluding those notices that do not pertain to any part of |
||||||
|
the Derivative Works; and |
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its |
||||||
|
distribution, then any Derivative Works that You distribute must |
||||||
|
include a readable copy of the attribution notices contained |
||||||
|
within such NOTICE file, excluding those notices that do not |
||||||
|
pertain to any part of the Derivative Works, in at least one |
||||||
|
of the following places: within a NOTICE text file distributed |
||||||
|
as part of the Derivative Works; within the Source form or |
||||||
|
documentation, if provided along with the Derivative Works; or, |
||||||
|
within a display generated by the Derivative Works, if and |
||||||
|
wherever such third-party notices normally appear. The contents |
||||||
|
of the NOTICE file are for informational purposes only and |
||||||
|
do not modify the License. You may add Your own attribution |
||||||
|
notices within Derivative Works that You distribute, alongside |
||||||
|
or as an addendum to the NOTICE text from the Work, provided |
||||||
|
that such additional attribution notices cannot be construed |
||||||
|
as modifying the License. |
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and |
||||||
|
may provide additional or different license terms and conditions |
||||||
|
for use, reproduction, or distribution of Your modifications, or |
||||||
|
for any such Derivative Works as a whole, provided Your use, |
||||||
|
reproduction, and distribution of the Work otherwise complies with |
||||||
|
the conditions stated in this License. |
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise, |
||||||
|
any Contribution intentionally submitted for inclusion in the Work |
||||||
|
by You to the Licensor shall be under the terms and conditions of |
||||||
|
this License, without any additional terms or conditions. |
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify |
||||||
|
the terms of any separate license agreement you may have executed |
||||||
|
with Licensor regarding such Contributions. |
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade |
||||||
|
names, trademarks, service marks, or product names of the Licensor, |
||||||
|
except as required for reasonable and customary use in describing the |
||||||
|
origin of the Work and reproducing the content of the NOTICE file. |
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or |
||||||
|
agreed to in writing, Licensor provides the Work (and each |
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS, |
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
||||||
|
implied, including, without limitation, any warranties or conditions |
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the |
||||||
|
appropriateness of using or redistributing the Work and assume any |
||||||
|
risks associated with Your exercise of permissions under this License. |
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory, |
||||||
|
whether in tort (including negligence), contract, or otherwise, |
||||||
|
unless required by applicable law (such as deliberate and grossly |
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be |
||||||
|
liable to You for damages, including any direct, indirect, special, |
||||||
|
incidental, or consequential damages of any character arising as a |
||||||
|
result of this License or out of the use or inability to use the |
||||||
|
Work (including but not limited to damages for loss of goodwill, |
||||||
|
work stoppage, computer failure or malfunction, or any and all |
||||||
|
other commercial damages or losses), even if such Contributor |
||||||
|
has been advised of the possibility of such damages. |
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing |
||||||
|
the Work or Derivative Works thereof, You may choose to offer, |
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity, |
||||||
|
or other liability obligations and/or rights consistent with this |
||||||
|
License. However, in accepting such obligations, You may act only |
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf |
||||||
|
of any other Contributor, and only if You agree to indemnify, |
||||||
|
defend, and hold each Contributor harmless for any liability |
||||||
|
incurred by, or claims asserted against, such Contributor by reason |
||||||
|
of your accepting any such warranty or additional liability. |
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS |
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work. |
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following |
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]" |
||||||
|
replaced with your own identifying information. (Don't include |
||||||
|
the brackets!) The text should be enclosed in the appropriate |
||||||
|
comment syntax for the file format. We also recommend that a |
||||||
|
file or class name and description of purpose be included on the |
||||||
|
same "printed page" as the copyright notice for easier |
||||||
|
identification within third-party archives. |
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner] |
||||||
|
|
||||||
|
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. |
@ -0,0 +1,35 @@ |
|||||||
|
const path = require('path'); |
||||||
|
|
||||||
|
|
||||||
|
function resolve(dir) { |
||||||
|
return path.join(__dirname, '..', dir) |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
context: resolve('src'), |
||||||
|
entry: './index.ts', |
||||||
|
plugins: [], |
||||||
|
module: { |
||||||
|
rules: [ |
||||||
|
{ |
||||||
|
test: /\.ts$/, |
||||||
|
use: 'ts-loader', |
||||||
|
exclude: /node_modules/ |
||||||
|
}, |
||||||
|
{ |
||||||
|
test: /\.css$/, |
||||||
|
use: ['style-loader', 'css-loader'], |
||||||
|
exclude: /node_modules/ |
||||||
|
} |
||||||
|
], |
||||||
|
}, |
||||||
|
resolve: { |
||||||
|
extensions: ['.ts', '.js'], |
||||||
|
}, |
||||||
|
output: { |
||||||
|
filename: 'index.js', |
||||||
|
path: resolve('dist'), |
||||||
|
libraryTarget: 'umd', |
||||||
|
umdNamedDefine: true |
||||||
|
} |
||||||
|
}; |
@ -0,0 +1,8 @@ |
|||||||
|
const baseWebpackConfig = require('./webpack.base.conf'); |
||||||
|
|
||||||
|
var conf = baseWebpackConfig; |
||||||
|
conf.devtool = 'inline-source-map'; |
||||||
|
conf.watch = true; |
||||||
|
conf.mode = 'development'; |
||||||
|
|
||||||
|
module.exports = conf; |
@ -0,0 +1,6 @@ |
|||||||
|
const baseWebpackConfig = require('./webpack.base.conf'); |
||||||
|
|
||||||
|
var conf = baseWebpackConfig; |
||||||
|
conf.mode = 'production'; |
||||||
|
|
||||||
|
module.exports = baseWebpackConfig; |
@ -0,0 +1,99 @@ |
|||||||
|
import { ChartwerkPod, TickOrientation, TimeFormat } from '@chartwerk/core'; |
||||||
|
import { LineTimeSerie, LineOptions, Mode } from './types'; |
||||||
|
export declare class ChartwerkLineChart extends ChartwerkPod<LineTimeSerie, LineOptions> { |
||||||
|
lineGenerator: any; |
||||||
|
metricContainer: any; |
||||||
|
constructor(_el: HTMLElement, _series?: LineTimeSerie[], _options?: LineOptions); |
||||||
|
renderMetrics(): void; |
||||||
|
initLineGenerator(): void; |
||||||
|
appendData(data: [number, number][]): void; |
||||||
|
_renderDots(datapoints: number[][], serieIdx: number): void; |
||||||
|
_renderLines(datapoints: number[][], serieIdx: number): void; |
||||||
|
_renderMetric(datapoints: number[][], metricOptions: { |
||||||
|
color: string; |
||||||
|
confidence: number; |
||||||
|
target: string; |
||||||
|
mode: Mode; |
||||||
|
serieIdx: number; |
||||||
|
renderDots: boolean; |
||||||
|
renderLines: boolean; |
||||||
|
}): void; |
||||||
|
updateCrosshair(): void; |
||||||
|
appendCrosshairCircles(): void; |
||||||
|
appendCrosshairCircle(serieIdx: number): void; |
||||||
|
renderSharedCrosshair(values: { |
||||||
|
x?: number; |
||||||
|
y?: number; |
||||||
|
}): void; |
||||||
|
hideSharedCrosshair(): void; |
||||||
|
moveCrosshairLine(xPosition: number, yPosition: number): void; |
||||||
|
moveCrosshairCircle(xPosition: number, yPosition: number, serieIdx: number): void; |
||||||
|
hideCrosshairCircle(serieIdx: number): void; |
||||||
|
getClosestDatapoint(serie: LineTimeSerie, xValue: number, yValue: number): [number, number]; |
||||||
|
getClosestIndex(datapoints: [number, number][], xValue: number, yValue: number): number; |
||||||
|
getValueInterval(columnIdx: number): number | undefined; |
||||||
|
onMouseMove(): void; |
||||||
|
findAndHighlightDatapoints(xValue: number, yValue: number): { |
||||||
|
value: [number, number]; |
||||||
|
color: string; |
||||||
|
label: string; |
||||||
|
}[]; |
||||||
|
isOutOfRange(closestDatapoint: [number, number], xValue: number, yValue: number, useOutOfRange?: boolean): boolean; |
||||||
|
onMouseOver(): void; |
||||||
|
onMouseOut(): void; |
||||||
|
} |
||||||
|
export declare const VueChartwerkLineChartObject: { |
||||||
|
render(createElement: any): any; |
||||||
|
mixins: { |
||||||
|
props: { |
||||||
|
id: { |
||||||
|
type: StringConstructor; |
||||||
|
required: boolean; |
||||||
|
}; |
||||||
|
series: { |
||||||
|
type: ArrayConstructor; |
||||||
|
required: boolean; |
||||||
|
default: () => any[]; |
||||||
|
}; |
||||||
|
options: { |
||||||
|
type: ObjectConstructor; |
||||||
|
required: boolean; |
||||||
|
default: () => {}; |
||||||
|
}; |
||||||
|
}; |
||||||
|
watch: { |
||||||
|
id(): void; |
||||||
|
series(): void; |
||||||
|
options(): void; |
||||||
|
}; |
||||||
|
mounted(): void; |
||||||
|
destroyed(): void; |
||||||
|
methods: { |
||||||
|
render(): void; |
||||||
|
renderSharedCrosshair(values: { |
||||||
|
x?: number; |
||||||
|
y?: number; |
||||||
|
}): void; |
||||||
|
hideSharedCrosshair(): void; |
||||||
|
onPanningRescale(event: any): void; |
||||||
|
renderChart(): void; |
||||||
|
appendEvents(): void; |
||||||
|
zoomIn(range: any): void; |
||||||
|
zoomOut(centers: any): void; |
||||||
|
mouseMove(evt: any): void; |
||||||
|
mouseOut(): void; |
||||||
|
onLegendClick(idx: any): void; |
||||||
|
panningEnd(range: any): void; |
||||||
|
panning(range: any): void; |
||||||
|
contextMenu(evt: any): void; |
||||||
|
sharedCrosshairMove(event: any): void; |
||||||
|
renderEnd(): void; |
||||||
|
}; |
||||||
|
}[]; |
||||||
|
methods: { |
||||||
|
render(): void; |
||||||
|
renderSharedCrosshair(values: any): void; |
||||||
|
hideSharedCrosshair(): void; |
||||||
|
}; |
||||||
|
}; |
||||||
|
export { LineTimeSerie, LineOptions, Mode, TickOrientation, TimeFormat }; |
File diff suppressed because one or more lines are too long
@ -0,0 +1,16 @@ |
|||||||
|
import { TimeSerie, Options } from '@chartwerk/core'; |
||||||
|
declare type LineTimeSerieParams = { |
||||||
|
confidence: number; |
||||||
|
mode: Mode; |
||||||
|
maxLength: number; |
||||||
|
renderDots: boolean; |
||||||
|
renderLines: boolean; |
||||||
|
useOutOfRange: boolean; |
||||||
|
}; |
||||||
|
export declare enum Mode { |
||||||
|
STANDARD = "Standard", |
||||||
|
CHARGE = "Charge" |
||||||
|
} |
||||||
|
export declare type LineTimeSerie = TimeSerie & Partial<LineTimeSerieParams>; |
||||||
|
export declare type LineOptions = Options; |
||||||
|
export {}; |
@ -0,0 +1,36 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> |
||||||
|
<meta content="utf-8" http-equiv="encoding"> |
||||||
|
|
||||||
|
<script src="../dist/index.js" type="text/javascript"></script> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="chart" style="width: 500px; height: 500px;"></div> |
||||||
|
|
||||||
|
<script type="text/javascript"> |
||||||
|
const startTime = 1590590148; |
||||||
|
const arrayLength = 20; |
||||||
|
const data1 = Array.from({ length: arrayLength }, (el, idx) => [Math.floor(Math.random() * 40), startTime + idx * 10000]); |
||||||
|
const data2 = Array.from({ length: arrayLength }, (el, idx) => [Math.floor(Math.random() * 100), startTime + idx * 10000]); |
||||||
|
const data3 = Array.from({ length: arrayLength }, (el, idx) => [Math.floor(Math.random() * 20) + 90, startTime + idx * 10000]); |
||||||
|
const zoomIn = (ranges) => { const range = ranges[0]; options.axis.x.range = range; pod.updateData(undefined, options) } |
||||||
|
const zoomOut = (ranges) => { console.log('zoomout'); options.axis.x.range = undefined; pod.updateData(undefined, options) } |
||||||
|
let options = { |
||||||
|
renderLegend: false, usePanning: false, axis: { y: { invert: false, range: [0, 350] }, x: { format: 'time' } }, |
||||||
|
eventsCallbacks: { zoomIn: zoomIn, zoomOut } |
||||||
|
} |
||||||
|
var pod = new ChartwerkLineChart( |
||||||
|
document.getElementById('chart'), |
||||||
|
[ |
||||||
|
{ target: 'test1', datapoints: data1, color: 'green' }, |
||||||
|
{ target: 'test2', datapoints: data2, color: 'blue' }, |
||||||
|
{ target: 'test3', datapoints: data3, color: 'orange' }, |
||||||
|
], |
||||||
|
options |
||||||
|
); |
||||||
|
pod.render(); |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,48 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html> |
||||||
|
|
||||||
|
<head> |
||||||
|
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> |
||||||
|
<meta content="utf-8" http-equiv="encoding"> |
||||||
|
|
||||||
|
<script src="../dist/index.js" type="text/javascript"></script> |
||||||
|
</head> |
||||||
|
|
||||||
|
<body> |
||||||
|
<div id="chart" style="width: 500px; height: 500px;"></div> |
||||||
|
|
||||||
|
<script type="text/javascript"> |
||||||
|
const startTime = 1590590148; |
||||||
|
const arrayLength = 100; |
||||||
|
const data1 = Array.from({ length: arrayLength }, (el, idx) => [Math.floor(Math.random() * 40), startTime + idx * 10000]); |
||||||
|
const data2 = Array.from({ length: arrayLength }, (el, idx) => [Math.floor(Math.random() * 100), startTime + idx * 10000]); |
||||||
|
const data3 = Array.from({ length: arrayLength }, (el, idx) => [Math.floor(Math.random() * 20) + 90, startTime + idx * 10000]); |
||||||
|
let options = { renderLegend: false, usePanning: false, axis: { y: { invert: false, range: [0, 350] }, x: { format: 'time' } } }; |
||||||
|
var pod = new ChartwerkLineChart( |
||||||
|
document.getElementById('chart'), |
||||||
|
[ |
||||||
|
{ target: 'test1', datapoints: data1, color: 'green', maxLength: arrayLength + 30, renderDots: true }, |
||||||
|
{ target: 'test2', datapoints: data2, color: 'blue', maxLength: arrayLength + 30, renderDots: true }, |
||||||
|
{ target: 'test3', datapoints: data3, color: 'orange', maxLength: arrayLength + 30, renderDots: true }, |
||||||
|
], |
||||||
|
options |
||||||
|
); |
||||||
|
pod.render(); |
||||||
|
let rerenderIdx = arrayLength; |
||||||
|
var test = setInterval(() => { |
||||||
|
rerenderIdx += 1; |
||||||
|
const d1 = [Math.floor(Math.random() * 40), startTime + rerenderIdx * 10000]; |
||||||
|
const d2 = [Math.floor(Math.random() * 100), startTime + rerenderIdx * 10000]; |
||||||
|
const d3 = [Math.floor(Math.random() * 20) + 90, startTime + rerenderIdx * 10000]; |
||||||
|
|
||||||
|
console.time('rerender'); |
||||||
|
pod.appendData([d1, d2, d3]); |
||||||
|
console.timeEnd('rerender'); |
||||||
|
if(rerenderIdx > arrayLength + 100) { |
||||||
|
clearInterval(test); |
||||||
|
} |
||||||
|
}, 1000); |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
|
||||||
|
</html> |
@ -0,0 +1,90 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> |
||||||
|
<meta content="utf-8" http-equiv="encoding"> |
||||||
|
|
||||||
|
<script src="../dist/index.js" type="text/javascript"></script> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="chart" style="width: 50%; height: 500px;"></div> |
||||||
|
|
||||||
|
<script type="text/javascript"> |
||||||
|
const startTime = 1590590148; |
||||||
|
const arrayLength = 20; |
||||||
|
const data1 = createDatapoints(arrayLength, startTime, 40); |
||||||
|
const data2 = createDatapoints(arrayLength, startTime, 30, 50); |
||||||
|
const data3 = createDatapoints(arrayLength, startTime, 20, 90); |
||||||
|
|
||||||
|
let options = { |
||||||
|
renderLegend: false, |
||||||
|
axis: { |
||||||
|
y: { invert: true, valueFormatter: timeValueFormatter, format: 'custom', colorFormatter: colorFormatter }, |
||||||
|
x: { format: 'numeric' } |
||||||
|
}, |
||||||
|
zoomEvents: { |
||||||
|
mouse: { |
||||||
|
pan: { isActive: true, orientation: 'vertical', keyEvent: 'shift' }, |
||||||
|
zoom: { isActive: true, orientation: 'vertical', keyEvent: 'main' } |
||||||
|
}, |
||||||
|
scroll: { |
||||||
|
pan: { isActive: true, orientation: 'vertical', keyEvent: 'main' }, |
||||||
|
zoom: { isActive: false, keyEvent: 'shift' } |
||||||
|
} |
||||||
|
}, |
||||||
|
eventsCallbacks: { zoomIn: zoomIn, zoomOut, panning: onPanning, }, |
||||||
|
crosshair: { orientation: 'horizontal' }, |
||||||
|
margin: { top: 30, right: 20, bottom: 20, left: 50 } |
||||||
|
} |
||||||
|
var pod = new ChartwerkLineChart( |
||||||
|
document.getElementById('chart'), |
||||||
|
[ |
||||||
|
{ target: 'test1', datapoints: data1, color: 'green' }, |
||||||
|
{ target: 'test2', datapoints: data2, color: 'blue' }, |
||||||
|
{ target: 'test3', datapoints: data3, color: 'orange' }, |
||||||
|
], |
||||||
|
options |
||||||
|
); |
||||||
|
pod.render(); |
||||||
|
|
||||||
|
|
||||||
|
function zoomIn(ranges) { |
||||||
|
const range = ranges[1]; |
||||||
|
options.axis.y.range = range; |
||||||
|
pod.updateData(undefined, options); |
||||||
|
} |
||||||
|
|
||||||
|
function zoomOut() { |
||||||
|
options.axis.y.range = undefined; |
||||||
|
console.log('zoomOut', pod.updateData); |
||||||
|
pod.updateData(undefined, options) |
||||||
|
} |
||||||
|
|
||||||
|
function onPanning() { |
||||||
|
console.log('panning', pod); |
||||||
|
} |
||||||
|
|
||||||
|
function createDatapoints(arrayLength, startTime, randomValue, randomOffset = 0) { |
||||||
|
return Array.from({ length: arrayLength }, (el, idx) => [ |
||||||
|
startTime + idx * 10000, // y axis |
||||||
|
Math.floor(Math.random() * randomValue) + randomOffset // x axis |
||||||
|
]); |
||||||
|
} |
||||||
|
|
||||||
|
function timeValueFormatter(value) { |
||||||
|
const date = new Date(value); |
||||||
|
const hours = date.getHours(); |
||||||
|
const minutes = '0' + date.getMinutes(); |
||||||
|
const seconds = '0' + date.getSeconds(); |
||||||
|
return hours + ':' + minutes.substr(-2) + ':' + seconds.substr(-2); |
||||||
|
} |
||||||
|
|
||||||
|
function colorFormatter(value, index) { |
||||||
|
if(index % 2 === 0) { |
||||||
|
return 'red'; |
||||||
|
} |
||||||
|
return 'black'; |
||||||
|
} |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,32 @@ |
|||||||
|
{ |
||||||
|
"name": "@chartwerk/line-chart", |
||||||
|
"version": "0.2.4", |
||||||
|
"description": "Chartwerk line chart", |
||||||
|
"main": "dist/index.js", |
||||||
|
"scripts": { |
||||||
|
"build": "webpack --config build/webpack.prod.conf.js", |
||||||
|
"dev": "webpack --config build/webpack.dev.conf.js", |
||||||
|
"test": "echo \"Error: no test specified\" && exit 1" |
||||||
|
}, |
||||||
|
"repository": { |
||||||
|
"type": "git", |
||||||
|
"url": "https://github.com/chartwerk/line-chart.git" |
||||||
|
}, |
||||||
|
"author": "CorpGlory", |
||||||
|
"license": "Apache-2.0", |
||||||
|
"dependencies": { |
||||||
|
"@chartwerk/core": "github:chartwerk/core#6e41013932bf16b05da2bc2757bcfaacba3cdb21" |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"@types/d3": "5.16.4", |
||||||
|
"@types/lodash": "^4.14.149", |
||||||
|
"css-loader": "^3.4.2", |
||||||
|
"d3": "5.16.0", |
||||||
|
"lodash": "^4.17.15", |
||||||
|
"style-loader": "^1.1.3", |
||||||
|
"ts-loader": "^6.2.1", |
||||||
|
"typescript": "^3.8.3", |
||||||
|
"webpack": "^4.42.0", |
||||||
|
"webpack-cli": "^3.3.11" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,522 @@ |
|||||||
|
import { ChartwerkPod, VueChartwerkPodMixin, TickOrientation, TimeFormat, CrosshairOrientation } from '@chartwerk/core'; |
||||||
|
import { LineTimeSerie, LineOptions, Mode } from './types'; |
||||||
|
|
||||||
|
import * as d3 from 'd3'; |
||||||
|
import * as _ from 'lodash'; |
||||||
|
|
||||||
|
const METRIC_CIRCLE_RADIUS = 1.5; |
||||||
|
const CROSSHAIR_CIRCLE_RADIUS = 3; |
||||||
|
const CROSSHAIR_BACKGROUND_RAIDUS = 9; |
||||||
|
const CROSSHAIR_BACKGROUND_OPACITY = 0.3; |
||||||
|
|
||||||
|
export class ChartwerkLineChart extends ChartwerkPod<LineTimeSerie, LineOptions> { |
||||||
|
lineGenerator = null; |
||||||
|
metricContainer = null; |
||||||
|
|
||||||
|
constructor(_el: HTMLElement, _series: LineTimeSerie[] = [], _options: LineOptions = {}) { |
||||||
|
super(d3, _el, _series, _options); |
||||||
|
} |
||||||
|
|
||||||
|
renderMetrics(): void { |
||||||
|
this.updateCrosshair(); |
||||||
|
this.initLineGenerator(); |
||||||
|
|
||||||
|
// TODO: seems that renderMetrics is not correct name
|
||||||
|
if(this.series.length === 0) { |
||||||
|
this.renderNoDataPointsMessage(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: move to core, and create only one container
|
||||||
|
// container for clip path
|
||||||
|
const clipContatiner = this.chartContainer |
||||||
|
.append('g') |
||||||
|
.attr('clip-path', `url(#${this.rectClipId})`) |
||||||
|
.attr('class', 'metrics-container'); |
||||||
|
|
||||||
|
// container for panning
|
||||||
|
this.metricContainer = clipContatiner |
||||||
|
.append('g') |
||||||
|
.attr('class', ' metrics-rect') |
||||||
|
|
||||||
|
for(let idx = 0; idx < this.series.length; ++idx) { |
||||||
|
if(this.series[idx].visible === false) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
// TODO: use _.defaults same as in core
|
||||||
|
const confidence = this.series[idx].confidence || 0; |
||||||
|
const mode = this.series[idx].mode || Mode.STANDARD; |
||||||
|
const target = this.series[idx].target; |
||||||
|
const renderDots = this.series[idx].renderDots !== undefined ? this.series[idx].renderDots : false; |
||||||
|
const renderLines = this.series[idx].renderLines !== undefined ? this.series[idx].renderLines : true; |
||||||
|
|
||||||
|
this._renderMetric( |
||||||
|
this.series[idx].datapoints, |
||||||
|
{ |
||||||
|
color: this.getSerieColor(idx), |
||||||
|
confidence, |
||||||
|
target, |
||||||
|
mode, |
||||||
|
serieIdx: idx, |
||||||
|
renderDots, |
||||||
|
renderLines, |
||||||
|
} |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
initLineGenerator(): void { |
||||||
|
this.lineGenerator = this.d3.line() |
||||||
|
.x(d => this.xScale(d[1])) |
||||||
|
.y(d => this.yScale(d[0])); |
||||||
|
} |
||||||
|
|
||||||
|
public appendData(data: [number, number][]): void { |
||||||
|
this.clearScaleCache(); |
||||||
|
|
||||||
|
for(let idx = 0; idx < this.series.length; ++idx) { |
||||||
|
if(this.series[idx].visible === false) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
this.series[idx].datapoints.push(data[idx]); |
||||||
|
const maxLength = this.series[idx].maxLength; |
||||||
|
if(maxLength !== undefined && this.series[idx].datapoints.length > maxLength) { |
||||||
|
this.series[idx].datapoints.shift(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for(let idx = 0; idx < this.series.length; ++idx) { |
||||||
|
this.metricContainer.select(`.metric-path-${idx}`) |
||||||
|
.datum(this.series[idx].datapoints) |
||||||
|
.attr('d', this.lineGenerator); |
||||||
|
|
||||||
|
if(this.series[idx].renderDots === true) { |
||||||
|
this.metricContainer.selectAll(`.metric-circle-${idx}`) |
||||||
|
.data(this.series[idx].datapoints) |
||||||
|
.attr('cx', d => this.xScale(d[1])) |
||||||
|
.attr('cy', d => this.yScale(d[0])); |
||||||
|
|
||||||
|
this._renderDots([data[idx]], idx); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
this.renderXAxis(); |
||||||
|
this.renderYAxis(); |
||||||
|
this.renderGrid(); |
||||||
|
} |
||||||
|
|
||||||
|
_renderDots(datapoints: number[][], serieIdx: number): void { |
||||||
|
this.metricContainer.selectAll(null) |
||||||
|
.data(datapoints) |
||||||
|
.enter() |
||||||
|
.append('circle') |
||||||
|
.attr('class', `metric-circle-${serieIdx} metric-el`) |
||||||
|
.attr('fill', this.getSerieColor(serieIdx)) |
||||||
|
.attr('r', METRIC_CIRCLE_RADIUS) |
||||||
|
.style('pointer-events', 'none') |
||||||
|
.attr('cx', d => this.xScale(d[1])) |
||||||
|
.attr('cy', d => this.yScale(d[0])); |
||||||
|
} |
||||||
|
|
||||||
|
_renderLines(datapoints: number[][], serieIdx: number): void { |
||||||
|
this.metricContainer |
||||||
|
.append('path') |
||||||
|
.datum(datapoints) |
||||||
|
.attr('class', `metric-path-${serieIdx} metric-el`) |
||||||
|
.attr('fill', 'none') |
||||||
|
.attr('stroke', this.getSerieColor(serieIdx)) |
||||||
|
.attr('stroke-width', 1) |
||||||
|
.attr('stroke-opacity', 0.7) |
||||||
|
.attr('pointer-events', 'none') |
||||||
|
.attr('d', this.lineGenerator); |
||||||
|
} |
||||||
|
|
||||||
|
_renderMetric( |
||||||
|
datapoints: number[][], |
||||||
|
metricOptions: { |
||||||
|
color: string, |
||||||
|
confidence: number, |
||||||
|
target: string, |
||||||
|
mode: Mode, |
||||||
|
serieIdx: number, |
||||||
|
renderDots: boolean, |
||||||
|
renderLines: boolean, |
||||||
|
} |
||||||
|
): void { |
||||||
|
if(_.includes(this.seriesTargetsWithBounds, metricOptions.target)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if(metricOptions.mode === Mode.CHARGE) { |
||||||
|
const dataPairs = this.d3.pairs(datapoints); |
||||||
|
this.metricContainer.selectAll(null) |
||||||
|
.data(dataPairs) |
||||||
|
.enter() |
||||||
|
.append('line') |
||||||
|
.attr('x1', d => this.xScale(d[0][1])) |
||||||
|
.attr('x2', d => this.xScale(d[1][1])) |
||||||
|
.attr('y1', d => this.yScale(d[0][0])) |
||||||
|
.attr('y2', d => this.yScale(d[1][0])) |
||||||
|
.attr('stroke-opacity', 0.7) |
||||||
|
.style('stroke-width', 1) |
||||||
|
.style('stroke', d => { |
||||||
|
if(d[1][0] > d[0][0]) { |
||||||
|
return 'green'; |
||||||
|
} else if (d[1][0] < d[0][0]) { |
||||||
|
return 'red'; |
||||||
|
} else { |
||||||
|
return 'gray'; |
||||||
|
} |
||||||
|
}); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if(metricOptions.renderLines === true) { |
||||||
|
this._renderLines(datapoints, metricOptions.serieIdx); |
||||||
|
} |
||||||
|
|
||||||
|
if(metricOptions.renderDots === true) { |
||||||
|
this._renderDots(datapoints, metricOptions.serieIdx); |
||||||
|
} |
||||||
|
|
||||||
|
let upperBoundDatapoints = []; |
||||||
|
let lowerBoundDatapoints = []; |
||||||
|
if( |
||||||
|
this.options.bounds !== undefined && |
||||||
|
this.options.bounds.upper !== undefined && |
||||||
|
this.options.bounds.lower !== undefined |
||||||
|
) { |
||||||
|
this.series.forEach(serie => { |
||||||
|
if(serie.target === this.formatedBound(this.options.bounds.upper, metricOptions.target)) { |
||||||
|
upperBoundDatapoints = serie.datapoints; |
||||||
|
} |
||||||
|
if(serie.target === this.formatedBound(this.options.bounds.lower, metricOptions.target)) { |
||||||
|
lowerBoundDatapoints = serie.datapoints; |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
if(upperBoundDatapoints.length > 0 && lowerBoundDatapoints.length > 0) { |
||||||
|
const zip = (arr1, arr2) => arr1.map((k, i) => [k[0],k[1], arr2[i][0]]); |
||||||
|
const data = zip(upperBoundDatapoints, lowerBoundDatapoints); |
||||||
|
|
||||||
|
this.metricContainer.append('path') |
||||||
|
.datum(data) |
||||||
|
.attr('fill', metricOptions.color) |
||||||
|
.attr('stroke', 'none') |
||||||
|
.attr('opacity', '0.3') |
||||||
|
.attr('d', this.d3.area() |
||||||
|
.x((d: number[]) => this.xScale(d[1])) |
||||||
|
.y0((d: number[]) => this.yScale(d[0])) |
||||||
|
.y1((d: number[]) => this.yScale(d[2])) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
if(metricOptions.confidence > 0) { |
||||||
|
this.metricContainer.append('path') |
||||||
|
.datum(datapoints) |
||||||
|
.attr('fill', metricOptions.color) |
||||||
|
.attr('stroke', 'none') |
||||||
|
.attr('opacity', '0.3') |
||||||
|
.attr('d', this.d3.area() |
||||||
|
.x((d: [number, number]) => this.xScale(d[1])) |
||||||
|
.y0((d: [number, number]) => this.yScale(d[0] + metricOptions.confidence)) |
||||||
|
.y1((d: [number, number]) => this.yScale(d[0] - metricOptions.confidence)) |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
updateCrosshair(): void { |
||||||
|
// Base don't know anything about crosshair circles, It is only for line pod
|
||||||
|
this.appendCrosshairCircles(); |
||||||
|
} |
||||||
|
|
||||||
|
appendCrosshairCircles(): void { |
||||||
|
// circle for each serie
|
||||||
|
this.series.forEach((serie: LineTimeSerie, serieIdx: number) => { |
||||||
|
this.appendCrosshairCircle(serieIdx); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
appendCrosshairCircle(serieIdx: number): void { |
||||||
|
this.crosshair.append('circle') |
||||||
|
.attr('class', `crosshair-circle-${serieIdx} crosshair-background`) |
||||||
|
.attr('r', CROSSHAIR_BACKGROUND_RAIDUS) |
||||||
|
.attr('clip-path', `url(#${this.rectClipId})`) |
||||||
|
.attr('fill', this.getSerieColor(serieIdx)) |
||||||
|
.style('opacity', CROSSHAIR_BACKGROUND_OPACITY) |
||||||
|
.style('pointer-events', 'none'); |
||||||
|
|
||||||
|
this.crosshair |
||||||
|
.append('circle') |
||||||
|
.attr('class', `crosshair-circle-${serieIdx}`) |
||||||
|
.attr('clip-path', `url(#${this.rectClipId})`) |
||||||
|
.attr('fill', this.getSerieColor(serieIdx)) |
||||||
|
.attr('r', CROSSHAIR_CIRCLE_RADIUS) |
||||||
|
.style('pointer-events', 'none'); |
||||||
|
} |
||||||
|
|
||||||
|
public renderSharedCrosshair(values: { x?: number, y?: number }): void { |
||||||
|
this.onMouseOver(); // TODO: refactor to use it once
|
||||||
|
const eventX = this.xScale(values.x); |
||||||
|
const eventY = this.yScale(values.y); |
||||||
|
this.moveCrosshairLine(eventX, eventY); |
||||||
|
const datapoints = this.findAndHighlightDatapoints(values.x, values.y); |
||||||
|
|
||||||
|
if(this.options.eventsCallbacks === undefined || this.options.eventsCallbacks.sharedCrosshairMove === undefined) { |
||||||
|
console.log('Shared crosshair move, but there is no callback'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
this.options.eventsCallbacks.sharedCrosshairMove({ |
||||||
|
datapoints: datapoints, |
||||||
|
eventX, eventY |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public hideSharedCrosshair(): void { |
||||||
|
this.crosshair.style('display', 'none'); |
||||||
|
} |
||||||
|
|
||||||
|
moveCrosshairLine(xPosition: number, yPosition: number): void { |
||||||
|
switch(this.options.crosshair.orientation) { |
||||||
|
case CrosshairOrientation.VERTICAL: |
||||||
|
this.crosshair.select('#crosshair-line-x') |
||||||
|
.attr('x1', xPosition) |
||||||
|
.attr('x2', xPosition); |
||||||
|
return; |
||||||
|
case CrosshairOrientation.HORIZONTAL: |
||||||
|
this.crosshair.select('#crosshair-line-y') |
||||||
|
.attr('y1', yPosition) |
||||||
|
.attr('y2', yPosition); |
||||||
|
return; |
||||||
|
case CrosshairOrientation.BOTH: |
||||||
|
this.crosshair.select('#crosshair-line-x') |
||||||
|
.attr('x1', xPosition) |
||||||
|
.attr('x2', xPosition); |
||||||
|
this.crosshair.select('#crosshair-line-y') |
||||||
|
.attr('y1', yPosition) |
||||||
|
.attr('y2', yPosition); |
||||||
|
return; |
||||||
|
default: |
||||||
|
throw new Error(`Unknown type of crosshair orientaion: ${this.options.crosshair.orientation}`); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
moveCrosshairCircle(xPosition: number, yPosition: number, serieIdx: number): void { |
||||||
|
this.crosshair.selectAll(`.crosshair-circle-${serieIdx}`) |
||||||
|
.attr('cx', xPosition) |
||||||
|
.attr('cy', yPosition) |
||||||
|
.style('display', null); |
||||||
|
} |
||||||
|
|
||||||
|
hideCrosshairCircle(serieIdx: number): void { |
||||||
|
// hide circle for singe serie
|
||||||
|
this.crosshair.selectAll(`.crosshair-circle-${serieIdx}`) |
||||||
|
.style('display', 'none'); |
||||||
|
} |
||||||
|
|
||||||
|
getClosestDatapoint(serie: LineTimeSerie, xValue: number, yValue: number): [number, number] { |
||||||
|
// get closest datapoint to the "xValue"/"yValue" in the "serie"
|
||||||
|
const datapoints = serie.datapoints; |
||||||
|
const closestIdx = this.getClosestIndex(datapoints, xValue, yValue); |
||||||
|
const datapoint = serie.datapoints[closestIdx]; |
||||||
|
return datapoint; |
||||||
|
} |
||||||
|
|
||||||
|
getClosestIndex(datapoints: [number, number][], xValue: number, yValue: number): number { |
||||||
|
let columnIdx; // 0 for y value, 1 for x value
|
||||||
|
let value; // xValue ot y Value
|
||||||
|
switch(this.options.crosshair.orientation) { |
||||||
|
case CrosshairOrientation.HORIZONTAL: |
||||||
|
columnIdx = 0; |
||||||
|
value = yValue; |
||||||
|
break; |
||||||
|
case CrosshairOrientation.VERTICAL: |
||||||
|
columnIdx = 1; |
||||||
|
value = xValue; |
||||||
|
break; |
||||||
|
case CrosshairOrientation.BOTH: |
||||||
|
// TODO: maybe use voronoi
|
||||||
|
columnIdx = 0; |
||||||
|
value = yValue; |
||||||
|
default: |
||||||
|
throw new Error(`Unknown type of crosshair orientaion: ${this.options.crosshair.orientation}`); |
||||||
|
} |
||||||
|
// TODO: d3.bisect is not the best way. Use binary search
|
||||||
|
const bisectIndex = this.d3.bisector((d: [number, number]) => d[columnIdx]).left; |
||||||
|
let closestIdx = bisectIndex(datapoints, value); |
||||||
|
// TODO: refactor corner cases
|
||||||
|
if(closestIdx < 0) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
if(closestIdx >= datapoints.length) { |
||||||
|
return datapoints.length - 1; |
||||||
|
} |
||||||
|
// TODO: do we realy need it? Binary search should fix it
|
||||||
|
if( |
||||||
|
closestIdx > 0 && |
||||||
|
Math.abs(value - datapoints[closestIdx - 1][columnIdx]) < |
||||||
|
Math.abs(value - datapoints[closestIdx][columnIdx]) |
||||||
|
) { |
||||||
|
closestIdx -= 1; |
||||||
|
} |
||||||
|
return closestIdx; |
||||||
|
} |
||||||
|
|
||||||
|
getValueInterval(columnIdx: number): number | undefined { |
||||||
|
// columnIdx: 0 for y, 1 for x
|
||||||
|
// inverval: x/y value interval between data points
|
||||||
|
// TODO: move it to base/state instead of timeInterval
|
||||||
|
const intervals = _.map(this.series, serie => { |
||||||
|
if(serie.datapoints.length < 2) { |
||||||
|
return undefined; |
||||||
|
} |
||||||
|
const start = _.head(serie.datapoints)[columnIdx]; |
||||||
|
const end = _.last(serie.datapoints)[columnIdx]; |
||||||
|
const range = Math.abs(end - start); |
||||||
|
const interval = range / (serie.datapoints.length - 1); |
||||||
|
return interval; |
||||||
|
}); |
||||||
|
return _.max(intervals); |
||||||
|
} |
||||||
|
|
||||||
|
onMouseMove(): void { |
||||||
|
const eventX = this.d3.mouse(this.chartContainer.node())[0]; |
||||||
|
const eventY = this.d3.mouse(this.chartContainer.node())[1]; |
||||||
|
const xValue = this.xScale.invert(eventX); // mouse x position in xScale
|
||||||
|
const yValue = this.yScale.invert(eventY); |
||||||
|
// TODO: isOutOfChart is a hack, use clip path correctly
|
||||||
|
if(this.isOutOfChart() === true) { |
||||||
|
this.crosshair.style('display', 'none'); |
||||||
|
return; |
||||||
|
} |
||||||
|
this.moveCrosshairLine(eventX, eventY); |
||||||
|
|
||||||
|
const datapoints = this.findAndHighlightDatapoints(xValue, yValue); |
||||||
|
|
||||||
|
if(this.options.eventsCallbacks === undefined || this.options.eventsCallbacks.mouseMove === undefined) { |
||||||
|
console.log('Mouse move, but there is no callback'); |
||||||
|
return; |
||||||
|
} |
||||||
|
// TDOO: is shift key pressed
|
||||||
|
// TODO: need to refactor this object
|
||||||
|
this.options.eventsCallbacks.mouseMove({ |
||||||
|
x: this.d3.event.pageX, |
||||||
|
y: this.d3.event.pageY, |
||||||
|
xVal: xValue, |
||||||
|
yVal: yValue, |
||||||
|
series: datapoints, |
||||||
|
chartX: eventX, |
||||||
|
chartWidth: this.width |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
findAndHighlightDatapoints(xValue: number, yValue: number): { value: [number, number], color: string, label: string }[] { |
||||||
|
if(this.series === undefined || this.series.length === 0) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
let points = []; // datapoints in each metric that is closest to xValue/yValue position
|
||||||
|
this.series.forEach((serie: LineTimeSerie, serieIdx: number) => { |
||||||
|
if( |
||||||
|
serie.visible === false || |
||||||
|
_.includes(this.seriesTargetsWithBounds, serie.target) |
||||||
|
) { |
||||||
|
this.hideCrosshairCircle(serieIdx); |
||||||
|
return; |
||||||
|
} |
||||||
|
const closestDatapoint = this.getClosestDatapoint(serie, xValue, yValue); |
||||||
|
if(closestDatapoint === undefined || this.isOutOfRange(closestDatapoint, xValue, yValue, serie.useOutOfRange)) { |
||||||
|
this.hideCrosshairCircle(serieIdx); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const yPosition = this.yScale(closestDatapoint[0]); |
||||||
|
const xPosition = this.xScale(closestDatapoint[1]); |
||||||
|
this.moveCrosshairCircle(xPosition, yPosition, serieIdx); |
||||||
|
|
||||||
|
points.push({ |
||||||
|
value: closestDatapoint, |
||||||
|
color: this.getSerieColor(serieIdx), |
||||||
|
label: serie.alias || serie.target |
||||||
|
}); |
||||||
|
}); |
||||||
|
return points; |
||||||
|
} |
||||||
|
|
||||||
|
isOutOfRange(closestDatapoint: [number, number], xValue: number, yValue: number, useOutOfRange = true): boolean { |
||||||
|
// find is mouse position more than xRange/yRange from closest point
|
||||||
|
// TODO: refactor getValueInterval to remove this!
|
||||||
|
if(useOutOfRange === false) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
let columnIdx; // 0 for y value, 1 for x value
|
||||||
|
let value; // xValue ot y Value
|
||||||
|
switch(this.options.crosshair.orientation) { |
||||||
|
case CrosshairOrientation.HORIZONTAL: |
||||||
|
columnIdx = 0; |
||||||
|
value = yValue; |
||||||
|
break; |
||||||
|
case CrosshairOrientation.VERTICAL: |
||||||
|
columnIdx = 1; |
||||||
|
value = xValue; |
||||||
|
break; |
||||||
|
case CrosshairOrientation.BOTH: |
||||||
|
// TODO: maybe use voronoi
|
||||||
|
columnIdx = 0; |
||||||
|
value = yValue; |
||||||
|
default: |
||||||
|
throw new Error(`Unknown type of crosshair orientaion: ${this.options.crosshair.orientation}`); |
||||||
|
} |
||||||
|
const range = Math.abs(closestDatapoint[columnIdx] - value); |
||||||
|
const interval = this.getValueInterval(columnIdx); // interval between points
|
||||||
|
// do not move crosshair circles, it mouse to far from closest point
|
||||||
|
return interval === undefined || range > interval / 2; |
||||||
|
} |
||||||
|
|
||||||
|
onMouseOver(): void { |
||||||
|
this.crosshair.style('display', null); |
||||||
|
this.crosshair.selectAll('.crosshair-circle') |
||||||
|
.style('display', null); |
||||||
|
} |
||||||
|
|
||||||
|
onMouseOut(): void { |
||||||
|
if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.mouseOut !== undefined) { |
||||||
|
this.options.eventsCallbacks.mouseOut(); |
||||||
|
} |
||||||
|
this.crosshair.style('display', 'none'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// it is used with Vue.component, e.g.: Vue.component('chartwerk-line-chart', VueChartwerkLineChartObject)
|
||||||
|
export const VueChartwerkLineChartObject = { |
||||||
|
// alternative to `template: '<div class="chartwerk-line-chart" :id="id" />'`
|
||||||
|
render(createElement) { |
||||||
|
return createElement( |
||||||
|
'div', |
||||||
|
{ |
||||||
|
class: { 'chartwerk-line-chart': true }, |
||||||
|
attrs: { id: this.id } |
||||||
|
} |
||||||
|
); |
||||||
|
}, |
||||||
|
mixins: [VueChartwerkPodMixin], |
||||||
|
methods: { |
||||||
|
render() { |
||||||
|
if(this.pod === undefined) {
|
||||||
|
this.pod = new ChartwerkLineChart(document.getElementById(this.id), this.series, this.options); |
||||||
|
this.pod.render(); |
||||||
|
} else { |
||||||
|
this.pod.updateData(this.series, this.options); |
||||||
|
} |
||||||
|
}, |
||||||
|
renderSharedCrosshair(values) { |
||||||
|
this.pod.renderSharedCrosshair(values); |
||||||
|
}, |
||||||
|
hideSharedCrosshair() { |
||||||
|
this.pod.hideSharedCrosshair(); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
export { LineTimeSerie, LineOptions, Mode, TickOrientation, TimeFormat }; |
@ -0,0 +1,16 @@ |
|||||||
|
import { TimeSerie, Options } from '@chartwerk/core'; |
||||||
|
|
||||||
|
type LineTimeSerieParams = { |
||||||
|
confidence: number, |
||||||
|
mode: Mode, |
||||||
|
maxLength: number, |
||||||
|
renderDots: boolean, |
||||||
|
renderLines: boolean, // TODO: refactor same as scatter-pod
|
||||||
|
useOutOfRange: boolean, // It's temporary hack. Need to refactor getValueInterval() method
|
||||||
|
} |
||||||
|
export enum Mode { |
||||||
|
STANDARD = 'Standard', |
||||||
|
CHARGE = 'Charge' |
||||||
|
} |
||||||
|
export type LineTimeSerie = TimeSerie & Partial<LineTimeSerieParams>; |
||||||
|
export type LineOptions = Options; |
@ -0,0 +1,22 @@ |
|||||||
|
{ |
||||||
|
"compilerOptions": { |
||||||
|
"target": "es5", |
||||||
|
"rootDir": "./src", |
||||||
|
"module": "esnext", |
||||||
|
"moduleResolution": "node", |
||||||
|
"declaration": true, |
||||||
|
"declarationDir": "dist", |
||||||
|
"allowSyntheticDefaultImports": true, |
||||||
|
"inlineSourceMap": false, |
||||||
|
"sourceMap": true, |
||||||
|
"noEmitOnError": false, |
||||||
|
"emitDecoratorMetadata": false, |
||||||
|
"experimentalDecorators": true, |
||||||
|
"noImplicitReturns": true, |
||||||
|
"noImplicitThis": false, |
||||||
|
"noImplicitUseStrict": false, |
||||||
|
"noImplicitAny": false, |
||||||
|
"noUnusedLocals": false, |
||||||
|
"baseUrl": "./src" |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue