Browse Source
sync_71c2bc720b5483a15616ffe66c13ae57ec9d08eb See merge request chartwerk/bar-pod!1merge-requests/2/merge
Alexander Velikiy
3 years ago
14 changed files with 5454 additions and 0 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,35 @@ |
|||||||
|
<!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"> |
||||||
|
var pod = new ChartwerkBarPod( |
||||||
|
document.getElementById('chart'), |
||||||
|
[ |
||||||
|
{ target: 'test11', datapoints: [[15, 100], [20, 110], [10, 300]], matchedKey: 'm-1', color: 'red' }, |
||||||
|
{ target: 'test12', datapoints: [[10, 100], [20, 200], [10, 300]], matchedKey: 'm-1', color: 'green' }, |
||||||
|
{ target: 'test21', datapoints: [[10, 130], [26, 230], [15, 330]], matchedKey: 'm-2', color: 'yellow'}, |
||||||
|
{ target: 'test22', datapoints: [[10, 130], [27, 230], [10, 330]], matchedKey: 'm-2', color: 'blue' }, |
||||||
|
], |
||||||
|
{ |
||||||
|
usePanning: false, |
||||||
|
axis: { |
||||||
|
x: { format: 'custom', invert: false, valueFormatter: (value) => { return 'L' + value; } }, |
||||||
|
y: { invert: false, range: [0, 30], valueFormatter: (value) => { return value + '%'; } } |
||||||
|
}, |
||||||
|
stacked: true, |
||||||
|
matching: true, |
||||||
|
maxBarWidth: 20 |
||||||
|
} |
||||||
|
); |
||||||
|
pod.render(); |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,73 @@ |
|||||||
|
import { ChartwerkPod, TickOrientation, TimeFormat, AxisFormat } from '@chartwerk/core'; |
||||||
|
import { BarTimeSerie, BarOptions, RowValues } from './types'; |
||||||
|
import * as d3 from 'd3'; |
||||||
|
export declare class ChartwerkBarPod extends ChartwerkPod<BarTimeSerie, BarOptions> { |
||||||
|
metricsContainer: any; |
||||||
|
constructor(el: HTMLElement, _series?: BarTimeSerie[], _options?: BarOptions); |
||||||
|
protected renderMetrics(): void; |
||||||
|
renderSerie(data: any): void; |
||||||
|
getBarOpacity(rowValues: RowValues): number; |
||||||
|
mergeMacthedSeriesAndSort(matchedSeries: any[]): any; |
||||||
|
get seriesUniqKeys(): string[]; |
||||||
|
get seriesForMatching(): BarTimeSerie[][]; |
||||||
|
getZippedDataForRender(series: BarTimeSerie[]): RowValues[]; |
||||||
|
renderSharedCrosshair(timestamp: number): void; |
||||||
|
hideSharedCrosshair(): void; |
||||||
|
onMouseMove(): void; |
||||||
|
getSeriesPointFromMousePosition(eventX: number): any[] | undefined; |
||||||
|
getBarColor(serie: any): any; |
||||||
|
onMouseOver(): void; |
||||||
|
onMouseOut(): void; |
||||||
|
contextMenu(): void; |
||||||
|
get barWidth(): number; |
||||||
|
updateBarWidthWithBorders(width: number): number; |
||||||
|
getBarHeight(value: number): number; |
||||||
|
getBarPositionX(key: number, idx: number): number; |
||||||
|
getBarPositionY(val: number, idx: number, values: number[]): number; |
||||||
|
get yScale(): d3.ScaleLinear<number, number>; |
||||||
|
get maxValue(): number | undefined; |
||||||
|
} |
||||||
|
export declare const VueChartwerkBarChartObject: { |
||||||
|
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; |
||||||
|
methods: { |
||||||
|
render(): void; |
||||||
|
renderChart(): void; |
||||||
|
appendEvents(): void; |
||||||
|
zoomIn(range: any): void; |
||||||
|
zoomOut(center: any): void; |
||||||
|
mouseMove(evt: any): void; |
||||||
|
mouseOut(): void; |
||||||
|
onLegendClick(idx: any): void; |
||||||
|
panningEnd(range: any): void; |
||||||
|
panning(range: any): void; |
||||||
|
contextMenu(evt: any): void; |
||||||
|
}; |
||||||
|
}[]; |
||||||
|
methods: { |
||||||
|
render(): void; |
||||||
|
}; |
||||||
|
}; |
||||||
|
export { BarTimeSerie, BarOptions, TickOrientation, TimeFormat, AxisFormat }; |
File diff suppressed because one or more lines are too long
@ -0,0 +1,22 @@ |
|||||||
|
import { TimeSerie, Options } from '@chartwerk/core'; |
||||||
|
export declare type BarSerieParams = { |
||||||
|
matchedKey: string; |
||||||
|
colorFormatter: (serie: BarTimeSerie) => string; |
||||||
|
}; |
||||||
|
export declare type BarTimeSerie = TimeSerie & Partial<BarSerieParams>; |
||||||
|
export declare type BarOptionsParams = { |
||||||
|
renderBarLabels: boolean; |
||||||
|
stacked: boolean; |
||||||
|
barWidth: number; |
||||||
|
maxBarWidth: number; |
||||||
|
minBarWidth: number; |
||||||
|
matching: boolean; |
||||||
|
opacityFormatter: (data: RowValues) => number; |
||||||
|
}; |
||||||
|
export declare type BarOptions = Options & Partial<BarOptionsParams>; |
||||||
|
export declare type RowValues = { |
||||||
|
key: number; |
||||||
|
values: number[]; |
||||||
|
additionalValues: (null | number)[]; |
||||||
|
colors: string[]; |
||||||
|
}; |
@ -0,0 +1,32 @@ |
|||||||
|
{ |
||||||
|
"name": "@chartwerk/bar-chart", |
||||||
|
"version": "0.2.4", |
||||||
|
"description": "Chartwerk bar 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/bar-chart.git" |
||||||
|
}, |
||||||
|
"author": "CorpGlory", |
||||||
|
"license": "Apache-2.0", |
||||||
|
"dependencies": { |
||||||
|
"@chartwerk/core": "github:chartwerk/core#880b8e064f6df9430df337c6fafb03cc658cb24f" |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"@types/d3": "^5.7.2", |
||||||
|
"@types/lodash": "^4.14.149", |
||||||
|
"css-loader": "^3.4.2", |
||||||
|
"d3": "^5.15.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,367 @@ |
|||||||
|
import { ChartwerkPod, VueChartwerkPodMixin, TickOrientation, TimeFormat, AxisFormat } from '@chartwerk/core'; |
||||||
|
|
||||||
|
import { BarTimeSerie, BarOptions, RowValues } from './types'; |
||||||
|
|
||||||
|
import * as d3 from 'd3'; |
||||||
|
import * as _ from 'lodash'; |
||||||
|
|
||||||
|
|
||||||
|
const DEFAULT_BAR_OPTIONS: BarOptions = { |
||||||
|
renderBarLabels: false, |
||||||
|
stacked: false, |
||||||
|
matching: false |
||||||
|
} |
||||||
|
|
||||||
|
export class ChartwerkBarPod extends ChartwerkPod<BarTimeSerie, BarOptions> { |
||||||
|
metricsContainer: any; |
||||||
|
|
||||||
|
constructor(el: HTMLElement, _series: BarTimeSerie[] = [], _options: BarOptions = {}) { |
||||||
|
super(d3, el, _series, _options); |
||||||
|
_.defaults(this.options, DEFAULT_BAR_OPTIONS); |
||||||
|
} |
||||||
|
|
||||||
|
protected renderMetrics(): void { |
||||||
|
if(this.series.length === 0 || this.series[0].datapoints.length === 0) { |
||||||
|
this.renderNoDataPointsMessage(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// container for clip path
|
||||||
|
const clipContatiner = this.chartContainer |
||||||
|
.append('g') |
||||||
|
.attr('clip-path', `url(#${this.rectClipId})`) |
||||||
|
.attr('class', 'metrics-container'); |
||||||
|
// container for panning
|
||||||
|
this.metricsContainer = clipContatiner |
||||||
|
.append('g') |
||||||
|
.attr('class', ' metrics-rect'); |
||||||
|
|
||||||
|
if(this.options.matching === false || this.seriesUniqKeys.length === 0) { |
||||||
|
const zippedData = this.getZippedDataForRender(this.visibleSeries); |
||||||
|
this.renderSerie(zippedData); |
||||||
|
return; |
||||||
|
} |
||||||
|
const matchedSeries = this.seriesForMatching.map((series: BarTimeSerie[], idx: number) => { |
||||||
|
return this.getZippedDataForRender(series); |
||||||
|
}); |
||||||
|
const concatedSeries = this.mergeMacthedSeriesAndSort(matchedSeries); |
||||||
|
this.renderSerie(concatedSeries); |
||||||
|
} |
||||||
|
|
||||||
|
renderSerie(data: any): void { |
||||||
|
this.metricsContainer.selectAll(`.rects-container`) |
||||||
|
.data(data) |
||||||
|
.enter().append('g') |
||||||
|
.attr('class', 'rects-container') |
||||||
|
.attr('clip-path', `url(#${this.rectClipId})`) |
||||||
|
.each((d: RowValues, i: number, nodes: any) => { |
||||||
|
const container = d3.select(nodes[i]); |
||||||
|
container.selectAll('rect') |
||||||
|
.data(d.values) |
||||||
|
.enter().append('rect') |
||||||
|
.style('fill', (val, i) => d.colors[i]) |
||||||
|
.attr('opacity', () => this.getBarOpacity(d)) |
||||||
|
.attr('x', (val: number, idx: number) => { |
||||||
|
return this.getBarPositionX(d.key, idx); |
||||||
|
}) |
||||||
|
.attr('y', (val: number, idx: number) => { |
||||||
|
return this.getBarPositionY(val, idx, d.values); |
||||||
|
}) |
||||||
|
.attr('width', this.barWidth) |
||||||
|
.attr('height', (val: number) => this.getBarHeight(val)) |
||||||
|
.on('contextmenu', this.contextMenu.bind(this)); |
||||||
|
}); |
||||||
|
|
||||||
|
// TODO: render bar labels
|
||||||
|
} |
||||||
|
|
||||||
|
getBarOpacity(rowValues: RowValues): number { |
||||||
|
if(this.options.opacityFormatter === undefined) { |
||||||
|
return 1; |
||||||
|
} |
||||||
|
return this.options.opacityFormatter(rowValues); |
||||||
|
} |
||||||
|
|
||||||
|
mergeMacthedSeriesAndSort(matchedSeries: any[]) { |
||||||
|
// TODO: refactor
|
||||||
|
if(matchedSeries.length === 0) { |
||||||
|
throw new Error('Cant mergeMacthedSeriesAndSort'); |
||||||
|
} |
||||||
|
if(matchedSeries.length === 1) { |
||||||
|
return matchedSeries[0]; |
||||||
|
} |
||||||
|
let unionSeries = _.clone(matchedSeries[0]); |
||||||
|
for(let i = 1; i < matchedSeries.length; i++){ |
||||||
|
unionSeries = [...unionSeries, ...matchedSeries[i]]; |
||||||
|
} |
||||||
|
const sortedSeries = _.sortBy(unionSeries, ['key']); |
||||||
|
return sortedSeries; |
||||||
|
} |
||||||
|
|
||||||
|
get seriesUniqKeys(): string[] { |
||||||
|
if(this.visibleSeries.length === 0) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
const keys = this.visibleSeries.map(serie => serie.matchedKey); |
||||||
|
const uniqKeys = _.uniq(keys); |
||||||
|
const filteredKeys = _.filter(uniqKeys, key => key !== undefined); |
||||||
|
return filteredKeys; |
||||||
|
} |
||||||
|
|
||||||
|
get seriesForMatching(): BarTimeSerie[][] { |
||||||
|
if(this.seriesUniqKeys.length === 0) { |
||||||
|
return [this.visibleSeries]; |
||||||
|
} |
||||||
|
const seriesList = this.seriesUniqKeys.map(key => { |
||||||
|
const seriesWithKey = _.filter(this.visibleSeries, serie => serie.matchedKey === key); |
||||||
|
return seriesWithKey; |
||||||
|
}); |
||||||
|
return seriesList; |
||||||
|
} |
||||||
|
|
||||||
|
getZippedDataForRender(series: BarTimeSerie[]): RowValues[] { |
||||||
|
if(series.length === 0) { |
||||||
|
throw new Error('There is no visible series'); |
||||||
|
} |
||||||
|
const keysColumn = _.map(series[0].datapoints, row => row[1]); |
||||||
|
const valuesColumns = _.map(series, serie => _.map(serie.datapoints, row => row[0])); |
||||||
|
// @ts-ignore
|
||||||
|
const additionalValuesColumns = _.map(series, serie => _.map(serie.datapoints, row => row[2] !== undefined ? row[2] : null)); |
||||||
|
const zippedAdditionalValuesColumn = _.zip(...additionalValuesColumns); |
||||||
|
const zippedValuesColumn = _.zip(...valuesColumns); |
||||||
|
const colors = _.map(series, serie => this.getBarColor(serie)); |
||||||
|
const zippedData = _.zip(keysColumn, zippedValuesColumn, zippedAdditionalValuesColumn); |
||||||
|
const data = _.map(zippedData, row => { return { key: row[0], values: row[1], additionalValues: row[2], colors } }); |
||||||
|
return data; |
||||||
|
} |
||||||
|
|
||||||
|
public renderSharedCrosshair(timestamp: number): void { |
||||||
|
this.crosshair.style('display', null); |
||||||
|
|
||||||
|
const x = this.xScale(timestamp); |
||||||
|
this.crosshair.select('#crosshair-line-x') |
||||||
|
.attr('x1', x) |
||||||
|
.attr('x2', x); |
||||||
|
} |
||||||
|
|
||||||
|
public hideSharedCrosshair(): void { |
||||||
|
this.crosshair.style('display', 'none'); |
||||||
|
} |
||||||
|
|
||||||
|
onMouseMove(): void { |
||||||
|
// TODO: mouse move work bad with matching
|
||||||
|
const event = this.d3.mouse(this.chartContainer.node()); |
||||||
|
const eventX = event[0]; |
||||||
|
if(this.isOutOfChart() === true) { |
||||||
|
this.crosshair.style('display', 'none'); |
||||||
|
return; |
||||||
|
} |
||||||
|
this.crosshair.select('#crosshair-line-x') |
||||||
|
.attr('x1', eventX) |
||||||
|
.attr('x2', eventX); |
||||||
|
|
||||||
|
const series = this.getSeriesPointFromMousePosition(eventX); |
||||||
|
|
||||||
|
if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.mouseMove !== undefined) { |
||||||
|
this.options.eventsCallbacks.mouseMove({ |
||||||
|
x: this.d3.event.pageX, |
||||||
|
y: this.d3.event.pageY, |
||||||
|
time: this.xScale.invert(eventX), |
||||||
|
series, |
||||||
|
chartX: eventX, |
||||||
|
chartWidth: this.width |
||||||
|
}); |
||||||
|
} else { |
||||||
|
console.log('mouse move, but there is no callback'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: not any[]
|
||||||
|
getSeriesPointFromMousePosition(eventX: number): any[] | undefined { |
||||||
|
if(this.series === undefined || this.series.length === 0) { |
||||||
|
return undefined; |
||||||
|
} |
||||||
|
|
||||||
|
const bisectDate = this.d3.bisector((d: [number, number]) => d[1]).left; |
||||||
|
const mouseDate = this.xScale.invert(eventX); |
||||||
|
|
||||||
|
let idx = bisectDate(this.series[0].datapoints, mouseDate) - 1; |
||||||
|
|
||||||
|
const series: any[] = []; |
||||||
|
for(let i = 0; i < this.series.length; i++) { |
||||||
|
if(this.series[i].visible === false || this.series[i].datapoints.length < idx + 1) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
series.push({ |
||||||
|
value: this.series[i].datapoints[idx][0], |
||||||
|
xval: this.series[i].datapoints[idx][1], |
||||||
|
color: this.getBarColor(this.series[i]), |
||||||
|
label: this.series[i].alias || this.series[i].target |
||||||
|
}); |
||||||
|
} |
||||||
|
return series; |
||||||
|
} |
||||||
|
|
||||||
|
getBarColor(serie: any) { |
||||||
|
if(serie.color === undefined) { |
||||||
|
return this.getSerieColor(0); |
||||||
|
} |
||||||
|
return serie.color; |
||||||
|
} |
||||||
|
|
||||||
|
onMouseOver(): void { |
||||||
|
this.crosshair.style('display', null); |
||||||
|
this.crosshair.raise(); |
||||||
|
} |
||||||
|
|
||||||
|
onMouseOut(): void { |
||||||
|
if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.mouseOut !== undefined) { |
||||||
|
this.options.eventsCallbacks.mouseOut(); |
||||||
|
} else { |
||||||
|
console.log('mouse out, but there is no callback'); |
||||||
|
} |
||||||
|
this.crosshair.style('display', 'none'); |
||||||
|
} |
||||||
|
|
||||||
|
contextMenu(): void { |
||||||
|
// maybe it is not the best name, but i took it from d3.
|
||||||
|
this.d3.event.preventDefault(); // do not open browser's context menu.
|
||||||
|
|
||||||
|
const event = this.d3.mouse(this.chartContainer.node()); |
||||||
|
const eventX = event[0]; |
||||||
|
const series = this.getSeriesPointFromMousePosition(eventX); |
||||||
|
|
||||||
|
if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.contextMenu !== undefined) { |
||||||
|
this.options.eventsCallbacks.contextMenu({ |
||||||
|
x: this.d3.event.pageX, |
||||||
|
y: this.d3.event.pageY, |
||||||
|
time: this.xScale.invert(eventX), |
||||||
|
series, |
||||||
|
chartX: eventX |
||||||
|
}); |
||||||
|
} else { |
||||||
|
console.log('contextmenu, but there is no callback'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
get barWidth(): number { |
||||||
|
// TODO: here we use first value + timeInterval as bar width. It is not a good idea
|
||||||
|
const xAxisStartValue = _.first(this.series[0].datapoints)[1]; |
||||||
|
let width = this.xScale(xAxisStartValue + this.timeInterval) / 2; |
||||||
|
if(this.options.barWidth !== undefined) { |
||||||
|
// barWidth now has axis-x dimension
|
||||||
|
width = this.xScale(this.minValueX + this.options.barWidth); |
||||||
|
} |
||||||
|
let rectColumns = this.visibleSeries.length; |
||||||
|
if(this.options.stacked === true) { |
||||||
|
rectColumns = 1; |
||||||
|
} |
||||||
|
return this.updateBarWidthWithBorders(width / rectColumns); |
||||||
|
} |
||||||
|
|
||||||
|
updateBarWidthWithBorders(width: number): number { |
||||||
|
let barWidth = width; |
||||||
|
if(this.options.minBarWidth !== undefined) { |
||||||
|
barWidth = Math.max(barWidth, this.options.minBarWidth); |
||||||
|
} |
||||||
|
if(this.options.maxBarWidth !== undefined) { |
||||||
|
barWidth = Math.min(barWidth, this.options.maxBarWidth); |
||||||
|
} |
||||||
|
return barWidth; |
||||||
|
} |
||||||
|
|
||||||
|
getBarHeight(value: number): number { |
||||||
|
// TODO: Property 'sign' does not exist on type 'Math'
|
||||||
|
// @ts-ignore
|
||||||
|
const height = Math.sign(value) * (this.yScale(0) - this.yScale(value)); |
||||||
|
return height; |
||||||
|
} |
||||||
|
|
||||||
|
getBarPositionX(key: number, idx: number): number { |
||||||
|
let xPosition: number = this.xScale(key); |
||||||
|
if(this.options.stacked === false) { |
||||||
|
xPosition += idx * this.barWidth; |
||||||
|
} |
||||||
|
return xPosition; |
||||||
|
} |
||||||
|
|
||||||
|
getBarPositionY(val: number, idx: number, values: number[]): number { |
||||||
|
let yPosition: number = this.yScale(Math.max(val, 0)); |
||||||
|
if(this.options.stacked === true) { |
||||||
|
const previousBarsHeight = _.sum( |
||||||
|
_.map(_.range(idx), i => this.getBarHeight(values[i])) |
||||||
|
); |
||||||
|
yPosition -= previousBarsHeight; |
||||||
|
} |
||||||
|
return yPosition; |
||||||
|
} |
||||||
|
|
||||||
|
get yScale(): d3.ScaleLinear<number, number> { |
||||||
|
if( |
||||||
|
this.minValue === undefined || |
||||||
|
this.maxValue === undefined |
||||||
|
) { |
||||||
|
return this.d3.scaleLinear() |
||||||
|
.domain([1, 0]) |
||||||
|
.range([0, this.height]); |
||||||
|
} |
||||||
|
return this.d3.scaleLinear() |
||||||
|
.domain([this.maxValue, Math.min(this.minValue, 0)]) |
||||||
|
.range([0, this.height]); |
||||||
|
} |
||||||
|
|
||||||
|
get maxValue(): number | undefined { |
||||||
|
if(this.series === undefined || this.series.length === 0 || this.series[0].datapoints.length === 0) { |
||||||
|
return undefined; |
||||||
|
} |
||||||
|
if(this.options.axis.y !== undefined && this.options.axis.y.range !== undefined) { |
||||||
|
return _.max(this.options.axis.y.range); |
||||||
|
} |
||||||
|
let maxValue: number; |
||||||
|
if(this.options.stacked === true) { |
||||||
|
if(this.options.matching === true && this.seriesUniqKeys.length > 0) { |
||||||
|
const maxValues = this.seriesForMatching.map(series => { |
||||||
|
const valuesColumns = _.map(series, serie => _.map(serie.datapoints, row => row[0])); |
||||||
|
const zippedValuesColumn = _.zip(...valuesColumns); |
||||||
|
return maxValue = _.max(_.map(zippedValuesColumn, row => _.sum(row))); |
||||||
|
}); |
||||||
|
return _.max(maxValues); |
||||||
|
} else { |
||||||
|
const valuesColumns = _.map(this.visibleSeries, serie => _.map(serie.datapoints, row => row[0])); |
||||||
|
const zippedValuesColumn = _.zip(...valuesColumns); |
||||||
|
maxValue = _.max(_.map(zippedValuesColumn, row => _.sum(row)));
|
||||||
|
} |
||||||
|
} else { |
||||||
|
maxValue = _.max( |
||||||
|
this.visibleSeries.map( |
||||||
|
serie => _.maxBy<number[]>(serie.datapoints, dp => dp[0])[0] |
||||||
|
) |
||||||
|
); |
||||||
|
} |
||||||
|
return Math.max(maxValue, 0); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// it is used with Vue.component, e.g.: Vue.component('chartwerk-bar-chart', VueChartwerkBarChartObject)
|
||||||
|
export const VueChartwerkBarChartObject = { |
||||||
|
// alternative to `template: '<div class="chartwerk-bar-chart" :id="id" />'`
|
||||||
|
render(createElement) { |
||||||
|
return createElement( |
||||||
|
'div', |
||||||
|
{ |
||||||
|
class: { 'chartwerk-bar-chart': true }, |
||||||
|
attrs: { id: this.id } |
||||||
|
} |
||||||
|
) |
||||||
|
}, |
||||||
|
mixins: [VueChartwerkPodMixin], |
||||||
|
methods: { |
||||||
|
render() { |
||||||
|
const pod = new ChartwerkBarPod(document.getElementById(this.id), this.series, this.options); |
||||||
|
pod.render(); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
export { BarTimeSerie, BarOptions, TickOrientation, TimeFormat, AxisFormat }; |
@ -0,0 +1,23 @@ |
|||||||
|
import { TimeSerie, Options } from '@chartwerk/core'; |
||||||
|
|
||||||
|
export type BarSerieParams = { |
||||||
|
matchedKey: string; |
||||||
|
colorFormatter: (serie: BarTimeSerie) => string; |
||||||
|
} |
||||||
|
export type BarTimeSerie = TimeSerie & Partial<BarSerieParams>; |
||||||
|
export type BarOptionsParams = { |
||||||
|
renderBarLabels: boolean; |
||||||
|
stacked: boolean; |
||||||
|
barWidth: number; // width in x axis unit
|
||||||
|
maxBarWidth: number; // in px
|
||||||
|
minBarWidth: number; // in px
|
||||||
|
matching: boolean; |
||||||
|
opacityFormatter: (data: RowValues) => number; |
||||||
|
} |
||||||
|
export type BarOptions = Options & Partial<BarOptionsParams>; |
||||||
|
export type RowValues = { |
||||||
|
key: number, |
||||||
|
values: number[], |
||||||
|
additionalValues: (null | number)[], // values in datapoints third column
|
||||||
|
colors: string[] |
||||||
|
} |
@ -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