@@ -84,30 +84,40 @@ function fire(target, type, properties) { | |||||
// https://css-tricks.com/snippets/javascript/loop-queryselectorall-matches/ | // https://css-tricks.com/snippets/javascript/loop-queryselectorall-matches/ | ||||
const ALL_CHART_TYPES = ['line', 'scatter', 'bar', 'percentage', 'heatmap', 'pie']; | |||||
const COMPATIBLE_CHARTS = { | |||||
bar: ['line', 'scatter', 'percentage', 'pie'], | |||||
line: ['scatter', 'bar', 'percentage', 'pie'], | |||||
pie: ['line', 'scatter', 'percentage', 'bar'], | |||||
percentage: ['bar', 'line', 'scatter', 'pie'], | |||||
heatmap: [] | |||||
}; | |||||
const BASE_MEASURES = { | |||||
margins: { | |||||
top: 10, | |||||
bottom: 10, | |||||
left: 20, | |||||
right: 20 | |||||
}, | |||||
paddings: { | |||||
top: 20, | |||||
bottom: 40, | |||||
left: 30, | |||||
right: 10 | |||||
}, | |||||
const DATA_COLOR_DIVISIONS = { | |||||
bar: 'datasets', | |||||
line: 'datasets', | |||||
pie: 'labels', | |||||
percentage: 'labels', | |||||
heatmap: HEATMAP_DISTRIBUTION_SIZE | |||||
baseHeight: 240, | |||||
titleHeight: 20, | |||||
legendHeight: 30, | |||||
titleFontSize: 12, | |||||
}; | }; | ||||
const BASE_CHART_TOP_MARGIN = 10; | |||||
const BASE_CHART_LEFT_MARGIN = 20; | |||||
const BASE_CHART_RIGHT_MARGIN = 20; | |||||
function getExtraHeight(m) { | |||||
let totalExtraHeight = m.margins.top + m.margins.bottom | |||||
+ m.paddings.top + m.paddings.bottom | |||||
+ m.titleHeight + m.legendHeight; | |||||
return totalExtraHeight; | |||||
} | |||||
const Y_AXIS_LEFT_MARGIN = 60; | |||||
const Y_AXIS_RIGHT_MARGIN = 40; | |||||
function getExtraWidth(m) { | |||||
let totalExtraWidth = m.margins.left + m.margins.right | |||||
+ m.paddings.left + m.paddings.right; | |||||
return totalExtraWidth; | |||||
} | |||||
const INIT_CHART_UPDATE_TIMEOUT = 700; | const INIT_CHART_UPDATE_TIMEOUT = 700; | ||||
const CHART_POST_ANIMATE_TIMEOUT = 400; | const CHART_POST_ANIMATE_TIMEOUT = 400; | ||||
@@ -130,9 +140,6 @@ const PERCENTAGE_BAR_DEFAULT_DEPTH = 2; | |||||
// More colors are difficult to parse visually | // More colors are difficult to parse visually | ||||
const HEATMAP_DISTRIBUTION_SIZE = 5; | const HEATMAP_DISTRIBUTION_SIZE = 5; | ||||
const HEATMAP_LEFT_MARGIN = 50; | |||||
const HEATMAP_TOP_MARGIN = 25; | |||||
const HEATMAP_SQUARE_SIZE = 10; | const HEATMAP_SQUARE_SIZE = 10; | ||||
const HEATMAP_GUTTER_SIZE = 2; | 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) { | function floatTwo(d) { | ||||
return parseFloat(d.toFixed(2)); | 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, | className: className, | ||||
inside: parent, | |||||
transform: transform | transform: transform | ||||
}); | |||||
}; | |||||
if(parent) args.inside = parent; | |||||
return createSVG('g', args); | |||||
} | } | ||||
@@ -1327,7 +1331,6 @@ class BaseChart { | |||||
this.rawChartArgs = options; | this.rawChartArgs = options; | ||||
this.title = options.title || ''; | this.title = options.title || ''; | ||||
this.argHeight = options.height || 240; | |||||
this.type = options.type || ''; | this.type = options.type || ''; | ||||
this.realData = this.prepareData(options.data); | this.realData = this.prepareData(options.data); | ||||
@@ -1337,10 +1340,18 @@ class BaseChart { | |||||
this.config = { | this.config = { | ||||
showTooltip: 1, // calculate | showTooltip: 1, // calculate | ||||
showLegend: options.showLegend || 1, | |||||
showLegend: 1, // calculate | |||||
isNavigable: options.isNavigable || 0, | isNavigable: options.isNavigable || 0, | ||||
animate: 1 | 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.state = {}; | ||||
this.options = {}; | this.options = {}; | ||||
@@ -1353,12 +1364,12 @@ class BaseChart { | |||||
this.configure(options); | 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) { | validateColors(colors, type) { | ||||
@@ -1375,17 +1386,22 @@ class BaseChart { | |||||
return validColors; | return validColors; | ||||
} | } | ||||
setMargins() { | |||||
setMeasures() { | |||||
// Override measures, including those for title and legend | |||||
// set config for legend and title | |||||
} | |||||
configure() { | |||||
let height = this.argHeight; | let height = this.argHeight; | ||||
this.baseHeight = height; | this.baseHeight = height; | ||||
this.height = height - 70; | |||||
this.topMargin = BASE_CHART_TOP_MARGIN; | |||||
this.height = height - getExtraHeight(this.measures); | |||||
// Horizontal margins | |||||
this.leftMargin = BASE_CHART_LEFT_MARGIN; | |||||
this.rightMargin = BASE_CHART_RIGHT_MARGIN; | |||||
// Bind window events | |||||
window.addEventListener('resize', () => this.draw(true)); | |||||
window.addEventListener('orientationchange', () => this.draw(true)); | |||||
} | } | ||||
// Has to be called manually | |||||
setup() { | setup() { | ||||
this.makeContainer(); | this.makeContainer(); | ||||
this.updateWidth(); | this.updateWidth(); | ||||
@@ -1394,10 +1410,6 @@ class BaseChart { | |||||
this.draw(false, true); | this.draw(false, true); | ||||
} | } | ||||
setupComponents() { | |||||
this.components = new Map(); | |||||
} | |||||
makeContainer() { | makeContainer() { | ||||
// Chart needs a dedicated parent element | // Chart needs a dedicated parent element | ||||
this.parent.innerHTML = ''; | this.parent.innerHTML = ''; | ||||
@@ -1444,11 +1456,71 @@ class BaseChart { | |||||
this.setupNavigation(init); | this.setupNavigation(init); | ||||
} | } | ||||
calc() {} // builds state | |||||
updateWidth() { | updateWidth() { | ||||
this.baseWidth = getElementContentWidth(this.parent); | 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) { | update(data) { | ||||
if(!data) { | if(!data) { | ||||
console.error('No data to update.'); | console.error('No data to update.'); | ||||
@@ -1458,16 +1530,6 @@ class BaseChart { | |||||
this.render(); | this.render(); | ||||
} | } | ||||
prepareData(data=this.data) { | |||||
return data; | |||||
} | |||||
prepareFirstData(data=this.data) { | |||||
return data; | |||||
} | |||||
calc() {} // builds state | |||||
render(components=this.components, animate=true) { | render(components=this.components, animate=true) { | ||||
if(this.config.isNavigable) { | if(this.config.isNavigable) { | ||||
// Remove all existing overlays | // 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() {} | renderLegend() {} | ||||
setupNavigation(init=false) { | setupNavigation(init=false) { | ||||
@@ -1606,39 +1606,13 @@ class BaseChart { | |||||
updateDataset() {} | 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() { | boundDrawFn() { | ||||
this.draw(true); | this.draw(true); | ||||
} | } | ||||
unbindWindowEvents(){ | 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() { | export() { | ||||
@@ -1834,7 +1808,7 @@ class ChartComponent { | |||||
} | } | ||||
setup(parent) { | setup(parent) { | ||||
this.layer = makeSVGGroup(parent, this.layerClass, this.layerTransform); | |||||
this.layer = makeSVGGroup(this.layerClass, this.layerTransform, parent); | |||||
} | } | ||||
make() { | make() { | ||||
@@ -2039,9 +2013,9 @@ let componentConfigs = { | |||||
data.cols.map((week, weekNo) => { | data.cols.map((week, weekNo) => { | ||||
if(weekNo === 1) { | if(weekNo === 1) { | ||||
this.labels.push( | 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(); | this.setup(); | ||||
} | } | ||||
configure(options) { | |||||
setMeasures(options) { | |||||
let m = this.measures; | |||||
this.discreteDomains = options.discreteDomains === 0 ? 0 : 1; | 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 d = this.data; | ||||
let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0; | let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0; | ||||
this.independentWidth = (getWeeksBetween(d.start, d.end) | this.independentWidth = (getWeeksBetween(d.start, d.end) | ||||
+ spacing) * COL_WIDTH + this.rightMargin + this.leftMargin; | |||||
+ spacing) * COL_WIDTH + m.margins.right + m.margins.left; | |||||
} | } | ||||
updateWidth() { | updateWidth() { | ||||
let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0; | let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0; | ||||
this.baseWidth = (this.state.noOfWeeks + spacing) * COL_WIDTH | this.baseWidth = (this.state.noOfWeeks + spacing) * COL_WIDTH | ||||
+ this.rightMargin + this.leftMargin; | |||||
+ getExtraWidth(this.measures); | |||||
} | } | ||||
prepareData(data=this.data) { | prepareData(data=this.data) { | ||||
@@ -2910,7 +2884,7 @@ class Heatmap extends BaseChart { | |||||
addDays(startOfWeek, 1); | 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); | addDays(startOfWeek, 1); | ||||
cols.push(this.getCol(startOfWeek, month, true)); | cols.push(this.getCol(startOfWeek, month, true)); | ||||
} | } | ||||
@@ -3091,26 +3065,27 @@ class AxisChart extends BaseChart { | |||||
this.setup(); | this.setup(); | ||||
} | } | ||||
configure(args) { | |||||
super.configure(args); | |||||
setMeasures(options) { | |||||
if(this.data.datasets.length <= 1) { | |||||
this.config.showLegend = 0; | |||||
this.measures.paddings.bottom = 30; | |||||
} | |||||
} | |||||
args.axisOptions = args.axisOptions || {}; | |||||
args.tooltipOptions = args.tooltipOptions || {}; | |||||
configure(options) { | |||||
super.configure(options); | |||||
this.config.xAxisMode = args.axisOptions.xAxisMode || 'span'; | |||||
this.config.yAxisMode = args.axisOptions.yAxisMode || 'span'; | |||||
this.config.xIsSeries = args.axisOptions.xIsSeries || 0; | |||||
options.axisOptions = options.axisOptions || {}; | |||||
options.tooltipOptions = options.tooltipOptions || {}; | |||||
this.config.formatTooltipX = args.tooltipOptions.formatTooltipX; | |||||
this.config.formatTooltipY = args.tooltipOptions.formatTooltipY; | |||||
this.config.xAxisMode = options.axisOptions.xAxisMode || 'span'; | |||||
this.config.yAxisMode = options.axisOptions.yAxisMode || 'span'; | |||||
this.config.xIsSeries = options.axisOptions.xIsSeries || 0; | |||||
this.config.valuesOverPoints = args.valuesOverPoints; | |||||
} | |||||
this.config.formatTooltipX = options.tooltipOptions.formatTooltipX; | |||||
this.config.formatTooltipY = options.tooltipOptions.formatTooltipY; | |||||
setMargins() { | |||||
super.setMargins(); | |||||
this.leftMargin = Y_AXIS_LEFT_MARGIN; | |||||
this.rightMargin = Y_AXIS_RIGHT_MARGIN; | |||||
this.config.valuesOverPoints = options.valuesOverPoints; | |||||
} | } | ||||
prepareData(data=this.data) { | prepareData(data=this.data) { | ||||
@@ -3434,11 +3409,13 @@ class AxisChart extends BaseChart { | |||||
bindTooltip() { | bindTooltip() { | ||||
// NOTE: could be in tooltip itself, as it is a given functionality for its parent | // NOTE: could be in tooltip itself, as it is a given functionality for its parent | ||||
this.container.addEventListener('mousemove', (e) => { | this.container.addEventListener('mousemove', (e) => { | ||||
let m = this.measures; | |||||
let o = getOffset(this.container); | 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); | this.mapTooltipXPosition(relX); | ||||
} else { | } else { | ||||
this.tip.hideTip(); | this.tip.hideTip(); | ||||
@@ -3452,6 +3429,7 @@ class AxisChart extends BaseChart { | |||||
let index = getClosestInArray(relX, s.xAxis.positions, true); | 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( | this.tip.setValues( | ||||
s.xAxis.positions[index] + this.tip.offset.x, | s.xAxis.positions[index] + this.tip.offset.x, | ||||
s.yExtremes[index] + this.tip.offset.y, | s.yExtremes[index] + this.tip.offset.y, | ||||
@@ -3471,12 +3449,11 @@ class AxisChart extends BaseChart { | |||||
renderLegend() { | renderLegend() { | ||||
let s = this.data; | let s = this.data; | ||||
this.legendArea.textContent = ''; | |||||
if(s.datasets.length > 1) { | if(s.datasets.length > 1) { | ||||
this.legendArea.textContent = ''; | |||||
s.datasets.map((d, i) => { | s.datasets.map((d, i) => { | ||||
let barWidth = AXIS_LEGEND_BAR_SIZE; | 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 multiplier = s.datasets.length - i; | ||||
let rect = legendBar( | let rect = legendBar( | ||||
// rightEndPoint - multiplier * barWidth, // To right align | // rightEndPoint - multiplier * barWidth, // To right align | ||||
@@ -3638,7 +3615,6 @@ class AxisChart extends BaseChart { | |||||
// removeDataPoint(index = 0) {} | // removeDataPoint(index = 0) {} | ||||
} | } | ||||
// import MultiAxisChart from './charts/MultiAxisChart'; | |||||
const chartTypes = { | const chartTypes = { | ||||
bar: AxisChart, | bar: AxisChart, | ||||
line: AxisChart, | line: AxisChart, | ||||
@@ -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} |
@@ -29,7 +29,7 @@ let lineCompositeChart = new Chart (c1, { | |||||
let barCompositeChart = new Chart (c2, { | let barCompositeChart = new Chart (c2, { | ||||
data: barCompositeData, | data: barCompositeData, | ||||
type: 'bar', | type: 'bar', | ||||
height: 190, | |||||
height: 210, | |||||
colors: ['violet', 'light-blue', '#46a9f9'], | colors: ['violet', 'light-blue', '#46a9f9'], | ||||
valuesOverPoints: 1, | valuesOverPoints: 1, | ||||
axisOptions: { | axisOptions: { | ||||
@@ -55,7 +55,7 @@ let typeChartArgs = { | |||||
title: "My Awesome Chart", | title: "My Awesome Chart", | ||||
data: typeData, | data: typeData, | ||||
type: 'axis-mixed', | type: 'axis-mixed', | ||||
height: 250, | |||||
height: 300, | |||||
colors: customColors, | colors: customColors, | ||||
maxLegendPoints: 6, | maxLegendPoints: 6, | ||||
@@ -139,7 +139,7 @@ let updateData = { | |||||
let updateChart = new Chart("#chart-update", { | let updateChart = new Chart("#chart-update", { | ||||
data: updateData, | data: updateData, | ||||
type: 'line', | type: 'line', | ||||
height: 250, | |||||
height: 300, | |||||
colors: ['#ff6c03'], | colors: ['#ff6c03'], | ||||
lineOptions: { | lineOptions: { | ||||
// hideLine: 1, | // hideLine: 1, | ||||
@@ -198,7 +198,7 @@ let plotChartArgs = { | |||||
title: "Mean Total Sunspot Count - Yearly", | title: "Mean Total Sunspot Count - Yearly", | ||||
data: trendsData, | data: trendsData, | ||||
type: 'line', | type: 'line', | ||||
height: 250, | |||||
height: 300, | |||||
colors: ['#238e38'], | colors: ['#238e38'], | ||||
lineOptions: { | lineOptions: { | ||||
hideDots: 1, | hideDots: 1, | ||||
@@ -263,7 +263,7 @@ let eventsChart = new Chart("#chart-events", { | |||||
title: "Jupiter's Moons: Semi-major Axis (1000 km)", | title: "Jupiter's Moons: Semi-major Axis (1000 km)", | ||||
data: eventsData, | data: eventsData, | ||||
type: 'bar', | type: 'bar', | ||||
height: 250, | |||||
height: 330, | |||||
colors: ['grey'], | colors: ['grey'], | ||||
isNavigable: 1, | isNavigable: 1, | ||||
}); | }); | ||||
@@ -286,8 +286,8 @@ let heatmapArgs = { | |||||
title: "Monthly Distribution", | title: "Monthly Distribution", | ||||
data: heatmapData, | data: heatmapData, | ||||
type: 'heatmap', | type: 'heatmap', | ||||
height: 115, | |||||
discreteDomains: 1, | discreteDomains: 1, | ||||
countLabel: 'Level', | |||||
colors: HEATMAP_COLORS_BLUE, | colors: HEATMAP_COLORS_BLUE, | ||||
legendScale: [0, 1, 2, 4, 5] | legendScale: [0, 1, 2, 4, 5] | ||||
}; | }; | ||||
@@ -39,9 +39,6 @@ function __$styleInject(css, ref) { | |||||
var HEATMAP_COLORS_BLUE = ['#ebedf0', '#c0ddf9', '#73b3f3', '#3886e1', '#17459e']; | var HEATMAP_COLORS_BLUE = ['#ebedf0', '#c0ddf9', '#73b3f3', '#3886e1', '#17459e']; | ||||
var HEATMAP_COLORS_YELLOW = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']; | var HEATMAP_COLORS_YELLOW = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']; | ||||
@@ -319,7 +316,7 @@ var lineCompositeChart = new Chart(c1, { | |||||
var barCompositeChart = new Chart(c2, { | var barCompositeChart = new Chart(c2, { | ||||
data: barCompositeData, | data: barCompositeData, | ||||
type: 'bar', | type: 'bar', | ||||
height: 190, | |||||
height: 210, | |||||
colors: ['violet', 'light-blue', '#46a9f9'], | colors: ['violet', 'light-blue', '#46a9f9'], | ||||
valuesOverPoints: 1, | valuesOverPoints: 1, | ||||
axisOptions: { | axisOptions: { | ||||
@@ -343,7 +340,7 @@ var typeChartArgs = { | |||||
title: "My Awesome Chart", | title: "My Awesome Chart", | ||||
data: typeData, | data: typeData, | ||||
type: 'axis-mixed', | type: 'axis-mixed', | ||||
height: 250, | |||||
height: 300, | |||||
colors: customColors, | colors: customColors, | ||||
maxLegendPoints: 6, | maxLegendPoints: 6, | ||||
@@ -430,7 +427,7 @@ var updateData = { | |||||
var updateChart = new Chart("#chart-update", { | var updateChart = new Chart("#chart-update", { | ||||
data: updateData, | data: updateData, | ||||
type: 'line', | type: 'line', | ||||
height: 250, | |||||
height: 300, | |||||
colors: ['#ff6c03'], | colors: ['#ff6c03'], | ||||
lineOptions: { | lineOptions: { | ||||
// hideLine: 1, | // hideLine: 1, | ||||
@@ -483,7 +480,7 @@ var plotChartArgs = { | |||||
title: "Mean Total Sunspot Count - Yearly", | title: "Mean Total Sunspot Count - Yearly", | ||||
data: trendsData, | data: trendsData, | ||||
type: 'line', | type: 'line', | ||||
height: 250, | |||||
height: 300, | |||||
colors: ['#238e38'], | colors: ['#238e38'], | ||||
lineOptions: { | lineOptions: { | ||||
hideDots: 1, | hideDots: 1, | ||||
@@ -543,7 +540,7 @@ var eventsChart = new Chart("#chart-events", { | |||||
title: "Jupiter's Moons: Semi-major Axis (1000 km)", | title: "Jupiter's Moons: Semi-major Axis (1000 km)", | ||||
data: eventsData, | data: eventsData, | ||||
type: 'bar', | type: 'bar', | ||||
height: 250, | |||||
height: 330, | |||||
colors: ['grey'], | colors: ['grey'], | ||||
isNavigable: 1 | isNavigable: 1 | ||||
}); | }); | ||||
@@ -566,8 +563,8 @@ var heatmapArgs = { | |||||
title: "Monthly Distribution", | title: "Monthly Distribution", | ||||
data: heatmapData, | data: heatmapData, | ||||
type: 'heatmap', | type: 'heatmap', | ||||
height: 115, | |||||
discreteDomains: 1, | discreteDomains: 1, | ||||
countLabel: 'Level', | |||||
colors: HEATMAP_COLORS_BLUE, | colors: HEATMAP_COLORS_BLUE, | ||||
legendScale: [0, 1, 2, 4, 5] | legendScale: [0, 1, 2, 4, 5] | ||||
}; | }; | ||||
@@ -27,7 +27,7 @@ | |||||
<div class="row hero" style="padding-top: 30px; padding-bottom: 0px;"> | <div class="row hero" style="padding-top: 30px; padding-bottom: 0px;"> | ||||
<div class="jumbotron" style="background: transparent;"> | <div class="jumbotron" style="background: transparent;"> | ||||
<h1>Frappe Charts</h1> | <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> | <p class="mt-2">with zero dependencies.</p> | ||||
</div> | </div> | ||||
@@ -75,7 +75,7 @@ | |||||
title: "My Awesome Chart", | title: "My Awesome Chart", | ||||
type: 'axis-mixed', // or 'bar', 'line', 'pie', 'percentage' | type: 'axis-mixed', // or 'bar', 'line', 'pie', 'percentage' | ||||
height: 250, | |||||
height: 300, | |||||
colors: ['purple', '#ffa3ef', 'red'] | colors: ['purple', '#ffa3ef', 'red'] | ||||
}); | }); | ||||
@@ -206,21 +206,19 @@ | |||||
</div> | </div> | ||||
<pre><code class="hljs javascript margin-vertical-px"> let heatmap = new Chart("#heatmap", { | <pre><code class="hljs javascript margin-vertical-px"> let heatmap = new Chart("#heatmap", { | ||||
type: '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'], | colors: ['#ebedf0', '#c0ddf9', '#73b3f3', '#3886e1', '#17459e'], | ||||
// Set of five incremental colors, | // 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> | });</code></pre> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
@@ -18,7 +18,6 @@ import precss from 'precss'; | |||||
import CleanCSS from 'clean-css'; | import CleanCSS from 'clean-css'; | ||||
import autoprefixer from 'autoprefixer'; | import autoprefixer from 'autoprefixer'; | ||||
import fs from 'fs'; | import fs from 'fs'; | ||||
import { HEATMAP_LEFT_MARGIN } from './src/js/utils/constants'; | |||||
fs.readFile('src/css/charts.scss', (err, css) => { | fs.readFile('src/css/charts.scss', (err, css) => { | ||||
postcss([precss, autoprefixer]) | postcss([precss, autoprefixer]) | ||||
@@ -49,6 +49,10 @@ | |||||
text-anchor: middle; | text-anchor: middle; | ||||
} | } | ||||
} | } | ||||
.legend-dataset-text { | |||||
fill: #6c7680; | |||||
font-weight: 600; | |||||
} | |||||
} | } | ||||
.graph-svg-tip { | .graph-svg-tip { | ||||
@@ -1,6 +1,6 @@ | |||||
import BaseChart from './BaseChart'; | import BaseChart from './BaseChart'; | ||||
import { dataPrep, zeroDataPrep, getShortenedLabels } from '../utils/axis-chart-utils'; | 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 { getComponent } from '../objects/ChartComponents'; | ||||
import { getOffset, fire } from '../utils/dom'; | import { getOffset, fire } from '../utils/dom'; | ||||
import { calcChartIntervals, getIntervalSize, getValueRange, getZeroIndex, scale, getClosestInArray } from '../utils/intervals'; | import { calcChartIntervals, getIntervalSize, getValueRange, getZeroIndex, scale, getClosestInArray } from '../utils/intervals'; | ||||
@@ -21,26 +21,27 @@ export default class AxisChart extends BaseChart { | |||||
this.setup(); | this.setup(); | ||||
} | } | ||||
configure(args) { | |||||
super.configure(args); | |||||
setMeasures(options) { | |||||
if(this.data.datasets.length <= 1) { | |||||
this.config.showLegend = 0; | |||||
this.measures.paddings.bottom = 30; | |||||
} | |||||
} | |||||
args.axisOptions = args.axisOptions || {}; | |||||
args.tooltipOptions = args.tooltipOptions || {}; | |||||
configure(options) { | |||||
super.configure(options); | |||||
this.config.xAxisMode = args.axisOptions.xAxisMode || 'span'; | |||||
this.config.yAxisMode = args.axisOptions.yAxisMode || 'span'; | |||||
this.config.xIsSeries = args.axisOptions.xIsSeries || 0; | |||||
options.axisOptions = options.axisOptions || {}; | |||||
options.tooltipOptions = options.tooltipOptions || {}; | |||||
this.config.formatTooltipX = args.tooltipOptions.formatTooltipX; | |||||
this.config.formatTooltipY = args.tooltipOptions.formatTooltipY; | |||||
this.config.xAxisMode = options.axisOptions.xAxisMode || 'span'; | |||||
this.config.yAxisMode = options.axisOptions.yAxisMode || 'span'; | |||||
this.config.xIsSeries = options.axisOptions.xIsSeries || 0; | |||||
this.config.valuesOverPoints = args.valuesOverPoints; | |||||
} | |||||
this.config.formatTooltipX = options.tooltipOptions.formatTooltipX; | |||||
this.config.formatTooltipY = options.tooltipOptions.formatTooltipY; | |||||
setMargins() { | |||||
super.setMargins(); | |||||
this.leftMargin = Y_AXIS_LEFT_MARGIN; | |||||
this.rightMargin = Y_AXIS_RIGHT_MARGIN; | |||||
this.config.valuesOverPoints = options.valuesOverPoints; | |||||
} | } | ||||
prepareData(data=this.data) { | prepareData(data=this.data) { | ||||
@@ -364,11 +365,13 @@ export default class AxisChart extends BaseChart { | |||||
bindTooltip() { | bindTooltip() { | ||||
// NOTE: could be in tooltip itself, as it is a given functionality for its parent | // NOTE: could be in tooltip itself, as it is a given functionality for its parent | ||||
this.container.addEventListener('mousemove', (e) => { | this.container.addEventListener('mousemove', (e) => { | ||||
let m = this.measures; | |||||
let o = getOffset(this.container); | 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); | this.mapTooltipXPosition(relX); | ||||
} else { | } else { | ||||
this.tip.hideTip(); | this.tip.hideTip(); | ||||
@@ -382,6 +385,7 @@ export default class AxisChart extends BaseChart { | |||||
let index = getClosestInArray(relX, s.xAxis.positions, true); | 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( | this.tip.setValues( | ||||
s.xAxis.positions[index] + this.tip.offset.x, | s.xAxis.positions[index] + this.tip.offset.x, | ||||
s.yExtremes[index] + this.tip.offset.y, | s.yExtremes[index] + this.tip.offset.y, | ||||
@@ -401,12 +405,11 @@ export default class AxisChart extends BaseChart { | |||||
renderLegend() { | renderLegend() { | ||||
let s = this.data; | let s = this.data; | ||||
this.legendArea.textContent = ''; | |||||
if(s.datasets.length > 1) { | if(s.datasets.length > 1) { | ||||
this.legendArea.textContent = ''; | |||||
s.datasets.map((d, i) => { | s.datasets.map((d, i) => { | ||||
let barWidth = AXIS_LEGEND_BAR_SIZE; | 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 multiplier = s.datasets.length - i; | ||||
let rect = legendBar( | let rect = legendBar( | ||||
// rightEndPoint - multiplier * barWidth, // To right align | // rightEndPoint - multiplier * barWidth, // To right align | ||||
@@ -1,13 +1,11 @@ | |||||
import SvgTip from '../objects/SvgTip'; | import SvgTip from '../objects/SvgTip'; | ||||
import { $, isElementInViewport, getElementContentWidth } from '../utils/dom'; | 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 { getColor, isValidColor } from '../utils/colors'; | ||||
import { runSMILAnimation } from '../utils/animation'; | import { runSMILAnimation } from '../utils/animation'; | ||||
import { downloadFile, prepareForExport } from '../utils/export'; | import { downloadFile, prepareForExport } from '../utils/export'; | ||||
import { Chart } from '../chart'; | |||||
export default class BaseChart { | export default class BaseChart { | ||||
constructor(parent, options) { | constructor(parent, options) { | ||||
@@ -23,7 +21,6 @@ export default class BaseChart { | |||||
this.rawChartArgs = options; | this.rawChartArgs = options; | ||||
this.title = options.title || ''; | this.title = options.title || ''; | ||||
this.argHeight = options.height || 240; | |||||
this.type = options.type || ''; | this.type = options.type || ''; | ||||
this.realData = this.prepareData(options.data); | this.realData = this.prepareData(options.data); | ||||
@@ -33,10 +30,18 @@ export default class BaseChart { | |||||
this.config = { | this.config = { | ||||
showTooltip: 1, // calculate | showTooltip: 1, // calculate | ||||
showLegend: options.showLegend || 1, | |||||
showLegend: 1, // calculate | |||||
isNavigable: options.isNavigable || 0, | isNavigable: options.isNavigable || 0, | ||||
animate: 1 | 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.state = {}; | ||||
this.options = {}; | this.options = {}; | ||||
@@ -49,12 +54,12 @@ export default class BaseChart { | |||||
this.configure(options); | 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) { | validateColors(colors, type) { | ||||
@@ -71,17 +76,22 @@ export default class BaseChart { | |||||
return validColors; | return validColors; | ||||
} | } | ||||
setMargins() { | |||||
setMeasures() { | |||||
// Override measures, including those for title and legend | |||||
// set config for legend and title | |||||
} | |||||
configure() { | |||||
let height = this.argHeight; | let height = this.argHeight; | ||||
this.baseHeight = height; | this.baseHeight = height; | ||||
this.height = height - 70; | |||||
this.topMargin = BASE_CHART_TOP_MARGIN; | |||||
this.height = height - getExtraHeight(this.measures); | |||||
// Horizontal margins | |||||
this.leftMargin = BASE_CHART_LEFT_MARGIN; | |||||
this.rightMargin = BASE_CHART_RIGHT_MARGIN; | |||||
// Bind window events | |||||
window.addEventListener('resize', () => this.draw(true)); | |||||
window.addEventListener('orientationchange', () => this.draw(true)); | |||||
} | } | ||||
// Has to be called manually | |||||
setup() { | setup() { | ||||
this.makeContainer(); | this.makeContainer(); | ||||
this.updateWidth(); | this.updateWidth(); | ||||
@@ -90,10 +100,6 @@ export default class BaseChart { | |||||
this.draw(false, true); | this.draw(false, true); | ||||
} | } | ||||
setupComponents() { | |||||
this.components = new Map(); | |||||
} | |||||
makeContainer() { | makeContainer() { | ||||
// Chart needs a dedicated parent element | // Chart needs a dedicated parent element | ||||
this.parent.innerHTML = ''; | this.parent.innerHTML = ''; | ||||
@@ -140,11 +146,71 @@ export default class BaseChart { | |||||
this.setupNavigation(init); | this.setupNavigation(init); | ||||
} | } | ||||
calc() {} // builds state | |||||
updateWidth() { | updateWidth() { | ||||
this.baseWidth = getElementContentWidth(this.parent); | 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) { | update(data) { | ||||
if(!data) { | if(!data) { | ||||
console.error('No data to update.'); | console.error('No data to update.'); | ||||
@@ -154,16 +220,6 @@ export default class BaseChart { | |||||
this.render(); | this.render(); | ||||
} | } | ||||
prepareData(data=this.data) { | |||||
return data; | |||||
} | |||||
prepareFirstData(data=this.data) { | |||||
return data; | |||||
} | |||||
calc() {} // builds state | |||||
render(components=this.components, animate=true) { | render(components=this.components, animate=true) { | ||||
if(this.config.isNavigable) { | if(this.config.isNavigable) { | ||||
// Remove all existing overlays | // 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() {} | renderLegend() {} | ||||
setupNavigation(init=false) { | setupNavigation(init=false) { | ||||
@@ -302,39 +296,13 @@ export default class BaseChart { | |||||
updateDataset() {} | 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() { | boundDrawFn() { | ||||
this.draw(true); | this.draw(true); | ||||
} | } | ||||
unbindWindowEvents(){ | 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() { | export() { | ||||
@@ -4,7 +4,7 @@ import { makeText, heatSquare } from '../utils/draw'; | |||||
import { DAY_NAMES_SHORT, addDays, areInSameMonth, getLastDateInMonth, setDayToSunday, getYyyyMmDd, getWeeksBetween, getMonthName, clone, | 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'; | NO_OF_MILLIS, NO_OF_YEAR_MONTHS, NO_OF_DAYS_IN_WEEK } from '../utils/date-utils'; | ||||
import { calcDistribution, getMaxCheckpoint } from '../utils/intervals'; | 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'; | HEATMAP_GUTTER_SIZE } from '../utils/constants'; | ||||
const COL_WIDTH = HEATMAP_SQUARE_SIZE + HEATMAP_GUTTER_SIZE; | const COL_WIDTH = HEATMAP_SQUARE_SIZE + HEATMAP_GUTTER_SIZE; | ||||
@@ -26,26 +26,26 @@ export default class Heatmap extends BaseChart { | |||||
this.setup(); | this.setup(); | ||||
} | } | ||||
configure(options) { | |||||
setMeasures(options) { | |||||
let m = this.measures; | |||||
this.discreteDomains = options.discreteDomains === 0 ? 0 : 1; | 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 d = this.data; | ||||
let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0; | let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0; | ||||
this.independentWidth = (getWeeksBetween(d.start, d.end) | this.independentWidth = (getWeeksBetween(d.start, d.end) | ||||
+ spacing) * COL_WIDTH + this.rightMargin + this.leftMargin; | |||||
+ spacing) * COL_WIDTH + m.margins.right + m.margins.left; | |||||
} | } | ||||
updateWidth() { | updateWidth() { | ||||
let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0; | let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0; | ||||
this.baseWidth = (this.state.noOfWeeks + spacing) * COL_WIDTH | this.baseWidth = (this.state.noOfWeeks + spacing) * COL_WIDTH | ||||
+ this.rightMargin + this.leftMargin; | |||||
+ getExtraWidth(this.measures); | |||||
} | } | ||||
prepareData(data=this.data) { | prepareData(data=this.data) { | ||||
@@ -246,7 +246,7 @@ export default class Heatmap extends BaseChart { | |||||
addDays(startOfWeek, 1); | 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); | addDays(startOfWeek, 1); | ||||
cols.push(this.getCol(startOfWeek, month, true)); | cols.push(this.getCol(startOfWeek, month, true)); | ||||
} | } | ||||
@@ -14,11 +14,11 @@ export default class MultiAxisChart extends AxisChart { | |||||
this.type = 'multiaxis'; | this.type = 'multiaxis'; | ||||
} | } | ||||
setMargins() { | |||||
super.setMargins(); | |||||
setMeasures() { | |||||
super.setMeasures(); | |||||
let noOfLeftAxes = this.data.datasets.filter(d => d.axisPosition === 'left').length; | 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() { } | prepareYAxis() { } | ||||
@@ -38,7 +38,7 @@ class ChartComponent { | |||||
} | } | ||||
setup(parent) { | setup(parent) { | ||||
this.layer = makeSVGGroup(parent, this.layerClass, this.layerTransform); | |||||
this.layer = makeSVGGroup(this.layerClass, this.layerTransform, parent); | |||||
} | } | ||||
make() { | make() { | ||||
@@ -243,9 +243,9 @@ let componentConfigs = { | |||||
data.cols.map((week, weekNo) => { | data.cols.map((week, weekNo) => { | ||||
if(weekNo === 1) { | if(weekNo === 1) { | ||||
this.labels.push( | this.labels.push( | ||||
makeText('domain-name', x, monthNameHeight, getMonthName(index, true), | |||||
makeText('domain-name', x, monthNameHeight, getMonthName(index, true).toUpperCase(), | |||||
{ | { | ||||
fontSize: 11 | |||||
fontSize: 9 | |||||
} | } | ||||
) | ) | ||||
); | ); | ||||
@@ -16,12 +16,40 @@ export const DATA_COLOR_DIVISIONS = { | |||||
heatmap: HEATMAP_DISTRIBUTION_SIZE | 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 | |||||
}, | |||||
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 const Y_AXIS_LEFT_MARGIN = 60; | |||||
export const Y_AXIS_RIGHT_MARGIN = 40; | |||||
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 INIT_CHART_UPDATE_TIMEOUT = 700; | ||||
export const CHART_POST_ANIMATE_TIMEOUT = 400; | 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 | // More colors are difficult to parse visually | ||||
export const HEATMAP_DISTRIBUTION_SIZE = 5; | 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_SQUARE_SIZE = 10; | ||||
export const HEATMAP_GUTTER_SIZE = 2; | export const HEATMAP_GUTTER_SIZE = 2; | ||||
@@ -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, | className: className, | ||||
inside: parent, | |||||
transform: transform | transform: transform | ||||
}); | |||||
}; | |||||
if(parent) args.inside = parent; | |||||
return createSVG('g', args); | |||||
} | } | ||||
export function wrapInSVGGroup(elements, className='') { | export function wrapInSVGGroup(elements, className='') { | ||||