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