[layout-svg] grid measure system for charts

This commit is contained in:
Prateeksha Singh 2018-04-19 03:19:35 +05:30
parent 57c1fcc3a3
commit ea872c10cb
21 changed files with 380 additions and 409 deletions

View File

@ -84,30 +84,40 @@ function fire(target, type, properties) {
// https://css-tricks.com/snippets/javascript/loop-queryselectorall-matches/
const ALL_CHART_TYPES = ['line', 'scatter', 'bar', 'percentage', 'heatmap', 'pie'];
const BASE_MEASURES = {
margins: {
top: 10,
bottom: 10,
left: 20,
right: 20
},
paddings: {
top: 20,
bottom: 40,
left: 30,
right: 10
},
const COMPATIBLE_CHARTS = {
bar: ['line', 'scatter', 'percentage', 'pie'],
line: ['scatter', 'bar', 'percentage', 'pie'],
pie: ['line', 'scatter', 'percentage', 'bar'],
percentage: ['bar', 'line', 'scatter', 'pie'],
heatmap: []
baseHeight: 240,
titleHeight: 20,
legendHeight: 30,
titleFontSize: 12,
};
const DATA_COLOR_DIVISIONS = {
bar: 'datasets',
line: 'datasets',
pie: 'labels',
percentage: 'labels',
heatmap: HEATMAP_DISTRIBUTION_SIZE
};
function getExtraHeight(m) {
let totalExtraHeight = m.margins.top + m.margins.bottom
+ m.paddings.top + m.paddings.bottom
+ m.titleHeight + m.legendHeight;
return totalExtraHeight;
}
const BASE_CHART_TOP_MARGIN = 10;
const BASE_CHART_LEFT_MARGIN = 20;
const BASE_CHART_RIGHT_MARGIN = 20;
function getExtraWidth(m) {
let totalExtraWidth = m.margins.left + m.margins.right
+ m.paddings.left + m.paddings.right;
const Y_AXIS_LEFT_MARGIN = 60;
const Y_AXIS_RIGHT_MARGIN = 40;
return totalExtraWidth;
}
const INIT_CHART_UPDATE_TIMEOUT = 700;
const CHART_POST_ANIMATE_TIMEOUT = 400;
@ -130,9 +140,6 @@ const PERCENTAGE_BAR_DEFAULT_DEPTH = 2;
// More colors are difficult to parse visually
const HEATMAP_DISTRIBUTION_SIZE = 5;
const HEATMAP_LEFT_MARGIN = 50;
const HEATMAP_TOP_MARGIN = 25;
const HEATMAP_SQUARE_SIZE = 10;
const HEATMAP_GUTTER_SIZE = 2;
@ -282,10 +289,6 @@ class SvgTip {
}
}
/**
* Returns the value of a number upto 2 decimal places.
* @param {Number} d Any number
*/
function floatTwo(d) {
return parseFloat(d.toFixed(2));
}
@ -489,12 +492,13 @@ function makeSVGDefs(svgContainer) {
});
}
function makeSVGGroup(parent, className, transform='') {
return createSVG('g', {
function makeSVGGroup(className, transform='', parent=undefined) {
let args = {
className: className,
inside: parent,
transform: transform
});
};
if(parent) args.inside = parent;
return createSVG('g', args);
}
@ -1327,7 +1331,6 @@ class BaseChart {
this.rawChartArgs = options;
this.title = options.title || '';
this.argHeight = options.height || 240;
this.type = options.type || '';
this.realData = this.prepareData(options.data);
@ -1337,10 +1340,18 @@ class BaseChart {
this.config = {
showTooltip: 1, // calculate
showLegend: options.showLegend || 1,
showLegend: 1, // calculate
isNavigable: options.isNavigable || 0,
animate: 1
};
this.measures = JSON.parse(JSON.stringify(BASE_MEASURES));
let m = this.measures;
this.setMeasures(options);
if(!this.title.length) { m.titleHeight = 0; }
if(!this.config.showLegend) m.legendHeight = 0;
this.argHeight = options.height || m.baseHeight;
this.state = {};
this.options = {};
@ -1353,12 +1364,12 @@ class BaseChart {
this.configure(options);
}
configure() {
this.setMargins();
prepareData(data) {
return data;
}
// Bind window events
window.addEventListener('resize', () => this.boundDrawFn);
window.addEventListener('orientationchange', () => this.boundDrawFn);
prepareFirstData(data) {
return data;
}
validateColors(colors, type) {
@ -1375,17 +1386,22 @@ class BaseChart {
return validColors;
}
setMargins() {
let height = this.argHeight;
this.baseHeight = height;
this.height = height - 70;
this.topMargin = BASE_CHART_TOP_MARGIN;
// Horizontal margins
this.leftMargin = BASE_CHART_LEFT_MARGIN;
this.rightMargin = BASE_CHART_RIGHT_MARGIN;
setMeasures() {
// Override measures, including those for title and legend
// set config for legend and title
}
configure() {
let height = this.argHeight;
this.baseHeight = height;
this.height = height - getExtraHeight(this.measures);
// Bind window events
window.addEventListener('resize', () => this.draw(true));
window.addEventListener('orientationchange', () => this.draw(true));
}
// Has to be called manually
setup() {
this.makeContainer();
this.updateWidth();
@ -1394,10 +1410,6 @@ class BaseChart {
this.draw(false, true);
}
setupComponents() {
this.components = new Map();
}
makeContainer() {
// Chart needs a dedicated parent element
this.parent.innerHTML = '';
@ -1444,11 +1456,71 @@ class BaseChart {
this.setupNavigation(init);
}
calc() {} // builds state
updateWidth() {
this.baseWidth = getElementContentWidth(this.parent);
this.width = this.baseWidth - (this.leftMargin + this.rightMargin);
this.width = this.baseWidth - getExtraWidth(this.measures);
}
makeChartArea() {
if(this.svg) {
this.container.removeChild(this.svg);
}
let m = this.measures;
this.svg = makeSVGContainer(
this.container,
'frappe-chart chart',
this.baseWidth,
this.baseHeight
);
this.svgDefs = makeSVGDefs(this.svg);
if(this.title.length) {
this.titleEL = makeText(
'title',
m.margins.left,
m.margins.top,
this.title,
{
fontSize: m.titleFontSize,
fill: '#666666',
dy: m.titleFontSize
}
);
}
let top = m.margins.top + m.titleHeight + m.paddings.top;
this.drawArea = makeSVGGroup(
this.type + '-chart chart-draw-area',
`translate(${m.margins.left + m.paddings.left}, ${top})`
);
if(this.config.showLegend) {
top += this.height + m.paddings.bottom;
this.legendArea = makeSVGGroup(
'chart-legend',
`translate(${m.margins.left + m.paddings.left}, ${top})`
);
}
if(this.title.length) { this.svg.appendChild(this.titleEL); }
this.svg.appendChild(this.drawArea);
if(this.config.showLegend) { this.svg.appendChild(this.legendArea); }
this.updateTipOffset(m.margins.left + m.paddings.left, m.margins.top + m.paddings.top + m.titleHeight);
}
updateTipOffset(x, y) {
this.tip.offset = {
x: x,
y: y
};
}
setupComponents() { this.components = new Map(); }
update(data) {
if(!data) {
console.error('No data to update.');
@ -1458,16 +1530,6 @@ class BaseChart {
this.render();
}
prepareData(data=this.data) {
return data;
}
prepareFirstData(data=this.data) {
return data;
}
calc() {} // builds state
render(components=this.components, animate=true) {
if(this.config.isNavigable) {
// Remove all existing overlays
@ -1498,68 +1560,6 @@ class BaseChart {
}
}
makeChartArea() {
if(this.svg) {
this.container.removeChild(this.svg);
}
let titleAreaHeight = 0;
let legendAreaHeight = 0;
if(this.title.length) {
titleAreaHeight = 40;
}
if(this.config.showLegend) {
legendAreaHeight = 30;
}
this.svg = makeSVGContainer(
this.container,
'frappe-chart chart',
this.baseWidth,
this.baseHeight + titleAreaHeight + legendAreaHeight
);
this.svgDefs = makeSVGDefs(this.svg);
// console.log(this.baseHeight, titleAreaHeight, legendAreaHeight);
if(this.title.length) {
this.titleEL = makeText(
'title',
this.leftMargin - AXIS_TICK_LENGTH * 6,
this.topMargin,
this.title,
{
fontSize: 12,
fill: '#666666'
}
);
this.svg.appendChild(this.titleEL);
}
let top = this.topMargin + titleAreaHeight;
this.drawArea = makeSVGGroup(
this.svg,
this.type + '-chart',
`translate(${this.leftMargin}, ${top})`
);
top = this.baseHeight - titleAreaHeight;
this.legendArea = makeSVGGroup(
this.svg,
'chart-legend',
`translate(${this.leftMargin}, ${top})`
);
this.updateTipOffset(this.leftMargin, this.topMargin + titleAreaHeight);
}
updateTipOffset(x, y) {
this.tip.offset = {
x: x,
y: y
};
}
renderLegend() {}
setupNavigation(init=false) {
@ -1606,39 +1606,13 @@ class BaseChart {
updateDataset() {}
getDifferentChart(type) {
const currentType = this.type;
let args = this.rawChartArgs;
if(type === currentType) return;
if(!ALL_CHART_TYPES.includes(type)) {
console.error(`'${type}' is not a valid chart type.`);
}
if(!COMPATIBLE_CHARTS[currentType].includes(type)) {
console.error(`'${currentType}' chart cannot be converted to a '${type}' chart.`);
}
// whether the new chart can use the existing colors
const useColor = DATA_COLOR_DIVISIONS[currentType] === DATA_COLOR_DIVISIONS[type];
// Okay, this is anticlimactic
// this function will need to actually be 'changeChartType(type)'
// that will update only the required elements, but for now ...
args.type = type;
args.colors = useColor ? args.colors : undefined;
return new Chart(this.parent, args);
}
boundDrawFn() {
this.draw(true);
}
unbindWindowEvents(){
window.removeEventListener('resize', () => this.boundDrawFn);
window.removeEventListener('orientationchange', () => this.boundDrawFn);
window.removeEventListener('resize', () => this.boundDrawFn.bind(this));
window.removeEventListener('orientationchange', () => this.boundDrawFn.bind(this));
}
export() {
@ -1834,7 +1808,7 @@ class ChartComponent {
}
setup(parent) {
this.layer = makeSVGGroup(parent, this.layerClass, this.layerTransform);
this.layer = makeSVGGroup(this.layerClass, this.layerTransform, parent);
}
make() {
@ -2039,9 +2013,9 @@ let componentConfigs = {
data.cols.map((week, weekNo) => {
if(weekNo === 1) {
this.labels.push(
makeText('domain-name', x, monthNameHeight, getMonthName(index, true),
makeText('domain-name', x, monthNameHeight, getMonthName(index, true).toUpperCase(),
{
fontSize: 11
fontSize: 9
}
)
);
@ -2690,26 +2664,26 @@ class Heatmap extends BaseChart {
this.setup();
}
configure(options) {
setMeasures(options) {
let m = this.measures;
this.discreteDomains = options.discreteDomains === 0 ? 0 : 1;
super.configure(options);
}
setMargins() {
super.setMargins();
this.leftMargin = HEATMAP_LEFT_MARGIN;
this.topMargin = HEATMAP_TOP_MARGIN;
m.paddings.top = ROW_HEIGHT * 3;
m.paddings.bottom = 0;
m.legendHeight = ROW_HEIGHT * 2;
m.baseHeight = ROW_HEIGHT * NO_OF_DAYS_IN_WEEK
+ getExtraHeight(m);
let d = this.data;
let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0;
this.independentWidth = (getWeeksBetween(d.start, d.end)
+ spacing) * COL_WIDTH + this.rightMargin + this.leftMargin;
+ spacing) * COL_WIDTH + m.margins.right + m.margins.left;
}
updateWidth() {
let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0;
this.baseWidth = (this.state.noOfWeeks + spacing) * COL_WIDTH
+ this.rightMargin + this.leftMargin;
+ getExtraWidth(this.measures);
}
prepareData(data=this.data) {
@ -2910,7 +2884,7 @@ class Heatmap extends BaseChart {
addDays(startOfWeek, 1);
}
if(col[NO_OF_DAYS_IN_WEEK - 1].dataValue) {
if(col[NO_OF_DAYS_IN_WEEK - 1].dataValue !== undefined) {
addDays(startOfWeek, 1);
cols.push(this.getCol(startOfWeek, month, true));
}
@ -3091,26 +3065,27 @@ class AxisChart extends BaseChart {
this.setup();
}
configure(args) {
super.configure(args);
args.axisOptions = args.axisOptions || {};
args.tooltipOptions = args.tooltipOptions || {};
this.config.xAxisMode = args.axisOptions.xAxisMode || 'span';
this.config.yAxisMode = args.axisOptions.yAxisMode || 'span';
this.config.xIsSeries = args.axisOptions.xIsSeries || 0;
this.config.formatTooltipX = args.tooltipOptions.formatTooltipX;
this.config.formatTooltipY = args.tooltipOptions.formatTooltipY;
this.config.valuesOverPoints = args.valuesOverPoints;
setMeasures(options) {
if(this.data.datasets.length <= 1) {
this.config.showLegend = 0;
this.measures.paddings.bottom = 30;
}
}
setMargins() {
super.setMargins();
this.leftMargin = Y_AXIS_LEFT_MARGIN;
this.rightMargin = Y_AXIS_RIGHT_MARGIN;
configure(options) {
super.configure(options);
options.axisOptions = options.axisOptions || {};
options.tooltipOptions = options.tooltipOptions || {};
this.config.xAxisMode = options.axisOptions.xAxisMode || 'span';
this.config.yAxisMode = options.axisOptions.yAxisMode || 'span';
this.config.xIsSeries = options.axisOptions.xIsSeries || 0;
this.config.formatTooltipX = options.tooltipOptions.formatTooltipX;
this.config.formatTooltipY = options.tooltipOptions.formatTooltipY;
this.config.valuesOverPoints = options.valuesOverPoints;
}
prepareData(data=this.data) {
@ -3434,11 +3409,13 @@ class AxisChart extends BaseChart {
bindTooltip() {
// NOTE: could be in tooltip itself, as it is a given functionality for its parent
this.container.addEventListener('mousemove', (e) => {
let m = this.measures;
let o = getOffset(this.container);
let relX = e.pageX - o.left - this.leftMargin;
let relY = e.pageY - o.top - this.topMargin;
let relX = e.pageX - o.left - m.margins.left - m.paddings.left;
let relY = e.pageY - o.top;
if(relY < this.height + this.topMargin * 2) {
if(relY < this.height + m.titleHeight + m.margins.top + m.paddings.top
&& relY > m.titleHeight + m.margins.top + m.paddings.top) {
this.mapTooltipXPosition(relX);
} else {
this.tip.hideTip();
@ -3452,6 +3429,7 @@ class AxisChart extends BaseChart {
let index = getClosestInArray(relX, s.xAxis.positions, true);
console.log(relX, s.xAxis.positions[index], s.xAxis.positions, this.tip.offset.x);
this.tip.setValues(
s.xAxis.positions[index] + this.tip.offset.x,
s.yExtremes[index] + this.tip.offset.y,
@ -3471,12 +3449,11 @@ class AxisChart extends BaseChart {
renderLegend() {
let s = this.data;
this.legendArea.textContent = '';
if(s.datasets.length > 1) {
this.legendArea.textContent = '';
s.datasets.map((d, i) => {
let barWidth = AXIS_LEGEND_BAR_SIZE;
// let rightEndPoint = this.baseWidth - this.leftMargin - this.rightMargin;
// let rightEndPoint = this.baseWidth - this.measures.margins.left - this.measures.margins.right;
// let multiplier = s.datasets.length - i;
let rect = legendBar(
// rightEndPoint - multiplier * barWidth, // To right align
@ -3638,7 +3615,6 @@ class AxisChart extends BaseChart {
// removeDataPoint(index = 0) {}
}
// import MultiAxisChart from './charts/MultiAxisChart';
const chartTypes = {
bar: AxisChart,
line: AxisChart,

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
.chart-container{position:relative;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif}.chart-container .axis,.chart-container .chart-label{fill:#555b51}.chart-container .axis line,.chart-container .chart-label line{stroke:#dadada}.chart-container .dataset-units circle{stroke:#fff;stroke-width:2}.chart-container .dataset-units path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container .dataset-path{stroke-width:2px}.chart-container .path-group path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container line.dashed{stroke-dasharray:5,3}.chart-container .axis-line .specific-value{text-anchor:start}.chart-container .axis-line .y-line{text-anchor:end}.chart-container .axis-line .x-line{text-anchor:middle}.graph-svg-tip{position:absolute;z-index:1;padding:10px;font-size:12px;color:#959da5;text-align:center;background:rgba(0,0,0,.8);border-radius:3px}.graph-svg-tip ol,.graph-svg-tip ul{padding-left:0;display:-webkit-box;display:-ms-flexbox;display:flex}.graph-svg-tip ul.data-point-list li{min-width:90px;-webkit-box-flex:1;-ms-flex:1;flex:1;font-weight:600}.graph-svg-tip strong{color:#dfe2e5;font-weight:600}.graph-svg-tip .svg-pointer{position:absolute;height:5px;margin:0 0 0 -5px;content:" ";border:5px solid transparent;border-top-color:rgba(0,0,0,.8)}.graph-svg-tip.comparison{padding:0;text-align:left;pointer-events:none}.graph-svg-tip.comparison .title{display:block;padding:10px;margin:0;font-weight:600;line-height:1;pointer-events:none}.graph-svg-tip.comparison ul{margin:0;white-space:nowrap;list-style:none}.graph-svg-tip.comparison li{display:inline-block;padding:5px 10px}
.chart-container{position:relative;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif}.chart-container .axis,.chart-container .chart-label{fill:#555b51}.chart-container .axis line,.chart-container .chart-label line{stroke:#dadada}.chart-container .dataset-units circle{stroke:#fff;stroke-width:2}.chart-container .dataset-units path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container .dataset-path{stroke-width:2px}.chart-container .path-group path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container line.dashed{stroke-dasharray:5,3}.chart-container .axis-line .specific-value{text-anchor:start}.chart-container .axis-line .y-line{text-anchor:end}.chart-container .axis-line .x-line{text-anchor:middle}.chart-container .legend-dataset-text{fill:#6c7680;font-weight:600}.graph-svg-tip{position:absolute;z-index:1;padding:10px;font-size:12px;color:#959da5;text-align:center;background:rgba(0,0,0,.8);border-radius:3px}.graph-svg-tip ol,.graph-svg-tip ul{padding-left:0;display:-webkit-box;display:-ms-flexbox;display:flex}.graph-svg-tip ul.data-point-list li{min-width:90px;-webkit-box-flex:1;-ms-flex:1;flex:1;font-weight:600}.graph-svg-tip strong{color:#dfe2e5;font-weight:600}.graph-svg-tip .svg-pointer{position:absolute;height:5px;margin:0 0 0 -5px;content:" ";border:5px solid transparent;border-top-color:rgba(0,0,0,.8)}.graph-svg-tip.comparison{padding:0;text-align:left;pointer-events:none}.graph-svg-tip.comparison .title{display:block;padding:10px;margin:0;font-weight:600;line-height:1;pointer-events:none}.graph-svg-tip.comparison ul{margin:0;white-space:nowrap;list-style:none}.graph-svg-tip.comparison li{display:inline-block;padding:5px 10px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -29,7 +29,7 @@ let lineCompositeChart = new Chart (c1, {
let barCompositeChart = new Chart (c2, {
data: barCompositeData,
type: 'bar',
height: 190,
height: 210,
colors: ['violet', 'light-blue', '#46a9f9'],
valuesOverPoints: 1,
axisOptions: {
@ -55,7 +55,7 @@ let typeChartArgs = {
title: "My Awesome Chart",
data: typeData,
type: 'axis-mixed',
height: 250,
height: 300,
colors: customColors,
maxLegendPoints: 6,
@ -139,7 +139,7 @@ let updateData = {
let updateChart = new Chart("#chart-update", {
data: updateData,
type: 'line',
height: 250,
height: 300,
colors: ['#ff6c03'],
lineOptions: {
// hideLine: 1,
@ -198,7 +198,7 @@ let plotChartArgs = {
title: "Mean Total Sunspot Count - Yearly",
data: trendsData,
type: 'line',
height: 250,
height: 300,
colors: ['#238e38'],
lineOptions: {
hideDots: 1,
@ -263,7 +263,7 @@ let eventsChart = new Chart("#chart-events", {
title: "Jupiter's Moons: Semi-major Axis (1000 km)",
data: eventsData,
type: 'bar',
height: 250,
height: 330,
colors: ['grey'],
isNavigable: 1,
});
@ -286,8 +286,8 @@ let heatmapArgs = {
title: "Monthly Distribution",
data: heatmapData,
type: 'heatmap',
height: 115,
discreteDomains: 1,
countLabel: 'Level',
colors: HEATMAP_COLORS_BLUE,
legendScale: [0, 1, 2, 4, 5]
};

View File

@ -39,9 +39,6 @@ function __$styleInject(css, ref) {
var HEATMAP_COLORS_BLUE = ['#ebedf0', '#c0ddf9', '#73b3f3', '#3886e1', '#17459e'];
var HEATMAP_COLORS_YELLOW = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'];
@ -319,7 +316,7 @@ var lineCompositeChart = new Chart(c1, {
var barCompositeChart = new Chart(c2, {
data: barCompositeData,
type: 'bar',
height: 190,
height: 210,
colors: ['violet', 'light-blue', '#46a9f9'],
valuesOverPoints: 1,
axisOptions: {
@ -343,7 +340,7 @@ var typeChartArgs = {
title: "My Awesome Chart",
data: typeData,
type: 'axis-mixed',
height: 250,
height: 300,
colors: customColors,
maxLegendPoints: 6,
@ -430,7 +427,7 @@ var updateData = {
var updateChart = new Chart("#chart-update", {
data: updateData,
type: 'line',
height: 250,
height: 300,
colors: ['#ff6c03'],
lineOptions: {
// hideLine: 1,
@ -483,7 +480,7 @@ var plotChartArgs = {
title: "Mean Total Sunspot Count - Yearly",
data: trendsData,
type: 'line',
height: 250,
height: 300,
colors: ['#238e38'],
lineOptions: {
hideDots: 1,
@ -543,7 +540,7 @@ var eventsChart = new Chart("#chart-events", {
title: "Jupiter's Moons: Semi-major Axis (1000 km)",
data: eventsData,
type: 'bar',
height: 250,
height: 330,
colors: ['grey'],
isNavigable: 1
});
@ -566,8 +563,8 @@ var heatmapArgs = {
title: "Monthly Distribution",
data: heatmapData,
type: 'heatmap',
height: 115,
discreteDomains: 1,
countLabel: 'Level',
colors: HEATMAP_COLORS_BLUE,
legendScale: [0, 1, 2, 4, 5]
};

File diff suppressed because one or more lines are too long

View File

@ -27,7 +27,7 @@
<div class="row hero" style="padding-top: 30px; padding-bottom: 0px;">
<div class="jumbotron" style="background: transparent;">
<h1>Frappe Charts</h1>
<p class="mt-2">GitHub-inspired simple and modern charts for the web</p>
<p class="mt-2">GitHub-inspired simple and modern SVG charts for the web</p>
<p class="mt-2">with zero dependencies.</p>
</div>
@ -75,7 +75,7 @@
title: "My Awesome Chart",
type: 'axis-mixed', // or 'bar', 'line', 'pie', 'percentage'
height: 250,
height: 300,
colors: ['purple', '#ffa3ef', 'red']
});
@ -206,21 +206,19 @@
</div>
<pre><code class="hljs javascript margin-vertical-px"> let heatmap = new Chart("#heatmap", {
type: 'heatmap',
height: 115,
data: heatmapData, // object with date/timestamp-value pairs
discreteDomains: 1 // default: 0
start: startDate,
// A Date object;
// default: today's date in past year
// for an annual heatmap
title: "Monthly Distribution",
data: {
dataPoints: {'1524064033': 8, /* ... */},
// object with timestamp-value pairs
start: startDate
end: endDate // Date objects
},
countLabel: 'Level',
discreteDomains: 0 // default: 1
colors: ['#ebedf0', '#c0ddf9', '#73b3f3', '#3886e1', '#17459e'],
// Set of five incremental colors,
// beginning with a low-saturation color for zero data;
// default: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127']
// preferably with a low-saturation color for zero data;
// def: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127']
});</code></pre>
</div>
</div>

View File

@ -18,7 +18,6 @@ import precss from 'precss';
import CleanCSS from 'clean-css';
import autoprefixer from 'autoprefixer';
import fs from 'fs';
import { HEATMAP_LEFT_MARGIN } from './src/js/utils/constants';
fs.readFile('src/css/charts.scss', (err, css) => {
postcss([precss, autoprefixer])

View File

@ -49,6 +49,10 @@
text-anchor: middle;
}
}
.legend-dataset-text {
fill: #6c7680;
font-weight: 600;
}
}
.graph-svg-tip {

View File

@ -1,6 +1,6 @@
import BaseChart from './BaseChart';
import { dataPrep, zeroDataPrep, getShortenedLabels } from '../utils/axis-chart-utils';
import { Y_AXIS_LEFT_MARGIN, Y_AXIS_RIGHT_MARGIN, AXIS_LEGEND_BAR_SIZE } from '../utils/constants';
import { AXIS_LEGEND_BAR_SIZE } from '../utils/constants';
import { getComponent } from '../objects/ChartComponents';
import { getOffset, fire } from '../utils/dom';
import { calcChartIntervals, getIntervalSize, getValueRange, getZeroIndex, scale, getClosestInArray } from '../utils/intervals';
@ -21,26 +21,27 @@ export default class AxisChart extends BaseChart {
this.setup();
}
configure(args) {
super.configure(args);
args.axisOptions = args.axisOptions || {};
args.tooltipOptions = args.tooltipOptions || {};
this.config.xAxisMode = args.axisOptions.xAxisMode || 'span';
this.config.yAxisMode = args.axisOptions.yAxisMode || 'span';
this.config.xIsSeries = args.axisOptions.xIsSeries || 0;
this.config.formatTooltipX = args.tooltipOptions.formatTooltipX;
this.config.formatTooltipY = args.tooltipOptions.formatTooltipY;
this.config.valuesOverPoints = args.valuesOverPoints;
setMeasures(options) {
if(this.data.datasets.length <= 1) {
this.config.showLegend = 0;
this.measures.paddings.bottom = 30;
}
}
setMargins() {
super.setMargins();
this.leftMargin = Y_AXIS_LEFT_MARGIN;
this.rightMargin = Y_AXIS_RIGHT_MARGIN;
configure(options) {
super.configure(options);
options.axisOptions = options.axisOptions || {};
options.tooltipOptions = options.tooltipOptions || {};
this.config.xAxisMode = options.axisOptions.xAxisMode || 'span';
this.config.yAxisMode = options.axisOptions.yAxisMode || 'span';
this.config.xIsSeries = options.axisOptions.xIsSeries || 0;
this.config.formatTooltipX = options.tooltipOptions.formatTooltipX;
this.config.formatTooltipY = options.tooltipOptions.formatTooltipY;
this.config.valuesOverPoints = options.valuesOverPoints;
}
prepareData(data=this.data) {
@ -364,11 +365,13 @@ export default class AxisChart extends BaseChart {
bindTooltip() {
// NOTE: could be in tooltip itself, as it is a given functionality for its parent
this.container.addEventListener('mousemove', (e) => {
let m = this.measures;
let o = getOffset(this.container);
let relX = e.pageX - o.left - this.leftMargin;
let relY = e.pageY - o.top - this.topMargin;
let relX = e.pageX - o.left - m.margins.left - m.paddings.left;
let relY = e.pageY - o.top;
if(relY < this.height + this.topMargin * 2) {
if(relY < this.height + m.titleHeight + m.margins.top + m.paddings.top
&& relY > m.titleHeight + m.margins.top + m.paddings.top) {
this.mapTooltipXPosition(relX);
} else {
this.tip.hideTip();
@ -382,6 +385,7 @@ export default class AxisChart extends BaseChart {
let index = getClosestInArray(relX, s.xAxis.positions, true);
console.log(relX, s.xAxis.positions[index], s.xAxis.positions, this.tip.offset.x);
this.tip.setValues(
s.xAxis.positions[index] + this.tip.offset.x,
s.yExtremes[index] + this.tip.offset.y,
@ -401,12 +405,11 @@ export default class AxisChart extends BaseChart {
renderLegend() {
let s = this.data;
this.legendArea.textContent = '';
if(s.datasets.length > 1) {
this.legendArea.textContent = '';
s.datasets.map((d, i) => {
let barWidth = AXIS_LEGEND_BAR_SIZE;
// let rightEndPoint = this.baseWidth - this.leftMargin - this.rightMargin;
// let rightEndPoint = this.baseWidth - this.measures.margins.left - this.measures.margins.right;
// let multiplier = s.datasets.length - i;
let rect = legendBar(
// rightEndPoint - multiplier * barWidth, // To right align

View File

@ -1,13 +1,11 @@
import SvgTip from '../objects/SvgTip';
import { $, isElementInViewport, getElementContentWidth } from '../utils/dom';
import { makeSVGContainer, makeSVGDefs, makeSVGGroup, makeText, AXIS_TICK_LENGTH } from '../utils/draw';
import { BASE_CHART_TOP_MARGIN, BASE_CHART_LEFT_MARGIN,
BASE_CHART_RIGHT_MARGIN, INIT_CHART_UPDATE_TIMEOUT, CHART_POST_ANIMATE_TIMEOUT, DEFAULT_COLORS,
ALL_CHART_TYPES, COMPATIBLE_CHARTS, DATA_COLOR_DIVISIONS} from '../utils/constants';
import { makeSVGContainer, makeSVGDefs, makeSVGGroup, makeText, yLine } from '../utils/draw';
import { BASE_MEASURES, getExtraHeight, getExtraWidth, INIT_CHART_UPDATE_TIMEOUT, CHART_POST_ANIMATE_TIMEOUT,
DEFAULT_COLORS} from '../utils/constants';
import { getColor, isValidColor } from '../utils/colors';
import { runSMILAnimation } from '../utils/animation';
import { downloadFile, prepareForExport } from '../utils/export';
import { Chart } from '../chart';
export default class BaseChart {
constructor(parent, options) {
@ -23,7 +21,6 @@ export default class BaseChart {
this.rawChartArgs = options;
this.title = options.title || '';
this.argHeight = options.height || 240;
this.type = options.type || '';
this.realData = this.prepareData(options.data);
@ -33,10 +30,18 @@ export default class BaseChart {
this.config = {
showTooltip: 1, // calculate
showLegend: options.showLegend || 1,
showLegend: 1, // calculate
isNavigable: options.isNavigable || 0,
animate: 1
};
this.measures = JSON.parse(JSON.stringify(BASE_MEASURES));
let m = this.measures;
this.setMeasures(options);
if(!this.title.length) { m.titleHeight = 0; }
if(!this.config.showLegend) m.legendHeight = 0;
this.argHeight = options.height || m.baseHeight;
this.state = {};
this.options = {};
@ -49,12 +54,12 @@ export default class BaseChart {
this.configure(options);
}
configure() {
this.setMargins();
prepareData(data) {
return data;
}
// Bind window events
window.addEventListener('resize', () => this.boundDrawFn);
window.addEventListener('orientationchange', () => this.boundDrawFn);
prepareFirstData(data) {
return data;
}
validateColors(colors, type) {
@ -71,17 +76,22 @@ export default class BaseChart {
return validColors;
}
setMargins() {
let height = this.argHeight;
this.baseHeight = height;
this.height = height - 70;
this.topMargin = BASE_CHART_TOP_MARGIN;
// Horizontal margins
this.leftMargin = BASE_CHART_LEFT_MARGIN;
this.rightMargin = BASE_CHART_RIGHT_MARGIN;
setMeasures() {
// Override measures, including those for title and legend
// set config for legend and title
}
configure() {
let height = this.argHeight;
this.baseHeight = height;
this.height = height - getExtraHeight(this.measures);
// Bind window events
window.addEventListener('resize', () => this.draw(true));
window.addEventListener('orientationchange', () => this.draw(true));
}
// Has to be called manually
setup() {
this.makeContainer();
this.updateWidth();
@ -90,10 +100,6 @@ export default class BaseChart {
this.draw(false, true);
}
setupComponents() {
this.components = new Map();
}
makeContainer() {
// Chart needs a dedicated parent element
this.parent.innerHTML = '';
@ -140,11 +146,71 @@ export default class BaseChart {
this.setupNavigation(init);
}
calc() {} // builds state
updateWidth() {
this.baseWidth = getElementContentWidth(this.parent);
this.width = this.baseWidth - (this.leftMargin + this.rightMargin);
this.width = this.baseWidth - getExtraWidth(this.measures);
}
makeChartArea() {
if(this.svg) {
this.container.removeChild(this.svg);
}
let m = this.measures;
this.svg = makeSVGContainer(
this.container,
'frappe-chart chart',
this.baseWidth,
this.baseHeight
);
this.svgDefs = makeSVGDefs(this.svg);
if(this.title.length) {
this.titleEL = makeText(
'title',
m.margins.left,
m.margins.top,
this.title,
{
fontSize: m.titleFontSize,
fill: '#666666',
dy: m.titleFontSize
}
);
}
let top = m.margins.top + m.titleHeight + m.paddings.top;
this.drawArea = makeSVGGroup(
this.type + '-chart chart-draw-area',
`translate(${m.margins.left + m.paddings.left}, ${top})`
);
if(this.config.showLegend) {
top += this.height + m.paddings.bottom;
this.legendArea = makeSVGGroup(
'chart-legend',
`translate(${m.margins.left + m.paddings.left}, ${top})`
);
}
if(this.title.length) { this.svg.appendChild(this.titleEL); }
this.svg.appendChild(this.drawArea);
if(this.config.showLegend) { this.svg.appendChild(this.legendArea); }
this.updateTipOffset(m.margins.left + m.paddings.left, m.margins.top + m.paddings.top + m.titleHeight);
}
updateTipOffset(x, y) {
this.tip.offset = {
x: x,
y: y
};
}
setupComponents() { this.components = new Map(); }
update(data) {
if(!data) {
console.error('No data to update.');
@ -154,16 +220,6 @@ export default class BaseChart {
this.render();
}
prepareData(data=this.data) {
return data;
}
prepareFirstData(data=this.data) {
return data;
}
calc() {} // builds state
render(components=this.components, animate=true) {
if(this.config.isNavigable) {
// Remove all existing overlays
@ -194,68 +250,6 @@ export default class BaseChart {
}
}
makeChartArea() {
if(this.svg) {
this.container.removeChild(this.svg);
}
let titleAreaHeight = 0;
let legendAreaHeight = 0;
if(this.title.length) {
titleAreaHeight = 40;
}
if(this.config.showLegend) {
legendAreaHeight = 30;
}
this.svg = makeSVGContainer(
this.container,
'frappe-chart chart',
this.baseWidth,
this.baseHeight + titleAreaHeight + legendAreaHeight
);
this.svgDefs = makeSVGDefs(this.svg);
// console.log(this.baseHeight, titleAreaHeight, legendAreaHeight);
if(this.title.length) {
this.titleEL = makeText(
'title',
this.leftMargin - AXIS_TICK_LENGTH * 6,
this.topMargin,
this.title,
{
fontSize: 12,
fill: '#666666'
}
);
this.svg.appendChild(this.titleEL);
}
let top = this.topMargin + titleAreaHeight;
this.drawArea = makeSVGGroup(
this.svg,
this.type + '-chart',
`translate(${this.leftMargin}, ${top})`
);
top = this.baseHeight - titleAreaHeight;
this.legendArea = makeSVGGroup(
this.svg,
'chart-legend',
`translate(${this.leftMargin}, ${top})`
);
this.updateTipOffset(this.leftMargin, this.topMargin + titleAreaHeight);
}
updateTipOffset(x, y) {
this.tip.offset = {
x: x,
y: y
};
}
renderLegend() {}
setupNavigation(init=false) {
@ -302,39 +296,13 @@ export default class BaseChart {
updateDataset() {}
getDifferentChart(type) {
const currentType = this.type;
let args = this.rawChartArgs;
if(type === currentType) return;
if(!ALL_CHART_TYPES.includes(type)) {
console.error(`'${type}' is not a valid chart type.`);
}
if(!COMPATIBLE_CHARTS[currentType].includes(type)) {
console.error(`'${currentType}' chart cannot be converted to a '${type}' chart.`);
}
// whether the new chart can use the existing colors
const useColor = DATA_COLOR_DIVISIONS[currentType] === DATA_COLOR_DIVISIONS[type];
// Okay, this is anticlimactic
// this function will need to actually be 'changeChartType(type)'
// that will update only the required elements, but for now ...
args.type = type;
args.colors = useColor ? args.colors : undefined;
return new Chart(this.parent, args);
}
boundDrawFn() {
this.draw(true);
}
unbindWindowEvents(){
window.removeEventListener('resize', () => this.boundDrawFn);
window.removeEventListener('orientationchange', () => this.boundDrawFn);
window.removeEventListener('resize', () => this.boundDrawFn.bind(this));
window.removeEventListener('orientationchange', () => this.boundDrawFn.bind(this));
}
export() {

View File

@ -4,7 +4,7 @@ import { makeText, heatSquare } from '../utils/draw';
import { DAY_NAMES_SHORT, addDays, areInSameMonth, getLastDateInMonth, setDayToSunday, getYyyyMmDd, getWeeksBetween, getMonthName, clone,
NO_OF_MILLIS, NO_OF_YEAR_MONTHS, NO_OF_DAYS_IN_WEEK } from '../utils/date-utils';
import { calcDistribution, getMaxCheckpoint } from '../utils/intervals';
import { HEATMAP_TOP_MARGIN, HEATMAP_LEFT_MARGIN, HEATMAP_DISTRIBUTION_SIZE, HEATMAP_SQUARE_SIZE,
import { getExtraHeight, getExtraWidth, HEATMAP_DISTRIBUTION_SIZE, HEATMAP_SQUARE_SIZE,
HEATMAP_GUTTER_SIZE } from '../utils/constants';
const COL_WIDTH = HEATMAP_SQUARE_SIZE + HEATMAP_GUTTER_SIZE;
@ -26,26 +26,26 @@ export default class Heatmap extends BaseChart {
this.setup();
}
configure(options) {
setMeasures(options) {
let m = this.measures;
this.discreteDomains = options.discreteDomains === 0 ? 0 : 1;
super.configure(options);
}
setMargins() {
super.setMargins();
this.leftMargin = HEATMAP_LEFT_MARGIN;
this.topMargin = HEATMAP_TOP_MARGIN;
m.paddings.top = ROW_HEIGHT * 3;
m.paddings.bottom = 0;
m.legendHeight = ROW_HEIGHT * 2;
m.baseHeight = ROW_HEIGHT * NO_OF_DAYS_IN_WEEK
+ getExtraHeight(m);
let d = this.data;
let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0;
this.independentWidth = (getWeeksBetween(d.start, d.end)
+ spacing) * COL_WIDTH + this.rightMargin + this.leftMargin;
+ spacing) * COL_WIDTH + m.margins.right + m.margins.left;
}
updateWidth() {
let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0;
this.baseWidth = (this.state.noOfWeeks + spacing) * COL_WIDTH
+ this.rightMargin + this.leftMargin;
+ getExtraWidth(this.measures);
}
prepareData(data=this.data) {
@ -246,7 +246,7 @@ export default class Heatmap extends BaseChart {
addDays(startOfWeek, 1);
}
if(col[NO_OF_DAYS_IN_WEEK - 1].dataValue) {
if(col[NO_OF_DAYS_IN_WEEK - 1].dataValue !== undefined) {
addDays(startOfWeek, 1);
cols.push(this.getCol(startOfWeek, month, true));
}

View File

@ -14,11 +14,11 @@ export default class MultiAxisChart extends AxisChart {
this.type = 'multiaxis';
}
setMargins() {
super.setMargins();
setMeasures() {
super.setMeasures();
let noOfLeftAxes = this.data.datasets.filter(d => d.axisPosition === 'left').length;
this.leftMargin = (noOfLeftAxes) * Y_AXIS_MARGIN || Y_AXIS_MARGIN;
this.rightMargin = (this.data.datasets.length - noOfLeftAxes) * Y_AXIS_MARGIN || Y_AXIS_MARGIN;
this.measures.margins.left = (noOfLeftAxes) * Y_AXIS_MARGIN || Y_AXIS_MARGIN;
this.measures.margins.right = (this.data.datasets.length - noOfLeftAxes) * Y_AXIS_MARGIN || Y_AXIS_MARGIN;
}
prepareYAxis() { }

View File

@ -38,7 +38,7 @@ class ChartComponent {
}
setup(parent) {
this.layer = makeSVGGroup(parent, this.layerClass, this.layerTransform);
this.layer = makeSVGGroup(this.layerClass, this.layerTransform, parent);
}
make() {
@ -243,9 +243,9 @@ let componentConfigs = {
data.cols.map((week, weekNo) => {
if(weekNo === 1) {
this.labels.push(
makeText('domain-name', x, monthNameHeight, getMonthName(index, true),
makeText('domain-name', x, monthNameHeight, getMonthName(index, true).toUpperCase(),
{
fontSize: 11
fontSize: 9
}
)
);

View File

@ -16,12 +16,40 @@ export const DATA_COLOR_DIVISIONS = {
heatmap: HEATMAP_DISTRIBUTION_SIZE
};
export const BASE_CHART_TOP_MARGIN = 10;
export const BASE_CHART_LEFT_MARGIN = 20;
export const BASE_CHART_RIGHT_MARGIN = 20;
export const BASE_MEASURES = {
margins: {
top: 10,
bottom: 10,
left: 20,
right: 20
},
paddings: {
top: 20,
bottom: 40,
left: 30,
right: 10
},
export const Y_AXIS_LEFT_MARGIN = 60;
export const Y_AXIS_RIGHT_MARGIN = 40;
baseHeight: 240,
titleHeight: 20,
legendHeight: 30,
titleFontSize: 12,
};
export function getExtraHeight(m) {
let totalExtraHeight = m.margins.top + m.margins.bottom
+ m.paddings.top + m.paddings.bottom
+ m.titleHeight + m.legendHeight;
return totalExtraHeight;
}
export function getExtraWidth(m) {
let totalExtraWidth = m.margins.left + m.margins.right
+ m.paddings.left + m.paddings.right;
return totalExtraWidth;
}
export const INIT_CHART_UPDATE_TIMEOUT = 700;
export const CHART_POST_ANIMATE_TIMEOUT = 400;
@ -44,9 +72,6 @@ export const PERCENTAGE_BAR_DEFAULT_DEPTH = 2;
// More colors are difficult to parse visually
export const HEATMAP_DISTRIBUTION_SIZE = 5;
export const HEATMAP_LEFT_MARGIN = 50;
export const HEATMAP_TOP_MARGIN = 25;
export const HEATMAP_SQUARE_SIZE = 10;
export const HEATMAP_GUTTER_SIZE = 2;

View File

@ -81,12 +81,13 @@ export function makeSVGDefs(svgContainer) {
});
}
export function makeSVGGroup(parent, className, transform='') {
return createSVG('g', {
export function makeSVGGroup(className, transform='', parent=undefined) {
let args = {
className: className,
inside: parent,
transform: transform
});
};
if(parent) args.inside = parent;
return createSVG('g', args);
}
export function wrapInSVGGroup(elements, className='') {