@@ -51,7 +51,7 @@ let bar_composite_chart = new Chart ({ | |||
height: 180, | |||
colors: ['orange'], | |||
isNavigable: 1, | |||
is_series: 1 | |||
isSeries: 1 | |||
// regionFill: 1 | |||
}); | |||
@@ -59,9 +59,12 @@ let line_composite_chart = new Chart ({ | |||
parent: c2, | |||
data: line_composite_data, | |||
type: 'line', | |||
options: { | |||
dotSize: 10 | |||
}, | |||
height: 180, | |||
colors: ['green'], | |||
is_series: 1 | |||
isSeries: 1 | |||
}); | |||
bar_composite_chart.parent.addEventListener('data-select', (e) => { | |||
@@ -75,16 +78,48 @@ let type_data = { | |||
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm", | |||
"12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"], | |||
yMarkers: [ | |||
{ | |||
name: "Marker 1", | |||
value: 42, | |||
type: 'dashed' | |||
}, | |||
{ | |||
name: "Marker 2", | |||
value: 25, | |||
type: 'dashed' | |||
} | |||
], | |||
yRegions: [ | |||
{ | |||
name: "Region Y 1", | |||
start: 10, | |||
end: 50 | |||
}, | |||
], | |||
// will depend on series code for calculating X values | |||
// xRegions: [ | |||
// { | |||
// name: "Region X 2", | |||
// start: , | |||
// end: , | |||
// } | |||
// ], | |||
datasets: [ | |||
{ | |||
name: "Some Data", | |||
values: [18, 40, 30, 35, 8, 52, 17, -4], | |||
axisPosition: 'right' | |||
axisPosition: 'right', | |||
chartType: 'bar' | |||
}, | |||
{ | |||
name: "Another Set", | |||
values: [30, 50, -10, 15, 18, 32, 27, 14], | |||
axisPosition: 'right' | |||
axisPosition: 'right', | |||
chartType: 'line' | |||
}, | |||
// { | |||
// name: "Yet Another", | |||
@@ -111,12 +146,14 @@ let type_chart = new Chart({ | |||
parent: "#chart-types", | |||
// title: "My Awesome Chart", | |||
data: type_data, | |||
type: 'multiaxis', | |||
type: 'line', | |||
height: 250, | |||
colors: ['purple', 'magenta'], | |||
is_series: 1, | |||
format_tooltip_x: d => (d + '').toUpperCase(), | |||
format_tooltip_y: d => d + ' pts' | |||
isSeries: 1, | |||
xAxisMode: 'tick', | |||
yAxisMode: 'span', | |||
// formatTooltipX: d => (d + '').toUpperCase(), | |||
// formatTooltipY: d => d + ' pts' | |||
}); | |||
Array.prototype.slice.call( | |||
@@ -164,7 +201,7 @@ let plot_chart_args = { | |||
type: 'line', | |||
height: 250, | |||
colors: ['blue'], | |||
is_series: 1, | |||
isSeries: 1, | |||
showDots: 0, | |||
heatline: 1, | |||
xAxisMode: 'tick', | |||
@@ -241,7 +278,7 @@ let update_chart = new Chart({ | |||
type: 'line', | |||
height: 250, | |||
colors: ['red'], | |||
is_series: 1, | |||
isSeries: 1, | |||
regionFill: 1 | |||
}); | |||
@@ -24,7 +24,7 @@ | |||
<div class="container"> | |||
<div class="row hero" style="padding-top: 30px; padding-bottom: 0px;"> | |||
<div class="jumbotron" style="background: transparent;"> | |||
<h1>Frappé Charts</h1> | |||
<h1>Frappe Charts</h1> | |||
<p class="mt-2">GitHub-inspired simple and modern charts for the web</p> | |||
<p class="mt-2">with zero dependencies.</p> | |||
<!--<p class="mt-2">Because dumb charts are hard to come by.</p>--> | |||
@@ -162,7 +162,7 @@ | |||
xAxisMode: 'tick', // for short label ticks | |||
// or 'span' for long spanning vertical axis lines | |||
yAxisMode: 'span', // for long horizontal lines, or 'tick' | |||
is_series: 1, // to allow for skipping of X values | |||
isSeries: 1, // to allow for skipping of X values | |||
...</code></pre> | |||
<div id="chart-trends" class="border"></div> | |||
<div class="btn-group chart-plot-buttons mt-1 mx-auto" role="group"> | |||
@@ -1,12 +1,11 @@ | |||
import '../scss/charts.scss'; | |||
import BarChart from './charts/BarChart'; | |||
import LineChart from './charts/LineChart'; | |||
import ScatterChart from './charts/ScatterChart'; | |||
import MultiAxisChart from './charts/MultiAxisChart'; | |||
import PercentageChart from './charts/PercentageChart'; | |||
import PieChart from './charts/PieChart'; | |||
import Heatmap from './charts/Heatmap'; | |||
import AxisChart from './charts/AxisChart'; | |||
// if (ENV !== 'production') { | |||
// // Enable LiveReload | |||
@@ -16,9 +15,13 @@ import Heatmap from './charts/Heatmap'; | |||
// ); | |||
// } | |||
// If type is bar | |||
const chartTypes = { | |||
line: LineChart, | |||
bar: BarChart, | |||
mixed: AxisChart, | |||
multiaxis: MultiAxisChart, | |||
scatter: ScatterChart, | |||
percentage: PercentageChart, | |||
@@ -27,8 +30,17 @@ const chartTypes = { | |||
}; | |||
function getChartByType(chartType = 'line', options) { | |||
if(chartType === 'line') { | |||
options.unitType = 'line'; | |||
return new AxisChart(options); | |||
} else if (chartType === 'bar') { | |||
options.unitType = 'bar'; | |||
return new AxisChart(options); | |||
} | |||
if (!chartTypes[chartType]) { | |||
return new LineChart(options); | |||
console.error("Undefined chart type: " + chartType); | |||
return; | |||
} | |||
return new chartTypes[chartType](options); | |||
@@ -1,8 +1,9 @@ | |||
import BaseChart from './BaseChart'; | |||
import { Y_AXIS_MARGIN } from '../utils/margins'; | |||
import { ChartComponent } from '../objects/ChartComponent'; | |||
import { BarChartController, LineChartController, getPaths } from '../objects/AxisChartControllers'; | |||
import { getOffset, fire } from '../utils/dom'; | |||
import { AxisChartRenderer, makePath, makeGradient } from '../utils/draw'; | |||
import { AxisChartRenderer } from '../utils/draw'; | |||
import { equilizeNoOfElements } from '../utils/draw-utils'; | |||
import { Animator } from '../utils/animate'; | |||
import { runSMILAnimation } from '../utils/animation'; | |||
@@ -12,11 +13,33 @@ import { floatTwo, fillArray } from '../utils/helpers'; | |||
export default class AxisChart extends BaseChart { | |||
constructor(args) { | |||
super(args); | |||
this.is_series = args.is_series; | |||
this.format_tooltip_y = args.format_tooltip_y; | |||
this.format_tooltip_x = args.format_tooltip_x; | |||
this.isSeries = args.isSeries; | |||
this.formatTooltipY = args.formatTooltipY; | |||
this.formatTooltipX = args.formatTooltipX; | |||
this.unitType = args.unitType || 'line'; | |||
this.setupUnitRenderer(); | |||
this.zeroLine = this.height; | |||
this.preSetup(); | |||
this.setup(); | |||
} | |||
configure(args) { | |||
super.configure(); | |||
this.config.xAxisMode = args.xAxisMode; | |||
this.config.yAxisMode = args.yAxisMode; | |||
} | |||
preSetup() {} | |||
setupUnitRenderer() { | |||
let options = this.rawChartArgs.options; | |||
this.unitRenderers = { | |||
bar: new BarChartController(options), | |||
line: new LineChartController(options) | |||
}; | |||
} | |||
setHorizontalMargin() { | |||
@@ -36,8 +59,16 @@ export default class AxisChart extends BaseChart { | |||
this.state = { | |||
xAxisLabels: [], | |||
xAxisPositions: [], | |||
xAxisMode: this.config.xAxisMode, | |||
yAxisMode: this.config.yAxisMode | |||
} | |||
this.data.datasets.map(d => { | |||
if(!d.chartType ) { | |||
d.chartType = this.unitType; | |||
} | |||
}); | |||
this.prepareYAxis(); | |||
} | |||
@@ -78,6 +109,8 @@ export default class AxisChart extends BaseChart { | |||
}); | |||
s.noOfDatasets = s.datasets.length; | |||
s.yMarkers = data.yMarkers; | |||
s.yRegions = data.yRegions; | |||
} | |||
prepareYAxis() { | |||
@@ -97,6 +130,7 @@ export default class AxisChart extends BaseChart { | |||
this.setYAxis(); | |||
this.calcYUnits(); | |||
this.calcYMaximums(); | |||
this.calcYRegions(); | |||
// should be state | |||
this.configUnits(); | |||
@@ -111,7 +145,8 @@ export default class AxisChart extends BaseChart { | |||
let s = this.state; | |||
this.setUnitWidthAndXOffset(); | |||
s.xAxisPositions = s.xAxisLabels.map((d, i) => | |||
floatTwo(s.xOffset + i * s.unitWidth)); | |||
floatTwo(s.xOffset + i * s.unitWidth) | |||
); | |||
s.xUnitPositions = new Array(s.noOfDatasets).fill(s.xAxisPositions); | |||
} | |||
@@ -150,8 +185,26 @@ export default class AxisChart extends BaseChart { | |||
// this.make_tooltip(); | |||
} | |||
calcYRegions() { | |||
let s = this.state; | |||
if(s.yMarkers) { | |||
s.yMarkers = s.yMarkers.map(d => { | |||
d.value = floatTwo(s.yAxis.zeroLine - d.value * s.yAxis.scaleMultiplier); | |||
return d; | |||
}); | |||
} | |||
if(s.yRegions) { | |||
s.yRegions = s.yRegions.map(d => { | |||
d.start = floatTwo(s.yAxis.zeroLine - d.start * s.yAxis.scaleMultiplier); | |||
d.end = floatTwo(s.yAxis.zeroLine - d.end * s.yAxis.scaleMultiplier); | |||
return d; | |||
}); | |||
} | |||
} | |||
configUnits() {} | |||
// Default, as per bar, and mixed. Only line will be a special case | |||
setUnitWidthAndXOffset() { | |||
this.state.unitWidth = this.width/(this.state.datasetLength); | |||
this.state.xOffset = this.state.unitWidth/2; | |||
@@ -180,11 +233,11 @@ export default class AxisChart extends BaseChart { | |||
// this.yAxesAux, | |||
...this.getYAxesComponents(), | |||
this.getXAxisComponents(), | |||
// this.getYMarkerLines(), | |||
// this.getXMarkerLines(), | |||
// TODO: regions too? | |||
...this.getPathComponents(), | |||
...this.getDataUnitsComponents(this.config), | |||
...this.getYRegions(), | |||
...this.getXRegions(), | |||
...this.getYMarkerLines(), | |||
// ...this.getXMarkerLines(), | |||
...this.getChartComponents(), | |||
]; | |||
} | |||
@@ -233,7 +286,9 @@ export default class AxisChart extends BaseChart { | |||
let s = this.state; | |||
// TODO: xAxis Label spacing | |||
return s.xAxisPositions.map((position, i) => | |||
this.renderer.xLine(position, s.xAxisLabels[i], {pos:'top'}) | |||
this.renderer.xLine(position, s.xAxisLabels[i] | |||
// , {pos:'top'} | |||
) | |||
); | |||
}, | |||
animate: (xLines) => { | |||
@@ -262,68 +317,156 @@ export default class AxisChart extends BaseChart { | |||
}); | |||
} | |||
getDataUnitsComponents() { | |||
return this.data.datasets.map((d, index) => { | |||
return new ChartComponent({ | |||
layerClass: 'dataset-units dataset-' + index, | |||
make: () => { | |||
let d = this.state.datasets[index]; | |||
let unitType = this.unitArgs; | |||
return d.positions.map((y, j) => { | |||
return this.renderer[unitType.type]( | |||
this.state.xAxisPositions[j], | |||
y, | |||
unitType.args, | |||
this.colors[index], | |||
j, | |||
index, | |||
this.state.noOfDatasets | |||
); | |||
}); | |||
}, | |||
animate: (svgUnits) => { | |||
let unitType = this.unitArgs.type; | |||
// have been updated in axis render; | |||
let newX = this.state.xAxisPositions; | |||
let newY = this.state.datasets[index].positions; | |||
let lastUnit = svgUnits[svgUnits.length - 1]; | |||
let parentNode = lastUnit.parentNode; | |||
if(this.oldState.xExtra > 0) { | |||
for(var i = 0; i<this.oldState.xExtra; i++) { | |||
let unit = lastUnit.cloneNode(true); | |||
parentNode.appendChild(unit); | |||
svgUnits.push(unit); | |||
} | |||
} | |||
getChartComponents() { | |||
let dataUnitsComponents = [] | |||
// this.state is not defined at this stage | |||
this.data.datasets.forEach((d, index) => { | |||
if(d.chartType === 'line') { | |||
dataUnitsComponents.push(this.getPathComponent(d, index)); | |||
} | |||
console.log(this.unitRenderers[d.chartType], d.chartType); | |||
dataUnitsComponents.push(this.getDataUnitComponent( | |||
d, index, this.unitRenderers[d.chartType] | |||
)); | |||
}); | |||
return dataUnitsComponents; | |||
} | |||
svgUnits.map((unit, i) => { | |||
if(newX[i] === undefined || newY[i] === undefined) return; | |||
this.elementsToAnimate.push(this.renderer['animate' + unitType]( | |||
unit, // unit, with info to replace where it came from in the data | |||
newX[i], | |||
newY[i], | |||
index, | |||
this.state.noOfDatasets | |||
)); | |||
}); | |||
getDataUnitComponent(d, index, unitRenderer) { | |||
return new ChartComponent({ | |||
layerClass: 'dataset-units dataset-' + index, | |||
make: () => { | |||
let d = this.state.datasets[index]; | |||
return d.positions.map((y, j) => { | |||
return unitRenderer.draw( | |||
this.state.xAxisPositions[j], | |||
y, | |||
this.colors[index], | |||
j, | |||
index, | |||
this.state.noOfDatasets | |||
); | |||
}); | |||
}, | |||
animate: (svgUnits) => { | |||
// have been updated in axis render; | |||
let newX = this.state.xAxisPositions; | |||
let newY = this.state.datasets[index].positions; | |||
let lastUnit = svgUnits[svgUnits.length - 1]; | |||
let parentNode = lastUnit.parentNode; | |||
if(this.oldState.xExtra > 0) { | |||
for(var i = 0; i<this.oldState.xExtra; i++) { | |||
let unit = lastUnit.cloneNode(true); | |||
parentNode.appendChild(unit); | |||
svgUnits.push(unit); | |||
} | |||
} | |||
}); | |||
svgUnits.map((unit, i) => { | |||
if(newX[i] === undefined || newY[i] === undefined) return; | |||
this.elementsToAnimate.push(unitRenderer.animate( | |||
unit, // unit, with info to replace where it came from in the data | |||
newX[i], | |||
newY[i], | |||
index, | |||
this.state.noOfDatasets | |||
)); | |||
}); | |||
} | |||
}); | |||
} | |||
getPathComponents() { | |||
return []; | |||
getPathComponent(d, index) { | |||
return new ChartComponent({ | |||
layerClass: 'path dataset-path', | |||
make: () => { | |||
let d = this.state.datasets[index]; | |||
let color = this.colors[index]; | |||
return getPaths( | |||
d.positions, | |||
this.state.xAxisPositions, | |||
color, | |||
this.config.heatline, | |||
this.config.regionFill | |||
); | |||
}, | |||
animate: (paths) => { | |||
let newX = this.state.xAxisPositions; | |||
let newY = this.state.datasets[index].positions; | |||
let oldX = this.oldState.xAxisPositions; | |||
let oldY = this.oldState.datasets[index].positions; | |||
let parentNode = paths[0].parentNode; | |||
[oldX, newX] = equilizeNoOfElements(oldX, newX); | |||
[oldY, newY] = equilizeNoOfElements(oldY, newY); | |||
if(this.oldState.xExtra > 0) { | |||
paths = getPaths( | |||
oldY, oldX, this.colors[index], | |||
this.config.heatline, | |||
this.config.regionFill | |||
); | |||
parentNode.textContent = ''; | |||
paths.map(path => parentNode.appendChild(path)); | |||
} | |||
const newPointsList = newY.map((y, i) => (newX[i] + ',' + y)); | |||
this.elementsToAnimate = this.elementsToAnimate | |||
.concat(this.renderer.animatepath(paths, newPointsList.join("L"))); | |||
} | |||
}); | |||
} | |||
getYMarkerLines() { | |||
return []; | |||
if(!this.data.yMarkers) { | |||
return []; | |||
} | |||
return this.data.yMarkers.map((d, index) => { | |||
return new ChartComponent({ | |||
layerClass: 'y-markers', | |||
make: () => { | |||
let s = this.state; | |||
return s.yMarkers.map(marker => | |||
this.renderer.yMarker(marker.value, marker.name, | |||
{pos:'right', mode: 'span', lineType: marker.type}) | |||
); | |||
}, | |||
animate: () => {} | |||
}); | |||
}); | |||
} | |||
// getXMarkerLines() { | |||
// return []; | |||
// } | |||
getYRegions() { | |||
if(!this.data.yRegions) { | |||
return []; | |||
} | |||
// return []; | |||
return this.data.yRegions.map((d, index) => { | |||
return new ChartComponent({ | |||
layerClass: 'y-regions', | |||
make: () => { | |||
let s = this.state; | |||
return s.yRegions.map(region => | |||
this.renderer.yRegion(region.start, region.end, region.name) | |||
); | |||
}, | |||
animate: () => {} | |||
}); | |||
}); | |||
} | |||
getXMarkerLines() { | |||
getXRegions() { | |||
return []; | |||
} | |||
@@ -345,6 +488,88 @@ export default class AxisChart extends BaseChart { | |||
} else { | |||
this.renderer.refreshState(state); | |||
} | |||
let meta = { | |||
totalHeight: this.height, | |||
totalWidth: this.width, | |||
zeroLine: this.state.zeroLine, | |||
unitWidth: this.state.unitWidth, | |||
}; | |||
Object.keys(this.unitRenderers).map(key => { | |||
this.unitRenderers[key].refreshMeta(meta); | |||
}); | |||
} | |||
bindTooltip() { | |||
// TODO: could be in tooltip itself, as it is a given functionality for its parent | |||
this.chartWrapper.addEventListener('mousemove', (e) => { | |||
let o = getOffset(this.chartWrapper); | |||
let relX = e.pageX - o.left - this.translateXLeft; | |||
let relY = e.pageY - o.top - this.translateY; | |||
if(relY < this.height + this.translateY * 2) { | |||
this.mapTooltipXPosition(relX); | |||
} else { | |||
this.tip.hide_tip(); | |||
} | |||
}); | |||
} | |||
mapTooltipXPosition(relX) { | |||
let s = this.state; | |||
if(!s.yUnitMinimums) return; | |||
let titles = s.xAxisLabels; | |||
if(this.formatTooltipX && this.formatTooltipX(titles[0])) { | |||
titles = titles.map(d=>this.formatTooltipX(d)); | |||
} | |||
let formatY = this.formatTooltipY && this.formatTooltipY(this.y[0].values[0]); | |||
for(var i=s.datasetLength - 1; i >= 0 ; i--) { | |||
let xVal = s.xAxisPositions[i]; | |||
// let delta = i === 0 ? s.unitWidth : xVal - s.xAxisPositions[i-1]; | |||
if(relX > xVal - s.unitWidth/2) { | |||
let x = xVal + this.translateXLeft; | |||
let y = s.yUnitMinimums[i] + this.translateY; | |||
let values = s.datasets.map((set, j) => { | |||
return { | |||
title: set.title, | |||
value: formatY ? this.formatTooltipY(set.values[i]) : set.values[i], | |||
color: this.colors[j], | |||
}; | |||
}); | |||
this.tip.set_values(x, y, titles[i], '', values); | |||
this.tip.show_tip(); | |||
break; | |||
} | |||
} | |||
} | |||
getDataPoint(index=this.current_index) { | |||
// check for length | |||
let data_point = { | |||
index: index | |||
}; | |||
let y = this.y[0]; | |||
['svg_units', 'yUnitPositions', 'values'].map(key => { | |||
let data_key = key.slice(0, key.length-1); | |||
data_point[data_key] = y[key][index]; | |||
}); | |||
data_point.label = this.xAxisLabels[index]; | |||
return data_point; | |||
} | |||
updateCurrentDataPoint(index) { | |||
index = parseInt(index); | |||
if(index < 0) index = 0; | |||
if(index >= this.xAxisLabels.length) index = this.xAxisLabels.length - 1; | |||
if(index === this.current_index) return; | |||
this.current_index = index; | |||
$.fire(this.parent, "data-select", this.getDataPoint()); | |||
} | |||
// API | |||
@@ -374,3 +599,7 @@ export default class AxisChart extends BaseChart { | |||
// | |||
} | |||
} | |||
// keep a binding at the end of chart | |||
@@ -1,83 +0,0 @@ | |||
import AxisChart from './AxisChart'; | |||
export default class BarChart extends AxisChart { | |||
constructor(args) { | |||
super(args); | |||
this.type = 'bar'; | |||
this.setup(); | |||
} | |||
configure(args) { | |||
super.configure(args); | |||
this.config.xAxisMode = args.xAxisMode || 'tick'; | |||
this.config.yAxisMode = args.yAxisMode || 'span'; | |||
} | |||
// setUnitWidthAndXOffset() { | |||
// this.state.unitWidth = this.width/(this.state.datasetLength); | |||
// this.state.xOffset = this.state.unitWidth/2; | |||
// } | |||
configUnits() { | |||
this.unitArgs = { | |||
type: 'bar', | |||
args: { | |||
spaceWidth: this.state.unitWidth/2, | |||
} | |||
}; | |||
} | |||
// makeOverlay() { | |||
// // Just make one out of the first element | |||
// let index = this.xAxisLabels.length - 1; | |||
// let unit = this.y[0].svg_units[index]; | |||
// this.updateCurrentDataPoint(index); | |||
// if(this.overlay) { | |||
// this.overlay.parentNode.removeChild(this.overlay); | |||
// } | |||
// this.overlay = unit.cloneNode(); | |||
// this.overlay.style.fill = '#000000'; | |||
// this.overlay.style.opacity = '0.4'; | |||
// this.drawArea.appendChild(this.overlay); | |||
// } | |||
// bindOverlay() { | |||
// // on event, update overlay | |||
// this.parent.addEventListener('data-select', (e) => { | |||
// this.update_overlay(e.svg_unit); | |||
// }); | |||
// } | |||
// bind_units(units_array) { | |||
// units_array.map(unit => { | |||
// unit.addEventListener('click', () => { | |||
// let index = unit.getAttribute('data-point-index'); | |||
// this.updateCurrentDataPoint(index); | |||
// }); | |||
// }); | |||
// } | |||
// update_overlay(unit) { | |||
// let attributes = []; | |||
// Object.keys(unit.attributes).map(index => { | |||
// attributes.push(unit.attributes[index]); | |||
// }); | |||
// attributes.filter(attr => attr.specified).map(attr => { | |||
// this.overlay.setAttribute(attr.name, attr.nodeValue); | |||
// }); | |||
// this.overlay.style.fill = '#000000'; | |||
// this.overlay.style.opacity = '0.4'; | |||
// } | |||
// onLeftArrow() { | |||
// this.updateCurrentDataPoint(this.currentIndex - 1); | |||
// } | |||
// onRightArrow() { | |||
// this.updateCurrentDataPoint(this.currentIndex + 1); | |||
// } | |||
} |
@@ -40,85 +40,4 @@ export default class LineChart extends AxisChart { | |||
this.state.xOffset = 0; | |||
} | |||
getDataUnitsComponents(config) { | |||
if(!config.showDots) { | |||
return []; | |||
} | |||
else { | |||
return super.getDataUnitsComponents(); | |||
} | |||
} | |||
getPathComponents() { | |||
return this.data.datasets.map((d, index) => { | |||
return new ChartComponent({ | |||
layerClass: 'path dataset-path', | |||
make: () => { | |||
let d = this.state.datasets[index]; | |||
let color = this.colors[index]; | |||
return this.getPaths( | |||
d.positions, | |||
this.state.xAxisPositions, | |||
color, | |||
this.config.heatline, | |||
this.config.regionFill | |||
); | |||
}, | |||
animate: (paths) => { | |||
let newX = this.state.xAxisPositions; | |||
let newY = this.state.datasets[index].positions; | |||
let oldX = this.oldState.xAxisPositions; | |||
let oldY = this.oldState.datasets[index].positions; | |||
let parentNode = paths[0].parentNode; | |||
[oldX, newX] = equilizeNoOfElements(oldX, newX); | |||
[oldY, newY] = equilizeNoOfElements(oldY, newY); | |||
if(this.oldState.xExtra > 0) { | |||
paths = this.getPaths( | |||
oldY, oldX, this.colors[index], | |||
this.config.heatline, | |||
this.config.regionFill | |||
); | |||
parentNode.textContent = ''; | |||
paths.map(path => parentNode.appendChild(path)); | |||
} | |||
const newPointsList = newY.map((y, i) => (newX[i] + ',' + y)); | |||
this.elementsToAnimate = this.elementsToAnimate | |||
.concat(this.renderer.animatepath(paths, newPointsList.join("L"))); | |||
} | |||
}); | |||
}); | |||
} | |||
getPaths(yList, xList, color, heatline=false, regionFill=false) { | |||
let pointsList = yList.map((y, i) => (xList[i] + ',' + y)); | |||
let pointsStr = pointsList.join("L"); | |||
let path = makePath("M"+pointsStr, 'line-graph-path', color); | |||
// HeatLine | |||
if(heatline) { | |||
let gradient_id = makeGradient(this.svgDefs, color); | |||
path.style.stroke = `url(#${gradient_id})`; | |||
} | |||
let components = [path]; | |||
// Region | |||
if(regionFill) { | |||
let gradient_id_region = makeGradient(this.svgDefs, color, true); | |||
let zeroLine = this.state.yAxis.zeroLine; | |||
// TODO: use zeroLine OR minimum | |||
let pathStr = "M" + `0,${zeroLine}L` + pointsStr + `L${this.width},${zeroLine}`; | |||
components.push(makePath(pathStr, `region-fill`, 'none', `url(#${gradient_id_region})`)); | |||
} | |||
return components; | |||
} | |||
} |
@@ -6,9 +6,12 @@ import { floatTwo } from '../utils/helpers'; | |||
export default class MultiAxisChart extends AxisChart { | |||
constructor(args) { | |||
super(args); | |||
// this.unitType = args.unitType || 'line'; | |||
// this.setup(); | |||
} | |||
preSetup() { | |||
this.type = 'multiaxis'; | |||
this.unitType = args.unitType || 'line'; | |||
this.setup(); | |||
} | |||
setHorizontalMargin() { | |||
@@ -107,7 +110,7 @@ export default class MultiAxisChart extends AxisChart { | |||
} | |||
// TODO remove renderer zeroline from above and below | |||
getDataUnitsComponents() { | |||
getChartComponents() { | |||
return this.data.datasets.map((d, index) => { | |||
return new ChartComponent({ | |||
layerClass: 'dataset-units dataset-' + index, | |||
@@ -0,0 +1,215 @@ | |||
import { getBarHeightAndYAttr } from '../utils/draw-utils'; | |||
import { createSVG, makePath, makeGradient } from '../utils/draw'; | |||
import { STD_EASING, UNIT_ANIM_DUR, MARKER_LINE_ANIM_DUR, PATH_ANIM_DUR } from '../utils/animate'; | |||
class AxisChartController { | |||
constructor(meta) { | |||
// TODO: make configurable passing args | |||
this.refreshMeta(meta); | |||
this.setupArgs(); | |||
} | |||
setupArgs() {} | |||
refreshMeta(meta) { | |||
this.meta = meta || {}; | |||
} | |||
draw() {} | |||
animate() {} | |||
} | |||
export class AxisController extends AxisChartController { | |||
constructor(meta) { | |||
super(meta); | |||
} | |||
setupArgs() {} | |||
draw(x, y, color, index) { | |||
return createSVG('circle', { | |||
style: `fill: ${color}`, | |||
'data-point-index': index, | |||
cx: x, | |||
cy: y, | |||
r: this.args.radius | |||
}); | |||
} | |||
animate(dot, x, yTop) { | |||
return [dot, {cx: x, cy: yTop}, UNIT_ANIM_DUR, STD_EASING]; | |||
// dot.animate({cy: yTop}, UNIT_ANIM_DUR, mina.easein); | |||
} | |||
} | |||
export class BarChartController extends AxisChartController { | |||
constructor(meta) { | |||
super(meta); | |||
} | |||
setupArgs() { | |||
this.args = { | |||
spaceRatio: 0.5, | |||
}; | |||
} | |||
draw(x, yTop, color, index, datasetIndex, noOfDatasets) { | |||
let totalWidth = this.meta.unitWidth - this.meta.unitWidth * this.args.spaceRatio; | |||
let startX = x - totalWidth/2; | |||
// temp commented | |||
// let width = totalWidth / noOfDatasets; | |||
// let currentX = startX + width * datasetIndex; | |||
// temp | |||
let width = totalWidth; | |||
let currentX = startX; | |||
let [height, y] = getBarHeightAndYAttr(yTop, this.meta.zeroLine, this.meta.totalHeight); | |||
return createSVG('rect', { | |||
className: `bar mini`, | |||
style: `fill: ${color}`, | |||
'data-point-index': index, | |||
x: currentX, | |||
y: y, | |||
width: width, | |||
height: height | |||
}); | |||
} | |||
animate(bar, x, yTop, index, noOfDatasets) { | |||
let start = x - this.meta.avgUnitWidth/4; | |||
let width = (this.meta.avgUnitWidth/2)/noOfDatasets; | |||
let [height, y] = getBarHeightAndYAttr(yTop, this.meta.zeroLine, this.meta.totalHeight); | |||
x = start + (width * index); | |||
return [bar, {width: width, height: height, x: x, y: y}, UNIT_ANIM_DUR, STD_EASING]; | |||
// bar.animate({height: args.newHeight, y: yTop}, UNIT_ANIM_DUR, mina.easein); | |||
} | |||
} | |||
export class LineChartController extends AxisChartController { | |||
constructor(meta) { | |||
super(meta); | |||
} | |||
setupArgs() { | |||
console.log(this); | |||
this.args = { | |||
radius: this.meta.dotSize || 4 | |||
}; | |||
} | |||
draw(x, y, color, index) { | |||
return createSVG('circle', { | |||
style: `fill: ${color}`, | |||
'data-point-index': index, | |||
cx: x, | |||
cy: y, | |||
r: this.args.radius | |||
}); | |||
} | |||
animate(dot, x, yTop) { | |||
return [dot, {cx: x, cy: yTop}, UNIT_ANIM_DUR, STD_EASING]; | |||
// dot.animate({cy: yTop}, UNIT_ANIM_DUR, mina.easein); | |||
} | |||
} | |||
export function getPaths(yList, xList, color, heatline=false, regionFill=false) { | |||
let pointsList = yList.map((y, i) => (xList[i] + ',' + y)); | |||
let pointsStr = pointsList.join("L"); | |||
let path = makePath("M"+pointsStr, 'line-graph-path', color); | |||
// HeatLine | |||
if(heatline) { | |||
let gradient_id = makeGradient(this.svgDefs, color); | |||
path.style.stroke = `url(#${gradient_id})`; | |||
} | |||
let components = [path]; | |||
// Region | |||
if(regionFill) { | |||
let gradient_id_region = makeGradient(this.svgDefs, color, true); | |||
let zeroLine = this.state.yAxis.zeroLine; | |||
// TODO: use zeroLine OR minimum | |||
let pathStr = "M" + `0,${zeroLine}L` + pointsStr + `L${this.width},${zeroLine}`; | |||
components.push(makePath(pathStr, `region-fill`, 'none', `url(#${gradient_id_region})`)); | |||
} | |||
return components; | |||
} | |||
// class BarChart extends AxisChart { | |||
// constructor(args) { | |||
// super(args); | |||
// this.type = 'bar'; | |||
// this.setup(); | |||
// } | |||
// configure(args) { | |||
// super.configure(args); | |||
// this.config.xAxisMode = args.xAxisMode || 'tick'; | |||
// this.config.yAxisMode = args.yAxisMode || 'span'; | |||
// } | |||
// // ================================= | |||
// makeOverlay() { | |||
// // Just make one out of the first element | |||
// let index = this.xAxisLabels.length - 1; | |||
// let unit = this.y[0].svg_units[index]; | |||
// this.updateCurrentDataPoint(index); | |||
// if(this.overlay) { | |||
// this.overlay.parentNode.removeChild(this.overlay); | |||
// } | |||
// this.overlay = unit.cloneNode(); | |||
// this.overlay.style.fill = '#000000'; | |||
// this.overlay.style.opacity = '0.4'; | |||
// this.drawArea.appendChild(this.overlay); | |||
// } | |||
// bindOverlay() { | |||
// // on event, update overlay | |||
// this.parent.addEventListener('data-select', (e) => { | |||
// this.update_overlay(e.svg_unit); | |||
// }); | |||
// } | |||
// bind_units(units_array) { | |||
// units_array.map(unit => { | |||
// unit.addEventListener('click', () => { | |||
// let index = unit.getAttribute('data-point-index'); | |||
// this.updateCurrentDataPoint(index); | |||
// }); | |||
// }); | |||
// } | |||
// update_overlay(unit) { | |||
// let attributes = []; | |||
// Object.keys(unit.attributes).map(index => { | |||
// attributes.push(unit.attributes[index]); | |||
// }); | |||
// attributes.filter(attr => attr.specified).map(attr => { | |||
// this.overlay.setAttribute(attr.name, attr.nodeValue); | |||
// }); | |||
// this.overlay.style.fill = '#000000'; | |||
// this.overlay.style.opacity = '0.4'; | |||
// } | |||
// onLeftArrow() { | |||
// this.updateCurrentDataPoint(this.currentIndex - 1); | |||
// } | |||
// onRightArrow() { | |||
// this.updateCurrentDataPoint(this.currentIndex + 1); | |||
// } | |||
// } | |||
@@ -44,7 +44,7 @@ export function equilizeNoOfElements(array1, array2, | |||
// return values.map((value, i) => { | |||
// let space_taken = getStringWidth(value, char_width) + 2; | |||
// if(space_taken > allowed_space) { | |||
// if(is_series) { | |||
// if(isSeries) { | |||
// // Skip some axis lines if X axis is a series | |||
// let skips = 1; | |||
// while((space_taken/skips)*2 > allowed_space) { | |||
@@ -1,16 +1,18 @@ | |||
import { getBarHeightAndYAttr } from './draw-utils'; | |||
import { getStringWidth } from './helpers'; | |||
import { STD_EASING, UNIT_ANIM_DUR, MARKER_LINE_ANIM_DUR, PATH_ANIM_DUR } from './animate'; | |||
const AXIS_TICK_LENGTH = 6; | |||
const LABEL_MARGIN = 4; | |||
const FONT_SIZE = 10; | |||
const BASE_LINE_COLOR = '#dadada'; | |||
const BASE_BG_COLOR = '#F7FAFC'; | |||
function $(expr, con) { | |||
return typeof expr === "string"? (con || document).querySelector(expr) : expr || null; | |||
} | |||
function createSVG(tag, o) { | |||
export function createSVG(tag, o) { | |||
var element = document.createElementNS("http://www.w3.org/2000/svg", tag); | |||
for (var i in o) { | |||
@@ -119,7 +121,7 @@ export function makeHeatSquare(className, x, y, size, fill='none', data={}) { | |||
y: y, | |||
width: size, | |||
height: size, | |||
fill: fill | |||
fill: 1 | |||
}; | |||
Object.keys(data).map(key => { | |||
@@ -140,7 +142,7 @@ export function makeText(className, x, y, content) { | |||
}); | |||
} | |||
export function makeVertLine(x, label, y1, y2, options={}) { | |||
function makeVertLine(x, label, y1, y2, options={}) { | |||
if(!options.stroke) options.stroke = BASE_LINE_COLOR; | |||
let l = createSVG('line', { | |||
className: 'line-vertical ' + options.className, | |||
@@ -172,7 +174,7 @@ export function makeVertLine(x, label, y1, y2, options={}) { | |||
return line; | |||
} | |||
export function makeHoriLine(y, label, x1, x2, options={}) { | |||
function makeHoriLine(y, label, x1, x2, options={}) { | |||
if(!options.stroke) options.stroke = BASE_LINE_COLOR; | |||
if(!options.lineType) options.lineType = ''; | |||
let className = 'line-horizontal ' + options.className + | |||
@@ -182,8 +184,8 @@ export function makeHoriLine(y, label, x1, x2, options={}) { | |||
className: className, | |||
x1: x1, | |||
x2: x2, | |||
y1: 0, | |||
y2: 0, | |||
y1: y, | |||
y2: y, | |||
styles: { | |||
stroke: options.stroke | |||
} | |||
@@ -191,7 +193,7 @@ export function makeHoriLine(y, label, x1, x2, options={}) { | |||
let text = createSVG('text', { | |||
x: x1 < x2 ? x1 - LABEL_MARGIN : x1 + LABEL_MARGIN, | |||
y: 0, | |||
y: y, | |||
dy: (FONT_SIZE / 2 - 2) + 'px', | |||
'font-size': FONT_SIZE + 'px', | |||
'text-anchor': x1 < x2 ? 'end' : 'start', | |||
@@ -199,7 +201,6 @@ export function makeHoriLine(y, label, x1, x2, options={}) { | |||
}); | |||
let line = createSVG('g', { | |||
transform: `translate(0, ${ y })`, | |||
'stroke-opacity': 1 | |||
}); | |||
@@ -231,46 +232,11 @@ export class AxisChartRenderer { | |||
this.zeroLine = zeroLine; | |||
} | |||
bar(x, yTop, args, color, index, datasetIndex, noOfDatasets) { | |||
let totalWidth = this.unitWidth - args.spaceWidth; | |||
let startX = x - totalWidth/2; | |||
// temp commented | |||
// let width = totalWidth / noOfDatasets; | |||
// let currentX = startX + width * datasetIndex; | |||
// temp | |||
let width = totalWidth; | |||
let currentX = startX; | |||
let [height, y] = getBarHeightAndYAttr(yTop, this.zeroLine, this.totalHeight); | |||
return createSVG('rect', { | |||
className: `bar mini`, | |||
style: `fill: ${color}`, | |||
'data-point-index': index, | |||
x: currentX, | |||
y: y, | |||
width: width, | |||
height: height | |||
}); | |||
} | |||
dot(x, y, args, color, index) { | |||
return createSVG('circle', { | |||
style: `fill: ${color}`, | |||
'data-point-index': index, | |||
cx: x, | |||
cy: y, | |||
r: args.radius | |||
}); | |||
} | |||
xLine(x, label, options={}) { | |||
if(!options.pos) options.pos = 'bottom'; | |||
if(!options.offset) options.offset = 0; | |||
if(!options.mode) options.mode = this.xAxisMode; | |||
console.log(this.xAxisMode); | |||
if(!options.stroke) options.stroke = BASE_LINE_COLOR; | |||
if(!options.className) options.className = ''; | |||
@@ -296,7 +262,8 @@ export class AxisChartRenderer { | |||
return makeVertLine(x, label, y1, y2, { | |||
stroke: options.stroke, | |||
className: options.className | |||
className: options.className, | |||
lineType: options.lineType | |||
}); | |||
} | |||
@@ -322,16 +289,102 @@ export class AxisChartRenderer { | |||
return makeHoriLine(y, label, x1, x2, { | |||
stroke: options.stroke, | |||
className: options.className | |||
className: options.className, | |||
lineType: options.lineType | |||
}); | |||
} | |||
xMarker() {} | |||
yMarker() {} | |||
yMarker(y, label, options={}) { | |||
let labelSvg = createSVG('text', { | |||
className: 'chart-label', | |||
x: this.totalWidth - getStringWidth(label, 5) - LABEL_MARGIN, | |||
y: y - FONT_SIZE - 2, | |||
dy: (FONT_SIZE / 2) + 'px', | |||
'font-size': FONT_SIZE + 'px', | |||
'text-anchor': 'start', | |||
innerHTML: label+"" | |||
}); | |||
let line = makeHoriLine(y, '', 0, this.totalWidth, { | |||
stroke: options.stroke || BASE_LINE_COLOR, | |||
className: options.className || '', | |||
lineType: options.lineType | |||
}); | |||
line.appendChild(labelSvg); | |||
xRegion() {} | |||
yRegion() {} | |||
return line; | |||
} | |||
xRegion() { | |||
return createSVG('rect', { | |||
className: `bar mini`, // remove class | |||
style: `fill: rgba(228, 234, 239, 0.49)`, | |||
// 'data-point-index': index, | |||
x: 0, | |||
y: y2, | |||
width: this.totalWidth, | |||
height: y1 - y2 | |||
}); | |||
return region; | |||
} | |||
yRegion(y1, y2, label) { | |||
// return a group | |||
let rect = createSVG('rect', { | |||
className: `bar mini`, // remove class | |||
style: `fill: rgba(228, 234, 239, 0.49)`, | |||
// 'data-point-index': index, | |||
x: 0, | |||
y: y2, | |||
width: this.totalWidth, | |||
height: y1 - y2 | |||
}); | |||
let upperBorder = createSVG('line', { | |||
className: 'line-horizontal', | |||
x1: 0, | |||
x2: this.totalWidth, | |||
y1: y2, | |||
y2: y2, | |||
styles: { | |||
stroke: BASE_LINE_COLOR | |||
} | |||
}); | |||
let lowerBorder = createSVG('line', { | |||
className: 'line-horizontal', | |||
x1: 0, | |||
x2: this.totalWidth, | |||
y1: y1, | |||
y2: y1, | |||
styles: { | |||
stroke: BASE_LINE_COLOR | |||
} | |||
}); | |||
let labelSvg = createSVG('text', { | |||
className: 'chart-label', | |||
x: this.totalWidth - getStringWidth(label, 4.5) - LABEL_MARGIN, | |||
y: y2 - FONT_SIZE - 2, | |||
dy: (FONT_SIZE / 2) + 'px', | |||
'font-size': FONT_SIZE + 'px', | |||
'text-anchor': 'start', | |||
innerHTML: label+"" | |||
}); | |||
let region = createSVG('g', {}); | |||
region.appendChild(rect); | |||
region.appendChild(upperBorder); | |||
region.appendChild(lowerBorder); | |||
region.appendChild(labelSvg); | |||
return region; | |||
} | |||
animatebar(bar, x, yTop, index, noOfDatasets) { | |||
let start = x - this.avgUnitWidth/4; | |||