Comparing version 0.4.0 to 0.5.0
{ | ||
"name": "mapd3", | ||
"version": "0.4.0", | ||
"version": "0.5.0", | ||
"description": "A fast chart library for the fastest DB", | ||
@@ -5,0 +5,0 @@ "main": "dist/mapd3.js", |
@@ -8,2 +8,4 @@ import Chart from "./charts/chart.js" | ||
import DomainEditor from "./charts/domain-editor.js" | ||
import BrushRangeEditor from "./charts/brush-range-editor.js" | ||
import Label from "./charts/label.js" | ||
import Brush from "./charts/brush.js" | ||
@@ -22,2 +24,4 @@ import Hover from "./charts/hover.js" | ||
DomainEditor, | ||
BrushRangeEditor, | ||
Label, | ||
Brush, | ||
@@ -24,0 +28,0 @@ Hover, |
import * as d3 from "./helpers/d3-service" | ||
export default function Axis (config, cache) { | ||
import {override} from "./helpers/common" | ||
export default function Axis (_container) { | ||
let config = { | ||
margin: { | ||
top: 60, | ||
right: 30, | ||
bottom: 40, | ||
left: 70 | ||
}, | ||
width: 800, | ||
height: 500, | ||
tickSizes: null, | ||
tickPadding: null, | ||
xAxisFormat: null, | ||
yAxisFormat: null, | ||
y2AxisFormat: null, | ||
keyType: null, | ||
yTicks: null, | ||
y2Ticks: null, | ||
xTickSkip: null, | ||
axisTransitionDuration: null, | ||
ease: null, | ||
grid: null, | ||
hoverZoneSize: 30, | ||
tickSpacing: 40 | ||
} | ||
let scales = { | ||
xScale: null, | ||
yScale: null, | ||
yScale2: null, | ||
hasSecondAxis: null | ||
} | ||
const cache = { | ||
container: _container, | ||
background: null, | ||
chartHeight: null, | ||
chartWidth: null, | ||
xAxis: null, | ||
yAxis: null, | ||
yAxis2: null, | ||
horizontalGridLines: null, | ||
verticalGridLines: null | ||
} | ||
function buildSVG () { | ||
cache.chartWidth = config.width - config.margin.left - config.margin.right | ||
cache.chartHeight = config.height - config.margin.top - config.margin.bottom | ||
if (!cache.svg) { | ||
cache.svg = cache.container.append("g") | ||
.classed("axis-group", true) | ||
.style("pointer-events", "none") | ||
cache.svg.append("g").attr("class", "grid-lines-group") | ||
cache.svg.append("g").attr("class", "axis x") | ||
cache.svg.append("g").attr("class", "axis y") | ||
cache.svg.append("g").attr("class", "axis y2") | ||
} | ||
cache.svg.attr("transform", `translate(${config.margin.left}, ${config.margin.top})`) | ||
} | ||
function buildAxis () { | ||
cache.xAxis = d3.axisBottom(cache.xScale) | ||
cache.xAxis = d3.axisBottom(scales.xScale) | ||
.tickSize(config.tickSizes, 0) | ||
@@ -11,10 +78,11 @@ .tickPadding(config.tickPadding) | ||
if (config.keyType === "time") { | ||
const formatter = d3.timeFormat(config.xAxisFormat) | ||
cache.xAxis.tickFormat(formatter) | ||
} else { | ||
cache.xAxis.tickValues(cache.xScale.domain().filter((d, i) => !(i % config.tickSkip))) | ||
if (config.xAxisFormat && config.xAxisFormat !== "auto") { | ||
const formatter = d3.timeFormat(config.xAxisFormat) | ||
cache.xAxis.tickFormat(formatter) | ||
} | ||
} else if (config.keyType === "string") { | ||
cache.xAxis.tickValues(scales.xScale.domain().filter((d, i) => !(i % config.xTickSkip))) | ||
} | ||
cache.yAxis = d3.axisLeft(cache.yScale) | ||
.ticks(config.yTicks) | ||
cache.yAxis = d3.axisLeft(scales.yScale) | ||
.tickSize([config.tickSizes]) | ||
@@ -24,8 +92,17 @@ .tickPadding(config.tickPadding) | ||
if (cache.hasSecondAxis) { | ||
cache.yAxis2 = d3.axisRight(cache.yScale2) | ||
.ticks(config.yTicks) | ||
if (Number.isInteger(config.yTicks)) { | ||
cache.yAxis.ticks(config.yTicks) | ||
} else { | ||
cache.yAxis.ticks(Math.ceil(cache.chartHeight / config.tickSpacing)) | ||
} | ||
if (scales.hasSecondAxis) { | ||
cache.yAxis2 = d3.axisRight(scales.yScale2) | ||
.tickSize([config.tickSizes]) | ||
.tickPadding(config.tickPadding) | ||
.tickFormat(d3.format(config.yAxisFormat)) | ||
.tickFormat(d3.format(config.y2AxisFormat)) | ||
if (!isNaN(config.y2Ticks)) { | ||
cache.yAxis2.ticks(config.y2Ticks) | ||
} | ||
} | ||
@@ -35,8 +112,10 @@ } | ||
function drawAxis () { | ||
cache.svg.select(".x-axis-group .axis.x") | ||
buildSVG() | ||
buildAxis() | ||
cache.svg.select(".axis.x") | ||
.attr("transform", `translate(0, ${cache.chartHeight})`) | ||
.call(cache.xAxis) | ||
cache.svg.select(".y-axis-group.axis.y") | ||
.attr("transform", `translate(${-config.xAxisPadding.left}, 0)`) | ||
cache.svg.select(".axis.y") | ||
.transition() | ||
@@ -47,5 +126,5 @@ .duration(config.axisTransitionDuration) | ||
if (cache.hasSecondAxis) { | ||
cache.svg.select(".y-axis-group2.axis.y") | ||
.attr("transform", `translate(${cache.chartWidth - config.xAxisPadding.right}, 0)`) | ||
if (scales.hasSecondAxis) { | ||
cache.svg.select(".axis.y2") | ||
.attr("transform", `translate(${cache.chartWidth}, 0)`) | ||
.transition() | ||
@@ -56,17 +135,4 @@ .duration(config.axisTransitionDuration) | ||
} | ||
} | ||
function drawAxisTitles () { | ||
cache.svg.select(".y-title") | ||
.text(config.yTitle) | ||
.attr("text-anchor", "middle") | ||
.attr("transform", function transform () { | ||
const textHeight = this.getBBox().height | ||
return `translate(${[textHeight, config.height / 2]}) rotate(-90)` | ||
}) | ||
cache.svg.select(".x-title") | ||
.text(config.xTitle) | ||
.attr("text-anchor", "middle") | ||
.attr("transform", `translate(${[config.width / 2, config.height]})`) | ||
return this | ||
} | ||
@@ -76,5 +142,12 @@ | ||
if (config.grid === "horizontal" || config.grid === "full") { | ||
let ticks = null | ||
if (Number.isInteger(config.yTicks)) { | ||
ticks = config.yTicks | ||
} else { | ||
ticks = Math.ceil(cache.chartHeight / config.tickSpacing) | ||
} | ||
cache.horizontalGridLines = cache.svg.select(".grid-lines-group") | ||
.selectAll("line.horizontal-grid-line") | ||
.data(cache.yScale.ticks(config.yTicks)) | ||
.data(scales.yScale.ticks(ticks)) | ||
@@ -87,6 +160,5 @@ cache.horizontalGridLines.enter() | ||
.duration(config.axisTransitionDuration) | ||
.attr("x1", (-config.xAxisPadding.left)) | ||
.attr("x2", cache.chartWidth) | ||
.attr("y1", cache.yScale) | ||
.attr("y2", cache.yScale) | ||
.attr("y1", scales.yScale) | ||
.attr("y2", scales.yScale) | ||
@@ -109,15 +181,27 @@ cache.horizontalGridLines.exit().remove() | ||
.attr("y2", cache.chartHeight) | ||
.attr("x1", cache.xScale) | ||
.attr("x2", cache.xScale) | ||
.attr("x1", scales.xScale) | ||
.attr("x2", scales.xScale) | ||
cache.verticalGridLines.exit().remove() | ||
} | ||
return this | ||
} | ||
function setConfig (_config) { | ||
config = override(config, _config) | ||
return this | ||
} | ||
function setScales (_scales) { | ||
scales = override(scales, _scales) | ||
return this | ||
} | ||
return { | ||
buildAxis, | ||
setConfig, | ||
setScales, | ||
drawAxis, | ||
drawAxisTitles, | ||
drawGridLines | ||
} | ||
} |
import * as d3 from "./helpers/d3-service" | ||
import {exclusiveToggle, toggleOnOff} from "./interactors" | ||
import {exclusiveToggle} from "./interactors" | ||
export default function Binning (_chart) { | ||
export default function Binning (_container) { | ||
@@ -14,4 +14,6 @@ let config = { | ||
}, | ||
toggle: ["auto"], | ||
exclusiveToggle: ["1y", "1q", "1mo", "1w"], | ||
width: 800, | ||
height: 500, | ||
autoLabel: "auto", | ||
binningToggles: [], | ||
label: "BIN:" | ||
@@ -21,68 +23,98 @@ } | ||
const cache = { | ||
chart: _chart, | ||
svg: null | ||
container: _container, | ||
root: null, | ||
autoItem: null, | ||
binningItems: null, | ||
selectedBin: null, | ||
isAuto: true, | ||
isEnabled: true | ||
} | ||
let chartCache = { | ||
svg: null | ||
} | ||
// events | ||
const dispatcher = d3.dispatch("change") | ||
function render () { | ||
buildSVG() | ||
function buildSVG () { | ||
if (!cache.root) { | ||
cache.root = cache.container.append("div") | ||
.attr("class", "binning-group") | ||
.style("float", "left") | ||
cache.label = cache.root.append("div") | ||
.attr("class", "bin-label") | ||
.text(config.label) | ||
cache.autoItem = cache.root.append("div") | ||
.attr("class", "item item-auto toggleOnOff") | ||
.on("click.select", function click () { | ||
const isSelected = this.classList.contains("selected") | ||
const toggled = !isSelected | ||
setAuto(toggled) | ||
drawBinning() | ||
dispatcher.call("change", this, {name: config.autoLabel, isSelected: toggled}) | ||
}) | ||
.text(config.autoLabel) | ||
cache.binningItems = cache.root.selectAll(".toggleExclusive") | ||
.data(config.binningToggles) | ||
.enter().append("div") | ||
.attr("class", (d) => `item item-${d} toggleExclusive`) | ||
.on("click.select", function click (d) { | ||
setBinning(d) | ||
drawBinning() | ||
const isSelected = this.classList.contains("selected") | ||
dispatcher.call("change", this, {name: d, isSelected}) | ||
}) | ||
.text((d) => d) | ||
} | ||
const LINE_HEIGHT = 20 | ||
cache.root | ||
.style("top", `${config.margin.top - LINE_HEIGHT}px`) | ||
.style("left", `${config.margin.left}px`) | ||
changeBinning(cache.selectedBin) | ||
toggleAuto(cache.isAuto) | ||
} | ||
render() | ||
function buildSVG () { | ||
chartCache = cache.chart.getCache() | ||
setConfig(cache.chart.getConfig()) | ||
function changeBinning (_selectedItemName) { | ||
if (_selectedItemName) { | ||
exclusiveToggle(cache.binningItems, `.item-${_selectedItemName}`) | ||
} | ||
} | ||
if (!cache.svg) { | ||
cache.svg = chartCache.svg.append("g") | ||
.classed("binning-group", true) | ||
.append("text") | ||
function toggleAuto (_shouldBeSelected) { | ||
cache.autoItem | ||
.classed("selected", _shouldBeSelected) | ||
.classed("dimmed", !_shouldBeSelected) | ||
} | ||
cache.svg.append("tspan") | ||
.text(config.label) | ||
.attr("y", "1em") | ||
.attr("class", "item") | ||
function drawBinning () { | ||
if (cache.isEnabled) { | ||
buildSVG() | ||
} else { | ||
destroy() | ||
} | ||
return this | ||
} | ||
cache.svg.attr("transform", `translate(${[config.margin.left, 0]})`) | ||
function setVisibility (_shouldBeVisible) { | ||
cache.isEnabled = _shouldBeVisible | ||
drawBinning() | ||
return this | ||
} | ||
const items = cache.svg.selectAll(".toggleOnOff") | ||
.data(config.toggle) | ||
items.enter().append("tspan") | ||
.attr("class", (d) => `item ${d} toggleOnOff`) | ||
.attr("dx", "0.8em") | ||
.attr("y", "1em") | ||
.on("click.select", toggleOnOff(".binning-group .item.toggleOnOff")) | ||
.on("click.d3.dispatch", function click (d) { | ||
const isSelected = this.classList.contains("selected") | ||
dispatcher.call("change", this, d, {isSelected}) | ||
}) | ||
.merge(items) | ||
.text((d) => d) | ||
items.exit().remove() | ||
function setBinning (_selectedBin) { | ||
cache.selectedBin = _selectedBin | ||
return this | ||
} | ||
const itemsExclusive = cache.svg.selectAll(".toggleExclusive") | ||
.data(config.exclusiveToggle) | ||
itemsExclusive.enter().append("tspan") | ||
.attr("class", (d) => `item ${d} toggleExclusive`) | ||
.attr("dx", "0.8em") | ||
.attr("y", "1em") | ||
.on("click.select", exclusiveToggle(".binning-group .item.toggleExclusive")) | ||
.on("click.d3.dispatch", function click (d) { | ||
const isSelected = this.classList.contains("selected") | ||
dispatcher.call("change", this, d, {isSelected}) | ||
}) | ||
.merge(itemsExclusive) | ||
.text((d) => d) | ||
itemsExclusive.exit().remove() | ||
function setAuto (_isAuto) { | ||
cache.isAuto = _isAuto | ||
return this | ||
} | ||
function on (...args) { | ||
return dispatcher.on(...args) | ||
dispatcher.on(...args) | ||
return this | ||
} | ||
@@ -95,22 +127,15 @@ | ||
function getCache () { | ||
return cache | ||
} | ||
function destroy () { | ||
cache.svg.remove() | ||
cache.root.remove() | ||
} | ||
function update () { | ||
render() | ||
return this | ||
} | ||
return { | ||
getCache, | ||
on, | ||
setConfig, | ||
destroy, | ||
update | ||
drawBinning, | ||
setBinning, | ||
setAuto, | ||
setVisibility | ||
} | ||
} |
import * as d3 from "./helpers/d3-service" | ||
import {keys} from "./helpers/constants" | ||
import {cloneData, invertScale, sortData} from "./helpers/common" | ||
import {cloneData, invertScale, sortData, override} from "./helpers/common" | ||
export default function Brush (_chart) { | ||
export default function Brush (_container) { | ||
@@ -16,10 +16,12 @@ let config = { | ||
width: 800, | ||
height: 500 | ||
height: 500, | ||
keyType: null | ||
} | ||
let scales = { | ||
xScale: null | ||
} | ||
const cache = { | ||
chart: _chart, | ||
svg: null, | ||
chartWidth: null, | ||
chartHeight: null, | ||
container: _container, | ||
dateRange: [null, null], | ||
@@ -29,10 +31,9 @@ brush: null, | ||
handle: null, | ||
data: null, | ||
xScale: null | ||
chartWidth: null, | ||
chartHeight: null, | ||
isEnabled: true | ||
} | ||
let chartCache = { | ||
xScale: null, | ||
dataBySeries: null, | ||
svg: null | ||
let data = { | ||
dataBySeries: null | ||
} | ||
@@ -43,31 +44,10 @@ | ||
function init () { | ||
render() | ||
} | ||
init() | ||
function render () { | ||
buildSVG() | ||
if (chartCache.dataBySeries) { | ||
cache.data = extractBrushDimension(cloneData(chartCache.dataBySeries)) | ||
buildScales() | ||
buildBrush() | ||
drawBrush() | ||
} | ||
} | ||
function buildSVG () { | ||
chartCache = cache.chart.getCache() | ||
setConfig(cache.chart.getConfig()) | ||
cache.chartWidth = Math.max(config.width - config.margin.left - config.margin.right, 0) | ||
cache.chartHeight = Math.max(config.height - config.margin.top - config.margin.bottom, 0) | ||
cache.chartWidth = config.width - config.margin.left - config.margin.right | ||
cache.chartHeight = config.height - config.margin.top - config.margin.bottom | ||
if (!cache.svg) { | ||
cache.svg = chartCache.svg.append("g") | ||
cache.svg = cache.container.append("g") | ||
.classed("brush-group", true) | ||
} | ||
cache.svg.attr("transform", `translate(${config.margin.left}, ${config.margin.top})`) | ||
} | ||
@@ -80,6 +60,2 @@ | ||
function buildScales () { | ||
cache.xScale = chartCache.xScale | ||
} | ||
function buildBrush () { | ||
@@ -92,5 +68,3 @@ cache.brush = cache.brush || d3.brushX() | ||
cache.brush.extent([[0, 0], [cache.chartWidth, cache.chartHeight]]) | ||
} | ||
function drawBrush () { | ||
cache.chartBrush = cache.svg.call(cache.brush) | ||
@@ -104,3 +78,3 @@ | ||
const selection = d3.event.selection | ||
const dataExtent = selection.map((d) => invertScale(chartCache.xScale, d, config.keyType)) | ||
const dataExtent = selection.map((d) => invertScale(scales.xScale, d, config.keyType)) | ||
return dataExtent | ||
@@ -127,3 +101,3 @@ } | ||
.transition() | ||
.call(d3.event.target.move, dataExtent.map(cache.xScale)) | ||
.call(d3.event.target.move, dataExtent.map(scales.xScale)) | ||
@@ -133,2 +107,17 @@ dispatcher.call("brushEnd", this, dataExtent, config) | ||
function drawBrush () { | ||
if (!cache.isEnabled) { | ||
destroy() | ||
} | ||
buildSVG() | ||
if (data.dataBySeries) { | ||
cache.data = extractBrushDimension(cloneData(data.dataBySeries)) | ||
buildBrush() | ||
} | ||
return this | ||
} | ||
// function setBrushByPercentages (_a, _b) { | ||
@@ -180,20 +169,29 @@ // const x0 = _a * cache.chartWidth | ||
function on (...args) { | ||
return dispatcher.on(...args) | ||
dispatcher.on(...args) | ||
return this | ||
} | ||
function setVisibility (_isEnabled) { | ||
cache.isEnabled = _isEnabled | ||
drawBrush() | ||
return this | ||
} | ||
function setConfig (_config) { | ||
config = Object.assign({}, config, _config) | ||
config = override(config, _config) | ||
return this | ||
} | ||
function getCache () { | ||
return cache | ||
function setScales (_scales) { | ||
scales = override(scales, _scales) | ||
return this | ||
} | ||
function destroy () { | ||
cache.svg.remove() | ||
function setData (_data) { | ||
data = Object.assign({}, data, _data) | ||
return this | ||
} | ||
function update () { | ||
render() | ||
function destroy (_data) { | ||
cache.svg.remove() | ||
return this | ||
@@ -203,8 +201,10 @@ } | ||
return { | ||
getCache, | ||
on, | ||
setConfig, | ||
destroy, | ||
update | ||
setData, | ||
setScales, | ||
drawBrush, | ||
setVisibility, | ||
destroy | ||
} | ||
} |
import * as d3 from "./helpers/d3-service" | ||
import {exportChart} from "./helpers/exportChart" | ||
import {colors} from "./helpers/colors" | ||
import {keys} from "./helpers/constants" | ||
import {cloneData, getUnique, invertScale, sortData} from "./helpers/common" | ||
import {cloneData, invertScale, sortData, override, throttle, rebind} from "./helpers/common" | ||
import Scale from "./scale" | ||
import Line from "./line" | ||
import Bar from "./bar" | ||
import Axis from "./axis" | ||
import Tooltip from "./tooltip" | ||
import Legend from "./legend" | ||
import Brush from "./brush" | ||
import Hover from "./hover" | ||
import Binning from "./binning" | ||
import DomainEditor from "./domain-editor" | ||
import BrushRangeEditor from "./brush-range-editor" | ||
import Label from "./label" | ||
@@ -16,2 +22,3 @@ export default function Chart (_container) { | ||
let config = { | ||
// common | ||
margin: { | ||
@@ -25,25 +32,25 @@ top: 48, | ||
height: 500, | ||
xAxisPadding: { | ||
top: 0, | ||
left: 0, | ||
bottom: 0, | ||
right: 0 | ||
}, | ||
tickPadding: 5, | ||
colorSchema: colors.mapdColors.map((d) => ({value: d})), | ||
dotRadius: 4, | ||
xAxisFormat: "%c", | ||
tickSkip: 1, | ||
tickSizes: 8, | ||
defaultColor: "skyblue", | ||
keyType: "time", | ||
chartType: "line", // line, area, stackedLine, stackedArea | ||
ease: d3.easeLinear, | ||
// intro animation | ||
isAnimated: false, | ||
ease: d3.easeLinear, | ||
animationDuration: 1500, | ||
axisTransitionDuration: 0, | ||
yTicks: 5, | ||
yTicks2: 5, | ||
// scale | ||
colorSchema: colors.mapdColors.map((d) => ({value: d})), | ||
defaultColor: "skyblue", | ||
// axis | ||
tickPadding: 5, | ||
xAxisFormat: "auto", | ||
yAxisFormat: ".2f", | ||
yAxisFormat2: ".2f", | ||
y2AxisFormat: ".2f", | ||
tickSizes: 8, | ||
yTicks: "auto", | ||
y2Ticks: "auto", | ||
xTickSkip: 0, | ||
grid: null, | ||
axisTransitionDuration: 0, | ||
@@ -53,25 +60,70 @@ xTitle: "", | ||
keyType: "time", | ||
chartType: "line" // line, area, stackedLine, stackedArea | ||
// hover | ||
dotRadius: 4, | ||
// tooltip | ||
valueFormat: ".2f", | ||
mouseChaseDuration: 0, | ||
tooltipEase: d3.easeQuadInOut, | ||
tooltipHeight: 48, | ||
tooltipWidth: 160, | ||
dateFormat: "%b %d, %Y", | ||
seriesOrder: [], | ||
// legend | ||
legendXPosition: "auto", | ||
legendYPosition: "auto", | ||
legendTitle: "", | ||
legendIsEnabled: true, | ||
// binning | ||
binningResolution: "1mo", | ||
binningIsAuto: true, | ||
binningToggles: ["10y", "1y", "1q", "1mo"], | ||
binningIsEnabled: true, | ||
// domain | ||
xDomain: null, | ||
yDomain: null, | ||
y2Domain: null, | ||
domainEditorIsEnabled: true, | ||
// brush range | ||
brushRangeMin: null, | ||
brushRangeMax: null, | ||
brushRangeIsEnabled: true, | ||
// brush | ||
brushIsEnabled: true, | ||
// label | ||
xLabel: "", | ||
yLabel: "", | ||
y2Label: "" | ||
} | ||
let scales = { | ||
xScale: null, | ||
yScale: null, | ||
yScale2: null, | ||
hasSecondAxis: null, | ||
colorScale: null | ||
} | ||
const cache = { | ||
container: _container, | ||
svg: null, | ||
panel: null, | ||
margin: null, | ||
maskingRectangle: null, | ||
verticalGridLines: null, | ||
horizontalGridLines: null, | ||
grid: null, | ||
verticalMarkerContainer: null, | ||
verticalMarkerLine: null, | ||
chartWidth: null, chartHeight: null, | ||
xAxis: null, yAxis: null, yAxis2: null | ||
} | ||
const dataObject = { | ||
dataBySeries: null, | ||
dataByKey: null, | ||
data: null, | ||
chartWidth: null, chartHeight: null, | ||
xScale: null, yScale: null, yScale2: null, colorScale: null, | ||
xAxis: null, yAxis: null, yAxis2: null, | ||
groupKeys: [], | ||
groupKeys: {}, | ||
hasSecondAxis: false, | ||
stackData: null, | ||
@@ -82,27 +134,16 @@ stack: null, | ||
const components = { | ||
scale: null, | ||
axis: null, | ||
line: null, | ||
bar: null, | ||
hover: null | ||
} | ||
const components = {} | ||
const eventCollector = {} | ||
// accessors | ||
const getKey = (d) => d[keys.DATA] | ||
const getGroup = (d) => d[keys.GROUP] | ||
const getID = (d) => d[keys.ID] | ||
// events | ||
const dispatcher = d3.dispatch("mouseOver", "mouseOut", "mouseMove") | ||
const dispatcher = d3.dispatch("mouseOverPanel", "mouseOutPanel", "mouseMovePanel") | ||
function init () { | ||
render() | ||
addMouseEvents() | ||
} | ||
init() | ||
function render () { | ||
buildSVG() | ||
if (cache.dataBySeries) { | ||
if (dataObject.dataBySeries) { | ||
buildChart() | ||
@@ -115,33 +156,76 @@ } | ||
function buildSVG () { | ||
const w = config.width || cache.container.clientWidth | ||
const h = config.height || cache.container.clientHeight | ||
cache.chartWidth = w - config.margin.left - config.margin.right | ||
cache.chartHeight = h - config.margin.top - config.margin.bottom | ||
const w = config.width === "auto" ? cache.container.clientWidth : config.width | ||
const h = config.height === "auto" ? cache.container.clientHeight : config.height | ||
cache.chartWidth = Math.max(w - config.margin.left - config.margin.right, 0) | ||
cache.chartHeight = Math.max(h - config.margin.top - config.margin.bottom, 0) | ||
console.log("cache.chartWidth", cache.chartWidth, w, config.width) | ||
if (!cache.svg) { | ||
const template = `<svg class="mapd3 line-chart"> | ||
<g class="container-group"> | ||
<g class="grid-lines-group"></g> | ||
<g class="x-axis-group"> | ||
<g class="axis x"></g> | ||
const template = `<div class="mapd3 mapd3-container"> | ||
<div class="header-group"></div> | ||
<svg class="chart"> | ||
<g class="chart-group"></g> | ||
<g class="panel-group"> | ||
<rect class="panel-background"></rect> | ||
</g> | ||
<g class="y-axis-group axis y"></g> | ||
<g class="y-axis-group2 axis y"></g> | ||
<g class="chart-group"></g> | ||
</g> | ||
<text class="x-title"></text> | ||
<text class="y-title"></text> | ||
<rect class="masking-rectangle"></rect> | ||
</svg>` | ||
<rect class="masking-rectangle"></rect> | ||
</svg> | ||
</div>` | ||
cache.svg = d3.select(cache.container) | ||
const base = d3.select(cache.container) | ||
.html(template) | ||
.select("svg") | ||
cache.container = base.select(".mapd3-container") | ||
.style("position", "relative") | ||
cache.svg = base.select("svg") | ||
cache.headerGroup = base.select(".header-group") | ||
.style("position", "absolute") | ||
cache.panel = cache.svg.select(".panel-group") | ||
cache.chart = cache.svg.select(".chart-group") | ||
addEvents() | ||
Object.assign(components, { | ||
scale: Scale(), | ||
axis: Axis(cache.chart), | ||
line: Line(cache.panel), | ||
tooltip: Tooltip(cache.container), | ||
legend: Legend(cache.container), | ||
brush: Brush(cache.panel), | ||
hover: Hover(cache.panel), | ||
binning: Binning(cache.headerGroup), | ||
domainEditor: DomainEditor(cache.container), | ||
brushRangeEditor: BrushRangeEditor(cache.headerGroup), | ||
label: Label(cache.container) | ||
}) | ||
Object.assign(eventCollector, { | ||
onBrush: rebind(components.brush), | ||
onHover: rebind(components.hover), | ||
onBinning: rebind(components.binning), | ||
onDomainEditor: rebind(components.domainEditor), | ||
onBrushRangeEditor: rebind(components.brushRangeEditor), | ||
onLabel: rebind(components.label), | ||
onPanel: rebind(dispatcher) | ||
}) | ||
} | ||
cache.svg.attr("width", config.width) | ||
cache.svg | ||
.attr("width", config.width) | ||
.attr("height", config.height) | ||
.select(".container-group") | ||
cache.headerGroup | ||
.style("width", `${cache.chartWidth}px`) | ||
.style("left", `${config.margin.left}px`) | ||
cache.panel | ||
.attr("transform", `translate(${config.margin.left},${config.margin.top})`) | ||
.select(".panel-background") | ||
.attr("width", cache.chartWidth) | ||
.attr("height", cache.chartHeight) | ||
.attr("fill", "transparent") | ||
return this | ||
@@ -151,34 +235,85 @@ } | ||
function buildChart () { | ||
components.scale = Scale(config, cache) | ||
components.line = Line(config, cache) | ||
components.bar = Bar(config, cache) | ||
components.axis = Axis(config, cache) | ||
components.scale | ||
.setConfig(config) | ||
.setData(dataObject) | ||
scales = components.scale.getScales() | ||
if (config.chartType === "stackedLine" | ||
|| config.chartType === "stackedArea" | ||
|| config.chartType === "stackedBar") { | ||
components.scale.buildStackedScales() | ||
} else { | ||
components.scale.buildScales() | ||
} | ||
components.axis | ||
.setConfig(config) | ||
.setScales(scales) | ||
.drawAxis() | ||
.drawGridLines() | ||
components.axis.buildAxis() | ||
components.axis.drawGridLines() | ||
components.axis.drawAxis() | ||
components.axis.drawAxisTitles() | ||
components.line | ||
.setConfig(config) | ||
.setScales(scales) | ||
.setData(dataObject) | ||
.drawMarks() | ||
if (config.chartType === "area") { | ||
components.line.drawAreas() | ||
} else if (config.chartType === "line") { | ||
components.line.drawLines() | ||
} else if (config.chartType === "stackedArea") { | ||
components.line.drawStackedAreas() | ||
} else if (config.chartType === "bar") { | ||
components.bar.drawBars() | ||
} else if (config.chartType === "stackedBar") { | ||
components.bar.drawStackedBars() | ||
} | ||
components.tooltip | ||
.setConfig(config) | ||
.setScales(scales) | ||
.bindEvents(dispatcher) | ||
.setVisibility(config.tooltipIsEnabled) | ||
const legendContent = dataObject.dataBySeries | ||
.map((d) => ({ | ||
id: d.id, | ||
key: d.key, | ||
label: d.label | ||
})) | ||
components.legend | ||
.setConfig(config) | ||
.setScales(scales) | ||
.setTitle(config.legendTitle) | ||
.setContent(legendContent) | ||
.setXPosition(config.legendXPosition) | ||
.setYPosition(config.legendYPosition) | ||
.drawTooltip() | ||
.setVisibility(config.legendIsEnabled) | ||
components.brush | ||
.setConfig(config) | ||
.setScales(scales) | ||
.setData(dataObject) | ||
.drawBrush() | ||
.setVisibility(config.brushIsEnabled) | ||
components.hover | ||
.setConfig(config) | ||
.setScales(scales) | ||
.setData(dataObject) | ||
.bindEvents(dispatcher) | ||
components.binning | ||
.setConfig(config) | ||
.setBinning(config.binningResolution) | ||
.setAuto(config.binningIsAuto) | ||
.drawBinning() | ||
.setVisibility(config.binningIsEnabled) | ||
components.domainEditor | ||
.setConfig(config) | ||
.setXDomain(config.xDomain) | ||
.setYDomain(config.yDomain) | ||
.setY2Domain(config.y2Domain) | ||
.drawDomainEditor() | ||
.setVisibility(config.domainEditorIsEnabled) | ||
components.brushRangeEditor | ||
.setConfig(config) | ||
.setRangeMin(config.brushRangeMin) | ||
.setRangeMax(config.brushRangeMax) | ||
.drawRangeEditor() | ||
.setVisibility(config.brushRangeIsEnabled) | ||
components.label | ||
.setConfig(config) | ||
.setXLabels(config.xLabel) | ||
.setYLabels(config.yLabel) | ||
.setY2Labels(config.y2Label) | ||
.drawLabels() | ||
triggerIntroAnimation() | ||
return this | ||
@@ -188,9 +323,7 @@ } | ||
function setData (_data) { | ||
cache.data = cloneData(_data[keys.SERIES]) | ||
dataObject.data = cloneData(_data[keys.SERIES]) | ||
const cleanedData = cleanData(_data) | ||
cache.dataBySeries = cleanedData.dataBySeries | ||
cache.dataByKey = cleanedData.dataByKey | ||
Object.assign(dataObject, cleanedData) | ||
render() | ||
return this | ||
@@ -224,7 +357,7 @@ } | ||
cache.flatDataSorted = sortData(flatData, config.keyType) | ||
const flatDataSorted = sortData(flatData, config.keyType) | ||
const dataByKey = d3.nest() | ||
.key(getKey) | ||
.entries(cache.flatDataSorted) | ||
.entries(flatDataSorted) | ||
.map((d) => { | ||
@@ -237,6 +370,28 @@ const dataPoint = {} | ||
const allGroupKeys = dataBySeries.map(getGroup) | ||
cache.groupKeys = getUnique(allGroupKeys) | ||
const groupKeys = {} | ||
dataBySeries.forEach((d) => { | ||
if (!groupKeys[d[keys.GROUP]]) { | ||
groupKeys[d[keys.GROUP]] = [] | ||
} | ||
groupKeys[d[keys.GROUP]].push(d[keys.ID]) | ||
}) | ||
return {dataBySeries, dataByKey} | ||
const stackData = dataByKey | ||
.map((d) => { | ||
const points = { | ||
key: d[keys.DATA] | ||
} | ||
d.series.forEach((dB) => { | ||
points[dB[keys.ID]] = dB[keys.VALUE] | ||
}) | ||
return points | ||
}) | ||
const stack = d3.stack() | ||
.keys(dataBySeries.map(getID)) | ||
.order(d3.stackOrderNone) | ||
.offset(d3.stackOffsetNone) | ||
return {dataBySeries, dataByKey, stack, stackData, flatDataSorted, groupKeys} | ||
} | ||
@@ -246,3 +401,3 @@ | ||
if (config.isAnimated) { | ||
cache.maskingRectangle = cache.svg.d3.select(".masking-rectangle") | ||
cache.maskingRectangle = cache.svg.select(".masking-rectangle") | ||
.attr("width", cache.chartWidth - 2) | ||
@@ -263,10 +418,17 @@ .attr("height", cache.chartHeight) | ||
function getNearestDataPoint (_mouseX) { | ||
const keyFromInvertedX = invertScale(cache.xScale, _mouseX, config.keyType) | ||
const keyFromInvertedX = invertScale(scales.xScale, _mouseX, config.keyType) | ||
const bisectLeft = d3.bisector(getKey).left | ||
const dataEntryIndex = bisectLeft(cache.dataByKey, keyFromInvertedX) | ||
const dataEntryForXPosition = cache.dataByKey[dataEntryIndex] | ||
const dataEntryIndex = bisectLeft(dataObject.dataByKey, keyFromInvertedX) | ||
const dataEntryForXPosition = dataObject.dataByKey[dataEntryIndex] | ||
const dataEntryForXPositionPrev = dataObject.dataByKey[Math.max(dataEntryIndex - 1, 0)] | ||
let nearestDataPoint = null | ||
if (keyFromInvertedX) { | ||
nearestDataPoint = dataEntryForXPosition | ||
if ((keyFromInvertedX - dataEntryForXPositionPrev.key) | ||
< (dataEntryForXPosition.key - keyFromInvertedX)) { | ||
nearestDataPoint = dataEntryForXPositionPrev | ||
} else { | ||
nearestDataPoint = dataEntryForXPosition | ||
} | ||
} | ||
@@ -276,21 +438,24 @@ return nearestDataPoint | ||
function addMouseEvents () { | ||
cache.svg | ||
.on("mouseover.d3.dispatch", function mouseover (d) { | ||
if (!cache.data) { return } | ||
dispatcher.call("mouseOver", this, d, d3.mouse(this)) | ||
function addEvents () { | ||
const THROTTLE_DELAY = 20 | ||
const throttledDispatch = throttle((...args) => { | ||
dispatcher.call(...args) | ||
}, THROTTLE_DELAY) | ||
cache.panel | ||
.on("mouseover.dispatch", () => { | ||
dispatcher.call("mouseOverPanel", null, d3.mouse(cache.panel.node())) | ||
}) | ||
.on("mouseout.d3.dispatch", function mouseout (d) { | ||
if (!cache.data) { return } | ||
dispatcher.call("mouseOut", this, d, d3.mouse(this)) | ||
.on("mouseout.dispatch", () => { | ||
dispatcher.call("mouseOutPanel", null, d3.mouse(cache.panel.node())) | ||
}) | ||
.on("mousemove.d3.dispatch", function mousemove () { | ||
if (!cache.data) { return } | ||
const mouseX = d3.mouse(this)[0] | ||
const xPosition = mouseX - config.margin.left | ||
.on("mousemove.dispatch", () => { | ||
const [mouseX, mouseY] = d3.mouse(cache.panel.node()) | ||
if (!dataObject.data) { return } | ||
const xPosition = mouseX | ||
const dataPoint = getNearestDataPoint(xPosition) | ||
if (dataPoint) { | ||
const dataPointXPosition = cache.xScale(dataPoint[keys.DATA]) | ||
dispatcher.call("mouseMove", this, dataPoint, dataPointXPosition) | ||
const dataPointXPosition = scales.xScale(dataPoint[keys.DATA]) | ||
throttledDispatch("mouseMovePanel", null, dataPoint, dataPointXPosition, mouseY) | ||
} | ||
@@ -300,33 +465,12 @@ }) | ||
function save (_filename, _title) { | ||
exportChart.call(this, cache.svg, _filename, _title) | ||
} | ||
function on (...args) { | ||
return dispatcher.on(...args) | ||
dispatcher.on(...args) | ||
return this | ||
} | ||
function setConfig (_config) { | ||
config = Object.assign({}, config, _config) | ||
config = override(config, _config) | ||
return this | ||
} | ||
function setXTitle (_xTitle) { | ||
config = Object.assign({}, config, {xTitle: _xTitle}) | ||
return this | ||
} | ||
function setYTitle (_yTitle) { | ||
config = Object.assign({}, config, {yTitle: _yTitle}) | ||
return this | ||
} | ||
function getConfig () { | ||
return config | ||
} | ||
function getCache () { | ||
return cache | ||
} | ||
function destroy () { | ||
@@ -340,10 +484,6 @@ cache.svg.on(".", null).remove() | ||
setData, | ||
setXTitle, | ||
setYTitle, | ||
getCache, | ||
getConfig, | ||
on, | ||
save, | ||
destroy | ||
destroy, | ||
events: eventCollector | ||
} | ||
} |
import * as d3 from "./helpers/d3-service" | ||
export {bisector, extent, sum, range, merge} from "d3-array" | ||
@@ -4,0 +3,0 @@ import {keys} from "./helpers/constants" |
import * as d3 from "./helpers/d3-service" | ||
import {toggleOnOff} from "./interactors" | ||
import {override} from "./helpers/common" | ||
import {blurOnEnter} from "./interactors" | ||
export default function DomainEditor (_chart) { | ||
export default function DomainEditor (_container) { | ||
@@ -14,117 +15,333 @@ let config = { | ||
}, | ||
position: {x: 0, y: 0} | ||
width: 800, | ||
height: 500 | ||
} | ||
const cache = { | ||
chart: _chart, | ||
svg: null, | ||
parentHtmlNode: null | ||
container: _container, | ||
root: null, | ||
xHitZone: null, | ||
yHitZone: null, | ||
y2HitZone: null, | ||
yMaxInput: null, | ||
yMinInput: null, | ||
yLockIcon: null, | ||
y2MaxInput: null, | ||
y2MinInput: null, | ||
y2LockIcon: null, | ||
xMinInput: null, | ||
xMaxInput: null, | ||
xLockIcon: null, | ||
chartWidth: null, | ||
chartHeight: null, | ||
xDomain: null, | ||
yDomain: null, | ||
y2Domain: null, | ||
isEnabled: true | ||
} | ||
let chartCache = { | ||
svg: null | ||
} | ||
// events | ||
const dispatcher = d3.dispatch("lockToggle", "domainChanged") | ||
const dispatcher = d3.dispatch("domainChange", "domainLockToggle") | ||
function render () { | ||
buildSVG() | ||
} | ||
render() | ||
function buildSVG () { | ||
chartCache = cache.chart.getCache() | ||
setConfig(cache.chart.getConfig()) | ||
cache.chartWidth = config.width - config.margin.left - config.margin.right | ||
cache.chartHeight = config.height - config.margin.top - config.margin.bottom | ||
if (!cache.svg) { | ||
cache.svg = chartCache.svg.append("g") | ||
.classed("domain-editor-group", true) | ||
if (!cache.root) { | ||
cache.root = cache.container | ||
.append("div") | ||
.attr("class", "domain-input-group") | ||
.style("position", "absolute") | ||
.style("top", 0) | ||
const panel = cache.svg.append("rect") | ||
.attr("width", "10") | ||
.attr("height", "14") | ||
.attr("opacity", "0") | ||
// hit zones | ||
cache.xHitZone = cache.root.append("div") | ||
.attr("class", "hit-zone x") | ||
.style("pointer-events", "all") | ||
.style("position", "absolute") | ||
.on("mouseover.dispatch", showXEditor) | ||
.on("mouseout.dispatch", hideXEditor) | ||
const lock = cache.svg.append("g") | ||
.attr("class", "lock-icon") | ||
.attr("transform", "scale(0.2, 0.3)") | ||
.attr("pointer-events", "none") | ||
lock.append("rect") | ||
.attr("x", "3") | ||
.attr("y", "26") | ||
.attr("width", "42") | ||
.attr("height", "21") | ||
const loop = lock.append("path") | ||
.attr("class", "loop closed") | ||
.attr("d", "M24,0c0,0 -5,-0 -10,2c-4,3 -4,9 -4,9l-0,14l4,0l0,-14c0,0 0,-5 3,-6c3,-1 7,-1 7,-1c0,0 3,-0 7,1c3,1 3,6 3,6l4,0c0,0 0,-6 -4,-9c-4,-3 -10,-2 -10,-2") | ||
.attr("transform", "translate(0, 14)") | ||
cache.yHitZone = cache.root.append("div") | ||
.attr("class", "hit-zone y") | ||
.style("pointer-events", "all") | ||
.style("position", "absolute") | ||
.on("mouseover.dispatch", showYEditor) | ||
.on("mouseout.dispatch", hideYEditor) | ||
panel.on("click", function click () { | ||
const isClosed = this.classList.contains("closed") | ||
loop.transition().attr("transform", `translate(0, ${isClosed ? "14" : "6"})`) | ||
this.classList.toggle("closed", !isClosed) | ||
}) | ||
cache.y2HitZone = cache.root.append("div") | ||
.attr("class", "hit-zone y2") | ||
.style("pointer-events", "all") | ||
.style("position", "absolute") | ||
.on("mouseover.dispatch", showY2Editor) | ||
.on("mouseout.dispatch", hideY2Editor) | ||
cache.svg.append("text") | ||
.attr("class", "domain-min") | ||
// y input group | ||
cache.yMaxInput = cache.yHitZone.append("div") | ||
.attr("class", "domain-input y max") | ||
.style("position", "absolute") | ||
.attr("contentEditable", true) | ||
.on("blur", function change () { | ||
dispatcher.call("domainChange", this, {value: this.innerText, axis: "y", type: "max"}) | ||
}) | ||
.call(blurOnEnter) | ||
cache.parentHtmlNode = cache.svg.node().ownerSVGElement.parentNode | ||
d3.select(cache.parentHtmlNode).append("input") | ||
cache.yMinInput = cache.yHitZone.append("div") | ||
.attr("class", "domain-input y min") | ||
.style("position", "absolute") | ||
.style("top", config.position.x) | ||
.style("left", config.position.y) | ||
.attr("contentEditable", true) | ||
.on("blur", function change () { | ||
dispatcher.call("domainChange", this, {value: this.innerText, axis: "y", type: "min"}) | ||
}) | ||
.call(blurOnEnter) | ||
cache.yLockIcon = cache.yHitZone.append("div") | ||
.attr("class", "domain-lock y") | ||
.style("position", "absolute") | ||
.on("click", function change () { | ||
const isLocked = this.classList.contains("locked") | ||
this.classList.toggle("locked", !isLocked) | ||
dispatcher.call("domainLockToggle", this, {isLocked: !isLocked, axis: "y"}) | ||
}) | ||
cache.svg.append("text") | ||
.attr("class", "domain-max") | ||
// y2 input group | ||
cache.y2MaxInput = cache.y2HitZone.append("div") | ||
.attr("class", "domain-input y2 max") | ||
.style("position", "absolute") | ||
.attr("contentEditable", true) | ||
.on("blur", function change () { | ||
dispatcher.call("domainChange", this, {value: this.innerText, axis: "y2", type: "max"}) | ||
}) | ||
.call(blurOnEnter) | ||
cache.y2MinInput = cache.y2HitZone.append("div") | ||
.attr("class", "domain-input y2 min") | ||
.style("position", "absolute") | ||
.attr("contentEditable", true) | ||
.on("blur", function change () { | ||
dispatcher.call("domainChange", this, {value: this.innerText, axis: "y2", type: "min"}) | ||
}) | ||
.call(blurOnEnter) | ||
cache.y2LockIcon = cache.y2HitZone.append("div") | ||
.attr("class", "domain-lock y2") | ||
.style("position", "absolute") | ||
.on("click", function change () { | ||
const isLocked = this.classList.contains("locked") | ||
this.classList.toggle("locked", !isLocked) | ||
dispatcher.call("domainLockToggle", this, {isLocked: !isLocked, axis: "y2"}) | ||
}) | ||
// x input group | ||
cache.xMinInput = cache.xHitZone.append("div") | ||
.attr("class", "domain-input x min") | ||
.style("position", "absolute") | ||
.attr("contentEditable", true) | ||
.on("blur", function change () { | ||
dispatcher.call("domainChange", this, {value: this.innerText, axis: "x", type: "min"}) | ||
}) | ||
.call(blurOnEnter) | ||
cache.xMaxInput = cache.xHitZone.append("div") | ||
.attr("class", "domain-input x max") | ||
.style("position", "absolute") | ||
.attr("contentEditable", true) | ||
.on("blur", function change () { | ||
dispatcher.call("domainChange", this, {value: this.innerText, axis: "x", type: "max"}) | ||
}) | ||
.call(blurOnEnter) | ||
cache.xLockIcon = cache.xHitZone.append("div") | ||
.attr("class", "domain-lock x") | ||
.style("position", "absolute") | ||
.on("click", function change () { | ||
const isLocked = this.classList.contains("locked") | ||
this.classList.toggle("locked", !isLocked) | ||
dispatcher.call("domainLockToggle", this, {isLocked: !isLocked, axis: "x"}) | ||
}) | ||
hideYEditor() | ||
hideY2Editor() | ||
hideXEditor() | ||
} | ||
// cache.svg.attr("transform", `translate(${[config.margin.left, 0]})`) | ||
const HOVER_ZONE_SIZE = 40 | ||
const LOCK_SIZE = 12 | ||
const INPUT_HEIGHT = 12 | ||
const PADDING = 4 | ||
const INPUT_WIDTH = HOVER_ZONE_SIZE - PADDING | ||
// const items = cache.svg.selectAll(".toggleOnOff") | ||
// .data(config.toggle) | ||
// items.enter().append("tspan") | ||
// .attr("class", (d) => `item ${d} toggleOnOff`) | ||
// .attr("dx", "0.8em") | ||
// .attr("y", "1em") | ||
// .on("click.d3.select", toggleOnOff(".binning-group .item.toggleOnOff")) | ||
// .on("click.d3.dispatch", function click (d) { | ||
// const isSelected = this.classList.contains("selected") | ||
// dispatcher.call("change", this, d, {isSelected}) | ||
// }) | ||
// .merge(items) | ||
// .text((d) => d) | ||
// items.exit().remove() | ||
cache.xHitZone | ||
.style("width", `${cache.chartWidth + HOVER_ZONE_SIZE * 2}px`) | ||
.style("height", `${HOVER_ZONE_SIZE}px`) | ||
.style("top", `${config.margin.top + cache.chartHeight}px`) | ||
.style("left", `${config.margin.left - HOVER_ZONE_SIZE}px`) | ||
cache.yHitZone | ||
.style("width", `${HOVER_ZONE_SIZE}px`) | ||
.style("height", `${cache.chartHeight + HOVER_ZONE_SIZE}px`) | ||
.style("top", `${config.margin.top - HOVER_ZONE_SIZE}px`) | ||
.style("left", `${config.margin.left - HOVER_ZONE_SIZE}px`) | ||
cache.y2HitZone | ||
.style("width", `${HOVER_ZONE_SIZE}px`) | ||
.style("height", `${cache.chartHeight + HOVER_ZONE_SIZE}px`) | ||
.style("top", `${config.margin.top - HOVER_ZONE_SIZE}px`) | ||
.style("left", `${config.margin.left + cache.chartWidth}px`) | ||
cache.yMaxInput | ||
.style("width", `${INPUT_WIDTH}px`) | ||
.style("top", `${HOVER_ZONE_SIZE}px`) | ||
.text(Array.isArray(cache.yDomain) | ||
&& !isNaN(cache.yDomain[1]) ? cache.yDomain[1] : "") | ||
cache.yMinInput | ||
.style("width", `${INPUT_WIDTH}px`) | ||
.style("top", `${cache.chartHeight + HOVER_ZONE_SIZE - INPUT_HEIGHT}px`) | ||
.text(Array.isArray(cache.yDomain) | ||
&& !isNaN(cache.yDomain[0]) ? cache.yDomain[0] : "") | ||
cache.yLockIcon | ||
.style("width", `${LOCK_SIZE}px`) | ||
.style("height", `${LOCK_SIZE}px`) | ||
.style("left", `${HOVER_ZONE_SIZE - LOCK_SIZE}px`) | ||
.style("top", `${HOVER_ZONE_SIZE - LOCK_SIZE}px`) | ||
cache.y2MaxInput | ||
.style("width", `${INPUT_WIDTH}px`) | ||
.style("top", `${HOVER_ZONE_SIZE}px`) | ||
.style("left", `${PADDING}px`) | ||
.text(Array.isArray(cache.y2Domain) | ||
&& !isNaN(cache.y2Domain[1]) ? cache.y2Domain[1] : "") | ||
cache.y2MinInput | ||
.style("width", `${INPUT_WIDTH}px`) | ||
.style("top", `${cache.chartHeight + HOVER_ZONE_SIZE - INPUT_HEIGHT}px`) | ||
.style("left", `${PADDING}px`) | ||
.text(Array.isArray(cache.y2Domain) | ||
&& !isNaN(cache.y2Domain[0]) ? cache.y2Domain[0] : "") | ||
cache.y2LockIcon | ||
.style("width", `${LOCK_SIZE}px`) | ||
.style("height", `${LOCK_SIZE}px`) | ||
.style("top", `${HOVER_ZONE_SIZE - LOCK_SIZE}px`) | ||
cache.xMinInput | ||
.style("width", `${INPUT_WIDTH}px`) | ||
.style("top", `${PADDING}px`) | ||
.style("left", `${HOVER_ZONE_SIZE}px`) | ||
.text(Array.isArray(cache.xDomain) | ||
&& typeof (cache.xDomain[0]) !== "undefined" ? cache.xDomain[0] : "") | ||
cache.xMaxInput | ||
.style("width", `${INPUT_WIDTH}px`) | ||
.style("top", `${PADDING}px`) | ||
.style("left", `${HOVER_ZONE_SIZE + cache.chartWidth - INPUT_WIDTH}px`) | ||
.text(Array.isArray(cache.xDomain) | ||
&& typeof (cache.xDomain[1]) !== "undefined" ? cache.xDomain[1] : "") | ||
cache.xLockIcon | ||
.style("width", `${LOCK_SIZE}px`) | ||
.style("height", `${LOCK_SIZE}px`) | ||
.style("left", `${HOVER_ZONE_SIZE + cache.chartWidth}px`) | ||
} | ||
function showYEditor () { | ||
cache.yHitZone.style("opacity", "1") | ||
} | ||
function hideYEditor () { | ||
cache.yHitZone.style("opacity", "0") | ||
} | ||
function showY2Editor () { | ||
cache.y2HitZone.style("opacity", "1") | ||
} | ||
function hideY2Editor () { | ||
cache.y2HitZone.style("opacity", "0") | ||
} | ||
function showXEditor () { | ||
cache.xHitZone.style("opacity", "1") | ||
} | ||
function hideXEditor () { | ||
cache.xHitZone.style("opacity", "0") | ||
} | ||
function drawDomainEditor () { | ||
if (cache.isEnabled) { | ||
buildSVG() | ||
} else { | ||
destroy() | ||
} | ||
return this | ||
} | ||
function on (...args) { | ||
return dispatcher.on(...args) | ||
dispatcher.on(...args) | ||
return this | ||
} | ||
function setConfig (_config) { | ||
config = Object.assign({}, config, _config) | ||
function setXDomain (_xDomain) { | ||
cache.xDomain = _xDomain | ||
return this | ||
} | ||
function getCache () { | ||
return cache | ||
function setYDomain (_yDomain) { | ||
cache.yDomain = _yDomain | ||
return this | ||
} | ||
function destroy () { | ||
cache.svg.remove() | ||
function setY2Domain (_y2Domain) { | ||
cache.y2Domain = _y2Domain | ||
return this | ||
} | ||
function update () { | ||
render() | ||
function setXLock (_xLock) { | ||
cache.xLock = _xLock | ||
return this | ||
} | ||
function setYLock (_yLock) { | ||
cache.yLock = _yLock | ||
return this | ||
} | ||
function setY2Lock (_y2Lock) { | ||
cache.y2Lock = _y2Lock | ||
return this | ||
} | ||
function setVisibility (_shouldBeVisible) { | ||
cache.isEnabled = _shouldBeVisible | ||
drawDomainEditor() | ||
return this | ||
} | ||
function setConfig (_config) { | ||
config = override(config, _config) | ||
return this | ||
} | ||
function destroy () { | ||
if (cache.root) { | ||
cache.root.remove() | ||
} | ||
} | ||
return { | ||
getCache, | ||
on, | ||
setConfig, | ||
destroy, | ||
update | ||
setXDomain, | ||
setYDomain, | ||
setY2Domain, | ||
setXLock, | ||
setYLock, | ||
setY2Lock, | ||
drawDomainEditor, | ||
setVisibility, | ||
destroy | ||
} | ||
} |
@@ -32,3 +32,3 @@ import {keys} from "./constants" | ||
export function invertScale (_scale, _mouseX, _keyType) { | ||
if (_keyType === "time") { | ||
if (_keyType === "time" || _keyType === "number") { | ||
return _scale.invert(_mouseX) | ||
@@ -41,1 +41,33 @@ } else { | ||
} | ||
export function override (a, b) { | ||
const accum = {} | ||
for (const x in a) { | ||
if (a.hasOwnProperty(x)) { | ||
accum[x] = (x in b) ? b[x] : a[x] | ||
} | ||
} | ||
return accum | ||
} | ||
export function throttle (callback, limit) { | ||
let wait = false | ||
let timer = null | ||
return function throttleFn (...args) { | ||
if (!wait) { | ||
wait = true | ||
clearTimeout(timer) | ||
timer = setTimeout(() => { | ||
wait = false | ||
callback(...args) | ||
}, limit) | ||
} | ||
} | ||
} | ||
export function rebind (target) { | ||
return function reapply (...args) { | ||
target.on(`${args[0]}.rebind`, ...args.slice(1)) | ||
return this | ||
} | ||
} |
@@ -6,2 +6,3 @@ export { | ||
timeFormat, | ||
utcFormat, | ||
format, | ||
@@ -8,0 +9,0 @@ bisector, |
import * as d3 from "./helpers/d3-service" | ||
import {keys} from "./helpers/constants" | ||
import {override} from "./helpers/common" | ||
export default function Hover (_chart) { | ||
export default function Hover (_container) { | ||
@@ -15,7 +16,16 @@ let config = { | ||
width: 800, | ||
height: 500 | ||
height: 500, | ||
dotRadius: null, | ||
chartType: null | ||
} | ||
let scales = { | ||
yScale: null, | ||
yScale2: null, | ||
hasSecondAxis: null, | ||
colorScale: null | ||
} | ||
const cache = { | ||
chart: _chart, | ||
container: _container, | ||
svg: null, | ||
@@ -29,15 +39,8 @@ chartWidth: null, | ||
data: null, | ||
xScale: null, | ||
yScale: null, | ||
yScale2: null, | ||
colorScale: null | ||
isEnabled: true | ||
} | ||
let chartCache = { | ||
xScale: null, | ||
yScale: null, | ||
yScale2: null, | ||
colorScale: null, | ||
dataBySeries: null, | ||
svg: null | ||
let data = { | ||
stack: null, | ||
groupKeys: null | ||
} | ||
@@ -48,22 +51,5 @@ | ||
function init () { | ||
cache.chart.on("mouseOver.hover", show) | ||
.on("mouseMove.hover", update) | ||
.on("mouseOut.hover", hide) | ||
const getColor = (d) => scales.colorScale(d[keys.ID]) | ||
render() | ||
hide() | ||
} | ||
init() | ||
function render () { | ||
buildSVG() | ||
buildScales() | ||
drawVerticalMarker() | ||
} | ||
function buildSVG () { | ||
chartCache = cache.chart.getCache() | ||
setConfig(cache.chart.getConfig()) | ||
cache.chartWidth = config.width - config.margin.left - config.margin.right | ||
@@ -73,19 +59,14 @@ cache.chartHeight = config.height - config.margin.top - config.margin.bottom | ||
if (!cache.svg) { | ||
cache.svg = chartCache.svg.append("g") | ||
cache.svg = cache.container.append("g") | ||
.classed("hover-group", true) | ||
.style("pointer-events", "none") | ||
} | ||
cache.svg.attr("transform", `translate(${config.margin.left}, ${config.margin.top})`) | ||
} | ||
function buildScales () { | ||
cache.xScale = chartCache.xScale | ||
cache.yScale = chartCache.yScale | ||
cache.yScale2 = chartCache.yScale2 | ||
cache.colorScale = chartCache.colorScale | ||
} | ||
function drawHover (_dataPoint, _dataPointXPosition) { | ||
buildSVG() | ||
function update (_dataPoint, _dataPointXPosition) { | ||
if (_dataPointXPosition) { | ||
if (!isNaN(_dataPointXPosition)) { | ||
moveVerticalMarker(_dataPointXPosition) | ||
drawVerticalMarker() | ||
if (config.chartType === "stackedLine" | ||
@@ -103,11 +84,13 @@ || config.chartType === "stackedArea" | ||
function show () { | ||
if (!cache.svg) { return null } | ||
cache.svg.style("display", "block") | ||
return this | ||
} | ||
function hide () { | ||
if (!cache.svg) { return null } | ||
cache.svg.style("display", "none") | ||
return this | ||
} | ||
const getColor = (d) => cache.colorScale(d[keys.ID]) | ||
function highlightDataPoints (_dataPoint) { | ||
@@ -127,8 +110,7 @@ const dotsData = _dataPoint[keys.SERIES] | ||
.merge(dots) | ||
// .attr("cy", (d) => cache.yScale2(d[keys.VALUE])) | ||
.attr("cy", (d) => { | ||
if (d[keys.GROUP] === chartCache.groupKeys[0]) { | ||
return cache.yScale(d[keys.VALUE]) | ||
if (config.chartType === "stackedArea" || data.groupKeys[0].indexOf(d[keys.ID]) > -1) { | ||
return scales.yScale(d[keys.VALUE]) | ||
} else { | ||
return cache.yScale2(d[keys.VALUE]) | ||
return scales.yScale2(d[keys.VALUE]) | ||
} | ||
@@ -150,3 +132,3 @@ }) | ||
const dotsStack = cache.stack([stackedDataPoint]) | ||
const dotsStack = data.stack([stackedDataPoint]) | ||
const dotsData = dotsStack.map((d) => { | ||
@@ -176,23 +158,37 @@ const dot = {value: d[0][1]} | ||
function moveVerticalMarker (_verticalMarkerXPosition) { | ||
cache.svg.attr("transform", `translate(${[_verticalMarkerXPosition + config.margin.left, config.margin.top]})`) | ||
cache.svg.attr("transform", `translate(${[_verticalMarkerXPosition, 0]})`) | ||
} | ||
function bindEvents (_dispatcher) { | ||
_dispatcher.on("mouseOverPanel.hover", show) | ||
.on("mouseMovePanel.hover", drawHover) | ||
.on("mouseOutPanel.hover", hide) | ||
return this | ||
} | ||
function on (...args) { | ||
return dispatcher.on(...args) | ||
dispatcher.on(...args) | ||
return this | ||
} | ||
function setConfig (_config) { | ||
config = Object.assign({}, config, _config) | ||
config = override(config, _config) | ||
return this | ||
} | ||
function getCache () { | ||
return cache | ||
function setScales (_scales) { | ||
scales = override(scales, _scales) | ||
return this | ||
} | ||
function destroy () { | ||
cache.svg.remove() | ||
function setData (_data) { | ||
data = Object.assign({}, data, _data) | ||
return this | ||
} | ||
return { | ||
setConfig, | ||
setScales, | ||
setData, | ||
bindEvents, | ||
highlightDataPoints, | ||
@@ -202,7 +198,4 @@ highlightStackedDataPoints, | ||
moveVerticalMarker, | ||
getCache, | ||
on, | ||
setConfig, | ||
destroy | ||
on | ||
} | ||
} |
@@ -12,5 +12,12 @@ import * as d3 from "./helpers/d3-service" | ||
export function toggleOnOff (selector) { | ||
return exclusiveToggle(selector, { | ||
toggleOffIsEnabled: true, | ||
export function toggleOnOff (selector, bool) { | ||
const shouldBeSelected = typeof bool === "undefined" ? !d3.select(selector).classed("selected") : bool | ||
d3.select(selector) | ||
.classed("selected", shouldBeSelected) | ||
.classed("dimmed", !shouldBeSelected) | ||
} | ||
export function exclusiveToggle (othersSelection, selector) { | ||
return toggle(othersSelection, selector, { | ||
toggleOffIsEnabled: false, | ||
toggleMultipleIsEnabled: false | ||
@@ -20,30 +27,32 @@ }) | ||
export function exclusiveToggle (selector, options = {toggleOffIsEnabled: false, toggleMultipleIsEnabled: false}) { | ||
export function toggle (othersSelection, selector, options = {toggleOffIsEnabled: false, toggleMultipleIsEnabled: false}) { | ||
/* eslint-disable consistent-this */ | ||
let hasSelection = false | ||
const selectionNode = document.querySelector(selector) | ||
return function toggle () { | ||
/* eslint-disable consistent-this */ | ||
const that = this | ||
let hasSelection = false | ||
const selection = d3.select(this.farthestViewportElement) | ||
.selectAll(selector) | ||
othersSelection.classed("selected", function selectedClass () { | ||
const isSelected = this.classList.contains("selected") | ||
const hasJustBeenClicked = this === selectionNode | ||
let shouldBeSelected = false | ||
selection.classed("selected", function selectedClass () { | ||
const isSelected = this.classList.contains("selected") | ||
const hasJustBeenClicked = this === that | ||
let shouldBeSelected = false | ||
if (hasJustBeenClicked) { | ||
shouldBeSelected = options.toggleOffIsEnabled ? !isSelected : true | ||
} else { | ||
shouldBeSelected = options.toggleMultipleIsEnabled ? isSelected : false | ||
} | ||
if (hasJustBeenClicked) { | ||
shouldBeSelected = options.toggleOffIsEnabled ? !isSelected : true | ||
} else { | ||
shouldBeSelected = options.toggleMultipleIsEnabled ? isSelected : false | ||
} | ||
hasSelection = hasSelection || shouldBeSelected | ||
return shouldBeSelected | ||
}) | ||
hasSelection = hasSelection || shouldBeSelected | ||
return shouldBeSelected | ||
}) | ||
selection.classed("dimmed", function dimmedClass () { | ||
return hasSelection && !this.classList.contains("selected") | ||
}) | ||
/* eslint-enable consistent-this */ | ||
} | ||
othersSelection.classed("dimmed", function dimmedClass () { | ||
return hasSelection && !this.classList.contains("selected") | ||
}) | ||
/* eslint-enable consistent-this */ | ||
} | ||
export function blurOnEnter (selection) { | ||
selection.on("keypress.enter", function keypress () { | ||
if (d3.event.key === "Enter") { this.blur() } | ||
}) | ||
} |
import Tooltip from "./tooltip" | ||
export default function Legend (_chart) { | ||
return Tooltip(_chart, true) | ||
export default function Legend (_container) { | ||
return Tooltip(_container) | ||
} |
import * as d3 from "./helpers/d3-service" | ||
import {keys} from "./helpers/constants" | ||
import {override} from "./helpers/common" | ||
export default function Line (config, cache) { | ||
export default function Line (_container) { | ||
const getColor = (d) => cache.colorScale(d[keys.ID]) | ||
let config = { | ||
margin: { | ||
top: 60, | ||
right: 30, | ||
bottom: 40, | ||
left: 70 | ||
}, | ||
width: 800, | ||
height: 500, | ||
chartType: null | ||
} | ||
let scales = { | ||
colorScale: null, | ||
xScale: null, | ||
yScale: null, | ||
yScale2: null | ||
} | ||
const cache = { | ||
container: _container, | ||
svg: null, | ||
chartHeight: null | ||
} | ||
let data = { | ||
dataBySeries: null, | ||
groupKeys: null, | ||
stack: null, | ||
stackData: null | ||
} | ||
const getColor = (d) => scales.colorScale(d[keys.ID]) | ||
function buildSVG () { | ||
cache.chartWidth = config.width - config.margin.left - config.margin.right | ||
cache.chartHeight = config.height - config.margin.top - config.margin.bottom | ||
if (!cache.svg) { | ||
cache.svg = cache.container.append("g") | ||
.classed("mark-group", true) | ||
} | ||
} | ||
function drawLines () { | ||
const seriesLine = d3.line() | ||
.x((d) => cache.xScale(d[keys.DATA])) | ||
.y((d) => cache.yScale(d[keys.VALUE])) | ||
.x((d) => scales.xScale(d[keys.DATA])) | ||
.y((d) => scales.yScale(d[keys.VALUE])) | ||
const seriesLine2 = d3.line() | ||
.x((d) => cache.xScale(d[keys.DATA])) | ||
.y((d) => cache.yScale2(d[keys.VALUE])) | ||
.x((d) => scales.xScale(d[keys.DATA])) | ||
.y((d) => scales.yScale2(d[keys.VALUE])) | ||
.curve(d3.curveCatmullRom) | ||
const lines = cache.svg.select(".chart-group").selectAll(".mark") | ||
.data(cache.dataBySeries) | ||
const lines = cache.svg.selectAll(".mark") | ||
.data(data.dataBySeries) | ||
lines.enter() | ||
.append("path") | ||
.attr("class", () => ["mark", "d3.line"].join(" ")) | ||
.merge(lines) | ||
.attr("class", () => ["mark", "line"].join(" ")) | ||
.attr("d", (d) => { | ||
if (d[keys.GROUP] === cache.groupKeys[0]) { | ||
if (d[keys.GROUP] === 0) { | ||
return seriesLine(d[keys.VALUES]) | ||
@@ -41,21 +84,21 @@ } else { | ||
const seriesArea = d3.area() | ||
.x((d) => cache.xScale(d[keys.DATA])) | ||
.y0((d) => cache.yScale(d[keys.VALUE])) | ||
.y1(() => cache.chartHeight) | ||
.x((d) => scales.xScale(d[keys.DATA])) | ||
.y0((d) => scales.yScale(d[keys.VALUE])) | ||
.y1(() => config.chartHeight) | ||
const seriesArea2 = d3.area() | ||
.x((d) => cache.xScale(d[keys.DATA])) | ||
.y0((d) => cache.yScale2(d[keys.VALUE])) | ||
.y1(() => cache.chartHeight) | ||
.x((d) => scales.xScale(d[keys.DATA])) | ||
.y0((d) => scales.yScale2(d[keys.VALUE])) | ||
.y1(() => config.chartHeight) | ||
.curve(d3.curveCatmullRom) | ||
const areas = cache.svg.select(".chart-group").selectAll(".mark") | ||
.data(cache.dataBySeries) | ||
const areas = cache.svg.selectAll(".mark") | ||
.data(data.dataBySeries) | ||
areas.enter() | ||
.append("path") | ||
.attr("class", () => ["mark", "d3.area"].join(" ")) | ||
.merge(areas) | ||
.attr("class", () => ["mark", "area"].join(" ")) | ||
.attr("d", (d) => { | ||
if (d[keys.GROUP] === cache.groupKeys[0]) { | ||
if (d[keys.GROUP] === 0) { | ||
return seriesArea(d[keys.VALUES]) | ||
@@ -74,16 +117,16 @@ } else { | ||
const seriesLine = d3.area() | ||
.x((d) => cache.xScale(d.data[keys.DATA])) | ||
.y0((d) => cache.yScale(d[0])) | ||
.y1((d) => cache.yScale(d[1])) | ||
.x((d) => scales.xScale(d.data[keys.DATA])) | ||
.y0((d) => scales.yScale(d[0])) | ||
.y1((d) => scales.yScale(d[1])) | ||
const areas = cache.svg.select(".chart-group").selectAll(".mark") | ||
.data(cache.stack(cache.stackData)) | ||
const areas = cache.svg.selectAll(".mark") | ||
.data(data.stack(data.stackData)) | ||
areas.enter() | ||
.append("path") | ||
.attr("class", () => ["mark", "stacked-d3.area"].join(" ")) | ||
.merge(areas) | ||
.attr("class", () => ["mark", "stacked-area"].join(" ")) | ||
.attr("d", seriesLine) | ||
.style("stroke", "none") | ||
.style("fill", (d) => cache.colorScale(d.key)) | ||
.style("fill", (d) => scales.colorScale(d.key)) | ||
@@ -93,7 +136,35 @@ areas.exit().remove() | ||
function drawMarks () { | ||
buildSVG() | ||
if (config.chartType === "area") { | ||
drawAreas() | ||
} else if (config.chartType === "line") { | ||
drawLines() | ||
} else if (config.chartType === "stackedArea") { | ||
drawStackedAreas() | ||
} | ||
} | ||
function setConfig (_config) { | ||
config = override(config, _config) | ||
return this | ||
} | ||
function setScales (_scales) { | ||
scales = override(scales, _scales) | ||
return this | ||
} | ||
function setData (_data) { | ||
data = Object.assign({}, data, _data) | ||
return this | ||
} | ||
return { | ||
drawLines, | ||
drawAreas, | ||
drawStackedAreas | ||
setConfig, | ||
setScales, | ||
setData, | ||
drawMarks | ||
} | ||
} |
import * as d3 from "./helpers/d3-service" | ||
import {keys} from "./helpers/constants" | ||
import {getUnique} from "./helpers/common" | ||
import {getUnique, override} from "./helpers/common" | ||
export default function Scale (config, cache) { | ||
export default function Scale () { | ||
let config = { | ||
margin: { | ||
top: 60, | ||
right: 30, | ||
bottom: 40, | ||
left: 70 | ||
}, | ||
height: null, | ||
width: null, | ||
keyType: null, | ||
chartType: null, | ||
colorSchema: null, | ||
defaultColor: null | ||
} | ||
let data = { | ||
dataByKey: null, | ||
dataBySeries: null, | ||
flatDataSorted: null, | ||
groupKeys: null | ||
} | ||
const getID = (d) => d[keys.ID] | ||
@@ -12,59 +34,42 @@ const getKey = (d) => d[keys.DATA] | ||
function buildStackedScales () { | ||
const allStackHeights = cache.dataByKey.map((d) => d3.sum(d.series.map((dB) => dB.value))) | ||
cache.stackData = cache.dataByKey.map((d) => { | ||
const points = { | ||
key: d[keys.DATA] | ||
} | ||
d.series.forEach((dB) => { | ||
points[dB[keys.ID]] = dB[keys.VALUE] | ||
}) | ||
return points | ||
}) | ||
cache.d3.stack = d3.stack() | ||
.keys(cache.dataBySeries.map(getID)) | ||
.order(d3.stackOrderNone) | ||
.offset(d3.stackOffsetNone) | ||
const valuesExtent = d3.extent(allStackHeights) | ||
const allKeys = cache.flatDataSorted.map(getKey) | ||
const allUniqueKeys = getUnique(allKeys) | ||
buildXScale(allUniqueKeys) | ||
buildColorScale() | ||
buildYScale([0, valuesExtent[1]]) | ||
} | ||
function buildXScale (_allKeys) { | ||
let datesExtent = null | ||
const chartWidth = config.width - config.margin.left - config.margin.right | ||
let domain = null | ||
let xScale = null | ||
if (config.keyType === "time") { | ||
datesExtent = d3.extent(_allKeys) | ||
cache.xScale = d3.scaleTime() | ||
domain = d3.extent(_allKeys) | ||
xScale = d3.scaleTime() | ||
} else if (config.keyType === "number") { | ||
domain = d3.extent(_allKeys) | ||
xScale = d3.scaleLinear() | ||
} else { | ||
datesExtent = _allKeys | ||
cache.xScale = (config.chartType === "bar" || config.chartType === "stackedBar") ? d3.scaleBand() : d3.scalePoint() | ||
cache.xScale.padding(0) | ||
domain = _allKeys | ||
xScale = (config.chartType === "bar" || config.chartType === "stackedBar") ? d3.scaleBand() : d3.scalePoint() | ||
xScale.padding(0) | ||
} | ||
cache.xScale.domain(datesExtent) | ||
.range([0, cache.chartWidth]) | ||
xScale.domain(domain) | ||
.range([0, chartWidth]) | ||
return xScale | ||
} | ||
function buildYScale (_extent) { | ||
const chartHeight = config.height - config.margin.top - config.margin.bottom | ||
const yScale = d3.scaleLinear() | ||
.domain(_extent) | ||
.rangeRound([chartHeight, 0]) | ||
.nice() | ||
return yScale | ||
} | ||
function buildColorScale () { | ||
const ids = cache.dataBySeries.map(getID) | ||
cache.colorScale = d3.scaleOrdinal() | ||
const ids = data.dataBySeries.map(getID) | ||
const colorScale = d3.scaleOrdinal() | ||
.range(config.colorSchema.map((d) => d.value)) | ||
.domain(config.colorSchema.map((d, i) => d.key || ids[i])) | ||
.unknown(config.defaultColor) | ||
} | ||
function buildYScale (_extent) { | ||
cache.yScale = d3.scaleLinear() | ||
.domain(_extent) | ||
.rangeRound([cache.chartHeight, 0]) | ||
.nice() | ||
return colorScale | ||
} | ||
@@ -74,3 +79,3 @@ | ||
const groups = {} | ||
cache.dataBySeries.forEach((d) => { | ||
data.dataBySeries.forEach((d) => { | ||
const key = d[keys.GROUP] | ||
@@ -90,28 +95,76 @@ if (!groups[key]) { | ||
function buildScales () { | ||
function getStackedScales () { | ||
const allStackHeights = data.dataByKey.map((d) => d3.sum(d.series.map((dB) => dB.value))) | ||
const valuesExtent = d3.extent(allStackHeights) | ||
const allKeys = data.flatDataSorted.map(getKey) | ||
const allUniqueKeys = getUnique(allKeys) | ||
const xScale = buildXScale(allUniqueKeys) | ||
const colorScale = buildColorScale() | ||
const yScale = buildYScale([0, valuesExtent[1]]) | ||
return { | ||
xScale, | ||
yScale, | ||
colorScale | ||
} | ||
} | ||
function getHorizontalScales () { | ||
const groups = splitByGroups() | ||
cache.hasSecondAxis = cache.groupKeys.length > 1 | ||
const hasSecondAxis = Object.keys(data.groupKeys).length > 1 | ||
const groupAxis1 = groups[cache.groupKeys[0]] | ||
const groupAxis1 = groups[0] | ||
const allUniqueKeys = groupAxis1.allKeys | ||
const valuesExtent = d3.extent(groupAxis1.allValues) | ||
buildXScale(allUniqueKeys) | ||
buildColorScale() | ||
buildYScale(valuesExtent) | ||
const xScale = buildXScale(allUniqueKeys) | ||
const colorScale = buildColorScale() | ||
const yScale = buildYScale(valuesExtent) | ||
if (cache.hasSecondAxis) { | ||
const groupAxis2 = groups[cache.groupKeys[1]] | ||
let yScale2 = null | ||
if (hasSecondAxis) { | ||
const groupAxis2 = groups[1] | ||
const valuesExtent2 = d3.extent(groupAxis2.allValues) | ||
cache.yScale2 = cache.yScale.copy() | ||
yScale2 = yScale.copy() | ||
.domain(valuesExtent2) | ||
} | ||
return { | ||
hasSecondAxis, | ||
xScale, | ||
yScale, | ||
yScale2, | ||
colorScale | ||
} | ||
} | ||
function getScales () { | ||
if (config.chartType === "stackedBar" | ||
|| config.chartType === "stackedArea") { | ||
return getStackedScales() | ||
} else { | ||
return getHorizontalScales() | ||
} | ||
} | ||
function setConfig (_config) { | ||
config = override(config, _config) | ||
return this | ||
} | ||
function setData (_data) { | ||
data = Object.assign({}, data, _data) | ||
return this | ||
} | ||
return { | ||
buildStackedScales, | ||
buildScales | ||
setConfig, | ||
setData, | ||
getScales | ||
} | ||
} |
import * as d3 from "./helpers/d3-service" | ||
import {keys} from "./helpers/constants" | ||
import {cloneData} from "./helpers/common" | ||
import {cloneData, override} from "./helpers/common" | ||
export default function Tooltip (_chart, isStatic) { | ||
export default function Tooltip (_container) { | ||
@@ -20,14 +20,6 @@ let config = { | ||
tooltipMaxTopicLength: 170, | ||
tooltipBorderRadius: 3, | ||
// Animations | ||
mouseChaseDuration: 30, | ||
ease: d3.easeQuadInOut, | ||
mouseChaseDuration: 0, | ||
tooltipEase: d3.easeQuadInOut, | ||
titleHeight: 32, | ||
elementHeight: 24, | ||
padding: 8, | ||
dotRadius: 4, | ||
tooltipHeight: 48, | ||
@@ -43,5 +35,9 @@ tooltipWidth: 160, | ||
let scales = { | ||
colorScale: null | ||
} | ||
const cache = { | ||
chart: _chart, | ||
svg: null, | ||
container: _container, | ||
root: null, | ||
chartWidth: null, | ||
@@ -52,164 +48,120 @@ chartHeight: null, | ||
tooltipTitle: null, | ||
tooltipBackground: null | ||
tooltipBackground: null, | ||
xPosition: null, | ||
yPosition: null, | ||
content: null, | ||
title: null, | ||
isEnabled: true | ||
} | ||
let chartCache = null | ||
function init () { | ||
if (!isStatic) { | ||
cache.chart.on("mouseOver.tooltip", show) | ||
.on("mouseMove.tooltip", update) | ||
.on("mouseOut.tooltip", hide) | ||
} | ||
render() | ||
} | ||
init() | ||
function render () { | ||
buildSVG() | ||
return this | ||
} | ||
function buildSVG () { | ||
chartCache = cache.chart.getCache() | ||
setConfig(cache.chart.getConfig()) | ||
if (!cache.svg) { | ||
cache.svg = chartCache.svg.append("g") | ||
.classed("mapd3 mapd3-tooltip", true) | ||
cache.tooltipBackground = cache.svg.append("rect") | ||
.classed("tooltip-text-container", true) | ||
cache.tooltipTitle = cache.svg.append("text") | ||
.classed("tooltip-title", true) | ||
.attr("dominant-baseline", "hanging") | ||
cache.tooltipDivider = cache.svg.append("line") | ||
.classed("tooltip-divider", true) | ||
cache.tooltipBody = cache.svg.append("g") | ||
.classed("tooltip-body", true) | ||
} | ||
cache.chartWidth = config.width - config.margin.left - config.margin.right | ||
cache.chartHeight = config.height - config.margin.top - config.margin.bottom | ||
cache.tooltipBackground.attr("rx", config.tooltipBorderRadius) | ||
.attr("ry", config.tooltipBorderRadius) | ||
if (!cache.root) { | ||
cache.root = cache.container.append("div") | ||
.attr("class", "tooltip-group") | ||
.style("position", "absolute") | ||
.style("pointer-events", "none") | ||
cache.tooltipTitle.attr("dy", config.padding) | ||
.attr("dx", config.padding) | ||
cache.tooltipTitle = cache.root.append("div") | ||
.attr("class", "tooltip-title") | ||
setSize("auto", "auto") | ||
hide() | ||
cache.tooltipBody = cache.root.append("div") | ||
.attr("class", "tooltip-body") | ||
} | ||
} | ||
function calculateTooltipPosition (_mouseX) { | ||
const tooltipX = _mouseX + config.margin.left | ||
let offset = 0 | ||
const tooltipY = config.margin.top | ||
function calculateTooltipPosition (_mouseX, _mouseY) { | ||
const OFFSET = 4 | ||
const tooltipSize = cache.root.node().getBoundingClientRect() | ||
const tooltipX = _mouseX | ||
let avoidanceOffset = OFFSET | ||
const tooltipY = _mouseY + config.margin.top - tooltipSize.height / 2 | ||
if (_mouseX > (cache.chartWidth / 2)) { | ||
offset = -config.tooltipWidth | ||
avoidanceOffset = -tooltipSize.width - OFFSET | ||
} | ||
return [tooltipX + offset, tooltipY] | ||
return [tooltipX + avoidanceOffset, tooltipY] | ||
} | ||
function setPosition (_xPosition) { | ||
const [tooltipX, tooltipY] = calculateTooltipPosition(_xPosition) | ||
function move () { | ||
const xPosition = cache.xPosition === "auto" | ||
? cache.chartWidth | ||
: cache.xPosition | ||
cache.svg.transition() | ||
const yPosition = cache.yPosition === "auto" | ||
? config.margin.top | ||
: cache.yPosition | ||
cache.root.transition() | ||
.duration(config.mouseChaseDuration) | ||
.ease(config.ease) | ||
.attr("transform", `translate(${tooltipX}, ${tooltipY})`) | ||
.ease(config.tooltipEase) | ||
.style("top", `${yPosition}px`) | ||
.style("left", function left () { | ||
const width = cache.yPosition === "auto" ? this.getBoundingClientRect().width : 0 | ||
return `${xPosition + config.margin.left - width}px` | ||
}) | ||
return this | ||
} | ||
function setSize (_width, _height) { | ||
let height = _height | ||
if (_height === "auto") { | ||
height = cache.tooltipBody.node().getBoundingClientRect().height + config.titleHeight + config.padding | ||
} | ||
let width = _width | ||
if (_width === "auto") { | ||
width = cache.tooltipBody.node().getBoundingClientRect().width + config.padding * 2 | ||
} | ||
function drawContent () { | ||
const content = cache.content | ||
const formatter = d3.format(config.valueFormat) | ||
cache.tooltipBackground.attr("width", width) | ||
.attr("height", height) | ||
const tooltipItems = cache.tooltipBody.selectAll(".tooltip-item") | ||
.data(content) | ||
const tooltipItemsUpdate = tooltipItems.enter().append("div") | ||
.attr("class", "tooltip-item") | ||
.merge(tooltipItems) | ||
tooltipItems.exit().remove() | ||
cache.tooltipDivider.attr("x2", width) | ||
.attr("y1", config.titleHeight) | ||
.attr("y2", config.titleHeight) | ||
const tooltipItem = tooltipItemsUpdate.selectAll(".section") | ||
.data((d) => { | ||
const legendData = [ | ||
{key: "color", value: scales.colorScale(d[keys.ID])}, | ||
{key: "label", value: d[keys.LABEL]} | ||
] | ||
if (typeof d[keys.VALUE] !== "undefined") { | ||
legendData.push({key: "value", value: d[keys.VALUE]}) | ||
} | ||
return legendData | ||
}) | ||
tooltipItem.enter().append("div") | ||
.merge(tooltipItem) | ||
.attr("class", (d) => ["section", d.key].join(" ")) | ||
.each(function each (d) { | ||
const selection = d3.select(this) | ||
if (d.key === "color") { | ||
selection.style("background", d.value) | ||
} else if (d.key === "value") { | ||
selection.html(formatter(d.value)) | ||
} else { | ||
selection.html(d.value) | ||
} | ||
}) | ||
tooltipItem.exit().remove() | ||
return this | ||
} | ||
function setSeriesContent (_series) { | ||
const tooltipLeft = cache.tooltipBody.selectAll(".tooltip-left-text") | ||
.data(_series) | ||
tooltipLeft.enter().append("text") | ||
.classed("tooltip-left-text", true) | ||
.attr("dominant-baseline", "hanging") | ||
.attr("dy", config.padding) | ||
.attr("dx", config.padding * 2 + config.dotRadius) | ||
.merge(tooltipLeft) | ||
.attr("y", (d, i) => i * config.elementHeight + config.titleHeight) | ||
.text((d) => d[keys.LABEL]) | ||
tooltipLeft.exit().remove() | ||
function drawTitle () { | ||
let title = cache.title | ||
const values = _series.map(getValueText) | ||
const tooltipRight = cache.tooltipBody.selectAll(".tooltip-right-text") | ||
.data(values) | ||
tooltipRight.enter().append("text") | ||
.classed("tooltip-right-text", true) | ||
.attr("text-anchor", "end") | ||
.attr("dominant-baseline", "hanging") | ||
.attr("dy", config.padding) | ||
.attr("dx", -config.padding) | ||
.merge(tooltipRight) | ||
.attr("x", config.tooltipWidth) | ||
.attr("y", (d, i) => i * config.elementHeight + config.titleHeight) | ||
.text((d) => d) | ||
tooltipRight.exit().remove() | ||
const tooltipCircles = cache.tooltipBody.selectAll(".tooltip-circle") | ||
.data(_series) | ||
tooltipCircles.enter().append("circle") | ||
.classed("tooltip-circle", true) | ||
.merge(tooltipCircles) | ||
.attr("cx", config.padding + config.dotRadius) | ||
.attr("cy", (d, i) => i * config.elementHeight + config.titleHeight + config.elementHeight / 2) | ||
.attr("r", config.dotRadius) | ||
.style("fill", (d) => chartCache.colorScale(d[keys.ID])) | ||
tooltipCircles.exit().remove() | ||
} | ||
function setTitle (_title) { | ||
let title = _title | ||
if (config.keyType === "time") { | ||
title = d3.timeFormat(config.dateFormat)(_title) | ||
if (typeof title === "object") { | ||
title = d3.timeFormat(config.dateFormat)(title) | ||
} | ||
cache.tooltipTitle.text(title) | ||
cache.tooltipTitle.html(title) | ||
return this | ||
} | ||
function getValueText (_data) { | ||
const value = _data[keys.VALUE] | ||
if (value) { | ||
const formatter = d3.format(config.valueFormat) | ||
return formatter(_data[keys.VALUE]) | ||
} else { | ||
return null | ||
} | ||
function drawTooltip () { | ||
buildSVG() | ||
drawTitle() | ||
drawContent() | ||
move() | ||
return this | ||
} | ||
function setContent (_series) { | ||
function setupContent (_series) { | ||
let series = _series | ||
@@ -223,4 +175,3 @@ | ||
setSeriesContent(series) | ||
cache.content = series | ||
return this | ||
@@ -239,4 +190,4 @@ } | ||
function hide () { | ||
cache.svg.style("display", "none") | ||
if (!cache.root) { return null } | ||
cache.root.style("display", "none") | ||
return this | ||
@@ -246,33 +197,71 @@ } | ||
function show () { | ||
cache.svg.style("display", "block") | ||
if (!cache.root) { return null } | ||
cache.root.style("display", "block") | ||
return this | ||
} | ||
function setVisibility (_shouldBeVisible) { | ||
cache.isEnabled = _shouldBeVisible | ||
if (!cache.root) { return null } | ||
if (cache.isEnabled) { | ||
show() | ||
} else { | ||
hide() | ||
} | ||
return this | ||
} | ||
function update (_dataPoint, _xPosition) { | ||
function setupTooltip (_dataPoint, _xPosition, _yPosition) { | ||
buildSVG() | ||
const [tooltipX, tooltipY] = calculateTooltipPosition(_xPosition, _yPosition) | ||
setXPosition(tooltipX) | ||
setYPosition(tooltipY) | ||
setTitle(_dataPoint[keys.DATA]) | ||
setContent(_dataPoint[keys.SERIES]) | ||
setSize(config.tooltipWidth, "auto") | ||
setPosition(_xPosition) | ||
setupContent(_dataPoint[keys.SERIES]) | ||
drawTooltip() | ||
return this | ||
} | ||
function bindEvents (_dispatcher) { | ||
_dispatcher.on("mouseOverPanel.tooltip", show) | ||
.on("mouseMovePanel.tooltip", setupTooltip) | ||
.on("mouseOutPanel.tooltip", hide) | ||
return this | ||
} | ||
function setConfig (_config) { | ||
config = Object.assign({}, config, _config) | ||
config = override(config, _config) | ||
return this | ||
} | ||
function getCache () { | ||
return cache | ||
function setScales (_scales) { | ||
scales = override(scales, _scales) | ||
return this | ||
} | ||
function destroy () { | ||
cache.chart.on(".tooltip", null) | ||
cache.svg.remove() | ||
function setTitle (_title) { | ||
cache.title = _title | ||
return this | ||
} | ||
function setXPosition (_xPosition) { | ||
cache.xPosition = _xPosition | ||
return this | ||
} | ||
function setYPosition (_yPosition) { | ||
cache.yPosition = _yPosition | ||
return this | ||
} | ||
function setContent (_content) { | ||
cache.content = _content | ||
return this | ||
} | ||
return { | ||
setPosition, | ||
setSize, | ||
bindEvents, | ||
setXPosition, | ||
setYPosition, | ||
setContent, | ||
@@ -282,8 +271,7 @@ setTitle, | ||
show, | ||
update, | ||
drawTooltip, | ||
setConfig, | ||
getCache, | ||
render, | ||
destroy | ||
setScales, | ||
setVisibility | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
2855833
68
6595