Negative and Zero Log Values in d3 - javascript

I am trying to plot multiple overlaying charts with X Axis Time and Y axis should toggle between Linear and Log Scale. I am not able to represent my charts appropriately for log scale even after using d3.scale.log().clamp(true).domain([]).range([]).nice()
Below is the code which I am using to map data points to Y axis
_initScaleYMap = function() {
const chart = this.$root().datum();
const data = chart.getDataSets() || [];
const dataSets = chart.getDataSets().filter((ds) => ds.getValueAxisId());
this.scaleYMap(new Map());
const dataSetsGroupedByValueAxisId = _.groupBy(dataSets, (dataSet) => dataSet.getValueAxisId());
Object.entries(dataSetsGroupedByValueAxisId).forEach(([valueAxisId, matchedDataSets]) => {
data.forEach((d) => {
const chartValueAxis = chart._getValueAxisById(valueAxisId);
if (!chartValueAxis) {
throw `Value axis '${valueAxisId} '`;
}
const min = chartValueAxis.getMin();
const max = chartValueAxis.getMax();
const domain = (Number.isFinite(min) && Number.isFinite(max) && !this.forceArea()) ? [min, max] : getYDomain(chart, matchedDataSets);
if (!d.getShowLog()) {
this.scaleYMap().set(valueAxisId,
d3.scale.linear()
.domain(domain)
.range([this.height() - this.margin().top - this.margin().bottom, 0])
);
} else {
this.scaleYMap().set(valueAxisId,
d3.scale.log().domain(domain).range([this.height() - this.margin().top - this.margin().bottom, 1]).nice()
);
}
});
});
return this;
};
I am attaching the before and after photos, before being the linear scale for y and after being the Log scale
On Linear Scale
On Log Scale

Related

d3.js transform.rescaleX() returning big and negative values

I'm making a semantic zoom on xAxis using the helper function transform.rescaleX() inside zoomed function
private manageZoom(svgs: AllSvg, allAxis: AllAxis, dimension: Dimension, sillons: Circulation[]): D3ZoomBehavior {
const zoom: D3ZoomBehavior = d3
.zoom()
.scaleExtent([1, 40])
.translateExtent([
[0, 0],
[dimension.width, dimension.height]
])
.on('zoom', zoomed.bind(null, allAxis, sillons, dimension, this.drawSillonService));
svgs.svgContainer.call(zoom);
return zoom;
function zoomed(
{ xAxis, xAxisBottom, yAxis }: AllAxis,
sillons: Circulation[],
dimension: Dimension,
drawSillonService: DrawSillonService,
{ transform }: any
) {
xAxis.axisContainer.call(xAxis.axis.scale(transform.rescaleX(xAxis.scale)) as any);
xAxisBottom.axisContainer.call(xAxisBottom.axis.scale(transform.rescaleX(xAxisBottom.scale)) as any);
svgs.sillons
.selectAll('path')
.nodes()
.forEach((path, j) => {
const pathSelect = d3.select(path);
const pathData = JSON.parse(pathSelect.attr('data'));
const line = d3
.line()
.x((d) => {
const transformScaled = transform.rescaleX(xAxis.scale);
const value = transformScaled(d[0]);
return value;
})
.y((d) => d[1]);
pathSelect.attr('d', line(pathData));
});
...
Here's the values of transform:
k:1.6817928305074288
x:-278.99232789420023
y:
-200.42112372679287
when d[0] = 0 the transformScaled(d[0]) return -133335,..
I don't know why I get these values. When I calculate transformation manually using x * transform.k + transform.x I get the right values (-79.5), but I would use rescaleX function.

LightningChartJs - add breaks to line chart for x axis

I would like to display breaks in the x axis of the line chart and display them with a fixed width, like at the image below:
Found this nice article that describes the functionality:
https://www.arction.com/news/using-scale-breaks-data-visualization/
If I understand correctly, there is the possibility in the SDK to display these with scaleAreas or even better with ClipAreas.
But I could not find this possibility in the LightningChart JS framework.
As starting point I was able to generate breaks in data with a NaN value and added a Textbox.
So my questions are:
1. How to add breaks and fit to predefined width in xaxis
2. How to rotate the Textbox 90 degrees?
Help would be greatly appreciated.
Thanks in advance. :o)
// Extract required parts from LightningChartJS.
const {
lightningChart,
AxisTickStrategies,
DataPatterns,
emptyFill,
ColorHEX,
emptyLine,
UIElementBuilders,
UIBackgrounds,
UIOrigins,
UIDraggingModes,
SolidLine,
SolidFill
} = lcjs
// Create a XY Chart.
const dateOrigin = new Date(2020, 0, 1)
const chart = lightningChart().ChartXY({
defaultAxisXTickStrategy: AxisTickStrategies.DateTime(
dateOrigin
)
})
.setTitle('Demo')
chart.setPadding({ right: '1' })
// Add a progressive line series.
// Using the DataPatterns object to select the horizontalProgressive pattern for the line series.
const lineSeries = chart.addLineSeries({ dataPattern: DataPatterns.horizontalProgressive })
.setName('Demo')
// Generate some points using for each month
const dataFrequency = 10;
// Setup view nicely.
chart.getDefaultAxisY()
.setScrollStrategy(undefined)
.setInterval(-20, 120)
.setTitle('Demo y')
// Data for the plotting
const data = [];
for (let i = 0; i < 1500; i++) {
let index = i;
if (i === 500) {
index = NaN;
}
if (i > 500) {
index = i + 1000;
}
data.push({
x: index,
y: Math.floor(Math.random() * 100)
});
}
chart.addUIElement(
UIElementBuilders.TextBox
.setBackground(UIBackgrounds.Rectangle),
chart.uiScale
)
.setText('Break')
.setPosition({ x: 45, y: 50 })
.setOrigin(UIOrigins.Center)
.setDraggingMode(UIDraggingModes.notDraggable)
.setFont((font) => font
.setSize(40)
)
.setBackground(style => style
.setFillStyle(new SolidFill({ color: ColorHEX('#f00') }))
)
.setTextFillStyle((style) => style
.setColor(ColorHEX('#0f0'))
)
// Adding points to the series
lineSeries.add(data.map((point) => ({ x: point.x * dataFrequency, y: point.y })))
<script src="https://unpkg.com/#arction/lcjs#1.3.1/dist/lcjs.iife.js"></script>
LightningChart JS doesn't have support for scale breaks yet. It's something that will most likely be developed at some point but there is no timeline for it yet.
TextBox rotation is not yet possible for UITextBox, it's coming in a future release but not the next release.

D3 semantic zooming with Reusable Pattern

I'm trying to implement semantic zooming while using Mike Bostock's Towards Reusable Charts pattern (where a chart is represented as a function). In my zoom handler, I'd like to use transform.rescaleX to update my scale and then simply call the function again.
It almost works but the rescaling seems to accumulate zoom transforms getting faster and faster. Here's my fiddle:
function chart() {
let aspectRatio = 10.33;
let margin = { top: 0, right: 0, bottom: 5, left: 0 };
let current = new Date();
let scaleBand = d3.scaleBand().padding(.2);
let scaleTime = d3.scaleTime().domain([d3.timeDay(current), d3.timeDay.ceil(current)]);
let axis = d3.axisBottom(scaleTime);
let daysThisMonth = d3.timeDay.count(d3.timeMonth(current), d3.timeMonth.ceil(current));
let clipTypes = [ClipType.Scheduled, ClipType.Alarm, ClipType.Motion];
let zoom = d3.zoom().scaleExtent([1 / daysThisMonth, 1440]);
let result = function(selection) {
selection.each(function(data) {
let selection = d3.select(this);
let outerWidth = this.getBoundingClientRect().width;
let outerHeight = outerWidth / aspectRatio;
let width = outerWidth - margin.left - margin.right;
let height = outerHeight - margin.top - margin.bottom;
scaleBand.domain(d3.range(data.length)).range([0, height * .8]);
scaleTime.range([0, width]);
zoom.on('zoom', _ => {
scaleTime = d3.event.transform.rescaleX(scaleTime);
selection.call(result);
});
let svg = selection.selectAll('svg').data([data]);
let svgEnter = svg.enter().append('svg').attr('viewBox', '0 0 ' + outerWidth + ' ' + outerHeight);//.attr('preserveAspectRatio', 'xMidYMin slice');
svg = svg.merge(svgEnter);
let defsEnter = svgEnter.append('defs');
let defs = svg.select('defs');
let gMainEnter = svgEnter.append('g').attr('id', 'main');
let gMain = svg.select('g#main').attr('transform', 'translate(' + margin.left + ' ' + margin.top + ')');
let gAxisEnter = gMainEnter.append('g').attr('id', 'axis');
let gAxis = gMain.select('g#axis').call(axis.scale(scaleTime));
let gCameraContainerEnter = gMainEnter.append('g').attr('id', 'camera-container');
let gCameraContainer = gMain.select('g#camera-container').attr('transform', 'translate(' + 0 + ' ' + height * .2 + ')').call(zoom);
let gCameraRowsEnter = gCameraContainerEnter.append('g').attr('id', 'camera-rows');
let gCameraRows = gCameraContainer.select('g#camera-rows');
let gCameras = gCameraRows.selectAll('g.camera').data(d => {
return d;
});
let gCamerasEnter = gCameras.enter().append('g').attr('class', 'camera');
gCameras = gCameras.merge(gCamerasEnter);
gCameras.exit().remove();
let rectClips = gCameras.selectAll('rect.clip').data(d => {
return d.clips.filter(clip => {
return clipTypes.indexOf(clip.type) !== -1;
});
});
let rectClipsEnter = rectClips.enter().append('rect').attr('class', 'clip').attr('height', _ => {
return scaleBand.bandwidth();
}).attr('y', (d, i, g) => {
return scaleBand(Array.prototype.indexOf.call(g[i].parentNode.parentNode.childNodes, g[i].parentNode)); //TODO: sloppy
}).style('fill', d => {
switch(d.type) {
case ClipType.Scheduled:
return '#0F0';
case ClipType.Alarm:
return '#FF0';
case ClipType.Motion:
return '#F00';
};
});
rectClips = rectClips.merge(rectClipsEnter).attr('width', d => {
return scaleTime(d.endTime) - scaleTime(d.startTime);
}).attr('x', d => {
return scaleTime(d.startTime);
});
rectClips.exit().remove();
let rectBehaviorEnter = gCameraContainerEnter.append('rect').attr('id', 'behavior').style('fill', '#000').style('opacity', 0);
let rectBehavior = gCameraContainer.select('rect#behavior').attr('width', width).attr('height', height * .8);//.call(zoom);
});
};
return result;
}
// data model
let ClipType = {
Scheduled: 0,
Alarm: 1,
Motion: 2
};
let data = [{
id: 1,
src: "assets/1.jpg",
name: "Camera 1",
server: 1
}, {
id: 2,
src: "assets/2.jpg",
name: "Camera 2",
server: 1
}, {
id: 3,
src: "assets/1.jpg",
name: "Camera 3",
server: 2
}, {
id: 4,
src: "assets/1.jpg",
name: "Camera 4",
server: 2
}].map((_ => {
let current = new Date();
let randomClips = d3.randomUniform(24);
let randomTimeSkew = d3.randomUniform(-30, 30);
let randomType = d3.randomUniform(3);
return camera => {
camera.clips = d3.timeHour.every(Math.ceil(24 / randomClips())).range(d3.timeDay.offset(current, -30), d3.timeDay(d3.timeDay.offset(current, 1))).map((d, indexEndTime, g) => {
return {
startTime: indexEndTime === 0 ? d : d3.timeMinute.offset(d, randomTimeSkew()),
endTime: indexEndTime === g.length - 1 ? d3.timeDay(d3.timeDay.offset(current, 1)) : null,
type: Math.floor(randomType())
};
}).map((d, indexStartTime, g) => {
if(d.endTime === null)
d.endTime = g[indexStartTime + 1].startTime;
return d;
});
return camera;
};
})());
let myChart = chart();
let selection = d3.select('div#container');
selection.datum(data).call(myChart);
<div id="container"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
Edit: The zoom handler below works fine, but I'd like a more general solution:
let newScaleTime = d3.event.transform.rescaleX(scaleTime);
d3.select('g#axis').call(axis.scale(newScaleTime));
d3.selectAll('rect.clip').attr('width', d => {
return newScaleTime(d.endTime) - newScaleTime(d.startTime);
}).attr('x', d => {
return newScaleTime(d.startTime);
});
The short answer is you need to implement a reference scale to indicate what the scale's base state is when unmanipulated by the zoom. Otherwise you will run into the problem you describe: "It almost works but the rescaling seems to accumulate zoom transforms getting faster and faster. "
To see why a reference scale is needed, zoom in on the graph and out (once each) without moving the mouse. When you zoom in, the axis changes. When you zoom out the axis does not. Note the scale factor on the intial zoom in and the first time you zoom out: 1.6471820345351462 on the zoom in, 1 on the zoom out. The number represents how much the to magnify/minify whatever it is we are zooming in on. On the initial zoom in we magnify by a factor of ~1.65. On the preceding zoom out we minify by a factor of 1, ie: not at all. If on the other hand you zoom out first, you minify by a factor of about 0.6 and then if you were to zoom in you magnify by a factor of 1. I've built a stripped down of your example to show this:
function chart() {
let zoom = d3.zoom().scaleExtent([0.25,20]);
let scale = d3.scaleLinear().domain([0,1000]).range([0,550]);
let axis = d3.axisBottom;
let result = function(selection) {
selection.each(function() {
let selection = d3.select(this);
selection.call(axis(scale));
selection.call(zoom);
zoom.on('zoom', function() {
scale = d3.event.transform.rescaleX(scale);
console.log(d3.event.transform.k);
selection.call(result);
});
})
}
return result;
}
d3.select("svg").call(chart());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="550" height="200"></svg>
The scale should be relative to the initial zoom factor, usually 1. In otherwords, the zoom is cumulative, it records magnification/minification as a factor of the initial scale, not the last step (otherwise transform k values would only be one of three values: one value for zooming out, another for zooming in and one for remaining the same and all relative to the current scale). This is why rescaling the initial scale doesn't work - you lose the reference point to the initial scale that the zoom is referencing.
From the docs, if you redefine a scale with d3.event.transform.rescaleX, we get a scale that reflects the zoom's (cumulative) transformation:
[the rescaleX] method does not modify the input scale x; x thus
represents the untransformed scale, while the returned scale
represents its transformed view. (docs)
Building on this, if we zoom in twice in a row, the first time we zoom in we see the transform.k value is ~1.6x on the first time, the second time it is ~2.7x. But, since we rescale the scale, we apply a zoom of 2.7x on a scale that has already been zoomed in 1.6x, giving us a scale factor of ~4.5x rather than 2.7x. To make matters worse, if we zoom in twice and then out once, the zoom (out) event gives us a scale value that is still greater than 1 (~1.6 on first zoom in, ~2.7 on second, ~1.6 on zoom out), hence we are still zooming in despite scrolling out:
function chart() {
let zoom = d3.zoom().scaleExtent([0.25,20]);
let scale = d3.scaleLinear().domain([0,1000]).range([0,550]);
let axis = d3.axisBottom;
let result = function(selection) {
selection.each(function() {
let selection = d3.select(this);
selection.call(axis(scale));
selection.call(zoom);
zoom.on('zoom', function() {
scale = d3.event.transform.rescaleX(scale);
var magnification = 1000/(scale.domain()[1] - scale.domain()[0]);
console.log("Actual magnification: "+magnification+"x");
console.log("Intended magnification: "+d3.event.transform.k+"x")
console.log("---");
selection.call(result);
});
})
}
return result;
}
d3.select("svg").call(chart());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="550" height="200"></svg>
I haven't discussed the x offset portion of the zoom, but you can imagine that a similar problem occurs - the zoom is cumulative but you lose the initial reference point that those cumulative changes are in reference to.
The idiomatic solution is to use a reference scale and the zoom to create a working scale used for plotting rectangles/axes/etc. The working scale is initially the same as the reference scale (generally) and is set as so: workingScale = d3.event.transform.rescaleX(referenceScale) on each zoom.
function chart() {
let zoom = d3.zoom().scaleExtent([0.25,20]);
let workingScale = d3.scaleLinear().domain([0,1000]).range([0,550]);
let referenceScale = d3.scaleLinear().domain([0,1000]).range([0,550]);
let axis = d3.axisBottom;
let result = function(selection) {
selection.each(function() {
let selection = d3.select(this);
selection.call(axis(workingScale));
selection.call(zoom);
zoom.on('zoom', function() {
workingScale = d3.event.transform.rescaleX(referenceScale);
var magnification = 1000/(workingScale.domain()[1] - workingScale.domain()[0]);
console.log("Actual magnification: "+magnification+"x");
console.log("Intended magnification: "+d3.event.transform.k+"x")
console.log("---");
selection.call(result);
});
})
}
return result;
}
d3.select("svg").call(chart());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="550" height="200"></svg>

Highcharts manually added svg elements not following stock graph on pan

I have a click event in my highstock / highchart graph, I have successfully added custom drawing tools such as adding lines and text. Here is the code for that
$('#stockchart-canvas-container').on('click','svg',function(e){
var svg = $('#stockchart-canvas-container svg')[0];
var point= svg.createSVGPoint(), svgP
point.x = e.clientX
point.y = e.clientY
svgP = point.matrixTransform(svg.getScreenCTM().inverse());
if(user.selected_tool=='line'){
if(user.previous_x == undefined && user.previous_y == undefined) {
user.current_x = svgP.x
user.current_y = svgP.y
user.previous_x = 0
user.previous_y = 0
$('#stockchart-canvas-container').on('mousemove','svg',function(ev){
var svg2 = $('#stockchart-canvas-container svg')[0];
var point2= svg.createSVGPoint(), svgP2
point2.x = ev.clientX
point2.y = ev.clientY
svgP2 = point2.matrixTransform(svg2.getScreenCTM().inverse());
$('#temp-line').remove()
stockchart.renderer.path(['M',
user.current_x,
user.current_y,
'L',
svgP2.x,
svgP2.y,
'Z',
]).attr({'stroke-width':2,stroke:'#ccc',id:'temp-line'}).add(stockchart.seriesGroup)
})
} else {
$('#stockchart-canvas-container').off('mousemove')
stockchart.renderer.path(['M',
user.current_x,
user.current_y,
'L',
svgP.x,
svgP.y,
'Z'
]).attr({'stroke-width':2,stroke:'#ccc'}).add(stockchart.seriesGroup)
user.current_x=0
user.current_y=0
user.previous_x=undefined
user.previous_y=undefined
}
} else if (user.selected_tool=='text') {
$('#insert-text-modal').modal('show')
$('#accept-insert-text').on('click',function(){
if($('#text-input').val()){
stockchart.renderer.text($('#text-input').val(),svgP.x,svgP.y).add(stockchart.seriesGroup)
}
$(this).off('click')
$('#insert-text-modal').modal('hide')
})
}
})
My problem is that I want the line and the text to follow the stock graph as I pan or zoom the graph. Any ideas how I can do this?
You have to preserve coordinate values at the moment the text/line is drawn - the coordinates in terms of axes. On each chart redraw, you need to reposition the line/text - so you have to calculate new pixel position (which can be calculated via axis.toPixels) and set the new values to the line/text. For a text you need to calculate one point, for a path element you need to recalculate each segment.
See the code below:
Function for calculating pixels from values and values from pixels - it includes some basic logic for hiding a text if it overflows a chart's plot area - but it should be adjusted depending on your needs.
function translate (x, y, chart, toPixels) {
const xAxis = chart.xAxis[0]
const yAxis = chart.yAxis[0]
let tx, ty, hide
if (toPixels) {
tx = xAxis.toPixels(x)
ty = yAxis.toPixels(y)
if (tx < xAxis.left || tx > xAxis.left + xAxis.width) {
hide = true
} else if (!hide && (ty < yAxis.top || ty > yAxis.top + yAxis.height)) {
hide = true
}
if (hide) {
tx = -9e7
ty = -9e7
}
} else {
tx = xAxis.toValue(x)
ty = yAxis.toValue(y)
}
return { x: tx, y: ty }
}
On chart click - it adds the text and keep in the array, on chart redraw r - it repositions items.
chart: {
events: {
load: function () {
this.drawnItems = []
},
click: function (e) {
const { x, y } = e
const text = this.renderer.text('custom text', x, y).add()
text.point = translate(x, y, this)
this.drawnItems.push(text)
},
redraw: function () {
this.drawnItems.forEach(item => {
const { x, y } = item.point
item.attr(translate(x, y, this, true))
})
}
}
},
Live example: http://jsfiddle.net/nsf67ro6/

D3 path tween changes side

I'm trying to do a path tween like this one: https://bl.ocks.org/mbostock/3916621
Problem: The path is changing the side. The gray path is changing from left to right and the white path is changing from bottom to top. This isn't the expected transition!
Edit: My expected transition should be a simple grow transition. So the small one should grow to the bigger one.
Example: https://jsfiddle.net/wdv3rufs/
const PATHS = {
FULL: {
GRAY: 'M1035,429l-4.6-73.7L1092,223l-66,1l-66.3-36.4l-102.5,67.6L623.8,0L467.4,302.1l-218.7-82.9L77.6,317.4L0,214.5V429H1035z',
WHITE: 'M0,429V292l249.4-72.9l135.4,56.6L623.8,0L824,232.5l135.7-44.9l26.7,190.5l29.3,16.8l19.3,34H0z'
},
SMALL: {
GRAY: 'M0,429h834l-134.2-34.6l-37,23.2l-130.6-35.2l-112.7,33.5l-144.2-67l-96.7,65.2L43.5,377.5L0,391.4V429z',
WHITE: 'M0,429h834l-134.2-34.6l-83.5,29l-84.1-41l-126.9,31.2l-130-64.7l-144.8,58.6l-87-30L0,386.1V429z'
}
};
const pathTween = (d1, precision) => {
return function() {
const path0 = this;
const path1 = path0.cloneNode();
const n0 = path0.getTotalLength();
const n1 = (path1.setAttribute('d', d1), path1).getTotalLength();
const distances = [0];
const dt = precision / Math.max(n0, n1);
let i = 0;
while ((i += dt) < 1) distances.push(i);
distances.push(1);
const points = distances.map(t => {
const p0 = path0.getPointAtLength(t * n0);
const p1 = path1.getPointAtLength(t * n1);
return d3.interpolate([p0.x, p0.y], [p1.x, p1.y]);
});
return t => {
return t < 1 ? 'M' + points.map(p => p(t)).join('L') : d1;
};
};
};
const pathTransition = (path, d1) => {
path.transition()
.duration(10000)
.attrTween('d', pathTween(d1, 4));
}
var svg = d3.select('svg');
svg.append('path')
.attr('class', 'white-mountain')
.attr('d', PATHS.SMALL.WHITE)
.call(pathTransition, PATHS.FULL.WHITE);
svg.append('path')
.attr('class', 'gray-mountain')
.attr('d', PATHS.SMALL.GRAY)
.call(pathTransition, PATHS.FULL.GRAY);
How can I get this working?

Categories