Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

mapd3

Package Overview
Dependencies
Maintainers
1
Versions
27
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mapd3 - npm Package Compare versions

Comparing version 0.4.0 to 0.5.0

src/charts/brush-range-editor.js

2

package.json
{
"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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc